Commit 5458a8f6 authored by haiyoucuv's avatar haiyoucuv

内置工具包built-in

埋点
parent 2bcf5fd8
......@@ -16,3 +16,4 @@ playground-temp
temp
TODOs.md
.eslintcache
*.tsbuildinfo
export * from "./md";
export * from "./utils";
export * from "./net";
export * from "./md";
import { doClickLog, doShowLog } from "./main";
import { getReference } from "../utils";
import { doShowLog } from "./main";
import { IAutoMdData } from "./interface.ts";
import { logClick } from "./manual.ts";
// 如果点击埋点和跳转埋点只能添加一个的话,两个mapper可以合并成为一个mapper
const clickMapper = new Map();
const generatorClick = (item: IAutoMdData) => {
const ele = getReference(item.ele);
const ele = document.querySelector(item.ele);
if (!!ele && !clickMapper.get(item.ele)) {
const handler = doClickLog.bind(null, item.data);
const handler = logClick.bind(null, item.data);
clickMapper.set(item.ele, {
ele,
handler,
......
export * from './interface.ts';
export * from './auto.ts';
export * from './manual.ts';
import "./intersection-observer.polyfill.js";
import "intersection-observer";
import { jsonp } from "../net";
// import axios from "axios";
import { obj2query } from "../utils";
import { IAutoMdData } from "./interface.ts";
import { logExposure } from "./manual.ts";
const showedMapper = {};
const intersectionObservers: IntersectionObserver[] = []
const intersectionObservers: IntersectionObserver[] = [];
function registryIntersectionObserver(items: any[]) {
for (const observer of intersectionObservers) {
observer.disconnect()
observer.disconnect();
}
intersectionObservers.splice(0)
intersectionObservers.splice(0);
items.forEach((item) => {
if (typeof item.ele === "string") {
......@@ -44,7 +41,7 @@ const generatorShow = (item: IAutoMdData) => {
observer.unobserve(change.target);
jsonp(`${data.domain}/exposure/standard?${obj2query(data)}`);
logExposure(data);
} else {
// 消失在视口
......@@ -63,7 +60,7 @@ const generatorShow = (item: IAutoMdData) => {
}
};
export function doShowLog(items) {
export function doShowLog(items: IAutoMdData[]) {
// 先注册一次
registryIntersectionObserver(items);
......@@ -73,8 +70,3 @@ export function doShowLog(items) {
});
observer.observe(document, { childList: true, subtree: true });
}
export function doClickLog(data) {
jsonp(`${location.host}/log/click?${obj2query(data)}`);
// axios.get(`/log/click?${obj2query(data)}`);
}
import { doClickLog } from "./main.ts";
import { IMdData } from "./interface.ts";
import { jsonp } from "../net";
import { obj2query } from "../utils";
export function handleLogExposure(data: IMdData) {
export function logExposure(data: IMdData) {
jsonp(`${data.domain}/exposure/standard?${obj2query(data)}`);
}
export function handleLogClick(data: IMdData) {
doClickLog(data);
export function logClick(data: IMdData) {
jsonp(`/log/click?${obj2query(data)}`);
}
......@@ -4,6 +4,7 @@ export function jsonp(url: string, params: any = {}) {
return new Promise<void>((resolve, reject) => {
const src = urlJoin(url, obj2query(params));
const scriptEl = document.createElement('script');
scriptEl.src = src;
scriptEl.onload = function () {
resolve();
......
/**
* 添加script
* @ctype PROCESS
* @description 添加script,可以多个
* @param {string} script - js脚本
* @param {HTMLElement} [parentNode=document.head] - 父节点
* @example 一般用法
* appendScript('function test(text){console.log("text:", text)}')
* test('hello'); //hello
*/
export function appendScript(script: string, parentNode: HTMLElement = document.head) {
if (!script) {
return {
type: 'failed'
}
}
if (script.indexOf('<script') === 0) {
let temp = document.createElement('div')
temp.innerHTML = script
for (let i = 0, li = temp.children.length; i < li; i++) {
const child: any = temp.children[i]
if (child.src) {
appendJsScript(child.src, parentNode)
i--
li--
} else {
evalJsScript(child.innerHTML, parentNode)
}
}
} else {
evalJsScript(script, parentNode)
}
}
/**
* 执行js脚本
* @ctype PROCESS
* @description 执行js脚本
* @param {string} script - 脚本文本
* @param {HTMLElement} [parentNode=document.head] - 父节点
*/
export function evalJsScript(script: string, parentNode: HTMLElement = document.head) {
let el = document.createElement('script')
el.innerHTML = script
document.head.appendChild(el)
setTimeout(() => {
document.head.removeChild(el)
}, 1)
}
/**
* 动态引入js
* @ctype PROCESS
* @description 执行js脚本
* @param {string} url - js脚本地址
* @param {HTMLElement} [parentNode=document.head] - 父节点
*/
export function appendJsScript(url: string, parentNode: HTMLElement = document.head) {
return new Promise((resolve, reject) => {
let scriptEl = document.createElement('script')
scriptEl.src = url
scriptEl.onload = function () {
document.head.removeChild(scriptEl)
resolve(true)
}
scriptEl.onerror = function () {
document.head.removeChild(scriptEl)
reject();
}
parentNode.appendChild(scriptEl)
})
}
export * from "./utils.ts";
export * from "./browser.ts";
......@@ -23,12 +23,3 @@ export function urlJoin(url: string, query: any) {
return url;
}
}
export const getReference = (ele: string | HTMLElement) => {
if (ele instanceof HTMLElement) {
return ele;
} else if (typeof ele === "string") {
return document.querySelector(ele);
}
}
\ No newline at end of file
......@@ -21,6 +21,9 @@
"noFallthroughCasesInSwitch": true,
"declaration": true,
"sourceMap": true,
"composite": false,
},
"include": ["src"]
}
{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/md/auto.ts","./src/md/index.ts","./src/md/interface.ts","./src/md/main.ts","./src/md/manual.ts","./src/net/index.ts","./src/net/jsonp.ts","./src/utils/index.ts","./src/utils/utils.ts"],"version":"5.6.3"}
\ No newline at end of file
{
"files": [],
"compilerOptions": {
"composite": false
},
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
......
......@@ -19,7 +19,9 @@
"strict": false,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
"noFallthroughCasesInSwitch": true,
"composite": false
},
"include": ["vite.config.ts"]
}
{"root":["./vite.config.ts"],"version":"5.6.3"}
\ No newline at end of file
# 顶部的EditorConfig文件
root = true
# unix风格的换行符,每个文件都以换行符结尾
[*]
end_of_line = lf
insert_final_newline = true
# 设置默认字符集
charset = utf-8
# 去除行尾空白字符
trim_trailing_whitespace = true
# 使用空格缩进,设置2个空格缩进
indent_style = space
indent_size = 2
......@@ -8,7 +8,6 @@ pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
......@@ -22,3 +21,6 @@ dist-ssr
*.njsproj
*.sln
*.sw?
dist/*/*
!dist/index.html
\ No newline at end of file
UPLOAD_DIR=db_games/spark/v3
\ No newline at end of file
# .env.production
CDN_DOMAIN=//yun.duiba.com.cn
OSS_REGION=oss-cn-hangzhou
OSS_BUCKET=duiba
OSS_ACCESS_KEY_ID=LTAI5tPUSSxgkEmKPAfVXUQQ
OSS_ACCESS_SECRET=6sk3EDd1BYrXlAUoh8maMuN7hOMkh1
\ No newline at end of file
import chalk from "chalk";
import AutoUpload from "../Uploader/Uploader.ts";
import * as path from "path";
interface IDuibaPublishOptions {
buildVersion: string | number,
uploadDir: string,
accessKeySecret: string,
accessKeyId: string,
bucket: string,
region: string,
}
export default function DuibaPublish(options: IDuibaPublishOptions) {
const {
buildVersion,
uploadDir,
accessKeySecret,
accessKeyId,
bucket,
region,
} = options;
return {
name: 'duiba-publish',
async closeBundle() {
const autoUpload = new AutoUpload({
dir: path.resolve("dist"),
originDir: `/${uploadDir}/${buildVersion}/`,
accessKeySecret, accessKeyId, bucket, region,
});
await autoUpload.start();
console.log(`${chalk.green(`上传成功: ${buildVersion}`)}`);
}
}
}
import * as path from "path";
import * as fs from "fs";
import ProgressBar from "progress";
import OSS from "ali-oss";
interface IAutoUploadOptions {
dir: string,
originDir: string,
bucket: string,
accessKeyId: string,
accessKeySecret: string,
region: string,
}
export default class AutoUpload {
options: IAutoUploadOptions = {
dir: "",
originDir: "",
region: "oss-cn-hangzhou",
accessKeyId: "",
accessKeySecret: "",
bucket: ""
};
client = null;
bar = null;
private _files: any[];
private existFiles: number = 0;
private uploadFiles: number = 0;
private errorFiles: number = 0;
constructor(props: IAutoUploadOptions) {
this.options = Object.assign({}, this.options, props);
const checkOptions = [
"dir", "originDir",
"bucket", "region",
"accessKeySecret", "accessKeyId",
];
for (const optionKey of checkOptions) {
if (!this.options[optionKey]) {
throw new Error(`AutoUpload: required option "${optionKey}"`);
}
}
this.init();
}
init() {
const {accessKeyId, accessKeySecret, bucket, region} = this.options;
this.client = new OSS({region, accessKeyId, accessKeySecret, bucket});
this.bar = new ProgressBar(`文件上传中 [:bar] :current/${this.files().length} :percent :elapseds`, {
complete: "●",
incomplete: "○",
width: 20,
total: this.files().length,
callback: () => {
console.log("%cAll complete.", "color: green");
console.log(`%c本次队列文件共${this.files().length}个,已存在文件${this.existFiles}个,上传文件${this.uploadFiles}个,上传失败文件${this.errorFiles}个`, "color: green");
}
})
return this;
}
files() {
if (this._files) return this._files;
this._files = [];
/**
* 文件遍历方法
* @param filePath 需要遍历的文件路径
*/
const fileDisplay = (filePath) => {
//根据文件路径读取文件,返回文件列表
const files = fs.readdirSync(filePath);
files.forEach((filename) => {
//获取当前文件的绝对路径
const fileDir = path.join(filePath, filename);
//根据文件路径获取文件信息,返回一个fs.Stats对象
const stats = fs.statSync(fileDir);
const isFile = stats.isFile();//是文件
const isDir = stats.isDirectory();//是文件夹
if (isFile) {
this._files.push(fileDir);
} else if (isDir) {
fileDisplay(fileDir);//递归,如果是文件夹,就继续遍历该文件夹下面的文件
}
});
}
//调用文件遍历方法
fileDisplay(this.options.dir);
return this._files;
}
async start() {
const ps = this.files().map((file) => {
const relativePath = path.relative(this.options.dir, file)
.replace(path.sep, "/");
this.existFiles = 0;
this.uploadFiles = 0;
this.errorFiles = 0;
const originPath = `${this.options.originDir}${relativePath}`;
return (async () => {
let originFile = null;
originFile = await this.client.head(originPath)
.catch((error: Error) => originFile = error);
try {
if (originFile.status === 404) {
await this.client.put(originPath, file);
this.uploadFiles += 1;
} else {
this.existFiles += 1;
}
} catch (error) {
this.errorFiles += 1;
}
this.bar.tick();
})();
});
await Promise.all(ps).catch((err) => {
console.error("上传错误", err);
});
}
}
......@@ -19,6 +19,11 @@ export default tseslint.config(
},
rules: {
...reactHooks.configs.recommended.rules,
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-unused-expressions": "off",
"prefer-rest-params": "off",
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
......
......@@ -2,12 +2,39 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<link rel="dns-prefetch" href="//yun.duiba.com.cn" />
<link rel="preconnect" href="//embedlog.duiba.com.cn">
<title>活动标题</title>
<script type="text/javascript">
if (localStorage && localStorage.isWebp) {
document
.getElementsByTagName('html')[0]
.setAttribute('duiba-webp', 'true');
}
</script>
<script src="//yun.duiba.com.cn/js-libs/rem/1.1.3/rem.min.js"></script>
<script>
var CFG = CFG || {};
CFG.projectId = location.pathname.split('/')[2] || '1';
function getUrlParam(name) {
var search = window.location.search;
var matched = search
.slice(1)
.match(new RegExp('(^|&)' + name + '=([^&]*)(&|$)', 'i'));
return search.length ? matched && matched[2] : null;
}
CFG.appID = '${APPID}';
if (!getUrlParam("appID")) {
// alert("【警告】检测到活动url中没有appID参数\n缺少该参数会导致埋点、分享、app信息获取错误。")
}
</script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
<script type="module" src="/src/App.tsx"></script>
</body>
</html>
import CryptoJS from 'crypto-js';
const { mode, pad, enc, AES } = CryptoJS;
const getOptions = (iv: string) => {
return {
iv: enc.Utf8.parse(iv),
mode: mode.CBC,
padding: pad.ZeroPadding,
};
}
/** 加密 */
export function AESEncrypt(str: string, key: string, iv: string) {
const options = getOptions(iv);
return AES.encrypt(str, enc.Utf8.parse(key), options).toString();
}
/** 解密 */
export function AESDecrypt(cipherText: string, key: string, iv: string) {
const options = getOptions(iv);
return AES.decrypt(cipherText, enc.Utf8.parse(key), options)
.toString(enc.Utf8)
.trim()
.replace(//g, '')
.replace(//g, '')
.replace(/\v/g, '')
.replace(/\x00/g, '');
}
import { AESEncrypt } from "./Crypto";
export default [
{
url: '/projectRule.query',
method: 'get',
response: ({query}) => {
return {
"data": "<p>以下是游戏规则:手速要快,点击红包雨。。333。。。。。。。。。。。。。。。。。。。。11111111111111sadasdadadsad5555555557777777777799999999999911111111111111111111111222222222222222222222222222222222222222222222222222222222222222333333333333333333333333333333333333333333333333333333333333311111111111111111111111111111111111111111111111111111111111111122222222222222222222222222222222222222222222222222222222222222233333333333333333333333333333333333333333333333333333333333331111111111111111111111111111111111111111111111111111111111111112222222222222222222222222222222222222222222222222222222222222223333333333333333333333333333333333333333333333333333333333333</p>",
"success": true
}
},
},
{
url: '/coop_frontVariable.query',
method: 'get',
response: ({query}) => {
return {
"success": true,
"message": "报错了~",
"code": null,
"data": {
"test_config_01": "前端配置项测试",
"test_config_03": null,
"test_config_02": "111"
}
}
},
},
{
url: '/spring/start.do',
method: 'get',
response: ({query}) => {
return {
"code": "code",
"success": true,
"message": "message",
"timeStamp": Date.now(),
"data": AESEncrypt(JSON.stringify({
"startId": "officia",
"countDown": 30
}), "1696BD3E5BB915A0", "cDOiBC1n2QrkAY2P"),
}
},
},
]
export default [
{
url: '/log/click',
method: 'get',
response: 1,
},
]
{
"name": "playground",
"name": "@grace/playground",
"private": true,
"version": "0.0.1",
"type": "module",
......@@ -10,22 +10,49 @@
"preview": "vite preview"
},
"dependencies": {
"@csstools/normalize.css": "^12.1.1",
"@grace/built-in": "workspace:^",
"@grace/svgaplayer": "workspace:^",
"@spark/ui": "^2.1.28",
"@types/node": "^22.7.6",
"axios": "^1.7.7",
"chalk": "^5.3.0",
"crypto-js": "^4.2.0",
"duiba-utils": "^2.0.2",
"intersection-observer": "^0.12.2",
"less": "^4.2.0",
"mobx": "^6.13.1",
"mobx-react": "^9.1.1",
"progress": "^2.0.3",
"react": "^18.3.1",
"react-dom": "^18.3.1"
"react-dom": "^18.3.1",
"spark-utils": "^1.1.2"
},
"devDependencies": {
"@eslint/js": "^9.13.0",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.3",
"eslint": "^9.13.0",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.13",
"globals": "^15.11.0",
"typescript": "~5.6.2",
"typescript-eslint": "^8.10.0",
"vite": "^5.4.9"
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/plugin-proposal-decorators": "^7.24.7",
"@eslint/js": "^9.9.0",
"@types/ali-oss": "^6.16.11",
"@types/crypto-js": "^4.2.2",
"@types/postcss-pxtorem": "^6.0.3",
"@types/progress": "^2.0.7",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-legacy": "^5.4.2",
"@vitejs/plugin-react": "^4.3.1",
"ali-oss": "^6.21.0",
"autoprefixer": "^10.4.20",
"dotenv": "^16.4.5",
"eslint": "^9.9.0",
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
"eslint-plugin-react-refresh": "^0.4.9",
"globals": "^15.9.0",
"mockjs": "^1.1.0",
"postcss-pxtorem": "^6.1.0",
"terser": "^5.36.0",
"typescript": "5.5.4",
"typescript-eslint": "^8.0.1",
"vite": "^5.4.6",
"vite-plugin-mock": "^3.0.2"
}
}
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
\ No newline at end of file
......@@ -30,3 +30,13 @@ body {
position: relative;
overflow: hidden;
}
.console {
width: 100%;
height: 100%;
}
.testBtn{
width: 750px;
height: 100px;
}
\ No newline at end of file
.svgaTest {
background: rgba(0, 0, 0, 0.5);
width: 300px;
height: 100px;
position: absolute;
}
.svgaTest2 {
background: rgba(0, 0, 0, 0.5);
width: 300px;
height: 100px;
position: absolute;
left: 0;
top: 110px;
}
.svgaTest3 {
background: rgba(0, 0, 0, 0.5);
width: 300px;
height: 100px;
position: absolute;
left: 0;
top: 220px;
}
.button {
margin: 5px;
padding: 10px;
border: 1px solid black;
border-radius: 15px;
display: flex;
align-items: center;
justify-content: center;
}
.container {
position: absolute;
left: 0;
top: 800px;
width: 100%;
display: flex;
align-items: center;
justify-content: start;
flex-wrap: wrap;
}
.play {
}
\ No newline at end of file
import { SvgaPlayer, SvgaPlayerRef } from "@grace/svgaplayer";
import React, { Component } from 'react'
import { observer } from "mobx-react";
import { createRoot } from "react-dom/client";
import store from "./store/store";
import styles from "./App.module.less";
import "./utils/checkwebp";
import svga from "./assets/svga/1红色标题.svga";
import svga2 from "./assets/svga/2输出加载页氛围.svga";
import icon from "./assets/react.svg";
import MD from "./MD"; // 埋点
import { PAGE_MAP } from "./utils/constants";
import LoadingDemo from "./pages/LoadingDemo/LoadingDemo";
import HomeDemo from "./pages/HomeDemo/HomeDemo";
import Modal from "./modal/modal";
import { useEffect, useRef } from "react";
import './App.less'
import '@csstools/normalize.css';
import API from "./api";
function App() {
MD();
const svgaRef = useRef<SvgaPlayerRef>(null);
const pageMap = {
[PAGE_MAP.LOADING_PAGE]: <LoadingDemo />,
[PAGE_MAP.HOME_PAGE]: <HomeDemo />,
};
useEffect(() => {
console.log(svgaRef.current.getPlayer());
});
@observer
class App extends Component {
const clickPlay = () => {
svgaRef.current?.start();
}
const clickStop = () => {
svgaRef.current?.stop();
}
const clickReplaceImage = () => {
svgaRef.current?.addImage("img_3929", "5555", icon, 200, 200);
async componentDidMount() {
await store.getFrontVariable();
store.initRule();
API.doJoin();
}
const clickRemoveImage = () => {
svgaRef.current?.removeImage("5555");
}
return <>
<SvgaPlayer
ref={svgaRef}
className={styles.svgaTest}
src={svga}
/>
<SvgaPlayer
className={styles.svgaTest2}
src={svga2}
/>
<SvgaPlayer
className={styles.svgaTest3}
src={svga2}
/>
<div className={styles.container}>
<div className={styles.button} onClick={clickPlay}>play</div>
<div className={styles.button} onClick={clickStop}>stop</div>
<div className={styles.button} onClick={clickReplaceImage}>replaceImage</div>
<div className={styles.button} onClick={clickRemoveImage}>removeImage</div>
</div>
render() {
const { curPage, pageData } = store;
return (
<>
{{ ...pageMap[curPage], props: { ...pageData } }}
<Modal />
</>
);
}
}
export default App
createRoot(document.getElementById('root')!).render(
<App />
);
import { logClick, logExposure, MDAuto } from "@grace/built-in";
const appId = CFG.appID;
const dcm = "202." + CFG.projectId + ".0.0";
const domain = "https://embedlog.duiba.com.cn";
const MDList = new Array(2).fill("").map((v, i) => {
return {
ele: `.md${i + 1}`,
data: {
dpm: `${appId}.110.${i + 1}.0`,
dcm,
domain,
appId,
},
once: false,
};
});
export default () =>
MDAuto({
show: MDList, // 曝光
click: MDList, // 点击
});
export function handleLogExposure(id, id2 = 0) {
logExposure({
dpm: `${appId}.110.${id}.${id2}`,
dcm,
domain,
appId,
});
}
export function handleLogClick(id, id2 = 0) {
logClick({
dpm: `${appId}.110.${id}.${id2}`,
dcm,
domain,
appId,
});
}
import { generateAPI } from "./utils"
const API = generateAPI({
/** 获取活动规则 */
getRule: 'projectRule.query',
/** 获取前端配置项 */
getFrontVariable: 'coop_frontVariable.query',
/** 参与接口 post请求 */
doJoin: {
uri: 'join.do',
method: "post"
},
/** 签到 */
doSign: {
uri: 'checkin_1/doSign.do',
withToken: true, // 携带token
},
// cookie丢失-临时保存cookie
tempSaveCookie: {
uri: "/autoLogin/tempSaveCookie",
showMsg: false,
},
// cookie丢失-重新设置cookie
resetCookie: "/autoLogin/resetCookie",
userLogin: {
uri: "userLogin.check",
showMsg: false,
},
buriedPoint: {
uri: "home/buriedPoint.do",
showMsg: false,
},
})
// console.log('======', API)
export default API
// import { Toast } from '@spark/ui'
import { isFromShare, newUser } from '../built-in/duiba-utils';
import { errorHandler } from "@/utils/errorHandler.js";
import { getPxToken } from "@/built-in/getPxToken.js";
import { Axios } from 'axios';
interface IRes {
success: boolean;
data: any;
msg: string;
code: number | string;
}
const mergeData = {
user_type: newUser ? '0' : '1',
is_from_share: isFromShare ? '0' : '1',
}
// let tempCookieId = "";
//
// export function setCookieId(cookieId) {
// tempCookieId = cookieId;
// }
export function resetBackCookie(duibaTempCookieId) {
return new Promise((resolve) => {
apiAxios.request({
url: "/autoLogin/resetCookie",
data: {
duibaTempCookieId
}
}).then((resp) => {
return resolve('success');
}, (e) => {
return resolve(e);
});
});
}
/**
* 请求方法get、post处理
* @param {*} value
* @returns
*/
function getRequestParams(value) {
if (typeof value === 'string') {
return {
uri: value,
method: 'get'
}
} else if (typeof value === 'object') {
const {
uri,
method = 'get',
showMsg = true,
headers,
withToken,
} = value;
return {
uri,
method,
headers,
withToken,
showMsg
}
} else {
console.error('getRequestParams: 传参有误');
}
}
const apiAxios = new Axios({
timeout: 10000,
});
apiAxios.interceptors.response.use(
async (resp) => {
try {
const res = JSON.parse(resp.data);
if (res.success) {
return res;
} else {
return { success: false };
}
} catch (e) {
return { success: false };
}
},
async (error) => {
console.error(error);
return { success: false };
}
);
/**
* 请求API通用处理
* @returns
* @param apiList
*/
export function generateAPI(apiList): { [key in string]: (params?, headers?) => Promise<IRes> } {
const api = {};
for (const key in apiList) {
let value = apiList[key];
if (typeof value === 'string') {
value = {
uri: value,
method: 'get'
}
}
const {
method = 'get',
uri,
headers: mHeaders,
withToken,
showMsg = true
} = value;
api[key] = async (params: any = {}, headers: any = {}) => {
// cookie丢失的问题
// 如遇跳转Cookie丢失,打开如下代码
// const duibaTempCookieId = localStorage.getItem("db_temp_cookie");
// // const duibaTempCookieId = tempCookieId;
//
// if (duibaTempCookieId) {
// localStorage.removeItem("db_temp_cookie");
// // tempCookieId = "";
//
// const res = await API.userLogin()
// .catch(async () => {
// await resetBackCookie(duibaTempCookieId);
// });
//
// if (!res || !res.success) {
// await resetBackCookie(duibaTempCookieId);
// }
// }
if (withToken) { // 是否携带token
params.token = await getPxToken()
.catch(() => {
// Toast('网络异常,请稍后再试~');
return ({ success: false, data: '' });
});
}
const mergedHeaders = { ...mHeaders, ...headers }
params = { ...params, ...mergeData };
const res: IRes = await apiAxios.request({
method,
url: uri,
headers: mergedHeaders,
data: params,
});
if (res) {
if (!res.success && showMsg) {
errorHandler(res);
}
return res;
} else {
return { success: false, data: '' };
}
}
}
return api;
}
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
\ No newline at end of file
export class CodeError {
name = 'CodeError';
code = '';
message = '';
stack: string;
payload: any;
constructor(input, message?) {
if (typeof input === 'number' || typeof input === 'string') {
this.code = input + '';
} else if (typeof input === 'object') {
if (input['code']) {
this.code = input['code'];
} else {
if (input instanceof Error) {
this.code = input['message'];
} else {
console.warn('input without code field:', JSON.stringify(input));
}
}
if (input['message']) {
this.message = input['message'];
}
if (input['payload']) {
this.payload = input['payload'];
}
}
if (message) {
this.message = message;
}
this.stack = (new Error()).stack;
if (!Error['captureStackTrace'])
this.stack = (new Error()).stack;
else
Error['captureStackTrace'](this, this.constructor);
}
}
export const Errors = {
UNKNOWN: 200001, // 未知异常
NET_ERROR: 210001, // 网络异常
// REQUEST_TIMEOUT, // 请求超时
// POLLING_TIMEOUT, // 轮询超时
// INVALID_RESPONSE, // 无效的响应体
// CALL_LIMITING, // 接口限流
GET_PX_TOKEN_FAILED: 220001, // 获取星速台token失败
};
/**
* Created by rockyl on 2019-12-10.
*/
export const queryParams: any = {};
let search = window.location.search;
try {
search = top.location.search; //尝试获取顶层的链接
} catch (e) { /* empty */ }
for (const item of search.replace('?', '').split('&')) {
const arr = item.split('=');
queryParams[arr[0]] = arr.length === 1 ? true : decodeURIComponent(arr[1]);
}
export function windowVisibility(callback?: (visible: boolean) => void) {
//设置隐藏属性和改变可见属性的事件的名称
let visibilityChange;
if (typeof document.hidden !== 'undefined') {
visibilityChange = 'visibilitychange';
} else if (typeof document['msHidden'] !== 'undefined') {
visibilityChange = 'msvisibilitychange';
} else if (typeof document['webkitHidden'] !== 'undefined') {
visibilityChange = 'webkitvisibilitychange';
}
const handleVisibilityChange = (e) => {
callback && callback(document.visibilityState == "visible");
};
document.addEventListener(
visibilityChange,
handleVisibilityChange,
false
);
}
/**
* Created by rockyl on 2020/11/30.
*/
declare const CFG: any;
/**
* Created by rockyl on 2020/9/21.
*/
export * from './net'
export * from './browser'
export * from './utils'
export * from './init'
/**
* Created by rockyl on 2020/9/21.
*/
import {queryParams, windowVisibility,} from "./browser";
import {accessLog} from "./utils";
export let appID: string, channelType: string, projectID: string, isFromShare: boolean, newUser: boolean = true;
//appID提取
if (queryParams.appID) {
appID = queryParams.appID;
}else if (CFG.appID) {
appID = CFG.appID;
}
//渠道类型提取
if (queryParams.channelType) {
channelType = queryParams.channelType;
}
//projectID提取
if (queryParams.projectID) {
projectID = queryParams.projectID;
} else {
const result = window.location.pathname.match(/\/projectx\/(.*?)\/.*?/);
if (result) {
projectID = result[1];
}
}
//是否是分享回流
isFromShare = Object.prototype.hasOwnProperty.call(window, 'isFromShare') ? window['isFromShare'] : !!queryParams.is_from_share;
/**
* 手动设置来自分享
*/
export function setFromShare() {
isFromShare = true;
}
window['setFromShare'] = setFromShare
//新用户标记提取
function getNewUser() {
if (!localStorage) {
return
}
const key = 'nu_' + appID + '_' + projectID;
const v = localStorage.getItem(key);
if (v) {
newUser = false;
} else {
localStorage.setItem(key, '1');
}
}
getNewUser();
if (window['isSharePage']) {
setTimeout(function () {
accessLog(506);
}, 100)
}
function dealPageRemainTime() {
let startTimer = new Date().getTime();
let endTimer;
windowVisibility((visibility) => {
if (visibility) {
startTimer = new Date().getTime();
//console.log('starttimer', startTimer)
} else {
endTimer = new Date().getTime();
//console.log('endTimer', endTimer);
sendData();
}
})
const sendData = () => {
const t0 = endTimer - startTimer;
//console.log('duration', t0);
accessLog(156, {
remain: t0,
});
};
document.body['onbeforeunload'] = () => {
endTimer = new Date().getTime();
return sendData();
}
}
if (!window['stop_report_page_remain_time']) {
dealPageRemainTime();
}
/**
* Created by rockyl on 2019-11-22.
*/
import { injectProp, obj2query } from "./utils";
/**
* http请求
* @param url
* @param method
* @param params
* @param type
* @param headers
*/
export function httpRequest(url: string, method: string = 'get', params?: any, type: 'text' | 'json' | 'jsonp' = 'text', headers?) {
let openUrl = url.indexOf('blob') === 0 ? url : urlJoin(url, '__ts__=' + Date.now());
let mParams = {};
injectProp(mParams, params);
return new Promise((resolve, reject) => {
let mHeaders = {
'Content-Type': 'application/x-www-form-urlencoded',
};
injectProp(mHeaders, headers);
let request = { url, method, params: mParams, type, headers: mHeaders };
const mock = window['mock'];
let mockRule;
if (mock) {
mock(request, (rule) => {
mockRule = rule;
if (!mockRule) {
doRequest(request, resolve, reject);
}
}, resolve, reject);
} else {
request.url = openUrl;
doRequest(request, resolve, reject);
}
});
}
function doProxyRequest(payload, resolve, reject) {
let proxyWindow = window['proxy_window'];
window.addEventListener('message', onMessage, false);
proxyWindow.postMessage(JSON.stringify({
action: 'http-request-proxy',
payload: payload,
}), '*');
function onMessage(event) {
window.removeEventListener('message', onMessage);
try {
let data = JSON.parse(event.data);
console.log('onMessage', event.data);
switch (data.action) {
case 'http-request-proxy-resolve':
resolve(data.payload);
break;
case 'http-request-proxy-reject':
reject(data.payload);
break;
}
} catch (e) {
}
}
}
function doXhrRequest({ url, method, params, type, headers }, resolve, reject) {
let xhr;
if (window["XMLHttpRequest"]) {
xhr = new XMLHttpRequest();
} else if (window["ActiveXObject"]) {
xhr = new window["ActiveXObject"]();
} else {
console.log('no xhr');
}
if (xhr != null) {
const isGet = method.toUpperCase() === 'GET';
const queryStr = obj2query(params);
let openUrl = url;
if (openUrl.indexOf('projectx') == 0) {
openUrl = '/' + openUrl;
}
if (isGet) {
openUrl = urlJoin(openUrl, queryStr);
}
xhr.open(method, openUrl, true);
for (let key in headers) {
xhr.setRequestHeader(key, headers[key]);
}
xhr.responseType = type;
xhr.onreadystatechange = () => {
if (xhr.readyState == 4 && xhr.status == 200) {
resolve(xhr.response)
}
};
xhr.onerror = (reason): void => {
reject(reason)
};
xhr.onloadend = (): void => {
if (xhr.status == 404) {
reject(url + ' 404 (Not Found)')
}
};
if (isGet) {
xhr.send();
} else {
xhr.send(queryStr);
}
}
}
function doRequest(payload, resolve, reject) {
if (window['proxy_window'] && payload.url.indexOf('blob') !== 0) {
doProxyRequest(payload, function (p) {
resolve(p);
}, reject);
} else {
doXhrRequest(payload, resolve, reject);
}
}
export function urlJoin(url, query) {
if (query) {
url += url.indexOf('?') < 0 ? '?' : '';
url += url[url.length - 1] === '?' ? '' : '&';
return url + query;
} else {
return url;
}
}
/**
* Created by rockyl on 2020/9/21.
*/
import {httpRequest} from "./net";
import {appID, projectID} from "./init";
export function cleanNewUser() {
if(!localStorage) return;
let key = 'nu_' + appID + '_' + projectID;
localStorage.removeItem(key);
}
export function accessLog(pageBizId, params?) {
let p = {
pageBizId,
};
injectProp(p, params);
return httpRequest('buriedPoint', 'get', p);
}
/**
* 属性注入方法
* @param target 目标对象
* @param data 被注入对象
* @param callback 自定义注入方法
* @param ignoreMethod 是否忽略方法
* @param ignoreNull 是否忽略Null字段
*
* @return 是否有字段注入
*/
export function injectProp(target: any, data?: any, callback?: Function, ignoreMethod: boolean = true, ignoreNull: boolean = true): boolean {
if(!target || !data) {
return false;
}
let result = false;
for(let key in data) {
let value: any = data[key];
if((!ignoreMethod || typeof value != 'function') && (!ignoreNull || value != null)) {
if(callback) {
callback(target, key, value);
} else {
try {
target[key] = value;
} catch(e) {
}
}
result = true;
}
}
return result;
}
/**
* 数组查找
* @param arr
* @param predicate
*/
export function arrayFind(arr, predicate) {
if(!arr) {
return;
}
for(let i = 0, li = arr.length; i < li; i++) {
const item = arr[i];
if(predicate(item, i, arr)) {
return item;
}
}
}
/**
* 对象转query字符串
* @param obj
*/
export function obj2query(obj: any): string {
if(!obj) {
return '';
}
const arr: string[] = [];
for(const key in obj) {
arr.push(key + (key ? '=' : '') + obj[key]);
}
return arr.join('&');
}
import { CodeError } from "./common-helpers/CodeError";
import { Errors as ERRORS } from "./common-helpers/errors";
import { jsonp } from "@grace/built-in";
import { evalJsScript } from "@grace/built-in";
import axios from "axios";
const isProd = location.href.indexOf('.com.cn/projectx') >= 0;
let pxTokenKeyValid = false;
let getting = false;
const _queue = [];
/**
* 获取token
* @ctype PROCESS
* @description 获取星速台防刷token
* @returns
* token string token
* @example 一般用法
* const token = await getPxToken().catch(e=>{
* console.log('获取失败,失败原因:', e.message)
* })
* if(token){
* console.log('获取成功,token:', token)
* }
*/
export async function getPxToken() {
if (!isProd) {
console.log('[Mock] getPxToken');
return 'test_token';
}
return new Promise((resolve, reject) => {
_queue.push({resolve, reject});
setTimeout(_getPxToken, 10);
});
}
async function _getPxToken() {
if (getting) {
return;
}
if (_queue.length > 0) {
const p = _queue.shift();
try {
getting = true;
const token = await _tryGetPxToken();
p.resolve(token);
getting = false;
setTimeout(_getPxToken, 10);
} catch (e) {
getting = false;
p.reject(e);
}
}
}
async function _tryGetPxToken() {
let token, err;
for (let i = 0; i < 2; i++) {
try {
token = await _doGetPxToken();
break;
} catch (e) {
err = e;
invalidPxTokenKey();
}
}
if (token) {
return token;
} else {
throw err;
}
}
async function _doGetPxToken() {
if (!pxTokenKeyValid) {
await refreshPxTokenKey();
}
return getToken();
}
/**
* 让tokenKey失效
* @ctype PROCESS
* @description 比如活动页跳转到其他星速台页面,回来的时候就需要监听history然后让tokenKey失效
* @example 一般用法
* invalidPxTokenKey()
*/
export function invalidPxTokenKey() {
pxTokenKeyValid = false;
}
async function refreshPxTokenKey() {
if (!isProd) {
pxTokenKeyValid = true;
console.log('[Mock] refreshPxTokenKey');
return;
}
pxTokenKeyValid = false;
await jsonp('getTokenKey?_t=' + Date.now());
pxTokenKeyValid = true;
}
async function getToken() {
const resp = await axios.get('getToken');
if (resp.data.success) {
evalJsScript(resp.data);
const token = window['ohjaiohdf']();
if (token) {
return token;
}
throw new CodeError(ERRORS.GET_PX_TOKEN_FAILED, '获取token失败,请查明key是否被覆盖');
} else {
throw new CodeError(resp);
}
}
/**
* index.jsx
* Created by 还有醋v on 2022/10/10 下午3:03.
* Copyright © 2022 haiyoucuv. All rights reserved.
*/
import React, { CSSProperties, MouseEventHandler, ReactElement, useState } from "react";
interface IButtonProps {
className?: string;
onClick?: MouseEventHandler;
children?: ReactElement,
style?: CSSProperties,
}
/**
* @return ReactElement
* @constructor
* @param props
*/
export const Button = (props: IButtonProps): ReactElement => {
const {
children,
className,
onClick = () => void 0,
style = {}
} = props;
const [scale, setScale] = useState("unset");
const onTouchStart = () => {
setScale("scale(0.9,0.9)");
};
const onTouchEnd = () => {
setScale("unset");
};
const onTouchCancel = onTouchEnd;
return React.createElement("div", {
className, onTouchStart, onTouchEnd, onTouchCancel, onClick, style: {
transitionDuration: 0.5, transform: scale, ...style
}
}, children);
};
import { createRoot } from 'react-dom/client'
import './index.less'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<App />,
)
.modal-hoc-bg {
position: fixed;
top: 0;
bottom: 0;
right: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.8);
z-index: 1000;
}
\ No newline at end of file
import React, { Component } from "react";
import './modal.less';
import { observer } from 'mobx-react';
import modalStore from '../store/modal';
import { toJS } from 'mobx';
/**
* 弹窗配置
*/
export const cfg = {};
@observer
class Modal extends Component {
constructor(props) {
super(props);
}
componentDidMount() { }
render() {
const list = toJS(modalStore.popList);
if (!list.length) {
// TODO:此处根据需要自行修改
// document.body.style.overflow='auto';
return <section></section>;
}
let PopUpMulti, popUpMultiData;
if (list.length > 1 && list[list.length - 1].isMulti == true) {
const popObj2 = list[list.length - 1];
PopUpMulti = cfg[popObj2.key];
popUpMultiData = popObj2.data;
}
const popObj = list[0];
const PopUp = cfg[popObj.key];
const popData = popObj.data;
if (PopUp || PopUpMulti) {
document.body.style.overflow='hidden';
}
return <section className="modal-hoc-bg" style={{
zIndex: modalStore.popList.length ? 1000 : -1,
display: modalStore.popList.length ? 'block' : 'none'
}}>
{PopUp && <PopUp popData={popData} />}
{PopUpMulti && <section className="modal-hoc-bg" style={{
zIndex: modalStore.popList.length ? 1000 : -1,
display: modalStore.popList.length ? 'block' : 'none'
}}><PopUpMulti popData={popUpMultiData} />
</section>}
</section>;
}
}
export default Modal;
\ No newline at end of file
@import "../../res.less";
.homeDemo {
position: absolute;
.homeImg {
position: absolute;
left: 100px;
top: 400px;
width: 200px;
height: 200px;
.webpBg("LoadingPage/loadingIp.png");
}
.svga {
position: absolute;
left: 0;
top: 0;
width: 400px;
height: 400px;
}
}
import React from 'react';
import { observer } from 'mobx-react';
import './HomeDemo.less';
import { SvgaPlayer } from "@grace/svgaplayer";
import svga from "../../assets/LoadingPage/1红色标题.svga";
import png from "../../assets/LoadingPage/loadingIp.png";
@observer
class HomeDemo extends React.Component {
componentDidMount() {
}
render() {
return <div className="homeDemo md1">
<div className="homeImg md1" />
<img src={png} />
当前为活动首页
<SvgaPlayer className="svga" src={svga} />
</div>;
}
}
export default HomeDemo;
@import "../../res.less";
.loading {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
.webpBg("LoadingPage/loadingBg.jpg");
.loading-ip {
position: absolute;
left: 198px;
top: 572-153px;
width: 570px;
height: 508px;
.webpBg("LoadingPage/loadingIp.png");
}
.progressBarBg {
position: absolute;
left: 82px;
top: 898-153px;
width: 590px;
height: 30px;
border-radius: 118px;
background: #FCE4D4;
border: 2px solid #FFFFFF;
}
.progressBar {
position: absolute;
left: 84px;
top: 900-153px;
width: 590px;
height: 30px;
overflow: hidden;
border-radius: 118px;
.progressBarFill {
position: absolute;
left: 0;
top: 0;
width: 590px;
height: 30px;
.webpBg("LoadingPage/loadingFill.png")
}
}
.progressTxt {
position: absolute;
left: 246px;
top: 952-153px;
width: 256px;
height: 22px;
opacity: 1;
font-size: 24px;
font-weight: normal;
line-height: 22px;
color: #999999;
}
}
import React from "react";
import { observer } from "mobx-react";
import { PAGE_MAP } from "../../utils/constants";
import store from "../../store/store";
import { preload } from "../../utils/preload.ts";
import "./LoadingDemo.less";
@observer
class LoadingDemo extends React.Component {
state = {
curPercentage: 0
}
curPercentage = 0;
intervalId: number = 0;
componentDidMount() {
this.preloadAssetInit();
// this.jump();
}
/**
* 资源预加载
*/
preloadAssetInit = async () => {
const files = import.meta.glob("../../assets/**/*", {
import: "default",
eager: true,
});
const urls = Object.values(files) as string[];
await preload(urls, (progress, loaded, total) => {
const percentage = Math.floor(progress * 100);
this.setEvenProgress(percentage);
});
};
jump = () => {
setTimeout(() => {
store.changePage(PAGE_MAP.HOME_PAGE); // 跳转页面
}, 100);
};
/**
* 以1%匀速加载进度
* @param {*} percentage
*/
setEvenProgress = (percentage) => {
this.intervalId && clearInterval(this.intervalId);
let curPercentage = this.curPercentage;
this.intervalId = window.setInterval(() => {
if (curPercentage >= percentage) {
clearInterval(this.intervalId);
this.jump();
return;
}
curPercentage += 1;
this.curPercentage = curPercentage;
this.setState({
curPercentage,
});
}, 10);
};
render() {
const { curPercentage } = this.state;
return <div className="loading">
<div className="loading-ip" />
<div className="progressBarBg" />
<div className="progressBar">
<div className="progressBarFill" style={{
transform: `translateX(${curPercentage - 100}%)`
}} />
</div>
<span className="progressTxt">金豆正在路上...... {curPercentage}%</span>
</div>;
}
}
export default LoadingDemo;
@RES_PATH: '/src/assets/';
@webp: '?x-oss-process=image/format,webp';
.sparkBg(@value) {
background-repeat: no-repeat;
background-size: 100% 100%;
background-position: left top;
--p: url("@{RES_PATH}@{value}");
background-image: var(--p);
[duiba-webp='true'] & {
--p: url("@{RES_PATH}@{value}@{webp}");
background-image: var(--p);
}
}
.webpBg(@value) {
.sparkBg("@{value}");
}
.mask(@value) {
mask-image: url("@{RES_PATH}@{value}");
-webkit-mask-image: url("@{RES_PATH}@{value}");
mask-repeat: no-repeat;
}
// 定位-水平垂直居中
.transform_center() {
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
position: absolute;
}
// flex-水平垂直居中
.flex_center() {
display: flex;
justify-content: center;
align-items: center;
}
/* 文本过长隐藏文字并显示省略号 单行*/
.lineClamp1() {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.lineClampN(@num) {
text-overflow: -o-ellipsis-lastline;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: @num;
overflow: hidden;
}
// 按钮呼吸动效
.breathAnimation() {
animation: scale 1s linear infinite alternate;
@keyframes scale {
0% {
transform: scale(1);
}
100% {
transform: scale(1.12);
}
}
}
// 弹窗居中缩放动效
// 注意 加该样式的元素居中方式不能用translate(-50%, -50%)
// 可以用margin-left、margin-top为负的宽度、高度的一半
.popupCenterShow() {
animation: centerShowAni 300ms ease-out;
@keyframes centerShowAni {
0% {
transform: scale(0);
}
66.7% {
transform: scale(1.1);
}
100% {
transform: scale(1);
}
}
}
// 渐入动画
.fade-in {
opacity: 0;
animation: fadeIn ease-in 1;
animation-fill-mode: forwards;
animation-duration: 1s;
}
@keyframes fadeIn {
0% {
opacity: 0;
}
70% {
opacity: 0;
}
100% {
opacity: 1;
}
}
// 隐藏滚动条
.hideScrollbar() {
scrollbar-width: none;
/* firefox */
-ms-overflow-style: none;
/* IE 10+ */
&::-webkit-scrollbar {
display: none;
/* Chrome Safari */
}
}
.ruleStyle {
overflow-x: hidden;
overflow-y: scroll;
word-break: break-all;
}
// 上滑弹窗动画
.topPop_move {
animation: move_top_ani 500ms ease-in-out;
@keyframes move_top_ani {
0% {
transform: translateY(100%);
}
100% {
transform: translateY(0);
}
}
}
import { makeAutoObservable } from 'mobx';
// 此处配置页面的优先级,越大优先级越高
// PopIndex:11
/**
* 弹窗优先级 可以是负数, 不写默认是10, 数值越小,层级越高
*/
const modalIndex = {}
const modalStore = makeAutoObservable({
popList: [],
/**
*
* @param {*} key 弹窗名,一般是类名的字符串
* @param {*} data 需要传递的数据,弹窗中使用 const {popData} = props; 获取
* @param {*} isMulti 是否是二级弹窗,在不关闭已有弹窗的基础上,弹出当前弹窗。注意,如果是二级弹窗,关闭时必须传key
*/
pushPop(key, data = {}, isMulti = false) {
if (this.popList.length) {
let cacheList = this.popList.slice();
cacheList.push({ key, data, isMulti });
cacheList = cacheList.sort((a, b) => ((modalIndex[b.key] ? modalIndex[b.key] : 10) - (modalIndex[a.key] ? modalIndex[a.key] : 10)))
this.popList.clear();
this.popList.push(...cacheList);
} else {
this.popList.push({ key, data, isMulti });
}
// console.log("this.popList:::",toJS(this.popList));
},
/**
*
* @param {*} key 弹窗名,一般是类名的字符串,关闭指定弹窗,若未指定,则关闭当前弹窗
*/
closePop(key) {
if (key) {
let cacheList = this.popList.slice();
cacheList = cacheList.filter(obj => (obj.key != key));
this.popList.clear();
this.popList.push(...cacheList);
} else {
this.popList.shift();
}
},
/**
* 关闭所有弹窗
*/
closePopAll() {
this.popList.clear();
}
});
export default modalStore;
import { makeAutoObservable, } from 'mobx';
import API from '../api/index';
import { PAGE_MAP } from "../utils/constants";
import { GetCurrSkinId, getCustomShareId } from "../utils/utils";
const skinId = GetCurrSkinId() || getCustomShareId();
class Store {
/** 前端开发配置 */
frontVariable = {};
/** 当前页面 */
curPage = {
// TODO 举例子,自定义页面,因为mng更新原因原数字id会对应一个新的字符串id
"5055": "sharePage",
Did1NDA0NDc: "sharePage",
myPrize: "myPrize", // TODO 举例子 新宿台奖品页
index: PAGE_MAP.LOADING_PAGE,
}[skinId] || PAGE_MAP.LOADING_PAGE;
pageData = {};
/** 场景切换 */
changePage(page, data = {}) {
this.pageData = data;
this.curPage = page;
}
ruleInfo = '';
/** 获取活动规则 */
async initRule() {
// 模拟获取远程的数据
const {data} = await API.getRule();
this.ruleInfo = data;
}
/** 获取前端配置项 */
async getFrontVariable() {
// 获取前端开发配置
const {data} = await API.getFrontVariable();
this.frontVariable = data || {};
console.log('前端开发配置', data)
}
}
const store: Store = makeAutoObservable(new Store());
export default store;
import { mode, pad, enc, AES } from 'crypto-js';
const getOptions = (iv: string) => {
return {
iv: enc.Utf8.parse(iv),
mode: mode.CBC,
padding: pad.ZeroPadding,
};
}
/** 加密 */
export function AESEncrypt(str: string, key: string, iv: string) {
const options = getOptions(iv);
return AES.encrypt(str, enc.Utf8.parse(key), options).toString();
}
/** 解密 */
export function AESDecrypt(cipherText: string, key: string, iv: string) {
const options = getOptions(iv);
return AES.decrypt(cipherText, enc.Utf8.parse(key), options)
.toString(enc.Utf8)
.trim()
.replace(//g, '')
.replace(//g, '')
.replace(/\v/g, '')
.replace(/\x00/g, '');
}
/**
* 检查是否支持.webp 格式图片
* 支持 webp CDN ?x-oss-process=image/format,webp
* 不支持 CDN ?x-oss-process=image/quality,Q_80
*/
(function () {
let lowAdr = false;
const ua = navigator.userAgent.toLowerCase()
const isAndroid = ua.indexOf('android') > -1 || ua.indexOf('adr') > -1;
if (isAndroid) {
const ver = parseFloat(ua.substr(ua.indexOf('android') + 8, 3));
lowAdr = ver < 4.4;
}
if (lowAdr && localStorage) {
delete localStorage.isWebp;
}
if (localStorage && !localStorage.isWebp) {
const img = new Image()
img.onload = function () {
if (img.width === 1 && !lowAdr) {
localStorage.isWebp = true;
document.getElementsByTagName('html')[0].setAttribute('duiba-webp', 'true')
} else {
localStorage.isWebp = '';
}
}
img.onerror = function () {
localStorage.isWebp = ''
}
img.src = 'data:image/webp;base64,UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoBAAEAAwA0JaQAA3AA/vuUAAA='
}
})();
export const webpQuery = "?x-oss-process=image/format,webp";
export function supportWebp() {
return !!localStorage?.isWebp;
}
\ No newline at end of file
/**
* 弹窗优先级 可以是负数, 不写默认是10, 数值越小,层级越高
*/
export const MODAL_INDEX = {
// rank: 1,
};
/** 网络异常默认展示 */
export const DEFAULT_NET_ERROR = '网络异常,请稍后再试';
export const PAGE_MAP = {
HOME_PAGE: "homePage",
LOADING_PAGE: "loadingPage",
};
// import { Toast } from "@spark/ui";
import { DEFAULT_NET_ERROR } from "./constants";
// 需要过滤的错误码
export const filterCode = ["600002"];
export const errMessageMap = {
1020: "活动未开始",
1021: "活动已结束",
1007: "活动太火爆了,奖品已抢完咯~",
100001: "登录过期啦,请重新登录哦~",
};
/**
* 统一错误处理
* @param e
*/
export function errorHandler(e) {
if ((e.code == 0 && e.message == "请稍后再试") || filterCode.indexOf(`${e.code}`) >= 0) return;
switch (e.code) {
default: {
const msg = errMessageMap[e.code] || e.message || DEFAULT_NET_ERROR;
// Toast(msg, 2000, { hideOthers: true });
break;
}
}
}
import { supportWebp, webpQuery } from "./checkwebp.ts";
export const svgaRegex = (/\.(svga)$/);
export const jpgRegex = (/\.(jpe?g)$/);
export const pngRegex = (/\.(png)$/);
export const webpRegex = (/\.(webp)$/);
export const gifRegex = (/\.(gif)$/);
export const imgRegex = (/\.(png|jpe?g|webp|gif)$/);
const typeRegex = (/\.([^.]+)$/);
const loaders = {
".jpg": loadImg,
".jpeg": loadImg,
".png": loadImg,
".webp": loadImg,
".gif": loadImg,
'.svga': loadSvga,
}
export async function preload(
urls: string[],
onProcess?: (progress: number, loaded: number, total: number) => void,
onComplete?: () => void
) {
const ps = [];
let loaded = 0;
const total = urls.length;
// 获取文件类型
urls.forEach((url) => {
const type = url.match(typeRegex)?.[0];
const loadFunc = loaders[type] || loadUnknown;
const p = loadFunc(url).then(() => {
loaded++;
const progress = loaded / total;
onProcess && onProcess(progress, loaded, total);
if (loaded >= total) {
onComplete && onComplete();
}
});
ps.push(p);
});
}
/**
* 加载一张图片
* @param {string} url 地址
*/
export function loadImg(url: string) {
return new Promise((resolve) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = err => {
console.warn('load', url, err);
resolve(null);
};
img.crossOrigin = 'anonymous';
img.src = url + (supportWebp() ? webpQuery : '');
});
}
export function loadSvga(url: string) {
return Promise.resolve();
}
export function loadUnknown(url: string) {
console.warn(`load unknown: ${url}`);
return Promise.resolve();
}
\ No newline at end of file
import { loadSvga } from '@spark/svgaplayer'
// import * as FYGE from 'fyge';
// import { Howl } from 'howler';
import { RES_PATH } from '../../sparkrc'
/**
* 预加载资源(/png|jpg|jpeg|svga|spi|json|mp3|wav/)
* @param {string[]} urlList 资源地址列表
* @param {number} batchNum 每批并行加载的资源个数(一般来说该数字越大整体加载速度越快,但加载前期会更卡顿)
* @param {Function} [onProgress] 加载进度回调,每加载完一个资源回调一次,入参为进度值(0,1]
* @returns {Promise} 返回一个只会resolve(loadedData)的promise,loadedData保存了所有预加载好的资源,可通过相对路径索引
* @example
* //例
* const loadedData = await PreloadAsset(urlList, 10, onProgress);
* const image = loadedData['image/fish.png'];
* const svgaData = loadedData['svga/fish.svga'];
* const spiData = loadedData['spine/fish.spi'];
* const lottieData = loadedData['lottie/fish.json'];
*/
export function preloadAsset(urlList, batchNum, onProgress) {
return new Promise((resolve) => {
/** 要加载资源总数 */
const totalNum = urlList.length;
/** 要加载的资源索引 */
let assetIndex = -1;
/** 已加载完毕的资源个数 */
let loadedNum = 0;
/** 存放加载好的数据,用地址索引 */
const loadedData = {};
/** 加载逻辑 */
const doLoad = async () => {
if (loadedNum >= totalNum) {
totalNum == 0 && onProgress && onProgress(1); // 无加载资源时,即为假loading
resolve(loadedData); // 加载完毕
} else {
assetIndex++;
if (assetIndex >= totalNum) return
const key = urlList[assetIndex];
const url = RES_PATH + urlList[assetIndex];
const result = await loadOneAsset(url);
if (!result) {
console.warn('加载异常', url);
}
loadedData[key] = result;
loadedNum++;
onProgress && onProgress(loadedNum / totalNum);
doLoad();
}
}
batchNum = batchNum || 1;
for (let index = 0; index < batchNum; index++) {
doLoad();
}
})
}
/**
* 加载一个资源
* @param {string} url 地址
*/
async function loadOneAsset(url) {
const fileType = url.split('.').pop();
switch (true) {
case (/png|jpg|jpeg/).test(fileType):
return await loadOneImg(url);
case (/svga/).test(fileType):
return await loadOneSvga(url);
// case (/spi/).test(fileType):
// return await loadOneSpi(url);
// case (/json/).test(fileType):
// return await loadOneJson(url);
// case (/mp3|wav/).test(fileType):
// return await loadOneAudio(url);
default:
console.warn('非法资源', url);
return false;
}
}
/**
* 加载一张图片
* @param {string} url 地址
*/
function loadOneImg(url) {
const isWebp = localStorage.isWebp;
return new Promise(resolve => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = err => {
console.warn('load', url, err);
resolve(false)
};
img.crossOrigin = 'Anonymous'
img.src = url + (isWebp ? '?x-oss-process=image/format,webp' : '');
})
}
/**
* 加载一个svga
* @param {string} url 地址
*/
function loadOneSvga(url) {
return new Promise(resolve => {
loadSvga(url).then((data) => resolve(data[0])).catch(err => {
console.warn('load', url, err);
resolve(false)
});
})
}
// /**
// * 加载一个spine
// * @param {string} url 地址
// */
// function loadOneSpi(url) {
// return new Promise(resolve => {
// FYGE.loadSpine(url, spineData => {
// resolve(spineData);
// }, err => {
// console.warn('load', url, err);
// resolve(false);
// })
// })
// }
// /**
// * 加载一个Json
// * @param {string} url 地址
// */
// function loadOneJson(url) {
// return new Promise(resolve => {
// FYGE.GlobalLoader.loadJson((result, res) => {
// if (result) {
// resolve(res);
// } else {
// console.warn('load fail', url);
// resolve(false);
// }
// }, url)
// })
// }
// /**
// * 加载一个音频
// * @param {string} url 地址
// */
// function loadOneAudio(url) {
// return new Promise(resolve => {
// const sound = new Howl({
// src: url,
// onload: () => resolve(sound),
// onloaderror: err => {
// console.warn('load fail', url, err);
// resolve(false);
// },
// });
// })
// }
/**
* @description: 小程序跳转
* @param {*}
* @return {*}
*/
export const miniGoUrl = (url) => {
wx.miniProgram.navigateTo({ url: url });
}
/**
* 判断是否为ios系统
*/
export function isIos() {
return navigator.userAgent.match(/iphone|ipod|ipad/gi)
}
/** 判断微信环境 */
export function isWeChat() {
const ua = window.navigator.userAgent.toLowerCase()
return ua.match(/MicroMessenger/i) == 'micromessenger'
}
This diff is collapsed.
/// <reference types="vite/client" />
declare module "*.svga" {
const src: string;
export default src;
......
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"lib": [
"ES2020",
"DOM",
"DOM.Iterable"
],
"useDefineForClassFields": true,
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "Bundler",
"experimentalDecorators": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"jsxImportSource": "react",
"strict": false,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
"noImplicitAny": false,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true,
"paths": {
"@/*": [
"./src/*"
]
}
},
"include": ["src"]
"include": [
"src"
]
}
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"lib": ["ES2023"],
"useDefineForClassFields": true,
"skipLibCheck": true,
"experimentalDecorators": true,
/* Bundler mode */
"moduleResolution": "Bundler",
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"strict": false,
"noImplicitAny": false,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
"include": [
"vite.config.ts"
]
}
import { defineConfig } from 'vite'
import { defineConfig, UserConfig } from 'vite'
import react from '@vitejs/plugin-react'
import legacy from '@vitejs/plugin-legacy'
import autoprefixer from "autoprefixer"
import postcsspxtorem from "postcss-pxtorem"
import { viteMockServe } from "vite-plugin-mock";
import DuibaPublish from "./config/DuibaPublish/DuibaPublish.ts";
import dotenv from 'dotenv';
import * as path from "node:path";
// https://vite.dev/config/
export default defineConfig({
// https://vitejs.dev/config/
export default defineConfig(({ mode }): UserConfig => {
dotenv.config({ path: [`./config/.env.global`, `./config/.env.${mode}`] });
const {
NODE_ENV,
UPLOAD_DIR, CDN_DOMAIN,
OSS_REGION, OSS_BUCKET,
OSS_ACCESS_KEY_ID, OSS_ACCESS_SECRET,
} = process.env;
// console.log(UPLOAD_DIR, NODE_ENV, CDN_DOMAIN, OSS_REGION, OSS_BUCKET, OSS_ACCESS_KEY_ID, OSS_ACCESS_SECRET)
const isDev = NODE_ENV == "development";
const versionStamp = Date.now();
const prodBase = `${CDN_DOMAIN}/${UPLOAD_DIR}/${versionStamp}/`;
return {
base: !isDev ? prodBase : "",
server: {
// port: 3001
open: false,
host: "0.0.0.0",
},
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
}
},
assetsInclude: [/\.(svga)$/],
build: {
build:{
cssTarget: 'chrome61',
},
plugins: [
......@@ -16,8 +53,44 @@ export default defineConfig({
],
},
}),
legacy({
targets: ['defaults', 'not IE 11'],
}),
viteMockServe({
// default
mockPath: 'mock',
enable: true,
}),
!isDev && DuibaPublish({
buildVersion: versionStamp,
uploadDir: UPLOAD_DIR,
region: OSS_REGION,
bucket: OSS_BUCKET,
accessKeyId: OSS_ACCESS_KEY_ID,
accessKeySecret: OSS_ACCESS_SECRET,
}),
],
css: {
postcss: {
plugins: [
autoprefixer({
overrideBrowserslist: [
"Android 4.1",
"iOS 7.1",
"Chrome > 31",
"ff > 31",
"ie >= 8",
"last 10 versions", // 所有主流浏览器最近10版本用
],
grid: true
}),
postcsspxtorem({
rootValue: 100,
propList: ["*", "!border"], // 除 border 外所有px 转 rem
selectorBlackList: [".noRem-"], // 过滤掉.noRem-开头的class,不进行rem转换
}),
],
},
preprocessorOptions: {
less: {
javascriptEnabled: true,
......@@ -27,4 +100,6 @@ export default defineConfig({
localsConvention: 'camelCase'
}
}
})
}
}
)
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment