first commit

This commit is contained in:
DEV_DSW
2026-01-28 11:49:55 +08:00
commit 52a8ea4bd5
34 changed files with 5025 additions and 0 deletions

9
.env.development Normal file
View File

@@ -0,0 +1,9 @@
# 环境
VITE_ENV = development
# vconsole开关控制器
VITE_CONSOLE = 1
# 接口地址
VITE_BASE_API =

13
.env.production Normal file
View File

@@ -0,0 +1,13 @@
# 环境
VITE_ENV = production
# vconsole开关控制器
VITE_CONSOLE = 0
# 接口地址
VITE_BASE_API =

14
.env.staging Normal file
View File

@@ -0,0 +1,14 @@
# 环境
VITE_ENV = staging
# vconsole开关控制器
VITE_CONSOLE = 1
# 接口地址
VITE_BASE_API =

3
.eslintignore Normal file
View File

@@ -0,0 +1,3 @@
/dist/
/node_modules/
/src/common/Track.js

86
.eslintrc.js Normal file
View File

@@ -0,0 +1,86 @@
// .eslintrc.js
module.exports = {
root: true,
parserOptions: {
parser: '@babel/eslint-parser',
requireConfigFile: false,
sourceType: 'module'
},
env: {
browser: true,
node: true,
es6: true
},
extends: [
'plugin:vue/vue3-essential',
'eslint:recommended',
'plugin:prettier/recommended'
],
// add your custom rules here
rules: {
'no-undef': 0,
// "off" or 0 - 关闭规则
// "warn" or 1 - 将规则视为一个警告
// "error" or 2 - 将规则视为一个错误
'prettier/prettier': 'error',
// allow async-await
'generator-star-spacing': 'off',
// 'no-console': process.env.VITE_ENV === 'production' ? 'error' : 'off',
// allow debugger during development
// 'no-debugger': process.env.VITE_ENV === 'production' ? 'error' : 'off',
/**
* 最佳实践
*/
eqeqeq: 2, // 强制使用 === 和 !==
'default-case': 1, // 要求 switch 语句中有 default 分支
'no-else-return': 1, // 禁止 if 语句中 return 语句之后有 else 块
'no-empty-function': 0, // 禁止出现空函数
'no-unused-vars': 0, // 禁止出现空函数
'no-multi-spaces': 1, // 禁止使用多个空格
radix: 1, // 强制在parseInt()使用基数参数
'no-useless-return': 1, // 禁止多余的 return 语句
'no-with': 2, //禁用 with 语句
/**
* 变量声明
*/
'init-declarations': ['error', 'always'], // 声明变量必须赋值
/**
* ECMAScript6
*/
'arrow-spacing': ['error', { before: true, after: true }], // 强制箭头函数的箭头前后使用空格
'no-var': 2, // 禁止使用 var 声明变量
'object-shorthand': 2, // 要求使用对象方法名和属性名简写
'prefer-arrow-callback': 2, // 要求回调函数使用箭头函数
'prefer-const': 2, // 使用 const 声明那些声明后不再被修改的变量
'prefer-rest-params': 2, // 要求使用剩余参数而不是 arguments
'no-duplicate-imports': 2, // 禁止重复模块导入
'prefer-destructuring': 1, // 优先使用数组和对象解构
/**
* 风格指南
*/
'space-before-function-paren': 0, // 函数名称或function关键字与开始参数之间允许有空格
'array-bracket-spacing': 0, // 数组方括号内必须空格
'comma-dangle': 2, // 禁止末尾逗号
'eol-last': 2, // 要求文件末尾存在空行
// 对象冒号前禁止空格,冒号后必须空格
'key-spacing': ['error', { beforeColon: false, afterColon: true }],
// 关键字if、else等前后必须有空格
'keyword-spacing': ['error', { before: true, after: true }],
// 禁止出现多行空行
'no-multiple-empty-lines': ['error', { max: 1 }],
semi: 0, // 禁止末尾分号
quotes: ['error', 'single'], // 强制使用单引号
'space-infix-ops': 2, // 操作符周围必须有空格
'spaced-comment': ['error', 'always'], // 注释后面必须跟随至少一个空白
'object-curly-spacing': 0,
'no-unused-expressions': 0,
'vue/multi-word-component-names': 0,
'no-process-env': 2,
camelcase: 2,
'no-lonely-if': 2,
'func-style': 2 // 禁止使用function声明请使用箭头函数声明
}
}

25
.gitignore vendored Normal file
View File

