feat: 代码更新

This commit is contained in:
xjs 2025-05-09 18:04:15 +08:00
commit 24220d8f27
66 changed files with 8452 additions and 0 deletions

28
.gitignore vendored Normal file
View File

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

1
.npmrc Normal file
View File

@ -0,0 +1 @@
engine-strict = true

1
.nvmdrc Normal file
View File

@ -0,0 +1 @@
23.8.0

12
.prettierignore Normal file
View File

@ -0,0 +1,12 @@
/dist/*
.local
.output.js
.github
.vscode
/node_modules/**
*.html
**/*.svg
**/*.sh
/public/**

10
.prettierrc.json Normal file
View File

@ -0,0 +1,10 @@
{
"printWidth": 160,
"arrowParens": "always",
"bracketSameLine": true,
"bracketSpacing": true,
"semi": true,
"singleAttributePerLine": false,
"vueIndentScriptAndStyle": true,
"htmlWhitespaceSensitivity": "ignore"
}

3
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

27
ALIYUN.pem Normal file
View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA2SOM6Heb+BNGha/ucoV+GopddM7ckyGWALhGPJp/Z7P5jgw6
NSJ1G/E7CFEukaLC50eupji9mA4o8Emtrgn8y7uMIc5lafHe0IPy+WA90PZyien4
0u7dD0NrEbKH41SIEuZFbGev0CgQJsxmkS8CmOytmglyJ0JDBpJD9tQLwSuG9kad
DrYPArQdQu+ZALt4gyG7m14c4mZ2hAcIUwNYqFY9g/HIp0q9al8SW3WqzkB1U7GS
kh3i/MwtUvZWPI16aKW4/tdUX7y8PPguHQ1MWLd6DI0iGo9tqTQt35o9ax41O+0S
EY4MgU1Q3XJaMrmrYUpZ2/Y4xJbHOHgyi6afzQIDAQABAoIBAGms4ovUgkSmZOD1
MU/s5eVWx4rsje7RHqa1CAHAkxbOQTq/eqiXX3U83qT6lXZtRvu2KCpfXO4eng/r
W6pi0/P3D4j4YOTBwNWsEdkJ3KvQ9QdnpiBJ/a3K+tW/FGEvp5XDGbBbefYNOWcY
fSZVQadZMFfSFwtCNUqCbq82nY3hkoFVGiQ9EHUlvNCQM4y5VeJuCPzsl8rzAsyo
gpkHyKxU/CNg+f5UuPwostR5eTXgkp6nlpa65yDK0szsww78keE/J1tOB0d4r+Oe
12ZVzYLQrzQwt2CwIGT9KkAUv7eO7ZTMDsG8MYNnRPGXKgjSZqBSW0MCq7ksz1/P
dHTJmSECgYEA9Q8VbnLkdk5NlRCNTONfjLhNUGmAqt/qLPi7tnJa0wfSpr3tg1Aj
AxnV455fT81vbDP8V8tGDNx+/d5jBkIsdsCYOMa6dqZr1HOfSgbH8wETtU9mbKdB
Gt/frdM3rkJvyd5RjoqMex4U7x4f1OWfThHVzvi8TzqfRSXNpbXBMdUCgYEA4tVa
TBQhwgnjMHl9OYrW1urtf2fKEGOZA/uGJI55O7IL2SoYnVjFUHjlE4D94K/hdijp
l2oFydD0GrWVkWULPvTdUFzMTRPH+SUfXbVqXBMJfHJYrRuZHsJBH2jIwIzu4LaA
hliletIdJUtK8mUvwEb+4TlVUwfCYqkveu1EuhkCgYAHjK5pV6rIJkNnmznvK3YP
HMJs/sMTAJDzT7pgtYcsxynrLyC5EefyOYKIX6GqELclCzjz73Q6AzT6VzaPw8wg
4HAQF7c43oml4uX+XtUcHGViCY8rO7/atxjp/v7RJITTIEE89fG7/UJB15i9c1GE
EzKWDL2oZzLu62o5d676/QKBgQDgiCBhvmvMDs18ZkW2d+BBzTpaKvqxTmVgs9EM
zpripFNmG21SE1T9Wy4mKEEl7/NVaxoObzxbkSKQbb4ntcV0BB4uNi1k/ner/zsV
H0aw7YcuUGHGuNLQx6h+1tIhB2BNv1lposXq1aFUETuWxOKHib8yYfY7wiqATshY
/hRRwQKBgCkT2Wt/XjbXLCpX1viEJOLzYYQkrNyPvruXknCnQQ+0/cq4LoRqWZZR
4RjS0RkoOF4vM38u/hgICiUUCR4bYUy/4O7uhNu5tDle6lIeB+ZxVpRRedE0OfQT
tCpQd0yN9iONBlGyfAtpq7oNzuXgsl1OR4usZ8wCinfImChAl6wv
-----END RSA PRIVATE KEY-----

81
README.md Normal file
View File

@ -0,0 +1,81 @@
# Vue 3 + TypeScript + Vite 6.x + VueRouter 4.x + Pinia 模板
pnpm作为包管理,主要可以解决幽灵依赖包的问题
1. Pinia 状态管理库带加密功能
本地建立.env
文件中`VITE_SECRET_KEY=xxx`
2. 自定义Axios 加密参数 错误日志记录
3. VueRouter 4.x 路由 权限控制 动态缓存
import { removeKeepAliveCache, resetKeepAliveCache } from '@/router/keepAlive'
const instance = getCurrentInstance()!
onBeforeRouteLeave((to) => {
if (to.path === '/C') {
removeKeepAliveCache(instance)
} else {
resetKeepAliveCache(instance)
}
})
通过 removeKeepAliveCache, resetKeepAliveCache 来维护exclude数组里面的内容达到 a 进入C后返回到A页面时,a页面的缓存被清楚,进入其他页面返回则不被清除缓存
权限控制
有身份权限的写在privateRoutes.ts中
没有身份权限的写在publicRoutes.ts中
4. 项目版本检测,确保启动顺利。检测node版本和工具包的版本是否符合要求。
可以根据项目需求修改package.json中的engines字段。
``` shell
pnpm run check-env
```
5. prettier 统一的代码格式,这样保存格式化的时候就不会因为不同的格式化工具导致其他的内容也出现修改。vscode配合prettier插件
.prettierrc.json的内容就是指定代码的格式的配置文件
6. SvgIcon组件,更加优雅的使用svg图片
将图片放入到 /public/icons 下面,name就是图片的名字例如certificate.svg
``` html
<SvgIcon name="certificate" class="w-10 h-10"></SvgIcon>
```
推荐使用较少的svg代码图片,不然html的体积会进一步增大
如果是大的背景图推荐使用`vite`的动态导入
```typescript
const getPath = async () => {
const { default: svg } = await import("/src/assets/svg-img/certificate.svg?raw");
test.value = svg;
};
getPath();
```
7. unocss 即时原子 CSS 引擎。主要想解决遍地css文件的麻烦。预设了tailwind css的预算值。所以可以直接对着tailwind css的官方文档编写
8. composables 这个文件夹推荐将业务逻辑写在此处的x.ts文件内,然后搭配 vue3的组合式语法。解决视图层和业务层的代码分离。
9. 异步加载动态组件, 针对大的页面要加载很多的JS,可以异步错开或者等待条件允许再加载。看个人业务需求
```html
<component :is="currentComponent"></component>
```
```typescript
let currentComponent = ref(defineAsyncComponent(() => import('@/components/TestComponent.vue')))
let flag = false
const handleClick = () => {
if (flag) {
currentComponent.value = defineAsyncComponent(() => import('@/components/TestComponent2.vue'));
} else {
currentComponent.value = defineAsyncComponent(() => import('@/components/TestComponent.vue'));
}
flag = !flag
}
```

16
index.html Normal file
View File

@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<!-- WX JS SDK 微信小程序一定要引入这个文件 可以自己去官方下载最新的-->
<script type="text/javascript" src="/js/jweixin-1.6.0.js"></script>
<script type="text/javascript" src="/js/uni.webview.1.5.6.js" defer></script>
<title></title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

51
package.json Normal file
View File

@ -0,0 +1,51 @@
{
"name": "vue-app-template",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"upload": "bash ./upload.sh",
"build-and-upload": "pnpm run build && pnpm run upload",
"preview": "vite preview",
"check-env": "node ./scripts/checkVersions.cjs"
},
"dependencies": {
"@types/crypto-js": "^4.2.2",
"@types/node": "^22.10.1",
"@unocss/reset": "^66.1.1",
"axios": "^1.7.9",
"crypto-js": "^4.2.0",
"echarts": "^5.6.0",
"echarts-gl": "^2.0.9",
"pinia": "^2.3.0",
"pinia-plugin-persistedstate": "^4.1.3",
"semver": "^7.6.3",
"unocss-preset-px-to-vw-or-vh": "^1.0.6",
"unocss-preset-scrollbar-hide": "^1.0.1",
"vue": "^3.5.13",
"vue-router": "4",
"vue-virtual-draglist": "^3.3.8"
},
"devDependencies": {
"@iconify-json/carbon": "^1.2.8",
"@types/qs": "^6.9.17",
"@unocss/preset-wind": "^0.65.2",
"@vitejs/plugin-basic-ssl": "^1.2.0",
"@vitejs/plugin-vue": "^5.2.1",
"prettier": "3.4.2",
"sass-embedded": "^1.86.0",
"svg-sprite-loader": "^6.0.11",
"typescript": "~5.6.2",
"unocss": "^0.65.2",
"unplugin-auto-import": "^0.19.0",
"vite": "^6.0.1",
"vue-tsc": "^2.1.10"
},
"engines": {
"node": ">=22.11.0",
"pnpm": ">=9.13.2"
},
"preinstall": "npx only-allow pnpm"
}

70
plugins/svgBuilder.ts Normal file
View File

@ -0,0 +1,70 @@
// /plugins/svgBuilder.ts
import {readFileSync, readdirSync} from 'fs'
import {join as pathJoin} from 'path'
import {Plugin} from 'vite'
let idPrefix = ''
const svgTitle = /<svg([^>+].*?)>/
const clearHeightWidth = /(width|height)="([^>+].*?)"/g
const clearFill = /fill="[^>+].*?"/g;
const hasViewBox = /(viewBox="[^>+].*?")/g
const clearReturn = /(\r)|(\n)/g
const findSvgFile = (dir: string) => {
const svgRes: string[] = []
const directory = readdirSync(dir, {withFileTypes: true})
for (const dirent of directory) {
if (dirent?.isDirectory()) {
svgRes.push(...findSvgFile(pathJoin(dir, dirent.name, '/')))
} else {
const svg = readFileSync(pathJoin(dir, dirent.name))
.toString()
.replace(clearReturn, '')
.replace(clearFill,'')
.replace(svgTitle, (_$1: string, $2: string) => {
let width = '0'
let height = '0'
let content = $2.replace(
clearHeightWidth,
(_s1, s2:string, s3:string) => {
if (s2 === 'width') {
width = s3
} else if (s2 === 'height') {
height = s3
}
return ''
}
)
if (!hasViewBox.test($2)) {
content += `viewBox="0 0 ${width} ${height}"`
}
return `<symbol id="${idPrefix}-${dirent.name.replace(
'.svg',
''
)}" ${content}>`
})
.replace('</svg>', '</symbol>')
svgRes.push(svg)
}
}
return svgRes
}
const svgBuilder = (path: string, prefix = 'icon'): Plugin => {
idPrefix = prefix
const res = findSvgFile(path)
return {
name: 'svg-transform',
transformIndexHtml(html) {
return html.replace(
'<body>',
`<body><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="position: absolute; width: 0; height: 0">${res.join('')}</svg>`
)
}
}
}
export default svgBuilder

5309
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

1
public/icons/circle.svg Normal file

File diff suppressed because one or more lines are too long

BIN
public/images/main-bg.png Normal file

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

18
scripts/checkVersions.cjs Normal file
View File

@ -0,0 +1,18 @@
const semver = require('semver')
const { engines } = require('../package')// 这里的package是你的package.json文件路径
const version = engines.node
if (!semver.satisfies(process.version, version)) {
console.log(
[
'node环境变量版本错误',
'你的node环境启动位置' + process.execPath + '.',
'要求环境版本' + version,
'你的环境版本' + process.version
].join('\n')
)
process.exit(1)
}else{
console.log(`echo check environment successfully`)
}

36
src/App.vue Normal file
View File

@ -0,0 +1,36 @@
<template>
<router-view v-slot="{ Component,route }">
<keep-alive :include="include" :exclude="excludes">
<component :is="Component" :key="route.path"/>
</keep-alive>
</router-view>
</template>
<script setup lang="ts">
import { excludes } from '@/router/keepAlive'
const include:string[] = []
</script>
<style scoped>
/* 过渡动画:左右滑动并且淡入淡出 */
.fade-slide-enter-active, .fade-slide-leave-active {
transition: transform 1s ease, opacity 1s ease;
}
/* 进入动画:从右侧进入 */
.fade-slide-enter {
transform: translateX(100%);
opacity: 0;
}
/* 离开动画:从左侧离开 */
.fade-slide-leave-to {
transform: translateX(-100%);
opacity: 0;
}
</style>

