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>