vue3--手写手机屏组件

powershell 复制代码
<!--
 * 手机预览
 * @Author: Hanyang
 * @Date: 2022-12-09 09:13:00
 * @LastEditors: Hanyang
 * @LastEditTime: 2023-01-12 15:37:00
-->
<template>
  <div
    class="public-preview-mobile"
    ref="previewMobileRef"
    :class="showMobile ? 'animation-show-mobile' : 'animation-hide-mobile'"
    :style="{ ...mobileStyle, height: props.height }">
    <div
      v-show="showMask"
      class="priveiew-mask"
      ref="previewMaskRef"
      @mousedown="showMask = true"
      @mouseup="maskDisapear"
      @mouseleave="maskDisapear"
      @mousemove="maskMoveFn ? maskMoveFn($event) : () => {}"></div>
    <div
      class="switch"
      :title="showMobile ? '点击收起手机' : '点击显示手机'"
      @click="switchMobile"></div>
    <div class="liu-hair-wrap">
      <div
        class="liu-hair"
        @mousedown="(showMask = true), (maskMoveFn = mobileMouseMove)"></div>
    </div>
    <div
      class="mobile-fixed-container"
      :class="{
        'fixed-container': props.isFixedContainer,
      }"
      :style="{ width: props.width }">
      <div
        class="mobile-wrap"
        @scroll="scrollMobile"
        :class="{
          'hidden-scroll': !canScroll,
        }">
        <div
          class="liu-hair-head"
          :style="getStatusBarStyle(isImmersive, statusBarBg)"></div>
        <slot></slot>
      </div>
    </div>
  </div>
</template>

<script lang="ts" setup>
import { ref, watch } from "vue";
type MobileStatusInfo = "on" | "off";

const props = defineProps({
  isOn: {
    //手机是否显示
    type: Boolean,
    default: true,
  },
  isImmersive: {
    //是否是沉浸式状态栏
    type: Boolean,
    default: false,
  },
  statusBarBg: {
    type: String,
    default: "#fff",
  },
  canScroll: {
    type: Boolean,
    default: true,
  },
  isFixedContainer: {
    //是否将手机内屏作为position:fixed;的屏幕视口
    type: Boolean,
    default: true,
  },
  width: {
    //手机宽度
    type: String,
    default: "300px",
  },
  height: {
    //手机高度
    type: String,
    default: "600px",
  },
});
const emit = defineEmits(["scroll"]);

const previewMobileRef = ref();
const showMask = ref(false);
const maskMoveFn = ref<any>(null);
const showMobile = ref(true);
const mobileStyle = ref<any>(null);

const getStatusBarStyle = (isImmersive: boolean, statusBarBg: string) => {
  const s: any = {};
  if (isImmersive == false) {
    s.background = statusBarBg;
  }
  return s;
};
const maskDisapear = () => {
  showMask.value = false;
  maskMoveFn.value = null;
};
const mobileMouseMove = (e: MouseEvent) => {
  const dom = previewMobileRef.value;
  let x_dis = parseFloat(window.getComputedStyle(dom).right) - e.movementX;
  let y_dis = parseFloat(window.getComputedStyle(dom).top) + e.movementY;
  (previewMobileRef.value as any).style.right = x_dis + "px";
  (previewMobileRef.value as any).style.top = y_dis + "px";

  showMobile.value = true;
};
const switchMobile = () => {
  showMobile.value = !showMobile.value;
};
//组件触发事件:删除事件
const scrollMobile = (e: any) => {
  emit("scroll", e);
};
/**
 * 外部调用方法:用于组件内鼠标按住移动所要执行的动作
 * maskMoveFn:执行的动作
 */
const maskApear = (fn?: (event: MouseEvent) => void) => {
  showMask.value = true;
  if (fn) maskMoveFn.value = fn;
};
/**
 * 外部调用方法:获取手机状态 on-开机 off-关机
 */
const getMobileStatus = (): MobileStatusInfo => {
  return showMobile.value ? "on" : "off";
};
/**
 * 外部调用方法:切换手机状态
 */
