Commit f6cac901 authored by 章志富's avatar 章志富

基本框架提交

parent 2fa83cc8
import MD from 'spark-utils/out/md/index.js';
let appId = CFG.appID;
const dcm = '202.' + CFG.projectId + '.0.0';
// const dom = CFG.channel + '.0.0.0'
const domain = '//embedlog.duiba.com.cn';
// 总埋点数据,包含所有埋点号
let rawList = [];
for (let index = 1; index <= 4; index++) {
rawList.push({
ele: `.md${index}`,
data: {
dpm: `${appId}.110.${index}.1`,
dcm,
// dom,
domain,
appId,
},
once: false,
index: index
});
}
// 不需要自动曝光埋点的索引
const noShowIndexs = [];
// 不需要自动点击埋点的索引
const noClickIndexs = []
// 曝光埋点列表
const showList = rawList.filter(v1 => !noShowIndexs.some(v2 => v2 == v1.index))
// 点击埋点列表
const clickList = rawList.filter(v1 => !noClickIndexs.some(v2 => v2 == v1.index))
// console.log('showList',showList);
// console.log('clickList',clickList);
export default () =>
MD({
show: showList, // 曝光
click: clickList // 点击
});
# yinlian_20211213
## 项目大雁文档地址
## 项目后端技术方案地址
## 项目前端技术方案地址
/*
将此文件放到project/config/scripts/assets/目录下
在package.json文件的"scripts"字段下,分别修改dev和build命令:
"dev": "node ./config/scripts/assets/generateAssetList.js && node ./config/webpack.dev.config.js"
"build": "node ./config/scripts/assets/generateAssetList.js && node ./config/scripts/assets/index.js imgmin imgup && node ./config/webpack.prod.config.js"
*/
const fs = require('fs');
/** 预加载资源文件夹正则 */
// const preloadFolderRegExp = require('../../../sparkrc.js').PRELOAD_ASSET_MATCH;
const preloadFolderRegExp = /\/(.+)\//; // 要匹配所有的资源,写/\/(.+)\//
/** 合法资源正则 */
const assetRegExp = /\.(png|jpg|jpeg|svga|spi|json|mp3|wav)$/i;
// 读资源目录
const [preloadAsset, otherAsset] = searchFileFromFolder('./src/assets', [
{
folderRegExp: preloadFolderRegExp,
fileRegExp: assetRegExp
}
]);
console.log('assetList', preloadAsset, otherAsset);
// 写资源列表json
fs.writeFileSync(
'./src/assetList.json',
JSON.stringify({
preloadAsset: preloadAsset.map(v => v.replace("./src/assets/", '')),
otherAsset: otherAsset.filter(v => assetRegExp.test(v)).map(v => v.replace("./src/assets/", '')),
})
);
/**
* 搜索文件夹里的文件
* @param {string} folderPath 文件夹地址,相对路径
* @param {object[]} ruleList 规则列表,例[{folderRegExp: /\/(index|game)\//, fileRegExp: /.(png|jpg)$/}]
* @returns {string[]} 返回文件相对路径地址
*/
function searchFileFromFolder(folderPath, ruleList) {
if (!fs.existsSync(folderPath)) {
console.warn('searchFileFromFolder', folderPath, '路径不存在');
return;
}
if (!fs.statSync(folderPath).isDirectory()) {
console.warn('searchFileFromFolder', folderPath, '不是文件夹');
return;
}
/** 最后输出的资源路径列表 */
const pathList = [];
for (let index = 0; index < ruleList.length + 1; index++) pathList.push([]);
/** 递归查找方法 */
const searchOneDir = (folderPath) => {
fs.readdirSync(folderPath).forEach(content => {
const secondaryPath = folderPath + '/' + content;
if(fs.statSync(secondaryPath).isFile()) {
// 文件匹配
let isMatched = false;
ruleList.forEach((v, i) => {
// 目录及文件匹配
if (v.folderRegExp.test(secondaryPath) && v.fileRegExp.test(secondaryPath)) {
pathList[i].push(secondaryPath);
isMatched = true;
}
})
!isMatched && pathList[pathList.length - 1].push(secondaryPath);
}else {
searchOneDir(secondaryPath);
}
});
}
searchOneDir(folderPath);
// console.warn('pathList', pathList);
return pathList;
}
const { assets } = require("spark-assets");
const args = process.argv.splice(2);
let argsObj = {
imgmin: false,
imgup: false
}
if (args.length == 1) {
argsObj.imgmin = 'imgmin' == args[0];
argsObj.imgup = 'imgup' == args[0];
} else if (args.length == 2) {
argsObj.imgmin = 'imgmin' == args[0];
argsObj.imgup = 'imgup' == args[1];
}
assets(argsObj)
\ No newline at end of file
exports.SPARK_CONFIG_DIR_KEY = ['OUTPUT_DIR', 'SOURCE_DIR', 'TEMP_DIR', 'ENTRY', 'TEMPLATE']
exports.SPARK_CONFIG = 'sparkrc.js'
//对应项目在线素材存储的cdn配置,用于迭代开发从线上拉取素材到本地
exports.SPARK_CDN_RES_CFG='sparkrescfg.json'
\ No newline at end of file
const loaderUtils = require('loader-utils');
module.exports = function (source) {
const options = loaderUtils.getOptions(this);
let result = source;
if (options.arr) {
options.arr.map(op => {
result = result.replace(op.replaceFrom, op.replaceTo);
})
} else {
result = source.replace(options.replaceFrom, options.replaceTo);
}
return result
};
const babel = require('@babel/core');
const HtmlWebpackPlugin = require("html-webpack-plugin");
class HtmlJsToES5Plugin {
process(htmlPluginData) {
return new Promise(function (resolve) {
const scriptRegExp = /<script>[\s\S]*?<\/script>/gis;
htmlPluginData.html = htmlPluginData.html.replace(scriptRegExp, function (match) {
const code = match.replace("<script>", "").replace("</script>", "");
const es5Code = babel.transform(code, { 'presets': ['@babel/preset-env'] }).code;
return `<script>${es5Code}</script>`;
});
resolve();
});
};
apply(compiler){
compiler.hooks.compilation.tap('HtmlJsToES5Plugin', (compilation) => {
HtmlWebpackPlugin.getHooks(compilation).afterTemplateExecution.tapAsync(
"HtmlJsToES5Plugin",
async (html, cb) => {
await this.process(html);
cb(null, html);
}
);
});
}
}
// exports.default = HtmlJsToES5Plugin;
module.exports = HtmlJsToES5Plugin;
// 端口是否被占用
exports.getProcessIdOnPort=function(port) {
try {
const execOptions = {
encoding: 'utf8',
stdio: [
'pipe',
'pipe',
'ignore',
],
};
return execSync('lsof -i:' + port + ' -P -t -sTCP:LISTEN', execOptions)
.split('\n')[0]
.trim();
} catch (e) {
return null;
}
}
const childProcessSync=async function(cmd, params, cwd, printLog = true) {
return new Promise((resolve, reject) => {
let proc = childProcess(cmd, params, cwd, printLog);
proc.on('close', (code) => {
if (code === 0) {
resolve(proc['logContent']);
} else {
reject(code);
}
});
});
}
const getGitBranch=async function(cwd) {
try {
const result = await childProcessSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], cwd, false);
if (!result.startsWith('fatal:')) {
return result.trim();
}
} catch (e) {
return undefined;
}
}
const getProjectNameByPackage=function() {
return require(`${process.cwd()}/package.json`).name
}
/**
* 理论上每个项目独一无二的文件夹名字-默认取分支名
* 如果当前未创建分支,取包名+日期
* (实际很多情况是直接clone老项目,包名相同,以防资源被替换,所以用日期加一下)
*/
exports.getCdnFolderName=async function() {
const branch = await getGitBranch(process.cwd());
const date = Date.now();
if (branch) {
return branch + "/" + date;
}
let foldername = getProjectNameByPackage() + "/" + date;
return foldername;
}
\ No newline at end of file
const path = require('path');
const fs = require("fs");
const { SPARK_CONFIG_DIR_KEY, SPARK_CONFIG } = require('./scripts/constant');
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const TerserPlugin = require("terser-webpack-plugin");
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
const ProgressBarPlugin = require("progress-bar-webpack-plugin");
module.exports = function (isProd) {
const appPath = process.cwd();
const sparkConfig = require(path.join(appPath, SPARK_CONFIG));
const isEslint = fs.existsSync(`${appPath}/.eslintrc.js`);
const cssReg = /\.(css|less)$/;
// 处理相对路径
SPARK_CONFIG_DIR_KEY.map((key) => {
sparkConfig[key] = path.resolve(appPath, sparkConfig[key]);
});
const stylePlugins = [
require("autoprefixer")({
overrideBrowserslist: ["> 1%", "last 2 versions", "not ie <= 8"],
})
];
if (sparkConfig.PX2REM) {
stylePlugins.push(
require("postcss-px2rem-exclude")({
remUnit: 100, // 注意算法,这是750设计稿,html的font-size按照750比例
exclude: /node_modules/i,
})
);
}
const styleLoader = (cssOptions = {}) => {
return [
{
loader: "style-loader",
},
isProd && {
loader: MiniCssExtractPlugin.loader,
options: {
esModule: false,
},
},
{
loader: "css-loader",
options: {
...cssOptions,
importLoaders: 2, // 如果遇到css里面的 @import 执行后面两个loader。 不然如果import了less,css-loader是解析不了
},
},
{
loader: "postcss-loader",
options: {
sourceMap: isProd,
plugins: stylePlugins,
},
},
{
loader: require.resolve("less-loader"),
options: {
sourceMap: isProd,
lessOptions: {
modifyVars: {
"@RES_PATH": `"${isProd ? sparkConfig.RES_PATH_PROD + '/' : sparkConfig.RES_PATH}"`,
},
}
},
},
].filter(Boolean);
};
return {
entry: sparkConfig.ENTRY,
mode: isProd ? 'production' : 'development',
devtool: isProd ? "source-map" : "cheap-module-source-map",
output: {
path: path.resolve(__dirname, sparkConfig.OUTPUT_DIR),
filename: "js/[name].js",
},
resolve: {
extensions: ['.js', '.jsx', '.json'],
alias: {
"@src": path.resolve(__dirname, sparkConfig.SOURCE_DIR),
},
},
module: {
rules: [
// 提前进行eslint, 默认从下往上,通过enforce pre提前
isEslint && {
test: /\.js|jsx/,
enforce: "pre",
loader: "eslint-loader",
options: {
cache: true,
formatter: require("eslint-friendly-formatter"),
fix: true,
failOnError: true,
configFile: `${appPath}/.eslintrc.js`,
},
include: sparkConfig.SOURCE_DIR,
},
{
test: cssReg,
use: styleLoader(),
include: sparkConfig.SOURCE_DIR,
},
{
test: /\.(js|jsx)$/,
loader: require.resolve("babel-loader"),
exclude: [path.resolve("node_modules")],
options: {
presets: [
require("@babel/preset-env").default,
require("@babel/preset-react").default
],
plugins: [
["@babel/plugin-proposal-decorators", { "legacy": true }],
["@babel/plugin-proposal-class-properties", { "loose": false }],
require("@babel/plugin-transform-runtime").default,
],
sourceType: 'unambiguous'
},
},
{
test: [/\.(jpg|jpeg|png|svg|bmp)$/, /\.(eot|woff2?|ttf|svg)$/],
loader: require.resolve("url-loader"),
options: {
name: "[path][name].[ext]", // name默认是加上hash值。这里做了更改,不让加
outputPath: "images",
limit: 10240, // url-loader处理图片默认是转成base64, 这里配置如果小于10kb转base64,否则使用file-loader打包到images文件夹下
},
},
].filter(Boolean),
},
plugins: [
isProd &&
new MiniCssExtractPlugin({
filename: "styles/[name].[hash].css",
}),
new HtmlWebpackPlugin({
template: sparkConfig.TEMPLATE,
minify: !sparkConfig.UNMINIFY_INDEX && isProd,
}),
new CleanWebpackPlugin({
// cleanOnceBeforeBuildPatterns:['**/*', 'dist'] // 这里不用写 是默认的。 路径会根据output 输出的路径去清除
}),
new ProgressBarPlugin(),
].filter(Boolean),
optimization: {
minimize: isProd,
minimizer: [
// 替换的js压缩 因为uglifyjs不支持es6语法,
new TerserPlugin({
cache: true,
sourceMap: isProd,
extractComments: false, // 提取注释
parallel: true, // 多线程
terserOptions: {
compress: {
pure_funcs: ["console.log"],
},
},
}),
// 压缩css
new OptimizeCSSAssetsPlugin({
assetNameRegExp: /\.optimize\.css$/g,
cssProcessor: require("cssnano"),
cssProcessorPluginOptions: {
preset: ["default", { discardComments: { removeAll: true } }],
},
canPrint: true,
}),
],
// 修改文件的ids的形成方式,避免单文件修改,会导致其他文件的hash值变化,影响缓存
moduleIds: "hashed",
splitChunks: {
chunks: "all",
minSize: 30000, //小于这个限制的会打包进Main.js
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10, // 优先级权重,层级 相当于z-index。 谁值越大权会按照谁的规则打包
name: "vendors",
},
},
},
// chunks 映射关系的 list单独从 app.js里提取出来
runtimeChunk: {
name: (entrypoint) => `runtime-${entrypoint.name}`,
},
},
};
}
\ No newline at end of file
const { SPARK_CONFIG } = require("./scripts/constant");
const Webpack = require("webpack");
const webpackBaseConfig = require("./webpack.common.config");
const WebpackMerge = require("webpack-merge");
const WebpackDevServer = require("webpack-dev-server");
const opn = require("opn");
const apiMocker = require('mocker-api');
const path = require('path');
const { getProcessIdOnPort } = require("./scripts/utils");
const sparkConfig = require(path.resolve(SPARK_CONFIG));
const webpackDevConfig = function () {
return {
devServer: {
useLocalIp: true,
open: false,
hot: true,
host: "0.0.0.0",
// hotOnly: true
before(app) {
if (sparkConfig.API_MOCK) {
apiMocker(app, path.resolve('./mock/index.js'), {
changeHost: true,
})
}
}
},
plugins: [
// new Webpack.WatchIgnorePlugin([/[\\/]mock[\\/]/]),
new Webpack.HotModuleReplacementPlugin()
]
};
};
const buildDev = async function (config) {
let { port } = config;
return new Promise((resolve, reject) => {
const config = WebpackMerge(webpackBaseConfig(false), webpackDevConfig());
const compiler = Webpack(config);
const devServerOptions = Object.assign({}, config.devServer);
console.log('devServerOptions', devServerOptions);
const server = new WebpackDevServer(compiler, devServerOptions);
if (getProcessIdOnPort(port)) {
reject(`端口 ${port} 已被使用`);
return;
} else {
server.listen(
port || 8088,
"0.0.0.0",
() => {
console.log(`Starting server on http://localhost:${port}`);
opn(`http://localhost:${port || 8088}`);
resolve();
},
(err) => {
if (err) console.error("server linsten err--", err);
reject();
}
);
}
});
};
const args = process.argv.splice(2);
const port = args[0] || 8088
buildDev({
port: Number(port)
})
const path = require("path");
const chalk = require("chalk");
const fs = require('fs-extra');
const Webpack = require("webpack");
const WebpackMerge = require("webpack-merge");
const webpackBaseConfig = require("./webpack.common.config");
const { uploadFiles } = require("spark-assets");
const isProd = true;
const { getCdnFolderName } = require("./scripts/utils");
const { SPARK_CONFIG } = require("./scripts/constant");
const HtmlJsToES5Plugin = require("./scripts/plugins/HtmlJsToES5Plugin");
const { DepReporter } =require('spark-log-event');
const sparkConfig = require('../sparkrc');
const ScriptExtHtmlWebpackPlugin = require("script-ext-html-webpack-plugin");
const webpackProdConfig = function (cdnFolderName, resPathProd) {
return {
output: {
publicPath: `//yun.duiba.com.cn/spark/v2/${cdnFolderName}/`,
filename: isProd ? "js/[name].[contenthash:8].js" : "js/[name].[contenthash:4].js",
},
resolveLoader: {
modules: ['node_modules', path.resolve(__dirname, './scripts/loaders')]
},
module: {
rules: [
{
test: /sparkrc\.js$/,
exclude: [path.resolve("node_modules")],
use: [
{
loader: 'replaceLoader',
options: {
arr: [
{
replaceFrom: /(MOCK_STATUS: true)|(MOCK_STATUS:true)|("MOCK_STATUS": true)|("MOCK_STATUS":true)/,
replaceTo: '"MOCK_STATUS": false'
},
{
replaceFrom: /(RES_PATH:'\/src\/assets\/')|(RES_PATH: '\/src\/assets\/')|("RES_PATH":"\/src\/assets\/")|("RES_PATH": "\/src\/assets\/")/,
replaceTo: `"RES_PATH":"${resPathProd}/"`
}
]
}
}
]
}
]
},
plugins: [
new Webpack.IgnorePlugin(/[\\/]mock[\\/]/),
new ScriptExtHtmlWebpackPlugin({
custom: {
test: /\.js$/,
attribute: 'crossorigin',
value: 'anonymous'
}
}),
new HtmlJsToES5Plugin(),
new DepReporter()
],
node: {
crypto: 'empty'
}
};
};
const buildProd = async function () {
const cdnFolderName = await getCdnFolderName();
const appPath = process.cwd();
const sparkConfig = require(path.join(appPath, SPARK_CONFIG));
const _webpackProdConfig = await webpackProdConfig(cdnFolderName, sparkConfig.RES_PATH_PROD || '');
//新增 JS_PATH_PROD 用作
let newSparkCfg = Object.assign({}, sparkConfig);
newSparkCfg['JS_PATH_PROD'] = `https://yun.duiba.com.cn/spark/v2/${cdnFolderName}/js`;
const str = `module.exports =${JSON.stringify(newSparkCfg, null, 2)}`;
fs.writeFileSync(path.join(appPath, SPARK_CONFIG), str);
return new Promise((resolve, reject) => {
const config = WebpackMerge(webpackBaseConfig(isProd), _webpackProdConfig);
const compiler = Webpack(config);
compiler.run(async (error, stats) => {
if (error) {
return reject(error);
}
console.log(
stats.toString({
chunks: false, // 使构建过程更静默无输出
colors: true, // 在控制台展示颜色
})
);
console.log(`${chalk.yellow("打包成功, 等待上传")}\n`);
// await uploadFiles(config.output.path, '', cdnFolderName);
await uploadFiles(config.output.path, '', cdnFolderName, /.map$/);
// 上传map到不同路径,避免泄漏源码
await uploadFiles(
config.output.path + "/js",
'js/map_123_map',
cdnFolderName,
/.(js|css|css\.map)$/
);
resolve();
});
});
};
buildProd();
{
"compilerOptions": {
"jsx": "react",
"experimentalDecorators": true,
"baseUrl": "./",
"paths": {
"@src/*": ["src/*"]
}
},
"exclude": [
"node_modules"
]
}
\ No newline at end of file
{"numOfComponents":1356,"numOfProject":734}
\ No newline at end of file
module.exports = {
"GET /hydPlayWay/index.do": {
"success": true,
"code": "ad officia laboris ipsum",
"data": {
"couponActivityState": 1,
"redPacketActivityState": 1,
"ddgActivityState": 1,
"couponLink": "https://www.baidu.com?id=couponLink",
"redpacketLink": "https://www.baidu.com?id=redpacketLink",
"ddgLink": "https://www.baidu.com?id=ddgLink"
}
}
}
\ No newline at end of file
module.exports = {
"GET /projectRule.query": {
"data": "<p>一、规则内容</p><p></p><p>1、除活动规则另有规定外,每次活动中,每个用户只限参加一次活动,每个用户只能中奖一次。同一手机、同一联系方式、同一IP地址、同一支付宝账户、同一身份证件、同一银行卡号、同一收货地址、同一终端设备号或其他可以合理显示为同一用户的情形,均视为同一用户。</p><p></p><p>2、用户参加活动的时间、秒杀到或拍下商品的时间及次序、支付款项的时间及次序,均以支付宝系统记录的时间为准(如有)。</p><p></p><p>3、除活动规则另有规定外,用户应自行负担因参与活动领取奖品(包括但不限于奖金、红包、集分宝、收益、实物)而产生的税费(如有)。</p><p></p><p>4、活动结束后或活动规则中约定的奖品发放时间结束后(以两者中时间较晚的为准)的10个工作日内,活动举办方未能联系上获奖用户且此期间获奖用户未向活动举办方主张奖品的,视为获奖用户放弃奖品,活动举办方无义务再为用户发放奖品。</p><p></p><p>5、活动期间,如发现有用户通过不正当手段(包括但不限于侵犯第三人合法权益、作弊、扰乱系统、实施网络攻击、恶意套现、刷信誉、批量注册、用机器注册支付宝账户、用机器模拟客户端)参加活动而有碍其他用户公平参加本次活动或有违反活动目的之行为,活动举办方有权取消该用户获奖资格或取消、追回该用户通过参加活动所获赠品或权益。如该作弊行为给活动举办方造成损失的,活动举办方保留向该用户追究赔偿的权利。</p><p></p><p>6、活动期间,任何用户不得使用任何装置、软件或程序干预或试图干预活动举办方平台的正常运作或正在该平台上进行的任何交易、活动。任何用户不得采取任何将导致不合理的庞大数据负载加诸活动举办方平台网络设备的行动。</p><p></p><p>7、活动期间,如遭遇自然灾害、网络攻击或系统故障、有权部门要求暂停、变更或终止活动等不可抗拒因素导致活动暂停、变更或停止的,活动举办方无需承担赔偿责任或进行补偿。</p><p>&nbsp</p><p>8、在参加活动之前,请您认真阅读本规则及具体活动中展示的规则,一旦您参与活动的,即视为您同意活动所涉及的所有活动规则。如您对活动规则有任何异议,请您不要继续参与活动。</p><br/><p>9、活动奖品由合作商家提供的,奖品以及服务质量由合作商家负责。奖品为实物的,活动所有奖品以实物为准,照片和奖品说明仅供参考</p>",
"success": true
}
}
\ No newline at end of file
const fs = require('fs')
let mockData = {};
fs.readdirSync(__dirname + '/common').forEach(v => {
Object.assign(mockData, require('./common/' + v))
});
// console.log('mockData',mockData)
module.exports = mockData;
This diff is collapsed.
{
"name": "temp_base",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "node ./config/scripts/assets/generateAssetList.js && node ./config/webpack.dev.config.js 8109",
"imgmin": "node ./config/scripts/assets/index.js imgmin",
"imgup": "node ./config/scripts/assets/index.js imgup",
"imgminup": "node ./config/scripts/assets/index.js imgmin imgup",
"prod": "node ./config/webpack.prod.config.js",
"build": "node ./config/scripts/assets/index.js imgmin imgup && node ./config/webpack.prod.config.js"
},
"dependencies": {
"@spark/animation": "2.0.52",
"@spark/api-base": "2.0.33",
"@spark/chary": "1.0.31",
"@spark/popup": "3.0.3",
"@spark/projectx": "2.0.13",
"@spark/share": "2.0.170",
"@spark/ui": "2.0.36",
"@spark/utils": "2.0.53",
"axios": "0.19.2",
"css-loader": "3.6.0",
"duiba-utils": "1.0.2",
"fyge": "^3.0.4",
"history": "4.10.1",
"howler": "^2.2.3",
"mobx": "6.2.0",
"mobx-react": "7.1.0",
"postcss-loader": "3.0.0",
"prettier": "2.0.5",
"qs": "6.9.4",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-redux": "7.2.4",
"react-router-dom": "5.2.1",
"redux": "4.1.1",
"redux-thunk": "2.3.0",
"spark-utils": "0.0.12",
"style-loader": "1.2.1"
},
"devDependencies": {
"@babel/core": "7.12.10",
"@babel/plugin-transform-runtime": "7.12.10",
"@babel/plugin-proposal-decorators": "7.13.15",
"@babel/preset-env": "7.12.11",
"@babel/preset-react": "7.12.10",
"autoprefixer": "9.8.6",
"babel-loader": "8.2.2",
"chalk": "4.1.0",
"clean-webpack-plugin": "3.0.0",
"eslint-loader": "4.0.2",
"fs-extra": "9.0.1",
"html-webpack-plugin": "4.5.1",
"less": "4.1.0",
"less-loader": "7.2.1",
"mini-css-extract-plugin": "1.3.4",
"mocker-api": "2.7.5",
"mockjs": "1.1.0",
"optimize-css-assets-webpack-plugin": "5.0.4",
"postcss-px2rem-exclude": "0.0.6",
"progress-bar-webpack-plugin": "2.1.0",
"spark-assets": "1.1.9",
"url-loader": "4.1.1",
"webpack": "4.43.0",
"webpack-cli": "4.3.1",
"webpack-dev-server": "3.11.0",
"webpack-merge": "4.2.2",
"spark-log-event": "1.0.4"
}
}
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<title>lc-template</title>
<script src="//yun.duiba.com.cn/spark/v2/spark.base.fz.wxpollyfill.js"></script>
<script src="//yun.duiba.com.cn/js-libs/rem/1.1.3/rem.min.js"></script>
<script src="//yun.duiba.com.cn/h5/lib/zepto.min.js"></script>
<!-- 动态域名 -->
<script src="//yun.duiba.com.cn/js-libs/dbdomain/1.0.25/dbdomain.js"></script>
<!-- 微信sdk -->
<!-- <script src="//res.wx.qq.com/open/js/jweixin-1.4.0.js"></script> -->
<!-- vconsole -->
<!-- <script src="//yun.duiba.com.cn/js-libs/vConsole/3.3.4/vconsole.min.js"></script> -->
<script>
// var vConsole = new VConsole();
// 打印当前链接
console.warn('page url', location.href);
var CFG = CFG || {};
CFG.appID = '${APPID}';
CFG.projectId = location.pathname.split('/')[2] || '1';
CFG.pageID = 'index'; // index-首页 | share-分享落地页;
CFG.shareConfig = {
title: 'title', //分享标题
content: 'content', //分享内容
url: 'url', //分享链接,见src/share.js里的处理
thumbnail: 'https://yun.duiba.com.cn/aurora/assets/72fe2b57cb9b4bcfe35e2d525c50d4b831920fd4.png', //分享缩略图
}
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
console.warn('CFG', CFG);
</script>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
module.exports ={
/** 正则表达式,匹配.src/asset目录下需要预加载资源的文件夹 */
"PRELOAD_ASSET_MATCH": /\/(test)\//,
"OUTPUT_DIR": "dist",
"SOURCE_DIR": "src",
"TEMP_DIR": "./.temp",
"ENTRY": "src/app.jsx",
"TEMPLATE": "./public/index.html",
"API_MOCK": true,
"PX2REM": true,
"IMAGE_Q1": 0.6,
"IMAGE_Q2": 0.8,
"RES_PATH": "/src/assets/",
"RES_PATH_PROD": "//yun.duiba.com.cn/spark/v2/temp_base/1624936160970"
}
\ No newline at end of file
const apiCfg = {
getRule: {
uri: `projectRule.query`,
method: "get",
// showMsg: false,
// showLoading: true,
},
hydPlayWay_index: {
uri: `hydPlayWay/index.do`,
method: "get"
},
}
export default apiCfg;
import apiCfg from './apicfg';
import { getPxToken } from "@spark/projectx";
import { callApi } from '@spark/api-base'
import { isFromShare, newUser } from 'duiba-utils';
import { Loading } from '@spark/ui'
import { Global } from '../global.js'
let mergeData = {
user_type: newUser ? '0' : '1',
is_from_share: isFromShare ? '0' : '1',
}
const apiList = {
...apiCfg
}
const API = generateAPI(apiList);
export default API;
function getRequestParams(value) {
if (typeof value === 'string') {
return {
uri: value,
method: 'get'
}
} else if (typeof value === 'object') {
const { uri, method = 'get', headers, withToken, secret, secretKey, contentType = 'form', showLoading = false, showMsg = true } = value;
return {
uri,
method,
headers,
withToken,//是否传入token
secret,
secretKey,
contentType,
showLoading,// 是否展示loading
showMsg,// 是否自动处理success:false并抛出msg
}
} else {
console.error('getRequestParams: 传参有误');
}
}
export function generateAPI(apiList) {
const api = {};
let isTokenRequestResponded = true;//用于标记带token的接口是否响应完毕
for (let key in apiList) {
let value = apiList[key];
let { method, uri, headers: mHeaders, withToken, secret, secretKey, contentType, showLoading, showMsg } = getRequestParams(value);
api[key] = async (params = {}, headers) => {
showLoading && Loading.show();// 根据接口配置showLoading展示loading
let token;
if (withToken) {
if (isTokenRequestResponded) {//上一个带token的接口响应完毕,则把新的带token的接口请求标记为尚未响应
isTokenRequestResponded = false;
} else {//上一个带token的接口尚未响应完毕,则停止新的带token的接口的请求
console.warn('上一个带token的接口尚未响应完毕,已停止新的带token的接口的请求,原因可能是:1.接口防连点未做好 2.弱网环境(延迟比防连点时长还高)的连续请求')
return
}
try {
token = await getPxToken();
} catch (e) {
isTokenRequestResponded = true;// 网络异常时置为true,保证网络恢复时能正常请求带token的接口
Global.Itoast('网络异常!')
showLoading && Loading.hide();// 根据接口配置showLoading关闭loading
return;
}
}
let mergedHeaders = { ...mHeaders, ...headers }
if (withToken && token) {
params.token = token;
}
params = { ...params, ...mergeData };
const result = await callApi(uri, params, method, mergedHeaders, false, secret, secretKey, contentType)
.catch(e => {
// 捕获网络异常
Global.Itoast('网络异常~')
});
showLoading && Loading.hide();// 根据接口配置showLoading关闭loading
if (withToken && token) {//带token的接口响应完毕
isTokenRequestResponded = true;
}
if (result) {// 网络正常,拿到了响应数据
if (result.success) {// success:true
return result; // return响应结果,用then拿数据
} else {// success:false
// 判断接口配置showMsg
if (showMsg) {// 自动处理,toast出后端msg,不用写catch也不会触发catch
Global.Itoast(result.message || '网络异常');
} else {// 手动处理,throw响应结果,用catch拿数据
throw result;
}
}
}
}
}
return api;
}
import React, { Component } from "react";
import ReactDOM from "react-dom";
import "./app.less";
import MD from '../MD';
import { Popup, showPop, hidePop } from '@spark/popup'
import Page_index from './pages/page_index/comp'
// import Page_share from './pages/Page_share/Page_share'
import { Global } from './global'
Global.showPop = showPop;
Global.hidePop = hidePop;
MD();
//此处为spark-cli动态生成
class App extends Component {
render() {
return (
<>
{/* 弹窗组件,写在前面,保证其后组件能立即使用其弹窗方法 */}
<Popup />
{/* 首页 */}
{CFG.pageID == 'index' && <Page_index />}
{/* 分享落地页 */}
{/* {CFG.pageID == 'share' && <Page_share />} */}
</>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
* {
margin: 0;
padding: 0;
-webkit-touch-callout: none; /*ios禁用Safari展示callout*/
-webkit-user-select: none; /* Safari */
-ms-user-select: none; /* IE 10+ and Edge */
user-select: none; /* Standard syntax */
-webkit-tap-highlight-color: rgba(0, 0, 0, 0); //防止点击出现半透明灰色背景
}
input {
//保证文字可选,否则可能影响输入框输入
-webkit-user-select: text !important; /* Safari */
-ms-user-select: text !important; /* IE 10+ and Edge */
user-select: text !important; /* Standard syntax */
}
html,
body {
font-size: 24px;
width: 100%;
height: 100%;
-webkit-text-size-adjust: 100% !important;
text-size-adjust: 100% !important;
-moz-text-size-adjust: 100% !important;
}
#root{
width: 750px;
height: 100%;
overflow: hidden;
position: fixed;
background-color: #fff;
}
// 设置元素可点击
.can_click {
pointer-events: auto;
}
// 设置元素点击穿透
.click_through {
pointer-events: none;
}
@keyframes up_down {
0% {transform: translateY(0px)}
50% {transform: translateY(-20px)}
100% {transform: translateY(0px)}
}
@keyframes rotate_light {
0% {transform: rotate(0deg)}
100% {transform: rotate(360deg)}
}
@keyframes big_small {
0% {transform: scale(1)}
50% {transform: scale(1.1)}
100% {transform: scale(1)}
}
// 弹窗由下向上弹出 animation: popup_from_bottom 250ms ease-in-out;
@keyframes popup_from_bottom {
0% {
transform: translateY(100%);
}
100% {
transform: translateY(0%);
}
}
\ No newline at end of file
{"preloadAsset":["test/btn_roll.png","test/coin.mp3","test/cow_spine.spi","test/finger.svga","test/smoke.json"],"otherAsset":[]}
\ No newline at end of file
This diff is collapsed.
'use strict';
import { Global } from '@src/global';
import * as FYGE from 'fyge';
import React, { useEffect, useState } from 'react';
import './comp.less';
export {
Comp_fyge_canvas,
addClick,
createContainer,
createTxt,
createImg,
createSvga,
createSpine,
createLottie,
}
/**
* 使用fyge引擎操作的canvas组件
* @param {object} props
* @param {Function} [props.onStageReady] stage准备好了回调,回调入参为stage实例
*/
function Comp_fyge_canvas({ onStageReady }) {
const [that] = useState({});
// 生成stage
useEffect(() => {
console.warn('Comp_fyge_canvas init');
const canvas = that.canvas;
//建舞台
const stage = new FYGE.Stage(
canvas,//canvas画布
750,//设计宽度,按设计搞给的就行
1624,//设计高度
canvas.getBoundingClientRect().width,//显示宽度,css宽度
canvas.getBoundingClientRect().height,//显示高度,css高度
FYGE.RENDERER_TYPE.WEBGL,//2d上下文
false,//舞台是否居中,false不居中左上置顶,true居中小于1624或750的视窗会上下或左右平均裁切
false,//是否定高,默认否(定宽)
window.devicePixelRatio || 1
);
//鼠标事件
const mouseEvent = stage.onMouseEvent.bind(stage);
stage.webMouseEventPreventDefault = true;
stage.webMouseEventStopPropagation = true;
canvas.addEventListener("touchstart", mouseEvent, false);
canvas.addEventListener('touchmove', mouseEvent, false);
canvas.addEventListener('touchend', mouseEvent, false);
//stage初始化事件
stage.addEventListener(FYGE.Event.INIT_STAGE, () => onStageReady?.(stage));
//每帧循环起来
const loop = () => {
FYGE.Tween.flush()
stage.flush();
that.requestID = window.requestAnimationFrame(loop);
}
loop();
return () => {
//Tween都移除,
FYGE.Tween.removeAllTweens()
//计时器取消
window.cancelAnimationFrame(that.requestID);
//舞台销毁
stage.destroy();
}
}, [])
return (
<canvas className="fyge_canvas" ref={ref => that.canvas = ref}></canvas>
);
}
/**
* 添加点击事件
* @param {*} node displayObject
* @param {Function} onClick 点击回调函数
* @param {*} data 点击事件携带数据,可通过event.target.__data获取
*/
function addClick(node, onClick, data = null) {
node.__data = data;
node.removeEventListener(FYGE.MouseEvent.CLICK, onClick);
node.addEventListener(FYGE.MouseEvent.CLICK, onClick);
}
/**
* 创建容器
* @param {object} option
*/
function createContainer({
x = 0,
y = 0,
width = 0,
height = 0,
color,
colorOpacity = 0.3,
visible = true,
onCreated,
}) {
const container = new FYGE.Container();
container.position.set(x, y);
container.width = width;
container.height = height;
container.visible = visible;
if (color) {
container.addChild(new FYGE.Graphics())
.beginFill(color, colorOpacity)
.drawRect(0, 0, width, height)
.endFill();
}
onCreated?.(container);
return container
}
/**
* 创建文字
* @param {object} option
*/
function createTxt({
text = '', //显示的文字内容
x = 0,
y = 0,
color = "#000000", //文本颜色
size = 16, //文字大小
width, //文字的宽度
height, //文字的高度
align = 'left', //文字的对齐方式
strokeColor, //文本的描边颜色
stroke, //描边的宽度
visible = true,
onCreated
}) {
const txt = new FYGE.TextField();
txt.text = text;
txt.position.set(x, y);
txt.fillColor = color;
txt.size = size;
width && (txt.textWidth = width);
height && (txt.textHeight = height);
txt.textAlign = align;
strokeColor && (txt.strokeColor = strokeColor);
stroke && (txt.stroke = stroke);
txt.visible = visible;
onCreated?.(txt);
return txt
}
/**
* 创建图片
* @param {object} option
*/
function createImg({
path,
x = 0,
y = 0,
scaleX = 1,
scaleY = 1,
visible = true,
onCreated
}) {
const img = new FYGE.Sprite(FYGE.Texture.fromImage(Global.assetData[path]));
img.position.set(x, y);
img.scaleX = scaleX;
img.scaleY = scaleY;
img.visible = visible;
onCreated?.(img);
return img
}
/**
* 创建svga
* @param {object} option
*/
function createSvga({
path,
x = 0,
y = 0,
scaleX = 1,
scaleY = 1,
autoPlay = true,
loop = 0,
onEnd,
mouseEnable = false,
visible = true,
onCreated,
}) {
const svga = new FYGE.MovieClip(Global.assetData[path]); //创建svga对象
svga.visible = visible;
svga.position.set(x, y); //设置svga位置
svga.scaleX = scaleX;
svga.scaleY = scaleY;
if (autoPlay) {
svga.startAniRange(0, svga.totalFrames, loop, () => onEnd?.());
} else {
svga.stop();
}
svga.mouseEnable = mouseEnable;
svga.mouseChildren = mouseEnable;
onCreated?.(svga);
return svga
}
/**
* 创建spine
* @param {object} option
*/
function createSpine({
path,
x = 0,
y = 0,
scaleX = 1,
scaleY = 1,
autoPlay = true,
skinName,
aniName,
loop = 0,
onEnd,
mouseEnable = false,
visible = true,
onCreated,
}) {
const spine = new FYGE.Spine(Global.assetData[path])
spine.visible = visible;
spine.position.set(x, y);
spine.scaleX = scaleX;
spine.scaleY = -scaleY;
skinName && spine.setSkin(skinName);
console.log("skinName", spine);
if (autoPlay && aniName) {
spine.animationManager.showAni(aniName, loop, () => onEnd?.());
}
spine.mouseEnable = mouseEnable;
spine.mouseChildren = mouseEnable;
onCreated?.(spine);
return spine
}
/**
* 创建lottie
* @param {object} option
*/
function createLottie({
path,
x = 0,
y = 0,
scaleX = 1,
scaleY = 1,
autoPlay = true,
loop = 0,
onEnd,
mouseEnable = false,
visible = true,
onCreated,
}) {
const lottie = new FYGE.Lottie(Global.assetData[path])
lottie.visible = visible;
lottie.position.set(x, y);
lottie.scaleX = scaleX;
lottie.scaleY = scaleY;
if (autoPlay) {
lottie.play(loop, () => onEnd?.());
}
lottie.mouseEnable = mouseEnable;
lottie.mouseChildren = mouseEnable;
onCreated?.(lottie);
return lottie
}
.fyge_canvas {
position: absolute;
width: 750px;
height: 1624px;
pointer-events: auto;
}
'use strict';
import React, { Component } from 'react';
import API from '../../api';
import './comp.less';
class Rule_for_test extends Component {
constructor(props) {
super(props);
this.state = {
rule: ''
}
}
async componentDidMount() {
API.getRule().then(rsp => {
if (rsp) {
this.setState({ rule: rsp.data });
}
})
// let rsp = await API.getRule();
// if (rsp) {
// this.setState({ rule: rsp.data });
// }
}
render() {
return (
<div className="pop_rule ">
<span onClick={()=>this.props.showPop({popName: 'RuleTest', isCenter: true, isClickBlankHide: true, zIndex: 1, maskOpacity: 0.3})} className="title ">游戏规则</span>
<div className="view">
<div className="text_rule " dangerouslySetInnerHTML={{ __html: this.state.rule }}></div>
</div>
</div>
);
}
}
export default Rule_for_test;
.pop_rule {
width: 610px;
height: 625px;
left: 70px;
top: 500px;
position: absolute;
background-color: white;
border-radius: 20px;
.title {
width: 610px;
top: 40px;
position: absolute;
font-size: 40px;
text-align: center;
color: black;
}
.view {
width: 540px;
height: 450px;
left: 49px;
top: 126px;
overflow: scroll;
position: absolute;
.text_rule {
position: absolute;
width: 522px;
font-size: 28px;
color: #333333;
word-wrap: break-word;
}
}
.btn_close {
width: 26px;
height: 26px;
right: 20px;
top: 20px;
position: absolute;
}
}
export {
SceneId
}
/** 场景id */
const SceneId = {
/** 无 */
DEFAULT: 0,
/** 主场景 */
MAIN: 1,
}
\ No newline at end of file
export { Global }
import {
Itoast,
GetUrlParams,
SendLog,
InputController,
DoEveryTime,
ProcessleftSecond,
CopyTxt,
TimerController,
PreloadAsset,
ProcessTimestamp,
SetElementStyle,
BenchAPI,
WaitTime,
OnVisibilityChange,
} from './lib/index.js'
let Global = {
Itoast,
GetUrlParams,
SendLog,
Throttle: (new InputController()).throttle_control,
DoEveryTime,
ProcessleftSecond,
CopyTxt,
TimerController,
PreloadAsset,
ProcessTimestamp,
SetElementStyle,
BenchAPI,
WaitTime,
OnVisibilityChange,
}
export { TimerController }
/**
* 定时器类
*/
class TimerController {
/** 存储标记的定时id */
signedIdMap = {}
/** 未标记的定时id列表 */
unsignedIdList = []
/**
* 定时执行一个方法
* @param {Function} fn 定时调用的方法
* @param {number} [time] 定时时间
* @param {string} [sign] 定时标记,执行时会先清除之前的同标记定时——意思是如果传入标记那么该定时只会同时存在一个
* @returns {number} timer id
*/
start = (fn, time, sign)=>{
sign && clearTimeout(sign);
let id = setTimeout(fn, time);
sign ? this.signedIdMap[sign] = id : this.unsignedIdList.push(id);
return id
}
/**
* 按标记清除定时,有则清除,没有也无副作用
* @param {string} sign 定时标记
*/
clear = (sign)=>{
this.signedIdMap[sign] && clearTimeout(this.signedIdMap[sign]);
}
/**
* 清除所有定时
*/
clearAll = ()=>{
Object.keys(this.signedIdMap).forEach(v => clearTimeout(this.signedIdMap[v]));
this.unsignedIdList.forEach(v => clearTimeout(v));
}
}
\ No newline at end of file
export { GetAndroidVersion, GetIosVersion, isWx, isWxMini }
/**
* 获取安卓版本号
* @param {string} [ua]
*/
function GetAndroidVersion(ua) {
ua = (ua || navigator.userAgent).toLowerCase();
var match = ua.match(/android\s([0-9\.]*)/);
return match ? match[1] : false;
}
/**
* 获取ios版本号
* @param {string} [ua]
*/
function GetIosVersion(ua) {
ua = (ua || navigator.userAgent).toLowerCase();
var match = ua.match(/cpu iphone os (.*?) like mac os/);
return match ? match[1] : false;
}
/**
* 判断是否是微信环境
*/
function isWx() {
return !!navigator.userAgent.match(/MicroMessenger/i)
}
/**
* 判断是否是微信小程序环境
* @example
* // 引入:
* <script type="text/javascript" src="//res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
* // 判断:
* let result = await isWxMini()
*/
function isWxMini() {
return new Promise((resolve)=>{
wx.miniProgram.getEnv((res) => {
res.miniprogram ? resolve(true) : resolve(false)
})
})
}
\ No newline at end of file
/*
将此文件放到project/config/scripts/assets/目录下
在package.json文件的"scripts"字段下,分别修改dev和build命令:
"dev": "node ./config/scripts/assets/generateAssetList.js && node ./config/webpack.dev.config.js"
"build": "node ./config/scripts/assets/generateAssetList.js && node ./config/scripts/assets/index.js imgmin imgup && node ./config/webpack.prod.config.js"
*/
const fs = require('fs');
/** 预加载资源文件夹正则,用于匹配./src/assets目录下的文件夹 */
const preloadFolderRegExp = /\/(test)\//; // 要匹配所有的资源,写/\/(.+)\//
/** 合法资源正则 */
const assetRegExp = /\.(png|jpg|jpeg|svga|spi|json|mp3|wav)$/i;
// 读资源目录
const [preloadAsset, otherAsset] = searchFileFromFolder('./src/assets', [
{
folderRegExp: preloadFolderRegExp,
fileRegExp: assetRegExp
}
]);
// console.log('assetList', preloadAsset, otherAsset);
// 写资源列表json
fs.writeFileSync(
'./src/assetList.json',
JSON.stringify({
preloadAsset: preloadAsset.map(v => v.replace("./src/assets/", '')),
otherAsset: otherAsset.filter(v => assetRegExp.test(v)).map(v => v.replace("./src/assets/", '')),
})
);
/**
* 搜索文件夹里的文件
* @param {string} folderPath 文件夹地址,相对路径
* @param {object[]} ruleList 规则列表,例[{folderRegExp: /\/(index|game)\//, fileRegExp: /.(png|jpg)$/}]
* @returns {string[]} 返回文件相对路径地址
*/
function searchFileFromFolder(folderPath, ruleList) {
if (!fs.existsSync(folderPath)) {
console.warn('searchFileFromFolder', folderPath, '路径不存在');
return;
}
if (!fs.statSync(folderPath).isDirectory()) {
console.warn('searchFileFromFolder', folderPath, '不是文件夹');
return;
}
/** 最后输出的资源路径列表 */
const pathList = [];
for (let index = 0; index < ruleList.length + 1; index++) pathList.push([]);
/** 递归查找方法 */
const searchOneDir = (folderPath) => {
fs.readdirSync(folderPath).forEach(content => {
const secondaryPath = folderPath + '/' + content;
if(fs.statSync(secondaryPath).isFile()) {
// 文件匹配
let isMatched = false;
ruleList.forEach((v, i) => {
// 目录及文件匹配
if (v.folderRegExp.test(secondaryPath) && v.fileRegExp.test(secondaryPath)) {
pathList[i].push(secondaryPath);
isMatched = true;
}
})
!isMatched && pathList[pathList.length - 1].push(secondaryPath);
}else {
searchOneDir(secondaryPath);
}
});
}
searchOneDir(folderPath);
// console.warn('pathList', pathList);
return pathList;
}
export * from './sendLog.js'
export * from './wrap.js'
export * from './kit.js'
export * from './inputController.js'
export * from './checkUa.js'
export * from './timerController.js'
export * from './preload.js'
\ No newline at end of file
export { InputController }
/**
* 输入控制类,用于节流(可防连点)和防抖控制
*/
class InputController {
/**
* 节流控制方法
* @param {string} sign 控制的标记,不同控制用不同的sign,建议写方法名
* @param {number} [time] 控制的时间,默认1500ms,为0则一直控制,直到通过相应sign来解除控制
* @returns {boolean} 要控制 = true | 不控制 = false
* @example
* //防连点:
* onClick = {() => throttle_control('doSomething1') || doSomething1()}
* //手动开关:
* onClick = {() => throttle_control('doSomething2', 0) || doSomething2()}
* //第一次点击可以正常走到doSomething2(),后续点击走不到,通过执行下面代码来解除控制:
* throttle_control['doSomething2'] = false;
*/
throttle_control = (sign, time = 1500) => {
if (this.throttle_control[sign]) {
return true;
} else {
this.throttle_control[sign] = true;
if (time) {
setTimeout(() => {
this.throttle_control[sign] = false;
}, time);
}
return false;
}
}
/**
* 节流改造方法
* @param {function} fn 要节流的方法
* @param {number} time 间隔时间,默认1500ms
* @returns {function} 带有节流功能的方法,由fn改造而来
* @example
* //把目标方法doSomething1改造成自带节流功能的方法doSomething2
* doSomething2 = throttle_reform(doSomething1);
*/
throttle_reform = (fn, time = 1500) => {
let sign = false;
return function throttleFn() {
if (!sign) {
sign = true;
fn();
setTimeout(() => {
sign = false;
}, time);
}
}
}
/**
* 防抖控制方法
* @param {string} sign 控制的标记
* @param {number} [time] 控制的时间,默认为500ms
* @example
* //input输入框onChange回调防抖:
* onChange = {async () => await debounce_control('doSomething1') || doSomething1()}
*/
debounce_control = (sign, time = 500) => {
let isRelease = false;//记录promise是否释放
return new Promise((resolve)=>{
this.debounce_control[sign] && clearTimeout(this.debounce_control[sign]);
this.debounce_control[sign] = setTimeout(() => {
resolve(false);
}, time);
setTimeout(() => {
if (!isRelease) {//未释放则释放
resolve(true);
}
}, time + 20);
})
}
/**
* 防抖改造方法
* @param {function} fn 要防抖的方法
* @param {number} time 间隔时间,默认500ms
* @returns {function} 带有防抖功能的方法,由fn改造而来
*/
debounce_reform = (fn, time = 500) => {
let timer = null
return function () {
timer && clearTimeout(timer)
timer = setTimeout(() => {
fn();
}, time)
}
}
}
\ No newline at end of file
export {
ProcessTimestamp,
ProcessleftSecond,
GetUrlParams,
SetElementStyle,
DoEveryTime,
CopyTxt,
CalcHourInterval,
GetDayIndex,
WaitTime,
OnVisibilityChange,
EaseNum,
}
/**
* 监听页面可见性变化
* @param {Function} onVisible 页面变可见回调
* @param {Function} onHidden 页面变隐藏回调
*/
function OnVisibilityChange(onVisible, onHidden) {
document.addEventListener( "visibilitychange" , () => {
document.visibilityState == "visible" && onVisible?.();
document.visibilityState == "hidden" && onHidden?.();
});
}
/**
* 等待一段时间
* @param {number} time 等待的时间ms
*/
function WaitTime(time) {
return new Promise(resolve => setTimeout(resolve, time))
}
/**
* 计算两个时间戳之间间隔的整点数
* @param {number} startTimestamp
* @param {number} endTimestamp
* @returns {number} hours
*/
function CalcHourInterval(startTimestamp, endTimestamp) {
if (startTimestamp > endTimestamp) {
console.warn('CalcHourInterval','入参有误!!! startTimestamp 应该比 endTimestamp 小');
return 0;
}
let hours = Math.floor((endTimestamp - startTimestamp) / 3600000);
if (endTimestamp % 3600000 < startTimestamp % 3600000) {
hours += 1;
}
return hours
}
/**
* 复制字符串到剪贴板
* @param {string} txt 要复制到剪贴板的字符串
* @param {Function} [success] 成功回调
* @param {Function} [fail] 失败回调
*/
function CopyTxt(txt, success, fail){
console.log('尝试复制', txt);
var textarea = document.createElement('textarea');
textarea.readOnly = 'readonly'
textarea.style.position = 'absolute'
textarea.style.left = '100%'
textarea.value = txt
document.body.appendChild(textarea);
textarea.select();
// textarea.setSelectionRange(0, textarea.value.length)
var result = document.execCommand('copy')
document.body.removeChild(textarea)
if (result) {
console.log('复制成功');
success && success()
} else {
console.log('复制失败');
fail && fail()
}
};
/**
* EaseNum的回调函数
* @callback OnEase
* @param {number} curNum 当前数字
* @param {Function} stopEase 停止缓动的方法
*/
/**
* 缓动数字(整数)
* @param {number} startNum 开始数
* @param {number} endNum 结束数
* @param {number} time 缓动时间
* @param {OnEase} onEase 缓动每步回调
* @returns {Function} 停止缓动方法
*/
function EaseNum(startNum, endNum, time, onEase) {
// console.log('EaseNum', startNum, endNum, time);
let stopfn = ()=>{};
const stopEase = () => stopfn();
if (time == 0 || startNum == endNum) {
onEase(endNum, stopEase);
} else {
let interval = 50; // 基础变化间隔时间
let perNum = (endNum - startNum) / (time / interval); // 单次增加量
if (perNum > 0) {
if (perNum < 1) {
perNum = 1;
interval = Math.floor(time / (endNum - startNum));
} else {
interval = Math.floor(interval * Math.ceil(perNum) / perNum);
perNum = Math.ceil(perNum);
}
}
if (perNum < 0) {
if (perNum > -1) {
perNum = -1;
interval = Math.floor(time / (startNum - endNum));
} else {
interval = Math.floor(interval * Math.floor(perNum) / perNum);
perNum = Math.floor(perNum);
}
}
// console.log('perNum', perNum, 'interval', interval);
let curNum = startNum;
stopfn = DoEveryTime(interval, (dt, stop) => {
stopfn = stop;
if ((perNum > 0 && (curNum >= endNum)) || (perNum < 0 && (curNum <= endNum))) {
curNum = endNum;
stop();
} else {
curNum += perNum;
}
onEase(curNum, stopEase);
})
}
return stopEase
}
/**
* DoEveryTime的回调函数
* @callback everyTimeCallback
* @param {number} dt 实际间隔时间
* @param {function} stopContinue 停止继续执行的方法
*/
/**
* 每time毫秒执行一次fn
* @param {number} time 时间间隔,毫秒
* @param {everyTimeCallback} fn 需要间隔执行的方法
* @returns 返回停止第一次执行的方法
*/
function DoEveryTime(time, fn) {
let expectedTime = Date.now();
let preTime = expectedTime;
let startTimer = setTimeout(doIt, time);
function doIt() {
expectedTime = expectedTime + time;
let nowTime = Date.now();
let offset = expectedTime - nowTime;
let nextTime = time + offset;
if (nextTime < 0) nextTime = 0;
let dt = nowTime - preTime;
preTime = nowTime
let continueTimer = setTimeout(doIt, nextTime);
let stopContinue = ()=>{continueTimer && clearTimeout(continueTimer)};
fn(dt, stopContinue);
}
let stopStart = ()=>{startTimer && clearTimeout(startTimer)}
return stopStart
}
/**
* 设置元素style
* @param {object} option 配置
* @param {HTMLElement} option.element HTMLElement
* @param {object} option.style element.style
* @param {Function} [option.onTransitionEnd] transitionend回调
* @param {Function} [option.onAnimationEnd] animationend回调
*/
function SetElementStyle({element, style, onTransitionEnd, onAnimationEnd}) {
const { transition, animation } = style;
if(transition !== undefined) element.style.transition = transition;
if(animation !== undefined) element.style.animation = animation;
setTimeout(()=>{
Object.keys(style).forEach(v => {
if (v != 'transition' && v != 'animation') {
element.style[v] = style[v];
}
});
})
if (onTransitionEnd) {
element.removeEventListener('transitionend', onTransitionEnd);
element.addEventListener('transitionend', onTransitionEnd);
}
if (onAnimationEnd) {
element.removeEventListener('animationend', onAnimationEnd);
element.addEventListener('animationend', onAnimationEnd);
}
}
/**
* 获取url中的请求参数
* @param {string} url 链接
* @returns 返回参数键值对object
*/
function GetUrlParams(url){
var paramObject = {};
var rawParamStr = url.split('?')[1];
if (rawParamStr) {
var rawKeyValues = rawParamStr.split('&');
rawKeyValues.forEach((v,i)=>{
var key = v.split('=')[0];
var value = v.split('=')[1];
paramObject[key] = decodeURIComponent(value);
})
}
return paramObject;
};
/**
* 加工时间戳
* @param {number} timestamp 时间戳
* @param {string} type 输出类型,自定义
*/
function ProcessTimestamp(timestamp, type) {
let time = new Date(+timestamp);
let yearStr = time.getFullYear();
let month = time.getMonth() + 1;
let monthStr = month > 9 ? month : '0' + month;
let date = time.getDate();
let dateStr = date > 9 ? date : '0' + date;
let hour = time.getHours();
let hourStr = hour > 9 ? hour : '0' + hour;
let minute = time.getMinutes();
let minuteStr = minute > 9 ? minute : '0' + minute;
let second = time.getSeconds();
let secondStr = second > 9 ? second : '0' + second;
switch (type) {
case '1':
return `${yearStr}-${monthStr}-${dateStr} ${hourStr}:${minuteStr}:${secondStr}`
case '2':
return `${yearStr}.${monthStr}.${dateStr} ${hourStr}:${minuteStr}`
}
}
/**
* 加工秒数,输出HH:MM:SS
* @param {number} leftSecond 剩余秒数
*/
function ProcessleftSecond(leftSecond) {
let hour = Math.floor(leftSecond / 3600);
let hourStr = hour.toString().length == 1 ? `0${hour}` : `${hour}`;
let minute = Math.floor((leftSecond % 3600) / 60);
let minuteStr = minute.toString().length == 1 ? `0${minute}` : `${minute}`;
let second = Math.ceil(leftSecond - 3600*hour - 60*minute);//不足1秒按1秒计
let secondStr = second.toString().length == 1 ? `0${second}` : `${second}`;
return `${hourStr}:${minuteStr}:${secondStr}`
}
/**
* 获取星期索引:0到6对应星期一到星期日
* @param {string|number} time 标准日期格式或时间戳
*/
function GetDayIndex(time) {
if (typeof time == 'string') {
time = time.replace(/-/g, '/');
}
let index = new Date(time).getDay() - 1;
if(index == -1) index = 6;
return index;
}
\ No newline at end of file
import { loadSvga } from '@spark/animation'
import * as FYGE from 'fyge';
import {Howl, Howler} from 'howler';
import { RES_PATH } from '../../sparkrc.js'
export { PreloadAsset }
/**
* 预加载资源(/png|jpg|jpeg|svga|spi|json|mp3|wav/)
* @param {string[]} urlList 资源地址列表
* @param {number} [batchNum] 每批并行加载的资源个数(一般来说该数字越大整体加载速度越快,但加载前期会更卡顿),默认值10
* @param {Function} [onProgress] 加载进度回调,每加载完一个资源回调一次,入参为进度值(0,1]
* @returns {Promise} 返回一个只会resolve(loadedData)的promise,loadedData保存了所有预加载好的资源,可通过相对路径索引
* @example
* //例
* const loadedData = await PreloadAsset(urlList);
* const image = loadedData['image/fish.png'];
* const svgaData = loadedData['svga/fish.svga'];
* const spiData = loadedData['spine/fish.spi'];
* const lottieData = loadedData['lottie/fish.json'];
* const audio = loadedData['audio/laugh.mp3'];
*/
function PreloadAsset(urlList, batchNum, onProgress) {
return new Promise((resolve) => {
/** 要加载资源总数 */
const totalNum = urlList.length;
/** 要加载的资源索引 */
let assetIndex = -1;
/** 已加载完毕的资源个数 */
let loadedNum = 0;
/** 存放加载好的数据,用地址索引 */
const loadedData = {};
let startLoadTime = new Date().getTime();
/** 加载逻辑 */
const doLoad = async () => {
if (loadedNum >= totalNum) {
console.log('加载完毕', new Date().getTime())
let endLoadTime = new Date().getTime();
let loadResTime = endLoadTime - startLoadTime;
loadedData['loadTime'] = loadResTime/1000;
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);
// Itoast('网络异常,请检查网络状态!');
}
loadedData[key] = result;
loadedNum++;
onProgress && onProgress(loadedNum / totalNum);
doLoad();
}
}
batchNum = batchNum || 10;
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) {
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;
})
}
/**
* 加载一个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 => {// 404时走不到?
console.warn('load', url, err);
resolve(false);
})
})
}
/**
* 加载一个Json
* @param {string} url 地址
*/
function loadOneJson(url) {
return new Promise(resolve => {
FYGE.GlobalLoader.loadJson((result, res) => {
// 404时走不到?
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);
},
});
})
}
\ No newline at end of file
export { SendLog };
/**
* 发送埋点
* @description
* dpm传后两位即可(如'appID.110.1.1',传'1.1'即可),会自动拼上window.CFG里写入appID和projectId,也可以手动传入appID和projectId
* @param {object} data 埋点参数
* @param {string} data.type 埋点类型 exposure: 曝光 | click: 点击
* @param {string} data.dpm dpm后两位
* @param {string} [data.dcm] dcm
* @param {string} [data.dom] dom
* @param {string} [data.appID] appID
* @param {string} [data.projectId] projectId
*/
function SendLog(data) {
let { type = 'click', dpm, dcm, dom, appID, projectId } = data;
const CFG = window.CFG;
appID = appID || (CFG ? CFG.appID : '');
projectId = projectId || (CFG ? CFG.projectId : '');
if (!appID || !projectId || !dpm) {
console.warn('未设置appID或projectId或dpm !!!');
return;
}
dpm = `${appID}.110.${dpm}`;
dcm = dcm || `202.${projectId}.0.0`;
let params = {
dpm,
dcm,
appId: appID,
};
if(dom) params.dom = dom;
if (type === 'click') {
jsonp('/log/click', params);
} else {
jsonp('//embedlog.duiba.com.cn/exposure/standard', params);
}
// console.log('sendLog', params);
}
/**
* 发送jsonp请求
* @param {string} url 请求地址
* @param {{}} params 请求参数
*/
function jsonp(url, params = {}) {
const src = url + '?' + getParams(params);
const scriptEl = document.createElement('script');
scriptEl.src = src;
scriptEl.onload = function () {
document.body.removeChild(scriptEl);
};
scriptEl.onerror = function () {
document.body.removeChild(scriptEl);
};
document.body.appendChild(scriptEl);
}
/**
* 对象格式转get请求参数格式,并且加上时间戳
* @param {{}} data
*/
function getParams(data = {}) {
const arr = [];
for (let key in data) {
arr.push(encodeURIComponent(key) + '=' + encodeURIComponent(data[key]));
}
//加时间戳防止浏览器取缓存的接口数据
arr.push('_=' + Date.now());
return arr.join('&');
}
import { Toast } from '@spark/ui'
import { generateAPI } from '../api/index'
export { Itoast, BenchAPI, }
// 自动添加一些wrap.js用到的样式
(function autoAddStyle() {
const ICSS = `
.self_toast_item {
border-radius: 0.2rem !important;
background-color: rgba(0,0,0,0.75) !important;
}
.self_toast_content {
padding: 0.25rem 0.6rem !important;
}
`
const style = document.createElement('style');
style.innerHTML = ICSS;
document.getElementsByTagName('head')[0].appendChild(style);
})()
/**
* Toast包装后的:优化了样式,一个toast时间内不会再出其它toast
* @param {*} content 内容
* @param {number} time 停留时间
*/
function Itoast(content, time = 1200) {
return Toast(content, time, { itemClass: 'self_toast_item', contentClass: 'self_toast_content', hideOthers: true })
}
/**
* 替补API
* @param {{}} options 请求配置
* @param {{}} [params] 请求参数
* @return {Promise} 请求结果Promise;
*/
function BenchAPI(options, params = {}) {
const API = generateAPI({ benchName: options });
return API.benchName(params);
}
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);
const PopUp = cfg[list[0]];
const _div = document.getElementById("root");
let _top = window.scrollY;
if (_div && PopUp) {
_div.style.position = "fixed";
} else {
_div && (_div.style.position = "static");
window.scrollTo(0, _top);
}
return <section className="modal-hoc-bg" style={{
zIndex: !!modalStore.popList.length ? 1000 : -1,
display:!!modalStore.popList.length?'block':'none'
}}>
{PopUp && <PopUp />}
</section>;
}
}
export default Modal;
\ No newline at end of file
.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
"use strict";
import React, { Component } from "react";
import { SceneId } from "@src/constant";
import { Global } from "@src/global";
import "./comp.less";
import Scene_test from "../scene_test/comp";
import AssetList from "../../assetList.json";
import { Loading } from "@spark/ui";
class Page_index extends Component {
state = {
sceneId: SceneId.MAIN, // 场景id
sceneData: null, // 场景参数
show: false,
};
async componentDidMount() {
Global.switchScene = this.switchScene;
Global.assetData = await Global.PreloadAsset(
AssetList.preloadAsset,
10,
(progress) => {}
);
// Global.assetData['loadTime']
console.log("111", Global.assetData["loadTime"]);
// showLoading()
setTimeout(() => {
this.setState({
show: true,
});
}, Global.assetData["loadTime"]);
}
/**
* 切换场景
* @param {number} sceneId 场景id
* @param {*} sceneData 场景参数
*/
switchScene = (sceneId, sceneData = null) => {
this.setState({ sceneId, sceneData });
};
render() {
const { sceneId, sceneData, show } = this.state;
return (
<>
{show ? <Scene_test sceneData={sceneData} /> : <>{<span>123</span>}</>}
{/* {sceneId == SceneId.MAIN && <Scene_test sceneData={sceneData} />} */}
{/* {sceneId == SceneId.MAIN && <Scene_test sceneData={sceneData} />} */}
</>
);
}
}
export default Page_index;
'use strict';
import React, { Component } from 'react';
import { Global } from '@src/global';
import { SceneId } from '@src/constant';
import Rule_for_test from "../../components/rule_for_test/comp";
import './comp.less';
import AssetList from '../../assetList.json'
import { Comp_fyge_canvas, createImg, createSvga, createLottie, createSpine } from '@src/components/comp_fyge_canvas/comp';
class Scene_test extends Component {
onStageReady = async (stage) => {
Global.assetData = await Global.PreloadAsset(AssetList.preloadAsset, 10, (progress) => {});
// console.log('Global.assetData', Global.assetData['loadTime']);
stage.addChildren(
createSpine({
path: 'test/cow_spine.spi', x: 400, y: 600, skinName: '4级', aniName: '开心', autoPlay: true, onCreated: spine => {
console.warn('spine11111=', spine);
}
})
)
}
/**
* 播放音频
* @param {object} option
*/
playAudio = ({ path, volume = 1, loop = false }) => {
const audio = Global.assetData[path];
console.warn('audio', audio);
audio.volume(volume);
audio.loop(loop);
audio.play();
return audio
}
render() {
return (
<div className="container" >
<Comp_fyge_canvas onStageReady={this.onStageReady} />
<div className="scroll_view">
<div className="for_click" onClick={() => Global.showPop({ popName: Rule_for_test, isClickBlankHide: true })}>弹窗测试</div>
</div>
</div>
);
}
}
export default Scene_test;
.container {
position: absolute;
height: 100%;
width: 100%;
background-color: gray;
.scroll_view {
position: absolute;
bottom: 0;
height: 500px;
width: 100%;
background-color: rgb(255, 255, 255);
}
.for_click {
position: relative;
height: 70px;
line-height: 70px;
width: 100%;
background-color: red;
color: white;
margin: 10px 0;
font-size: 32px;
font-weight: bold;
text-align: center;
}
}
\ No newline at end of file
@RES_PATH: '/src/assets/';
.sparkBg(@value) {
background: url("@{RES_PATH}@{value}") no-repeat top left / 100% 100%;
}
import { start, Weixin, WeixinMini, NingboBank, updateShare, callShare, showShareGuide } from '@spark/share'
export {
InitWxShare,
InitWxMiniShare,
UpdateWxShare,
InitAppShare,
CallAppShare,
ShowShareGuide
}
/**
* 设置分享动态域名链接
*/
async function setShareUrl() {
const domain = await window.dbdomain?.getDomain().catch(err => console.warn('动态域名err', err));
console.warn('动态域名', domain);
CFG.shareConfig.url = `${domain}/projectx/${CFG.projectId}/shareLanding.html?appID=${CFG.appID}`
}
/**
* 微信分享初始化
* @param {{}} shareConfig 分享配置
*/
function InitWxShare(shareConfig) {
return new Promise(async resolve => {
await setShareUrl();
start([Weixin]).then(() => {
updateShare(shareConfig);
resolve(true);
}).catch(err => {
console.warn('分享初始化失败', err);
resolve(false);
})
})
}
/**
* 微信小程序分享初始化
*/
function InitWxMiniShare(shareConfig) {
return new Promise(async resolve => {
await setShareUrl();
start([WeixinMini]).then(() => {
updateShare(shareConfig);
resolve(true);
}).catch(err => {
console.warn('分享初始化失败', err);
resolve(false);
})
})
}
/** 更新微信分享配置 */
const UpdateWxShare = updateShare;
/**分享引导蒙层 */
const ShowShareGuide = showShareGuide;
/**
* app分享初始化
*/
function InitAppShare() {
return new Promise(async resolve => {
await setShareUrl();
start([NingboBank], (way, success, payload) => {
resolve(true)
console.warn('app分享' + success ? '成功' : '失败')
})
})
}
/** 唤起app分享 */
const CallAppShare = callShare;
\ No newline at end of file
import { makeAutoObservable } from 'mobx';
import API from '../api/index';
const store = makeAutoObservable({
ruleInfo: '',
setRule(ruleInfo) {
this.ruleInfo = ruleInfo;
},
async initRule() {
// 模拟获取远程的数据
const { data } = await API.getRule();
this.setRule(data);
},
});
export default store;
import { makeAutoObservable } from 'mobx';
//此处配置页面的优先级,越大优先级越高
// PopIndex:11
const modalIndex = {
}
const modalStore = makeAutoObservable({
popList: [],
pushPop(key) {
if (this.popList.length) {
let cacheList = this.popList.slice();
cacheList.push(key);
cacheList = cacheList.sort((a, b) => ((modalIndex[b] ? modalIndex[b] : 10) - (modalIndex[a] ? modalIndex[a] : 10)))
this.popList.clear();
this.popList.push(...cacheList);
} else {
this.popList.push(key);
}
},
closePop(key) {
if (key) {
this.popList.remove(key);
} else {
this.popList.shift();
}
}
});
export default modalStore;
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