205
src/api/customAxios.ts Normal file
View File

@ -0,0 +1,205 @@
import axios, { AxiosRequestConfig } from 'axios'
import { handleUrl } from './encryptUrl'
// 自定义判断元素类型JS
function toType(obj: any): string {
return {}.toString
.call(obj)
.match(/\s([a-zA-Z]+)/)![1]
.toLowerCase()
}
// 参数过滤函数
function filterNull(o: any) {
for (var key in o) {
if (o[key] === null) {
delete o[key]
}
if (toType(o[key]) === 'string') {
o[key] = o[key].trim()
} else if (toType(o[key]) === 'object') {
o[key] = filterNull(o[key])
} else if (toType(o[key]) === 'array') {
o[key] = filterNull(o[key])
}
}
return o
}
/*
接口处理函数
这个函数每个项目都是不一样的,我现在调整的是适用于
https://cnodejs.org/api/v1 的接口,如果是其他接口
需要根据接口的参数进行调整。参考说明文档地址:
https://cnodejs.org/topic/5378720ed6e2d16149fa16bd
主要是,不同的接口的成功标识和失败提示是不一致的。
另外,不同的项目的处理方法也是不一致的,这里出错就是简单的alert
*/
function apiAxios(
method: string,
url: string,
params: null | string | object,
success: any,
failure: any,
unEncrypt: boolean = false // 是否不加密
) {
let contentTypeIsJson = false
if (params && typeof params != 'string') {
params = filterNull(params)
} else contentTypeIsJson = true
axios({
method: method,
url: url,
data: method === 'POST' || method === 'PUT' ? params : null,
params: method === 'GET' || method === 'DELETE' ? params : null,
withCredentials: true,
crossDomain: true,
unEncrypt,
transformRequest: [
function (data) {
if (contentTypeIsJson) return data
let ret = ''
for (let it in data) {
ret +=
encodeURIComponent(it) + '=' + encodeURIComponent(data[it]) + '&'
}
return ret
},
],
headers: {
'Content-Type': contentTypeIsJson
? 'application/json'
: 'application/x-www-form-urlencoded',
},
} as AxiosRequestConfig<any>)
.then(function (res) {
let response = res.data
if (response.code == 200) {
if (success) {
success(response)
}
} else {
if (failure) {
failure(response)
} else {
if (response.code == 2) {
//错误处理
setTimeout(() => {
location.reload()
}, 1000)
} else {
//错误处理
}
}
}
})
.catch(function (err) {
let res = err.response
console.error(res || err)
if (res) {
// 清楚所有的错误提示
clearTimeout(timeObj)
if (res.data.msg) {
//错误处理
} else {
//错误处理
}
return
}
})
}
let requestCount = 0
let timeObj: NodeJS.Timeout
// http request 拦截器
axios.interceptors.request.use((config) => {
requestCount++
if (requestCount == 1) {
timeObj = setTimeout(() => {
//加载中提示
}, 800)
}
if (
config.data &&
Object.prototype.toString.call(config.data) == '[object FormData]'
) {
config.headers!!['Content-Type'] = 'multipart/form-data;charset=utf-8'
config.transformRequest = [
function (data) {
return data
},
]
}
// 拦截配置,有新的配置,在这里新增函数处理,然后合并config
let _config = handleUrl(config)
config = Object.assign(config, _config)
return config
})
// http response 拦截器
axios.interceptors.response.use((response) => {
requestCount--
if (requestCount === 0) {
setTimeout(() => {
// 关闭所有提示
}, 1500)
clearTimeout(timeObj)
}
return response
},error =>{
// let xhrErrL = { type: "XHRERR", data: error.response };
if (error.response) {
const { status, data } = error.response;
if (status === 422) {
alert(data)
}
}
})
// 返回在vue模板中的调用接口
export default {
get: function (
url: string,
params: string | object | null,
success: any,
failure: any
) {
return apiAxios('GET', url, params, success, failure)
},
post: function (
url: string,
params: string | object,
success: any,
failure: any
) {
return apiAxios('POST', url, params, success, failure)
},
put: function (
url: string,
params: string | object,
success: any,
failure: any
) {
return apiAxios('PUT', url, params, success, failure)
},
delete: function (
url: string,
params: string | object,
success: any,
failure: any
) {
return apiAxios('DELETE', url, params, success, failure)
},
// 不加密的post请求
unEncryptPost: function (
url: string,
params: string | object,
success: any,
failure: any,
) {
return apiAxios('POST', url, params, success, failure,true)
}
}

12
src/api/encryptUrl.ts Normal file
View File

@ -0,0 +1,12 @@
// 对指定请求链接进行加密
import { AxiosRequestConfig } from "axios";
import { useUserStore } from "@/store/user";
export const handleUrl = async (config: (AxiosRequestConfig & {unEncrypt?: boolean})) => {
const userStore = useUserStore()
if(userStore.getToken){
config.headers!!["Authorization"] = `Bearer ${userStore.getToken}`
}
return config;
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1"><defs><linearGradient x1="0.5" y1="0" x2="0.5" y2="1" id="master_svg0_95_7905"><stop offset="0%" stop-color="#23518C" stop-opacity="0.20000000298023224"/><stop offset="100%" stop-color="#17439D" stop-opacity="0.10000000149011612"/></linearGradient><radialGradient cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" id="master_svg1_95_7906" gradientTransform="translate(272.25 210.25) rotate(90) scale(33.016276359558105 293.6848504078262)"><stop offset="0%" stop-color="#2773FF" stop-opacity="0.10000000149011612"/><stop offset="100%" stop-color="#2773FF" stop-opacity="0"/></radialGradient><radialGradient cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" id="master_svg2_95_7907" gradientTransform="translate(272.25 210.25) rotate(90) scale(7.3813605308532715 170.33950726831247)"><stop offset="0%" stop-color="#2773FF" stop-opacity="0.20000000298023224"/><stop offset="100%" stop-color="#2773FF" stop-opacity="0"/></radialGradient><radialGradient cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" id="master_svg3_95_7908" gradientTransform="translate(272.25 210.25) rotate(90) scale(3.5998570919036865 121.80235547062695)"><stop offset="0%" stop-color="#2773FF" stop-opacity="0.5"/><stop offset="100%" stop-color="#2773FF" stop-opacity="0"/></radialGradient><linearGradient x1="0.5" y1="0.05106060206890106" x2="0.5" y2="1" id="master_svg4_81_5443"><stop offset="0%" stop-color="#525DFF" stop-opacity="0"/><stop offset="100%" stop-color="#525DFF" stop-opacity="0.47999998927116394"/></linearGradient><radialGradient cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" id="master_svg5_13_2169" gradientTransform="translate(248.49790859222412 1.25) rotate(0) scale(5.699835777282715 Infinity)"><stop offset="0%" stop-color="#4FE4FF" stop-opacity="1"/><stop offset="100%" stop-color="#2773FF" stop-opacity="0"/></radialGradient><linearGradient x1="1" y1="0.5" x2="0" y2="0.5" id="master_svg6_13_2170"><stop offset="0%" stop-color="#36C8FF" stop-opacity="0"/><stop offset="53.635478019714355%" stop-color="#27AEFF" stop-opacity="1"/><stop offset="100%" stop-color="#1997FF" stop-opacity="0.20000000298023224"/></linearGradient></defs><g><g><path d="M0.25,0.25L544.25,0.25L544.25,210.25L0.25,210.25L0.25,0.25Z" fill-rule="evenodd" fill="#000000" fill-opacity="0.25"/><path d="M0.25,0.25L544.25,0.25L544.25,210.25L0.25,210.25L0.25,0.25Z" fill-rule="evenodd" fill="url(#master_svg0_95_7905)" fill-opacity="1"/><path d="M0.25,0.25L544.25,0.25L544.25,210.25L0.25,210.25L0.25,0.25Z" fill-rule="evenodd" fill="url(#master_svg1_95_7906)" fill-opacity="1"/><path d="M0.25,0.25L544.25,0.25L544.25,210.25L0.25,210.25L0.25,0.25Z" fill-rule="evenodd" fill="url(#master_svg2_95_7907)" fill-opacity="1"/><path d="M0.25,0.25L544.25,0.25L544.25,210.25L0.25,210.25L0.25,0.25Z" fill-rule="evenodd" fill="url(#master_svg3_95_7908)" fill-opacity="1"/><path d="M544.5,0L0,0L0,210.5L544.5,210.5L544.5,0ZM0.5,210L0.5,0.5L544,0.5L544,210L0.5,210Z" fill-rule="evenodd" fill="url(#master_svg4_81_5443)" fill-opacity="1"/></g><g style="opacity:0.44999998807907104;"><path d="M478.25,1.75L287.725,1.75L243.498,1.75L62.25,1.75L62.25,0.75L243.498,0.75L287.725,0.75L478.25,0.75L478.25,1.75Z" fill-rule="evenodd" fill="url(#master_svg5_13_2169)" fill-opacity="1"/><path d="M478.25,1.75L287.725,1.75L243.498,1.75L62.25,1.75L62.25,0.75L243.498,0.75L287.725,0.75L478.25,0.75L478.25,1.75Z" fill-rule="evenodd" fill="url(#master_svg6_13_2170)" fill-opacity="1"/></g></g></svg>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" viewBox="0 0 127 130.5"><defs><radialGradient cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" id="master_svg0_85_02406" gradientTransform="translate(62.63833460211754 260.6206922531128) rotate(90) scale(129.7585763875395 128.02340110725947)"><stop offset="0%" stop-color="#4EAAFF" stop-opacity="1"/><stop offset="36.93000078201294%" stop-color="#0066FF" stop-opacity="0.5"/><stop offset="100%" stop-color="#000000" stop-opacity="0"/></radialGradient></defs><g><g transform="matrix(1,0,0,-1,0,261)" style="opacity:0.10000000149011612;"><path d="M63.5,130.5C28.4311,130.5,0,188.7103,0,260.5C0,260.5,127,260.5,127,260.5C127,188.6801,98.5743,130.5,63.5,130.5C63.5,130.5,63.5,130.5,63.5,130.5Z" fill="url(#master_svg0_85_02406)" fill-opacity="1"/></g><g style="opacity:0.20000000298023224;"><path d="M127,102L1,102L1,101L127,101L127,102Z" fill-rule="evenodd" fill="#0867A2" fill-opacity="1"/></g><g><path d="M126,1L0,1L0,0L126,0L126,1Z" fill-rule="evenodd" fill="#0867A2" fill-opacity="1"/></g><g><path d="M12,1L0,1L0,0L12,0L12,1Z" fill-rule="evenodd" fill="#00AAFF" fill-opacity="1"/></g></g></svg>

File diff suppressed because one or more lines are too long

88
src/auto-imports.d.ts vendored Normal file
View File

@ -0,0 +1,88 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
const EffectScope: typeof import('vue')['EffectScope']
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
const computed: typeof import('vue')['computed']
const createApp: typeof import('vue')['createApp']
const createPinia: typeof import('pinia')['createPinia']
const customRef: typeof import('vue')['customRef']
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: typeof import('vue')['defineComponent']
const defineStore: typeof import('pinia')['defineStore']
const effectScope: typeof import('vue')['effectScope']
const getActivePinia: typeof import('pinia')['getActivePinia']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const h: typeof import('vue')['h']
const inject: typeof import('vue')['inject']
const isProxy: typeof import('vue')['isProxy']
const isReactive: typeof import('vue')['isReactive']
const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef']
const mapActions: typeof import('pinia')['mapActions']
const mapGetters: typeof import('pinia')['mapGetters']
const mapState: typeof import('pinia')['mapState']
const mapStores: typeof import('pinia')['mapStores']
const mapWritableState: typeof import('pinia')['mapWritableState']
const markRaw: typeof import('vue')['markRaw']
const nextTick: typeof import('vue')['nextTick']
const onActivated: typeof import('vue')['onActivated']
const onBeforeMount: typeof import('vue')['onBeforeMount']
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onDeactivated: typeof import('vue')['onDeactivated']
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
const onMounted: typeof import('vue')['onMounted']
const onRenderTracked: typeof import('vue')['onRenderTracked']
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
const onScopeDispose: typeof import('vue')['onScopeDispose']
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
const provide: typeof import('vue')['provide']
const reactive: typeof import('vue')['reactive']
const readonly: typeof import('vue')['readonly']
const ref: typeof import('vue')['ref']
const resolveComponent: typeof import('vue')['resolveComponent']
const setActivePinia: typeof import('pinia')['setActivePinia']
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
const shallowReactive: typeof import('vue')['shallowReactive']
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const storeToRefs: typeof import('pinia')['storeToRefs']
const toRaw: typeof import('vue')['toRaw']
const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs']
const toValue: typeof import('vue')['toValue']
const triggerRef: typeof import('vue')['triggerRef']
const unref: typeof import('vue')['unref']
const useAttrs: typeof import('vue')['useAttrs']
const useCssModule: typeof import('vue')['useCssModule']
const useCssVars: typeof import('vue')['useCssVars']
const useId: typeof import('vue')['useId']
const useLink: typeof import('vue-router')['useLink']
const useModel: typeof import('vue')['useModel']
const useRoute: typeof import('vue-router')['useRoute']
const useRouter: typeof import('vue-router')['useRouter']
const useSlots: typeof import('vue')['useSlots']
const useTemplateRef: typeof import('vue')['useTemplateRef']
const watch: typeof import('vue')['watch']
const watchEffect: typeof import('vue')['watchEffect']
const watchPostEffect: typeof import('vue')['watchPostEffect']
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
}
// for type re-export
declare global {
// @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
}

