Skip to content

核心技术与功能模块 ​

通用功能设计 ​

各种浏览器低版本过滤与提示 ​

<!DOCTYPE html>
<html lang="">

<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width,initial-scale=1.0">
  <link rel="icon" href="<%= BASE_URL %>favicon.ico">
  <title>杰恩设计中台系统</title>
  <!-- 使用CDN的CSS文件 -->
  <% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.css) { %>
  <link href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" rel="preload" as="style">
  <link href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" rel="stylesheet">
  <% } %>
  <!-- 使用CDN的JS文件 -->
  <% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js) { %>
  <link href="<%= htmlWebpackPlugin.options.cdn.js[i] %>" rel="preload" as="script">
  <script src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
  <% } %>
</head>

<body>
  <noscript>
    <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
      Please enable it to continue.</strong>
  </noscript>
  <div id="mainapp"></div>
  <!-- built files will be auto injected -->





  <!---------------------------------------------------------------------------------------->
  <!-----------------------------------屏蔽各种低版本浏览器----------------------------------->
  <!---------------------------------------------------------------------------------------->
  <table id="browerContainer" style="z-index:9999;display:none;position:fixed;top:0;right:0;bottom:0;left:0;width:100%;height:100%;background-color: #000;user-select: none;">
    <tr>
      <td style="text-align:center;">
        <p style="margin-bottom:50px;line-height:50px;font-size:24px;color:#E6A23C;">当前浏览器版本太低,已经严重影响网站优质体验,请前往下载最新浏览器体验</p>

        <table style="margin:auto;width:500px;text-align: center;">
          <tr>
            <td>
              <a href="https://www.google.cn/intl/zh-CN/chrome/" target="_blank">
                <p style="line-height: 40px;color:#409EFF;">Chrome 浏览器</p>
                <img style="width:50px;border:none;" src="http://shejishi-web-static.oss-cn-shenzhen.aliyuncs.com/static/common/images/brower/chrome.png" alt="">
              </a>
            </td>
            <td>
              <a href="https://browser.qq.com/" target="_blank">
                <p style="line-height: 40px;color:#409EFF;"">QQ 浏览器</p>
                <img style="width:50px;border:none;" src="http://shejishi-web-static.oss-cn-shenzhen.aliyuncs.com/static/common/images/brower/qq.png" alt="">
              </a>
            </td>
            <td>
              <a href="http://www.firefox.com.cn/" target="_blank">
                <p style="line-height: 40px;color:#409EFF;"">Firefox 浏览器</p>
                <img style="width:50px;border:none;" src="http://shejishi-web-static.oss-cn-shenzhen.aliyuncs.com/static/common/images/brower/firefox.png" alt="">
              </a>
            </td>
            <td>
              <a href="" target="_blank">
                <p style="line-height: 40px;color:#409EFF;"">360 浏览器</p>
                <img style="width:50px;border:none;" src="http://shejishi-web-static.oss-cn-shenzhen.aliyuncs.com/static/common/images/brower/360.png" alt="">
              </a>
            </td>
          </tr>
        </table>
      </td>
    </tr>
  </table>

  <table id="mobileContainer" style="z-index:9999;display:none;position:fixed;top:0;right:0;bottom:0;left:0;width:100%;height:100%;background-color: #000;user-select: none;">
    <tr>
      <td style="line-height:30px;text-align:center;color:#E6A23C;">
        <img src="http://shejishi-web-static.oss-cn-shenzhen.aliyuncs.com/static/common/images/brower/pc.png" alt="">
        <p>暂不支持移动设备浏览</p>
        <p>为了增强用户体验, 请前往PC电脑网站</p>
      </td>
    </tr>
  </table>
</body>
<script type="text/javascript">
  var ua = navigator.userAgent.toLowerCase();
  var isIE = ua.indexOf("compatible") > -1 && ua.indexOf("MSIE") > -1;   // 判断是否IE<11浏览器   

  if ( /mobile/i.test(ua) && window.outerWidth < 1366 ) {
    document.getElementById('mobileContainer').style.display = 'table';
  } else if (!window.fetch || isIE) {
    document.getElementById('browerContainer').style.display = 'table';
  }
</script>

</html>

网页全屏 Fullscreen ​

<body> 
  <div>
    <button type='button' id="launchFullscreen">开启全屏模式</button>
    <button type='button' id="exitFullscreen">退出全屏模式</button>
    <button type='button' id="checkFullscreen">检测全屏模式</button>
  </div>
</body>

<script type="module">
  const launchSelector = document.getElementById('launchFullscreen');
  const exitSelector = document.getElementById('exitFullscreen');
  const checkSelector = document.getElementById('checkFullscreen');

  // 进入全屏
  launchSelector.addEventListener('click', function() {
    launchFullscreen(document.querySelector('div'));
  }, false);

  // el: 申明全屏对象
  function launchFullscreen(el) {
    if (el.requestFullscreen) {
      el.requestFullscreen();
    } else if (el.mozRequestFullScreen) {
      el.mozRequestFullScreen();
    } else if (el.msRequestFullscreen) {
      el.msRequestFullscreen();
    } else if (el.webkitRequestFullscreen) {
      el.webkitRequestFullScreen();
    } else {
      throw new Error('浏览器暂时不支持该属性');
    }
  }

  // 监听全屏事件
  document.addEventListener("fullscreenchange", function (e) {
    if (document.fullscreenElement) {
      console.log('进入全屏');
    } else {
      console.log('退出全屏');
    }
  });

  // 手动退出全屏
  exitSelector.addEventListener('click', () => {
    exitFullscreen();
  }, false);
  // 退出全屏
  function exitFullscreen() {
    if (document.exitFullscreen) {
      document.exitFullscreen()
    } else if (document.msExitFullscreen) {
      document.msExitFullscreen()
    } else if (document.mozCancelFullScreen) {
      document.mozCancelFullScreen()
    } else if (document.webkitExitFullscreen) {
      document.webkitExitFullscreen()
    }
  }

  // 检测是否处于全屏模式
  checkSelector.addEventListener('click', () => {
    console.log(document.fullscreenElement); // 非全屏模式:null | 全屏模式: Element-即全屏模式的对象元素
  }, false);







  _main.default.prototype.displayDensity = function() {
    return window.devicePixelRatio;
  };

  function launchFullscreen(element) {
    var enabled =
      document.fullscreenEnabled ||
      document.webkitFullscreenEnabled ||
      document.mozFullScreenEnabled ||
      document.msFullscreenEnabled;
    if (!enabled) {
      throw new Error('Fullscreen not enabled in this browser.');
    }
    if (element.requestFullscreen) {
      element.requestFullscreen();
    } else if (element.mozRequestFullScreen) {
      element.mozRequestFullScreen();
    } else if (element.webkitRequestFullscreen) {
      element.webkitRequestFullscreen();
    } else if (element.msRequestFullscreen) {
      element.msRequestFullscreen();
    }
  }

  function exitFullscreen() {
    if (document.exitFullscreen) {
      document.exitFullscreen();
    } else if (document.mozCancelFullScreen) {
      document.mozCancelFullScreen();
    } else if (document.webkitExitFullscreen) {
      document.webkitExitFullscreen();
    } else if (document.msExitFullscreen) {
      document.msExitFullscreen();
    }
  }
