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>
相关推荐
张元清38 分钟前
Neant:0心智负担的React状态管理库
前端·javascript·面试
李明卫杭州42 分钟前
使用fastmap快速搭建基于js实现的MCP服务
前端·javascript
何其幸44 分钟前
js类型转换的知识点整理
前端·javascript·面试
四月友人A1 小时前
不要再用addEventListener了!这个API救了我的命
javascript
大明881 小时前
数组的空项(empty slots)处理行为
前端·javascript
用户1512905452201 小时前
HTML5 Canvas
前端·javascript
尝尝你的优乐美2 小时前
前端查缺补漏系列(一)JS对象及其扩展
前端·javascript·面试
江城开朗的豌豆2 小时前
Vue做SEO太难?6年老司机带你轻松搞定!
前端·javascript·vue.js
江城开朗的豌豆2 小时前
Vue性能优化实战:让你的应用快如闪电⚡
前端·javascript·vue.js
前端Hardy2 小时前
HTML&CSS:有趣的轮播图
前端·javascript·css