View File

@ -0,0 +1,25 @@
<template>
<div v-html="content">
</div>
</template>
<script setup lang="ts">
defineProps({
content:{
type:String,
default: ""
}
})
</script>
<style scoped>
:deep(svg){
width:100%;
height:100%;
vertical-align: middle;
overflow: hidden;
object-fit: contain;
}
</style>

View File

@ -0,0 +1,5 @@
<template>
</template>
<script lang="ts" setup></script>

View File

@ -0,0 +1,315 @@
<template>
<div class="chart-container" v-echartResize>
<div class="chart" ref="chartRef"></div>
<!-- 底座背景 -->
<div class="bg"></div>
<!-- 中间文字 -->
<div class="center-text">
<div class="value">{{ totalValue }}</div>
<div class="unit">人</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, reactive, computed } from 'vue'
import { getPie3D, getParametricEquation } from '@/composables/useEchart'
import * as echarts from 'echarts'
import 'echarts-gl' // 3d图表库
import { fitChartSize } from '@/utils/echartData'
import { vEchartResize } from '@/directives/chart-resize'
const color = ['#5AF3B8', '#93DBFF']
const props = defineProps({
optionData: {
type: Array,
default: () => []
}
})
// refs
const chartRef = ref(null)
let statusChart = ref(null)
// 计算数据总和
const totalValue = computed(() => {
return props.optionData.reduce((sum, item) => sum + item.value, 0)
})
const option = reactive({})
// 初始化label样式
const setLabel = () => {
props.optionData.forEach((item, index) => {
item.itemStyle = {
color: color[index]
}
item.label = {
show: true,
color: color[index],
formatter: '{b} {d}%', // 简化格式化字符串,避免使用数组join和富文本
rich: {
b: {
color: color[index],
lineHeight: 25,
align: 'left',
fontSize: 12,
},
d: {
color: color[index],
align: 'left',
fontSize: 12
}
}
}
item.labelLine = {
lineStyle: {
width: 1,
color: color[index]
}
}
})
}
// 图表初始化
const initChart = () => {
statusChart.value = echarts.init(chartRef.value)
// 传入数据生成 option, 构建3d饼状图, 参数工具文件已经备注的很详细
Object.assign(option, getPie3D(props.optionData, 0.8, 240, 36, 13, 0.5))
statusChart.value.setOption(option)
// 是否需要label指引线,如果要就添加一个透明的2d饼状图并调整角度使得labelLine和3d的饼状图对齐,并再次setOption
option.series.push({
name: '', //自己根据场景修改
backgroundColor: 'transparent',
type: 'pie',
label: {
opacity: 1,
fontSize: 12,
},
startAngle: -40, // 起始角度,支持范围[0, 360]。
clockwise: false, // 饼图的扇区是否是顺时针排布。上述这两项配置主要是为了对齐3d的样式
radius: ['20%', '50%'],
center: ['50%', '50%'],
data: props.optionData,
itemStyle: {
opacity: 0 //这里必须是0,不然2d的图会覆盖在表面
}
})
statusChart.value.setOption(option)
bindListen(statusChart.value)
}
// 监听鼠标事件,实现饼图选中效果(单选),近似实现高亮(放大)效果。
// optionName是防止有多个图表进行定向option传递,单个图表可以不传,默认是opiton
const bindListen = (myChart) => {
let selectedIndex = ''
let hoveredIndex = ''
// 监听点击事件,实现选中效果(单选)
myChart.on('click', (params) => {
// 确保params.seriesIndex有效且对应的对象存在
if (params.seriesIndex === undefined || !option.series[params.seriesIndex] || !option.series[params.seriesIndex].pieStatus) {
return;
}
// 从 option.series 中读取重新渲染扇形所需的参数,将是否选中取反。
const isSelected = !option.series[params.seriesIndex].pieStatus.selected
const isHovered = option.series[params.seriesIndex].pieStatus.hovered
const k = option.series[params.seriesIndex].pieStatus.k
const startRatio = option.series[params.seriesIndex].pieData.startRatio
const endRatio = option.series[params.seriesIndex].pieData.endRatio
// 如果之前选中过其他扇形,将其取消选中(对 option 更新)
if (selectedIndex !== '' && selectedIndex !== params.seriesIndex &&
option.series[selectedIndex] && option.series[selectedIndex].pieStatus) {
option.series[selectedIndex].parametricEquation = getParametricEquation(
option.series[selectedIndex].pieData.startRatio,
option.series[selectedIndex].pieData.endRatio,
false,
false,
k,
option.series[selectedIndex].pieData.value
)
option.series[selectedIndex].pieStatus.selected = false
}
// 对当前点击的扇形,执行选中/取消选中操作(对 option 更新)
option.series[params.seriesIndex].parametricEquation = getParametricEquation(
startRatio,
endRatio,
isSelected,
isHovered,
k,
option.series[params.seriesIndex].pieData.value
)
option.series[params.seriesIndex].pieStatus.selected = isSelected
// 如果本次是选中操作,记录上次选中的扇形对应的系列号 seriesIndex
selectedIndex = isSelected ? params.seriesIndex : null
// 使用更新后的 option,渲染图表
myChart.setOption(option)
})
// 监听 mouseover,近似实现高亮(放大)效果
myChart.on('mouseover', (params) => {
// 准备重新渲染扇形所需的参数
let isSelected
let isHovered
let startRatio
let endRatio
let k
// 如果触发 mouseover 的扇形当前已高亮,则不做操作
if (hoveredIndex === params.seriesIndex) {
// 否则进行高亮及必要的取消高亮操作
} else {
// 如果当前有高亮的扇形,取消其高亮状态(对 option 更新)
if (hoveredIndex !== '' && option.series[hoveredIndex] && option.series[hoveredIndex].pieStatus) {
// 从 option.series 中读取重新渲染扇形所需的参数,将是否高亮设置为 false。
isSelected = option.series[hoveredIndex].pieStatus.selected
isHovered = false
startRatio = option.series[hoveredIndex].pieData.startRatio
endRatio = option.series[hoveredIndex].pieData.endRatio
k = option.series[hoveredIndex].pieStatus.k
// 对当前点击的扇形,执行取消高亮操作(对 option 更新)
option.series[hoveredIndex].parametricEquation = getParametricEquation(
startRatio,
endRatio,
isSelected,
isHovered,
k,
option.series[hoveredIndex].pieData.value
)
option.series[hoveredIndex].pieStatus.hovered = isHovered
// 将此前记录的上次选中的扇形对应的系列号 seriesIndex 清空
hoveredIndex = ''
}
// 如果触发 mouseover 的扇形不是透明圆环,将其高亮(对 option 更新)
if (
params.seriesName !== 'mouseoutSeries' &&
params.seriesName !== 'pie2d' &&
params.seriesIndex !== undefined &&
option.series[params.seriesIndex] &&
option.series[params.seriesIndex].pieStatus
) {
// 从 option.series 中读取重新渲染扇形所需的参数,将是否高亮设置为 true。
isSelected = option.series[params.seriesIndex].pieStatus.selected
isHovered = true
startRatio = option.series[params.seriesIndex].pieData.startRatio
endRatio = option.series[params.seriesIndex].pieData.endRatio
k = option.series[params.seriesIndex].pieStatus.k
// 对当前点击的扇形,执行高亮操作(对 option 更新)
option.series[params.seriesIndex].parametricEquation = getParametricEquation(
startRatio,
endRatio,
isSelected,
isHovered,
k,
option.series[params.seriesIndex].pieData.value + 60
)
option.series[params.seriesIndex].pieStatus.hovered = isHovered
// 记录上次高亮的扇形对应的系列号 seriesIndex
hoveredIndex = params.seriesIndex
}
// 使用更新后的 option,渲染图表
myChart.setOption(option)
}
})
// 修正取消高亮失败的 bug
myChart.on('globalout', () => {
// 准备重新渲染扇形所需的参数
let isSelected
let isHovered
let startRatio
let endRatio
let k
if (hoveredIndex !== '' && option.series[hoveredIndex] && option.series[hoveredIndex].pieStatus) {
// 从 option.series 中读取重新渲染扇形所需的参数,将是否高亮设置为 true。
isSelected = option.series[hoveredIndex].pieStatus.selected
isHovered = false
k = option.series[hoveredIndex].pieStatus.k
startRatio = option.series[hoveredIndex].pieData.startRatio
endRatio = option.series[hoveredIndex].pieData.endRatio
// 对当前点击的扇形,执行取消高亮操作(对 option 更新)
option.series[hoveredIndex].parametricEquation = getParametricEquation(
startRatio,
endRatio,
isSelected,
isHovered,
k,
option.series[hoveredIndex].pieData.value
)
option.series[hoveredIndex].pieStatus.hovered = isHovered
// 将此前记录的上次选中的扇形对应的系列号 seriesIndex 清空
hoveredIndex = ''
}
// 使用更新后的 option,渲染图表
myChart.setOption(option)
})
}
// 自适应宽高
// const changeSize = () => {
// statusChart.value?.resize()
// }
// 生命周期钩子
onMounted(() => {
setLabel()
initChart()
// 根据窗口变化自动调节图表大小
// window.addEventListener('resize', changeSize)
})
// onBeforeUnmount(() => {
// window.removeEventListener('resize', changeSize)
// })
</script>
<style lang='scss' scoped>
.chart-container {
position: relative;
width: 100%;
height: 100%;
.chart,
.bg {
width: 100%;
height: 100%;
}
.bg {
position: absolute;
bottom: 50px;
left: 50%;
z-index: -1;
width: 180px;
height: 73px;
background: no-repeat center;
background-image: url('/images/particles-bg.png');
background-size: 100% 100%;
transform: translateX(-50%);
}
.center-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 10;
text-align: center;
.value {
display: inline-block;
font-size: 24px;
font-weight: 500;
color: #8FC8FF;
}
.unit {
display: inline-block;
font-size: 18px;
font-weight: 500;
color: #FFFFFF;
}
}
}
</style>