</script>

新手引导 ​

person_data.ts ​

/**
* @desc 海报编辑器首开指引 |个人版本 | 数据配置
* @author wangjun
* @since 2021-03-27 12:27:10
*/
import {
  ASIDE_POSTER_PLANE,
  GUIDER_FONT_TEXT_HIDE,
  GUIDER_FONT_TEXT_SHOW,
  SCENE_TYPE,
} from 'components/guider/constants';
import { fireGuiderEvent, skipHandler } from 'components/guider/guider-tools';
import { SCENE } from 'components/guider/types';

export const POSTER_P_EDITOR: SCENE = {
  load: false,
  name: '海报编辑器首开-个人版',
  type: SCENE_TYPE.POSTER_P_EDITOR,
  version: '1.0.0',
  maskVisible: true,
  activation: 'GUIDER_ENABLED_POSTER_E_Editor',
  beforeEnabled() {},
  afterEnabled() {},
  skip: {
    visible: true,
    handler() {
      skipHandler(POSTER_P_EDITOR);
    },
  },
  currentStep: 1,
  finish: '开始制作',
  list: [
    {
      el: [
        '.e-navbar-container',
        {
          el: '#adsorbent',
          image: 'https://static.h5no1.com/rp/plugin/assets/3bf2d114-cc41-43de-be4a-40a6fb0d1afe',
        },
      ],
      label: ASIDE_POSTER_PLANE,
      beforeStep() {
        const adsorbent = document.querySelector('#adsorbent');
        if (!adsorbent) {
          POSTER_P_EDITOR.label = 'adsorbent';
          fireGuiderEvent(GUIDER_EVENT_POSTER_P.Poster.show);
        }
      },
      afterStep() {
        const adsorbent = document.querySelector('#adsorbent');
        if (adsorbent && POSTER_P_EDITOR.label === 'adsorbent') {
          POSTER_P_EDITOR.label = null;
          fireGuiderEvent(GUIDER_EVENT_POSTER_P.Poster.hide);
        }
      },
      style: {
        direction: 'top',
        top: 20,
        left: 10,
        width: 460,
        height: 211,
        triangleLeft: 70,
      },
      title: '模板、元素库',
      course: {
        link: 'https://docs.tuzhanai.com/pages/viewpage.action?pageId=27492418',
      },
      content: {
        text: '您可以在这里选择模板、文本、图片和二维码等,在左侧面板中点击元素即可添加至画布区',
      },
    },
    {
      el: '.e-stage-main',
      style: {
        direction: 'right',
        top: 70,
        width: 460,
        height: 211,
        triangleTop: 55,
      },
      title: '画布编辑区',
      course: {
        link: 'https://docs.tuzhanai.com/pages/viewpage.action?pageId=27492418',
      },
      content: {
        text: '您可以在这里自由地添加文本、图片、二维码等素材,生成自己独特的作品',
      },
    },
    {
      el: '.e-toolbox',
      style: {
        direction: 'right',
        top: 215,
        width: 460,
        height: 211,
      },
      title: '辅助功能及工具区',
      course: {
        link: 'https://docs.tuzhanai.com/pages/viewpage.action?pageId=27492418',
      },
      content: {
        text: '您可以在这里导入PSD,设置图层,调整画布大小,使用标尺、参考线、网格等辅助工具',
      },
    },
    {
      el: '.e-layout-right',
      beforeStep() {
        fireGuiderEvent(GUIDER_FONT_TEXT_SHOW);
      },
      afterStep() {
        fireGuiderEvent(GUIDER_FONT_TEXT_HIDE);
      },
      style: {
        direction: 'right',
        width: 460,
        height: 190,
      },
      title: '页面/元素设置区',
      course: {
        link: 'https://docs.tuzhanai.com/pages/viewpage.action?pageId=27492418',
      },
      content: { text: '您可以在这里设置海报背景或元素样式' },
    },
    {
      el: '.preview-save-publish',
      style: {
        direction: 'top',
        top: 20,
        left: 10,
        width: 460,
        height: 211,
        align: 'right',
        triangleRight: 80,
      },
      title: '海报设置及保存下载区',
      course: {
        link: 'https://docs.tuzhanai.com/pages/viewpage.action?pageId=27492418',
      },
      content: {
        text: '您可以在这里设置海报信息,最后保存创作好的海报,下载进行传播',
      },
    },
  ],
};

export const GUIDER_EVENT_POSTER_P = {
  // 物料面板
  Material: {
    show: 'Material_Show',
    hide: 'Material_Hide',
  },
  // 文本右侧属性面板
  Text: {
    show: 'Text_Show',
    hide: 'Text_Hide',
  },
  Poster: {
    show: 'Poster_Show',
    hide: 'Poster_Hide',
  },
};

common.tsx ​

/**
* @desc 指引组件 | 公共业务逻辑流程
* @author wangjun
* @since 2021-03-27 12:27:10
*/
import classnames from 'classnames';
import Button from 'common-components/button';
import {
  COURSE_TEXT,
  GUIDER_CONTENT_CLASS,
  GUIDER_HELP_CONTROL,
  SCENE_TYPE_CLASS,
} from 'components/guider/constants';
import {
  finishHandler,
  fireGuiderEvent,
  nextStepHandler,
  prevStepHandler,
} from 'components/guider/guider-tools';
import { Course, SCENE } from 'components/guider/types';
import React from 'react';