@@ -0,0 +1,25 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
stats.html
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

3
.npmrc Normal file
View File

@@ -0,0 +1,3 @@
registry=https://r.npm.taobao.org/
sass_binary_site=https://cdn.npmmirror.com/binaries/node-sass
shamefully-hoist=true

23
.prettierrc.js Normal file
View File

@@ -0,0 +1,23 @@
// .preitterrc.js
module.exports = {
// 一行的字符数如果超过会进行换行默认为80
printWidth: 80,
// 一个tab代表几个空格数默认为80
tabWidth: 2,
// 是否使用tab进行缩进默认为false表示用空格进行缩减
useTabs: false,
// 字符串是否使用单引号默认为false使用双引号
singleQuote: true,
// 行位是否使用分号默认为true
semi: false,
// 是否使用尾逗号,有三个可选值"<none|es5|all>"
trailingComma: "none",
// 对象大括号直接是否有空格默认为true效果{ foo: bar }
bracketSpacing: true,
// 代码的解析引擎默认为babylon与babel相同
// "parser": "babylon",
// 开启 eslint 支持
eslintIntegration: true,
autocrlf: false
}

45
README.md Normal file
View File

@@ -0,0 +1,45 @@
# 智念移动端单页应用Vue3模板
# 安装依赖
Yarn 安装
```bash
yarn install
```
npm 安装
```bash
npm install
```
# 运行开发
```bash
yarn dev
```
```bash
npm run dev
```
# 测试打包
```bash
yarn build:stage
```
```bash
npm build:stage
```
# 生产打包
```bash
yarn build:prod
```
```bash
npm build:prod
```

22
commitlint.config.js Normal file
View File

@@ -0,0 +1,22 @@
/**
* feat新功能
* fix修补bug
* refactor重构某个功能
* revert: 回滚到上一个版本
* style仅样式改动
* docs仅文档新增/改动
* chore构建过程或辅助工具的变动
*/
module.exports = {
extends: [
// 直接继承官网规则
'@commitlint/config-conventional'
],
rules: {
'type-enum': [
2,
'always',
['feat', 'fix', 'refactor', 'revert', 'style', 'docs', 'chore']
]
}
}

20
index.html Normal file
View File

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport"
content="width=device-width, viewport-fit=cover, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no" />
<meta name="format-detection" content="telephone=no" />
<meta name="applicable-device" content="mobile" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<title>&lrm;</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

13
jsconfig.json Normal file
View File

@@ -0,0 +1,13 @@
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": [
"src/*"
]
}
},
"exclude": [
"node_modules"
]
}

50
package.json Normal file
View File