View File

@ -0,0 +1,122 @@
<template>
<div
class="progress-container"
:style="{
'--progress-width': `${percentage}%`,
'--progress-start-color': startColor,
'--progress-end-color': endColor || startColor,
'--progress-bg-color': backgroundColor,
'--progress-height': height
}"
>
<div class="progress-bar">
<div class="progress-inner">
<div class="progress-text" v-if="showText">
{{ percentage }}%
</div>
</div>
</div>
<slot></slot>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
interface ProgressProps {
percentage: number
startColor?: string
endColor?: string
backgroundColor?: string
showText?: boolean
striped?: boolean
animated?: boolean
height?: string
}
const props = withDefaults(defineProps<ProgressProps>(), {
percentage: 0,
startColor: 'rgba(41,241,250,0)',
endColor: '#29F1FA',
backgroundColor: '#0e2e65',
showText: false,
striped: false,
animated: false,
height: '30px'
})
// 确保百分比在0-100之间
const percentage = computed(() => {
if (props.percentage < 0) return 0
if (props.percentage > 100) return 100
return props.percentage
})
</script>
<style lang="scss" scoped>
.progress-container {
--progress-width: 0%;
--progress-start-color: #44F1FF;
--progress-end-color: #44F1FF;
--progress-bg-color: #031743;
width: 100%;
position: relative;
}
.progress-bar {
width: 100%;
height: var(--progress-height);
background-color: var(--progress-bg-color);
border-radius: 15px;
overflow: hidden;
position: relative;
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
}
.progress-inner {
position: absolute;
left: 0;
top: 0;
height: 100%;
width: var(--progress-width);
background: linear-gradient(90deg, var(--progress-start-color), var(--progress-end-color));
border-radius: 15px;
transition: width 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 0 10px 1px var(--progress-start-color);
// 条纹效果
&::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image: v-bind('props.striped ? "linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent)" : "none"');
background-size: 40px 40px;
opacity: 0.6;
z-index: 1;
animation: v-bind('props.animated && props.striped ? "progress-animation 2s linear infinite" : "none"');
}
}
.progress-text {
color: #fff;
font-size: 14px;
position: relative;
z-index: 2;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
}
@keyframes progress-animation {
0% {
background-position: 40px 0;
}
100% {
background-position: 0 0;
}
}
</style>

View File

@ -0,0 +1,35 @@
<script setup lang="ts">
export interface SvgIconProps {
name: string
}
const props = defineProps<SvgIconProps>()
const styles = useCssModule()
const attrs = useAttrs()
const iconName = computed(() => `#icon-${props.name}`);
const svgClass = computed(() => {
const className = [styles['svg-icon']]
if (props.name) className.push(`icon-${props.name}`)
return className
})
</script>
<template>
<svg :class="svgClass" v-bind="attrs">
<use :xlink:href="iconName"></use>
</svg>
</template>
<style module>
.svg-icon {
width: 1em;
height: 1em;
fill: currentColor;
vertical-align: middle;
overflow: hidden;
}
</style>

View File

@ -0,0 +1,10 @@
import SvgIcon from './SvgIcon.vue'
import type {SvgIconProps} from './SvgIcon.vue'
export {
SvgIcon
}
export type {
SvgIconProps
}

View File

@ -0,0 +1,278 @@
<template>
<div class="digital-watch" :style="{
'--size-scale': sizeScale,
'--color-led': ledColor,
'--color-led-off': ledOffColor,
'--color-background': backgroundColor,
'--color-glow': glowColor
}">
<div class="time-container">
<div class="digit-group">
<div class="seven-segment">
<div :class="getSegmentClass(hours[0], 0)" class="segment-a"></div>
<div :class="getSegmentClass(hours[0], 1)" class="segment-b"></div>
<div :class="getSegmentClass(hours[0], 2)" class="segment-c"></div>
<div :class="getSegmentClass(hours[0], 3)" class="segment-d"></div>
<div :class="getSegmentClass(hours[0], 4)" class="segment-e"></div>
<div :class="getSegmentClass(hours[0], 5)" class="segment-f"></div>
<div :class="getSegmentClass(hours[0], 6)" class="segment-g"></div>
</div>
<div class="seven-segment">
<div :class="getSegmentClass(hours[1], 0)" class="segment-a"></div>
<div :class="getSegmentClass(hours[1], 1)" class="segment-b"></div>
<div :class="getSegmentClass(hours[1], 2)" class="segment-c"></div>
<div :class="getSegmentClass(hours[1], 3)" class="segment-d"></div>
<div :class="getSegmentClass(hours[1], 4)" class="segment-e"></div>
<div :class="getSegmentClass(hours[1], 5)" class="segment-f"></div>
<div :class="getSegmentClass(hours[1], 6)" class="segment-g"></div>
</div>
</div>
<div class="separator">
<div class="dot"></div>
<div class="dot"></div>
</div>
<div class="digit-group">
<div class="seven-segment">
<div :class="getSegmentClass(minutes[0], 0)" class="segment-a"></div>
<div :class="getSegmentClass(minutes[0], 1)" class="segment-b"></div>
<div :class="getSegmentClass(minutes[0], 2)" class="segment-c"></div>
<div :class="getSegmentClass(minutes[0], 3)" class="segment-d"></div>
<div :class="getSegmentClass(minutes[0], 4)" class="segment-e"></div>
<div :class="getSegmentClass(minutes[0], 5)" class="segment-f"></div>
<div :class="getSegmentClass(minutes[0], 6)" class="segment-g"></div>
</div>
<div class="seven-segment">
<div :class="getSegmentClass(minutes[1], 0)" class="segment-a"></div>
<div :class="getSegmentClass(minutes[1], 1)" class="segment-b"></div>
<div :class="getSegmentClass(minutes[1], 2)" class="segment-c"></div>
<div :class="getSegmentClass(minutes[1], 3)" class="segment-d"></div>
<div :class="getSegmentClass(minutes[1], 4)" class="segment-e"></div>
<div :class="getSegmentClass(minutes[1], 5)" class="segment-f"></div>
<div :class="getSegmentClass(minutes[1], 6)" class="segment-g"></div>
</div>
</div>
<div class="separator">
<div class="dot"></div>
<div class="dot"></div>
</div>
<div class="digit-group">
<div class="seven-segment">
<div :class="getSegmentClass(seconds[0], 0)" class="segment-a"></div>
<div :class="getSegmentClass(seconds[0], 1)" class="segment-b"></div>
<div :class="getSegmentClass(seconds[0], 2)" class="segment-c"></div>
<div :class="getSegmentClass(seconds[0], 3)" class="segment-d"></div>
<div :class="getSegmentClass(seconds[0], 4)" class="segment-e"></div>
<div :class="getSegmentClass(seconds[0], 5)" class="segment-f"></div>
<div :class="getSegmentClass(seconds[0], 6)" class="segment-g"></div>
</div>
<div class="seven-segment">
<div :class="getSegmentClass(seconds[1], 0)" class="segment-a"></div>
<div :class="getSegmentClass(seconds[1], 1)" class="segment-b"></div>
<div :class="getSegmentClass(seconds[1], 2)" class="segment-c"></div>
<div :class="getSegmentClass(seconds[1], 3)" class="segment-d"></div>
<div :class="getSegmentClass(seconds[1], 4)" class="segment-e"></div>
<div :class="getSegmentClass(seconds[1], 5)" class="segment-f"></div>
<div :class="getSegmentClass(seconds[1], 6)" class="segment-g"></div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { useDate } from '@/composables/useDate';
// 尺寸缩放比例
const sizeScale = ref(1);
// 颜色设置
const ledColor = ref('#44F1FF'); // LED点亮时的颜色
const ledOffColor = ref('#031743'); // LED熄灭时的颜色
const backgroundColor = ref('#031743'); // 背景颜色
const glowColor = ref('#44F1FF'); // 发光效果颜色
// 定义每个数字对应的七段显示状态 [a, b, c, d, e, f, g]
const digitSegments: { [key: string]: boolean[] } = {
'0': [true, true, true, true, true, true, false], // 0: 显示a,b,c,d,e,f
'1': [false, true, true, false, false, false, false], // 1: 只显示b,c
'2': [true, true, false, true, true, false, true], // 2: 显示a,b,d,e,g
'3': [true, true, true, true, false, false, true], // 3: 显示a,b,c,d,g
'4': [false, true, true, false, false, true, true], // 4: 显示b,c,f,g
'5': [true, false, true, true, false, true, true], // 5: 显示a,c,d,f,g
'6': [true, false, true, true, true, true, true], // 6: 显示a,c,d,e,f,g
'7': [true, true, true, false, false, false, false], // 7: 显示a,b,c
'8': [true, true, true, true, true, true, true], // 8: 显示所有段
'9': [true, true, true, true, false, true, true], // 9: 显示a,b,c,d,f,g
' ': [false, false, false, false, false, false, false], // 空格: 不显示任何段
};
const getSegmentClass = (digit: string, index: number) => {
const segments = digitSegments[digit] || digitSegments[' '];
return {
'segment': true,
'segment-on': segments[index]
};
};
let timer: number | null = null;
let { hours, minutes, seconds, updateTime } = useDate()
onMounted(() => {
updateTime();
timer = window.setInterval(updateTime, 500);
});
onUnmounted(() => {
if (timer) {
clearInterval(timer);
timer = null;
}
});
</script>
<style lang="scss" scoped>
.digital-watch {
// 基础尺寸定义,通过CSS变量和缩放比例结合使用
--size-scale: 1;
--base-segment-width: 5px;
--base-segment-height: 2px;
--base-segment-vertical-width: 2px;
--base-segment-vertical-height: 5px;
--base-digit-width: 9px;
--base-digit-height: 14px;
--base-digit-gap: 5px;
--base-dot-size: 2px;
--base-separator-margin: 2px;
// 颜色变量定义
--color-led: #44F1FF; // 默认LED点亮颜色 (红色)
--color-led-off: #031743; // 默认LED熄灭颜色
--color-background: #031743; // 默认背景颜色
--color-glow: #031743; // 默认发光效果颜色
// 应用缩放后的尺寸
--segment-width: calc(var(--base-segment-width) * var(--size-scale));
--segment-height: calc(var(--base-segment-height) * var(--size-scale));
--segment-vertical-width: calc(var(--base-segment-vertical-width) * var(--size-scale));
--segment-vertical-height: calc(var(--base-segment-vertical-height) * var(--size-scale));
--segment-spacing: calc(2px * var(--size-scale));
--digit-width: calc(var(--base-digit-width) * var(--size-scale));
--digit-height: calc(var(--base-digit-height) * var(--size-scale));
--digit-gap: calc(var(--base-digit-gap) * var(--size-scale));
--dot-size: calc(var(--base-dot-size) * var(--size-scale));
--separator-margin: calc(var(--base-separator-margin) * var(--size-scale));
--border-radius: calc(3px * var(--size-scale));
--glow-radius: calc(5px * var(--size-scale));
--clock-padding: calc(20px * var(--size-scale));
--clock-border-radius: calc(10px * var(--size-scale));
background-color: var(--color-background);
border-radius: var(--clock-border-radius);
display: flex;
flex-direction: column;
align-items: center;
width: fit-content;
}
.time-container {
display: flex;
align-items: center;
}
.digit-group {
display: flex;
gap: var(--digit-gap);
}
.separator {
display: flex;
flex-direction: column;
justify-content: space-around;
height: var(--digit-height);
margin: 0 var(--separator-margin);
.dot {
width: var(--dot-size);
height: var(--dot-size);
background-color: var(--color-led);
border-radius: 50%;
}
}
.seven-segment {
position: relative;
width: var(--digit-width);
height: var(--digit-height);
}
.segment {
position: absolute;
background-color: var(--color-led-off);
border-radius: var(--segment-spacing);
&-on {
background-color: var(--color-led);
// box-shadow: 0 0 var(--glow-radius) var(--color-glow);
}
}
.segment-a {
width: var(--segment-width);
height: var(--segment-height);
top: 0;
left: calc((var(--digit-width) - var(--segment-width)) / 2);
border-radius: var(--border-radius);
}
.segment-b {
width: var(--segment-vertical-width);
height: var(--segment-vertical-height);
top: var(--segment-spacing);
right: 0;
border-radius: var(--border-radius);
}
.segment-c {
width: var(--segment-vertical-width);
height: var(--segment-vertical-height);
bottom: var(--segment-spacing);
right: 0;
border-radius: var(--border-radius);
}
.segment-d {
width: var(--segment-width);
height: var(--segment-height);
bottom: 0;
left: calc((var(--digit-width) - var(--segment-width)) / 2);
border-radius: var(--border-radius);
}
.segment-e {
width: var(--segment-vertical-width);
height: var(--segment-vertical-height);
bottom: var(--segment-spacing);
left: 0;
border-radius: var(--border-radius);
}
.segment-f {
width: var(--segment-vertical-width);
height: var(--segment-vertical-height);
top: var(--segment-spacing);
left: 0;
border-radius: var(--border-radius);
}
.segment-g {
width: var(--segment-width);
height: var(--segment-height);
top: calc(var(--digit-height) / 2 - var(--segment-height) / 2);
left: calc((var(--digit-width) - var(--segment-width)) / 2);
border-radius: var(--border-radius);
}
</style>