export default function Scene(scene: SCENE) {
  const { currentStep, skip, finish, list, type } = scene;
  const stepItem = list[currentStep - 1];
  const { el, title, course, content, style, spot, label, className } = stepItem;
  const elAny = Array.isArray(el) ? el[0] : el;
  const targetEl = elAny && typeof elAny === 'object' ? elAny.el : elAny;

  const selector = (targetEl && document.querySelector(targetEl)) || null;
  const spotEl = spot && spot.el && document.querySelector(spot.el);
  const spotTop = (spot && spot.top) || 0;
  const spotLeft = (spot && spot.left) || 0;

  const styleValue: React.CSSProperties = {};
  const triangleStyle: React.CSSProperties = {};
  const spotStyle: React.CSSProperties = {};

  // 自定义样式
  if (selector) {
    const { width, height, left, top } = selector.getBoundingClientRect();
    const rect = spotEl && spotEl.getBoundingClientRect();
    spotStyle.position = rect ? 'fixed' : 'absolute';
    if (spot && spot.image) {
      spotStyle.backgroundImage = `url(${spot.image})`;
    }

    // 三角形箭头指向
    switch (style.direction) {
      case 'top':
        styleValue.top = top + height + ((style && style.top) || 0);
        styleValue.left = left + ((style && style.left) || 0);
        if (style && style.align === 'right')
          styleValue.left += width - ((style && style.width) || 0);
        break;
      case 'right':
        styleValue.top = top + ((style && style.top) || 0);
        styleValue.left = left - style.width - 20;
        spotStyle.top = rect ? rect.top + spotTop : 0;
        spotStyle.left = rect ? rect.left + spotLeft : -64;
        break;
      case 'bottom':
        break;
      case 'left':
        styleValue.top = top + ((style && style.top) || 0);
        styleValue.left = left + width + ((style && style.left) || 0) + 20;
        break;
      default:
      //
    }
    styleValue.width = style.width;
    styleValue.height = style.height;
  }
  if (style.triangleTop !== undefined) triangleStyle.top = style.triangleTop;
  if (style.triangleRight !== undefined) triangleStyle.right = style.triangleRight;
  if (style.triangleBottom !== undefined) triangleStyle.bottom = style.triangleBottom;
  if (style.triangleLeft !== undefined) triangleStyle.left = style.triangleLeft;

  const stepTotal = list.length;
  const _title = typeof title === 'string' ? `${currentStep}.${title}` : title;
  const _course = (course: Course) => {
    return (
      <a href={course.link} target="_blank" rel="noreferrer">
        {course.text || COURSE_TEXT}
      </a>
    );
  };

  const stepEvent = (type: 'prev' | 'next') => {
    if (type === 'prev') {
      prevStepHandler(scene);
    }
    if (type === 'next') {
      nextStepHandler(scene);
    }
  };

  const skipEvent = () => {
    skip && skip.handler();

    // 跳过 | 启动5s帮助提示动效
    fireGuiderEvent(GUIDER_HELP_CONTROL, scene.type);
  };

  const finishEvent = () => {
    if (!finish) return;
    typeof finish === 'object' && finish.handler && finish.handler();
    finishHandler(scene);

    // 开始制作 | 启动5s帮助提示动效
    !scene.disabledHelpEffect && fireGuiderEvent(GUIDER_HELP_CONTROL, scene.type);
  };

  return (
    <div
      className={classnames(GUIDER_CONTENT_CLASS, SCENE_TYPE_CLASS[type], label, className)}
      style={styleValue}>
      <em
        className={classnames('triangle', style.direction, style.align && `align-${style.align}`)}
        style={triangleStyle}
      />
      {spot && <em className="spot" style={spotStyle} />}
      <h3>
        {_title} {course && _course(course)}
      </h3>
      <div className="guider-scene-body">
        {content && content.text && <p>{content.text}</p>}
        {content && content.image && <img src={content.image} />}
      </div>
      <div className="guider-scene-action">
        {skip && skip.visible && (
          <span className="skip">
            {currentStep !== list.length && <label onClick={() => skipEvent()}>跳过</label>}
          </span>
        )}
        <span className="step-total">
          ({currentStep}/{stepTotal})
        </span>
        {currentStep > 1 && (
          <Button className="prev-step" label="上一步" onClick={() => stepEvent('prev')} />
        )}
        {currentStep !== list.length ? (
          <Button className="next-step" label="下一步" onClick={() => stepEvent('next')} />
        ) : (
          finish && (
            <Button
              className="next-step"
              label={typeof finish === 'string' ? finish : finish.text}
              onClick={() => finishEvent()}
            />
          )
        )}
      </div>
    </div>
  );
}

guider-tools.ts ​

/**
* @desc 指引组件 | 工具函数
* @author wangjun
* @since 2021-03-27 12:27:10
*/
import { globalEvent } from '@fe/common/lib/events';
import { LSKEY } from 'components/adsorbent';
import {
  GUIDER_CANVAS_ID,
  GUIDER_CONTAINER,
  GUIDER_CONTENT_CLASS,
  GUIDER_STROKE_CLASS,
  INIT_GUIDER_SCENE,
  INIT_GUIDER_STEP,
  SCENE_TYPE,
} from 'components/guider/constants';
import { H5_E_EDITOR } from 'components/guider/scene/h5_editor/enterprise_data';
import { H5_P_EDITOR } from 'components/guider/scene/h5_editor/person_data';
import { POSTER_E_EDITOR } from 'components/guider/scene/poster_editor/enterprise_data';
import { POSTER_P_EDITOR } from 'components/guider/scene/poster_editor/person_data';
import { SCENE } from 'components/guider/types';
import LS from 'utils/localStorage';

/**
* @export
* @param {SCENE} scene 场景数据
* @param {boolean} [handlerEnable] 是否手动启动
*/
export async function initGuider(scene: SCENE, handlerEnable?: boolean) {
  scene.auto = !handlerEnable; // 自动与手动刚好相反

  // 自动控制
  const enable = Number(LS.get(scene.activation || '')) === 0;

  scene.activation && LS.set(scene.activation, String(1));

  // 开启指引流程
  if (enable || handlerEnable) {
    // 启动场景前事务钩子
    scene.beforeEnabled && (await scene.beforeEnabled());

    showGuider(scene);

    // 启动场景后事务钩子
    scene.afterEnabled && scene.afterEnabled();
  }
}

/**
* 显示引导
*/
export function showGuider(scene: SCENE) {
  globalEvent.fire(INIT_GUIDER_SCENE, scene); // 通知指引组件,开启指引流程
}

