Commit ac1b9701 authored by 张九刚's avatar 张九刚

Merge branch 'dev' into 'master'

Dev

See merge request sparkprojects/20250528_FHQ1!41
parents c4af49dd 6f1612d5
File added
{
"name": "Vue 3 项目开发规范",
"version": "1.0.0",
"description": "Vue 3 Composition API 项目开发规范与最佳实践",
"rules": {
"项目结构": {
"description": "项目目录结构规范",
"rules": [
"/ 作为源代码根目录",
"components/ 存放可复用组件",
"composables/ 存放组合式函数",
"views/ 存放页面级组件",
"router/ 路由配置",
"store/ 状态管理",
"assets/ 静态资源",
"utils/ 工具函数",
"api/ 接口请求",
"pages/ 页面文件",
"static/ 静态资源",
"uni_modules/ 插件目录"
]
},
"组件开发": {
"description": "组件开发规范",
"rules": [
"使用 <script setup> 语法糖",
"组件名使用 PascalCase 命名",
"props 必须指定类型",
"使用 defineProps 和 defineEmits 声明属性和事件",
"组件文件使用 .vue 扩展名",
"组件名与文件名保持一致"
]
},
"组合式函数": {
"description": "组合式函数开发规范",
"rules": [
"使用 use 前缀命名",
"一个函数只做一件事",
"返回响应式数据和方法",
"处理异步操作时使用 try/catch",
"提供清晰的函数文档"
]
},
"状态管理": {
"description": "状态管理规范",
"rules": [
"使用 ref 管理简单状态",
"使用 reactive 管理复杂状态",
"使用 computed 处理派生状态",
"使用 watch 处理副作用",
"大型应用使用 Pinia 进行状态管理",
"避免过度使用全局状态"
]
},
"性能优化": {
"description": "性能优化规范",
"rules": [
"合理使用 v-memo",
"避免不必要的组件重渲染",
"使用 shallowRef 处理大型对象",
"及时清理事件监听器",
"使用 keep-alive 缓存组件",
"合理使用异步组件"
]
},
"代码风格": {
"description": "代码风格规范",
"rules": [
"使用 ESLint 和 Prettier",
"遵循 Vue 3 风格指南",
"使用有意义的变量命名",
"添加必要的代码注释",
"保持代码简洁清晰",
"遵循 DRY 原则"
]
},
"错误处理": {
"description": "错误处理规范",
"rules": [
"使用 try/catch 处理异步错误",
"实现全局错误处理",
"提供友好的错误提示",
"记录关键错误日志",
"处理边界情况",
"实现优雅的降级策略"
]
},
"测试规范": {
"description": "测试规范",
"rules": [
"编写单元测试",
"测试关键业务逻辑",
"模拟外部依赖",
"测试错误场景",
"保持测试代码简洁",
"定期运行测试用例"
]
},
"uni-app规范": {
"description": "uni-app 开发规范",
"rules": [
"使用条件编译处理平台差异",
"遵循 uni-app 生命周期规范",
"使用 uni-app 内置组件和API",
"合理使用 uni-app 路由管理",
"适配不同平台样式差异",
"使用 uni-app 插件市场组件"
]
},
"跨平台适配": {
"description": "跨平台开发规范",
"rules": [
"使用 rpx 作为尺寸单位",
"适配不同平台导航栏",
"处理平台特定API差异",
"使用条件编译处理平台特性",
"统一管理平台特定样式",
"处理不同平台兼容性问题"
]
},
"性能优化": {
"description": "uni-app 性能优化规范",
"rules": [
"合理使用分包加载",
"优化图片资源大小",
"使用组件按需加载",
"避免频繁的页面跳转",
"合理使用缓存机制",
"优化首屏加载速度"
]
}
},
"开发工具": {
"description": "推荐使用的开发工具",
"tools": [
"HBuilderX - 开发工具",
"Vite - 构建工具",
"ESLint - 代码检查",
"Prettier - 代码格式化",
"Vue DevTools - 调试工具",
"Vitest - 单元测试"
]
},
"最佳实践": {
"description": "Vue 3 开发最佳实践",
"practices": [
"使用 Composition API 组织代码",
"实现响应式数据管理",
"优化组件性能",
"保持代码可维护性",
"遵循 Vue 3 官方推荐",
"遵循 uni-app 开发规范",
"做好跨平台适配",
"注重性能优化"
]
}
}
\ 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
# 忽略eslint校验路径,例如:
# src/libs/@spark
\ No newline at end of file
module.exports = {
parser: '@babel/eslint-parser',
env: {
browser: true,
es6: true,
node: true,
},
globals: {
CFG: true,
wx: true,
FYGE: true,
SPARK_ESLINT_PLUGIN: true,
remScale: true,
},
plugins: ['html', 'react', '@spark/best-practices', '@spark/security'],
extends: ['eslint:recommended', 'plugin:react/recommended'],
settings: {
react: {
version: 'detect',
},
},
parserOptions: {
sourceType: 'module',
ecmaVersion: 7,
ecmaFeatures: {
experimentalObjectRestSpread: true,
jsx: true,
},
babelOptions: {
configFile: './node_modules/@spark/code-inspector/static/babel.config.js',
},
},
rules: {
'no-undef': 'error',
'no-unused-vars': ['error', { vars: 'all', args: 'after-used', argsIgnorePattern: '^_', varsIgnorePattern: '^_', ignoreRestSiblings: true }],
'no-dupe-keys': 'error',
'no-fallthrough': 'error',
'no-global-assign': 'error',
'no-implied-eval': 'error',
'no-self-assign': 'error',
'no-self-compare': 'error',
'no-sequences': 'error',
'no-unused-expressions': ['error', { allowShortCircuit: true, allowTernary: true, allowTaggedTemplates: true }],
'no-useless-escape': 'error',
'no-empty-pattern': 'error',
'no-empty-function': ['error', { allow: ['arrowFunctions', 'functions', 'methods'] }],
'no-var': 'error',
'no-dupe-class-members': 'error',
'no-unsafe-optional-chaining': 'error',
'no-const-assign': 'error',
'no-empty': ['error', { allowEmptyCatch: true }],
'prefer-const': 'warn',
'no-extra-boolean-cast': 'warn',
'no-mixed-spaces-and-tabs': 'warn',
'no-alert': 'warn',
'no-new-wrappers': 'warn',
'no-useless-concat': 'warn',
'no-useless-return': 'warn',
'prefer-promise-reject-errors': ['warn', { allowEmptyReject: true }],
'spaced-comment': 'warn',
'react/prop-types': 'off',
'react/display-name': 'off',
'react/jsx-pascal-case': 'error',
'jsx-quotes': 'warn',
// 'react/jsx-tag-spacing': 'error',
'react/require-resnder-return': 'error',
'semi': [1]
},
overrides: [
{
files: ['public/**/*.html'],
rules: {
'no-var': 'off',
'@spark/security/third-party-whitelist': 'error',
'@spark/best-practices/no-url-in-js': 'error',
'@spark/best-practices/no-arrow-function': 'error',
'@spark/best-practices/no-es6-variable-declaration': 'error',
},
},
{
files: ['src/**/*.{js,jsx}'],
rules: {
'@spark/best-practices/no-url-in-js': 'error',
},
},
],
};
.DS_Store
node_modules/
___cache/
__cache/
coverage/
npm-debug.log
selenium-debug.log
.idea
.builds
.project
.vscode
yarn-error.log
.yarn
.package-lock
yarn.lock
.cache
packages/**/package-lock.json
released
output.js
output.js.map
.psd
.psb
#src/assets/
\ No newline at end of file
unpackage/
dist/
registry = http://npm.dui88.com
\ No newline at end of file
module.exports = {
semi: true, // 结尾加分号
singleQuote: false, // 使用单引号
jsxSingleQuote: false, // jsx中使用单引号
bracketSpacing: true, // 括号和参数之间有空格
jsxBracketSameLine: true, // 标签属性较多时,标签箭头>另起一行
quoteProps: 'as-needed', // 属性加引号需要加时再加
printWidth: 120, // 每行字符个数
};
registry "http://npm.dui88.com"
\ No newline at end of file
<script>
export default {
onLaunch: function () {
console.log("App Launch");
},
onShow: function () {
console.log("App Show");
},
onHide: function () {
console.log("App Hide");
},
};
</script>
<style lang="less">
@import '/wxcomponents/vant/common/index.wxss';
/*每个页面公共css */
::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
color: transparent;
}
::-ms-scrollbar {
display: none;
width: 0;
height: 0;
color: transparent;
}
::-moz-scrollbar {
display: none;
width: 0;
height: 0;
color: transparent;
}
</style>
import MD from 'spark-utils/out/md/index.js';
import { logClick, logExposure } from "@spark/utils/src-js/md";
let appId = CFG.appID;
const dcm = '202.' + CFG.projectId + '.0.0';
const domain = '//embedlog.duiba.com.cn';
let MDList = [
{
ele: `.test-md1`,
data: {
dpm: `${appId}.110.5.1`,
dcm,
domain,
appId
},
once: false
}
];
export default () =>
MD({
show: MDList, // 曝光
click: MDList // 点击
});
export function handleLogExposure(id, id2 = 1) {
logExposure({
dpm: `${appId}.110.${id}.${id2}`,
dcm,
domain,
appId,
});
}
export function handleLogClick(id, id2 = 1) {
logClick({
dpm: `${appId}.110.${id}.${id2}`,
dcm,
domain,
appId,
});
}
### 注意事项
xxxxxxx
### 迭代日志
## 20230202 [大雁链接](https://www.bilibili.com)
+ haha
+ haha1
+ haha2
\ No newline at end of file
This diff is collapsed.
import requestModule from './request.js';
const {
api
} = requestModule;
export const fetchBrandJSON = () => api.get('/c/front/content',{type:'brand'});
\ No newline at end of file
import requestModule from "./request.js";
const { api } = requestModule;
/**
* 通过此接口完成手机号授权,注册新用户
* @param {*} data : {phoneEncryptedData, phoneIv, code, codeLogin}
* @returns
*/
export const uploadImage = (file64) =>
api.post(
"/c/upload/image",
{
img64: file64,
},
{
headers: {
"Content-Type": "application/json",
},
}
);
export const getHealthField = () => api.get("/c/user/getHealthField");
// API 配置文件
export const BASE_URL = 'https://api.example.com' // 替换为实际的 API 地址
// 请求超时时间
export const TIMEOUT = 10000
// 请求状态码
export const HTTP_STATUS = {
SUCCESS: 200,
CREATED: 201,
ACCEPTED: 202,
CLIENT_ERROR: 400,
AUTHENTICATE: 401,
FORBIDDEN: 403,
NOT_FOUND: 404,
SERVER_ERROR: 500,
BAD_GATEWAY: 502,
SERVICE_UNAVAILABLE: 503,
GATEWAY_TIMEOUT: 504
}
// 错误信息
export const ERROR_MESSAGE = {
NETWORK_ERROR: '网络异常,请检查您的网络连接',
TIMEOUT_ERROR: '请求超时,请稍后重试',
SERVER_ERROR: '服务器异常,请稍后重试',
UNKNOWN_ERROR: '未知错误,请稍后重试'
}
\ No newline at end of file
import requestModule from './request.js';
const {
api
} = requestModule;
/**
* 获取积分信息
* @returns
*/
export const fetchContentLibraryJSON = () => api.get('/c/front/content',{type:'contentLibrary'});
\ No newline at end of file
import requestModule from './request.js';
const {
api
} = requestModule;
/**
* 获取首页信息
* @returns
*/
export const fetchHomeInfo = () => api.get('/c/user/index');
export const fetchHomeJSON = () => api.get('/c/front/content',{type:'home'});
\ No newline at end of file
import { api } from './request'
// 用户相关接口
export const userApi = {
// 登录
demo: (data) => api.post('/demo', data),
}
import requestModule from './request.js';
const {
api
} = requestModule;
/**
* 获取积分信息
* @returns
*/
export const fetchIntegralJSON = () => api.get('/c/front/content',{type:'integral'});
\ No newline at end of file
import {
useGlobalStore
} from '../stores/global.js';
import {
storeToRefs
} from 'pinia';
import {
HTTP_STATUS
} from './config.js'
const globalStore = useGlobalStore();
const {
cuk
} = storeToRefs(globalStore);
// request.js
// 通常可以吧 baseUrl 单独放在一个 js 文件了
// const baseUrl = "http://172.16.224.178:7777/pmall";
// const baseUrl = "https://momclub-uat.feihe.com/pmall";//测试环境
const baseUrl = "https://momclub.feihe.com/pmall";//生产环境
const request = (options = {}) => {
// 在这里可以对请求头进行一些设置
// 例如:
// options.header = {
// "Content-Type": "application/x-www-form-urlencoded"
// }
return new Promise((resolve, reject) => {
uni
.request({
url: baseUrl + options.url || "",
method: options.type || "GET",
data: options.data || {},
header: {
...options.header,
cuk: cuk.value || undefined
},
})
.then((data) => {
// console.log('request data ===>', data);
if (data.statusCode !== HTTP_STATUS.SUCCESS) {
uni.showToast({
title: data.errMsg,
icon: 'none'
});
reject(data);
} else if (!data.data?.ok) {
uni.showToast({
title: data.data?.message,
icon: 'none'
});
reject(data.data);
} else {
resolve(data.data);
}
})
.catch((error) => {
reject(error);
});
});
};
const get = (url, data, options = {}) => {
options.type = "GET";
options.data = data;
options.url = url;
return request(options);
};
const post = (url, data, options = {}) => {
options.type = "POST";
options.data = data;
options.url = url;
return request(options);
};
export default {
request,
api: {
get,
post
}
};
\ No newline at end of file
import requestModule from './request.js';
const {
api
} = requestModule;
/**
* 获取历史搜索
* @returns
*/
export const historySearch = () => api.get('/c/mini/historySearch');
/**
* 清空历史搜索
* @returns
*/
export const historyClear = () => api.get('/c/mini/historyClear');
/**
* 搜索
*/
export const search = (data) => api.get('/c/mini/search',data);
\ No newline at end of file
import requestModule from './request.js';
const {
api
} = requestModule;
/**
* 获取用户信息
* @returns
*/
export const fetchUserInfo = () => api.get('/c/user/memberInfo');
/**
* 获取宝宝信息
* @returns
*/
export const fetchBabyInfo = () => api.get('/c/user/babyInfo');
export const fetchBabyInfoById = (id) => api.get('/c/user/babyInfo', { id });
export const getGestationalWeeks = (dueDate) => api.get('/c/user/calGestationalWeeks', { dueDate });
/**
* 根据wx.login接口返回的code完成登录
* @param {*} code
* @returns
*/
export const autoLoginByCode = (code) => api.get('/c/login/autologin', {
code,
});
/**
* 手机号授权,调用微信手机号快速验证组件,获取encryptedData、iv、code
* 通过此接口完成手机号授权,注册新用户
* @param {*} data : {phoneEncryptedData, phoneIv, code, codeLogin}
* @returns
*/
export const fetchAutoPhone = (data) => api.post('/c/login/authPhone', data);
/**
* 获取用户积分信息
* @returns
*/
export const fetchMemberInfo = () => api.get('/c/user/memberInfo');
/* * 更新宝宝信息
* @param {*} data
* @returns
*/
export const updateBabyInfo = (data) => api.post('/c/user/saveBaby', data);
export const fetchUserJSON = () => api.get('/c/front/content',{type:'my'});
\ No newline at end of file
This diff is collapsed.
// 主题色
@color-gold-main: #D3A358; // 尊贵金,用于主题色
@color-gold-cover: #B27C1E; // 真诚棕,用于覆盖主题色等上层或其他场景重点高亮颜色
// 辅助色
@color-gold-light: #FDE0A5; // 闪耀金,用于辅助色
@color-white-soft: #FEF7F2; // 柔和白,用于辅助色
@color-purple-energy: #A68DBB; // 能量紫,用于辅助色
@color-pink-cute: #E5C5DB; // 可爱粉,用于辅助色
// 中性色
@color-black-deep: #1D1E25; // 深黑,用于一级文字/图标等
@color-black-medium: #6F6D67; // 中黑,用于次级文字/图标等
@color-gray-medium: #DBDFE3; // 中灰,用于未选中/失效状态等
@color-gray-light: #F6F8FA; // 浅灰,用于页面背景色/卡片底色等
.hide-scrollbar {
&::-webkit-scrollbar {
display:none;
width:0;
height:0;
color:transparent;
}
}
<template>
<view class="baby-switcher-container">
<view class="baby-switcher-mask" @click.self="onClose"> </view>
<view class="baby-switcher-popup">
<view
v-for="baby in babyList"
:key="baby.id"
class="baby-item"
@click="selectBaby(baby)"
>
<text v-if="baby.babyStage == 0">备孕</text>
<text v-else-if="baby.babyStage == 1">孕中</text>
<text v-else>{{ baby.babyName || "暂无昵称" }}</text>
<view
class="selected-icon"
:style="{ backgroundColor: baby.selected ? '#d3a358' : '#E8E8E8' }"
/>
</view>
<view
v-if="babyList.length < 3"
class="baby-item add-item"
@click="onAdd"
>
<image :src="addIcon" class="add-icon" />
</view>
</view>
</view>
</template>
<script setup>
import { defineProps, defineEmits } from "vue";
const props = defineProps({
show: Boolean,
babyList: {
type: Array,
default: () => [],
},
addIcon: {
type: String,
default: "", // 传入新增icon路径
},
});
const emits = defineEmits(["close", "select", "add"]);
function onClose() {
emits("close");
}
function selectBaby(baby) {
emits("select", baby);
}
function onAdd() {
emits("add");
}
</script>
<style lang="less" scoped>
.baby-switcher-container {
touch-action: none;
}
.baby-switcher-mask {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: 1;
}
.baby-switcher-popup {
position: absolute;
left: 0;
top: 40rpx;
z-index: 2;
background: #fff;
border-radius: 16rpx;
padding: 10rpx 36rpx 22rpx 36rpx;
box-shadow: 0 0 30rpx rgba(204, 204, 204, 0.8);
box-sizing: border-box;
min-width: 247rpx;
}
.baby-item {
display: flex;
align-items: center;
justify-content: center;
padding: 20rpx 25rpx;
font-size: 26rpx;
font-weight: 500;
color: #1d1e25;
flex-wrap: nowrap;
border-bottom: 1px solid #eee;
&:nth-child(3) {
border-bottom: none;
}
text {
white-space: nowrap;
}
}
.selected-icon {
width: 12rpx;
height: 12rpx;
margin-left: 17rpx;
background-color: #e8e8e8;
border-radius: 50%;
}
.add-item {
margin-top: 10rpx;
padding-top: 20rpx;
padding: 10rpx 0;
border-bottom: none;
}
.add-icon {
width: 96rpx;
height: 36rpx;
display: block;
}
</style>
\ No newline at end of file
<template>
<view class="Layer-custom">
<!-- 遮罩层 -->
<view v-if="visible" class="layer-mask" @click="onMaskClick"></view>
<!-- 底部弹出层 -->
<view v-if="visible" class="layer-popup">
<view class="layer-panel">
<!-- 头部插槽或默认头部 -->
<slot name="header">
<view class="layer-header" v-if="!customHeader">
<text v-if="showCancel" @click="onCancel" class="layer-cancel"
>取消</text
>
<text v-if="title" class="layer-title">{{ title }}</text>
<text v-if="showConfirm" @click="onConfirm" class="layer-confirm"
>确定</text
>
</view>
</slot>
<!-- 内容插槽 -->
<view class="layer-content">
<slot></slot>
</view>
<!-- 底部插槽 -->
<slot name="footer"></slot>
</view>
</view>
</view>
</template>
<script setup>
import { ref, watch, defineEmits, defineProps } from "vue";
const props = defineProps({
modelValue: { type: Boolean, default: false },
title: { type: String, default: "" },
showCancel: { type: Boolean, default: true },
showConfirm: { type: Boolean, default: true },
maskClosable: { type: Boolean, default: true },
customHeader: { type: Boolean, default: false },
});
const emit = defineEmits([
"update:modelValue",
"confirm",
"cancel",
"open",
"close",
]);
const visible = ref(props.modelValue);
watch(
() => props.modelValue,
(val) => {
visible.value = val;
if (val) emit("open");
else emit("close");
}
);
function open() {
visible.value = true;
emit("update:modelValue", true);
emit("open");
}
function close() {
visible.value = false;
emit("update:modelValue", false);
emit("close");
}
function onConfirm() {
emit("confirm");
close();
}
function onCancel() {
emit("cancel");
close();
}
function onMaskClick() {
if (props.maskClosable) {
onCancel();
}
}
defineExpose({ open, close });
</script>
<style lang="less" scoped>
.layer-mask {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 3999;
}
.layer-popup {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 4000;
display: flex;
flex-direction: column;
align-items: center;
animation: layer-up 0.3s;
}
@keyframes layer-up {
from {
transform: translateY(100%);
}
to {
transform: translateY(0);
}
}
.layer-panel {
width: 100vw;
min-height: 20vh;
background: #f6f8fa;
border-top-left-radius: 48rpx;
border-top-right-radius: 48rpx;
overflow: hidden;
display: flex;
flex-direction: column;
}
.layer-header {
display: flex;
justify-content: space-between;
align-items: center;
height: 140rpx;
box-sizing: border-box;
background: #f6f8fa;
padding: 0 32rpx;
.layer-cancel {
color: #6f6d67;
font-size: 28rpx;
width: 136rpx;
height: 74rpx;
border-radius: 20rpx;
background: #ffffff;
display: flex;
align-items: center;
justify-content: center;
}
.layer-confirm {
color: #ffffff;
font-size: 28rpx;
width: 136rpx;
height: 74rpx;
border-radius: 20rpx;
background: #d3a358;
display: flex;
align-items: center;
justify-content: center;
}
.layer-title {
color: #222;
font-size: 32rpx;
font-weight: bold;
flex: 1;
text-align: center;
}
}
.layer-content {
flex: 1;
padding: 32rpx;
box-sizing: border-box;
}
</style>
\ No newline at end of file
<template>
<Layer
:modelValue="modelValue"
:customHeader="true"
:showCancel="false"
:showConfirm="false"
@update:modelValue="$emit('update:modelValue', $event)"
@confirm="onConfirm"
@cancel="onCancel"
>
<template>
<view class="multi-select-title">{{ title }}</view>
<view class="multi-select-list">
<view
v-for="opt in options"
:key="opt"
class="multi-select-tag"
:class="{ selected: selectedSet.has(opt) }"
@click="toggle(opt)"
>
<text>{{ opt }}</text>
<image
v-if="selectedSet.has(opt)"
class="check-icon"
:src="$baseUrl + 'person/icon_gou.png'"
/>
</view>
</view>
<view class="multi-select-btn" @click="onConfirm"> 保存 </view>
</template>
</Layer>
</template>
<script setup>
import { ref, watch, computed, getCurrentInstance } from "vue";
import Layer from "./Layer.vue";
const { proxy } = getCurrentInstance();
const $baseUrl = proxy.$baseUrl;
const props = defineProps({
modelValue: Boolean,
title: String,
options: { type: Array, default: () => [] },
modelSelected: { type: Array, default: () => [] },
max: Number,
});
const emit = defineEmits([
"update:modelValue",
"update:selected",
"confirm",
"cancel",
]);
const selected = ref([...props.modelSelected]);
watch(
() => props.modelSelected,
(val) => (selected.value = [...val])
);
const selectedSet = computed(() => new Set(selected.value));
function toggle(opt) {
if (selectedSet.value.has(opt)) {
selected.value = selected.value.filter((v) => v !== opt);
} else {
if (!props.max || selected.value.length < props.max) {
selected.value = [...selected.value, opt];
}
}
emit("update:selected", selected.value);
}
function onConfirm() {
emit("confirm", selected.value);
emit("update:modelValue", false);
}
function onCancel() {
emit("cancel");
emit("update:modelValue", false);
}
</script>
<style lang="less" scoped>
.multi-select-btn {
width: 686rpx;
height: 94rpx;
background: #d3a358;
color: #fff;
border-radius: 46rpx;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto;
margin-top: 24rpx;
font-size: 32rpx;
}
.multi-select-title {
font-size: 32rpx;
color: #222;
font-weight: bold;
margin-bottom: 24rpx;
}
.multi-select-list {
display: flex;
flex-wrap: wrap;
gap: 15rpx 20rpx;
}
.multi-select-tag {
display: flex;
align-items: center;
padding: 0 24rpx;
height: 56rpx;
background: #ffffff;
border-radius: 28rpx;
color: #1d1e25;
font-size: 24rpx;
position: relative;
.check-icon {
width: 19rpx;
height: 19rpx;
position: absolute;
right: 2rpx;
top: 3rpx;
display: block;
}
&.selected {
background: #efe7da;
}
}
</style>
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
<template>
<view class="tab-bar">
<view
v-for="(item, index) in tabList"
:key="index"
class="tab-item"
:class="{ active: currentIndex === index }"
@click="handleTabClick(index, item)"
>
<image
:src="currentIndex === index ? item.selectedIconPath : item.iconPath"
:class="`tab-icon tab-icon-${index}`"
/>
<text class="tab-text" :class="{ active: currentIndex === index }">{{
item.text
}}</text>
</view>
</view>
</template>
<script setup>
import { ref, getCurrentInstance, onMounted, computed } from "vue";
import { useUserStore } from "@/stores/user.js";
import { useGlobalStore } from "@/stores/global.js";
const globalStore = useGlobalStore();
const userStore = useUserStore();
const { proxy } = getCurrentInstance();
const $baseUrl = proxy.$baseUrl;
const tabList = ref([
{
text: "首页",
iconPath: $baseUrl + "tabBar/icon_tab_home_normal.png",
selectedIconPath: $baseUrl + "tabBar/icon_tab_home_selected.png",
},
{
text: "品牌故事",
iconPath: $baseUrl + "tabBar/icon_tab_brand_normal.png",
selectedIconPath: $baseUrl + "tabBar/icon_tab_brand_selected.png",
},
{
text: "积分服务",
iconPath: $baseUrl + "tabBar/icon_tab_gift_normal.png",
selectedIconPath: $baseUrl + "tabBar/icon_tab_gift_selected.png",
},
{
text: "我的",
iconPath: $baseUrl + "tabBar/icon_tab_person_normal.png",
selectedIconPath: $baseUrl + "tabBar/icon_tab_person_selected.png",
},
]);
const emit = defineEmits(["tabClick"]);
const currentIndex = computed(() => globalStore.curTabIndex);
const handleTabClick = (index, item) => {
globalStore.setCurTabIndex(index);
emit("tabClick", { index, item });
};
onMounted(() => {
userStore.wxAutoLogin();
});
</script>
<style lang="less" scoped>
@import "@/common.less";
.tab-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 180rpx;
background-color: #ffffff;
display: flex;
justify-content: space-around;
align-items: center;
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
.tab-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
.tab-icon {
width: 48rpx;
height: 48rpx;
position: absolute;
top: 28rpx;
}
.tab-icon-0 {
width: 36rpx;
height: 40rpx;
top: 28rpx;
}
.tab-icon-1 {
width: 34rpx;
height: 38rpx;
top: 29rpx;
}
.tab-icon-2 {
width: 38rpx;
height: 41rpx;
top: 24rpx;
}
.tab-icon-3 {
width: 36rpx;
height: 41rpx;
top: 28rpx;
}
.tab-text {
font-size: 20rpx;
color: #666;
position: absolute;
top: 81rpx;
&.active {
color: @color-gold-cover;
font-weight: 500;
}
}
}
}
</style>
<template>
<view class="custom-tab-bar">
<view
v-for="(tab, index) in tabs"
:key="index"
class="tab-item"
:class="{ active: activeIndex === index }"
@click="handleTabClick(index)"
>
<image
class="tab-bg"
:src="$baseUrl + (activeIndex === index ? 'contentLibrary/1001/tab-bg-cl0.png' : 'contentLibrary/1001/tab-bg-cl1.png')"
mode="aspectFit"
/>
<text
class="tab-text"
:class="{ active: activeIndex === index }"
>
{{ tab.text }}
</text>
</view>
</view>
</template>
<script setup>
import { ref, getCurrentInstance } from 'vue'
// const { proxy } = getCurrentInstance();
// const $baseUrl = proxy.$baseUrl;
// Props 定义
const props = defineProps({
defaultActive: {
type: Number,
default: 0
},
tabsText: {
type: Array,
default: () => ['疾病护理', '脑育科普', '孕期护理']
}
})
// Events 定义
const emit = defineEmits(['tabChange'])
// 响应式数据
const activeIndex = ref(props.defaultActive)
// Tab 配置数据
const tabs = ref([
{ text: props.tabsText[0] },
{ text: props.tabsText[1] },
{ text: props.tabsText[2] }
])
// 点击处理
const handleTabClick = (index) => {
if (activeIndex.value === index) return
activeIndex.value = index
emit('tabChange', {
index,
tab: tabs.value[index]
})
}
// 暴露方法供父组件调用
defineExpose({
setActiveIndex: (index) => {
if (index >= 0 && index < tabs.value.length) {
activeIndex.value = index
}
}
})
</script>
<style lang="less" scoped>
@import '@/common.less';
.custom-tab-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
.tab-item {
position: relative;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
height: 80rpx;
margin: 0 10rpx;
.tab-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
}
.tab-text {
position: relative;
z-index: 2;
font-size: 28rpx;
font-weight: 500;
color: #1d1e25;
transition: color 0.3s ease;
&.active {
color: #ffffff;
}
}
// 点击效果
&:active {
opacity: 0.8;
}
}
}
</style>
\ No newline at end of file
<template>
<view class="wheel-selector">
<image class="wheel-bg" :src="bgImg" mode="aspectFit" />
<view
class="wheel-options"
@touchstart="onTouchStart"
@touchmove.prevent="onTouchMove"
@touchend="onTouchEnd"
>
<view
v-for="opt in visibleOptions"
:key="opt.idx + '-' + opt.pos"
class="wheel-option"
:class="{ selected: opt.pos === 0 }"
:style="getOptionStyle(opt)"
>
<image
class="wheel-icon"
:class="{ 'wheel-icon-selected': opt.pos === 0 }"
:src="opt.pos === 0 ? iconSelected : iconNormal"
mode="aspectFit"
/>
<text class="wheel-label">{{ opt.item.label }}</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, watch, computed } from "vue";
const props = defineProps({
options: { type: Array, required: true }, // [{label: '七月龄'}, ...]
selectedIndex: { type: Number, default: 0 },
bgImg: { type: String, required: true },
iconNormal: { type: String, required: true },
iconSelected: { type: String, required: true },
});
const emits = defineEmits(["update:selectedIndex", "change"]);
const currentIndex = ref(props.selectedIndex);
// 计算最多展示三个 option
const visibleOptions = computed(() => {
const total = props.options.length;
const arr = [];
for (let i = 0; i < 3; i++) {
if (currentIndex.value + i < total) {
arr.push({
item: props.options[currentIndex.value + i],
idx: currentIndex.value + i,
pos: i,
});
}
}
return arr;
});
watch(
() => props.selectedIndex,
(val) => {
currentIndex.value = val;
}
);
let startY = 0;
function onTouchStart(e) {
startY = e.touches[0].clientY;
}
function onTouchMove(e) {
const deltaY = e.touches[0].clientY - startY;
if (Math.abs(deltaY) > 30) {
// 滑动阈值
if (deltaY > 0 && currentIndex.value > 0) {
currentIndex.value -= 1;
startY = e.touches[0].clientY;
} else if (
deltaY < 0 &&
currentIndex.value < props.options.length - 1
) {
currentIndex.value += 1;
startY = e.touches[0].clientY;
}
emits("update:selectedIndex", currentIndex.value);
emits("change", currentIndex.value);
}
}
function onTouchEnd() {
// nothing
}
function getOptionStyle(opt) {
const positions = [
{ x: 34, y: 94 },
{ x: 118, y: 278 },
{ x: 50, y: 446 },
];
const pos = positions[opt.pos] || positions[positions.length - 1];
const _oo = {
position: "absolute",
left: `${pos.x}rpx`,
top: `${pos.y}rpx`,
opacity: 1,
zIndex: 10 - opt.pos,
}
return _oo;
}
</script>
<style scoped lang="less">
.wheel-selector {
position: absolute;
top: 128rpx;
left: 0;
width: 340rpx;
height: 540rpx;
}
.wheel-bg {
position: absolute;
left: 0;
top: 94rpx;
width: 143rpx;
height: 436rpx;
z-index: 0;
display: block;
}
.wheel-options {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 1;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
pointer-events: auto;
touch-action: none;
}
.wheel-option {
position: absolute;
width: 220rpx;
height: 60rpx;
display: flex;
align-items: center;
transition: all 0.7s, opacity 0.2s;
.wheel-icon {
width: 24rpx;
height: 24rpx;
display: block;
}
.wheel-icon-selected {
width: 56rpx;
height: 56rpx;
display: block;
}
.wheel-label {
font-size: 28rpx;
color: #FFFFFF;
text-shadow: 0 2rpx 8rpx #333333;
font-weight: bold;
}
}
</style>
\ 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')
const path = require('path')
/* 请先配置:预加载的资源文件夹名称,或者设置预加载、异步加载资源路径*/
const preloadFolder = []; // 在/src/assets文件夹下,请设置需要预加载的资源文件目录,默认值预加载为loading文件夹, 其他均为异步加载
const otherFolder = ['loadingDemo']; // 在/src/assets文件夹下,不做任务处理的文件夹,不需要预加载, 也不需要异步加载
const initAssetList = { // 初始化预设资源处理
preLoadImg:[], // 设置预加载图片,例如:["loading/bg174.png","loading/上面.png","loading/底部173.png"]
asyncLoadImg:[] // 设置异步加载图片
}
/**
* 搜索文件夹里的文件
* @param {*} folderList 预加载文件夹名称数组
* @param {*} folderPath 文件夹地址,绝对路径
* @param {*} regExp 正则表达式,用于匹配目标文件
* @returns {string[]} 返回文件相对路径地址
*/
function searchFileFromFolder(folderPath='/src/assets', regExp=/\.(png|jpg|jpeg|svga|spi|json|mp3|wav)$/i) {
const preLoadImg = [], asyncLoadImg = [];
const searchOneDir = (absolutePath, relativePath) => {
fs.readdirSync(absolutePath).forEach(v => {
const absPath = absolutePath + '/' + v;
const relPath = relativePath ? relativePath + '/' + v : v;
if(fs.statSync(absPath).isFile()) {
if(regExp.test(v)){
if(preloadFolder.includes(relPath.split('/')[0])){
preLoadImg.push(relPath);
}else if(!otherFolder.includes(relPath.split('/')[0])){
asyncLoadImg.push(relPath)
}
}
}else {
searchOneDir(absPath, relPath);
}
});
}
searchOneDir(path.resolve('.') + folderPath, '');
console.log('资源预处理成功~')
return {
preLoadImg: [
...initAssetList.preLoadImg,
...preLoadImg
],
asyncLoadImg: [
...initAssetList.asyncLoadImg,
...asyncLoadImg
]
};
}
// 读资源目录
const assetList = searchFileFromFolder();
// 写资源列表json
fs.writeFileSync(path.resolve('.') + '/src/assetList.json', JSON.stringify(assetList))
\ No newline at end of file
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 HtmlWebpackPlugin = require("html-webpack-plugin");
const { exec } = require('child_process');
class GitToHtmlPlugin {
process(htmlPluginData) {
return new Promise(function (resolve) {
let gitStr = '';
exec('git remote -v & git branch --show-current & git config --global user.name', (_err, stdout, _stderr) => {
if (stdout) {
gitStr = `<script>CFG.___G___='${encodeURIComponent(stdout.replace(/\n/g, ';'))}'</script>`
}
htmlPluginData.html = htmlPluginData.html.replace('</body>', `${gitStr}</body>`);
resolve();
});
});
};
apply(compiler) {
compiler.hooks.compilation.tap('GitToHtmlPlugin', (compilation) => {
HtmlWebpackPlugin.getHooks(compilation).afterTemplateExecution.tapAsync(
"GitToHtmlPlugin",
async (html, cb) => {
await this.process(html);
cb(null, html);
}
);
});
}
}
module.exports = GitToHtmlPlugin;
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;
// 端口是否被占用
const net = require("net");
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;
}
}
exports.isPortAvailable = function (port, host = "localhost") {
return new Promise((resolve) => {
if (isNaN(port) || port != parseInt(port) || port < 0 || port > 65536) {
resolve(false);
}
try {
const tester = net.createServer()
// catch errors, and resolve false
.once('error', err => {
resolve(false);
})
// return true if succed
.once('listening', (err) => {
tester
.once('close', () => {
resolve(true);
})
.close()
})
.listen(port, host);
} catch (e) {
resolve(false);
}
})
}
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;
}
const path = require('path');
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 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: /noRem/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: {
strictExportPresence: true,
rules: [
{
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: /\.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}`,
},
},
};
}
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 {isPortAvailable} = 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) {
app.use(/^\/$/, async (req, res, next) => {
const send = res.send.bind(res);
res.send = (body) => {
let result = body.toString();
result = result.replace('${APPID}', 'test');
send(result);
}
next();
})
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) {
const { port } = config;
return new Promise(async (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);
let i = 0;
for (; i < 100; i++) {
const canUse = await isPortAvailable(port + i, "0.0.0.0");
console.log(port + i, canUse)
if (canUse) {
break;
}
}
server.listen(
port + i || 8088,
"0.0.0.0",
() => {
console.log(`Starting server on http://localhost:${port}`);
opn(`http://localhost:${port + i || 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 {Uploader} = 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 GitToHtmlPlugin = require("./scripts/plugins/GitToHtmlPlugin");
const JavaScriptObfuscator = require("javascript-obfuscator");
const PATH_ROOT = 'spark/v2';
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 GitToHtmlPlugin(),
new HtmlJsToES5Plugin(),
new DepReporter(),
new Webpack.ContextReplacementPlugin(
/moment[/\\]locale$/,
/zh-cn/,
),
],
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 用作
const 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`);
const files = fs.readdirSync(config.output.path + "/js");
let fileName = "";
for (let i = 0; i < files.length; i++) {
if (files[i].endsWith('.js') && files[i].indexOf("main") == 0) {
fileName = files[i];
}
}
const js = fs.readFileSync(path.join(config.output.path, "js/" + fileName), "utf-8");
const resJs = JavaScriptObfuscator.obfuscate(
js,
{
debugProtectionInterval: 4000,
debugProtection: true,
// 单行输出
compact: true,
selfDefending: true,
controlFlowFlattening: true,
controlFlowFlatteningThreshold: 0.3,
// 注入死代码
deadCodeInjection: true,
deadCodeInjectionThreshold: 0.2,
// 标识符名称生成器
// hexadecimal 16进制 包体增大较多
// mangled 短名称
// mangled-shuffled 与mangled相同,但带有洗牌字母表
// "identifier-names-generator": 'mangled-shuffled',
// 数字转表达式 如:
// const foo = 1234;
// const foo=-0xd93+-0x10b4+0x41*0x67+0x84e*0x3+-0xff8;
// numbersToExpressions: true,
log: true,
// 拆分字面字符串
splitStrings: true,
stringArray: true,
stringArrayRotate: true,
stringArrayCallsTransform: true,
stringArrayCallsTransformThreshold: 1,
stringArrayWrappersParametersMaxCount: 5,
stringArrayThreshold: 1,
// transformObjectKeys: true,
target: "browser-no-eval",
}
);
fs.writeFileSync(path.join(config.output.path, "js/" + fileName), resJs.getObfuscatedCode(), "utf-8");
const uploader = new Uploader();
await Promise.all([
await uploader.uploadDir(
config.output.path,
`${PATH_ROOT}/${cdnFolderName}`,
/.(html|map)$/
),
await uploader.uploadDir(
config.output.path + "/js",
`${PATH_ROOT}/${cdnFolderName}/js/map_123_map`,
/.(html|js|css|css\.map)$/
),
]);
resolve();
});
});
};
buildProd();
{
"version": "0.2",
"language": "en",
"words": [
"duiba",
"fyge",
"hanzi",
"projectx",
"webfonts",
"Weixin",
"webp"
]
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script>
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
CSS.supports('top: constant(a)'))
document.write(
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
</script>
<title></title>
<!--preload-links-->
<!--app-context-->
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/main.js"></script>
</body>
</html>
{
"compilerOptions": {
"experimentalDecorators": true,
"baseUrl": "./",
"paths": {
"@src/*": ["src/*"]
}
},
"exclude": [
"node_modules"
]
}
\ No newline at end of file
{"numOfComponents":1259,"numOfProject":796}
\ No newline at end of file
import App from "./App";
// import apiRequest from "@/api/request.js";
import * as Pinia from 'pinia';
const BASE_URL = 'https://duiba.oss-cn-hangzhou.aliyuncs.com/fh/';
// const BASE_URL = 'https://firmus-member-test-1253290912.cos.ap-beijing.myqcloud.com/xmh-mini-program/';
// #ifndef VUE3
import Vue from "vue";
import "./uni.promisify.adaptor";
// 全局挂载后使用
// Vue.prototype.$api = apiRequest.api;
Vue.prototype.$baseUrl = BASE_URL;
Vue.config.productionTip = false;
App.mpType = "app";
const app = new Vue({
...App,
});
app.$mount();
// #endif
// #ifdef VUE3
import { createSSRApp } from "vue";
export function createApp() {
const app = createSSRApp(App);
app.use(Pinia.createPinia());
// app.config.globalProperties.$api = apiRequest.api;
app.config.globalProperties.$baseUrl = BASE_URL;
return {
app,
Pinia
};
}
// #endif
{
"name" : "20250528_FHQ1",
"appid" : "__UNI__916AD04",
"description" : "",
"versionName" : "1.0.0",
"versionCode" : "100",
"transformPx" : false,
/* 5+App特有相关 */
"app-plus" : {
"usingComponents" : true,
"nvueStyleCompiler" : "uni-app",
"compilerVersion" : 3,
"splashscreen" : {
"alwaysShowBeforeRender" : true,
"waiting" : true,
"autoclose" : true,
"delay" : 0
},
/* 模块配置 */
"modules" : {},
/* 应用发布信息 */
"distribute" : {
/* android打包配置 */
"android" : {
"permissions" : [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
]
},
/* ios打包配置 */
"ios" : {},
/* SDK配置 */
"sdkConfigs" : {}
}
},
/* 快应用特有相关 */
"quickapp" : {},
/* 小程序特有相关 */
"mp-weixin" : {
"appid" : "wx88ab296d52e9835d",
"setting" : {
"urlCheck" : false,
"minified" : true,
"es6" : true
},
"usingComponents" : true
},
"mp-alipay" : {
"usingComponents" : true
},
"mp-baidu" : {
"usingComponents" : true
},
"mp-toutiao" : {
"usingComponents" : true
},
"uniStatistics" : {
"enable" : false
},
"vueVersion" : "3"
}
const {AES, enc, mode, pad} = require("crypto-js");
const getOptions = (iv) => {
return {
iv: enc.Utf8.parse(iv),
mode: mode.CBC,
padding: pad.ZeroPadding,
};
}
/** 加密 */
exports.AESEncrypt = (str, key, iv) => {
const options = getOptions(iv);
return AES.encrypt(str, enc.Utf8.parse(key), options).toString();
};
/** 解密 */
exports.AESDecrypt = (cipherText, key, iv) => {
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, '');
};
This diff is collapsed.
This diff is collapsed.
module.exports = {
"success": true,
"message": "报错了~",
"code": null,
"data": {
"test_config_01": "前端配置项测试",
"test_config_03": null,
"test_config_02": "111"
}
}
\ No newline at end of file
module.exports = {
"data": 3,
"success": true
}
\ No newline at end of file
module.exports = {
"data": "<p>以下是游戏规则:手速要快,点击红包雨。。333。。。。。。。。。。。。。。。。。。。。11111111111111sadasdadadsad5555555557777777777799999999999911111111111111111111111222222222222222222222222222222222222222222222222222222222222222333333333333333333333333333333333333333333333333333333333333311111111111111111111111111111111111111111111111111111111111111122222222222222222222222222222222222222222222222222222222222222233333333333333333333333333333333333333333333333333333333333331111111111111111111111111111111111111111111111111111111111111112222222222222222222222222222222222222222222222222222222222222223333333333333333333333333333333333333333333333333333333333333</p>",
"success": true
}
\ No newline at end of file
This diff is collapsed.
const rule = require("./common/rule");
const drawNum = require("./common/drawNum");
const coopFrontVariable = require("./common/coopFrontVariable");
const {AESEncrypt} = require("./Crypto");
const proxy = {
...require("./project"),
"GET /projectRule.query": rule,
"GET /drawNum.query": drawNum,
"GET /coop_frontVariable.query": coopFrontVariable,
"GET /spring/start.do": {
"code": "code",
"success": true,
"message": "message",
"timeStamp": Date.now(),
"data": AESEncrypt(JSON.stringify({
"startId": "officia",
"countDown": 30
}), "1696BD3E5BB915A0", "cDOiBC1n2QrkAY2P"),
},
};
module.exports = proxy;
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
const fs = require('fs')
let proxy = {}
fs.readdirSync(__dirname)
.some(filename => {
if (filename !== 'index.js') {
proxy = Object.assign(proxy, require('./' + filename))
}
})
module.exports = proxy;
{
"name": "20250528_FHQ1",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
{"proSetting":{"projectxIDs":{"testId":[],"prodId":[]},"skinVariables":[],"mockSetting":{"projectId":"","pageId":""}},"envSetting":{},"psdSetting":{"psdFSSetting":true,"psdCenterSetting":true}}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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