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>
相关推荐
来自星星的坤1 小时前
【Vue 3 + Vue Router 4】如何正确重置路由实例(resetRouter)——避免“VueRouter is not defined”错误
前端·javascript·vue.js
香蕉可乐荷包蛋6 小时前
浅入ES5、ES6(ES2015)、ES2023(ES14)版本对比,及使用建议---ES6就够用(个人觉得)
前端·javascript·es6
未来之窗软件服务6 小时前
资源管理器必要性———仙盟创梦IDE
前端·javascript·ide·仙盟创梦ide
西哥写代码7 小时前
基于cornerstone3D的dicom影像浏览器 第十八章 自定义序列自动播放条
前端·javascript·vue
清风细雨_林木木8 小时前
Vue 中生成源码映射文件,配置 map
前端·javascript·vue.js
雪芽蓝域zzs8 小时前
JavaScript splice() 方法
开发语言·javascript·ecmascript
霸王蟹9 小时前
React中巧妙使用异步组件Suspense优化页面性能。
前端·笔记·学习·react.js·前端框架
森叶9 小时前
Electron 主进程中使用Worker来创建不同间隔的定时器实现过程
前端·javascript·electron
霸王蟹9 小时前
React 19 中的useRef得到了进一步加强。
前端·javascript·笔记·学习·react.js·ts
霸王蟹9 小时前
React 19版本refs也支持清理函数了。
前端·javascript·笔记·react.js·前端框架·ts