// 跳过
export function skipHandler(scene: SCENE) {
  const sceneItem = scene.list[scene.currentStep - 1];
  sceneItem && sceneItem.afterStep && sceneItem.afterStep();
  scene.currentStep = 1;
  scene.load = false;

  const container = document.querySelector(`#${GUIDER_CONTAINER}`);
  const modalContainer = document.querySelector(`.${GUIDER_CONTENT_CLASS}`);

  if (scene.disabledFinishAnimate) {
    modalContainer && modalContainer.remove();
  } else {
    if (scene.type === SCENE_TYPE.GUIDER_WANFA_DEVELOP) {
      modalContainer && modalContainer.setAttribute('data-animate', 'top-center');
    } else {
      modalContainer && modalContainer.setAttribute('data-animate', 'top-right');
    }
  }

  const canvas = document.querySelector(`#${GUIDER_CANVAS_ID}`);
  canvas && canvas.remove();

  clearGuiderStroke();

  window.setTimeout(() => {
    container && container.remove();
  }, 1200);
}

// 上一步
export function prevStepHandler(scene: SCENE) {
  const sceneItem = scene.list[scene.currentStep - 1];
  sceneItem.afterStep && sceneItem.afterStep();

  scene.currentStep -= 1;
  const prevSceneItem = scene.list[scene.currentStep - 1];
  prevSceneItem.beforeStep && prevSceneItem.beforeStep();

  globalEvent.fire(INIT_GUIDER_STEP, scene);
}

// 下一步
export function nextStepHandler(scene: SCENE) {
  const sceneItem = scene.list[scene.currentStep - 1];
  sceneItem.afterStep && sceneItem.afterStep();

  scene.currentStep += 1;
  const nextSceneItem = scene.list[scene.currentStep - 1];
  nextSceneItem.beforeStep && nextSceneItem.beforeStep();

  globalEvent.fire(INIT_GUIDER_STEP, scene);
}

// 完成
export function finishHandler(scene: SCENE) {
  skipHandler(scene);
}

// 清除描边
export function clearGuiderStroke() {
  const strokeContaner = document.querySelectorAll(`.${GUIDER_STROKE_CLASS}`);
  Array.from(strokeContaner).forEach((element: Element) => {
    element.remove();
  });
}

/**
* @export
* @param {UserModel} user 用户信息
* @param {boolean} isPosterEditor 是否为海报
*/
export function initGuiderHandler(
  user: UserModel,
  isPosterEditor: boolean,
  handlerEnable?: boolean
) {
  localStorage.setItem(LSKEY, '1'); // 强制设置隐藏,解决用户无需手动清除变量缓存问题
  // 企业身份 | 非企业身份
  if (user.organize) {
    isPosterEditor
      ? initGuider(POSTER_E_EDITOR, handlerEnable)
      : initGuider(H5_E_EDITOR, handlerEnable);
  } else {
    isPosterEditor
      ? initGuider(POSTER_P_EDITOR, handlerEnable)
      : initGuider(H5_P_EDITOR, handlerEnable);
  }
}

export function getSceneData(user: UserModel, isPosterEditor: boolean) {
  // 企业身份 | 非企业身份
  if (user.organize) {
    return isPosterEditor ? POSTER_E_EDITOR : H5_E_EDITOR;
  }
  return isPosterEditor ? POSTER_P_EDITOR : H5_P_EDITOR;
}

/**
* 发布引导组件特殊全局事件
* @param {string} key
* @param {number} step
*/
export function fireGuiderEvent(key: string, data?: any) {
  globalEvent.fire(key, data);
}

/**
* 监听引导组件特殊全局事件
* @param {string} key
* @param {function} func
* @param {boolean} delayOn 延迟订阅
*/
export function onGuiderEvent(key: string, func: (data?: any) => void, delayOn?: boolean) {
  globalEvent.on(key, func, delayOn);
}

/**
* 取消监听引导组件特殊全局事件
* @param {string} key
*/
export function offGuiderEvent(key: string) {
  globalEvent.off(key);
}

</script>


# types.ts
<script>
import React from 'react';

/**
* 场景数据模型声明
* @author wangjun<wangjun@tuzhanai.com>
* @since 2021/12/21
*/
export interface Course {
  text?: string; // [可选] 课程文案, 默认为"详细操作课程"文案
  link: string; // [必选] 课程链接, !!link && link != ''则显示课程文案与链接跳转
}

export interface Content {
  text?: string | React.ReactNode; // [可选] 文本
  image?: string; // [可选] 图片
}

export interface Skip {
  visible: boolean; // [必选] 是否显示"跳过"按钮, 0-不显示,1-显示,默认为0不显示。
  handler: (...args: any[]) => any; // [必选] 执行跳过事务钩子
}

export interface Finish {
  text: string; // [必选] 完成流程的按钮文本
  handler?: (...args: any[]) => any; // [必选] 完成流程的事务操作
}

export interface HighLight {
  image: string; // [可选] 图片地址
  top: number; //
  right: number; //
  bottom: number; //
  left: number; //
  width: number; //
  height: number; //
}

export interface Style {
  direction: string; // 模态框方向,三角形箭头的角度根据该字段来自动调整方向
  top?: number; // 模态框距离高亮目标顶部距离
  right?: number; // 模态框距离高亮目标右侧距离
  bottom?: number; // 模态框距离高亮目标底部距离
  left?: number; // 模态框距离高亮目标左侧距离
  width: number; // 模态框宽度
  height: number; // 模态框高度
  align?: string; // 模态框与高亮目标对齐方式,其值有:'left'-左对齐 | 'right'-右对齐 | 'top'-顶对齐 | 'bottom'-底对齐
  triangleTop?: number; // 三角形距离顶部距离
  triangleRight?: number; // 三角形距离右侧距离
  triangleBottom?: number; // 三角形距离底部距离
  triangleLeft?: number; // 三角形距离左侧距离
}

export interface Spot {
  top?: number; // 距离顶部距离
  right?: number; //
  bottom?: number; //
  left?: number; //
  el?: string; // 聚光灯目标对象
  image?: string; // 图片背景
}

export interface Elconfig {
  el: string; // 高亮区域目标对象
  image: string; // 填充背景图片
  imageBig?: string; // 大图模式
}

export interface SceneItem {
  el?: string | string[] | Elconfig[] | Array<string | Elconfig>; // [可选] 高亮区域目标对象, 为数组时即渲染多个高亮目标
  spot?: Spot; // [可选] 聚光灯目标
  delayTime?: number; // [可选] 是否需要延迟多久捕捉目标,默认为不延迟,单位ms
  label?: string; // [可选] 标识符
  highLight?: HighLight; // [可选] 高亮配置,如果配置了此字段,则el失效
  beforeStep?: (...args: any[]) => any; // [可选] 执行流程前的事务钩子
  afterStep?: (...args: any[]) => any; // [可选] 执行当前流程后的事务钩子
  numberType?: string; // [可选] 数字编号类型,'arab'-'1.' | 'china'-'一、',默认为阿拉伯数字排序
  style: Style; // [必选] 样式配置
  title?: string | React.ReactNode; // [可选] 标题
  course?: Course; // [可选] 课程配置,默认不显示
  content?: Content; // [可选] 主题内容
  className?: string; // 提示面板的自定义样式
  extends?: Record<string, any>; // [可选] 扩展字段
}