View File

@ -0,0 +1,40 @@
export const useDate = () => {
const hours = ref("00");
const minutes = ref("00");
const seconds = ref("00");
const year = ref("");
const month = ref("");
const day = ref("");
const weekday = ref(""); // 新增星期几变量
let timer: number | null = null;
const updateTime = () => {
const now = new Date();
hours.value = now.getHours().toString().padStart(2, "0");
minutes.value = now.getMinutes().toString().padStart(2, "0");
seconds.value = now.getSeconds().toString().padStart(2, "0");
year.value = now.getFullYear().toString();
month.value = (now.getMonth() + 1).toString().padStart(2, "0");
day.value = now.getDate().toString().padStart(2, "0");
// 获取星期几并转换为中文格式
const weekdayNumber = now.getDay(); // 0是星期日,1-6是星期一至星期六
const weekdayNames = ["日", "一", "二", "三", "四", "五", "六"];
weekday.value = "星期" + weekdayNames[weekdayNumber];
};
return {
hours,
minutes,
seconds,
year,
month,
day,
weekday, // 返回星期几
timer,
updateTime,
};
};

View File

@ -0,0 +1,32 @@
// 自定义 Hook
export const useScreenAuto = (designWidth = 1920, designHeight = 1080) => {
const screenEl = useTemplateRef<HTMLDivElement>("screen")
const handleScreenAuto = () => {
const clientWidth = document.documentElement.clientWidth
const clientHeight = document.documentElement.clientHeight
const scale = clientWidth / clientHeight < designWidth / designHeight
? clientWidth / designWidth
: clientHeight / designHeight
if (screenEl.value) {
screenEl.value.style.transform = `scale(${scale}) translate(-50%, -50%)`
screenEl.value.classList.add('screen')
}
}
// 生命周期
onMounted(() => {
handleScreenAuto()
window.addEventListener('resize', handleScreenAuto)
})
onUnmounted(() => {
window.removeEventListener('resize', handleScreenAuto)
})
return {
handleScreenAuto
}
}

View File

@ -0,0 +1,214 @@
// 定义二进制数字类型
type BinaryDigits = Record<string, boolean[]>;
type CanvasRef = HTMLCanvasElement | null;
/**
* 数字电子表Hook
* @returns {Object} - 包含canvas引用和初始化方法
*/
export const useDigitalWatch = () => {
const canvasRef = ref<CanvasRef>(null)
let animationTimer: number | null = null
// 二进制表示数字的配置
const binary: BinaryDigits = {
"0": [true, true, true, false, true, true, true],
"1": [false, false, true, false, false, true, false],
"2": [true, false, true, true, true, false, true],
"3": [true, false, true, true, false, true, true],
"4": [false, true, true, true, false, true, false],
"5": [true, true, false, true, false, true, true],
"6": [true, true, false, true, true, true, true],
"7": [true, true, true, false, false, true, false],
"8": [true, true, true, true, true, true, true],
"9": [true, true, true, true, false, true, false]
}
// 样式配置
let on = "#44F1FF" // 激活颜色
let off = "#031743" // 未激活颜色
// 缩放比例
let scale = 1
// 工具函数 - 格式化数字为两位(补零)
const formatZero = (n: number): string => {
return n >= 10 ? n.toString() : `0${n}`
}
// 渲染数字的一个区段
const renderDigit = (ctx: CanvasRenderingContext2D, x = 0, y = 0, binaryPattern: boolean[]) => {
const s = scale
// top
ctx.beginPath()
ctx.moveTo(x + 20 * s, y + 5 * s)
ctx.lineTo(x + 35 * s, y)
ctx.lineTo(x + 105 * s, y)
ctx.lineTo(x + 120 * s, y + 5 * s)
ctx.lineTo(x + 105 * s, y + 20 * s)
ctx.lineTo(x + 35 * s, y + 20 * s)
ctx.lineTo(x + 20 * s, y + 5 * s)
ctx.fillStyle = binaryPattern[0] ? on : off
ctx.fill()
// top-left
ctx.beginPath()
ctx.moveTo(x + 15 * s, y + 10 * s)
ctx.lineTo(x + 10 * s, y + 25 * s)
ctx.lineTo(x + 10 * s, y + 95 * s)
ctx.lineTo(x + 15 * s, y + 110 * s)
ctx.lineTo(x + 30 * s, y + 95 * s)
ctx.lineTo(x + 30 * s, y + 25 * s)
ctx.fillStyle = binaryPattern[1] ? on : off
ctx.fill()
// top-right
ctx.beginPath()
ctx.moveTo(x + 125 * s, y + 10 * s)
ctx.lineTo(x + 130 * s, y + 25 * s)
ctx.lineTo(x + 130 * s, y + 95 * s)
ctx.lineTo(x + 125 * s, y + 110 * s)
ctx.lineTo(x + 110 * s, y + 95 * s)
ctx.lineTo(x + 110 * s, y + 25 * s)
ctx.fillStyle = binaryPattern[2] ? on : off
ctx.fill()
// middle
ctx.beginPath()
ctx.moveTo(x + 20 * s, y + 115 * s)
ctx.lineTo(x + 35 * s, y + 100 * s)
ctx.lineTo(x + 105 * s, y + 100 * s)
ctx.lineTo(x + 120 * s, y + 115 * s)
ctx.lineTo(x + 105 * s, y + 130 * s)
ctx.lineTo(x + 35 * s, y + 130 * s)
ctx.fillStyle = binaryPattern[3] ? on : off
ctx.fill()
// bottom-left
ctx.beginPath()
ctx.moveTo(x + 15 * s, y + 120 * s)
ctx.lineTo(x + 10 * s, y + 135 * s)
ctx.lineTo(x + 10 * s, y + 205 * s)
ctx.lineTo(x + 15 * s, y + 220 * s)
ctx.lineTo(x + 30 * s, y + 205 * s)
ctx.lineTo(x + 30 * s, y + 135 * s)
ctx.fillStyle = binaryPattern[4] ? on : off
ctx.fill()
// bottom-right
ctx.beginPath()
ctx.moveTo(x + 125 * s, y + 120 * s)
ctx.lineTo(x + 130 * s, y + 135 * s)
ctx.lineTo(x + 130 * s, y + 205 * s)
ctx.lineTo(x + 125 * s, y + 220 * s)
ctx.lineTo(x + 110 * s, y + 205 * s)
ctx.lineTo(x + 110 * s, y + 135 * s)
ctx.fillStyle = binaryPattern[5] ? on : off
ctx.fill()
// bottom
ctx.beginPath()
ctx.moveTo(x + 20 * s, y + 225 * s)
ctx.lineTo(x + 35 * s, y + 210 * s)
ctx.lineTo(x + 105 * s, y + 210 * s)
ctx.lineTo(x + 120 * s, y + 225 * s)
ctx.lineTo(x + 105 * s, y + 235 * s)
ctx.lineTo(x + 35 * s, y + 235 * s)
ctx.lineTo(x + 20 * s, y + 225 * s)
ctx.fillStyle = binaryPattern[6] ? on : off
ctx.fill()
}
// 计算合适的缩放比例
const calculateScale = (width: number, height: number): number => {
// 原始设计下,整个表宽约为940,高约为240
const originalWidth = 940
const originalHeight = 240
const scaleX = width / originalWidth
const scaleY = height / originalHeight
// 取较小的缩放比例,确保完全显示
return Math.min(scaleX, scaleY) * 0.9 // 稍微缩小一点,留出边距
}
// 绘制时间
const drawTime = () => {
if (!canvasRef.value) return
const canvas = canvasRef.value
const ctx = canvas.getContext('2d')
if (!ctx) return
// 清除画布
ctx.clearRect(0, 0, canvas.width, canvas.height)
// 获取当前时间
const date = new Date()
// 格式化时间数字
const [hour_ten_digit, hour_one_digit] = formatZero(date.getHours()).split("")
const [minute_ten_digit, minute_one_digit] = formatZero(date.getMinutes()).split("")
const [second_ten_digit, second_one_digit] = formatZero(date.getSeconds()).split("")
// 计算中心位置,使表居中
const centerY = (canvas.height - 240 * scale) / 2 + 20 * scale
const startX = (canvas.width - 940 * scale) / 2 + 20 * scale
// 使用指定的间距渲染每个数字
const spacing = 140 * scale
// 渲染每个数字
renderDigit(ctx, startX, centerY, binary[hour_ten_digit])
renderDigit(ctx, startX + spacing, centerY, binary[hour_one_digit])
renderDigit(ctx, startX + spacing * 2.5, centerY, binary[minute_ten_digit])
renderDigit(ctx, startX + spacing * 3.5, centerY, binary[minute_one_digit])
renderDigit(ctx, startX + spacing * 5, centerY, binary[second_ten_digit])
renderDigit(ctx, startX + spacing * 6, centerY, binary[second_one_digit])
}
// 初始化函数
const initWatch = (width = 1000, height = 300, interval = 500) => {
if (!canvasRef.value) return
const canvas = canvasRef.value
canvas.width = width
canvas.height = height
// 计算适合的缩放比例
scale = calculateScale(width, height)
// 启动定时器更新时间显示
drawTime() // 立即首次绘制
animationTimer = window.setInterval(() => {
drawTime()
}, interval)
}
// 停止计时器
const stopWatch = () => {
if (animationTimer) {
clearInterval(animationTimer)
animationTimer = null
}
}
// 自动清理
onBeforeUnmount(() => {
stopWatch()
})
// 返回需要暴露的内容
return {
canvasRef,
initWatch,
stopWatch,
drawTime,
// 自定义选项
setColors: (onColor: string, offColor: string) => {
on = onColor
off = offColor
}
}
}