const switchMobileStatus = (status: MobileStatusInfo, style?: any) => {
  return new Promise((resolve, reject) => {
    try {
      if (
        (status === "on" && showMobile.value) ||
        (status === "off" && !showMobile.value)
      ) {
        resolve(1);
      } else {
        const dom = previewMobileRef.value as any;
        dom?.addEventListener(
          "animationend",
          () => {
            resolve(1);
          },
          {
            once: true,
          }
        );

        switch (status) {
          case "on":
            showMobile.value = true;
            break;
          case "off":
            showMobile.value = false;
            break;
          default:
            showMobile.value = true;
        }
        if (style) {
          mobileStyle.value = style;
        }
      }
    } catch (e) {
      reject(e);
    }
  });
};
watch(
  () => props.isOn,
  (val: Boolean) => {
    showMobile.value = !!val;
  },
  { immediate: true }
);
</script>
<style lang="scss">
@use "sass:math";
.public-preview-mobile {
  cursor: pointer;
  position: fixed;
  display: block;
  top: 180px;
  right: 10px;
  z-index: 10;
  $LH_H: 20px; //刘海高度
  $M_B: 4px; //手机边框
  border: $M_B solid #000;
  border-radius: 12px;
  height: 600px;
  @keyframes hide-mobile {
    from {
      // top: 0px;
    }
    to {
      top: 97vh;
    }
  }
  @keyframes show-mobile {
    from {
      // top: 0px;
    }
    to {
      top: 97vh;
    }
  }
  &.animation-hide-mobile {
    animation-name: hide-mobile;
    animation-duration: 0.2s;
    animation-fill-mode: forwards;
    filter: brightness(0.2);
  }
  &.animation-show-mobile {
    animation-name: show-mobile;
    animation-duration: 0.2s;
    animation-fill-mode: forwards;
    animation-direction: reverse;
  }
  .priveiew-mask {
    position: fixed;
    cursor: pointer;
    z-index: 4023;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
  }
  .switch {
    width: 60px;
    height: 25px;
    background: pink;
    position: absolute;
    right: 20px;
    top: -7px;
    border-radius: 6px;
    background: #000;
    transition: all 0.2s;
    &::before {
      content: "";
      position: absolute;
      top: -16px;
      left: -8px;
      right: -8px;
      bottom: 0px;
      z-index: 12;
    }
    &:hover {
      top: -9px;
    }
    &:active {
      top: -6px;
    }
  }
  .liu-hair-wrap {
    position: absolute;
    height: 20px;
    width: 36%;
    overflow: hidden;
    z-index: 4013;
    left: 50%;
    transform: translateX(-50%);
    margin-top: 0px;
    .liu-hair {
      cursor: move;
      display: inline-block;
      height: $LH_H;
      width: 100%;
      background: #000;
      border-radius: math.div($LH_H, 2);
      transform: translateY(-50%);
    }
  }
  .mobile-fixed-container {
    width: 300px;
    height: inherit;
    display: inline-block;
    overflow: hidden;
    user-select: none;
    background: transparent;
    box-sizing: border-box;
    &.fixed-container {
      transform: scale(1);
    }
    .mobile-wrap {
      position: relative;
      width: inherit;
      height: inherit;
      display: inline-block;
      border: $M_B solid #000;
      border-radius: 12px;
      overflow: auto;
      background: #fff;
      user-select: none;
      margin: -$M_B;
      &::-webkit-scrollbar {
        width: 2px;
        /*滚动条宽度*/
        height: 2px;
        /*滚动条高度*/
        cursor: pointer;
      }
      scrollbar-width: none;
      &.hidden-scroll {
        overflow-y: hidden !important;
      }
      .liu-hair-head {
        position: sticky;
        top: 0;
        left: 0px;
        right: 0px;
        z-index: 11;
        height: $LH_H;
      }
    }
  }
}
</style>
相关推荐
洋流5 分钟前
什么?还没弄懂关键字this?一篇文章带你速通
前端·javascript
晴殇i5 分钟前
for...in 循环的坑,别再用它遍历 JavaScript 数组了!
前端·javascript
海底火旺6 分钟前
寻找缺失的最小正整数:从暴力到最优的算法演进
javascript·算法·面试
洋流12 分钟前
JavaScript事件流机制详解:捕获、冒泡与阻止传播
前端·javascript
tjh000119 分钟前
vue3+TS 手动实现表格滚动
前端·javascript·vue.js
XU磊26032 分钟前
深入理解表单---提交用户与网页交互的重要方式:GET 与 POST 的本质区别与应用实践
服务器·前端·javascript
kadog1 小时前
《Python3网络爬虫开发实战(第二版)》配套案例 spa6
开发语言·javascript·爬虫·python
珎珎啊1 小时前
uniapp+vue3移动端实现输入验证码
前端·javascript·uni-app
86Eric1 小时前
Vue 中 使用 Mixins 解决 多页面共用相同组件的相关问题
前端·javascript·vue.js·mixins·公用组件
qq_25249639961 小时前
react 子组件暴露,父组件接收
前端·javascript·react.js