export interface SCENE {
  auto?: boolean; // 是否自动载入,默认为自动载入,自动载入-true | 手动载入-false
  load: boolean; // 当前载入状态,即当前是否处于使用活跃状态, 默认为false。
  name: string; // [必选] 名称
  type: string; // [必选] 类型
  version?: string; // [可选] 版本
  maskVisible?: boolean; // [可选] 是否显示蒙层
  activation?: string; // [可选] 是否可激活,0-唤起指引 | 1-不唤起指引,默认为0。
  beforeEnabled?: (...args: any[]) => any; // [可选] 在唤起场景指引之前的执行钩子
  afterEnabled?: (...args: any[]) => any; // [可选] 在唤起场景指引之后的执行钩子
  skip?: Skip; // [可选] 跳过配置
  currentStep: number; // [必选] 当前步骤位置, 默认为 0
  finish?: Finish | string; // [必选] 完成流程的事务钩子
  list: SceneItem[]; // [必选] 步骤执行列表
  label?: any; // [必选] 标识符
  disabledHelpEffect?: boolean; // 忽略帮助提示效果
  disabledFinishAnimate?: boolean; // 忽略完成后收起动画
  disabledStrokeLight?: boolean; // 忽略描绘高亮
  extends?: Record<string, any>; // [可选] 扩展字段
}

guider.tsx ​

/**
* @desc 引导组件
* @author wangjun
* @since 2021-03-26 12:27:10
*/
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { globalEvent } from '@fe/common/lib/events';

import {
  ASIDE_ADSORBENT_PLAN,
  ASIDE_POSTER_PLANE,
  ASIDE_TAB_PLANE,
  GUIDER_CANVAS_ID,
  GUIDER_CONTAINER,
  GUIDER_CONTENT,
  GUIDER_STROKE_CLASS,
  INIT_GUIDER_SCENE,
  INIT_GUIDER_STEP,
  SCENE_TYPE,
  WANFA,
} from 'components/guider/constants';
import 'components/guider/guider.less';
import CommonSceneFragment from 'components/guider/scene/common';
import WanfaGuideFragment from 'components/guider/scene/wanfa_guide/index';
import { Elconfig, SCENE } from 'components/guider/types';

import { clearGuiderStroke, fireGuiderEvent } from './guider-tools';
import { GUIDER_EVENT_DEVELOP } from './scene/wanfa_develop/data';

// 过滤非标准场景流程
const excludesScene: string[] = [SCENE_TYPE.GUIDER_WANFA_GUIDE];
const includesPoster: string[] = [SCENE_TYPE.POSTER_E_EDITOR, SCENE_TYPE.POSTER_P_EDITOR];
const RENDER_ASIDE_LIST: string[] = [ASIDE_ADSORBENT_PLAN, ASIDE_TAB_PLANE, ASIDE_POSTER_PLANE];
let GLOBAL_SCENE: SCENE;

class Guider extends Component {
  componentDidMount() {
    this.initSceneSubscriber();
    this.initStepSubscriber();
    this.initSceneAdapter();
  }

  debounce = (() => {
    let timeout: Node.js.Timeout | null = null;
    return (fn: (...args: any[]) => void, wait: number) => {
      if (timeout) clearTimeout(timeout);
      timeout = setTimeout(fn, wait);
    };
  })();

  delayRender(GLOBAL_SCENE: SCENE) {
    if (!GLOBAL_SCENE || !GLOBAL_SCENE.load) return;
    const { currentStep, list } = GLOBAL_SCENE;
    if (currentStep && list[currentStep - 1] && list[currentStep - 1].delayTime) {
      // 解决缩放时目标元素漂移动效带来的延迟问题
      window.setTimeout(() => {
        this.initStep(GLOBAL_SCENE);
      }, list[currentStep - 1].delayTime);
      return;
    }
    this.initStep(GLOBAL_SCENE);
  }

  debounceHandler() {
    this.delayRender(GLOBAL_SCENE);
  }

  // 兼容浏览器页面放大缩小情况的适配情况
  initSceneAdapter() {
    window.addEventListener(
      'resize',
      () => {
        this.debounce(() => {
          this.debounceHandler();
        }, 50);
      },
      false
    );
    window.addEventListener(
      'wheel',
      (e: Record<string, any>) => {
        if (!e.ctrlKey) return; // Ctrl + 滚动组合事件
        this.delayRender(GLOBAL_SCENE);
      },
      false
    );
  }

  // 订阅开启指引流程机制
  initSceneSubscriber() {
    globalEvent.on(INIT_GUIDER_SCENE, (scene: SCENE) => {
      GLOBAL_SCENE = scene;
      this.initScene(scene);
    });
  }

  // 订阅启动步骤流程
  initStepSubscriber() {
    globalEvent.on(INIT_GUIDER_STEP, (scene: SCENE) => {
      this.initStep(scene);
    });
  }

  // 初始化场景
  initScene(scene: SCENE) {
    scene.load = true;

    // 玩法操作指南
    if (scene.type === SCENE_TYPE.GUIDER_WANFA_GUIDE) {
      fireGuiderEvent(GUIDER_EVENT_DEVELOP.Material.show);
      WanfaGuideFragment();
      return;
    }

    const { list } = scene;
    if (!list.length) return;

    // 初始化场景容器
    this.initSceneContainer();

    // 海报版本 | 第一次初始化渲染时无法实时获取海报模板的DOM问题
    if (includesPoster.includes(scene.type) && scene.auto) {
      if (scene.list[0].label === ASIDE_POSTER_PLANE) {
        window.setTimeout(() => {
          this.initStep(scene);
        }, 300);
        return;
      }
    }

    // 初始化步骤
    this.initStep(scene);
  }

  // 初始化场景容器
  initSceneContainer() {
    let guiderContainer = document.querySelector(`#${GUIDER_CONTAINER}`);
    if (!guiderContainer) guiderContainer = document.createElement('div');
    guiderContainer.setAttribute('id', GUIDER_CONTAINER);
    guiderContainer.setAttribute('class', 'guider-container');
    document.body.appendChild(guiderContainer);
  }