View File

@ -0,0 +1,365 @@
/**
* 绘制3d图
* @param pieData 总数据
* @param internalDiameterRatio:透明的空心占比
* @param distance 视角到主体的距离
* @param alpha 旋转角度
* @param pieHeight 立体的高度
* @param opacity 饼或者环的透明度
*/
// 定义饼图数据项接口
interface PieDataItem {
name: string;
value: number;
itemStyle?: {
color?: string;
opacity?: number;
};
startRatio?: number;
endRatio?: number;
}
// 定义图例数据项接口
interface LegendItem {
name: string;
value: number;
}
// 参数方程接口
interface ParametricEquation {
u: {
min: number;
max: number;
step: number;
};
v: {
min: number;
max: number;
step: number;
};
x: (u: number, v: number) => number;
y: (u: number, v: number) => number;
z: (u: number, v: number) => number;
}
// ECharts配置相关类型
interface EChartsOption {
series: SeriesItem[];
[key: string]: any;
}
// 定义图表系列项接口
interface SeriesItem {
name: string;
type: string;
parametric: boolean;
wireframe: {
show: boolean;
};
pieData: PieDataItem;
pieStatus: {
selected: boolean;
hovered: boolean;
k: number;
};
center: string[];
itemStyle?: {
color?: string | number;
opacity?: number;
};
parametricEquation?: ParametricEquation;
}
const getPie3D = (
pieData: PieDataItem[],
internalDiameterRatio: number,
distance: number,
alpha: number,
pieHeight: number,
opacity: number = 1
): EChartsOption => {
const series: SeriesItem[] = []
let sumValue: number = 0
let startValue: number = 0
let endValue: number = 0
let legendData: LegendItem[] = []
let legendBfb: LegendItem[] = []
const k: number = 1 - internalDiameterRatio
pieData.sort((a: PieDataItem, b: PieDataItem) => {
return b.value - a.value
})
// 为每一个饼图数据,生成一个 series-surface 配置
for (let i = 0; i < pieData.length; i++) {
sumValue += pieData[i].value
const seriesItem: SeriesItem = {
name:
typeof pieData[i].name === 'undefined'
? `series${i}`
: pieData[i].name,
type: 'surface',
parametric: true,
wireframe: {
show: false
},
pieData: pieData[i],
pieStatus: {
selected: false,
hovered: false,
k: k
},
center: ['10%', '50%']
}
if (typeof pieData[i].itemStyle !== 'undefined') {
const itemStyle: { color?: string | number; opacity?: number } = {}
itemStyle.color =
typeof pieData[i].itemStyle?.color !== 'undefined'
? pieData[i].itemStyle?.color
: opacity
itemStyle.opacity =
typeof pieData[i].itemStyle?.opacity !== 'undefined'
? pieData[i].itemStyle?.opacity
: opacity
seriesItem.itemStyle = itemStyle
}
series.push(seriesItem)
}
// 使用上一次遍历时,计算出的数据和 sumValue,调用 getParametricEquation 函数,
// 向每个 series-surface 传入不同的参数方程 series-surface.parametricEquation,也就是实现每一个扇形。
legendData = []
legendBfb = []
for (let i = 0; i < series.length; i++) {
endValue = startValue + series[i].pieData.value
series[i].pieData.startRatio = startValue / sumValue
series[i].pieData.endRatio = endValue / sumValue
series[i].parametricEquation = getParametricEquation(
series[i].pieData.startRatio || 0,
series[i].pieData.endRatio || 0,
false,
false,
k,
series[i].pieData.value
)
startValue = endValue
const bfb = fomatFloat(series[i].pieData.value / sumValue, 4)
if (typeof bfb === 'string') {
legendData.push({
name: series[i].name,
value: parseFloat(bfb)
})
legendBfb.push({
name: series[i].name,
value: parseFloat(bfb)
})
}
}
const boxHeight = getHeight3D(series, pieHeight) // 通过pieHeight设定3d饼/环的高度,单位是px
// 准备待返回的配置项,把准备好的 legendData、series 传入。
const option: EChartsOption = {
// legend: {
// show: false,
// data: legendData,
// orient: 'vertical',
// left: 10,
// top: 10,
// itemGap: 10,
// textStyle: {
// color: '#A1E2FF'
// },
// icon: 'circle',
// formatter: function (param: string) {
// const item = legendBfb.filter(item => item.name === param)[0]
// if (item) {
// const bfs = fomatFloat(item.value * 100, 2)
// if (typeof bfs === 'string') {
// return `${item.name} ${bfs}%`
// }
// }
// return ''
// }
// },
labelLine: {
show: true,
lineStyle: {
color: '#fff'
}
},
label: {
show: true,
position: 'outside',
formatter: '{b} \n{c} {d}%'
},
tooltip: {
backgroundColor: '#033b77',
borderColor: '#21f2c4',
textStyle: {
color: '#fff',
fontSize: 13
},
formatter: (_params: any) => {
// if (
// params.seriesName !== 'mouseoutSeries' &&
// params.seriesName !== 'pie2d'
// ) {
// // 确保params.seriesIndex存在且在option.series范围内
// if (params.seriesIndex !== undefined && option.series && option.series[params.seriesIndex] && option.series[params.seriesIndex].pieData) {
// const bfb = (
// ((option.series[params.seriesIndex].pieData.endRatio || 0) -
// (option.series[params.seriesIndex].pieData.startRatio || 0)) *
// 100
// ).toFixed(2)
// return (
// `${params.seriesName}<br/>` +
// `<span style="display:inline-block;margin-right:5px;border-radius:10px;width:10px;height:10px;background-color:${params.color};"></span>` +
// `${bfb}%`
// )
// }
// // 如果索引无效,返回简单的系列名称
// return `${params.seriesName}`
// }
return ''
}
},
xAxis3D: {
min: -1,
max: 1
},
yAxis3D: {
min: -1,
max: 1
},
zAxis3D: {
min: -1,
max: 1
},
grid3D: {
show: false,
boxHeight: boxHeight, // 圆环的高度
viewControl: {
// 3d效果可以放大、旋转等,请自己去查看官方配置
alpha, // 角度
distance, // 调整视角到主体的距离,类似调整zoom
rotateSensitivity: 0, // 设置为0无法旋转
zoomSensitivity: 0, // 设置为0无法缩放
panSensitivity: 0, // 设置为0无法平移
autoRotate: false // 自动旋转
}
},
series: series
}
return option
}
/**
* 生成扇形的曲面参数方程,用于 series-surface.parametricEquation
*/
const getParametricEquation = (
startRatio: number,
endRatio: number,
isSelected: boolean,
isHovered: boolean,
k: number,
h: number
): ParametricEquation => {
// 计算
const midRatio = (startRatio + endRatio) / 2
const startRadian = startRatio * Math.PI * 2
const endRadian = endRatio * Math.PI * 2
const midRadian = midRatio * Math.PI * 2
// 如果只有一个扇形,则不实现选中效果。
if (startRatio === 0 && endRatio === 1) {
isSelected = false
}
// 通过扇形内径/外径的值,换算出辅助参数 k(默认值 1/3)
k = typeof k !== 'undefined' ? k : 1 / 3
// 计算选中效果分别在 x 轴、y 轴方向上的位移(未选中,则位移均为 0)
const offsetX = isSelected ? Math.cos(midRadian) * 0.1 : 0
const offsetY = isSelected ? Math.sin(midRadian) * 0.1 : 0
// 计算高亮效果的放大比例(未高亮,则比例为 1)
const hoverRate = isHovered ? 1.05 : 1
// 返回曲面参数方程
return {
u: {
min: -Math.PI,
max: Math.PI * 3,
step: Math.PI / 32
},
v: {
min: 0,
max: Math.PI * 2,
step: Math.PI / 20
},
x: function (u: number, v: number): number {
if (u < startRadian) {
return (
offsetX +
Math.cos(startRadian) * (1 + Math.cos(v) * k) * hoverRate
)
}
if (u > endRadian) {
return (
offsetX + Math.cos(endRadian) * (1 + Math.cos(v) * k) * hoverRate
)
}
return offsetX + Math.cos(u) * (1 + Math.cos(v) * k) * hoverRate
},
y: function (u: number, v: number): number {
if (u < startRadian) {
return (
offsetY +
Math.sin(startRadian) * (1 + Math.cos(v) * k) * hoverRate
)
}
if (u > endRadian) {
return (
offsetY + Math.sin(endRadian) * (1 + Math.cos(v) * k) * hoverRate
)
}
return offsetY + Math.sin(u) * (1 + Math.cos(v) * k) * hoverRate
},
z: function (u: number, v: number): number {
if (u < -Math.PI * 0.5) {
return Math.sin(u)
}
if (u > Math.PI * 2.5) {
return Math.sin(u) * h * 0.1
}
return Math.sin(v) > 0 ? 1 * h * 0.1 : -1
}
}
}
/**
* 获取3d丙图的最高扇区的高度
*/
const getHeight3D = (series: SeriesItem[], height: number): number => {
series.sort((a: SeriesItem, b: SeriesItem) => {
return b.pieData.value - a.pieData.value
})
return (height * 25) / series[0].pieData.value
}
/**
* 格式化浮点数
*/
const fomatFloat = (num: number, n: number): string | boolean => {
let f = parseFloat(num.toString())
if (isNaN(f)) {
return false
}
f = Math.round(num * Math.pow(10, n)) / Math.pow(10, n) // n 幂
let s = f.toString()
let rs = s.indexOf('.')
// 判定如果是整数,增加小数点再补0
if (rs < 0) {
rs = s.length
s += '.'
}
while (s.length <= rs + n) {
s += '0'
}
return s
}
export { getPie3D, getParametricEquation }

View File

@ -0,0 +1,20 @@
import * as ECharts from "echarts";
const HANDLER = "_vue_resize_handler";
export const vEchartResize = {
mounted(el:any,binding:any){
el[HANDLER] = binding.value
? binding.value
: () => {
let chart = ECharts.getInstanceByDom(el);
if (!chart) {
return;
}
chart.resize();
};
window.addEventListener("resize",el[HANDLER]);
},
beforeUnmount(el:any){
window.removeEventListener("resize",el[HANDLER]);
}
}

18
src/main.ts Normal file
View File

@ -0,0 +1,18 @@
import { createApp } from 'vue'
import '@unocss/reset/tailwind-compat.css'
// import './style.css'
import 'uno.css';
import App from './App.vue'
// Pinia 持久化
import store from '@/store/index'
//导入路由
import router from '@/router/index'
const app = createApp(App)
// 注册已经加上了持久化的pinia
app.use(store)
app.use(router)
app.mount('#app')

49
src/router/index.ts Normal file
View File

@ -0,0 +1,49 @@
import { createRouter, createWebHistory } from "vue-router";
import { publicRoutes } from "./publicRoutes";
import { privateRoutes } from "./privateRoutes";
function getRoutes() {
const routes = [
// 私有路由,请在这里添加
...privateRoutes,
// 公共路由
...publicRoutes,
];
/**
* 如果要对 routes 做一些处理,请在这里修改
*/
return routes;
}
const router = createRouter({
history: createWebHistory(),
routes: getRoutes(),
});
// 全局前置守卫,这边可以对身份进行验证
router.beforeEach((to, _from, next) => {
let userRole = "admin";
// 如果目标路由没有角色限制
if (!to.meta.role) {
next();
}
// 判断当前用户角色是否在目标路由的允许角色列表中
if ((to.meta.role as string[]).includes(userRole)) {
// 如果角色匹配,允许进入目标路由
next();
} else {
// 如果角色不匹配,跳转到 unauthorized 页面
next({ path: "/unauthorized" });
}
});
// 监听路由变化,动态设置网页标题
router.afterEach((to) => {
if (to.meta.title) {
document.title = to.meta.title as string;
}
});
export default router;

