feat: 代码更新
|
|
@ -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
|
||||||
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
|
@ -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;
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 40 KiB |
|
|
@ -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>
|
||||||
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
|
@ -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>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
|
@ -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>
|
||||||
|
<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>
|
||||||
|
|
@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 }
|
||||||
|
|
@ -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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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')
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export const privateRoutes = [];
|
||||||
|
|
@ -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')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
.text-color{
|
||||||
|
background: linear-gradient(90deg, #8FC8FF 0%, #FFFFFF 100%);
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
};
|
||||||
|
|
@ -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));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
@ -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 }} {{ 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>
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
<template>
|
||||||
|
<div>401 - Unauthorized</div>
|
||||||
|
</template>
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
interface Window {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -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/*"]
|
||||||
|
}
|
||||||
|
|
@ -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)" }]],
|
||||||
|
});
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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;`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||