feat: 代码更新
This commit is contained in:
commit
24220d8f27
|
|
@ -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
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
/dist/*
|
||||
.local
|
||||
.output.js
|
||||
.github
|
||||
.vscode
|
||||
/node_modules/**
|
||||
|
||||
*.html
|
||||
**/*.svg
|
||||
**/*.sh
|
||||
|
||||
/public/**
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"printWidth": 160,
|
||||
"arrowParens": "always",
|
||||
"bracketSameLine": true,
|
||||
"bracketSpacing": true,
|
||||
"semi": true,
|
||||
"singleAttributePerLine": false,
|
||||
"vueIndentScriptAndStyle": true,
|
||||
"htmlWhitespaceSensitivity": "ignore"
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
||||
|
|
@ -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-----
|
||||
|
|
@ -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
|
||||
|
||||
}
|
||||
```
|
||||
|
|
@ -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>
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
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
|
|
@ -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`)
|
||||
}
|
||||
|
||||
|
|
@ -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>
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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')
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup></script>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import SvgIcon from './SvgIcon.vue'
|
||||
import type {SvgIconProps} from './SvgIcon.vue'
|
||||
|
||||
export {
|
||||
SvgIcon
|
||||
}
|
||||
|
||||
export type {
|
||||
SvgIconProps
|
||||
}
|
||||
|
|
@ -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>
|
||||
Loading…
Reference in New Issue