13
src/router/keepAlive.ts Normal file
View File

@ -0,0 +1,13 @@
import { ComponentInternalInstance, ref } from 'vue'
export const excludes = ref<string[]>([])
export function removeKeepAliveCache(instance: ComponentInternalInstance) {
if (!excludes.value.includes(instance.type.name!)) {
excludes.value.push(instance.type.name!)
}
}
export function resetKeepAliveCache(instance: ComponentInternalInstance) {
excludes.value = excludes.value.filter((item) => item !== instance.type.name)
}

View File

@ -0,0 +1 @@
export const privateRoutes = [];

View File

@ -0,0 +1,19 @@
export const publicRoutes = [
{
path:"/unauthorized",
name:'unauthorized',
meta:{
title:'unauthorized'
},
component:()=>import('../views/unauthorized.vue')
},
{
path:"/",
name:'home',
meta:{
title:'home'
},
component:()=>import('../views/home.vue')
}
]

13
src/store/index.ts Normal file
View File

@ -0,0 +1,13 @@
// pinia数据持久化存储
import { createPinia } from 'pinia'
import { createPersistedState } from 'pinia-plugin-persistedstate'
import { SelfStorage } from './secureStore'
// 第一个参数是应用程序中 store 的唯一 id
const store = createPinia()
store.use(
createPersistedState({
storage: SelfStorage
})
)
export default store

26
src/store/secureStore.ts Normal file
View File

@ -0,0 +1,26 @@
import { StorageLike } from 'pinia-plugin-persistedstate'
import CryptoJS from 'crypto-js'
const SECRET_KEY = import.meta.env.VITE_SECRET_KEY // 自定义加密密钥
// 自定义加密存储
export const SelfStorage: StorageLike = {
setItem(key: string, value: string) {
// 加密值
const encryptedValue = CryptoJS.AES.encrypt(value, SECRET_KEY).toString()
// 存储加密后的值
localStorage.setItem(key, encryptedValue)
},
getItem(key: string): string | null {
// 获取加密值
const encryptedValue = localStorage.getItem(key)
if (encryptedValue) {
// 解密
const bytes = CryptoJS.AES.decrypt(encryptedValue, SECRET_KEY)
const decryptedValue = bytes.toString(CryptoJS.enc.Utf8)
return decryptedValue || null
}
return null
},
}

17
src/store/user.ts Normal file
View File

@ -0,0 +1,17 @@
import { defineStore } from "pinia";
export const useUserStore = defineStore("user", {
persist:true,
state:()=>({
token:""
}),
getters:{
getToken(state){
return state.token;
}
},
actions:{
setToken(token:string){
this.token = token;
}
}
});

156
src/style.css Normal file
View File

@ -0,0 +1,156 @@
* {
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
touch-action: manipulation;
-webkit-touch-callout: none;
box-sizing: border-box;
}
html {
line-height: 1.15;
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
}
main {
display: block;
}
h1 {
font-size: 2em;
margin: 0.67em 0;
}
hr {
box-sizing: content-box;
height: 0;
overflow: visible;
}
pre {
font-family: monospace, monospace;
font-size: 1em;
}
a {
background-color: transparent;
}
abbr[title] {
border-bottom: none;
text-decoration: underline;
text-decoration: underline dotted;
}
b,
strong {
font-weight: bolder;
}
code,
kbd,
samp {
font-family: monospace, monospace;
font-size: 1em;
}
small {
font-size: 80%;
}
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
img {
border-style: none;
}
button,
input,
optgroup,
select,
textarea {
font-family: inherit;
font-size: 100%;
line-height: 1.15;
margin: 0;
}
button,
input {
overflow: visible;
}
button,
select {
text-transform: none;
}
[type="button"],
[type="reset"],
[type="submit"],
button {
-webkit-appearance: button;
}
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner,
button::-moz-focus-inner {
border-style: none;
padding: 0;
}
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring,
button:-moz-focusring {
outline: 1px dotted ButtonText;
}
fieldset {
padding: 0.35em 0.75em 0.625em;
}
legend {
box-sizing: border-box;
color: inherit;
display: table;
max-width: 100%;
padding: 0;
white-space: normal;
}
progress {
vertical-align: baseline;
}
textarea {
overflow: auto;
}
[type="checkbox"],
[type="radio"] {
box-sizing: border-box;
padding: 0;
}
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
[type="search"] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-file-upload-button {
-webkit-appearance: button;
font: inherit;
}
details {
display: block;
}
summary {
display: list-item;
}
template {
display: none;
}
[hidden] {
display: none;
}

View File

@ -0,0 +1,3 @@
.text-color{
background: linear-gradient(90deg, #8FC8FF 0%, #FFFFFF 100%);
}

17
src/styles/utils.scss Normal file
View File

@ -0,0 +1,17 @@
// 使用 scss 的 math 函数,https://sass-lang.com/documentation/breaking-changes/slash-div
@use "sass:math";
// 默认设计稿的宽度
$designWidth: 1920;
// 默认设计稿的高度
$designHeight: 1080;
// px 转为 vw 的函数
@function vw($px) {
@return math.div($px, $designWidth) * 100vw;
}
// px 转为 vh 的函数
@function vh($px) {
@return math.div($px, $designHeight) * 100vh;
}

11
src/types/uni.d.ts vendored Normal file
View File

@ -0,0 +1,11 @@
/**
* uni-app API 类型声明
*/
declare const uni: {
postMessage: (options: { data: Record<string, any> }) => void;
getEnv: (callback: (res: any) => void) => void;
switchTab: (options: { url: string }) => void;
reLaunch: (options: { url: string }) => void;
navigateBack: (options: { delta: number }) => void;
[key: string]: any;
};

9
src/utils/echartData.ts Normal file
View File

@ -0,0 +1,9 @@
// 根据设计稿来放大缩小字体
export const fitChartSize = (size:number,defaultWidth = 1920) => {
let clientWidth = window.innerWidth||document.documentElement.clientWidth||document.body.clientWidth;
if (!clientWidth) return size;
let scale = (clientWidth / defaultWidth);
return Number((size*scale).toFixed(3));
}

View File

@ -0,0 +1,131 @@
<template>
<div class="w-max h-max relative">
<SvgComponent :content="groupSvg" class="w-[544px] h-[210px]" />
<div class="w-full h-full absolute top-0 left-0 pt-[31px] pb-[21px] pl-[14px] pr-[19px] flex items-center">
<div class="w-[216px]">
<div class="relative w-[154px] h-[124px] flex items-center flex-col">
<div class="text-[#44C1EF] italic text-[20px] font-700">总缴费</div>
<div class="bg-gradient-to-b from-[#8FC8FF] to-white bg-clip-text text-transparent mb-[15px] z-10">
<span class="text-[40px] italic">1265</span>
<span class="text-[20px]">人</span>
</div>
<SvgComponent :content="lightningBoxSvg" class="box-light absolute" />
</div>
<div>
<div class="flex items-center justify-between text-[12px]">
<div class="text-[#6B89BC]">当前值1265人</div>
<div class="text-[#69D4FF]">
计划:
<span class="text-[15px]">4000</span>
<span class="text-[13px]">人</span>
</div>
</div>
</div>
<YProgress :percentage="25" height="12px" class="mt-[7px]" />
</div>
<div class="ml-[22px] relative">
<ProportionCharts :chart-data="chartData" class="z-2 relative" />
<SvgComponent :content="paymentChartSvg" class="w-[143px] h-[143px] absolute top-0 left-0 z-1" />
<div
class="text-[18px] text-[#fff] font-700 text-shadow-[0_0_10px_rgba(12,32,72,0.42)] italic absolute top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%] z-3">
缴费
<br />
占比
</div>
</div>
<ul class="ml-[10px] flex flex-col justify-evenly h-full flex-1">
<li class="flex items-center flex-wrap" v-for="item in chartData" :key="item.name">
<div class="w-[6px] h-[6px] rounded-full" :style="{ backgroundColor: item.color }"></div>
<div class="flex-1 flex items-center text-[#C0EEFF] text-[12px] justify-between">
<span class="ml-[4px] mr-[9px]">{{ item.name }}</span>
<span>{{ item.value }}人</span>
</div>
<div class="border-image w-full mt-[6px]"></div>
</li>
</ul>
</div>
</div>
</template>
<script lang="ts" setup>
import SvgComponent from "@/components/SvgComponent.vue";
import YProgress from "@/components/progress/YProgress.vue";
import ProportionCharts from "@/views/components/ProportionCharts.vue";
const groupSvg = ref("");
const getGroupBackgroundSvg = async () => {
const { default: svg } = await import("/src/assets/svg-img/group-background.svg?raw");
groupSvg.value = svg;
};
const chartData = ref([
{ name: "类型A", value: 250, color: "#0783FA" },
{ name: "类型B", value: 274, color: "#07D1FA" },
{ name: "类型C", value: 310, color: "#20E6A4" },
{ name: "类型D", value: 135, color: "#FFD15C" },
]);
const lightningBoxSvg = ref("");
const getLightningBoxSvg = async () => {
const { default: svg } = await import("/src/assets/svg-img/box-light.svg?raw");
lightningBoxSvg.value = svg;
};
const paymentChartSvg = ref("");
const getPaymentChartSvg = async () => {
const { default: svg } = await import("/src/assets/svg-img/payment-chart.svg?raw");
paymentChartSvg.value = svg;
};
onBeforeMount(() => {
getGroupBackgroundSvg();
getLightningBoxSvg();
getPaymentChartSvg();
});
</script>
<style scoped lang="scss">
@use "@/styles/text-color.scss";
.border-image {
border: 1px solid;
border-image: linear-gradient(
90deg,
#217ac600,
#227cc8,
#217ac600
)
1 1;
opacity: 0.3;
}
.rotate-animation {
animation: rotate-animation 1s infinite;
}
@keyframes rotate-animation {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(-180deg);
}
}
:deep(.box-light) {
.lightning-flashing {
animation: lightning-flashing 1s infinite;
}
@keyframes lightning-flashing {
0% {
fill-opacity: 0;
}
50% {
fill-opacity: 1;
}
100% {
fill-opacity: 0;
}
}
}
</style>

View File