@@ -0,0 +1,50 @@
{
"name": "webapp-vue-frontend",
"description": "智念移动端单页应用Vue3模板",
"version": "1.0.0",
"scripts": {
"dev": "vite",
"build:prod": "vite build && rimraf ./dist/assets/*.map && rimraf stats.html",
"build:stage": "vite build --mode staging",
"preview": "vite preview",
"lint": "eslint --ext .vue,.js src",
"lint:fix": "eslint --fix --ext .vue,.js src",
"prepare": "husky install"
},
"dependencies": {
"axios": "^1.13.4",
"compressorjs": "^1.2.1",
"mitt": "^3.0.1",
"vant": "^4.9.22",
"vconsole": "^3.15.1",
"vue": "^3.5.27",
"vue-router": "^4.6.4"
},
"devDependencies": {
"@babel/core": "^7.19.6",
"@babel/eslint-parser": "^7.19.1",
"@commitlint/cli": "^17.1.2",
"@commitlint/config-conventional": "^17.1.0",
"@vitejs/plugin-legacy": "^2.2.0",
"@vitejs/plugin-vue": "^3.1.0",
"consola": "^2.15.3",
"eslint": "^8.26.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-vue": "^9.6.0",
"husky": "^8.0.1",
"postcss-pxtorem": "^6.0.0",
"prettier": "^2.7.1",
"rimraf": "^3.0.2",
"rollup-plugin-visualizer": "^5.8.3",
"sass": "^1.58.3",
"terser": "^5.15.1",
"typescript": "^4.8.4",
"unplugin-auto-import": "^0.11.3",
"unplugin-vue-components": "^0.22.9",
"vite": "3.1.0",
"vite-plugin-style-import": "^2.0.0",
"vite-plugin-vconsole": "^1.2.2",
"vite-plugin-vue-setup-extend": "^0.4.0"
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

46
src/App.vue Normal file
View File

@@ -0,0 +1,46 @@
<template>
<router-view v-slot="{ Component }">
<keep-alive :include="KeepAliveList">
<component :is="Component" />
</keep-alive>
</router-view>
</template>
<script setup>
const router = useRouter()
const KeepAliveList = ref([])
router.beforeEach((to, from, next) => {
const keepAlive = to?.meta?.keepAlive
if (!router.hasRoute(to.name)) {
router.push('/error')
return
}
if (keepAlive) {
if (KeepAliveList.value.indexOf(to.name) === -1) {
KeepAliveList.value.push(to.name)
} else {
const index = KeepAliveList.value.findIndex((name) => name === from.name)
index > -1 ? KeepAliveList.value.splice(index, 1) : null
}
}
next()
})
</script>
<style lang="scss">
#app {
width: 100%;
height: 100%;
overflow: hidden;
@media only screen and (min-width: 500px) {
max-width: 500px;
margin: auto;
transform: scale(1);
}
}
</style>

BIN
src/assets/font/bebas.ttf Normal file

Binary file not shown.

20
src/assets/font/icon.css Normal file
View File

@@ -0,0 +1,20 @@
@font-face {
font-family: 'icon-font'; /* Project id 634611 (更新icon直接替换新的cdn地址即可无需下载) */
src: url('//at.alicdn.com/t/font_634611_fe4z5kh8e8n.woff2?t=1654585244447') format('woff2'),
url('//at.alicdn.com/t/font_634611_fe4z5kh8e8n.woff?t=1654585244447') format('woff'),
url('//at.alicdn.com/t/font_634611_fe4z5kh8e8n.ttf?t=1654585244447') format('truetype');
}
.icon-font {
font-family: 'icon-font' !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
@font-face {
font-family: 'money';
src: url('./bebas.ttf');
font-weight: normal;
font-style: normal;
}

View File

@@ -0,0 +1,96 @@
@import './normalize';
*,
*::before,
*::after {
-webkit-box-sizing: border-box;
box-sizing: border-box;
outline: none;
-webkit-tap-highlight-color: rgba(255, 0, 0, 0); // 移除移动端阴影
-webkit-overflow-scrolling: touch; //滚动回弹 ios13+
}
[data-theme='dark'] {
&,
* {
color-scheme: dark !important;
transition: background-color 300ms;
}
}
[data-theme='light'] {
&,
* {
color-scheme: light !important;
transition: background-color 300ms;
}
body {
background-color: #f5f5f5;
}
}
// 安全区域变量初始化
:root {
--safe-area-inset-top: 24px;
--safe-area-inset-right: 0px;
--safe-area-inset-bottom: 0px;
--safe-area-inset-left: 0px;
@supports (top: constant(safe-area-inset-top)) and (padding: Max(10px, 20px)) {
--safe-area-inset-top: Max(constant(safe-area-inset-top), 24px);
--safe-area-inset-right: constant(safe-area-inset-right);
--safe-area-inset-bottom: constant(safe-area-inset-bottom);
--safe-area-inset-left: constant(safe-area-inset-left);
}
@supports (top: env(safe-area-inset-top)) and (padding: Max(10px, 20px)) {
--safe-area-inset-top: Max(env(safe-area-inset-top), 24px);
--safe-area-inset-right: env(safe-area-inset-right);
--safe-area-inset-bottom: env(safe-area-inset-bottom);
--safe-area-inset-left: env(safe-area-inset-left);
}
}
.van-safe-area-top {
padding-top: var(--safe-area-inset-top) !important;
}
.van-image-preview__index,
.van-image-preview__close-icon--top-right {
top: 46px !important;
}
// 表单样式
.van-field__error-message {
text-align: right !important;
}
.van-hairline--top-bottom:after,
.van-hairline-unset--top-bottom:after {
border-width: 0 !important;
}
// 弹窗样式
.van-dialog {
width: 270px !important;
}
.van-dialog__confirm,
.van-dialog__confirm:active {
color: #14b498 !important;
}
.van-dialog__content--isolated {
min-height: 72px !important;
}
.van-dialog__message {
color: #333333;
font-size: 17px !important;
font-family: PingFangSC-Semibold, PingFang SC;
font-weight: 600;
line-height: 24px !important;
}

355
src/assets/scss/normalize.scss vendored Normal file
View File

@@ -0,0 +1,355 @@
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
/* Document
========================================================================== */
/**
* 1. Correct the line height in all browsers.
* 2. Prevent adjustments of font size after orientation changes in iOS.
*/
html {
line-height: 1.15; /* 1 */
-webkit-text-size-adjust: 100%; /* 2 */
height: 100%;
}
/* Sections
========================================================================== */
/**
* Remove the margin in all browsers.
*/
body {
margin: 0;
height: 100%;
}
/**
* Render the `main` element consistently in IE.
*/
main {
display: block;
}
/**
* Correct the font size and margin on `h1` elements within `section` and
* `article` contexts in Chrome, Firefox, and Safari.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/* Grouping content
========================================================================== */
/**
* 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
*/
hr {
box-sizing: content-box; /* 1 */
height: 0; /* 1 */
overflow: visible; /* 2 */
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
pre {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/* Text-level semantics
========================================================================== */
/**
* Remove the gray background on active links in IE 10.
*/
a {
background-color: transparent;
}
/**
* 1. Remove the bottom border in Chrome 57-
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
*/
abbr[title] {
border-bottom: none; /* 1 */
text-decoration: underline; /* 2 */
text-decoration: underline dotted; /* 2 */
}
/**
* Add the correct font weight in Chrome, Edge, and Safari.
*/
b,
strong {
font-weight: bolder;
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/**
* Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/* Embedded content
========================================================================== */
/**
* Remove the border on images inside links in IE 10.
*/
img {
border-style: none;
}
/* Forms
========================================================================== */
/**
* 1. Change the font styles in all browsers.
* 2. Remove the margin in Firefox and Safari.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit; /* 1 */
font-size: 100%; /* 1 */
line-height: 1.15; /* 1 */
margin: 0; /* 2 */
}
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
*/
button,
input { /* 1 */
overflow: visible;
}
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
*/
button,
select { /* 1 */
text-transform: none;
}
/**
* Correct the inability to style clickable types in iOS and Safari.
*/
button,
[type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
}
/**
* Remove the inner border and padding in Firefox.
*/
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
}
/**
* Restore the focus styles unset by the previous rule.
*/
button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
}
/**
* Correct the padding in Firefox.
*/
fieldset {
padding: 0.35em 0.75em 0.625em;
}
/**
* 1. Correct the text wrapping in Edge and IE.
* 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
*/
legend {
box-sizing: border-box; /* 1 */
color: inherit; /* 2 */
display: table; /* 1 */
max-width: 100%; /* 1 */
padding: 0; /* 3 */
white-space: normal; /* 1 */
}
/**
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
*/
progress {
vertical-align: baseline;
}
/**
* Remove the default vertical scrollbar in IE 10+.
*/
textarea {
overflow: auto;
}
/**
* 1. Add the correct box sizing in IE 10.
* 2. Remove the padding in IE 10.
*/
[type="checkbox"],
[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
[type="search"] {
-webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
}
/**
* Remove the inner padding in Chrome and Safari on macOS.
*/
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
}
/* Interactive
========================================================================== */
/*
* Add the correct display in Edge, IE 10+, and Firefox.
*/
details {
display: block;
}
/*
* Add the correct display in all browsers.
*/
summary {
display: list-item;
}
/* Misc
========================================================================== */
/**
* Add the correct display in IE 10+.
*/
template {
display: none;
}
/**
* Add the correct display in IE 10.
*/
[hidden] {
display: none;
}
button {
border: 0;
}

222
src/common/Track.js Normal file
View File

@@ -0,0 +1,222 @@
import { bindEvent, last } from 'vtils'
const Track = {
app: null,
ready: false,
App: {
created() {
Track.app = this
},
watch: {
'$route.path': {
immediate: true,
handler: async (fromPath, toPath) => {
await Track.ensureReady()
Track.app.$nextTick(async () => {
const { id, name } = await Track.injectMark()
window.loadEventPush(
fromPath,
toPath || '',
Track.app.$route.query,
{
mark_id: id,
mark_name: name
}
)
Track.app.$nextTick(() => {
window.elementAddEventListener()
})
})
}
}
}
},
ensureReady: async () => {
if (Track.ready) return
return new Promise((resolve) => {
const timer = setInterval(() => {
if (
// @ts-ignore
typeof window.Countly !== 'undefined' &&
// @ts-ignore
typeof window.anyEventPush !== 'undefined' &&
Track.app !== null
) {
clearInterval(timer)
Track.ready = true
resolve()
}
}, 60)
})
},
prepareData(data) {
return {
...(data.id && data.name
? {
control_id: data.id,
control_name: data.name,
control_value: data.value || data.name
}
: {}),
...(data.goods
? data.goods.isUniGoods
? {
content_id: data.goods.id,
content_name: data.goods.name,
resource_id: data.goods.resourceId,
resource_name: data.goods.resourceName,
resource_position_id: data.goods.resourcePositionId,
resource_position_name: data.goods.resourcePositionName,
resource_position_no: data.index || 0
}
: {
content_id: data.goods.commodityId,
content_name:
data.goods.commodityTitle ||
// 兼容订单下的商品
data.goods.name,
...(data.goods.commodityResourceId
? {
resource_id: data.goods.commodityResourceId,
resource_name: data.goods.commodityTitle,
resource_position_id: '',
resource_position_name: data.name,
resource_position_no: data.index || 0
}
: {})
}
: {}),
...(data.resource
? {
resource_id: data.resource.id,
resource_name: data.resource.title || data.resource.name || '',
resource_position_id: data.resource.stateId || '',
resource_position_name: data.resource.stateName || '',
resource_position_no: data.index || 0
}
: {}),
...(data.resourcePosition
? {
resource_position_id: data.resourcePosition.id,
resource_position_name: data.resourcePosition.name,
resource_position_no: data.index || 0
}
: {}),
...(data.mark
? {
mark_id: data.mark.id,
mark_name: data.mark.name
}
: {}),
...(data.content
? {
content_name: data.content
}
: {})
}
},
pushEvent: async (data) => {
await Track.ensureReady()
const trackData = Track.prepareData(data)
if (!trackData.mark_id && !trackData.mark_name) {
const { id, name } = await Track.injectMark()
trackData.mark_id = id
trackData.mark_name = name
}
window.anyEventPush(data.type, data.path, data.name, trackData)
},
provideMark(cb) {
return {
methods: {
provideTrackMark: cb
}
}
},
injectMark: async () => {
const currentRoute = Track.app.$route
const matched = currentRoute.matched
const currentPage = last(matched)
const currentPageInstance = currentPage?.instances.default
const provideTrackMark = currentPageInstance?.provideTrackMark
const { id = 'NONE', name = 'NONE' } =
typeof provideTrackMark === 'function' ? await provideTrackMark() : {}
return {
id,
name
}
},
install(app, options) {
Track.ensureReady().then(() => {
// @ts-ignore
countlyConfig.baseContextPath = options.appid
})
async function applyTrack(el, payload) {
await Track.ensureReady()
let data = payload.value
// @ts-ignore
if (data && !data.disabled) {
data = {
...data,
name: `${options.prefix || ''}${data.name}`,
// @ts-ignore
path: el.__track_data__?.path || window.getRoutePath()
}
el.__track_data__ = data
// 兼容曝光事件
if (data.expose !== false) {
el.setAttribute('exposure-event', data.name)
el.setAttribute(
'extend_attr',
JSON.stringify(Track.prepareData(data))
)
if (data.value !== null) {
el.setAttribute('alt', data.value)
}
}
const shouldListen = !el.dataset.track
if (shouldListen) {
el.dataset.track = '1'
el.__track_dispose__ = bindEvent(el)(
'click',
() => {
console.log(`埋点:点击「${data.name}`)
Track.pushEvent({
type: 'click',
...el.__track_data__,
value: el.innerText.trim().replace(/\s+/g, '')
})
},
{
// 必须使用捕获模式以在其他事件触发前处理(比如其他事件触发了页面跳转,就会导致问题)
capture: true,
// 使用被动模式告诉浏览器这不会产生阻塞行为
passive: true
}
)
}
}
}
function disposeTrack(el) {
// 需延时一下,不然当遇到点击跳转时页面卸载把事件也卸载了就触发不了了
setTimeout(() => {
delete el.__track_data__
el.__track_dispose__?.()
delete el.__track_dispose__
}, 0)
}
app.directive('track', {
mounted: applyTrack,
updated: applyTrack,
beforeUpdate: applyTrack,
unmounted: disposeTrack
})
console.log(1)
}
}
export default Track

131
src/common/ajax.js Normal file
View File

@@ -0,0 +1,131 @@
import axios from 'axios'
import {
md5Hash,
rsaEncrypt,
rsaDecrypt,
aesEncrypt,
aesDecrypt,
generateUUID,
dataSign,
generateRandomString,
getLocalStorage
} from '@dcb/utils'
const encryptionOff = Number(import.meta.env.VITE_ENCRYPTION)
const env = import.meta.env.VITE_ENV === 'development'
const STORE_OFF = Number(import.meta.env.VITE_STORE_OFF)
const RSA = import.meta.env.VITE_APP_RSA
const DATA_RSA = STORE_OFF ? import.meta.env.VITE_DATA_RSA : RSA
const martId = import.meta.env.VITE_MART_ID
const appKey = import.meta.env.VITE_KEY
const appSecret = import.meta.env.VITE_SECRET
const request = async (method, url, data, config = {}) => {
const p = env ? import.meta.env.VITE_P : decodeURI(getLocalStorage('p'))
const options = Object.assign({}, config, {
url,
method
})
console.log('入参', data)
const uuid = generateUUID()
const secret = '4RBA7^@0$@KM$5D2333'
const title = md5Hash(uuid + secret)
const aesKey = generateRandomString(16) // 生成16位随机数
const init = rsaEncrypt(aesKey, RSA)
const base = aesEncrypt(JSON.stringify(data), aesKey)
options.data = options.data || {}
/*
* @ 验签
* @wiki: http://wiki.gogpay.cn/pages/viewpage.action?pageId=75301563
* */
url = url.replace(/^https?:\/\/.+?\/([^/]+)/, '')
const timestamp = Date.now()
let asHex = ''
const header = Object.assign(
{ clientId: '03' },
STORE_OFF ? { martId, path: url, timestamp } : { path: url, timestamp }
)
for (const [key, value] of Object.entries(header)) {
asHex += key + value
}
// console.log(asHex + appSecret)
const sign = dataSign(header, appSecret)
if (method === 'post') {
if (!config.white && encryptionOff) {
options.data = {
timestamp: uuid,
title,
init,
base
}
} else {
options.data = data
}
}
options.headers = Object.assign({}, options.headers, header, {
sign,
appKey,
p
})
console.log(options.headers, STORE_OFF)
return new Promise((resolve, reject) => {
axios
.request(options)
.then((res) => {
let { data } = res
if (!data) {
return resolve(data)
}
if (method === 'post' && encryptionOff && !config.white) {
data = JSON.parse(
aesDecrypt(data.base, rsaDecrypt(data.init, DATA_RSA))
)
}
console.log('返回值', data)
resolve(data)
})
.catch((res) => {
reject(res)
})
})
}
export const ajax = {
request,
get(url, config) {
return request('get', url, null, config)
},
delete(url, config) {
return request('delete', url, null, config)
},
head(url, config) {
return request('head', url, null, config)
},
post(url, data, config) {
return request('post', url, data, config)
},
put(url, data, config) {
return request('put', url, data, config)
},
patch(url, data, config) {
return request('path', url, data, config)
},
setCommonHeader(key, value) {
window.axios.defaults.headers.common[key] = value
}
}

View File

@@ -0,0 +1,16 @@
/* eslint-disable no-process-env */
import { initAjax } from '@dcb/ajax'
const debug = import.meta.env.VITE_ENV
const RSA = import.meta.env.VITE_APP_RSA
const appid = import.meta.env.VITE_APP_ID
const BASE_API = import.meta.env.VITE_BASE_API
const encryptionOff = import.meta.env.VITE_ENCRYPTION
export const useAjax = function () {
const ajax = initAjax(true)
const options = { RSA, appid, BASE_API, encryptionOff, debug }
ajax.config(options)
return ajax
}

50
src/common/flexible.js Normal file
View File

@@ -0,0 +1,50 @@
// ref: https://github.com/amfe/lib-flexible/blob/2.0/index.js
;(function flexible(window, document) {
const docEl = document.documentElement
const dpr = window.devicePixelRatio || 1
// adjust body font size
const setBodyFontSize = () => {
if (document.body) {
document.body.style.fontSize = `${12 * dpr}px`
} else {
document.addEventListener('DOMContentLoaded', setBodyFontSize)
}
}
setBodyFontSize()
// set 1rem = viewWidth / 10
const setRemUnit = () => {
const rem =
Math.min(
// 宽度限制在 500 内
500,
docEl.clientWidth
) / 10
docEl.style.fontSize = `${rem}px`
}
setRemUnit()
// reset rem unit on page resize
window.addEventListener('resize', setRemUnit)
window.addEventListener('pageshow', (e) => {
if (e.persisted) {
setRemUnit()
}
})
// detect 0.5px supports
if (dpr >= 2) {
const fakeBody = document.createElement('body')
const testElement = document.createElement('div')
testElement.style.border = '.5px solid transparent'
fakeBody.appendChild(testElement)
docEl.appendChild(fakeBody)
if (testElement.offsetHeight === 1) {
docEl.classList.add('hairlines')
}
docEl.removeChild(fakeBody)
}
})(window, document)

3
src/common/index.js Normal file
View File

@@ -0,0 +1,3 @@
import { ajax } from './ajax'
export { ajax }

53
src/components/Empty.vue Normal file
View File

@@ -0,0 +1,53 @@
<template>
<div class="empty">
<img class="empty__img" :src="img" />
<div class="empty__desc" v-if="description" :style="{ color }">
{{ description }}
</div>
</div>
</template>
<script setup>
import imgEmpty from '@/assets/images/img_empty_order.png'
const props = defineProps({
description: {
type: String,
default: ''
},
img: {
type: Object,
default: imgEmpty
},
color: {
type: String,
default: '#999999'
}
})
</script>
<style scoped lang="scss">
.empty {
width: 100%;
height: 288px;
@extend %flex-center;
&__img {
width: 150px;
height: 150px;
display: block;
margin: 105px auto 0;
}
&__desc {
font-size: 14px;
font-family: PingFangSC-Regular, PingFang SC;
color: #999999;
line-height: 20px;
text-align: center;
margin-top: 8px;
}
}
</style>

View File

@@ -0,0 +1,148 @@
<template>
<div class="page-container" :style="{ backgroundColor: bgColor }">
<van-nav-bar :class="['nav-bar', navBgColor, isCustom ? 'custom-style' : '']" safe-area-inset-top left-arrow fixed
v-if="showNavigator" :border="false" :left-text="navBarLeftText" @click-left="handleBack">
<template #title>
<slot name="title">{{ title === '首页' ? '' : title }}</slot>
</template>
<template #right>
<slot name="nav-bar-right"></slot>
</template>
</van-nav-bar>
<slot name="body"></slot>
</div>
</template>
<script setup>
const { proxy } = getCurrentInstance()
const props = defineProps({
// nav-bar返回按钮位置的文字
navBarLeftText: {
type: String,
default: ''
},
// nav-bar标题
title: {
type: String,
default: ''
},
// 背景颜色
bgColor: {
type: String,
default: 'white'
},
// nav-bar背景颜色
navBgColor: {
type: String,
default: ''
},
// fixed情况下返回按钮背景颜色
backBgColor: {
type: String,
default: 'lightgray'
},
// 是否自定义样式
isCustom: {
type: Boolean,
default: false
},
// 是否显示导航栏
show: {
type: Boolean,
default: true
},
goback: {
type: Number,
default: 0
}
})
// 是否显示隐藏导航
const showNavigator = ref(props.show)
// 获取页面标题
const { title } = proxy.$router.currentRoute.value.meta
const handleBack = () => {
if (props.goback > 0) {
proxy.$router.go(-props.goback)
} else {
proxy.$router.back()
}
}
</script>
<style scoped lang="scss">
.page-container {
height: 100vh;
overflow-y: auto;
background-color: white;
padding-bottom: var(--safe-area-inset-bottom);
.body {
min-height: calc(100vh - 46px + var(--safe-area-inset-top));
padding-top: calc(46px + var(--safe-area-inset-top));
overflow-y: auto;
&::-webkit-scrollbar {
display: none;
}
}
.nav-bar {
background-color: transparent;
z-index: 2000;
position: fixed;
:deep(.van-icon-arrow-left) {
color: #fff;
font-size: 20px;
}
:deep(.van-nav-bar__title) {
color: #fff;
font-size: 18px;
font-weight: bold;
line-height: 22px;
opacity: 1;
}
&.style-1,
&.style-2 {
:deep(.van-icon-arrow-left) {
color: #333;
}
:deep(.van-nav-bar__title) {
color: #333;
}
}
&.style-1 {
background-color: #fff;
}
&.style-2 {
background-color: transparent;
}
&.custom-style {
background-color: rgba(22, 187, 191, 1);
:deep(.van-icon-arrow-left) {
color: #fff;
}
}
}
}
</style>

2
src/components/index.js Normal file
View File

@@ -0,0 +1,2 @@
export { default as PageContainer } from './PageContainer.vue'
export { default as Empty } from './Empty.vue'

5
src/hooks/tool.js Normal file
View File

@@ -0,0 +1,5 @@
import { reactive } from 'vue'
export const observer = reactive({
userInfo: undefined
})

13
src/main.js Normal file
View File

@@ -0,0 +1,13 @@
import { createApp } from 'vue'
import '@/assets/font/icon.css'
import '@/common/flexible'
import App from './App.vue'
import router from '@/router'
import store from '@/store'
const app = createApp(App)
app.config.globalProperties.$store = store
app.use(router)
app.mount('#app')

22
src/router/index.js Normal file
View File

@@ -0,0 +1,22 @@
import { createRouter, createWebHashHistory } from 'vue-router'
// 路由规则
const routes = [
{
path: '/home',
name: 'home', // 请和文件名一样
component: () => import('@/views/home/index.vue'),
meta: {
title: '首页', // 自动设置当前页面的标题
keepAlive: true
}
}
]
const router = createRouter({
// vueRouter@3版本的mode改成了historyhash模式配置createWebHashHistoryhistory模式配置createWebHistory
history: createWebHashHistory(),
routes
})
export default router

18
src/store/index.js Normal file
View File

@@ -0,0 +1,18 @@
import { reactive } from 'vue'
export default {
state: reactive({
userInfo: undefined
}),
mutations: {
setUserInfo(state, data) {
state.userInfo = data
}
},
commit(eventName, { data, dept }) {
data.dept.preCheck = dept.preCheck
this.mutations[eventName](this.state, data)
}
}

22
src/views/home/index.vue Normal file
View File

@@ -0,0 +1,22 @@
<template>
<van-button @click="handleClick">跳转记录</van-button>
</template>
<script setup name="home">
const router = useRouter()
// 跳转按钮操作
const handleClick = () => {
console.log('跳转记录')
}
onMounted(() => {
console.log('onMounted')
})
onActivated(() => {
console.log('onActivated')
})
</script>
<style lang="scss" scoped></style>

107
vite.config.js Normal file
View File

@@ -0,0 +1,107 @@
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import VueSetupExtend from 'vite-plugin-vue-setup-extend'
import AutoImport from 'unplugin-auto-import/vite'
import { createStyleImportPlugin, VantResolve } from 'vite-plugin-style-import'
import postCssPxToRem from 'postcss-pxtorem'
import legacy from '@vitejs/plugin-legacy'
import { resolve } from 'path'
import { viteVConsole } from 'vite-plugin-vconsole'
import visualizer from 'rollup-plugin-visualizer'
import Components from 'unplugin-vue-components/vite'
import { VantResolver } from 'unplugin-vue-components/resolvers'
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
const { VITE_ENV, VITE_CONSOLE } = loadEnv(mode, process.cwd())
const isProd = VITE_ENV === 'production'
const isVconsole = Number(VITE_CONSOLE)
return {
base: isProd ? './' : './', // 是否是生产环境
build: {
cssCodeSplit: false,
chunkSizeWarningLimit: 2048,
sourcemap: isProd
},
esbuild: {
drop: isProd ? ['console', 'debugger'] : undefined
},
plugins: [
legacy({
targets: ['defaults', 'not IE 11']
}),
vue(),
VueSetupExtend(),
Components({
dts: false,
resolvers: [VantResolver()]
}),
AutoImport({
// 后续vue/vue-router的API都不需要再单独import到setup里面了
imports: ['vue', 'vue-router'],
// dts: 'src/auto-imports...', // 可以自定义文件生成的位置与是否生成,默认是根目录下
dts: false
}),
createStyleImportPlugin({
resolves: [VantResolve()],
libs: [
{
libraryName: 'vant',
esModule: false,
resolveStyle: (name) => {
return `vant/es/${name}/style`
}
}
]
}),
viteVConsole({
entry: [resolve('src/main.js')],
localEnabled: isVconsole,
enabled: isVconsole,
config: {
maxLogNumber: 1000,
theme: 'light'
}
}),
!isProd
? visualizer({
open: true,
gzipSize: true,
brotliSize: true
})
: null
],
resolve: {
// 设置路径别名
alias: {
'@': resolve(__dirname, './src'),
'~': resolve(__dirname, './src/assets')
}
},
server: {
hmr: true,
port: 8080,
host: true,
open: true
},
css: {
// css预处理器
preprocessorOptions: {
scss: {
charset: false,
additionalData: '@import "./src/assets/scss/index.scss";'
}
},
postcss: {
plugins: [
postCssPxToRem({
rootValue: 37.5,
propList: ['*']
})
]
}
}
}
})

3370
yarn.lock Normal file

File diff suppressed because it is too large Load Diff