切换至深色主题
核心技术与功能模块
通用功能设计
各种浏览器低版本过滤与提示
<!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
- https://developer.mozilla.org/zh-CN/docs/Web/API/Fullscreen_API
- GitHub:https://github.com/sindresorhus/screenfull.js
<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>