@ -0,0 +1,124 @@
<template>
<div class="w-[143px] h-[143px]">
<div class="w-[143px] h-[143px] bg-transparent" ref="chartRef"></div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import * as echarts from 'echarts'
// 图表容器引用
const chartRef = ref<HTMLElement | null>(null)
let chartInstance: echarts.ECharts | null = null
// 接收数据作为prop
const props = defineProps({
chartData: {
type: Array,
required: true,
default: () => []
}
})
// 初始化图表
const initChart = () => {
if (chartRef.value) {
// 销毁之前的实例
if (chartInstance) {
chartInstance.dispose()
}
// 创建新实例
chartInstance = echarts.init(chartRef.value)
// 配置颜色从数据中获取
const formattedData = props.chartData.map((item: any) => ({
...item,
// 确保每个项都有itemStyle且包含color
itemStyle: {
color: item.color || (item.itemStyle && item.itemStyle.color),
borderWidth: 0 // 移除边框以消除间隔
}
}))
// 配置项
const option = {
tooltip: {
trigger: 'item',
backgroundColor: 'rgba(0, 0, 0, 0.7)',
borderColor: '#44F1FF',
textStyle: {
color: '#fff',
fontSize: 10
},
formatter: '{b}: {d}%'
},
series: [
{
name: '数据占比',
type: 'pie',
radius: ['70%', '90%'],
center: ['50%', '50%'],
avoidLabelOverlap: false,
itemStyle: {
borderWidth: 0 // 移除扇区边框
},
label: {
show: false
},
emphasis: {
scale: false,
itemStyle: {
shadowBlur: 5,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
},
labelLine: {
show: false
},
data: formattedData,
// 移除间隙
gap: 0
}
],
animation: true,
animationDuration: 500
}
// 设置配置项
chartInstance.setOption(option)
// 监听窗口大小变化
window.addEventListener('resize', handleResize)
}
}
// 处理窗口大小变化
const handleResize = () => {
if (chartInstance) {
chartInstance.resize()
}
}
// 生命周期钩子
onMounted(() => {
initChart()
})
onUnmounted(() => {
// 移除事件监听
window.removeEventListener('resize', handleResize)
// 销毁图表
if (chartInstance) {
chartInstance.dispose()
chartInstance = null
}
})
</script>
<style lang="scss" scoped>
</style>

View File

@ -0,0 +1,111 @@
<template>
<div class="w-max h-max relative">
<SvgComponent :content="groupSvg" class="w-[544px] h-[210px]" />
<div class="w-full h-full absolute top-0 left-0 pt-[31px] pb-[21px] pl-[14px] pr-[19px] flex items-center">
<div class="w-[216px]">
<div class="relative w-[144px] h-[113px] flex items-center flex-col">
<div class="text-[#44C1EF] italic text-[20px] font-700">今日缴费</div>
<div class="bg-gradient-to-b from-[#8FC8FF] to-white bg-clip-text text-transparent mb-[15px] z-10">
<span class="text-[34px] italic">125</span>
<span class="text-[18px]">人</span>
</div>
<SvgComponent :content="hexagonalBoxSvg" class="box-light absolute" />
</div>
<div>
<div class="flex items-center justify-between text-[12px]">
<div class="text-[#6B89BC]">当前值1265人</div>
<div class="text-[#69D4FF]">
计划:
<span class="text-[15px]">4000</span>
<span class="text-[13px]">人</span>
</div>
</div>
</div>
</div>
<div class="ml-[22px] relative">
<SvgComponent :content="paymentBorderSvg" class="w-[127px] h-[130px] top-0 left-0 z-1" />
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import SvgComponent from "@/components/SvgComponent.vue";
const groupSvg = ref("");
const getGroupBackgroundSvg = async () => {
const { default: svg } = await import("/src/assets/svg-img/group-background.svg?raw");
groupSvg.value = svg;
};
const hexagonalBoxSvg = ref("");
const getLightningBoxSvg = async () => {
const { default: svg } = await import("/src/assets/svg-img/hexagonal-light.svg?raw");
hexagonalBoxSvg.value = svg;
};
const paymentBorderSvg = ref("");
const getPaymentChartSvg = async () => {
const { default: svg } = await import("/src/assets/svg-img/payment-border.svg?raw");
paymentBorderSvg.value = svg;
};
onBeforeMount(() => {
getGroupBackgroundSvg();
getLightningBoxSvg();
getPaymentChartSvg();
});
</script>
<style scoped lang="scss">
@use "@/styles/text-color.scss";
.border-image {
border: 1px solid;
border-image: linear-gradient(
90deg,
#217ac600,
#227cc8,
#217ac600
)
1 1;
opacity: 0.3;
}
.rotate-animation {
animation: rotate-animation 1s infinite;
}
@keyframes rotate-animation {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(-180deg);
}
}
:deep(.box-light) {
.lightning-flashing {
animation: lightning-flashing 1s infinite;
}
@keyframes lightning-flashing {
0% {
fill-opacity: 0;
}
50% {
fill-opacity: 1;
}
100% {
fill-opacity: 0;
}
}
}
</style>

74
src/views/home.vue Normal file
View File

@ -0,0 +1,74 @@
<template>
<div class="main-bg aspect-[16/9]">
<header class="relative flex items-center">
<SvgComponent :content="headerSvg" class="w-full h-[98px]" />
<SvgComponent :content="titleSvg" class="w-[50%] h-[69px] absolute top-0 left-50% translate-x-[-50%]" />
<div class="absolute top-[25%] right-[24px] translate-y-[-25%] z-1 flex items-center justify-center w-max">
<div class="text-[#45A2FF] text-[14px]">{{ year }}-{{ month }}-{{ day }}&nbsp;{{ weekday }}</div>
<DigitalWatch class="ml-[10px]" />
</div>
</header>
<div class="flex items-center justify-end pr-[24px] cursor-pointer">
<SvgIcon name="circle" class="text-[14px] text-[#C0EEFF] hover:rotate-90 transition-all duration-300" />
<div class="text-[#C0EEFF] text-[12px] ml-[5px]">数据更新时间:6.12 12:00:00</div>
</div>
<div class="flex items-center px-[24px]">
<PaymentTotal />
<TodayPayment class="ml-[20px]" />
</div>
<!-- <TDCharts :optionData="chartData" /> -->
</div>
</template>
<script lang="ts" setup>
import SvgComponent from "@/components/SvgComponent.vue";
import SvgIcon from "@/components/svg-icon/SvgIcon.vue";
import TDCharts from "@/components/ease-charts/TDCharts.vue";
import DigitalWatch from "@/components/watch/DigitalWatch.vue";
import PaymentTotal from "@/views/components/PaymentTotal.vue";
import TodayPayment from "@/views/components/TodayPayment.vue";
import { useDate } from "@/composables/useDate";
const { year, month, day, weekday, updateTime } = useDate();
const headerSvg = ref("");
const headerBackgroundSvg = async () => {
const { default: svg } = await import("/src/assets/svg-img/header-background.svg?raw");
headerSvg.value = svg;
};
const titleSvg = ref("");
const headerTitleSvg = async () => {
const { default: svg } = await import("/src/assets/svg-img/header-title.svg?raw");
titleSvg.value = svg;
};
onBeforeMount(() => {
updateTime();
headerBackgroundSvg();
headerTitleSvg();
});
// 示例数据
const chartData = ref([
{ name: "线上", value: 335, itemStyle: { color: "#d62728" } },
{ name: "线下", value: 310, itemStyle: { color: "#ffcc5c" } },
]);
</script>
<style scoped lang="scss">
.main-bg {
background-image: url("/images/main-bg.png");
background-size: 100% 100%;
background-position: center;
background-repeat: no-repeat;
}
</style>

View File

@ -0,0 +1,3 @@
<template>
<div>401 - Unauthorized</div>
</template>

8
src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1,8 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import { DefineComponent } from 'vue'
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
const component: DefineComponent<{}, {}, any>
export default component
}

3
src/window-env.d.ts vendored Normal file
View File

@ -0,0 +1,3 @@
interface Window {
}

28
tsconfig.app.json Normal file
View File

@ -0,0 +1,28 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "preserve",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "src/composables/useDigitalWatch.js"]
}

13
tsconfig.json Normal file
View File

@ -0,0 +1,13 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
},
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

25
tsconfig.node.json Normal file
View File

@ -0,0 +1,25 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts","./plugins/*"]
}

27
uno.config.ts Normal file
View File

@ -0,0 +1,27 @@
import { defineConfig, presetIcons,presetUno } from "unocss";
import presetWind from "@unocss/preset-wind";
import { presetScrollbarHide } from 'unocss-preset-scrollbar-hide'
// import { presetPxToViewport } from "unocss-preset-px-to-vw-or-vh";
export default defineConfig({
presets: [
// presetPxToViewport({
// // 可选:自定义设计尺寸(默认为 1920x1080)
// designWidth: 1920,
// designHeight: 1080,
// keyToVw: ["font-size"],
// }),
presetUno(),
presetWind(),
presetScrollbarHide(),
presetIcons({
scale: 1,
warn: true,
extraProperties: {
display: "inline-block",
"vertical-align": "middle",
},
}),
],
rules: [["pb-safe", { "padding-bottom": "calc(env(safe-area-inset-bottom) + 52rpx)" }]],
});

43
upload.sh Normal file
View File

@ -0,0 +1,43 @@
#!/bin/bash
# 服务器信息
SERVER_USER="root"
SERVER_HOST="106.14.30.150"
SERVER_PATH="/opt/1panel/apps/openresty/openresty/www/sites/sort.ycymedu.com/index"
PRIVATE_KEY="ALIYUN.pem"
BACKUP_PATH="${SERVER_PATH}-backup-$(date +%Y%m%d%H%M%S).zip"
DINGDING_WEBHOOK="https://oapi.dingtalk.com/robot/send?access_token=fca104958fea6273c9c7ef3f08b3d552645c214f929066785e8caf6e1885a5a6"
# 在上传之前备份原来的文件并压缩
ssh -i $PRIVATE_KEY $SERVER_USER@$SERVER_HOST "cd $(dirname $SERVER_PATH) && zip -r $(basename $BACKUP_PATH) $(basename $SERVER_PATH)"
# 使用 scp 上传文件
scp -i $PRIVATE_KEY -r dist/* $SERVER_USER@$SERVER_HOST:$SERVER_PATH
# 提示上传完成
if [ $? -eq 0 ]; then
echo "上传成功!备份存储于 $BACKUP_PATH"
# 发送钉钉通知
curl -X POST "$DINGDING_WEBHOOK" \
-H "Content-Type: application/json" \
-d '{
"msgtype": "text",
"text": {
"content": "single html| upload success!!! backup to'"$BACKUP_PATH"'"
}
}'
else
echo "上传失败,请检查错误信息。"
# 发送钉钉通知
curl -X POST "$DINGDING_WEBHOOK" \
-H "Content-Type: application/json" \
-d '{
"msgtype": "text",
"text": {
"content": "single html|upload fail,please check error info。"
}
}'
fi

71
vite.config.ts Normal file
View File

@ -0,0 +1,71 @@
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { resolve } from "path";
import AutoComplete from "unplugin-auto-import/vite";
import UnoCSS from 'unocss/vite'
import svgBuilder from "./plugins/svgBuilder";
const pathSrc = resolve(__dirname, "src");
// https://vite.dev/config/
export default defineConfig({
assetsInclude: ["**/*.svg"],
base: "./" /* 这个就是webpack里面的publicPath */,
build: {
rollupOptions: {
output: {
// 最小化拆分包
manualChunks: (id) => {
if (id.includes("node_modules")) {
return id.toString().split("node_modules/")[1].split("/")[0].toString();
}
}, // 用于从入口点创建的块的打包输出格式[name]表示文件名,[hash]表示该文件内容hash值
entryFileNames: "js/[name].[hash].js", // 用于命名代码拆分时创建的共享块的输出命名
chunkFileNames: "js/[name].[hash].js", // 用于输出静态资源的命名,[ext]表示文件扩展名
},
},
},
plugins: [
vue(),
UnoCSS(),
svgBuilder(resolve("./public/icons")), // svg存储的名字
AutoComplete({
imports: [
"vue",
"vue-router",
"pinia", // 自动导入 Pinia API(如 defineStore)
],
dts: resolve(pathSrc, "auto-imports.d.ts"),
}),
],
resolve: {
alias: {
"@/": `${pathSrc}/`,
},
},
server: {
host: "localhost",
port: 3001,
proxy:{
'/api':{
target:'http://192.168.31.149:5006',
changeOrigin:true,
}
}
},
esbuild: {
drop: ["debugger"],
},
optimizeDeps: { // 在开发服务器启动时预构建依赖,将 CommonJS/UMD 模块转换为 ESM 格式,并缓存结果以提高性能。
include: [],
},
css:{
preprocessorOptions: {
scss: {
additionalData: `@use "@/styles/utils.scss" as cssUtils;`,
}
}
}
});