  // 初始化步骤
  async initStep(scene: SCENE) {
    if (excludesScene.includes(scene.type)) return;

    const { currentStep, maskVisible, list } = scene;

    // 步骤流程
    const stepItem = list[currentStep - 1];

    // 执行此步骤之前的事务钩子
    stepItem.beforeStep && (await stepItem.beforeStep());

    // 初始化蒙层
    maskVisible && this.initMask();

    // 初始化高亮区域目标
    maskVisible && stepItem.el && this.initHighLight(stepItem.el, scene);

    // 初始化步骤容器
    this.initStepContainer();

    ReactDOM.render(CommonSceneFragment(scene), document.querySelector(`#${GUIDER_CONTENT}`));
  }

  // 初始化蒙层
  initMask() {
    const guiderContainer = document.querySelector(`#${GUIDER_CONTAINER}`);
    if (!guiderContainer) return;

    let canvas = document.querySelector(`#${GUIDER_CANVAS_ID}`);
    canvas && canvas.remove();

    const { offsetWidth, offsetHeight } = guiderContainer as HTMLElement;

    canvas = document.createElement('canvas');
    canvas.setAttribute('id', GUIDER_CANVAS_ID);
    canvas.setAttribute('class', 'guider-canvas-bg');
    canvas.setAttribute('width', String(offsetWidth));
    canvas.setAttribute('height', String(offsetHeight));
    guiderContainer.appendChild(canvas);

    const ctx = (canvas as HTMLCanvasElement).getContext('2d');
    if (!ctx) return;

    // 绘制蒙层背景
    ctx.beginPath();
    ctx.fillStyle = 'rgb(0, 0, 0, 0.8)';
    ctx.fillRect(0, 0, offsetWidth, offsetHeight);
  }

  // 蓝色描边
  strokeLight(scene: SCENE, top: number, left: number, width: number, height: number) {
    // 拦截玩法指引除了第一步骤有蓝色描边的效果外,其他步骤不显示
    const { type, currentStep, disabledStrokeLight } = scene;
    if (type === SCENE_TYPE.GUIDER_WANFA_DEVELOP && currentStep !== 1) {
      return;
    }
    if (disabledStrokeLight) return;
    const guiderCcontainer = document.querySelector(`#${GUIDER_CONTAINER}`);
    if (!guiderCcontainer) return;

    const strokeStyle = `;top:${top}px;left:${left}px;width:${width}px;height:${height}px;`;

    const strokeContainer = document.createElement('div');
    strokeContainer.setAttribute('class', GUIDER_STROKE_CLASS);
    strokeContainer.setAttribute('style', strokeStyle);
    guiderCcontainer.appendChild(strokeContainer);
  }

  // 玩法设置 | 动效
  wanfaSetEffect(scene: SCENE, top: number, left: number) {
    const guiderContent = document.querySelector(`#${GUIDER_CONTENT}`);
    if (!guiderContent) return;

    const isNotWanfa = scene.type !== SCENE_TYPE.GUIDER_WANFA_DEVELOP;
    const isNotEffect = scene.list[scene.currentStep].label !== WANFA.effectShowClass;
    if (isNotWanfa || isNotEffect) {
      return;
    }

    const effectStyle: React.CSSProperties = {
      top: top + 53,
      left: left + 90,
    };
    let container = document.querySelector('.wanfa-effect-container');
    if (container) container.remove();

    container = document.createElement('div');
    container.setAttribute('class', 'wanfa-effect-container');
    guiderContent.appendChild(container);

    const effectFragment = (
      <div className="container" style={effectStyle}>
        <div className="guide-point-back" />
        <div className="guide-point-front" />
        <i className="e-icon icon-zhiyinxiaoshou" />
      </div>
    );

    ReactDOM.render(effectFragment, container);
  }

  // 渲染高亮区域
  renderHighLight(scene: SCENE, ctx: CanvasRenderingContext2D, item: Elconfig) {
    const { el, image } = item;

    const selector = el && document.querySelector(el);
    if (!selector) return;
    const { left, top } = selector.getBoundingClientRect();

    const img = new Image();
    img.setAttribute('crossorigin', 'crossorigin');
    img.onload = () => {
      ctx.drawImage(img, left, top, img.naturalWidth, img.naturalHeight);
      this.strokeLight(scene, top, left, img.naturalWidth, img.naturalHeight);
    };
    img.src = image;

    this.wanfaSetEffect(scene, top, left);
  }

  renderAdsorbent(scene: SCENE, ctx: CanvasRenderingContext2D, item: Elconfig) {
    const { el, image, imageBig } = item;
    const selector = el && document.querySelector(el);
    if (!selector) return;
    const { left, top, height } = selector.getBoundingClientRect();
    const asideLeft = 0;
    let { width } = selector.getBoundingClientRect();
    width += left;

    const img = new Image();
    img.setAttribute('crossorigin', 'crossorigin');
    img.onload = () => {
      // 海报指引不存在imageBig字段,因此不需要考虑大图祸小图的问题,默认为小图
      if (!imageBig || img.naturalWidth >= width - 20) {
        ctx.drawImage(img, asideLeft, top, img.naturalWidth, img.naturalHeight);
        this.strokeLight(scene, top, asideLeft, img.naturalWidth, height);
      }
    };
    img.src = image;

    if (imageBig) {
      const imgBig = new Image();
      imgBig.setAttribute('crossorigin', 'crossorigin');
      imgBig.onload = () => {
        if (imgBig.naturalWidth <= width + 20) {
          ctx.drawImage(imgBig, asideLeft, top, imgBig.naturalWidth, imgBig.naturalHeight);
          this.strokeLight(scene, top, asideLeft, imgBig.naturalWidth, height);
        }
      };
      imgBig.src = imageBig;
    }
  }

  // 初始化高亮区域目标
  initHighLight(el: string | string[] | Elconfig[] | Array<string | Elconfig>, scene: SCENE) {
    if (scene.type === SCENE_TYPE.GUIDER_WANFA_DEVELOP && scene.currentStep !== 2) {
      const wanfaContainer = document.querySelector('.wanfa-effect-container');
      wanfaContainer && wanfaContainer.remove();
    }

    const { currentStep, list } = scene;
    const stepItem = list[currentStep - 1];

    // 清除描边
    clearGuiderStroke();

    const elements = Array.isArray(el) ? el : [el];

    const canvas = document.querySelector(`#${GUIDER_CANVAS_ID}`);
    const ctx = canvas && (canvas as HTMLCanvasElement).getContext('2d');
    if (!ctx) return;

    elements.forEach((selector: string | Elconfig) => {
      if (typeof selector === 'object') {
        if (stepItem.label && RENDER_ASIDE_LIST.includes(stepItem.label)) {
          this.renderAdsorbent(scene, ctx, selector);
          return;
        }
        this.renderHighLight(scene, ctx, selector);
        return;
      }

      const elem = selector && document.querySelector(selector);
      if (!elem) return;

      const { left, top, width, height } = elem.getBoundingClientRect();
      ctx.save();
      ctx.beginPath();
      ctx.rect(left, top, width, height);
      ctx.clip();
      ctx.clearRect(left, top, width, height);
      ctx.restore();

      this.strokeLight(scene, top, left, width, height);
    });
  }

  initStepContainer() {
    const guiderContainer = document.querySelector(`#${GUIDER_CONTAINER}`);
    const guiderContent = document.querySelector(`#${GUIDER_CONTENT}`); // 避免存在该容器的情况下重新DOM结构引起层次置前的问题
    if (!guiderContainer || guiderContent) return;

    const guider = document.createElement('div');
    guider.setAttribute('id', GUIDER_CONTENT);
    guider.setAttribute('class', 'guider-content');
    guiderContainer.appendChild(guider);
  }

  render() {
    return null;
  }
}

export default Guider;

页面顶部加载进度条 ​

# 资源文档
> 顶部加载条:https://github.com/rstacruz/nprogress
> 顶部加载条:https://ricostacruz.com/nprogress/
> Loading Bar:https://loading.io/progress/


# 简介与核心思想
> 模仿手机上单击网页跳转前的顶部加载进度条。

# 参考案例
> vue中页面加载顶部进度条效果的实现:https://blog.csdn.net/weixin_44533589/article/details/110389105


# 监听页面加载进度状况
<script>
  switch (document.readyState) {
    case "loading":  // 文档正在加载
      console.log("文档正在加载...");
      break;
    case "interactive":  // 文档已加载完成且已被解析,但子资源(如image,css,iframe等)仍在加载
      console.log("文档已加载完成,可以操作DOM元素")
      break;
    case "complete":    // 文档以及文档里包含的子资源都已经完成加载,这是会触发load事件
      // 页面已完成加载,包括css,图片等
      console.log("第一个css规则为: " + document.styleSheets[0].cssRules[0].cssText);
      break;
  }


  // 等同于readyState的interactive状态
  document.addEventListener("DOMContentLoaded", function(event) {
    console.log("DOM文档已加载完成");
  });

  // document.readyState === "complete"
  window.addEventListener("load", function(event) {
    console.log("文档以及它包含的资源都已加载完成");
  });

</script>


# 使用nprogress插件示例
<script>

</script>




# 使用Loading Bar插件示例
<script>
  var bar = new ldBar(".myBar", {
   "stroke": '#f00',
   "stroke-width": 10,
   "preset": "fan",
   "value": 65
  });

  // 更新值
  bar.set(
    50,     /* target value. */
    false   /* enable animation. default is true */
  );
</script>

视频网站 ​

电商网站 ​

抢购秒杀 ​

# 核心技术
> 倒计时精度与容错机制
> 高并发限流机制
> 反作弊与安全机制

# 简介与核心思想
> 活动:抢购、秒杀
> QPS(Query Per Second):每秒处理请求数
> 100万并发的请求


# 倒计时偏差矫正与容错机制
> 偏差:服务器与客户端的时间差异,以服务器时间为基准,给客户端返回开始时间与结束时间
> 环境:离屏与熄屏造成时间暂停的问题,这里使用Document.visibilityState来监听屏的变化矫正时间

# 高并发限流机制
> 前端提交事件的限制与约束

# 反作弊与安全机制


# 限流
> 在业务逻辑入口,限制每个账户仅请求一次,将多余的请求过滤,防御同一个账户利用非法工具发起大规模请求,破坏了秒杀和抢购的公平性
> 使用队列机制,在未处理完第一个请求之前,将其他的请求放入队列中,后续处理

# 多个账号,一次性发送多个请求
> 限制同一个IP发起多个请求,约束IP请求频率

# 伪真实用户
> IP代理池

# 前端技术实现
> 倒计时,每5秒轮询一次服务器时间,同步更新倒计时显示剩余时间
# 缺陷
> 因为JS单线程的缘故,只要主线程因为执行栈处于阻塞状况,任务队列中的异步任务就会延迟,从而出现时间偏差的情况


# 过载保护:检测到系统满负载状态,拒绝请求

# 技术架构
> redis将抢购商品信息与订单数量状态存放在redis数据库中


# 代码实现

产品主图放大镜 ​

<ul
  id="Slider-Gallery-{{ section.id }}"
  class="product__media-list contains-media grid grid--peek list-unstyled slider slider--mobile enlarger-product-thumbnail"
  role="list"
>


<script type="text/javascript">
  function initProductMainModel() {
    // 不兼容移动端
    if (!isPC()) return;

    const productThumbnail = document.querySelector('.enlarger-product-thumbnail');
    if (!productThumbnail) return;

    const options = {
      enter: false,
      target: null,
      elem: null,
      src: null,
      rect: null,
    };

    const layerOptions = {
      id: 'enlarger-product-thumbnail-layer',
      top: 0,
      left: 0,
      width: 0,
      height: 0,
    };

    const containerOptions = {
      id: 'enlarger-product-thumbnail-container',
      top: 0,
      left: 0,
      width: 0,
      height: 0,
    };

    const enlargerOptions = {
      id: 'enlarger-product-thumbnail-img',
      scale: 4,
      top: 0,
      left: 0,
      width: 0,
      height: 0,
    };

    // 进入 主图范围
    productThumbnail.addEventListener(
      'mouseenter',
      async function (e) {
        options.enter = true;
        options.target = e.target;

        if (e.target?.nodeName?.toLowerCase() !== 'ul') return;

        const collection = Array.from(e.target.children);
        for (let elem of collection) {
          const display = getStyle(elem, 'display');
          if (display !== 'none') {
            options.elem = elem;
            break;
          }
        }

        if (!options.elem) return;
        options.src = options.elem.getAttribute('data-product-thumbnail');
        options.rect = e.target.getBoundingClientRect();

        const { width, height, top, right, bottom, left } = options?.rect || {};
        const { pageX, pageY } = e;

        /**
         * 创建光标浮漂背影
         */
        const layerElem = document.querySelector(`#${layerOptions.id}`);
        if (layerElem) layerElem.remove();

        layerOptions.width = width / 4;
        layerOptions.height = height / 4;

        // 计算位置
        layerOptions.top = pageY - top - layerOptions.width / 2;
        layerOptions.left = pageX - left - layerOptions.height / 2;

        // 矫正偏离范围
        if (pageY - layerOptions.height / 2 < top) {
          layerOptions.top = 0;
        }
        if (pageX - layerOptions.width / 2 < left) {
          layerOptions.left = 0;
        }
        if (pageY + layerOptions.height / 2 > bottom) {
          layerOptions.top = bottom - top - layerOptions.height;
        }
        if (pageX + layerOptions.width / 2 > right) {
          layerOptions.left = right - left - layerOptions.width;
        }

        const layer = document.createElement('div');
        layer.setAttribute('id', layerOptions.id);
        const layerStyle = `display:block;position:absolute;top:${layerOptions.top}px;left:${layerOptions.left}px;width:${layerOptions.width}px;height:${layerOptions.height}px;pointer-events:none;z-index:999;border:2px solid #ddd;background-color:rgba(216,191,216,0.5);`;
        layer.setAttribute('style', layerStyle);
        options.target.appendChild(layer);

        // 创建放大镜
        containerOptions.top = top;
        containerOptions.left = right + 20;
        containerOptions.width = width;
        containerOptions.height = height;

        /**
         * 放大镜主图缩放效果
         */
        enlargerOptions.width = containerOptions.width * enlargerOptions.scale;
        enlargerOptions.height = containerOptions.height * enlargerOptions.scale;

        // 等比缩放光标坐标
        const cursorY = (pageY - top) * enlargerOptions.scale;
        const cursorX = (pageX - left) * enlargerOptions.scale;

        // 保障放大镜的主图位居正中央
        if (cursorX > containerOptions.width / 2) {
          enlargerOptions.left = -(cursorX - containerOptions.width / 2);
        } else {
          enlargerOptions.left = 0;
        }
        if (cursorY > containerOptions.height / 2) {
          enlargerOptions.top = -(cursorY - containerOptions.height / 2);
        } else {
          enlargerOptions.top = 0;
        }

        // 矫正偏离范围
        if (enlargerOptions.width - cursorX < containerOptions.width / 2) {
          enlargerOptions.left = -(enlargerOptions.width - containerOptions.width);
        }
        if (enlargerOptions.height - cursorY < containerOptions.height / 2) {
          enlargerOptions.top = -(enlargerOptions.height - containerOptions.height);
        }

        const containerElem = document.querySelector(`#${containerOptions.id}`);
        if (containerElem) containerElem.remove();

        const container = document.createElement('div');
        const thumbnail = document.createElement('img');
        thumbnail.setAttribute('src', options.src);
        const imgStyle = `position:absolute;top:${enlargerOptions.top}px;left:${enlargerOptions.left}px;width:${enlargerOptions.width}px;height:${enlargerOptions.height}px;pointer-events:none;`;
        thumbnail.setAttribute('style', imgStyle);
        thumbnail.setAttribute('id', enlargerOptions.id);
        container.appendChild(thumbnail);

        const divStyle = `position:absolute;top:${containerOptions.top}px;left:${containerOptions.left}px;width:${containerOptions.width}px;height:${containerOptions.height}px;border:2px solid #eee;overflow:hidden;z-index:999;`;
        container.setAttribute('style', divStyle);
        container.setAttribute('id', containerOptions.id);
        document.body.appendChild(container);
      },
      false
    );

    // 离开 主图范围
    productThumbnail.addEventListener(
      'mouseleave',
      function () {
        options.enter = false;
        options.target = null;
        options.elem = null;
        options.src = null;

        const layerElem = document.querySelector(`#${layerOptions.id}`);
        if (layerElem) layerElem.remove();

        const containerElem = document.querySelector(`#${containerOptions.id}`);
        if (containerElem) containerElem.remove();
      },
      false
    );

    // 移动 主图范围
    productThumbnail.addEventListener(
      'mousemove',
      function (e) {
        const { width, height, top, right, bottom, left } = options?.rect || {};
        const { pageX, pageY } = e;

        /**
         * 创建光标浮漂背影
         */
        layerOptions.top = pageY - top - layerOptions.width / 2;
        layerOptions.left = pageX - left - layerOptions.height / 2;

        if (pageY - layerOptions.height / 2 < top) {
          layerOptions.top = 0;
        }
        if (pageX - layerOptions.width / 2 < left) {
          layerOptions.left = 0;
        }
        if (pageY + layerOptions.height / 2 > bottom) {
          layerOptions.top = bottom - top - layerOptions.height;
        }
        if (pageX + layerOptions.width / 2 > right) {
          layerOptions.left = right - left - layerOptions.width;
        }

        const layerElem = document.querySelector(`#${layerOptions.id}`);
        if (layerElem) {
          const layerStyle = `display:block;position:absolute;top:${layerOptions.top}px;left:${layerOptions.left}px;width:${layerOptions.width}px;height:${layerOptions.height}px;pointer-events:none;z-index:999;border:2px solid #ddd;background-color:rgba(216,191,216,0.5);`;
          layerElem.setAttribute('style', layerStyle);
        }

        /**
         * 放大镜主图缩放效果
         */

        // 等比缩放光标坐标
        const cursorY = (pageY - top) * enlargerOptions.scale;
        const cursorX = (pageX - left) * enlargerOptions.scale;

        // 保障放大镜的主图位居正中央
        if (cursorX > containerOptions.width / 2) {
          enlargerOptions.left = -(cursorX - containerOptions.width / 2);
        } else {
          enlargerOptions.left = 0;
        }
        if (cursorY > containerOptions.height / 2) {
          enlargerOptions.top = -(cursorY - containerOptions.height / 2);
        } else {
          enlargerOptions.top = 0;
        }

        // 矫正偏离范围
        if (enlargerOptions.width - cursorX < containerOptions.width / 2) {
          enlargerOptions.left = -(enlargerOptions.width - containerOptions.width);
        }
        if (enlargerOptions.height - cursorY < containerOptions.height / 2) {
          enlargerOptions.top = -(enlargerOptions.height - containerOptions.height);
        }

        // 产品主图放大镜
        const enlargerElem = document.querySelector(`#${enlargerOptions.id}`);
        if (enlargerElem) {
          const imgStyle = `position:absolute;top:${enlargerOptions.top}px;left:${enlargerOptions.left}px;width:${enlargerOptions.width}px;height:${enlargerOptions.height}px;pointer-events:none;`;
          enlargerElem.setAttribute('style', imgStyle);
        }
      },
      false
    );
  }

  // 初始化模型
  initProductMainModel();
</script>

Released under the MIT License.