This commit is contained in:
lijianyou
2025-09-30 15:00:32 +08:00
parent 1b262f06b8
commit b4e759cf69
202 changed files with 22035 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
{
"presets": ["es2015"]
}
+2
View File
@@ -0,0 +1,2 @@
VITE_BASE_URL=/
VITE_API_URL=http://localhost
+2
View File
@@ -0,0 +1,2 @@
VITE_BASE_URL=/
VITE_API_URL=http://localhost:10101
+1
View File
@@ -0,0 +1 @@
VITE_BASE_URL=/
+16
View File
@@ -0,0 +1,16 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
.vscode
.history
.idea
*.log
*session.sql
package-lock.json
yarn.lock
stepin-template.session.sql
docs/.vitepress/cache
target
components.d.ts
+22
View File
@@ -0,0 +1,22 @@
{
"arrowParens": "always",
"bracketSpacing": true,
"endOfLine": "lf",
"htmlWhitespaceSensitivity": "css",
"insertPragma": false,
"singleAttributePerLine": false,
"bracketSameLine": false,
"jsxBracketSameLine": false,
"bracketLine": false,
"jsxSingleQuote": true,
"printWidth": 120,
"proseWrap": "preserve",
"quoteProps": "as-needed",
"requirePragma": false,
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"useTabs": false,
"vueIndentScriptAndStyle": true
}
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 stepui
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+83
View File
@@ -0,0 +1,83 @@
<h1 align="center">Stepin Template</h1>
<div align="center">
开箱即用的中后台前端/设计解决方案
<br/>
(原[ Vue Antd Admin](https://github.com/iczer/vue-antd-admin/) Vue3 版本)
![GitHub package.json version (subfolder of monorepo)](https://img.shields.io/github/package-json/v/stepui/stepin-template)
![GitHub repo size](https://img.shields.io/github/repo-size/stepui/stepin-template)
![GitHub last commit](https://img.shields.io/github/last-commit/stepui/stepin-template)
![GitHub release (latest by date)](https://img.shields.io/github/v/release/stepui/stepin-template)
![Docs by iczer](https://img.shields.io/badge/docs%20by-iczer-green)
![preview](./docs/images/preview.png)
</div>
English | [简体中文](./README.md)
- 预览地址: https://stepui.gitee.io/stepin-template
- 使用文档: http://stepui.gitee.io/stepin-template-docs/
- 常见问题: http://stepui.gitee.io/stepin-template-docs/issue
- 国内镜像: https://gitee.com/stepui/stepin-template
## 特性
- 强大的主题定制功能,实时动态切换
- 多页签功能,助力后台管理高效开发
- 内置权限控制,兼容 ABAC 和 RBAC 模型
- 丰富的内置业务组件和常用页面模板
## 浏览器支持
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/opera/opera_48x48.png" alt="Opera" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Opera |
| --- | --- | --- | --- | --- |
| last 2 versions | last 2 versions | last 2 versions | last 2 versions | last 2 versions |
## 环境要求
|git|node|yarn|
|---|----|----|
|`^2.15.0`|`^16.14.0`|`^1.21.1`|
## 使用
### 拉取代码
```bash
git clone https://github.com/stepui/stepin-template.git
```
### 安装依赖
```bash
yarn install
```
### 启动
```
yarn dev
```
### 预览
启动成功后,控制台会显示本地访问地址:http://127.0.0.1:5173,浏览器打开即可预览。
更新多信息请参考 [使用文档](http://stepui.gitee.io/stepin-template-docs/)
## 参与贡献
我们非常欢迎你的贡献,你可以通过以下方式和我们一起共建 :star2::
- 在你的公司或个人项目中使用 Stepin Template。
- 通过 [Issue](https://github.com/stepui/stepin-template/issues/new) 报告:bug:或进行咨询。
- 提交 [Pull Request](https://github.com/stepui/stepin-template/pulls) 改进 Stepin Template 的代码。
- 加入社群,与小伙伴们一同交流心得。QQ 群:441231578
## 打赏作者
如果该项目对您有所帮助,可以请作者喝一杯咖啡。
<p>
<img src="./docs/images/alipay.png" width="320px" style="display: inline-block; border-radius: 8px;" />
<img src="./docs/images/wechatpay.png" width="320px" style="display: inline-block; margin-left: 24px; border-radius: 8px;" />
</p>
+84
View File
@@ -0,0 +1,84 @@
<h1 align="center">Stepin Template</h1>
<div align="center">
开箱即用的中后台前端/设计解决方案
<br/>
(原[ Vue Antd Admin](https://github.com/iczer/vue-antd-admin/) Vue3 版本)
![GitHub package.json version (subfolder of monorepo)](https://img.shields.io/github/package-json/v/stepui/stepin-template)
![GitHub repo size](https://img.shields.io/github/repo-size/stepui/stepin-template)
![GitHub last commit](https://img.shields.io/github/last-commit/stepui/stepin-template)
![GitHub release (latest by date)](https://img.shields.io/github/v/release/stepui/stepin-template)
![Docs by iczer](https://img.shields.io/badge/docs%20by-iczer-green)
![preview](./docs/images/preview.png)
</div>
简体中文 | [English](./README-en_US.md)
- 预览地址: https://stepui.gitee.io/stepin-template
- 使用文档: http://stepui.gitee.io/stepin-template-docs/
- 常见问题: http://stepui.gitee.io/stepin-template-docs/issue
- 国内镜像: https://gitee.com/stepui/stepin-template
## 特性
- 强大的主题定制功能,实时动态切换
- 多页签功能,助力后台管理高效开发
- 内置权限控制,兼容 ABAC 和 RBAC 模型
- 丰富的内置业务组件和常用页面模板
## 浏览器支持
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/opera/opera_48x48.png" alt="Opera" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Opera |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| last 2 versions | last 2 versions | last 2 versions | last 2 versions | last 2 versions |
## 环境要求
| git | node | yarn |
| --------- | ---------- | --------- |
| `^2.15.0` | `^16.14.0` | `^1.21.1` |
## 使用
### 拉取代码
```sh
git clone https://github.com/stepui/stepin-template.git
```
### 安装依赖
```sh
yarn install
```
### 启动
```sh
yarn dev
```
### 预览
启动成功后,控制台会显示本地访问地址:http://127.0.0.1:5173,浏览器打开即可预览。
更新多信息请参考 [使用文档](http://stepui.gitee.io/stepin-template-docs/)
## 参与贡献
我们非常欢迎你的贡献,你可以通过以下方式和我们一起共建 :star2::
- 在你的公司或个人项目中使用 Stepin Template。
- 通过 [Issue](https://github.com/stepui/stepin-template/issues/new) 报告:bug:或进行咨询。
- 提交 [Pull Request](https://github.com/stepui/stepin-template/pulls) 改进 Stepin Template 的代码。
- 加入社群,与小伙伴们一同交流心得。QQ 群:441231578
- less报错
yarn remove less && yarn add less@4.1.3 -D
## 打赏作者
如果该项目对您有所帮助,可以请作者喝一杯咖啡。
<p>
<img src="./docs/images/alipay.png" width="320px" style="display: inline-block; border-radius: 8px;" />
<img src="./docs/images/wechatpay.png" width="320px" style="display: inline-block; margin-left: 24px; border-radius: 8px;" />
</p>
+86
View File
@@ -0,0 +1,86 @@
import { series, src, dest } from 'gulp';
import clean from 'gulp-clean';
import replace from 'gulp-replace';
import vueTsc from './vueTemplateTsc';
export { default as tsc } from './vueTemplateTsc';
function cleanAll() {
return src(['./target/*', '!./target/node_modules'], { allowEmpty: true }).pipe(clean({ force: true }));
}
function copyIndex() {
return src(['./index.html']).pipe(replace('.ts', '.js')).pipe(replace(' + TS', '')).pipe(dest('./target'));
}
function copyPublic() {
return src(['./public/**/*']).pipe(dest('./target/public'));
}
function copyStyleAndJs() {
return src(['./src/**/*.{less,css,js}']).pipe(dest('./target/src'));
}
function copyAssets() {
return src(['./src/assets/**/*']).pipe(dest('./target/src/assets'));
}
function copyConfig() {
return src([
'./.env*',
'./.gitignore',
'.babelrc',
'./*.{json,cjs}',
'./LICENSE',
'!./tsconfig.*',
'!./package*.json',
]).pipe(dest('./target'));
}
function copyReadme() {
return src(['./*.md']).pipe(replace('stepin-template.git', 'stepin-template-js.git')).pipe(dest('./target'));
}
function copyPackageJson() {
return src(['./package.json'])
.pipe(replace(/,?[\r\n]+.*"(gulp[\-\w]*|vue-tsc|typescript)"[^,\r\n]*/g, ''))
.pipe(replace(/vue-tsc --noEmit && /g, ''))
.pipe(dest('./target'));
}
function cleanJsRepository() {
return src([
'../stepin-template-js/*',
'!../stepin-template-js/node_modules',
'!../stepin-template-js/.git',
'!../stepin-template-js/.history',
'!../stepin-template-js/.vscode',
]).pipe(clean({ force: true }));
}
export function copyToJsRepository() {
return src([
'./target/**/*',
'./.env*',
'./.gitignore',
'.babelrc',
'!./target/node_modules/**/*',
'!./target/node_modules',
]).pipe(dest('../stepin-template-js'));
}
export function copyDocs() {
return src(['./docs/**/*', '!./docs/.vitepress/**/*', '!./docs/.vitepress']).pipe(dest('../stepin-template-js/docs'));
}
export const makeJs = series(cleanJsRepository, copyToJsRepository, copyDocs);
export default series(
cleanAll,
vueTsc,
copyIndex,
copyPublic,
copyStyleAndJs,
copyConfig,
copyReadme,
copyAssets,
copyPackageJson
);
+19
View File
@@ -0,0 +1,19 @@
import { Transform } from 'stream';
export class BufferTransform extends Transform {
constructor(transform) {
super({
objectMode: true,
transform(file, enc, callback) {
if (file.isBuffer()) {
file.contents = Buffer.from(transform(new String(file.contents)));
}
if (file.isStream()) {
console.log('stream file', file.path);
}
return callback(null, file);
},
});
return this;
}
}
+152
View File
@@ -0,0 +1,152 @@
import { src, dest, series } from 'gulp';
import rename from 'gulp-rename';
import replace from 'gulp-replace';
import { BufferTransform } from './transform';
import ts from 'typescript';
import prettier from 'prettier';
import tsConfig from '../tsconfig.json';
tsConfig.compilerOptions.sourceMap = false;
tsConfig.compilerOptions.importsNotUsedAsValues = 'preserve';
/**
* vue 文件代码格式化
* @param {*} code
* @returns
*/
const vueFormatter = () =>
new BufferTransform((code) =>
prettier.format(code, {
trailingComma: 'es5',
parser: 'vue',
vueIndentScriptAndStyle: true,
singleQuote: true,
})
);
/**
* ts 文件代码格式化
* @param {*} code
* @returns
*/
const tsFormatter = () =>
new BufferTransform((code) =>
prettier.format(code, {
trailingComma: 'es5',
parser: 'typescript',
singleQuote: true,
})
);
/** ts编译器 */
const tsToJs = (tsStrCode) => {
const code = tsStrCode
.replace(/defineEmits<[\s\S]*>\(\);/g, (match) =>
match.replace(/'?([\w:]*)'?:\s*\[.*\];/g, "'$1',").replace(/\(e:\s*(\'[^',]*\')[^;]*;/g, '$1,')
)
.replace(/<{([^\{\}<>]*)}>\(\)/g, '([$1])');
const result = ts.transpileModule(code, tsConfig);
const jsStrCode = result.outputText;
return jsStrCode;
};
/**
* SFC 文件 ts 代码编译
* @param {*} tmpContent
* @returns
*/
const handleTmpContent = (tmpContent) => {
if (!tmpContent) {
return tmpContent;
}
return tmpContent
.replace(/(?<=((@|:|v-)[\w\-]*="))([^"]*)(?=")/g, function (match) {
if (/\s*{[\s\S]*}\s*/.test(match)) {
return tsToJs(`const patch = ${match}`).replace(/;\s*/g, '').replace('const patch = ', '');
}
return `${tsToJs(match).replace(/;\s*/g, '')}`;
})
.replace(/(?<=({{))([^{}]*)(?=}})/g, function (match) {
if (/\s*{[\s\S]*}\s*/.test(match)) {
return tsToJs(`const patch = ${match}`).replace(/;\s*/g, '').replace('const patch = ', '');
}
return `${tsToJs(match).replace(/;\s*$/, '')}`;
});
};
function transformVue(options) {
return new BufferTransform((content) =>
content
.replace(/(?<=(\<template>))([\s\S]*)(?=(\<\/template>))/, function (match) {
return handleTmpContent(match);
})
.replace(/(?<=(\<script lang="ts".*>))([\s\S]*)(?=(\<\/script>))/, (match) => tsToJs(match))
.replace(/lang="ts"/, '')
);
}
function cleanTypeImport() {
const types = [
'MenuProps',
'GuiderOption',
'IconSelectGroup',
'IconSelectOption',
'PropType',
'EChartsType',
'Color',
'FormInstance',
'PaginationProps',
'Dayjs',
'Ref',
'TreeSelectProps',
'ComponentPublicInstance',
];
const typeImportRegexp = new RegExp(
'(?<=(import\\s+[\\w,\\s]*{[^{]*[,\\s]+))(' + types.join('|') + '),?(?=([^}]*\\s*}\\s*from))',
'g'
);
return new BufferTransform((content) =>
content
.replace(/import\s+{\s*Response\s*}\s+from\s+\'.*\';?/g, '')
.replace(typeImportRegexp, '')
.replace(/import\s+.*from\s+\'.*\/interface(.d.ts)?\';?/g, '')
.replace(/import\s+{\s*}\s*from\s+\'.*\'/g, '')
);
}
function transformTs() {
return new BufferTransform((content) => tsToJs(content));
}
function tsc() {
tsConfig.compilerOptions.importsNotUsedAsValues = 'preserve';
tsConfig.compilerOptions.preserveValueImports = true;
return src(['./src/**/*.vue'])
.pipe(transformVue())
.pipe(cleanTypeImport())
.pipe(vueFormatter())
.pipe(dest('./target/src'));
}
function tsTsc() {
tsConfig.compilerOptions.importsNotUsedAsValues = 'remove';
tsConfig.compilerOptions.preserveValueImports = false;
return src(['./src/**/*.ts', '!./src/**/*.d.ts'])
.pipe(transformTs())
.pipe(replace('{ts,vue,tsx}', '{js,vue}'))
.pipe(replace('(js|ts|tsx|vue)', '(js|jsx|vue)'))
.pipe(cleanTypeImport())
.pipe(tsFormatter())
.pipe(rename((path) => (path.extname = '.js')))
.pipe(dest('./target/src'));
}
function compileViteConfig() {
return src(['./vite.config.ts'])
.pipe(transformTs())
.pipe(tsFormatter())
.pipe(rename((path) => (path.extname = '.js')))
.pipe(dest('./target'));
}
export default series(tsc, tsTsc, compileViteConfig);
+128
View File
@@ -0,0 +1,128 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/logo1.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>抖音同步</title>
</head>
<body>
<div id="stepin-app">
<style>
html {
overflow: hidden;
}
body {
height: 100vh;
text-align: center;
align-items: center;
background-color: #011627;
display: flex;
justify-content: center;
overflow: hidden;
}
.loader {
color: rgb(201, 195, 195);
font-family: "Poppins", sans-serif;
font-weight: 500;
font-size: 20px;
-webkit-box-sizing: content-box;
box-sizing: content-box;
height: 40px;
padding: 10px 10px;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
text-align: center;
justify-content: center;
border-radius: 8px;
}
.words {
overflow: hidden;
}
.word {
display: block;
height: 100%;
padding-left: 6px;
color: #ffca29;
animation: spin_4991 4s infinite;
}
@keyframes spin_4991 {
10% {
-webkit-transform: translateY(-105%);
transform: translateY(-105%);
}
25% {
-webkit-transform: translateY(-100%);
transform: translateY(-100%);
}
35% {
-webkit-transform: translateY(-205%);
transform: translateY(-205%);
}
50% {
-webkit-transform: translateY(-200%);
transform: translateY(-200%);
}
60% {
-webkit-transform: translateY(-305%);
transform: translateY(-305%);
}
75% {
-webkit-transform: translateY(-300%);
transform: translateY(-300%);
}
85% {
-webkit-transform: translateY(-405%);
transform: translateY(-405%);
}
100% {
-webkit-transform: translateY(-400%);
transform: translateY(-400%);
}
}
</style>
<!-- <img style="margin-bottom: 8px;" width="64px" src="/vite.svg" /> -->
<div class="loader">
<!-- <img src="/vite.svg"> -->
<div> loading ...</div>
<!-- <div class="words">
<span class="word">buttons</span>
<span class="word">forms</span>
<span class="word">switches</span>
<span class="word">cards</span>
<span class="word">buttons</span>
</div> -->
</div>
<div class="" style="font-size: 15px; color: rgba(249, 244, 244, 0.55)">首次加载可能较慢,请耐心等待...</div>
</div>
<script type="module" src="/src/main.ts"></script>
<script>
if (!global) {
var global = globalThis;
}
</script>
</body>
<!-- <link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/font-awesome/5.15.4/css/all.min.css"> -->
<style>
html {
overflow: hidden;
}
</style>
</html>
+12
View File
@@ -0,0 +1,12 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
"target": "ES6",
"module": "ESNext",
"allowSyntheticDefaultImports": true
},
"exclude": ["node_modules", "dist"]
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

+74
View File
@@ -0,0 +1,74 @@
{
"name": "stepin-template",
"private": true,
"version": "1.2.0-preview",
"type": "module",
"scripts": {
"dev": "vite",
"api": "node service/index.js",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview",
"deploy": "yarn build && gh-pages -d dist -b pages -r https://gitee.com/stepui/stepin-template.git",
"deploy:github": "yarn build --mode github && gh-pages -d dist -b master -r git@github.com:stepui/stepui.github.io.git",
"docs:dev": "vitepress dev docs",
"docs:build": "vitepress build docs",
"docs:serve": "vitepress serve docs",
"docs:preview": "vitepress preview docs",
"docs:deploy": "vitepress build docs && gh-pages -d docs/.vitepress/dist -b main -r https://gitee.com/stepui/stepin-template-docs.git"
},
"dependencies": {
"@ant-design/icons-vue": "^6.1.0",
"@fortawesome/fontawesome-free": "^5.15.4",
"@vueuse/core": "^10.1.0",
"ant-design-vue": "^3.2.15",
"axios": "^0.21.1",
"clipboard": "^2.0.8",
"dayjs": "^1.11.6",
"default-passive-events": "^2.0.0",
"echarts": "^5.3.3",
"enquire.js": "^2.1.6",
"js-cookie": "^2.2.1",
"lodash": "^4.17.21",
"nprogress": "^0.2.0",
"pinia": "^2.0.33",
"qs": "^6.10.1",
"stepin": "2.1.42-beta",
"vue": "^3.3.4",
"vue-json-editor": "^1.4.3",
"vue-request": "^2.0.4",
"vue-router": "^4.1.6"
},
"devDependencies": {
"@tailwindcss/container-queries": "^0.1.0",
"@types/js-cookie": "^3.0.2",
"@types/lodash": "^4.14.191",
"@types/mockjs": "^1.0.3",
"@types/node": "^14.14.37",
"@types/nprogress": "^0.2.0",
"@types/qs": "^6.9.6",
"@vitejs/plugin-vue": "^4.1.0",
"autoprefixer": "^10.4.7",
"babel-preset-es2015": "^6.24.1",
"babel-register": "^6.26.0",
"body-parser": "^1.20.1",
"crypto-js": "^4.1.1",
"gh-pages": "^3.1.0",
"gulp": "^4.0.2",
"gulp-clean": "^0.4.0",
"gulp-rename": "^2.0.0",
"gulp-replace": "^1.1.4",
"highlight.js": "^11.6.0",
"less": "4.1.3",
"less-loader": "^12.3.0",
"mockjs": "^1.1.0",
"postcss": "^8.4.14",
"prettier": "^2.8.7",
"tailwindcss": "^3.2.4",
"typescript": "^4.6.4",
"unplugin-vue-components": "^0.24.1",
"vite": "^4.2.1",
"vite-plugin-compression": "^0.5.1",
"vitepress": "^1.0.0-alpha.61",
"vue-tsc": "^1.0.13"
}
}
+7510
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
plugins: [require('tailwindcss'), require('autoprefixer')],
};
+7
View File
@@ -0,0 +1,7 @@
<svg width="40" height="40" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<rect fill="rgba(0, 0, 0, 0)" width="20" height="20" />
<g fill="rgba(0, 0, 220, 0.05)" >
<rect width="10" height="10" />
<rect x="10" y="10" width="10" height="10" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 320 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+147
View File
@@ -0,0 +1,147 @@
<template>
<ThemeProvider is-root v-bind="themeConfig" :apply-style="false">
<stepin-view system-name="douyin.sync.net" :class="`${contentClass}`" :user="user" :navMode="navigation" :useTabs="useTabs" :themeList="themeList" v-model:show-setting="showSetting" v-model:theme="theme" @themeSelect="configTheme" logo-src="@/assets/logo1.png">
<template #headerActions>
<HeaderActions @showSetting="showSetting = true" />
</template>
<template #pageFooter>
<PageFooter />
</template>
<template #themeEditorTab>
<a-tab-pane tab="其它" key="other">
<Setting />
</a-tab-pane>
</template>
</stepin-view>
</ThemeProvider>
<my-personal ref="personalRef" />
<email-set ref="emailRef" />
<!-- <login-modal :unless="['/login']" /> -->
</template>
<script lang="ts" setup>
import { reactive, ref, computed, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { useAccountStore, useMenuStore, useSettingStore, storeToRefs, useApiStore } from '@/store';
import avatar from '@/assets/avatar.png';
import { PageFooter, HeaderActions } from '@/components/layout';
import Setting from './components/setting';
import { LoginModal } from '@/pages/login';
import { MyPersonal, EmailSet } from '@/pages/personal';
import { configTheme, themeList } from '@/theme';
import { ThemeProvider } from 'stepin';
// logout,profile
const { logout } = useAccountStore();
const showPersonalDrawer = ref<boolean>(false);
const personalRef = ref(null);
const emailRef = ref(null);
const showSetting = ref(false);
const router = useRouter();
// useMenuStore().getMenuList();
const { navigation, useTabs, theme, contentClass } = storeToRefs(useSettingStore());
const themeConfig = computed(() => themeList.find((item) => item.key === theme.value)?.config ?? {});
const user = reactive({
name: 'admin',
avatar: avatar,
menuList: [
// { title: '个人中心', key: 'personal', icon: 'UserOutlined', onClick: () => router.push('/profile') },
// { title: '设置', key: 'setting', icon: 'SettingOutlined', onClick: () => (showSetting.value = true) },
// { type: 'divider' },
{
title: '个人设置',
key: 'seting',
icon: 'SmileOutlined',
onClick: () => {
personalRef.value.show(true);
},
},
{ type: 'divider' },
// {
// title: '邮件通知',
// key: 'email',
// icon: 'BellOutlined',
// onClick: () => {
// emailRef.value.showEmail(true);
// },
// },
{ type: 'divider' },
{
title: '退出登录',
key: 'logout',
icon: 'LogoutOutlined',
onClick: () => logout().then(() => router.push('/login')),
},
],
});
onMounted(() => {
useApiStore()
.apiCheckInitStatus()
.then((res) => {
if (res.code === 0) {
useApiStore()
.apiUserInfo()
.then((res) => {
if (res.code === 0 && res.code !== '') {
if (res.data.avatar && res.data.avatar != null) {
user.avatar = `/upload/${res.data.avatar}`;
}
user.name = res.data.userName;
}
});
}
});
});
</script>
<style lang="less">
.stepin-view {
::-webkit-scrollbar {
width: 4px;
height: 4px;
border-radius: 4px;
background-color: theme('colors.primary.500');
}
::-webkit-scrollbar-thumb {
border-radius: 4px;
background-color: theme('colors.primary.400');
&:hover {
background-color: theme('colors.primary.500');
}
}
::-webkit-scrollbar-track {
box-shadow: inset 0 0 1px rgba(0, 0, 0, 0);
border-radius: 4px;
background: theme('backgroundColor.layout');
}
}
html {
height: 100vh;
overflow-y: hidden;
}
body {
margin: 0;
height: 100vh;
overflow-y: hidden;
}
.stepin-img-checkbox {
@apply transition-transform;
&:hover {
@apply scale-105 ~"-translate-y-[2px]";
}
img {
@apply shadow-low rounded-md transition-transform;
}
}
</style>
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

+30
View File
@@ -0,0 +1,30 @@
<script lang="ts" setup>
import { PropType } from 'vue';
export type AvatarType = {
nickname: string;
avatar: string;
};
defineProps({
source: Array as PropType<AvatarType[]>,
size: {
type: Number,
default: 22,
required: false,
},
})
</script>
<template>
<div class="avatar-list">
<a-avatar :style="`margin-left: -${size / 2}px`" v-for="(item, i) in source" :size="size" :key="i"
:src="item.avatar" />
</div>
</template>
<style lang="less" scoped>
.avatar-list {
:deep(.ant-avatar) {
@apply outline-1 outline-white outline;
}
}
</style>
+110
View File
@@ -0,0 +1,110 @@
<template>
<div style="height: 240px" ref="container" class="bar-chart"></div>
</template>
<script lang="ts" setup>
import { onBeforeUnmount, onMounted, PropType, ref } from 'vue';
import { EChartsType, Color } from 'echarts';
import * as echarts from 'echarts';
const container = ref<HTMLElement>();
let chart: EChartsType | null = null;
const props = defineProps({
color: Array as PropType<Color[]>,
list: Array,
});
function resize() {
chart?.resize();
}
onMounted(() => {
chart = echarts.init(container.value!);
chart.setOption({
color: props.color ?? ['#ff0000'],
backgroundColor: {
type: 'linear',
x: 0,
y: 0,
x2: 1,
y2: 0,
colorStops: [
{
offset: 0,
color: '#00369e',
},
{
offset: 0.33,
color: '#005cfd',
},
{
offset: 1,
color: '#a18dff',
},
],
},
grid: [
{
top: 40,
left: 56,
right: 20,
bottom: 40,
},
],
xAxis: [
{
name: '时间',
nameTextStyle: { color: 'rgba(0, 0, 0, 0)' },
type: 'category',
axisTick: { show: false },
axisLine: { show: false },
axisLabel: { color: '#fff' },
splitLine: {
show: false,
},
},
],
darkMode: true,
yAxis: [
{
name: '销售额',
nameTextStyle: { color: 'rgba(0, 0, 0, 0)' },
type: 'value',
axisTick: { show: false },
axisLine: { show: false },
axisLabel: { color: '#fff' },
splitLine: {
lineStyle: {
type: 'dashed',
width: 2,
color: 'rgba(255, 255, 255, 0.25)',
},
},
},
],
series: [
{
type: 'bar',
barWidth: 24,
itemStyle: {
borderRadius: 4,
},
data: props.list,
},
],
});
window.addEventListener('resize', resize);
});
onBeforeUnmount(() => {
window.removeEventListener('resize', resize);
});
</script>
<style scoped lang="less">
.bar-chart {
:deep(canvas) {
@apply rounded-lg;
}
}
</style>
+116
View File
@@ -0,0 +1,116 @@
<template>
<div
style="width: 100%; height: 400px"
class="line-chart"
ref="container"
></div>
</template>
<script lang="ts" setup>
import { onBeforeUnmount, onMounted, ref, nextTick } from 'vue';
import type { EChartsType } from 'echarts';
import * as echarts from 'echarts';
let chart: EChartsType | null = null;
const container = ref<HTMLElement>();
function resize() {
chart?.resize();
}
onMounted(() => {
chart = echarts.init(container.value!);
chart.setOption({
color: ['#005af9', '#985af9'],
grid: [
{
top: 100,
left: 32,
right: 12,
bottom: 20,
},
],
xAxis: [
{
name: '时间',
nameTextStyle: { color: 'rgba(0 , 0, 0, 0)' },
type: 'category',
axisTick: { show: false },
axisLine: { show: false },
boundaryGap: 0,
splitLine: {
show: false,
},
},
],
yAxis: [
{
name: '销售额',
nameTextStyle: { color: 'rgba(0 , 0, 0, 0)' },
type: 'value',
axisTick: { show: false },
axisLine: { show: false },
splitLine: {
lineStyle: {
type: 'dashed',
width: 2,
color: 'rgba(0, 0, 0, 0.15)',
},
},
},
],
legend: {
show: true,
right: '8',
top: 0,
orient: 'vertical',
},
tooltip: {
show: true,
trigger: 'axis',
},
series: [
{
name: '销售额',
type: 'line',
smooth: true,
lineStyle: {
width: 3,
},
data: [
['一月', 12],
['二月', 8],
['三月', 92],
['四月', 32],
['五月', 22],
['六月', 89],
['七月', 72],
],
},
{
name: '订单',
type: 'line',
smooth: true,
width: 4,
lineStyle: {
width: 3,
},
data: [
['一月', 12],
['二月', 8],
['三月', 24],
['四月', 32],
['五月', 56],
['六月', 56],
['七月', 56],
],
},
],
});
window.addEventListener('resize', resize);
});
onBeforeUnmount(() => {
window.removeEventListener('resize', resize);
});
</script>
@@ -0,0 +1,142 @@
<script lang="ts" setup>
import { computed, nextTick, PropType, ref, toRefs } from 'vue';
export type AntInputType =
| 'input'
| 'textarea'
| 'radio'
| 'timePicker'
| 'datePicker'
| 'rangePicker'
| 'select'
| 'mention'
| 'rate'
| 'upload'
| 'treeSelect'
| 'transfer'
| 'checkbox'
| 'cascader'
| 'autoComplete'
| 'inputNumber'
| 'slider'
| 'switch';
const emit = defineEmits<{
(e: 'update:edit', edit: boolean): void;
(e: 'update:value', value: any): void;
(e: 'pressEnter', event: KeyboardEvent): void;
}>();
const props = defineProps({
value: [String, Number, Boolean, Object],
edit: {
type: Boolean,
default: null,
},
editOnClick: {
type: Boolean,
default: true,
},
type: {
type: String as PropType<AntInputType>,
default: 'input',
validator(val: string) {
return [
'input',
'radio',
'timePicker',
'datePicker',
'rangePicker',
'select',
'mention',
'rate',
'upload',
'treeSelect',
'transfer',
'checkbox',
'cascader',
'autoComplete',
'inputNumber',
'slider',
'switch',
'textarea',
].includes(val);
},
},
options: Object,
});
const { type } = toRefs(props);
const component = computed(() => {
const _type = type.value;
return 'A' + _type.substring(0, 1).toUpperCase() + _type.substring(1);
});
const cacheEdit = ref(false);
const _edit = computed({
get() {
if (props.edit !== null) {
cacheEdit.value = props.edit;
}
return props.edit ?? cacheEdit.value;
},
set(val) {
cacheEdit.value = val;
emit('update:edit', val);
},
});
const input = ref();
function editCell() {
if (props.editOnClick) {
_edit.value = true;
nextTick(() => input.value?.focus());
}
}
function complete() {
_edit.value = false;
}
const cacheVal: any = ref(null);
const _value = computed({
get() {
if (props.value !== undefined) {
cacheVal.value = props.value;
}
return props.value ?? cacheVal.value;
},
set(val) {
cacheVal.value = val;
emit('update:value', val);
},
});
</script>
<template>
<slot v-if="_edit" class="editable-cell-input" name="input">
<component
ref="input"
@keyup.enter="complete"
@blur="complete"
v-model:value="_value"
class="editable-cell-input-component"
v-bind="options"
:is="component"
/>
</slot>
<div v-else @click="editCell" class="editable-cell-show">
<slot>
{{ value }}
</slot>
</div>
</template>
<style lang="less" scoped>
.editable-cell {
&-input {
&-component {
}
}
}
</style>
@@ -0,0 +1,2 @@
import EditableCell from './EditableCell.vue';
export default EditableCell;
+49
View File
@@ -0,0 +1,49 @@
<template>
<div v-if="parts" class="splitter">
<template v-for="(part, i) in parts" :key="i">
<span class="splitter-part">{{ part }}</span>
</template>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
export default defineComponent({
name: 'Splitter',
props: {
value: String,
partLength: { type: Number, default: 4 },
sensitive: Array as PropType<number[]>,
},
setup(props, { attrs, slots, emit }) {},
computed: {
parts(): string[] {
let { value = '', partLength, sensitive = [] } = this;
const [start = -1, end = -1] = sensitive;
let sense = '';
for (let i = 0; i < end - start; i++) {
sense += '*';
}
value = `${value.substring(0, start)}${sense}${value.substring(
end,
value.length
)}`;
const parts = [];
for (let i = 0; i < value.length; i += partLength) {
parts.push(value.substring(i, i + partLength));
}
return parts;
},
},
});
</script>
<style lang="less" scoped>
.splitter {
&-part {
&:not(:first-child) {
@apply ml-2;
}
}
}
</style>
@@ -0,0 +1,32 @@
<script lang="ts" setup>
import { ref, PropType, watch } from 'vue';
import { FullscreenExitOutlined, FullscreenOutlined } from '@ant-design/icons-vue';
import { useFullScreen } from '@/utils/htmlHelper';
const prop = defineProps({
target: { type: [String, Object] as PropType<string | HTMLElement>, required: true },
});
const emit = defineEmits<{
(e: 'change', value: boolean);
}>();
const { enterFullScreen, exitFullscreen, isEnter } = useFullScreen(prop.target);
function toggle() {
if (isEnter.value) {
exitFullscreen();
} else {
enterFullScreen();
}
}
watch(isEnter, (val) => emit('change', val));
</script>
<template>
<div class="inline-block text-lg" @click="toggle">
<FullscreenExitOutlined v-if="isEnter" />
<FullscreenOutlined v-else />
</div>
</template>
<style scoped lang="less"></style>
+366
View File
@@ -0,0 +1,366 @@
<script lang="ts" setup>
import { offsetScreen } from '@/utils/htmlHelper';
import { GuiderOption, GuideTarget } from './interface';
import { PropType, watch, reactive, computed, ref, onMounted, onBeforeUnmount } from 'vue';
const props = defineProps({
current: [HTMLElement, Object, String] as PropType<GuideTarget>,
options: {
required: true,
type: Array<GuiderOption>,
},
show: Boolean,
});
const index = ref(0);
const currentIndex = computed({
get(): number {
if (!props.current) {
return index.value;
}
index.value = props.options.findIndex((item) => item.target === props.current);
return index.value;
},
set(val) {
const target = props.options[val];
if (target) {
index.value = val;
emit('update:current', target.target);
} else {
index.value = 0;
emit('close');
emit('update:current', props.options[0].target);
emit('update:show', false);
}
},
});
const doc = ref<HTMLElement>();
const flag = ref(props.show);
const location = reactive({
width: 0,
height: 0,
left: 0,
top: 0,
right: 0,
bottom: 0,
});
// 事件
// defineEmits(['close', 'update:show', 'update:current'])
const emit = defineEmits<{
(e: 'close'): void;
(e: 'update:show', show: boolean): void;
(e: 'update:current', target?: GuideTarget): void;
}>();
// 目标 html 元素
const targetEl = computed<HTMLElement>(() => {
const _el = props.current ?? props.options[index.value]?.target;
if (!_el) {
return;
}
if (typeof _el === 'string') {
return document.querySelector(_el);
} else {
// @ts-ignore
return _el instanceof HTMLElement ? _el : _el.$el;
}
});
// 方位
type Direction = 'vertical' | 'horizontal';
// 朝向
type Site = 'top' | 'right' | 'bottom' | 'left';
// 位置
type Placement = {
main: Direction;
sub: Direction;
vertical: Site | 'center';
horizontal: Site | 'center';
};
// doc 文档显示位置
const placement = computed<Placement>(() => {
const { offsetWidth: docWidth, offsetHeight: docHeight } = doc.value || {
offsetWidth: 0,
offsetHeight: 0,
};
const { left: tLeft, top: tTop, right: tRight, bottom: tBottom, height: tHeight, width: tWidth } = location;
const p: Placement = {
main: 'horizontal',
sub: 'vertical',
horizontal: 'left',
vertical: 'top',
};
// 判断文档位置在主方向为水平还是垂直
if (Math.max(tLeft, tRight) >= Math.max(tTop, tBottom)) {
p.main = 'horizontal';
p.sub = 'vertical';
p.horizontal = tLeft > tRight ? 'left' : 'right';
if (tTop + tHeight / 2 < docHeight / 2 && tBottom + tHeight / 2 > docHeight / 2) {
p.vertical = 'top';
} else if (tTop + tHeight / 2 > docHeight / 2 && tBottom + tHeight / 2 < docHeight / 2) {
p.vertical = 'bottom';
} else {
p.vertical = 'center';
}
} else {
p.main = 'vertical';
p.sub = 'horizontal';
p.vertical = tTop > tBottom ? 'top' : 'bottom';
if (tLeft + tWidth / 2 < docWidth / 2 && tRight + tWidth / 2 > docWidth / 2) {
p.horizontal = 'left';
} else if (tLeft + tWidth / 2 > docWidth / 2 && tRight + tWidth / 2 < docWidth / 2) {
p.horizontal = 'right';
} else {
p.horizontal = 'center';
}
}
return p;
});
type Position = {
top?: number;
right?: number;
bottom?: number;
left?: number;
};
// 指引文档位置
const docPosition = computed(() => {
const p: Position = { left: 0, top: 0 };
if (!props.show || !doc.value || !flag.value) {
return p;
}
const { top, left, right, bottom, height, width } = location;
const place = placement.value;
const main = place[place.main] as Site;
const sub = place[place.sub];
const margin = 10;
const offset = (place.main === 'horizontal' ? doc.value?.offsetWidth : doc.value?.offsetHeight) ?? 0;
p[main] = location[main] - offset - margin;
if (main === 'right') {
p.left = left + width + margin;
} else if (main === 'bottom') {
p.top = top + height + margin;
}
if (sub === 'center') {
if (place.main === 'horizontal') {
p.top = top + height / 2 - (doc.value?.offsetHeight ?? 0) / 2;
} else {
p.left = left + width / 2 - (doc.value?.offsetWidth ?? 0) / 2;
}
} else {
p[sub] = location[sub];
}
if (p.left === undefined) {
p.left = window.innerWidth - p.right! - doc.value?.offsetWidth!;
}
if (p.top === undefined) {
p.top = window.innerHeight - p.bottom! - doc.value?.offsetHeight!;
}
return p;
});
// 指示箭头位置
const arrowStyle = computed(() => {
const p: Position = { left: -16, top: -16 };
if (!props.show || !doc.value || !flag.value) {
return p;
}
const place = placement.value;
const main = place[place.main] as Site;
const sub = place[place.sub];
const { offsetHeight = 0, offsetWidth = 0 } = doc.value ?? {};
if (main === 'left') {
p.left = (offsetWidth ?? 0) - 4;
} else if (main === 'top') {
p.top = (offsetHeight ?? 0) - 4;
}
if (place.main === 'horizontal') {
if (sub === 'center') {
p.top = offsetHeight / 2 - 10;
} else if (sub === 'bottom') {
p.top = offsetHeight - location.height / 2 - 10;
} else {
p.top = location.height / 2 - 10;
}
}
if (place.main === 'vertical') {
if (sub === 'center') {
p.left = offsetWidth / 2 - 10;
} else if (sub === 'right') {
p.left = offsetWidth - location.width / 2 - 10;
} else {
p.left = location.width / 2 - 10;
}
}
return p;
});
/**
* 设置目标元素位置
*/
function setPosition() {
const el = targetEl.value;
if (!el) {
return;
}
const p = offsetScreen(el);
location.left = p[0];
location.top = p[1];
location.width = el.offsetWidth;
location.height = el.offsetHeight;
location.right = window.innerWidth - location.width - location.left;
location.bottom = window.innerHeight - location.height - location.top;
}
watch(() => [targetEl.value, props.show], setPosition);
watch(
() => props.show,
(val) => {
flag.value = val;
},
{
flush: 'post',
}
);
onMounted(() => {
window.addEventListener('resize', setPosition);
});
onBeforeUnmount(() => {
window.removeEventListener('resize', setPosition);
});
function nextStep() {
currentIndex.value += 1;
}
function onClose() {
emit('close');
emit('update:show', false);
}
</script>
<template>
<Teleport to="body">
<div class="guider" :style="`display: ${show ? 'static' : 'none'}`">
<div class="guider-left" :style="`border-left: ${location.left - 2}px solid rgba(0, 0, 0, 0.25);`"></div>
<div
class="guider-top"
:style="`left: ${location.left - 2}px; width: ${location.width + 4}px; border-top: ${
location.top - 2
}px solid rgba(0, 0, 0, 0.25);`"
></div>
<div
class="guider-right"
:style="`left: ${location.left + location.width + 2}px; background-color: rgba(0, 0, 0, 0.25)`"
></div>
<div
class="guider-bottom"
:style="`left: ${location.left - 2}px; width: ${location.width + 4}px; top: ${
location.top + location.height + 2
}px`"
></div>
<div
ref="doc"
class="guider-doc flex flex-col justify-between rounded-md"
:style="`left: ${docPosition.left}px;top:${docPosition.top}px`"
>
<div
class="arrow"
:style="`left: ${arrowStyle.left}px; top: ${arrowStyle.top}px; border-${
placement[placement.main]
}-color: white`"
></div>
<div class="guider-content">
<h1>第一步</h1>
<div>
<slot></slot>
</div>
</div>
<div class="guider-footer flex justify-between w-full">
<a-button @click="onClose">关闭</a-button>
<a-button type="primary" @click="nextStep">下一步</a-button>
</div>
</div>
</div>
</Teleport>
</template>
<style scoped lang="less">
.guider-left {
position: fixed;
left: 0px;
top: 0px;
height: 100vh;
z-index: 99;
}
.guider-top {
position: fixed;
z-index: 99;
top: 0;
}
.guider-right {
top: 0;
right: 0;
position: fixed;
z-index: 99;
height: 100vh;
}
.guider-bottom {
position: fixed;
background-color: rgba(0, 0, 0, 0.25);
bottom: 0;
z-index: 99;
}
.guider-doc {
transition: all 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.125);
width: 20%;
height: 200px;
background-color: white;
position: fixed;
z-index: 100;
box-shadow: 0px 4px 20px 0px rgba(0, 0, 0, 0.5);
padding: 6px 8px;
.arrow {
transition: all 0.25s ease-in;
border-width: 10px;
z-index: 9;
border-style: solid;
position: absolute;
border-color: transparent;
}
.guider-footer {
}
}
</style>
+4
View File
@@ -0,0 +1,4 @@
import Guider from './Guider.vue';
export type { GuideTarget, GuiderOption } from './interface';
export default Guider;
+7
View File
@@ -0,0 +1,7 @@
import { ComponentPublicInstance, FunctionalComponent, AsyncComponentOptions, AsyncComponentLoader } from 'vue';
export type GuideTarget = HTMLElement | ComponentPublicInstance | String;
export type GuiderOption = {
target?: GuideTarget;
doc?: GuideTarget | FunctionalComponent | AsyncComponentOptions | AsyncComponentLoader;
};
+4
View File
@@ -0,0 +1,4 @@
<template>
<router-view></router-view>
</template>
<script lang="ts" setup></script>
+86
View File
@@ -0,0 +1,86 @@
<template>
<div class="common-view">
<div class="common-header">
<div class="common-header-main">
<div class="logo">
<img class="img" src="@/assets/vite.svg" />
</div>
<div class="navigation">
<div class="nav-item">
<stepin-link to="/login"> 文档 </stepin-link>
</div>
<div class="nav-item">
<stepin-link to="/login"> API </stepin-link>
</div>
<div class="nav-item">
<stepin-link to="/login"> 关于 </stepin-link>
</div>
<div class="nav-item">
<stepin-link to="/login"> 商业合作 </stepin-link>
</div>
</div>
<div class="actions">
<a-button class="login-btn" type="primary">注册</a-button>
</div>
</div>
</div>
<div class="common-content">
<div class="main">
<router-view />
</div>
</div>
</div>
</template>
<script lang="ts" setup></script>
<style scoped lang="less">
.common-view {
display: grid;
min-height: 100vh;
grid-template-rows: 64px 1fr;
@apply bg-gray-800;
.common-header {
@apply bg-gray-800 flex items-center pt-lg;
&-main {
width: 1400px;
margin: 0 auto;
@apply flex items-center;
.logo {
flex: none;
.img {
height: 36px;
}
}
.navigation {
flex: 1;
display: flex;
align-items: center;
.nav-item {
font-size: 18px;
margin-left: 64px;
.stepin-link {
color: theme('colors.text-inverse');
&:hover {
color: theme('colors.primary.500');
}
}
}
}
.actions {
flex: none;
.login-btn {
height: 38px;
width: 88px;
font-size: 16px;
@apply bg-gray-700 border-gray-600 rounded-md hover:bg-gray-600;
}
}
}
}
.common-content {
.main {
width: 1400px;
margin: 0 auto;
}
}
}
</style>
@@ -0,0 +1,118 @@
<script lang="ts" setup>
import { LogoutOutlined } from '@ant-design/icons-vue';
import { onMounted } from 'vue';
import { ThemeProvider, alert } from 'stepin';
onMounted(() => {
// alert.info(
// `<div class="text-text">
// Stepin is a fast, light framework to Vue3 try it out today with the
// <span class="underline">Stepin Template Beta</span>.
// </div>`,
// { renderRaw: true, duration: -1 }
// );
});
const navList = [
{
title: 'Products',
children: [
{
title: 'Stepin Template',
list: ['Stepin Pro', 'Stepin Style', 'Stepin Admin'],
},
{
title: 'Stepin',
list: ['Stepin Vue', 'Stepin React', 'Stepin Angular'],
},
],
},
{
title: 'Developers',
children: [
{
title: 'Developers',
list: ['Docs', 'Get Started', 'UI Library', 'Community', 'Open Source'],
},
],
},
{
title: 'Sponsors',
},
{
title: 'Business',
children: [{ title: 'Business', list: ['Contact Us', 'Cooperation', 'Support'] }],
},
{
title: 'About Us',
},
];
</script>
<template>
<ThemeProvider :color="{ middle: { 'bg-base': '#1896ff' }, primary: { DEFAULT: '#1896ff' } }" :autoAdapt="true">
<div class="front-view flex flex-col">
<div class="text-text flex-1">
<div class="front-header flex items-baseline py-md px-xl">
<div class="text-xxl text-text hover:text-text">
<!-- <img src="@/assets/png.svg" /> -->
dy.sync.net
</div>
<!-- <div style="width: calc(100% - 430px)" class="front-navigation mx-xl flex overflow-hidden items-center text-lg overflow-ellipsis whitespace-nowrap">
<div :class="`front-nav-item flex items-center cursor-pointer mx-base ${nav.children ? 'with-list' : ''}`" v-for="nav in navList">
<template v-if="!nav.children">
{{ nav.title }}
</template>
<a-popover :mouseEnterDelay="0.1" v-else placement="bottom">
<div class="front-nav-item-content">
{{ nav.title }}
</div>
<template #content>
<div class="flex">
<div class="not-[:first-child]:ml-lg" v-for="group in nav.children">
<h3>{{ group.title }}</h3>
<div class="cursor-pointer hover:text-text text-subtext font-light py-xs text-lg" v-for="item in group.list">
{{ item }}
</div>
</div>
</div>
</template>
</a-popover>
</div>
</div>
<div>
<router-link to="/login" class="h-[46px] border-transparent hover:text-text hover:border-transparent text-lg text-text">
<LogoutOutlined class="mr-xs" />
Sign In
</router-link>
<a-button class="ml-md px-lg border-text hover:border-text hover:bg-text border-2 h-[46px] hover:text-bg-container" size="large">Get Started</a-button>
</div> -->
</div>
<div class="front-content ">
<router-view />
</div>
</div>
</div>
</ThemeProvider>
</template>
<style lang="less" scoped>
.front-view {
.front-header {
.front-nav-item {
&.with-list .front-nav-item-content {
&:after {
content: '';
@apply ~"h-[8px]" ~"w-[8px]" transition-transform ml-2 inline-block border-text border-l-0 border-t-0 border-r-2 border-b-2 border-solid ~"rotate-[-135deg]" translate-y-1/4;
}
&:hover {
&:after {
@apply ~"rotate-[45deg]" translate-y-0;
}
}
}
}
}
.front-content {
height: 100vh;
}
}
</style>
+65
View File
@@ -0,0 +1,65 @@
<script lang="ts" setup>
import { LogoutOutlined } from '@ant-design/icons-vue';
import { onMounted } from 'vue';
import { ThemeProvider, alert } from 'stepin';
import http from '@/store/http';
import { useRouter } from 'vue-router';
const router = useRouter();
// console.log(router.getRoutes());
onMounted(() => {
// alert.info(
// `<div class="text-text">
// Stepin is a fast, light framework to Vue3 try it out today with the
// <span class="underline">Stepin Template Beta</span>.
// </div>`,
// { renderRaw: true, duration: -1 }
// );
// console.log(router.getRoutes());
if (http.checkAuthorization()) {
// console.log(22222);
router.push('/dashboard');
} else {
router.push('/login');
}
});
</script>
<template>
<ThemeProvider :color="{ middle: { 'bg-base': '#fff','bg-container':'#fff','bg-container-light':'#fff' }, primary: { DEFAULT: '#1896ff' } }" :autoAdapt="true">
<div class="front-view flex flex-col" style="background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)">
<div class="text-xxl text-text hover:text-text" style="margin-left:20px;">
<img src="/logo1.png" />
douyin.sync.net
</div>
<div class="front-content ">
<router-view />
</div>
</div>
</ThemeProvider>
</template>
<style lang="less" scoped>
.front-view {
.front-header {
.front-nav-item {
&.with-list .front-nav-item-content {
&:after {
content: '';
@apply ~"h-[8px]" ~"w-[8px]" transition-transform ml-2 inline-block border-text border-l-0 border-t-0 border-r-2 border-b-2 border-solid ~"rotate-[-135deg]" translate-y-1/4;
}
&:hover {
&:after {
@apply ~"rotate-[45deg]" translate-y-0;
}
}
}
}
}
.front-content {
height: 100vh;
}
.front-view {
height: 100vh;
}
}
</style>
+100
View File
@@ -0,0 +1,100 @@
<script lang="ts" setup>
import { reactive } from 'vue';
import { StepinHeaderAction } from 'stepin';
import Notice from '@/components/notice/Notice.vue';
import DayNightSwitch from '@/components/switch/DayNightSwitch.vue';
import { BellOutlined } from '@ant-design/icons-vue';
import Fullscreen from '../fullscreen/Fullscreen.vue';
defineEmits<{
(e: 'showSetting'): void;
}>();
const noticeList = reactive([
{
title: '消息',
list: [
{
title: '影佑',
content: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
img: 'src/assets/avatar/face-1.jpg',
time: 0,
},
],
},
]);
</script>
<template>
<!-- <StepinHeaderAction>
<a-input placeholder="开始搜索...">
<template #prefix>
<search-outlined />
</template>
</a-input>
</StepinHeaderAction> -->
<StepinHeaderAction>
<DayNightSwitch />
</StepinHeaderAction>
<StepinHeaderAction>
<a-tooltip>
<!-- placement="rightTop" -->
<template #title><span style="color:yellow">Swagger Api</span></template>
<a class="action-item" href="/swagger" target="_blank">
<!-- <ApiOutlined /> -->
<img class="gitee-logo" alt="swagger api" src="@/assets/swagger.png" />
</a>
</a-tooltip>
</StepinHeaderAction>
<!-- <StepinHeaderAction>
<a class="action-item" href="http://github.com/stepui/stepin-template" target="_blank">
<GithubOutlined />
</a>
</StepinHeaderAction>
<StepinHeaderAction>
<a class="action-item" href="http://gitee.com/stepui/stepin-template" target="_blank">
<img class="gitee-logo" src="@/assets/gitee.svg" />
</a>
</StepinHeaderAction> -->
<!-- <StepinHeaderAction>
<div class="action-item setting" @click="$emit('showSetting')">
<SettingOutlined />
</div>
</StepinHeaderAction> -->
<!-- <a-popover placement="bottomRight">
<StepinHeaderAction>
<div class="action-item notice">
<BellOutlined />
</div>
</StepinHeaderAction>
<template #content>
<Notice :data-source="noticeList" />
</template>
</a-popover> -->
<!-- <StepinHeaderAction>
<Fullscreen class="-mx-xs -my-sm h-[56px] px-xs py-sm flex items-center" target=".stepin-layout" />
</StepinHeaderAction> -->
</template>
<style scoped lang="less">
.gitee-logo {
width: 20px;
}
.action-item {
font-size: 20px;
height: 100%;
margin: 0 -8px;
padding: 0 4px;
line-height: 40px;
display: flex;
align-items: center;
&.setting {
font-size: 18px;
}
&.notice {
font-size: 18px;
}
}
</style>
+3
View File
@@ -0,0 +1,3 @@
<template>
<div>link view</div>
</template>
+41
View File
@@ -0,0 +1,41 @@
<script lang="ts" setup></script>
<template>
<div class="page-footer">
<div class="links">
<!-- <a class="link" href="https://github.com/stepui/stepin-template" target="_blank"> Stepin 首页 </a> -->
<!-- <a class="link" href="https://github.com/stepui/stepin-template" target="_blank">
<GithubOutlined />
</a> -->
<!-- <a class="link" href="https://www.antdv.com/docs/vue/introduce-cn/" target="_blank"> Ant Design </a> -->
</div>
<div class="copyright">
Copyright
<CopyrightOutlined class="icon-copyright" />
2023 dy.net
</div>
</div>
</template>
<style scoped lang="less">
.page-footer {
text-align: center;
@apply text-gray-400;
.links {
display: flex;
justify-content: center;
.link {
@apply hover:text-gray-400 pl-4 pr-4;
}
}
.copyright {
margin-top: 8px;
.icon-copyright {
margin: 0;
}
}
}
</style>
+5
View File
@@ -0,0 +1,5 @@
export { default as HeaderActions } from './HeaderActions.vue';
export { default as PageFooter } from './PageFooter.vue';
export { default as BlankView } from './BlankView.vue';
// export { default as CommonView } from './CommonView.vue';
export { default as FrontView } from './FrontView.vue';
+59
View File
@@ -0,0 +1,59 @@
<script lang="ts" setup>
import { ref, PropType, computed } from 'vue';
import { PaginationProps } from 'ant-design-vue';
const props = defineProps({
pagination: { type: [Object, Boolean] as PropType<PaginationProps> },
column: { type: Number, default: 8 },
gap: [Array<Number>, Number],
dataSource: { type: Array<any>, default: [] },
});
const _pagination = computed<PaginationProps>(() => {
if (props.pagination && typeof props.pagination === 'boolean') {
return {};
}
return props.pagination ?? {};
});
const list = computed(() => {
if (typeof props.pagination === 'boolean' && !props.pagination) {
return props.dataSource?.slice(0);
}
const { current = 1, pageSize = 10 } = _pagination.value;
let start = 0;
let end = pageSize;
if (props.dataSource.length > pageSize) {
start = (current - 1) * pageSize;
end = current * pageSize;
}
return props.dataSource?.slice(start, end);
});
const col = computed(() => (list.value.length > 0 ? props.column : 1));
</script>
<template>
<div
v-bind="$attrs"
class="grid-list grid"
:style="`${column ? 'grid-template-columns:repeat(' + col + ', minmax(0, 1fr))' : ''}`"
>
<template v-if="list.length > 0" v-for="item in list">
<slot name="renderItem" :item="item">
{{ item }}
</slot>
</template>
<template v-else>
<a-empty />
</template>
</div>
<a-pagination
class="mt-3"
v-if="pagination"
v-bind="{ total: dataSource?.length, ...(pagination as PaginationProps) }"
/>
</template>
<style lang="less" scoped>
.grid-list {
}
</style>
+71
View File
@@ -0,0 +1,71 @@
<template>
<div class="loader relative">
<span class="absolute -bottom-12 text-primary-500">loading...</span>
</div>
</template>
<style scoped lang="css">
.loader {
width: 48px;
height: 48px;
margin: auto;
position: relative;
}
.loader:before {
content: '';
width: 48px;
height: 5px;
background: var(--color-primary-6);
position: absolute;
top: 60px;
left: 0;
border-radius: 50%;
animation: shadow324 0.5s linear infinite;
}
.loader:after {
content: '';
width: 100%;
height: 100%;
background: var(--color-primary-4);
position: absolute;
top: 0;
left: 0;
border-radius: 4px;
animation: jump7456 0.5s linear infinite;
}
@keyframes jump7456 {
15% {
border-bottom-right-radius: 3px;
}
25% {
transform: translateY(9px) rotate(22.5deg);
}
50% {
transform: translateY(18px) scale(1, 0.9) rotate(45deg);
border-bottom-right-radius: 40px;
}
75% {
transform: translateY(9px) rotate(67.5deg);
}
100% {
transform: translateY(0) rotate(90deg);
}
}
@keyframes shadow324 {
0%,
100% {
transform: scale(1, 1);
}
50% {
transform: scale(1.2, 1);
}
}
</style>
+529
View File
@@ -0,0 +1,529 @@
<template>
<div class="socket">
<div class="gel center-gel">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c1 r1">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c2 r1">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c3 r1">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c4 r1">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c5 r1">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c6 r1">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c7 r2">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c8 r2">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c9 r2">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c10 r2">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c11 r2">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c12 r2">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c13 r2">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c14 r2">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c15 r2">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c16 r2">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c17 r2">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c18 r2">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c19 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c20 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c21 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c22 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c23 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c24 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c25 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c26 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c28 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c29 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c30 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c31 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c32 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c33 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c34 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c35 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c36 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c37 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
</div>
</template>
<style scoped lang="css">
.socket {
width: 200px;
height: 200px;
position: absolute;
left: 50%;
margin-left: -100px;
top: 50%;
margin-top: -100px;
}
.hex-brick {
background: var(--color-primary-7);
width: 30px;
height: 17px;
position: absolute;
top: 5px;
animation-name: fade00;
animation-duration: 2s;
animation-iteration-count: infinite;
-webkit-animation-name: fade00;
-webkit-animation-duration: 2s;
-webkit-animation-iteration-count: infinite;
}
.h2 {
transform: rotate(60deg);
-webkit-transform: rotate(60deg);
}
.h3 {
transform: rotate(-60deg);
-webkit-transform: rotate(-60deg);
}
.gel {
height: 30px;
width: 30px;
transition: all 0.3s;
-webkit-transition: all 0.3s;
position: absolute;
top: 50%;
left: 50%;
}
.center-gel {
margin-left: -15px;
margin-top: -15px;
animation-name: pulse00;
animation-duration: 2s;
animation-iteration-count: infinite;
-webkit-animation-name: pulse00;
-webkit-animation-duration: 2s;
-webkit-animation-iteration-count: infinite;
}
.c1 {
margin-left: -47px;
margin-top: -15px;
}
.c2 {
margin-left: -31px;
margin-top: -43px;
}
.c3 {
margin-left: 1px;
margin-top: -43px;
}
.c4 {
margin-left: 17px;
margin-top: -15px;
}
.c5 {
margin-left: -31px;
margin-top: 13px;
}
.c6 {
margin-left: 1px;
margin-top: 13px;
}
.c7 {
margin-left: -63px;
margin-top: -43px;
}
.c8 {
margin-left: 33px;
margin-top: -43px;
}
.c9 {
margin-left: -15px;
margin-top: 41px;
}
.c10 {
margin-left: -63px;
margin-top: 13px;
}
.c11 {
margin-left: 33px;
margin-top: 13px;
}
.c12 {
margin-left: -15px;
margin-top: -71px;
}
.c13 {
margin-left: -47px;
margin-top: -71px;
}
.c14 {
margin-left: 17px;
margin-top: -71px;
}
.c15 {
margin-left: -47px;
margin-top: 41px;
}
.c16 {
margin-left: 17px;
margin-top: 41px;
}
.c17 {
margin-left: -79px;
margin-top: -15px;
}
.c18 {
margin-left: 49px;
margin-top: -15px;
}
.c19 {
margin-left: -63px;
margin-top: -99px;
}
.c20 {
margin-left: 33px;
margin-top: -99px;
}
.c21 {
margin-left: 1px;
margin-top: -99px;
}
.c22 {
margin-left: -31px;
margin-top: -99px;
}
.c23 {
margin-left: -63px;
margin-top: 69px;
}
.c24 {
margin-left: 33px;
margin-top: 69px;
}
.c25 {
margin-left: 1px;
margin-top: 69px;
}
.c26 {
margin-left: -31px;
margin-top: 69px;
}
.c27 {
margin-left: -79px;
margin-top: -15px;
}
.c28 {
margin-left: -95px;
margin-top: -43px;
}
.c29 {
margin-left: -95px;
margin-top: 13px;
}
.c30 {
margin-left: 49px;
margin-top: 41px;
}
.c31 {
margin-left: -79px;
margin-top: -71px;
}
.c32 {
margin-left: -111px;
margin-top: -15px;
}
.c33 {
margin-left: 65px;
margin-top: -43px;
}
.c34 {
margin-left: 65px;
margin-top: 13px;
}
.c35 {
margin-left: -79px;
margin-top: 41px;
}
.c36 {
margin-left: 49px;
margin-top: -71px;
}
.c37 {
margin-left: 81px;
margin-top: -15px;
}
.r1 {
animation-name: pulse00;
animation-duration: 2s;
animation-iteration-count: infinite;
animation-delay: 0.2s;
-webkit-animation-name: pulse00;
-webkit-animation-duration: 2s;
-webkit-animation-iteration-count: infinite;
-webkit-animation-delay: 0.2s;
}
.r2 {
animation-name: pulse00;
animation-duration: 2s;
animation-iteration-count: infinite;
animation-delay: 0.4s;
-webkit-animation-name: pulse00;
-webkit-animation-duration: 2s;
-webkit-animation-iteration-count: infinite;
-webkit-animation-delay: 0.4s;
}
.r3 {
animation-name: pulse00;
animation-duration: 2s;
animation-iteration-count: infinite;
animation-delay: 0.6s;
-webkit-animation-name: pulse00;
-webkit-animation-duration: 2s;
-webkit-animation-iteration-count: infinite;
-webkit-animation-delay: 0.6s;
}
.r1 > .hex-brick {
animation-name: fade00;
animation-duration: 2s;
animation-iteration-count: infinite;
animation-delay: 0.2s;
-webkit-animation-name: fade00;
-webkit-animation-duration: 2s;
-webkit-animation-iteration-count: infinite;
-webkit-animation-delay: 0.2s;
}
.r2 > .hex-brick {
animation-name: fade00;
animation-duration: 2s;
animation-iteration-count: infinite;
animation-delay: 0.4s;
-webkit-animation-name: fade00;
-webkit-animation-duration: 2s;
-webkit-animation-iteration-count: infinite;
-webkit-animation-delay: 0.4s;
}
.r3 > .hex-brick {
animation-name: fade00;
animation-duration: 2s;
animation-iteration-count: infinite;
animation-delay: 0.6s;
-webkit-animation-name: fade00;
-webkit-animation-duration: 2s;
-webkit-animation-iteration-count: infinite;
-webkit-animation-delay: 0.6s;
}
@keyframes pulse00 {
0% {
-webkit-transform: scale(1);
transform: scale(1);
}
50% {
-webkit-transform: scale(0.01);
transform: scale(0.01);
}
100% {
-webkit-transform: scale(1);
transform: scale(1);
}
}
@keyframes fade00 {
0% {
background: var(--color-primary-5);
}
50% {
background: var(--color-primary-7);
}
100% {
background: var(--color-primary-5);
}
}
</style>
@@ -0,0 +1,252 @@
<template>
<div class="loader">
<div>
<ul>
<li>
<svg fill="currentColor" viewBox="0 0 90 120">
<path
d="M90,0 L90,120 L11,120 C4.92486775,120 0,115.075132 0,109 L0,11 C0,4.92486775 4.92486775,0 11,0 L90,0 Z M71.5,81 L18.5,81 C17.1192881,81 16,82.1192881 16,83.5 C16,84.8254834 17.0315359,85.9100387 18.3356243,85.9946823 L18.5,86 L71.5,86 C72.8807119,86 74,84.8807119 74,83.5 C74,82.1745166 72.9684641,81.0899613 71.6643757,81.0053177 L71.5,81 Z M71.5,57 L18.5,57 C17.1192881,57 16,58.1192881 16,59.5 C16,60.8254834 17.0315359,61.9100387 18.3356243,61.9946823 L18.5,62 L71.5,62 C72.8807119,62 74,60.8807119 74,59.5 C74,58.1192881 72.8807119,57 71.5,57 Z M71.5,33 L18.5,33 C17.1192881,33 16,34.1192881 16,35.5 C16,36.8254834 17.0315359,37.9100387 18.3356243,37.9946823 L18.5,38 L71.5,38 C72.8807119,38 74,36.8807119 74,35.5 C74,34.1192881 72.8807119,33 71.5,33 Z"
></path>
</svg>
</li>
<li>
<svg fill="currentColor" viewBox="0 0 90 120">
<path
d="M90,0 L90,120 L11,120 C4.92486775,120 0,115.075132 0,109 L0,11 C0,4.92486775 4.92486775,0 11,0 L90,0 Z M71.5,81 L18.5,81 C17.1192881,81 16,82.1192881 16,83.5 C16,84.8254834 17.0315359,85.9100387 18.3356243,85.9946823 L18.5,86 L71.5,86 C72.8807119,86 74,84.8807119 74,83.5 C74,82.1745166 72.9684641,81.0899613 71.6643757,81.0053177 L71.5,81 Z M71.5,57 L18.5,57 C17.1192881,57 16,58.1192881 16,59.5 C16,60.8254834 17.0315359,61.9100387 18.3356243,61.9946823 L18.5,62 L71.5,62 C72.8807119,62 74,60.8807119 74,59.5 C74,58.1192881 72.8807119,57 71.5,57 Z M71.5,33 L18.5,33 C17.1192881,33 16,34.1192881 16,35.5 C16,36.8254834 17.0315359,37.9100387 18.3356243,37.9946823 L18.5,38 L71.5,38 C72.8807119,38 74,36.8807119 74,35.5 C74,34.1192881 72.8807119,33 71.5,33 Z"
></path>
</svg>
</li>
<li>
<svg fill="currentColor" viewBox="0 0 90 120">
<path
d="M90,0 L90,120 L11,120 C4.92486775,120 0,115.075132 0,109 L0,11 C0,4.92486775 4.92486775,0 11,0 L90,0 Z M71.5,81 L18.5,81 C17.1192881,81 16,82.1192881 16,83.5 C16,84.8254834 17.0315359,85.9100387 18.3356243,85.9946823 L18.5,86 L71.5,86 C72.8807119,86 74,84.8807119 74,83.5 C74,82.1745166 72.9684641,81.0899613 71.6643757,81.0053177 L71.5,81 Z M71.5,57 L18.5,57 C17.1192881,57 16,58.1192881 16,59.5 C16,60.8254834 17.0315359,61.9100387 18.3356243,61.9946823 L18.5,62 L71.5,62 C72.8807119,62 74,60.8807119 74,59.5 C74,58.1192881 72.8807119,57 71.5,57 Z M71.5,33 L18.5,33 C17.1192881,33 16,34.1192881 16,35.5 C16,36.8254834 17.0315359,37.9100387 18.3356243,37.9946823 L18.5,38 L71.5,38 C72.8807119,38 74,36.8807119 74,35.5 C74,34.1192881 72.8807119,33 71.5,33 Z"
></path>
</svg>
</li>
<li>
<svg fill="currentColor" viewBox="0 0 90 120">
<path
d="M90,0 L90,120 L11,120 C4.92486775,120 0,115.075132 0,109 L0,11 C0,4.92486775 4.92486775,0 11,0 L90,0 Z M71.5,81 L18.5,81 C17.1192881,81 16,82.1192881 16,83.5 C16,84.8254834 17.0315359,85.9100387 18.3356243,85.9946823 L18.5,86 L71.5,86 C72.8807119,86 74,84.8807119 74,83.5 C74,82.1745166 72.9684641,81.0899613 71.6643757,81.0053177 L71.5,81 Z M71.5,57 L18.5,57 C17.1192881,57 16,58.1192881 16,59.5 C16,60.8254834 17.0315359,61.9100387 18.3356243,61.9946823 L18.5,62 L71.5,62 C72.8807119,62 74,60.8807119 74,59.5 C74,58.1192881 72.8807119,57 71.5,57 Z M71.5,33 L18.5,33 C17.1192881,33 16,34.1192881 16,35.5 C16,36.8254834 17.0315359,37.9100387 18.3356243,37.9946823 L18.5,38 L71.5,38 C72.8807119,38 74,36.8807119 74,35.5 C74,34.1192881 72.8807119,33 71.5,33 Z"
></path>
</svg>
</li>
<li>
<svg fill="currentColor" viewBox="0 0 90 120">
<path
d="M90,0 L90,120 L11,120 C4.92486775,120 0,115.075132 0,109 L0,11 C0,4.92486775 4.92486775,0 11,0 L90,0 Z M71.5,81 L18.5,81 C17.1192881,81 16,82.1192881 16,83.5 C16,84.8254834 17.0315359,85.9100387 18.3356243,85.9946823 L18.5,86 L71.5,86 C72.8807119,86 74,84.8807119 74,83.5 C74,82.1745166 72.9684641,81.0899613 71.6643757,81.0053177 L71.5,81 Z M71.5,57 L18.5,57 C17.1192881,57 16,58.1192881 16,59.5 C16,60.8254834 17.0315359,61.9100387 18.3356243,61.9946823 L18.5,62 L71.5,62 C72.8807119,62 74,60.8807119 74,59.5 C74,58.1192881 72.8807119,57 71.5,57 Z M71.5,33 L18.5,33 C17.1192881,33 16,34.1192881 16,35.5 C16,36.8254834 17.0315359,37.9100387 18.3356243,37.9946823 L18.5,38 L71.5,38 C72.8807119,38 74,36.8807119 74,35.5 C74,34.1192881 72.8807119,33 71.5,33 Z"
></path>
</svg>
</li>
<li>
<svg fill="currentColor" viewBox="0 0 90 120">
<path
d="M90,0 L90,120 L11,120 C4.92486775,120 0,115.075132 0,109 L0,11 C0,4.92486775 4.92486775,0 11,0 L90,0 Z M71.5,81 L18.5,81 C17.1192881,81 16,82.1192881 16,83.5 C16,84.8254834 17.0315359,85.9100387 18.3356243,85.9946823 L18.5,86 L71.5,86 C72.8807119,86 74,84.8807119 74,83.5 C74,82.1745166 72.9684641,81.0899613 71.6643757,81.0053177 L71.5,81 Z M71.5,57 L18.5,57 C17.1192881,57 16,58.1192881 16,59.5 C16,60.8254834 17.0315359,61.9100387 18.3356243,61.9946823 L18.5,62 L71.5,62 C72.8807119,62 74,60.8807119 74,59.5 C74,58.1192881 72.8807119,57 71.5,57 Z M71.5,33 L18.5,33 C17.1192881,33 16,34.1192881 16,35.5 C16,36.8254834 17.0315359,37.9100387 18.3356243,37.9946823 L18.5,38 L71.5,38 C72.8807119,38 74,36.8807119 74,35.5 C74,34.1192881 72.8807119,33 71.5,33 Z"
></path>
</svg>
</li>
</ul>
</div>
<span class="text-primary-500 text-xxxl">Loading...</span>
</div>
</template>
<style scoped lang="css">
.loader {
--background: linear-gradient(135deg, var(--color-primary-3), var(--color-primary-6));
--shadow: var(--color-primary-7);
--text: var(--color-primary-6);
--page: rgba(255, 255, 255, 0.36);
--page-fold: rgba(255, 255, 255, 0.52);
--duration: 4s;
width: 200px;
height: 140px;
position: relative;
transform: scale(0.65);
}
.loader:before,
.loader:after {
--r: -6deg;
content: '';
position: absolute;
bottom: 8px;
width: 120px;
top: 80%;
box-shadow: 0 16px 12px var(--shadow);
transform: rotate(var(--r));
}
.loader:before {
left: 4px;
}
.loader:after {
--r: 6deg;
right: 4px;
}
.loader div {
width: 100%;
height: 100%;
border-radius: 13px;
position: relative;
z-index: 1;
perspective: 600px;
box-shadow: 0 4px 6px var(--shadow);
background-image: var(--background);
}
.loader div ul {
margin: 0;
padding: 0;
list-style: none;
position: relative;
}
.loader div ul li {
--r: 180deg;
--o: 0;
--c: var(--page);
position: absolute;
top: 10px;
left: 10px;
transform-origin: 100% 50%;
color: var(--c);
opacity: var(--o);
transform: rotateY(var(--r));
-webkit-animation: var(--duration) ease infinite;
animation: var(--duration) ease infinite;
}
.loader div ul li:nth-child(2) {
--c: var(--page-fold);
-webkit-animation-name: page-2;
animation-name: page-2;
}
.loader div ul li:nth-child(3) {
--c: var(--page-fold);
-webkit-animation-name: page-3;
animation-name: page-3;
}
.loader div ul li:nth-child(4) {
--c: var(--page-fold);
-webkit-animation-name: page-4;
animation-name: page-4;
}
.loader div ul li:nth-child(5) {
--c: var(--page-fold);
-webkit-animation-name: page-5;
animation-name: page-5;
}
.loader div ul li svg {
width: 90px;
height: 120px;
display: block;
}
.loader div ul li:first-child {
--r: 0deg;
--o: 1;
}
.loader div ul li:last-child {
--o: 1;
}
.loader span {
display: block;
left: 0;
right: 0;
top: 100%;
margin-top: 20px;
text-align: center;
color: var(--text);
}
@keyframes page-2 {
0% {
transform: rotateY(180deg);
opacity: 0;
}
20% {
opacity: 1;
}
35%,
100% {
opacity: 0;
}
50%,
100% {
transform: rotateY(0deg);
}
}
@keyframes page-3 {
15% {
transform: rotateY(180deg);
opacity: 0;
}
35% {
opacity: 1;
}
50%,
100% {
opacity: 0;
}
65%,
100% {
transform: rotateY(0deg);
}
}
@keyframes page-4 {
30% {
transform: rotateY(180deg);
opacity: 0;
}
50% {
opacity: 1;
}
65%,
100% {
opacity: 0;
}
80%,
100% {
transform: rotateY(0deg);
}
}
@keyframes page-5 {
45% {
transform: rotateY(180deg);
opacity: 0;
}
65% {
opacity: 1;
}
80%,
100% {
opacity: 0;
}
95%,
100% {
transform: rotateY(0deg);
}
}
</style>
+54
View File
@@ -0,0 +1,54 @@
<script lang="ts" setup>
import { ref, PropType } from 'vue';
import dayjs from 'dayjs';
export type Notice = {
img: string;
title: string;
content: string;
time: number;
};
export type NoticeGroup = {
title: string;
list: Notice[];
};
defineProps({
dataSource: Array as PropType<NoticeGroup[]>,
});
const active = ref(0);
</script>
<template>
<a-tabs class="w-60" v-model:active="active">
<a-tab-pane :key="i" :tab="group.title" v-for="(group, i) in dataSource">
<div class="list max-h-40 px-2 overflow-y-auto overflow-x-hidden">
<div class="not-[:first-child]:mt-3 flex items-center" v-for="item in group.list">
<img class="w-11 rounded-full" :src="item.img" />
<div class="flex flex-col ml-2">
<div class="text-title text-xs font-semibold">
{{ item.title }}
<span class="text-subtext text-xs ml-1 font-normal">
{{ dayjs(item.time).format('hh:mm') }}
</span>
</div>
<div class="text-subtext">{{ item.content }}</div>
</div>
</div>
</div>
</a-tab-pane>
</a-tabs>
</template>
<style lang="less" scoped>
:deep(.ant-tabs) {
&-tab {
@apply flex-1 justify-center;
}
&-nav {
&-list {
@apply w-full;
}
}
}
</style>
@@ -0,0 +1,289 @@
<script lang="ts" setup>
import { ref, computed, Ref } from 'vue';
import type { Component, PropType } from 'vue';
import GridList from '../list/GridList.vue';
import { PaginationProps } from 'ant-design-vue';
import { debounce } from 'lodash';
import useModelValue from '@/utils/useModelValue';
export interface IconSelectOption {
label?: string;
component: string | Component;
value: string | number;
}
export interface IconSelectGroup {
title: string;
key: string | number;
list: IconSelectOption[];
}
type SelectMode = 'multiple' | 'single';
const props = defineProps({
mode: {
type: String as PropType<SelectMode>,
default: 'multiple',
},
value: {
type: [Array<string | number>, String, Number],
default(rawProps: any) {
if (rawProps.mode === 'multiple' || !rawProps.mode) {
return undefined;
} else {
return undefined;
}
},
},
column: {
type: Number,
default: 8,
},
placeholder: {
type: String,
default: '选择图标,输入文字搜索...',
},
options: {
type: [Array<IconSelectOption>, Array<IconSelectGroup>],
default: [],
},
});
const emit = defineEmits<{
(e: 'update:value', args: (string | number)[] | undefined | string | number): void;
}>();
// 分页
const pageBase: PaginationProps = {
pageSize: props.column * 5,
hideOnSinglePage: true,
showSizeChanger: false,
size: 'small',
};
const visible = ref(false);
const isMultiple = computed(() => props.mode === 'multiple');
/**
* 格式化分组
*/
const groupList = computed<(IconSelectGroup & { _searchList: IconSelectOption[]; _current: Ref<number> })[]>(() => {
if (props.options.length === 0 || (props.options as IconSelectGroup[])[0].title !== undefined) {
return (props.options as IconSelectGroup[]).map((group) => ({
...group,
_current: ref(1),
_searchList: group.list,
}));
}
return [
{
title: '全部图标',
key: '__dft',
list: props.options as IconSelectOption[],
_current: ref(1),
_searchList: [...props.options] as IconSelectOption[],
},
];
});
/**
* icon 字典 (方便搜索和查找)
*/
const iconMap = computed(() => {
const map = new Map<string | number, IconSelectOption>();
groupList.value
.flatMap((group) => group.list)
.forEach((item) => {
map.set(item.value, item);
});
return map;
});
const { value: select } = useModelValue(
() =>
Array.isArray(props.value) || props.value === undefined ? (props.value as Array<string | number>) : [props.value],
(val) => emit('update:value', isMultiple.value ? val : val?.[0])
);
/**
* 选中图标
* @param icon
*/
function onSelect(icon: IconSelectOption) {
const index = select.value?.indexOf(icon.value) ?? -1;
if (index === -1) {
select.value = isMultiple.value ? [...(select.value ?? []), icon.value] : [icon.value];
} else if (isMultiple.value) {
remove(icon.value);
}
if (!isMultiple.value) {
visible.value = false;
}
searchValue.value = '';
searchIcon('');
}
/**
* 移除选中
* @param icon
*/
function remove(iconKey: string | number) {
const index = select.value?.findIndex((icon) => icon === iconKey) ?? -1;
if (index >= 0) {
select.value = select.value?.filter((icon) => icon !== iconKey);
}
}
/**
* 搜索图标
* @param keyword
*/
function searchIcon(keyword: string) {
const empty = keyword === '';
const group = groupList.value.find((item) => item.key === active.value)!;
const reg = new RegExp(keyword.toLowerCase());
const filterIcon = (reg: RegExp, list: IconSelectOption[]) => {
return list.filter((icon) => reg.test(icon.label!.toLocaleLowerCase()));
};
group._searchList = empty ? [...group.list] : filterIcon(reg, group.list);
setTimeout(() => {
groupList.value
.filter((item) => item !== group)
.forEach((g) => {
g._searchList = empty ? [...g.list] : filterIcon(reg, g.list);
});
});
loading.value = false;
}
/**
* 搜索防抖
*/
const _search = debounce(searchIcon, 300);
/**
* 搜索监听
* @param value
*/
function onSearch(value: string) {
searchValue.value = value;
loading.value = true;
_search(value);
}
// 当前激活分组
const active = ref(groupList.value[0]?.key);
// 搜索关键字
const searchValue = ref('');
const loading = ref(false);
function selected(icon: IconSelectOption) {
return select.value?.includes(icon.value);
}
</script>
<template>
<a-select
@click="() => (visible = true)"
:showSearch="true"
mode="multiple"
v-model:value="select"
:open="visible"
@blur="() => (visible = false)"
@search="onSearch"
:searchValue="searchValue"
v-bind="{ placeholder }"
allow-clear
>
<template #dropdownRender>
<a-spin tip="搜索中..." :spinning="loading">
<a-tabs
v-model:activeKey="active"
:class="[
'icon-selector',
'px-base',
'pb-base',
{ 'no-group pt-[12px]': groupList.length === 1 && groupList[0].key === '__dft' },
]"
@mousedown.prevent
>
<a-tab-pane :key="group.key" v-for="group in groupList">
<template #tab>
<a-badge
:class="{ 'text-primary-500': active === group.key }"
:count="group._searchList.length === group.list.length ? undefined : group._searchList.length"
showZero
>
{{ group.title }}
</a-badge>
</template>
<GridList
class="icon-container"
:dataSource="group._searchList"
:column="column"
:pagination="{
...pageBase,
current: group._current.value,
'onUpdate:current': (val) => (group._current.value = val),
}"
>
<template #renderItem="{ item }">
<div
@click="onSelect(item)"
:class="`icon-item bg-container cursor-pointer h-10 w-10 flex justify-center items-center ${
selected(item) ? 'bg-primary-100' : ''
}`"
>
<component class="icon transition" :is="item.component" />
</div>
</template>
</GridList>
</a-tab-pane>
</a-tabs>
</a-spin>
</template>
<template #tagRender="item">
<div class="mx-0.5 bg-bg-disabled p-1 rounded-sm flex items-center cursor-pointer">
<component :is="iconMap.get(item.value)?.component" />
<CloseOutlined
v-if="isMultiple"
class="text-subtext text-[10px] ml-1 hover:text-text"
@click="remove(item.value)"
/>
</div>
</template>
</a-select>
</template>
<style lang="less" scoped>
.icon-selector {
:deep(.icon-container) {
@apply p-2 text-xl grid gap-2 bg-layout rounded;
.icon-item {
@apply rounded-sm border border-solid border-transparent;
&:hover {
@apply border-primary-500;
.icon {
@apply scale-125;
}
}
}
}
&.no-group {
:deep(.ant-tabs-nav) {
@apply hidden;
}
}
}
</style>
+23
View File
@@ -0,0 +1,23 @@
<script lang="ts" setup>
import { LabelWrapper } from 'stepin';
import { useSettingStore } from '@/store';
const setting = useSettingStore();
</script>
<template>
<div class="setting px-md">
<LabelWrapper justify="between" label="导航模式">
<a-radio-group v-model:value="setting.navigation" button-style="solid">
<a-radio value="side">侧边</a-radio>
<a-radio value="head">顶部</a-radio>
<a-radio style="margin-right: 0" value="mix">混合</a-radio>
</a-radio-group>
</LabelWrapper>
<LabelWrapper justify="between" label="多页签">
<a-switch v-model:checked="setting.useTabs" />
</LabelWrapper>
<LabelWrapper justify="between" label="过滤菜单">
<a-switch v-model:checked="setting.filterMenu" />
</LabelWrapper>
</div>
</template>
<style scoped lang="less"></style>
+2
View File
@@ -0,0 +1,2 @@
import Setting from './Setting.vue';
export default Setting;
@@ -0,0 +1,31 @@
<template>
<div class="mini-statistic-card overflow-hidden relative min-h-[112px] bg-container inline-flex items-center justify-between drop-shadow-sm p-md border-border rounded-lg">
<div class="statistic-main flex-1">
<div class="statistic-title text-subtext text-xs">{{ title }}</div>
<div class="statistic-content flex items-baseline">
<span class="value text-title text-xxl font-bold">{{ value }}</span>
<span class="suffix ml-1 text-xs text-green-500 font-bold">+30%</span>
</div>
</div>
<div class="statistic-icon absolute bottom-0 right-0">
<slot name="icon"></slot>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
export default defineComponent({
props: {
title: String,
value: [String, Number] as PropType<string | number>,
},
name: 'MiniStatisticCard',
});
</script>
<style lang="less" scoped>
.mini-statistic-card {
}
</style>
@@ -0,0 +1,41 @@
<template>
<div class="overview-title">
{{ title }}
<div class="subtitle">
{{ subtitle }}
<span :class="{ change: true, up, down }">{{ change }}</span>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
export default defineComponent({
name: 'OverviewTitle',
props: {
title: String,
subtitle: String,
change: [String, Number] as PropType<string | number>,
up: { type: Boolean, required: false },
down: { type: Boolean, required: false },
},
setup(props, { attrs, slots, emit }) {},
});
</script>
<style scoped lang="less">
.overview-title {
@apply text-title font-bold text-base;
.subtitle {
@apply text-subtext text-xs;
.change {
@apply text-primary-500 text-sm ml-xs;
&.up {
@apply text-success-500;
}
&.down {
@apply text-error-500;
}
}
}
}
</style>
@@ -0,0 +1,85 @@
<script lang="ts" setup>
import { PropType, watch, computed } from 'vue';
import useModelValue from '@/utils/useModelValue';
import cloneDeep from 'lodash/cloneDeep';
import { storeToRefs } from 'pinia';
import { useThemeStore, ThemeProvider } from 'stepin/es/theme-provider';
export type Type = 'day' | 'night';
const props = defineProps({
value: { type: String as PropType<Type> },
nightColor: { type: String, default: '#1D1D1D' },
});
const emit = defineEmits<{
(e: 'update:value', value: Type): void;
}>();
const { value: _value } = useModelValue(
() => props.value,
(val) => emit('update:value', val),
'day'
);
const switcher: { [key in Type]: Type } = {
day: 'night',
night: 'day',
};
const { theme } = storeToRefs(useThemeStore());
// 监听主题色变换,更新缓存
let cachedMiddleColors = cloneDeep(theme.value.color.middle);
watch(
theme,
(val) => {
if (val.color.middle['bg-base'] !== props.nightColor) {
cachedMiddleColors = cloneDeep(val.color.middle);
_value.value = 'day';
} else {
_value.value = 'night';
}
},
{ deep: true }
);
// 主题颜色配置
const colorCfg = computed(() => {
if (_value.value === 'day') {
return { middle: cachedMiddleColors };
}
return { middle: { 'bg-base': props.nightColor } };
});
</script>
<template>
<ThemeProvider is-root :color="colorCfg">
<div
@click="() => (_value = switcher[_value])"
class="bg-fill-2 day-night-switch hover:border-border relative border-border-2 text-lg rounded-full border border-solid flex items-center"
>
<div :class="`spot transition-[left] duration-300 h-full absolute rounded-full bg-container ${_value}`"></div>
<IconFont :class="`day-night-switch-item ${_value === 'day' ? 'checked' : ''}`" name="icon-sun" />
<IconFont :class="`day-night-switch-item ${_value === 'night' ? 'checked' : ''}`" name="icon-moono" />
</div>
</ThemeProvider>
</template>
<style scoped lang="less">
.day-night-switch {
.spot {
width: calc(50% - 1px);
z-index: 1;
left: 0;
&.night {
left: calc(50% + 1px);
@apply bg-layout;
}
}
&-item {
@apply z-20 bg-transparent p-xxs rounded-full text-disabled ~"last:ml-[2px]";
&.checked {
@apply text-text;
}
}
}
</style>
+28
View File
@@ -0,0 +1,28 @@
import { AlertApi } from 'stepin/es/alert-message';
import { MessageApi } from 'ant-design-vue/es/message';
import IconFont from '@/plugins/iconfont/IconFont.vue';
declare module 'vue' {
export interface ComponentCustomProperties {
$message: MessageApi;
$alert: AlertApi;
}
}
declare module 'vue-router' {
interface RouteMeta {
cacheable?: boolean;
closeable?: boolean;
icon?: DefineComponent | FunctionalComponent | string;
badge?: string | number | boolean;
href?: string;
target?: '_blank' | '_self';
permission?: string;
title?: string;
renderMenu?: boolean;
_cache?: RouteMeta;
view?: string;
_is404Page?: boolean;
}
}
export {};
+26
View File
@@ -0,0 +1,26 @@
import { createApp } from 'vue';
import App from './App.vue';
import router from '@/router';
import stepin from 'stepin/es';
import pinia from '@/store';
// import '@/mock';
// 生产打包时可去除 ant-design-vue/dist/antd.variable.less 的引用。
// 开发引入此包是为了加载优化,防止首次打开页面过慢
import 'ant-design-vue/dist/antd.variable.less';
import 'stepin/es/style';
// import 'default-passive-events';
import '@/theme/index.less';
import { AuthPlugin, IconfontPlugin } from '@/plugins';
const app = createApp(App);
app.use(pinia);
app.use(router);
app.use(stepin, { router });
app.use(AuthPlugin, { action: 'disable' });
// iconfont 插件。url为你的 iconfont 图标资源地址(你的iconfont 仓库可获取此地址)
app.use(IconfontPlugin, { url: '//at.alicdn.com/t/c/font_3805284_ulvha6ct7d.js' });
app.config.errorHandler = function (err) {
console.error('未捕获的异常,', err);
};
app.mount('#stepin-app');
+9
View File
@@ -0,0 +1,9 @@
<template>
<ThemeProvider :color="{ middle: { 'bg-base': '#f9e9f9' } }">
<div ref="demo" class="demo p-8 mb-4">demo</div>
</ThemeProvider>
</template>
<script lang="ts" setup>
import { ThemeProvider } from 'stepin';
</script>
+17
View File
@@ -0,0 +1,17 @@
<script lang="ts" setup>
import { useRoute } from 'vue-router';
defineProps({
permission: String,
path: String,
});
console.log(useRoute());
</script>
<template>
<div>
<div>403 Forbidden (no permission)</div>
<div>path: {{ path }}</div>
<div>
need permission: <a-tag color="blue">{{ permission }}</a-tag>
</div>
</div>
</template>
+42
View File
@@ -0,0 +1,42 @@
<template>
<div v-if="!loading">
<div>404 Not Found</div>
<div>path: {{ $route.path }}</div>
</div>
<div v-else class="loading flex items-center justify-center">
<ReadingLoader />
</div>
</template>
<script lang="ts" setup>
import ReadingLoader from '@/components/loaders/ReadingLoader.vue';
import { configPage } from 'stepin/es/tabs-view';
import { useRoute, useRouter } from 'vue-router';
import { useMenuStore, storeToRefs } from '@/store';
import { watch } from 'vue';
const props = defineProps({
loading: Boolean,
});
const route = useRoute();
const { loading: _loading } = storeToRefs(useMenuStore());
const router = useRouter();
if (props.loading) {
if (!_loading.value) {
router.push(route.fullPath);
} else {
watch(_loading, () => {
router.push(route.fullPath);
});
}
configPage(route, { title: 'loading' });
configPage(route, { title: undefined });
}
</script>
<style scoped>
.loading {
min-height: calc(100vh - theme(height.header) - 182px);
}
</style>
+107
View File
@@ -0,0 +1,107 @@
<template>
<div class="test-page">
<div>
<a-button v-auth="`personal:edit`" @click="showGuid = true">新手引导1</a-button>
<a-button class="btn1" ref="btn1" type="primary" @click="target = btn2">button 1</a-button>
<a-button class="btn2" ref="btn2" type="primary" @click="target = btn1">button 2</a-button>
<span v-auth="`hello`" @click="onClick" class="ml-40 p-2">test</span>
</div>
<Guider :current="target" :options="options" v-model:show="showGuid">
<div ref="doc" @click="sayHello">功能指引</div>
</Guider>
</div>
</template>
<script lang="ts" setup>
import { ComponentPublicInstance, onMounted, reactive, ref } from 'vue';
import Guider, { GuiderOption } from '@/components/guider';
import { useAuthStore } from '@/plugins';
const authStore = useAuthStore();
authStore.setAuthorities(['personal:edit', 'personal:remove']);
const sayHi = authStore.useAuth('personal:edit', (name: string) => console.log('hi, ' + name));
const onClick = () => console.log('say hi');
sayHi('jack');
const btn1 = ref<ComponentPublicInstance>();
const btn2 = ref<ComponentPublicInstance>();
const doc = ref<HTMLElement>();
let options: GuiderOption[] = [];
const target = ref<ComponentPublicInstance | HTMLElement>();
const showGuid = ref(false);
function sayHello() {
console.log('hello');
}
onMounted(() => {
target.value = btn1.value;
options.push(
{
target: btn1.value,
doc: doc.value,
},
{
target: btn2.value,
doc: doc.value,
}
);
});
</script>
<style lang="less" scoped>
.test-page {
height: calc(100vh);
padding: 24px;
position: relative;
.ant-btn {
position: absolute;
}
.btn1 {
left: 0px;
top: 92px;
}
.btn2 {
top: 92px;
right: 0;
}
.btn3 {
bottom: 0;
right: 0;
}
.btn4 {
bottom: 0;
left: 0px;
}
.btn5 {
left: calc(50% - 85px);
top: calc(50% - 32px);
}
.btn6 {
left: calc(50%);
top: calc(50% - 32px);
}
.btn7 {
left: calc(50% - 85px);
top: calc(50%);
}
.btn8 {
left: calc(50%);
top: calc(50%);
}
}
</style>
+419
View File
@@ -0,0 +1,419 @@
<script lang="ts" setup>
import { getBase64 } from '@/utils/file';
import { FormInstance } from 'ant-design-vue';
import { reactive, ref, onMounted } from 'vue';
import dayjs from 'dayjs';
import { Dayjs } from 'dayjs';
import { EditFilled } from '@ant-design/icons-vue';
import { useApiStore } from '@/store';
import type { UnwrapRef } from 'vue';
const columns = [
{
title: '用户昵称',
dataIndex: 'userName',
},
{ title: '状态', dataIndex: 'status' },
{ title: '收藏文件存储路径', dataIndex: 'savePath' },
{ title: '喜欢文件存储路径', dataIndex: 'favSavePath' },
{ title: 'Cookie', dataIndex: 'cookies' },
{ title: 'SecUserId', dataIndex: 'secUserId' },
{ title: '操作', dataIndex: 'edit', width: 200 },
// { title: 'id', dataIndex: 'id', width: 200, hiden: false },
];
type DataItem = {
id?: string;
userName?: string;
cookies?: string;
savePath?: string;
favSavePath?: string;
secUserId?: string;
status?: number;
_isNew?: boolean;
};
// const dataSource = reactive<DataItem[]>([
// {
// userName: 'Li Zhi',
// cookies: '131231',
// savePath: 'x',
// status: 1,
// id: '1',
// },
// ]);
const loading = ref(false);
const datas: UnwrapRef<DataItem[]> = reactive([]);
const pagination = ref({
current: 1,
defaultPageSize: 10,
total: 0,
showTotal: () => `${0}`,
});
interface QuaryParam {
pageIndex: number;
pageSize: number;
}
const quaryData: UnwrapRef<QuaryParam> = reactive({
pageIndex: 0,
pageSize: 20,
});
const GetRecords = () => {
loading.value = true;
quaryData.pageIndex = pagination.value.current;
quaryData.pageSize = pagination.value.defaultPageSize;
useApiStore()
.CookiePageList(quaryData)
.then((res) => {
loading.value = false;
if (res.code === 0) {
dataSource.value = res.data.data;
pagination.value.current = res.data.pageIndex;
pagination.value.defaultPageSize = res.data.pageSize;
pagination.value.total = res.data.total;
pagination.value.showTotal = () => `${res.data.total}`;
}
});
};
onMounted(() => {
GetRecords();
});
function addNew() {
showModal.value = true;
form._isNew = true;
}
const showModal = ref(false);
const newAuthor = (author?: DataItem) => {
if (!author) {
author = { _isNew: true };
}
author.userName = undefined;
author.cookies = undefined;
author.savePath = undefined;
author.favSavePath = undefined;
author.secUserId = undefined;
author.status = 0;
author.id = '0';
return author;
};
const copyObject = (target: any, source?: any) => {
if (!source) {
return target;
}
Object.keys(target).forEach((key) => (target[key] = source[key]));
};
const form = reactive<DataItem>(newAuthor());
function reset() {
return newAuthor(form);
}
function cancel() {
showModal.value = false;
reset();
}
const formModel = ref<FormInstance>();
const formLoading = ref(false);
function submit() {
formLoading.value = true;
let self = this;
formModel.value
?.validateFields()
.then((resData: DataItem) => {
if (form._isNew) {
// authors.push({ ...res });
} else {
copyObject(editRecord.value, resData);
}
// console.log('1', authors);
// console.log('2', res);
useApiStore()
.UpdateConfig(resData)
.then((res) => {
loading.value = false;
if (res.code === 0) {
showModal.value = false;
reset();
GetRecords();
}
});
})
.catch((e) => {
console.error(e);
})
.finally(() => {
formLoading.value = false;
});
}
const editRecord = ref<DataItem>();
import { Modal } from 'ant-design-vue'; // 假设使用Ant Design Vue的Modal组件
const deleted = (id: string) => {
// 显示确认对话框
Modal.confirm({
title: '确认删除',
content: '确定要删除这条记录吗?此操作不可撤销。',
okText: '确认',
cancelText: '取消',
onOk: () => {
// 用户确认后执行删除操作
useApiStore()
.deleteCookie(id)
.then((res) => {
loading.value = false;
if (res.code === 0) {
showModal.value = false;
reset();
GetRecords();
}
});
},
onCancel: () => {
// 用户取消删除,不执行任何操作
console.log('已取消删除');
},
});
};
/**
* 编辑
* @param record
*/
function edit(record: DataItem) {
editRecord.value = record;
copyObject(form, record);
showModal.value = true;
}
type Status = 0 | 1;
const StatusDict = {
0: '关闭',
1: '开启',
};
const dataSource = ref(datas);
const showCookiesModal = ref(false);
let showCookiesData = '';
const showCookies = (recode: DataItem) => {
showCookiesModal.value = true;
showCookiesData = recode.cookies;
};
</script>
<template>
<a-modal :title="form._isNew ? '新增Cookie' : '编辑Cookie'" v-model:visible="showModal" @ok="submit" @cancel="cancel" width="1000px">
<a-form ref="formModel" :model="form" :labelCol="{ span: 3 }" :wrapperCol="{ span: 20 }">
<a-form-item label="用户名" required name="userName">
<a-input v-model:value="form.userName" />
</a-form-item>
<a-form-item label="id" required name="id" v-show="false">
<a-input v-model:value="form.id" />
</a-form-item>
<a-form-item required label="收藏存储路径" name="savePath">
<a-input v-model:value="form.savePath" />
</a-form-item>
<a-form-item required label="Cookie" name="cookies">
<a-textarea v-model:value="form.cookies" rows='13' />
</a-form-item>
<a-form-item label="喜欢存储路径" name="favSavePath">
<a-input v-model:value="form.favSavePath" />
<a-alert message="如果要同步“我喜欢的”视频,需要填写!!!" type="warning" />
</a-form-item>
<a-form-item label="SecUserId" name="secUserId">
<a-input v-model:value="form.secUserId" />
<a-alert message="如果要同步“我喜欢的”视频,需要填写!!!" type="warning" />
</a-form-item>
<a-form-item required label="状态" name="status">
<a-select style="width: 90px" v-model:value="form.status" :options="[
{ label: '关闭', value: 0 },
{ label: '开启', value: 1 },
]" />
</a-form-item>
</a-form>
</a-modal>
<!-- 成员表格 -->
<a-table v-bind="$attrs" :columns="columns" :dataSource="dataSource" :pagination="false">
<template #title>
<!-- 关键修改 justify-between 改为 justify-end使子元素靠右侧对齐 -->
<div class="flex justify-end pr-4">
<a-button type="primary" @click="GetRecords()" :loading="formLoading" class="mr-2">
<template #icon>
<SearchOutlined />
</template>
查询
</a-button>
<a-button type="primary" @click="addNew" :loading="formLoading">
<template #icon>
<PlusOutlined />
</template>
新增
</a-button>
</div>
</template>
<template #bodyCell="{ column, text, record }">
<template v-if="column.dataIndex === 'status'">
<a-badge class="text-subtext" :color="'green'">
<template #text>
<span class="text-subtext">{{ StatusDict[text as Status] }}</span>
</template>
</a-badge>
</template>
<template v-else-if="column.dataIndex === 'cookies'">
<!-- 触发按钮 -->
<a-button @click="showCookies(record)">查看</a-button>
</template>
<template v-else-if="column.dataIndex === 'edit'">
<a-button :disabled="showModal" type="link" @click="edit(record)">
<template #icon>
<EditFilled />
</template>
编辑
</a-button>
<a-button type="link" @click="deleted(record.id)" danger>
<template #icon>
<DeleteFilled />
</template>
删除
</a-button>
</template>
<div v-else class="text-subtext">
{{ text }}
</div>
</template>
</a-table>
<!-- Modal弹窗 -->
<a-modal title="Cookie 详情" :visible="showCookiesModal" style="width:1200px;" @cancel="showCookiesModal = false" @ok="showCookiesModal = false">
<!-- 弹窗内容 -->
<div class="cookie-content">
{{ showCookiesData || '无Cookie数据' }}
</div>
</a-modal>
</template>
<style scoped>
.cookie-content {
white-space: pre-wrap;
word-break: break-all;
max-height: 400px;
overflow-y: auto;
padding: 10px;
box-sizing: border-box;
}
/* 透明滚动条样式 - WebKit内核浏览器 */
.cookie-content::-webkit-scrollbar {
width: 6px; /* 更细的滚动条 */
}
/* 轨道完全透明 */
.cookie-content::-webkit-scrollbar-track {
background: transparent;
}
/* 滑块半透明(默认几乎看不见) */
.cookie-content::-webkit-scrollbar-thumb {
background: rgba(150, 150, 150, 0.2); /* 浅灰透明 */
border-radius: 3px;
}
/* hover时稍微显示一点 */
.cookie-content::-webkit-scrollbar-thumb:hover {
background: rgba(150, 150, 150, 0.4); /* 略深一点的透明 */
}
/* 角落也透明 */
.cookie-content::-webkit-scrollbar-corner {
background: transparent;
}
/* Firefox 透明滚动条适配 */
.cookie-content {
scrollbar-width: thin;
scrollbar-color: rgba(150, 150, 150, 0.2) transparent;
}
.cookie-content {
white-space: pre-wrap;
word-break: break-all;
max-height: 400px;
overflow-y: auto;
padding: 10px;
box-sizing: border-box;
}
.cookie-content::-webkit-scrollbar {
width: 6px;
}
.cookie-content::-webkit-scrollbar-track {
background: transparent;
}
.cookie-content::-webkit-scrollbar-thumb {
background: rgba(150, 150, 150, 0.2);
border-radius: 3px;
}
.cookie-content::-webkit-scrollbar-thumb:hover {
background: rgba(150, 150, 150, 0.4);
}
.cookie-content::-webkit-scrollbar-corner {
background: transparent;
}
.cookie-content {
scrollbar-width: thin;
scrollbar-color: rgba(150, 150, 150, 0.2) transparent;
}
/* ---------------------- 新增:a-textarea 透明滚动条 ---------------------- */
/* 1. 穿透 scoped,定位 a-textarea 内部的原生 textarea 元素 */
:deep(.ant-input-textarea-input) {
/* 确保内容超出时显示滚动条(a-textarea 默认已配置,可省略) */
overflow-y: auto;
/* Firefox 透明滚动条:thin 细滚动条 + 滑块颜色/轨道颜色 */
scrollbar-width: thin;
scrollbar-color: rgba(150, 150, 150, 0.2) transparent;
}
/* 2. WebKit 浏览器(Chrome/Safari/Edge)透明滚动条 */
/* 滚动条宽度 */
:deep(.ant-input-textarea-input)::-webkit-scrollbar {
width: 6px; /* 与 cookie-content 保持一致的细滚动条 */
height: 6px; /* 横向滚动条(如需) */
}
/* 滚动条轨道(完全透明) */
:deep(.ant-input-textarea-input)::-webkit-scrollbar-track {
background: transparent;
}
/* 滚动条滑块(半透明,hover 时加深) */
:deep(.ant-input-textarea-input)::-webkit-scrollbar-thumb {
background: rgba(150, 150, 150, 0.2); /* 浅灰透明,默认几乎看不见 */
border-radius: 3px; /* 圆角优化 */
}
:deep(.ant-input-textarea-input)::-webkit-scrollbar-thumb:hover {
background: rgba(150, 150, 150, 0.4); /* hover 时略深,提升交互感知 */
}
/* 滚动条角落(完全透明,避免留白) */
:deep(.ant-input-textarea-input)::-webkit-scrollbar-corner {
background: transparent;
}
</style>
+8
View File
@@ -0,0 +1,8 @@
<script lang="ts" setup>
import CookieTable from './CookieTable.vue';
</script>
<template>
<div class="table w-full">
<CookieTable />
</div>
</template>
+2
View File
@@ -0,0 +1,2 @@
import Table from './Table.vue';
export default Table;
+17
View File
@@ -0,0 +1,17 @@
import { Component, DefineComponent } from 'vue';
type LazyComponent = () => Promise<Component | DefineComponent>;
const modules: Record<string, LazyComponent> = import.meta.glob('./**/*.{ts,vue,tsx}');
type DynamicModule = { [key: string]: LazyComponent };
const Pages = Object.entries(modules).reduce((r, [key, _module]) => {
key = key.replace(/^.\//, '@/pages/');
r[key] = _module;
if (/\/index.(js|ts|tsx|vue)/.test(key)) {
r[key.replace(/\/index.(js|ts|tsx|vue)/, '')] = _module;
}
return r;
}, {} as DynamicModule);
export default Pages;
+129
View File
@@ -0,0 +1,129 @@
<template>
<a-card :bordered="false" :bodyStyle='{"padding-top":"0px","padding-bottom":"100px"}'>
<a-form :model="formState" :label-col="labelCol" :rules="rules" :wrapper-col="wrapperCol" ref="formRef">
<a-divider orientation="left"></a-divider>
<a-form-item has-feedback label="同步周期(分钟)" ref="Cron" name="Cron">
<a-input v-model:value="formState.Cron" placeholder="1:数字-例如20-表示20分钟执行一次;2:cron表达式,根据表达式周期执行" />
</a-form-item>
<a-form-item label="在线Cron表达式">
<a target="_blank" href="https://www.bejson.com/othertools/cron/">查看示例</a>
</a-form-item>
<a-form-item :wrapper-col="{ span: 10, offset: 3 }">
<a-button type="primary" danger @click="onSubmit">进入系统</a-button>
</a-form-item>
</a-form>
</a-card>
</template>
<script lang="ts" setup>
import { reactive, toRaw, ref, watch, createVNode, h } from 'vue';
import type { UnwrapRef } from 'vue';
import { Form } from 'ant-design-vue';
import type { Rule } from 'ant-design-vue/es/form';
import type { FormInstance } from 'ant-design-vue';
import { useApiStore } from '@/store';
import { Modal } from 'ant-design-vue';
import { checkDomain, checksubDomainPrefix, checkPass, checkUserName } from '@/utils/regexHelper';
const formRef = ref<FormInstance>();
const SK = ref(null);
interface FormState {
cloudName: string;
domainName: string;
recordType: string;
ipv6Prefix: string;
domainRecord: string;
AK: string;
SK: string;
DbType: number;
ConnString: string;
Cron: string;
UserName: string;
UswePwd: string;
}
interface CloudInfo {
key: string;
value: string;
doc: string;
}
interface DbTypeInfo {
key: number;
value: string;
conn: string;
}
interface showDBconn {
value: boolean;
}
const showDBconn: UnwrapRef<showDBconn> = reactive({
value: false,
});
const formState: UnwrapRef<FormState> = reactive({
cloudName: 'aliyun',
domainName: '',
recordType: '',
ipv6Prefix: '',
domainRecord: '',
AK: '',
SK: '',
DbType: 2,
ConnString: '',
Cron: '30',
UserName: '',
UswePwd: '',
});
const rules: Record<string, Rule[]> = {
Cron: [{ required: true, message: '请输入任务调度周期', trigger: 'change' }],
};
watch(
() => formState.DbType,
() => {
formRef.value.validateFields(['ConnString']);
},
{ flush: 'post' }
);
//检查数据库连接
import { message } from 'ant-design-vue';
import router from '@/router';
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
const isFirst = ref(false);
//提交初始化
const onSubmit = () => {
// console.log('submit!', toRaw(formState));
formRef.value
.validate()
.then(() => {
console.log('values', formState, toRaw(formState));
useApiStore()
.apiInit(toRaw(formState))
.then((res) => {
if (res.code === 0) {
message.success('初始化成功');
setTimeout(() => {
router.push('/login');
}, 1000);
} else {
message.error(res.erro, 8);
}
});
})
.catch((error) => {
console.log('error', error);
});
};
const labelCol = { style: { width: '150px' } };
const wrapperCol = { span: 4 };
</script>
<style scoped>
.ant-card-body {
padding: 5px !important;
}
</style>
+3
View File
@@ -0,0 +1,3 @@
import Init from './Init.vue';
export default Init;
+56
View File
@@ -0,0 +1,56 @@
<template>
<div class="login flex items-center justify-center">
<login-box class="shadow-lg" @success="onLoginSuccess" @failure="onLoginFail" />
</div>
</template>
<script lang="ts" setup>
import LoginBox from './LoginBox.vue';
import { useRouter } from 'vue-router';
// import http from '@/store/http';
const router = useRouter();
function onLoginSuccess() {
router.push('/dashboard');
}
import { message } from 'ant-design-vue';
function onLoginFail(status, res) {
console.log(res);
if (res.code === 0) {
} else {
message.error(res.erro, 5);
}
}
</script>
<style scoped lang="less">
.login {
height: 100vh;
// 与深色登录框搭配的渐变背景
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
// 可以添加一些装饰性背景元素
&::before {
content: '';
position: absolute;
width: 400px;
height: 400px;
border-radius: 50%;
background: rgba(54, 191, 250, 0.1);
top: 20%;
left: 15%;
filter: blur(80px);
}
&::after {
content: '';
position: absolute;
width: 300px;
height: 300px;
border-radius: 50%;
background: rgba(54, 191, 250, 0.08);
bottom: 10%;
right: 10%;
filter: blur(60px);
}
}
</style>
+62
View File
@@ -0,0 +1,62 @@
<template>
<ThemeProvider :color="{ middle: { 'bg-base': '#1a1a1a' }, primary: { DEFAULT: '#36bffA' } }">
<!-- 加宽后的登录框使用更大的宽度设置 -->
<div class="login-box rounded-lg bg-gray-900 shadow-lg p-8 max-w-2xl mx-auto my-10 border border-gray-800 transition-all duration-300 hover:shadow-xl">
<a-form :model="form" :wrapperCol="{ span: 24 }" @finish="login" class="login-form w-full p-lg text-gray-200">
<div class="third-platform">
<div class="third-title mb-6 text-xl text-center font-semibold text-white">抖音同步工具</div>
</div>
<a-divider class="my-6 bg-gray-700"></a-divider>
<!-- 增加输入框宽度并调整间距 -->
<a-form-item :required="true" name="username" class="mb-5">
<a-input v-model:value="form.username" autocomplete="new-username" placeholder="请输入用户名" class="login-input h-[45px] rounded-md bg-gray-800 border-gray-700 text-white placeholder:text-gray-500 focus:border-primary text-lg" />
</a-form-item>
<a-form-item :required="true" name="password" class="mb-6">
<a-input v-model:value="form.password" autocomplete="new-password" placeholder="请输入密码" class="login-input h-[45px] rounded-md bg-gray-800 border-gray-700 text-white placeholder:text-gray-500 focus:border-primary text-lg" type="password" />
</a-form-item>
<a-button htmlType="submit" class="h-[48px] w-full rounded-md transition-colors hover:opacity-90 bg-primary border-primary text-lg" type="primary" :loading="loading">
登录
</a-button>
</a-form>
</div>
</ThemeProvider>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { useAccountStore } from '@/store';
import { ThemeProvider } from 'stepin';
export interface LoginFormProps {
username: string;
password: string;
}
const loading = ref(false);
const form = reactive({
username: undefined,
password: undefined,
});
const emit = defineEmits<{
(e: 'success', fields: LoginFormProps): void;
(e: 'failure', reason: string, fields: LoginFormProps): void;
}>();
const accountStore = useAccountStore();
function login(params: LoginFormProps) {
loading.value = true;
accountStore
.login(params.username, params.password)
.then((res) => {
emit('success', params);
})
.catch((e) => {
emit('failure', e.message, e.data);
})
.finally(() => (loading.value = false));
}
</script>
+50
View File
@@ -0,0 +1,50 @@
<template>
<a-modal
width="460px"
v-model:visible="_visible"
wrap-class-name="login-modal"
:closable="false"
:footer="null"
:body-style="{ padding: 0 }"
>
<login-box />
</a-modal>
</template>
<script lang="ts" setup>
import LoginBox from './LoginBox.vue';
import useModelValue from '@/utils/useModelValue';
import { useAccountStore } from '@/store';
import { useRoute } from 'vue-router';
import { computed } from 'vue';
const props = defineProps({
visible: { type: Boolean, default: undefined },
unless: Array<String>,
});
const accountStore = useAccountStore();
const route = useRoute();
const emit = defineEmits<{
(e: 'update:visible', visible?: boolean): void;
}>();
const _visible = computed({
get(): boolean {
return !!sVisible.value && !props.unless?.includes(route.fullPath);
},
set(val: boolean) {
sVisible.value = val;
},
});
const { value: sVisible } = useModelValue(
() => props.visible ?? !accountStore.logged,
(val) => emit('update:visible', val)
);
</script>
<style lang="less">
.login-modal .ant-modal-content {
@apply bg-transparent;
}
</style>
+3
View File
@@ -0,0 +1,3 @@
import Login from './Login.vue';
export { default as LoginModal } from './LoginModal.vue';
export default Login;
+79
View File
@@ -0,0 +1,79 @@
<template>
<a-form layout="inline" style="margin-top:5px;margin-bottom:5px;">
<a-form-item>
<a-date-picker v-model:value="dateValue" format="YYYYMMDD" :locale="locale" @change="datePickChange" />
</a-form-item>
<a-form-item>
<a-radio-group v-model:value="typeValue" button-style="solid" @change="typeChange">
<a-radio-button value="debug">debug</a-radio-button>
<a-radio-button value="error">error</a-radio-button>
</a-radio-group>
</a-form-item>
</a-form>
<div class="container">
<a-card title="" :bordered="true">
<pre>{{ logs }}</pre>
</a-card>
</div>
</template>
<script lang="ts" setup>
import { defineComponent, reactive, ref, watch, onMounted } from 'vue';
import { useApiStore } from '@/store';
import type { UnwrapRef } from 'vue';
import dayjs, { Dayjs } from 'dayjs';
import locale from 'ant-design-vue/es/date-picker/locale/zh_CN';
type RangeValue = [Dayjs, Dayjs];
import 'dayjs/locale/zh-cn';
dayjs.locale('zh-cn');
const dateValue = ref<Dayjs>(dayjs(Date()));
const typeValue = ref<string>('debug');
const iframeUrl = ref<string>('');
const dateValue1 = ref<string>();
const logs = ref<string>('');
dateValue1.value = dayjs(Date()).format('YYYYMMDD');
iframeUrl.value = `type=${typeValue.value}&date=${dateValue1.value}`;
const datePickChange = (e, dateStr) => {
dateValue1.value = dateStr;
iframeUrl.value = `type=${typeValue.value}&date=${dateValue1.value}`;
console.log(iframeUrl);
loadLogs();
};
const typeChange = (e) => {
console.log(e.target);
iframeUrl.value = `type=${e.target.value}&date=${dateValue1.value}`;
loadLogs();
};
const mIfrm = ref<any>(null);
onMounted(() => {
// console.log(mIfrm.value);
loadLogs();
});
const loadLogs = () => {
useApiStore()
.apiGetLogs(iframeUrl.value)
.then((log) => {
// console.log(log);
const lines = log.split('\n'); // 将文本按换行符分割为行数组
const reversedLines = lines.reverse(); // 对行数组进行倒序操作
const reversedText = reversedLines.join('\n'); // 将行数组重新连接为一个字符串
logs.value = reversedText;
});
};
</script>
<style lang='less' scoped>
html {
height: 100vh;
}
.container {
width: 100%;
height: 100%;
// max-height: 400px;
iframe {
.word-wrap {
color: white !important;
}
}
}
</style>
+3
View File
@@ -0,0 +1,3 @@
import Logs from './MyLogs.vue';
export default Logs;
+173
View File
@@ -0,0 +1,173 @@
<template>
<a-drawer v-model:visible="visible" class="custom-class" title="邮件配置" placement="right" @after-visible-change="afterVisibleChange" :maskClosable="true">
<a-tabs v-model:activeKey="activeKey">
<a-tab-pane key="1">
<template #tab>
<span>
<!-- <BellOutlined /> -->
邮件STMP配置
</span>
</template>
<a-form ref="emailFromRef" :model="EmailFormData" name="basic" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }" autocomplete="off" @finish="onFinish" @finishFailed="onFinishFailed">
<a-form-item label="是否开启" ref="Open">
<a-checkbox v-model:checked="EmailFormData.Open">解析结果邮件通知</a-checkbox>
</a-form-item>
<a-form-item v-if="EmailFormData.Open" label="Stmp地址" ref="Stmp" name="Stmp" :rules="[{ required: EmailFormData.Open, message: '请输入stmp服务地址!',validator:validateStmp }]">
<a-input v-model:value="EmailFormData.Stmp" />
</a-form-item>
<a-form-item v-if="EmailFormData.Open" label="Stmp端口" ref="Port" name="Port" :rules="[{ required: EmailFormData.Open, message: '请输入stmp服务端口!',validator:validatePort }]">
<a-input v-model:value="EmailFormData.Port" placeholder="默认465yeah-587" />
</a-form-item>
<a-form-item v-if="EmailFormData.Open" label="收件Email" type='email' ref="From" name="From" :rules="[{ required: EmailFormData.Open, message: '请输入收件Email地址!',validator:validateEmail }]">
<a-input v-model:value="EmailFormData.From" />
<span style="color:#888">此处收件人亦是发件人</span>
</a-form-item>
<a-form-item v-if="EmailFormData.Open" label="Stmp授权码" ref="Code" name="Code" :rules="[{ required: EmailFormData.Open, message: '请输入Stmp授权码!' }]">
<a-input v-model:value="EmailFormData.Code" />
<span style="color:#888">邮箱设置开启stmp服务会提示</span>
</a-form-item>
<a-form-item :wrapper-col="{ offset: 8, span: 16 }">
<a-button type="primary" html-type="submit">确认</a-button>
</a-form-item>
</a-form>
</a-tab-pane>
</a-tabs>
</a-drawer>
</template>
<script lang="ts" setup>
import { message } from 'ant-design-vue';
import { defineComponent, ref, onMounted, reactive, watch } from 'vue';
import type { FormInstance } from 'ant-design-vue';
import { useApiStore, useAccountStore } from '@/store';
import http from '@/store/http';
import type { Rule } from 'ant-design-vue/es/form';
import { checkEmail, checkDomain, checkPort } from '@/utils/regexHelper';
const activeKey = ref<string>('1');
const visible = ref<boolean>(false);
function showEmail(vis: boolean) {
visible.value = vis;
if (vis) loadStmpInfo();
}
const emailFromRef = ref<FormInstance>();
const afterVisibleChange = (bool: boolean) => {
if (!bool) {
emailFromRef.value.resetFields();
}
};
const loadStmpInfo = () => {
// useApiStore()
// .apiGetStmpConfig()
// .then((res) => {
// if (res.code === 0) {
// EmailFormData.Open = res.data.open;
// EmailFormData.Stmp = res.data.stmp;
// EmailFormData.Port = res.data.port;
// EmailFormData.Code = res.data.code;
// EmailFormData.From = res.data.from;
// EmailFormData.Id = res.data.id;
// }
// });
};
interface EmailFormState {
Open: boolean;
Stmp: string;
Port: number;
Code: string;
From: string;
Id: string;
To: string;
}
const EmailFormData = reactive<EmailFormState>({
Open: false,
Stmp: 'smtp.qq.com',
Port: 465,
Code: '',
From: '',
Id: '',
To: '',
});
const onFinish = (values: EmailFormState) => {
EmailFormData.To = EmailFormData.From;
// useApiStore()
// .apiSaveStmpConfig(EmailFormData)
// .then((res) => {
// console.log(res);
// if (res.code === 0) {
// message.success('修改配置成功');
// visible.value = false;
// } else {
// message.error(res.erro, 8);
// }
// });
};
//验证邮箱
const validateEmail = async (_rule: Rule, value: string) => {
if (checkEmail(value)) {
return Promise.resolve();
} else {
return Promise.reject('请输入正确的邮箱地址');
}
};
//验证stmp服务
const validateStmp = async (_rule: Rule, value: string) => {
if (checkDomain(value)) {
return Promise.resolve();
} else {
return Promise.reject('请输入正确的stmp-server地址');
}
};
//验证端口
const validatePort = async (_rule: Rule, value: string) => {
if (checkPort(value)) {
return Promise.resolve();
} else {
return Promise.reject('请输入正确的端口');
}
};
const onFinishFailed = (errorInfo: any) => {
console.log('Failed:', errorInfo);
};
watch(
() => EmailFormData.Open,
() => {
emailFromRef.value.validateFields(['Stmp', 'Port', 'From', 'Code']);
},
{ flush: 'post' }
);
// 将updateMessage方法暴露给父组件调用
defineExpose({
showEmail,
});
</script>
<style>
.avatar-uploader > .ant-upload {
width: 100px;
height: 100px;
}
.ant-upload-picture-card-wrapper {
height: 100%;
}
.ant-upload-select-picture-card i {
font-size: 32px;
color: #999;
margin-top: 50px !important;
}
.ant-upload-select-picture-card {
margin-top: 50px !important;
}
.ant-upload-select-picture-card .ant-upload-text {
margin-top: 8px;
color: #666;
}
.ant-tabs-content {
text-align: center;
}
</style>
+206
View File
@@ -0,0 +1,206 @@
<template>
<a-drawer v-model:visible="visible" class="custom-class" title="个人设置" placement="right" @after-visible-change="afterVisibleChange" :maskClosable="true">
<a-tabs v-model:activeKey="activeKey">
<!-- <a-tab-pane key="1" style="height:100%">
<template #tab>
<span>
<user-outlined />
修改头像
</span>
</template>
<a-upload style="" v-model:file-list="fileList" name="file" list-type="picture-card" class="avatar-uploader" :show-upload-list="false" :action="uploadAction" :before-upload="beforeUpload" @change="handleChange" accept=".jpg, .jpeg, .png">
<img v-if="imageUrl" :src="imageUrl" alt="avatar" style="height: 112px; width: 112px; border-radius: 50%;" />
<div v-else>
<loading-outlined v-if="loading"></loading-outlined>
<plus-outlined v-else></plus-outlined>
<div class="ant-upload-text">选择图片</div>
</div>
</a-upload>
<div style="margin-top:20px;">
上传成功即修改成功
</div>
</a-tab-pane> -->
<a-tab-pane key="2">
<template #tab>
<span>
<safety-outlined />
修改用户信息
</span>
</template>
<a-form ref="passFromRef" :model="passwordFormData" name="basic" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }" autocomplete="off" @finish="onFinish" @finishFailed="onFinishFailed" validateTrigger="blur">
<a-form-item label="原密码" ref="OldPassword" name="OldPassword" :rules="[{ required: true, message: '请输入原密码!' }]">
<a-input-password v-model:value="passwordFormData.OldPassword" />
</a-form-item>
<a-form-item label="新密码" ref="Password" name="Password" :rules="[{ required: true, message: '请输入正确新密码!密码要求6位以上包含大小写与数字' ,validator:checkPassword}]">
<a-input-password v-model:value="passwordFormData.Password" />
</a-form-item>
<a-form-item label="确认密码" ref="ConfirmPassword" name="ConfirmPassword" :rules="[{ required: true, message: '请输入正确新密码!',validator:checkPassword}]">
<a-input-password v-model:value="passwordFormData.ConfirmPassword" />
</a-form-item>
<a-form-item label="修改账户" ref="UserName" name="UserName" :rules="[{ required: true, message: '请输入新账号!' }]">
<a-input v-model:value="passwordFormData.UserName" />
</a-form-item>
<a-form-item :wrapper-col="{ offset: 8, span: 16 }">
<a-button type="primary" html-type="submit">确认</a-button>
</a-form-item>
</a-form>
</a-tab-pane>
</a-tabs>
</a-drawer>
</template>
<script lang="ts" setup>
import { message } from 'ant-design-vue';
import { defineComponent, ref, onMounted, reactive } from 'vue';
import type { UploadChangeParam, UploadProps, FormInstance } from 'ant-design-vue';
import { useApiStore, useAccountStore } from '@/store';
import http from '@/store/http';
import { checkPass } from '@/utils/regexHelper';
import type { Rule } from 'ant-design-vue/es/form';
const activeKey = ref<string>('2');
const uploadAction = ref<string>('');
const visible = ref<boolean>(false);
function show(vis: boolean) {
visible.value = vis;
if (vis) loadUserInfo();
}
const passFromRef = ref<FormInstance>();
const afterVisibleChange = (bool: boolean) => {
if (!bool) {
if (passFromRef != null && passFromRef.value != null) passFromRef.value.resetFields();
}
};
function getBase64(img: Blob, callback: (base64Url: string) => void) {
const reader = new FileReader();
reader.addEventListener('load', () => callback(reader.result as string));
reader.readAsDataURL(img);
}
const fileList = ref([]);
const loading = ref<boolean>(false);
const imageUrl = ref<string>('');
const handleChange = (info: UploadChangeParam) => {
if (info.file.status === 'uploading') {
loading.value = true;
return;
}
if (info.file.status === 'done') {
// Get this url from response in real world.
getBase64(info.file.originFileObj, (base64Url: string) => {
imageUrl.value = base64Url;
loading.value = false;
});
}
if (info.file.status === 'error') {
loading.value = false;
message.error('upload error');
}
};
const beforeUpload = (file: UploadProps['fileList'][number]) => {
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png' || file.type === 'image/jpg';
if (!isJpgOrPng) {
message.error('仅允许上传jpg|png|jpeg格式');
}
const isLt10M = file.size / 1024 / 1024 < 10;
if (!isLt10M) {
message.error('最大允许上传5M的文件!');
}
return isJpgOrPng && isLt10M;
};
// const userInfo = ref<string>();
const loadUserInfo = () => {
useApiStore()
.apiUserInfo()
.then((res) => {
if (res.code === 0) {
if (res.data.avatar != null && res.data.avatar !== '') imageUrl.value = `/upload/${res.data.avatar}`;
passwordFormData.UserId = res.data.id;
uploadAction.value = `/api/auth/UpdateUserAvatar?Uid=${res.data.id}`;
}
});
};
interface PassFormState {
UserId: string;
OldPassword: string;
Password: string;
ConfirmPassword: string;
UserName: string;
}
const onFinish = (values: PassFormState) => {
if (values.Password !== values.ConfirmPassword) {
message.error('新密码与确认密码不一致');
return;
} else {
useApiStore()
.apiChangePwd(passwordFormData)
.then((res) => {
console.log(res);
if (res.code === 0) {
message.success('修改密码成功,请重新登陆!');
useAccountStore().setLogged(false);
visible.value = false;
http.removeAuthorization();
} else {
message.error(res.erro, 8);
}
});
}
};
const checkPassword = async (_rule: Rule, value: string) => {
if (checkPass(value)) {
return Promise.resolve();
} else {
return Promise.reject('密码要求6位以上包含大小写与数字');
}
};
const onFinishFailed = (errorInfo: any) => {
console.log('Failed:', errorInfo);
};
const passwordFormData = reactive<PassFormState>({
UserId: '',
OldPassword: '',
Password: '',
ConfirmPassword: '',
UserName: '',
});
// 将updateMessage方法暴露给父组件调用
defineExpose({
show,
});
</script>
<style>
.avatar-uploader > .ant-upload {
width: 100px;
height: 100px;
}
.ant-upload-picture-card-wrapper {
height: 100%;
}
.ant-upload-select-picture-card i {
font-size: 32px;
color: #999;
margin-top: 50px !important;
}
.ant-upload-select-picture-card {
margin-top: 50px !important;
}
.ant-upload-select-picture-card .ant-upload-text {
margin-top: 8px;
color: #666;
}
.ant-tabs-content {
text-align: center;
}
</style>
+4
View File
@@ -0,0 +1,4 @@
import MyPersonal from './MyPersonal.vue';
import EmailSet from './EmailSet.vue';
export { MyPersonal, EmailSet };
+114
View File
@@ -0,0 +1,114 @@
<template>
<a-card :bordered="false" :bodyStyle='{}'>
<a-form :model="formState" :label-col="labelCol" :rules="rules" :wrapper-col="wrapperCol" ref="formRef">
<a-form-item has-feedback label="同步周期(分钟)" ref="Cron" name="Cron">
<a-input v-model:value="formState.Cron" placeholder="" />
<a-alert message="1:数字 20-表示20分钟执行一次;" type="success" />
<a-alert message="2:cron表达式,根据表达式周期执行" type="success" />
</a-form-item>
<a-form-item label="在线Cron表达式">
<a target="_blank" href="https://www.bejson.com/othertools/cron/">查看示例</a>
</a-form-item>
<a-form-item has-feedback label="扫描行数" ref="BatchCount" name="BatchCount">
<a-input v-model:value="formState.BatchCount" placeholder="" />
<a-alert message="每次扫描行数,第一次同步完成后,可以适当调高提高效率" type="success" />
</a-form-item>
<a-form-item :wrapper-col="{ span: 10, offset: 3 }">
<a-space>
<a-button type="primary" @click="onUpdate" v-if="componentDisabled">修改配置</a-button>
<a-button type="primary" danger @click="onSubmit" v-if="!componentDisabled">确认</a-button>
<a-button type="default" @click="onCancel" v-if="!componentDisabled">取消</a-button>
</a-space>
</a-form-item>
</a-form>
</a-card>
</template>
<script lang="ts" setup>
import { reactive, toRaw, ref, watch } from 'vue';
import type { UnwrapRef } from 'vue';
import { Form } from 'ant-design-vue';
import type { Rule } from 'ant-design-vue/es/form';
import type { FormInstance } from 'ant-design-vue';
import { useApiStore } from '@/store';
import { message } from 'ant-design-vue';
import { onMounted } from 'vue';
const formRef = ref<FormInstance>();
const componentDisabled = ref(true);
const SK = ref(null);
interface FormState {
Cron: string;
Id: string;
BatchCount: number;
}
const formState: UnwrapRef<FormState> = reactive({
Cron: '30',
Id: '0',
BatchCount: 10,
});
const rules: Record<string, Rule[]> = {
Cron: [{ required: true, message: '请输入任务调度周期', trigger: 'change' }],
};
const getConfig = () => {
useApiStore()
.apiGetConfig()
.then((res) => {
if (res.code === 0) {
formState.Cron = res.data.cron;
formState.Id = res.data.id;
formState.BatchCount = res.data.batchCount;
} else {
message.error(res.erro, 8);
}
});
};
onMounted(() => {
getConfig();
});
//提交初始化
const onSubmit = () => {
// console.log('submit!', toRaw(formState));
formRef.value
.validate()
.then(() => {
console.log('values', formState, toRaw(formState));
useApiStore()
.apiUpdateConfig(toRaw(formState))
.then((res) => {
if (res.code === 0) {
message.success('配置修改生效,同步任务将按照新的规则执行');
componentDisabled.value = true;
} else {
message.error(res.erro, 8);
}
});
})
.catch((error) => {
console.log('error', error);
});
};
//修改配置开启
const onUpdate = () => {
componentDisabled.value = false;
};
const onCancel = () => {
componentDisabled.value = true;
};
const labelCol = { style: { width: '150px' } };
const wrapperCol = { span: 4 };
</script>
<style lang='less' scoped>
.ant-radio-button-wrapper-disabled.ant-radio-button-wrapper-checked {
color: rgb(164 158 158) !important;
background-color: #e6e6e6 !important;
}
</style>
+2
View File
@@ -0,0 +1,2 @@
import Set from './AppSet.vue';
export default Set;
+144
View File
@@ -0,0 +1,144 @@
<template>
<div class="theme">
<a-steps :current="1">
<a-step>
<template #title>Finished</template>
<template #description>
<span>This is a description.</span>
</template>
</a-step>
<a-step
title="In Progress"
sub-title="Left 00:00:08"
description="This is a description."
/>
<a-step title="Waiting" description="This is a description." />
</a-steps>
<a-steps>
<a-step status="finish" title="Login">
<template #icon>
<user-outlined />
</template>
</a-step>
<a-step status="finish" title="Verification">
<template #icon>
<solution-outlined />
</template>
</a-step>
<a-step status="process" title="Pay">
<template #icon>
<loading-outlined />
</template>
</a-step>
<a-step status="wait" title="Done">
<template #icon>
<smile-outlined />
</template>
</a-step>
</a-steps>
<div class="global-search-wrapper" style="width: 300px">
<a-auto-complete
class="global-search"
style="width: 100%"
option-label-prop="title"
>
<a-input-search placeholder="input here" enterButton></a-input-search>
</a-auto-complete>
</div>
<a-cascader :options="options" placeholder="Please select" />
<a-checkbox-group :options="['Apple', 'Pear', 'Orange']" />
<a-space direction="vertical">
<a-date-picker />
<a-month-picker placeholder="Select month" />
<a-range-picker />
<a-week-picker placeholder="Select week" />
<a-range-picker
:show-time="{ format: 'HH:mm' }"
format="YYYY-MM-DD HH:mm"
:placeholder="['Start Time', 'End Time']"
/>
</a-space>
<a-form :model="form">
<a-form-item required label="用户名" name="username">
<a-input v-model:value="form.username" />
</a-form-item>
<a-form-item required label="密码" name="password">
<a-input-password v-model:value="form.password" />
</a-form-item>
<a-button>提交</a-button>
</a-form>
<a-mentions autofocus v-model:value="author">
<a-mentions-option value="afc163">afc163</a-mentions-option>
<a-mentions-option value="zombieJ">zombieJ</a-mentions-option>
<a-mentions-option value="yesmeck">yesmeck</a-mentions-option>
</a-mentions>
<a-radio disabled>Radio</a-radio>
<a-radio-group>
<a-radio-button value="a">Hangzhou</a-radio-button>
<a-radio-button disabled value="b">Shanghai</a-radio-button>
<a-radio-button value="c">Beijing</a-radio-button>
<a-radio-button value="d">Chengdu</a-radio-button>
</a-radio-group>
<a-rate v-model:value="rate" allow-half />
<a-switch checked-children="" un-checked-children="" />
<br />
<a-switch checked-children="1" un-checked-children="0" />
<br />
<a-switch>
<template #checkedChildren><check-outlined /></template>
<template #unCheckedChildren><close-outlined /></template>
</a-switch>
<a-switch loading :checked="true" />
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
name: 'Theme',
data() {
return {
author: undefined,
rate: 1,
form: {
username: '李志',
password: '123456',
},
options: [
{
value: 'zhejiang',
label: 'Zhejiang',
children: [
{
value: 'hangzhou',
label: 'Hangzhou',
children: [
{
value: 'xihu',
label: 'West Lake',
},
],
},
],
},
{
value: 'jiangsu',
label: 'Jiangsu',
children: [
{
value: 'nanjing',
label: 'Nanjing',
children: [
{
value: 'zhonghuamen',
label: 'Zhong Hua Men',
},
],
},
],
},
],
};
},
});
</script>
+2
View File
@@ -0,0 +1,2 @@
import Theme from './Theme.vue';
export default Theme;
+161
View File
@@ -0,0 +1,161 @@
<template>
<a-form layout="inline" style="margin-top:10px;" :model="quaryData">
<a-form-item label="同步日期">
<a-range-picker v-model:value="value1" :ranges="ranges" :locale="locale" @change="datePicked" />
</a-form-item>
<a-form-item label="抖音作者" ref="author" name="author">
<a-input v-model:value="quaryData.author"></a-input>
</a-form-item>
<a-form-item>
<a-radio-group v-model:value="quaryData.viedoType" button-style="solid" @change="onViedoTypeChanged">
<a-radio-button value="*">全部</a-radio-button>
<a-radio-button value="1">我喜欢的</a-radio-button>
<a-radio-button value="2">我收藏的</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item :wrapper-col="{ offset: 8, span: 16 }">
<a-space>
<a-button type="primary" @click="GetRecords">查询</a-button>
<a-button type="danger" @click="StartNow">立即重新同步</a-button>
</a-space>
</a-form-item>
</a-form>
<a-table :columns="columns" :data-source="dataSource" bordered :pagination="pagination" @change="handleTableChange" :loading="loading">
</a-table>
</template>
<script lang="ts" setup>
import { defineComponent, reactive, ref } from 'vue';
import { useApiStore } from '@/store';
import type { UnwrapRef } from 'vue';
import { onMounted } from 'vue';
import dayjs, { Dayjs } from 'dayjs';
import locale from 'ant-design-vue/es/date-picker/locale/zh_CN';
type RangeValue = [Dayjs, Dayjs];
import 'dayjs/locale/zh-cn';
dayjs.locale('zh-cn');
const columns = ref([
{
title: '同步时间',
dataIndex: 'syncTimeStr',
// sorter: true,
align: 'center',
width: 180,
},
{
title: '同步类型',
dataIndex: 'viedoTypeStr',
// sorter: true,
align: 'center',
width: 100,
},
{
title: '视频类型',
dataIndex: 'viedoCate',
// sorter: true,
width: 300,
align: 'center',
},
{
title: '作者',
dataIndex: 'author',
// sorter: true,
align: 'center',
width: 150,
},
{
title: '视频名称',
dataIndex: 'videoTitle',
// sorter: true,
align: 'left',
},
{
title: '用户',
dataIndex: 'dyUser',
// sorter: true,
align: 'center',
width: 200,
},
]);
const loading = ref(false);
interface DataItem {}
const datas: UnwrapRef<DataItem[]> = reactive([]);
interface QuaryParam {
dates?: string[];
pageIndex: number;
pageSize: number;
author: string;
tag: string;
viedoType: string;
}
const value1 = ref<RangeValue>();
const ranges = {
今天: [dayjs(), dayjs()] as RangeValue,
本月: [dayjs(), dayjs().endOf('month')] as RangeValue,
};
const quaryData: UnwrapRef<QuaryParam> = reactive({
datas: [],
pageIndex: 0,
pageSize: 20,
author: '',
tag: '',
viedoType: '*',
});
const GetRecords = () => {
loading.value = true;
quaryData.pageIndex = pagination.value.current;
quaryData.pageSize = pagination.value.defaultPageSize;
useApiStore()
.VideoPageList(quaryData)
.then((res) => {
loading.value = false;
if (res.code === 0) {
dataSource.value = res.data.data;
pagination.value.current = res.data.pageIndex;
pagination.value.defaultPageSize = res.data.pageSize;
pagination.value.total = res.data.total;
pagination.value.showTotal = () => `${res.data.total}`;
}
});
};
onMounted(() => {
GetRecords();
});
const pagination = ref({
current: 1,
defaultPageSize: 10,
total: 0,
showTotal: () => `${0}`,
});
const handleTableChange = (e) => {
console.log(e);
pagination.value.current = e.current;
pagination.value.defaultPageSize = e.defaultPageSize;
pagination.value.total = e.total;
pagination.value.showTotal = () => `${e.total}`;
GetRecords();
};
const StartNow = () => {
useApiStore()
.StartJobNow()
.then((res) => {});
};
const datePicked = (ref, dateArry) => {
quaryData.dates = dateArry;
console.log(dateArry);
};
const dataSource = ref(datas);
const onViedoTypeChanged = (e) => {
// console.log(e.target.value);
quaryData.viedoType = e.target.value;
pagination.value.current = 0;
GetRecords();
};
</script>
<style scoped>
</style>
+20
View File
@@ -0,0 +1,20 @@
<template>
<div class="workplace grid grid-rows-none gap-4 mt-xxs">
<div class="project-list grid grid-cols-24 gap-4">
<records class="col-span-12 xlx:col-span-7 xxlx:col-span-8 drop-shadow-sm" />
</div>
</div>
</template>
<script lang="ts" setup>
import { reactive } from 'vue';
import Records from './RecordTable.vue';
import { useUnbounded } from '@/utils/useTheme';
useUnbounded();
</script>
<style scoped lang="less">
.workplace {
}
</style>
+20
View File
@@ -0,0 +1,20 @@
<template>
<div class="workplace grid grid-rows-none gap-4 mt-xxs">
<div class="project-list grid grid-cols-24 gap-4">
<records class="col-span-12 xlx:col-span-7 xxlx:col-span-8 drop-shadow-sm" />
</div>
</div>
</template>
<script lang="ts" setup>
import { reactive } from 'vue';
import Records from './RecordTable.vue';
import { useUnbounded } from '@/utils/useTheme';
useUnbounded();
</script>
<style scoped lang="less">
.workplace {
}
</style>
+2
View File
@@ -0,0 +1,2 @@
import DmRecords from './Records.vue';
export default DmRecords;
+922
View File
@@ -0,0 +1,922 @@
<template>
<div class="stats-dashboard">
<main class="main-content">
<!-- 上方统计卡片修改为四列布局 -->
<section class="top-stats">
<!-- 视频总数-->
<div class="stat-card video-card">
<div class="stat-info">
<p class="stat-label">视频总数</p>
<h3 class="stat-value">{{ totalVideos }}</h3>
<p class="stat-trend positive"></p>
</div>
<div class="stat-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polygon points="23 7 16 12 23 17 23 7"></polygon>
<rect x="1" y="5" width="15" height="14" rx="2" ry="2"></rect>
</svg>
</div>
</div>
<!-- 我喜欢的数量卡片 -->
<div class="stat-card like-card">
<div class="stat-info">
<p class="stat-label">我喜欢的</p>
<h3 class="stat-value">{{ favoriteCount }}</h3>
<p class="stat-trend positive"></p>
</div>
<div class="stat-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path>
</svg>
</div>
</div>
<!-- 我收藏的数量卡片 -->
<div class="stat-card collect-card">
<div class="stat-info">
<p class="stat-label">我收藏的</p>
<h3 class="stat-value">{{ collectCount }}</h3>
<p class="stat-trend positive"></p>
</div>
<div class="stat-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path>
</svg>
</div>
</div>
<!-- 作者总数卡片 -->
<div class="stat-card author-card">
<div class="stat-info">
<p class="stat-label">作者总数</p>
<h3 class="stat-value">{{ totalAuthors }}</h3>
<p class="stat-trend positive"></p>
</div>
<div class="stat-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
</div>
</div>
<!-- 分类总数卡片 -->
<div class="stat-card cate-card">
<div class="stat-info">
<p class="stat-label">分类总数</p>
<h3 class="stat-value">{{ categoryTotal }}</h3>
<p class="stat-trend positive"></p>
</div>
<div class="stat-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="5" width="20" height="14" rx="2" />
<path d="M17 5v4" />
<path d="M7 5v4" />
</svg>
</div>
</div>
<!-- 视频占用空间卡片 -->
<div class="stat-card size-card">
<div class="stat-info">
<p class="stat-label">占用空间</p>
<h3 class="stat-value">{{ fileSizeTotal }} G</h3>
<p class="stat-trend positive"></p>
</div>
<div class="stat-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<!-- 补充硬盘图标定义原代码缺失避免引用错误 -->
<path d="M22 12H2v8h20v-8z" />
<path d="M6 18h.01" />
<path d="M10 18h.01" />
</svg>
</div>
</div>
</section>
<!-- 下方统计调换Tab顺序视频作者在前视频分类在后 -->
<section class="category-stats">
<!-- Tab切换控制器调换Tab顺序 + 调整下划线位置 -->
<div class="tab-switcher" :class="{'currentTab-type': currentTab === 'type'}">
<!-- 1. 调换Tab项顺序视频作者 -> 视频分类 -->
<div class="tab-item" :class="{ active: currentTab === 'author' }" @click="currentTab = 'author'">
视频作者
</div>
<div class="tab-item" :class="{ active: currentTab === 'type' }" @click="currentTab = 'type'">
视频分类
</div>
<div class="tab-underline"></div>
</div>
<!-- 内容区域调换显示顺序默认显示视频作者 -->
<transition name="content-fade" mode="out-in">
<!-- 2. 调换内容顺序先显示作者统计再显示分类统计 -->
<div v-if="currentTab === 'author'" key="author-content" class="tab-content">
<div class="category-section">
<div class="grid-container authors-grid">
<div class="grid-item" v-for="(author, index) in authors" :key="index">
<div class="category-icon" style="background-color: #722ed1; display: flex; align-items: center; justify-content: center;">
<img :src="author.icon" alt="作者头像" width="60" height="60" style="object-fit: cover; border-radius: 50%;">
</div>
<h3 class="item-name">{{ author.name }}</h3>
<p class="item-count">作品数: {{ author.count }}</p>
</div>
</div>
</div>
</div>
<div v-else key="type-content" class="tab-content">
<div class="category-section">
<div class="grid-container categories-grid">
<div class="grid-item" v-for="(category, index) in categories" :key="index" :style="{ '--category-color': category.color }">
<div class="category-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 12 12" fill="white" stroke="white" stroke-width="2">
<use :xlink:href="`#${category.icon}`"></use>
</svg>
</div>
<h3 class="item-name">{{ category.name }}</h3>
<p class="item-count">作品数: {{ category.count }}</p>
</div>
</div>
</div>
</div>
</transition>
</section>
</main>
<!-- 定义SVG图标 -->
<svg style="display: none;">
<!-- 日常用品类 -->
<symbol id="cup" viewBox="0 0 12 12">
<path d="M18 4h2v16h-2zM4 4h14v2H4zM4 8h10v2H4zM4 12h10v2H4zM4 16h6v2H4zM4 20h6v2H4z" />
</symbol>
<symbol id="spoon" viewBox="0 0 12 12">
<path d="M18 2c-.55 0-1 .45-1 1v5.59L6.12 20.88c-.39.39-1.02.39-1.41 0-.39-.39-.39-1.02 0-1.41L15.58 8H10c-.55 0-1-.45-1-1s.45-1 1-1h8c.55 0 1 .45 1 1s-.45 1-1 1z" />
</symbol>
<symbol id="fork" viewBox="0 0 12 12">
<path d="M8 3v11h4v-5h4v5h4V3M6 3v16h12V3H6z" />
</symbol>
<symbol id="knife" viewBox="0 0 12 12">
<path d="M4 7h16v2H4zM4 11h13v2H4zM4 15h8v2H4zM7 3v2h10V3z" />
</symbol>
<symbol id="plate" viewBox="0 0 12 12">
<circle cx="12" cy="12" r="8" />
<circle cx="12" cy="12" r="6" />
</symbol>
<symbol id="bottle" viewBox="0 0 12 12">
<path d="M10 2h4v5h-4zM8 7v15c0 .55.45 1 1 1h6c.55 0 1-.45 1-1V7H8z" />
</symbol>
<symbol id="toothbrush" viewBox="0 0 12 12">
<path d="M11 3h2v11h-2zM5 3h2v15H5zM17 3h2v11h-2z" />
</symbol>
<symbol id="comb" viewBox="0 0 12 12">
<path d="M4 5h16v2H4zM4 9h13v2H4zM4 13h10v2H4zM4 17h8v2H4z" />
</symbol>
<symbol id="mirror" viewBox="0 0 12 12">
<rect x="3" y="3" width="18" height="18" rx="2" />
<circle cx="12" cy="10" r="3" />
<path d="M12 15c-2.2 0-4 1.8-4 4h8c0-2.2-1.8-4-4-4z" />
</symbol>
<symbol id="soap" viewBox="0 0 12 12">
<rect x="6" y="6" width="12" height="12" rx="3" />
<path d="M9 9h6v6H9z" />
</symbol>
<!-- 电子设备类 -->
<symbol id="mobile" viewBox="0 0 12 12">
<rect x="5" y="2" width="14" height="20" rx="2" ry="2" />
<line x1="12" y1="18" x2="12.01" y2="18" />
</symbol>
<symbol id="tablet" viewBox="0 0 12 12">
<rect x="4" y="2" width="16" height="20" rx="2" ry="2" />
<line x1="12" y1="20" x2="12.01" y2="20" />
</symbol>
<symbol id="camera" viewBox="0 0 12 12">
<circle cx="12" cy="12" r="6" />
<circle cx="12" cy="12" r="3" />
<rect x="18" y="18" width="4" height="4" />
</symbol>
<symbol id="headphones" viewBox="0 0 12 12">
<path d="M3 10v4c0 2.21 1.79 4 4 4h2c2.21 0 4-1.79 4-4v-4H3zm14 4c0 2.21 1.79 4 4 4v-4h-4z" />
<path d="M15 10v4c0 2.21-1.79 4-4 4H7c-2.21 0-4-1.79-4-4v-4h12z" />
</symbol>
<symbol id="speaker" viewBox="0 0 12 12">
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z" />
</symbol>
<symbol id="tv" viewBox="0 0 12 12">
<rect x="2" y="4" width="20" height="15" rx="2" ry="2" />
<path d="M20 20H4L2 22h20z" />
</symbol>
<symbol id="watch" viewBox="0 0 12 12">
<circle cx="12" cy="12" r="8" />
<circle cx="12" cy="12" r="1" />
<line x1="12" y1="8" x2="12" y2="12" />
<line x1="12" y1="12" x2="15" y2="14" />
</symbol>
<symbol id="charger" viewBox="0 0 12 12">
<path d="M10 16v-2h4v2h5v2h-5v4h-2v-4H5v-2h5zm11-9h-6V3c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v4H1v2h22v-2z" />
</symbol>
<symbol id="router" viewBox="0 0 12 12">
<rect x="4" y="4" width="16" height="16" rx="2" />
<circle cx="8" cy="8" r="1" />
<circle cx="16" cy="8" r="1" />
<circle cx="8" cy="16" r="1" />
<circle cx="16" cy="16" r="1" />
<circle cx="12" cy="12" r="1" />
</symbol>
<symbol id="printer" viewBox="0 0 12 12">
<path d="M6 18v2h12v-2H6zM6 14h12v2H6zM19 8H5c-1.1 0-2 .9-2 2v4c0 1.1.9 2 2 2h4v2h-4c-2.21 0-4-1.79-4-4V10c0-2.21 1.79-4 4-4h14c2.21 0 4 1.79 4 4v4c0 2.21-1.79 4-4 4h-4v-2h4c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2z" />
</symbol>
<!-- 工具类 -->
<symbol id="axe" viewBox="0 0 12 12">
<path d="M13 12h7v2h-7zM5 19h14v2H5zM19 3h-4.18C14.4 1.84 13.3 1 12 1c-1.3 0-2.4.84-2.82 2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-7-.25c.41 0 .75.34.75.75s-.34.75-.75.75-.75-.34-.75-.75.34-.75.75-.75zM10 12H3v-2h7v2zm5-6h-2v2H7V6H5v2h7v2h2V6z" />
</symbol>
<symbol id="saw" viewBox="0 0 12 12">
<path d="M3 5h18v2H3zM3 9h16v2H3zM3 13h14v2H3zM3 17h12v2H3z" />
<path d="M21 19H3v2h18z" />
</symbol>
<symbol id="screwdriver" viewBox="0 0 12 12">
<path d="M7 19h10V5H7v14zm2-8h6v2H9v-2zm0 4h6v2H9v-2zm0-8h6v2H9V7z" />
</symbol>
<symbol id="ladder" viewBox="0 0 12 12">
<path d="M4 3v18h2V5h14V3H4z" />
<path d="M8 7v10M12 7v10M16 7v10" />
</symbol>
<symbol id="flashlight" viewBox="0 0 12 12">
<rect x="11" y="3" width="2" height="12" />
<path d="M9 15h6l3 6V9l-3 6z" />
</symbol>
<symbol id="tape-measure" viewBox="0 0 12 12">
<path d="M16 13h-3V3H5v18h6v-3h3v3h6V13zM7 5h5v8H7zm10 14h-5v-8h5z" />
<path d="M10 18h2v2h-2z" />
</symbol>
<symbol id="compass" viewBox="0 0 12 12">
<circle cx="12" cy="12" r="9" />
<circle cx="12" cy="12" r="2" />
<line x1="12" y1="4" x2="12" y2="8" />
<line x1="12" y1="16" x2="12" y2="20" />
<line x1="4" y1="12" x2="8" y2="12" />
<line x1="16" y1="12" x2="20" y2="12" />
</symbol>
<symbol id="binoculars" viewBox="0 0 12 12">
<path d="M11 7h2v2h-2zm0 4h2v6h-2zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z" />
<path d="M14.85 12.65l-3.79-3.79-1.41 1.42 3.79 3.79z" />
</symbol>
<symbol id="magnifying-glass" viewBox="0 0 12 12">
<circle cx="11" cy="11" r="7" />
<line x1="21" y1="21" x2="16.65" y2="16.65" />
</symbol>
<symbol id="multimeter" viewBox="0 0 12 12">
<rect x="5" y="5" width="14" height="14" rx="2" />
<circle cx="12" cy="12" r="3" />
<line x1="8" y1="8" x2="10" y2="10" />
<line x1="14" y1="14" x2="16" y2="16" />
</symbol>
<!-- 交通类 -->
<symbol id="car" viewBox="0 0 12 12">
<path d="M18.92 6.01C18.72 5.42 18.16 5 17.5 5H6.5C5.84 5 5.29 5.42 5.08 6.01L3 12v8c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1h12v1c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-8l-2.08-5.99zM6.5 16c-.83 0-1.5-.67-1.5-1.5S5.67 13 6.5 13s1.5.67 1.5 1.5S7.33 16 6.5 16zm11 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zM5 11l1.5-4.5h11L19 11H5z" />
</symbol>
<symbol id="bicycle" viewBox="0 0 12 12">
<circle cx="5" cy="18" r="3" />
<circle cx="19" cy="18" r="3" />
<path d="M10 18c0-1.1-.9-2-2-2H5.83l.69-4.17h2.67l-.5 3h2.75l-.13 1zm5.17-13c-.77 0-1.41.5-1.63 1.22l-3.76 9.03c-.2.48.07 1.01.56 1.22.49.22 1.07-.08 1.27-.56l3.76-9.03C17.64 5.5 16.99 5 16.17 5z" />
</symbol>
<symbol id="bus" viewBox="0 0 12 12">
<path d="M18 10.5V6c0-1.1-.9-2-2-2H4c-1.1 0-1.99.9-1.99 2v12c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2v-4.5l-2-2zm-2-1.5v-2c1.1 0 2 .9 2 2v2h-2zM4 6h12v2H4V6zm14 14H4v-2h14v2zm0-4H4v-2h14v2zm-6-4h2v4h-2z" />
</symbol>
<symbol id="train" viewBox="0 0 12 12">
<rect x="4" y="3" width="16" height="12" rx="2" />
<rect x="2" y="11" width="2" height="2" />
<rect x="20" y="11" width="2" height="2" />
<circle cx="8" cy="19" r="2" />
<circle cx="16" cy="19" r="2" />
<path d="M12 15v3" />
</symbol>
<symbol id="boat" viewBox="0 0 12 12">
<path d="M21 14c0 4.97-4.03 9-9 9s-9-4.03-9-9c0-.62.08-1.21.21-1.79L8 16h8l7.79-3.79C20.92 12.79 21 13.38 21 14zM1 13c0 3.87 3.13 7 7 7s7-3.13 7-7H1z" />
</symbol>
<symbol id="airplane" viewBox="0 0 12 12">
<path d="M20.5 19h-17c-.83 0-1.5-.67-1.5-1.5v-11c0-.83.67-1.5 1.5-1.5h17c.83 0 1.5.67 1.5 1.5v11c0 .83-.67 1.5-1.5 1.5zm-16-10c-.28 0-.5-.22-.5-.5s.22-.5.5-.5h16c.28 0 .5.22.5.5s-.22.5-.5.5h-16zm0 3c-.28 0-.5-.22-.5-.5s.22-.5.5-.5h16c.28 0 .5.22.5.5s-.22.5-.5.5h-16zm0 3c-.28 0-.5-.22-.5-.5s.22-.5.5-.5h10c.28 0 .5.22.5.5s-.22.5-.5.5h-10z" />
<path d="M4.5 16.5l6-3.5 6 3.5" />
</symbol>
<symbol id="helicopter" viewBox="0 0 12 12">
<path d="M18 10c0-1.1-.9-2-2-2h-6c-1.1 0-2 .9-2 2v2H4c-1.1 0-2 .9-2 2v4c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2v-4c0-1.1-.9-2-2-2h-4v-2zM7 18H4v-2h3v2zm13 0h-3v-2h3v2zm0-4H4v-2h16v2z" />
<path d="M10 6h4v6h-4z" />
</symbol>
<symbol id="rocket" viewBox="0 0 12 12">
<path d="M12 2L4 7l8 5 8-5-8-5zM4 15l8 5 8-5M4 11l8 5 8-5" />
</symbol>
<symbol id="ship" viewBox="0 0 12 12">
<path d="M21 14c0 4.97-4.03 9-9 9s-9-4.03-9-9c0-.62.08-1.21.21-1.79L8 16h8l7.79-3.79C20.92 12.79 21 13.38 21 14zM1 13c0 3.87 3.13 7 7 7s7-3.13 7-7H1z" />
<path d="M12 10V7c0-1.66-1.34-3-3-3S6 5.34 6 7v3H5v2h14v-2h-1V7c0-1.66-1.34-3-3-3s-3 1.34-3 3v3z" />
</symbol>
<symbol id="motorcycle" viewBox="0 0 12 12">
<circle cx="6" cy="17" r="3" />
<circle cx="17" cy="16" r="3" />
<path d="M17 13c-2.76 0-5 2.24-5 5h-2c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7v-2c1.66 0 3-1.34 3-3s-1.34-3-3-3z" />
<path d="M6 14c-1.66 0-3-1.34-3-3s1.34-3 3-3h2l3 4H6z" />
</symbol>
<!-- 食物类 -->
<symbol id="apple" viewBox="0 0 12 12">
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" />
<path d="M12 5c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z" />
</symbol>
<symbol id="banana" viewBox="0 0 12 12">
<path d="M17.5 5c-1.93 0-3.71.78-4.97 2.03l-3.5 3.5C7.78 11.29 7 13.07 7 15c0 1.93 1.57 3.5 3.5 3.5.53 0 1.03-.11 1.5-.31l1.5 1.5c-.63.45-1.37.71-2.16.79v1.5c0 .83.67 1.5 1.5 1.5.92 0 1.73-.47 2.21-1.16l4.43-6.65C20.47 13.23 21 11.42 21 9.5 21 7.01 19.49 5 17.5 5z" />
</symbol>
<symbol id="bread" viewBox="0 0 12 12">
<rect x="4" y="6" width="16" height="12" rx="2" />
<path d="M6 6v-.5c0-.83.67-1.5 1.5-1.5h9c.83 0 1.5.67 1.5 1.5V6" />
<path d="M6 18v2h12v-2" />
</symbol>
<symbol id="cheese" viewBox="0 0 12 12">
<path d="M20 3H9v2h11v14h-4v2h6V3zM4 3H3v18h1v-9h3.5c.8 0 1.5-.7 1.5-1.5v-5c0-.8-.7-1.5-1.5-1.5H4V3z" />
<path d="M6.5 9H4v1h2.5c.3 0 .5.2.5.5v3c0 .3-.2.5-.5.5H4v1h2.5c.8 0 1.5-.7 1.5-1.5v-3c0-.8-.7-1.5-1.5-1.5z" />
</symbol>
<symbol id="coffee" viewBox="0 0 12 12">
<path d="M18 8h1v8h-1zM2 8h16v2H2zM6 14h12v2H6z" />
<path d="M4.25 5h15.5c.69 0 1.25.56 1.25 1.25v.25c0 .69-.56 1.25-1.25 1.25H4.25C3.56 7.5 3 6.94 3 6.25v-.25C3 5.56 3.56 5 4.25 5z" />
</symbol>
<symbol id="pizza" viewBox="0 0 12 12">
<circle cx="12" cy="12" r="8" />
<path d="M12 4v16M4 12h16" />
</symbol>
<symbol id="ice-cream" viewBox="0 0 12 12">
<path d="M17 11c.34 0 .67.03 1 .08V6c0-1.1-.9-2-2-2H8c-1.1 0-2 .9-2 2v5.08c.33-.05.66-.08 1-.08 1.66 0 3 1.34 3 3s-1.34 3-3 3c-1.66 0-3-1.34-3-3H4c0 2.76 2.24 5 5 5s5-2.24 5-5c0-1.66-1.34-3-3-3z" />
<path d="M7 11l5 5 5-5" />
</symbol>
<symbol id="hamburger" viewBox="0 0 12 12">
<rect x="4" y="5" width="16" height="2" />
<rect x="4" y="11" width="16" height="2" />
<rect x="4" y="17" width="16" height="2" />
</symbol>
<symbol id="carrot" viewBox="0 0 12 12">
<path d="M12.01 4.05L10.6 3.24c-.4-.15-.84.16-.97.56L7.6 10.23c-.14.41.16.84.57.97l1.41.47 5.29 1.76c.4.13.85-.15.98-.55l2.2-5.06c.13-.39-.16-.83-.56-.96l-4.76-1.6z" />
<path d="M5.21 12.04l9.05 3.02-2.7 5.88c-.24.52.22 1.11.79 1.11.32 0 .61-.16.79-.41l6.22-13.35c.24-.52-.22-1.11-.79-1.11h-15c-.56 0-1.03.59-.79 1.11l2.71 5.89z" />
</symbol>
<symbol id="cake" viewBox="0 0 12 12">
<path d="M12 3L2 12h3v8h6v-6h2v6h6v-8h3L12 3z" />
<circle cx="12" cy="8" r="2" />
</symbol>
<!-- 运动类 -->
<symbol id="football" viewBox="0 0 12 12">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z" />
<path d="M12 6.5c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 9c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm-4.95-1.6L12 9.05l4.95 6.85-9.9-6.85zm9.9 5.75L12 14.95l-4.95 3.15 9.9-3.15z" />
</symbol>
<symbol id="basketball" viewBox="0 0 12 12">
<circle cx="12" cy="12" r="10" />
<path d="M2 12h20M12 2v20" />
<path d="M5.64 5.64l12.72 12.72M5.64 18.36l12.72-12.72" />
</symbol>
<symbol id="tennis" viewBox="0 0 12 12">
<circle cx="12" cy="12" r="6" />
<circle cx="12" cy="12" r="2" />
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z" />
</symbol>
<symbol id="swim" viewBox="0 0 12 12">
<path d="M20.57 14.86L22 13.43 20.57 12 17 15.57 13.43 12 12 13.43 15.57 17 12 20.57 13.43 22 17 18.43 20.57 22 22 20.57 18.43 17 20.57 14.86zM10 15c0-1.66-1.34-3-3-3-.35 0-.69.07-1 .18V6c0-1.1.9-2 2-2h4c.73 0 1.39.45 1.73 1.03l1.46 3.4L15.5 12l.68 1.53c.01-.05.02-.1.02-.16 0-1.66-1.34-3-3-3zm-1.15-8h-2.9v2.9l2.9-2.9z" />
</symbol>
<symbol id="run" viewBox="0 0 12 12">
<path d="M13.5 5.5c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zM9.8 8.9L7 23h2.1l1.8-8 2.1 2v6h2v-7.5l-2.1-2 .6-3C14.8 12 16.8 13 19 13v-2c-1.9 0-3.5-1-4.3-2.4l-1-1.6c-.4-.6-1-1-1.7-1-.3 0-.5.1-.8.1L6 8.3V13h2V9.6l1.8-.7" />
</symbol>
<symbol id="bike" viewBox="0 0 12 12">
<circle cx="5" cy="18" r="3" />
<circle cx="19" cy="18" r="3" />
<path d="M10 18c0-1.1-.9-2-2-2H5.83l.69-4.17h2.67l-.5 3h2.75l-.13 1zm5.17-13c-.77 0-1.41.5-1.63 1.22l-3.76 9.03c-.2.48.07 1.01.56 1.22.49.22 1.07-.08 1.27-.56l3.76-9.03C17.64 5.5 16.99 5 16.17 5z" />
</symbol>
<symbol id="boxing" viewBox="0 0 12 12">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z" />
<path d="M15 8H9v2h6zm-2 4v6h-2v-6z" />
</symbol>
<symbol id="golf" viewBox="0 0 12 12">
<path d="M12 10.9c-1.7 0-3.24.81-4.16 2.14-.42.61-.28 1.45.26 1.87.55.42 1.39.28 1.81-.26.37-.47.74-.96 1.08-1.46.34-.5.7-1 .99-1.37H13c.73 0 1.41-.21 2-.58.59-.37 1.02-.96 1.02-1.64 0-1.11-.9-2.01-2.01-2.01zM5.1 19.1c-.88-.22-1.66-.61-2.3-.98-.19-.11-.4-.03-.51.16-.11.19-.03.4.16.51.72.41 1.56.67 2.45.75.36.03.68-.25.71-.61.03-.36-.25-.68-.61-.71zm15.58-.98c-.71.38-1.5.77-2.38.99-.36.03-.68-.25-.71-.61.03-.36.25-.68.61-.71.88-.22 1.66-.61 2.3-.98.19-.11.4-.03.51.16.11.19.03.4-.16.51z" />
<circle cx="12" cy="12" r="2" />
</symbol>
<symbol id="yoga" viewBox="0 0 12 12">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1.41 16.09V20h2.67v-1.93c1.71-.36 3.16-1.46 3.27-3.4h1.73c-.1 2.43-1.96 4.42-4.44 4.93V20h2.67v-2h-3.91c-.31-.01-.55-.3-.55-.63v-4.15c0-.31.23-.57.53-.6l3.87-.53c.48-.07.83.39.72.86l-.85 5.27c1.81-.42 3.21-2.08 3.21-4.07 0-2.31-1.91-4.19-4.25-4.19-1.72 0-3.15.85-4.09 2.15l-1.7-1.13c1.2-1.66 3.02-2.68 5.04-2.68 3.41 0 6.16 2.82 6.16 6.33 0 3.52-2.75 6.33-6.16 6.33-1.55 0-2.95-.64-3.9-1.66l-1.4 1.4A8.89 8.89 0 0 0 12 20c4.96 0 9-4.04 9-9s-4.04-9-9-9z" />
</symbol>
<symbol id="weight" viewBox="0 0 12 12">
<circle cx="8" cy="12" r="5" />
<circle cx="16" cy="12" r="5" />
<line x1="13" y1="12" x2="11" y2="12" />
</symbol>
<!-- 天气类 -->
<symbol id="sun" viewBox="0 0 12 12">
<circle cx="12" cy="12" r="4" />
<line x1="12" y1="1" x2="12" y2="3" />
<line x1="12" y1="21" x2="12" y2="23" />
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
<line x1="1" y1="12" x2="3" y2="12" />
<line x1="21" y1="12" x2="23" y2="12" />
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
</symbol>
<symbol id="moon" viewBox="0 0 12 12">
<path d="M12 3c-4.97 0-9 4.03-9 9s4.03 9 9 9c.83 0 1.5-.67 1.5-1.5 0-.39-.15-.74-.39-1.01-.23-.26-.38-.61-.38-.99 0-.83.67-1.5 1.5-1.5H16c2.76 0 5-2.24 5-5 0-4.42-4.03-8-9-8z" />
</symbol>
<symbol id="cloud" viewBox="0 0 12 12">
<path d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM19 18H6c-2.21 0-4-1.79-4-4 0-2.05 1.53-3.76 3.56-3.97l1.07-.11.5-.95C8.08 7.14 9.94 6 12 6c2.62 0 4.88 1.86 5.39 4.43l.3 1.5 1.53.11c1.56.1 2.78 1.41 2.78 2.96 0 1.65-1.35 3-3 3z" />
</symbol>
<symbol id="rain" viewBox="0 0 12 12">
<path d="M16 18v-2h1v2h-1zM8 18v-2h1v2H8zM12 18v-2h1v2h-1z" />
<path d="M19.07 4.93l-1.41-1.41c-.39-.39-1.02-.39-1.41 0L14 5.59l-1.65-1.66c-.39-.39-1.02-.39-1.41 0l-1.41 1.41c-.39.39-.39 1.02 0 1.41L10.59 9 5.41 3.83c-.39-.39-1.02-.39-1.41 0L2.29 5.25c-.39.39-.39 1.02 0 1.41L8 11.59l-1.65 1.65c-.39.39-.39 1.02 0 1.41l1.41 1.41c.39.39 1.02.39 1.41 0L12 14.41l1.65 1.66c.39.39 1.02.39 1.41 0l1.41-1.41c.39-.39.39-1.02 0-1.41L15.41 9l5.17 5.17c.39.39 1.02.39 1.41 0l1.41-1.41c.39-.39.39-1.02 0-1.41L20.41 7l1.65-1.66c.39-.39.39-1.02 0-1.41zM13 13h-2v-2h2v2z" />
</symbol>
<symbol id="snow" viewBox="0 0 12 12">
<path d="M12 18c-3.31 0-6-2.69-6-6s2.69-6 6-6 6 2.69 6 6-2.69 6-6 6zm0-10c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4z" />
<path d="M7 19h2v2H7zM15 19h2v2h-2zM7 5h2v2H7zM15 5h2v2h-2z" />
</symbol>
<symbol id="wind" viewBox="0 0 12 12">
<path d="M20 12H4M12 20l-8-8 8-8M16 16l-4-4 4-4" />
</symbol>
<symbol id="storm" viewBox="0 0 12 12">
<path d="M12 7c2.76 0 5 2.24 5 5h2c0-3.87-3.13-7-7-7s-7 3.13-7 7h2c0-2.76 2.24-5 5-5zM12 17h-2v-2h2v2zm0-4h-2V7h2v6zm8 4h-2v-2h2v2zm0-4h-2V7h2v6z" />
<path d="M19.07 4.93l-1.41-1.41c-.39-.39-1.02-.39-1.41 0L14 5.59l-1.65-1.66c-.39-.39-1.02-.39-1.41 0l-1.41 1.41c-.39.39-.39 1.02 0 1.41L10.59 9 5.41 3.83c-.39-.39-1.02-.39-1.41 0L2.29 5.25c-.39.39-.39 1.02 0 1.41L8 11.59l-1.65 1.65c-.39.39-.39 1.02 0 1.41l1.41 1.41c.39.39 1.02.39 1.41 0L12 14.41l1.65 1.66c.39.39 1.02.39 1.41 0l1.41-1.41c.39-.39.39-1.02 0-1.41L15.41 9l5.17 5.17c.39.39 1.02.39 1.41 0l1.41-1.41c.39-.39.39-1.02 0-1.41L20.41 7l1.65-1.66c.39-.39.39-1.02 0-1.41z" />
</symbol>
<symbol id="fog" viewBox="0 0 12 12">
<path d="M20 18h2v2h-2zM1 18h2v2H1zm13 0h2v2h-2zm6-8h2v2h-2zM1 10h2v2H1zm20-8H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 16H3V4h18v14zM6 15c0-3.87 3.13-7 7-7s7 3.13 7 7H6z" />
</symbol>
<symbol id="thermometer" viewBox="0 0 12 12">
<path d="M15 13V5c0-1.66-1.34-3-3-3S9 3.34 9 5v8c-1.21.91-2 2.37-2 4 0 2.76 2.24 5 5 5s5-2.24 5-5c0-1.63-.79-3.09-2-4zm-3-9c.55 0 1 .45 1 1v6h-2V5c0-.55.45-1 1-1z" />
<path d="M12 19c-1.66 0-3-1.34-3-3 0-2 3-5.4 3-5.4s3 3.4 3 5.4c0 1.66-1.34 3-3 3z" />
</symbol>
<symbol id="umbrella" viewBox="0 0 12 12">
<path d="M9 16c0 1.1.9 2 2 2s2-.9 2-2H9zm3-14C8.14 2 5 5.14 5 9c0 2.38 1.19 4.47 3 5.74V17c0 .55.45 1 1 1h6c.55 0 1-.45 1-1v-2.26c1.81-1.27 3-3.36 3-5.74 0-3.86-3.14-7-7-7z" />
</symbol>
<!-- 教育类 -->
<symbol id="pencil" viewBox="0 0 12 12">
<path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z" />
</symbol>
<symbol id="notebook" viewBox="0 0 12 12">
<path d="M18 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zM6 4h5v8l-2.5-1.5L6 12V4z" />
</symbol>
<symbol id="calculator" viewBox="0 0 12 12">
<path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z" />
</symbol>
<symbol id="ruler" viewBox="0 0 12 12">
<path d="M3 3v18h18V3H3zm16 16H5V5h14v14zM7 7h2v2H7zm0 4h2v2H7zm0 4h2v2H7zm4-8h2v2h-2zm0 4h2v2h-2zm0 4h2v2h-2zm4-8h2v2h-2zm0 4h2v2h-2z" />
</symbol>
<symbol id="glasses" viewBox="0 0 12 12">
<path d="M6.5 20c.83 0 1.5-.67 1.5-1.5v-11c0-.83-.67-1.5-1.5-1.5S5 6.67 5 7.5v11c0 .83.67 1.5 1.5 1.5zm11 0c.83 0 1.5-.67 1.5-1.5v-11c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v11c0 .83.67 1.5 1.5 1.5zM16 16c0-1.11-.9-2-2-2-.29 0-.62.02-.97.05 1.16-.84 1.97-1.97 1.97-3.39V7c0-2.76-2.24-5-5-5S7 4.24 7 7v3.66c0 1.42.81 2.55 1.97 3.39-.35-.03-.68-.05-.97-.05-1.1 0-2 .89-2 2s.9 2 2 2c1.1 0 2-.89 2-2h4c0 1.11.9 2 2 2s2-.89 2-2z" />
</symbol>
<symbol id="graduation-cap" viewBox="0 0 12 12">
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
</symbol>
<symbol id="bookmark" viewBox="0 0 12 12">
<path d="M19 21l-7-3-7 3V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z" />
</symbol>
<symbol id="microscope" viewBox="0 0 12 12">
<path d="M18 6v6c0 1.1-.9 2-2 2h-2v4l-4-4H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2h12c1.1 0 2 .9 2 2zM7 14l4 4 4-4H7z" />
<circle cx="12" cy="8" r="2" />
<circle cx="12" cy="8" r="1" />
</symbol>
<symbol id="telescope" viewBox="0 0 12 12">
<path d="M21 5H3c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zM3 7h2v10H3V7zm18 10h-2V7h2v10zM12 12c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0-3c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1z" />
<path d="M16.5 8.5l2.5 2.5-2.5 2.5M7.5 8.5l-2.5 2.5 2.5 2.5" />
</symbol>
<symbol id="globe" viewBox="0 0 12 12">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z" />
<path d="M12 4c-4.41 0-8 3.59-8 8s3.59 8 8 8v-2c-3.31 0-6-2.69-6-6s2.69-6 6-6z" />
<path d="M12 6v6l4 2.34" />
</symbol>
<!-- 医疗类 -->
<symbol id="heart" viewBox="0 0 12 12">
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" />
</symbol>
<symbol id="stethoscope" viewBox="0 0 12 12">
<circle cx="19" cy="10" r="2" />
<path d="M21 8c0-2.76-2.24-5-5-5s-5 2.24-5 5H5c-1.1 0-2 .9-2 2v6c0 1.1.9 2 2 2h1v3c0 .55.45 1 1 1h6c.55 0 1-.45 1-1v-3h5c1.1 0 2-.9 2-2v-6c0-1.1-.9-2-2-2zM7 15H5v-4h2v4zm4 0H9v-4h2v4zm4 0h-2v-4h2v4zm4 0h-5v-4h5v4z" />
</symbol>
<symbol id="pill" viewBox="0 0 12 12">
<path d="M2.5 17c.83 0 1.5-.67 1.5-1.5v-9c0-.83-.67-1.5-1.5-1.5S1 5.67 1 6.5v9c0 .83.67 1.5 1.5 1.5zm4-13c.83 0 1.5.67 1.5 1.5v13c0 .83-.67 1.5-1.5 1.5S6 18.33 6 17.5v-13C6 4.67 6.67 4 7.5 4zm14 13c.83 0 1.5-.67 1.5-1.5v-9c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v9c0 .83.67 1.5 1.5 1.5zm-4-13c.83 0 1.5.67 1.5 1.5v13c0 .83-.67 1.5-1.5 1.5s-1.5-.67-1.5-1.5v-13c0-.83.67-1.5 1.5-1.5z" />
</symbol>
<symbol id="bandage" viewBox="0 0 12 12">
<rect x="5" y="5" width="14" height="14" rx="2" />
<rect x="9" y="7" width="2" height="10" />
<rect x="13" y="7" width="2" height="10" />
<rect x="7" y="9" width="10" height="2" />
<rect x="7" y="13" width="10" height="2" />
</symbol>
<symbol id="syringe" viewBox="0 0 12 12">
<path d="M20 7h-4V3c0-.55-.45-1-1-1H9c-.55 0-1 .45-1 1v4H4c-.55 0-1 .45-1 1v11c0 .55.45 1 1 1h16c.55 0 1-.45 1-1V8c0-.55-.45-1-1-1zM18 18H6V8h5V5h2v3h5v10z" />
<path d="M8 15h8v2H8z" />
</symbol>
<symbol id="thermometer-medical" viewBox="0 0 12 12">
<path d="M13 5c0-1.66-1.34-3-3-3S7 3.34 7 5v8c-1.21.91-2 2.37-2 4 0 2.76 2.24 5 5 5s5-2.24 5-5c0-1.63-.79-3.09-2-4V5zm-1 16c-1.66 0-3-1.34-3-3 0-1.31-.69-2.5-1.76-3.24l.04-.06 4.95-4.95.06.04c.73 1.05 1.93 1.75 3.22 1.76 1.66 0 3 1.34 3 3s-1.34 3-3 3h-2z" />
</symbol>
<symbol id="first-aid" viewBox="0 0 12 12">
<path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z" />
</symbol>
<symbol id="eyeglasses" viewBox="0 0 12 12">
<path d="M6.5 20c.83 0 1.5-.67 1.5-1.5v-11c0-.83-.67-1.5-1.5-1.5S5 6.67 5 7.5v11c0 .83.67 1.5 1.5 1.5zm11 0c.83 0 1.5-.67 1.5-1.5v-11c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v11c0 .83.67 1.5 1.5 1.5zM16 16c0-1.11-.9-2-2-2-.29 0-.62.02-.97.05 1.16-.84 1.97-1.97 1.97-3.39V7c0-2.76-2.24-5-5-5S7 4.24 7 7v3.66c0 1.42.81 2.55 1.97 3.39-.35-.03-.68-.05-.97-.05-1.1 0-2 .89-2 2s.9 2 2 2c1.1 0 2-.89 2-2h4c0 1.11.9 2 2 2s2-.89 2-2z" />
</symbol>
<symbol id="heartbeat" viewBox="0 0 12 12">
<path d="M17.5 12c0 2.48-2.02 4.5-4.5 4.5S8.5 14.48 8.5 12H10c0 1.38 1.12 2.5 2.5 2.5s2.5-1.12 2.5-2.5h1.5zM12 22c1.52 0 2.73-.47 3.69-1.24.96-.77 1.58-1.89 1.79-3.16.13-.76-.31-1.49-.99-1.62-.67-.13-1.29.39-1.62.99-.25.5-.57.97-1.01 1.38L12 18l-.66-.66c-.44-.41-.76-.88-1.01-1.38-.33-.6-.95-1.12-1.62-.99-.68.13-1.12.86-.99 1.62.21 1.27.83 2.39 1.79 3.16C9.27 21.53 10.48 22 12 22zM12 2C8.14 2 5 5.14 5 9c0 .88.24 1.72.66 2.45.15.25.13.61-.06.81l-1.43 1.79c-.25.31-.68.36-.96.11-.28-.25-.29-.66-.02-.96l1.2-1.54c.4-.5.92-.93 1.5-1.26.58-.33 1.2-.53 1.84-.61V9c0-2.76 2.24-5 5-5s5 2.24 5 5v.28c.64.08 1.26.28 1.84.61.58.33 1.1.76 1.5 1.26l1.2 1.54c.27.3.26.71-.02.96-.28.25-.71.2-1.01-.11l-1.43-1.79c-.19-.2-.21-.56-.06-.81.42-.73.66-1.57.66-2.45 0-3.86-3.14-7-7-7z" />
</symbol>
<symbol id="wheelchair" viewBox="0 0 12 12">
<circle cx="5" cy="19" r="2" />
<circle cx="17" cy="19" r="2" />
<path d="M18 13c0-3.87-3.13-7-7-7S4 9.13 4 13v-2c0-3.31 2.69-6 6-6s6 2.69 6 6v2h2v-2z" />
</symbol>
<!-- 自然类 -->
<symbol id="tree" viewBox="0 0 12 12">
<path d="M17 12h-5v5h5v-5zM16 1v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2h-1V1h-2zm3 18H5V8h14v11z" />
</symbol>
<symbol id="flower" viewBox="0 0 12 12">
<circle cx="12" cy="12" r="3" />
<path d="M12 2c-2.21 0-4 1.79-4 4 0 .89.29 1.71.78 2.38l-2.47 2.47c-1.12.37-1.81 1.47-1.81 2.71 0 1.93 1.57 3.5 3.5 3.5.59 0 1.17-.1 1.7-.31l2.47 2.47c.67.49 1.49.78 2.38.78 2.21 0 4-1.79 4-4 0-.89-.29-1.71-.78-2.38l2.47-2.47c1.12-.37 1.81-1.47 1.81-2.71 0-1.93-1.57-3.5-3.5-3.5-.59 0-1.17.1-1.7.31l-2.47-2.47C13.71 2.29 12.89 2 12 2zm0 1.5c1.38 0 2.5 1.12 2.5 2.5 0 .52-.16.99-.44 1.38l.44.44c.74.23 1.42.57 2.01 1.06l1.06-1.06c.49-.59.83-1.27 1.06-2.01l.44-.44c.39-.28.86-.44 1.38-.44 1.38 0 2.5 1.12 2.5 2.5 0 .73-.2 1.42-.54 2.01l-2.64 2.64c.34.59.54 1.28.54 2.01 0 1.38-1.12 2.5-2.5 2.5-.73 0-1.42-.2-2.01-.54l-2.64 2.64c-.59-.34-1.28-.54-2.01-.54-1.38 0-2.5-1.12-2.5-2.5 0-.73.2-1.42.54-2.01L3.46 11.5c-.34-.59-.54-1.28-.54-2.01 0-1.38 1.12-2.5 2.5-2.5.73 0 1.42.2 2.01.54l2.64-2.64c.59.34 1.28.54 2.01.54z" />
</symbol>
<symbol id="leaf" viewBox="0 0 12 12">
<path d="M17 7h-4v4H7v4h4v4h4v-4h4v-4h-4V7z" />
<path d="M3 11h2v2H3zm0 4h2v2H3zm14-4h2v2h-2zm0 4h2v2h-2z" />
</symbol>
<symbol id="mountain" viewBox="0 0 12 12">
<path d="M13 2v8h8c0-4.42-3.58-8-8-8zm6.32 13.89C20.37 14.54 21 12.84 21 11H6.44l-.95-2H2v2h2.22s1.89 4.07 2.12 4.42c-1.1.59-1.84 1.75-1.84 3.08C4.5 21.43 6.07 23 8 23c1.76 0 3.22-1.3 3.46-3h2.08c.24 1.7 1.7 3 3.46 3 1.93 0 3.5-1.57 3.5-3.5 0-1.04-.46-1.97-1.18-2.61z" />
</symbol>
<symbol id="river" viewBox="0 0 12 12">
<path d="M20 20c-1.39 0-2.76-.57-3.68-1.56C16.56 17.76 17 16.39 17 15c0-3.87-3.13-7-7-7s-7 3.13-7 7c0 1.39.44 2.76 1.44 3.68C5.76 20.43 7.11 21 8.5 21s2.74-.57 3.65-1.56c.91.99 2.28 1.56 3.65 1.56 1.39 0 2.74-.57 3.66-1.56.9.99 2.27 1.56 3.66 1.56 1.38 0 2.73-.57 3.64-1.56-.91-.99-2.27-1.56-3.64-1.56zM4 15c0-2.76 2.24-5 5-5s5 2.24 5 5-2.24 5-5 5-5-2.24-5-5zm16 0c0-2.76-2.24-5-5-5s-5 2.24-5 5 2.24 5 5 5 5-2.24 5-5z" />
</symbol>
<symbol id="star" viewBox="0 0 12 12">
<path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z" />
</symbol>
<symbol id="cactus" viewBox="0 0 12 12">
<rect x="11" y="3" width="2" height="10" />
<rect x="7" y="7" width="2" height="4" />
<rect x="15" y="7" width="2" height="4" />
<rect x="9" y="13" width="6" height="8" rx="2" />
</symbol>
<symbol id="fire" viewBox="0 0 12 12">
<path d="M12.5 4.2c.88.7 1.5 1.76 1.5 2.95 0 2.21-1.79 4-4 4-.34 0-.67-.03-1-.08.41 1.21 1.52 2.16 2.89 2.54.27.08.45.3.45.59v1.66c0 .38-.31.7-.7.7-1.22-.07-2.38-.57-3.28-1.37-.43-.41-.68-.99-.68-1.61 0-1.78 1.46-3.22 3.25-3.22.88 0 1.67.39 2.16 1 .5.61.75 1.39.75 2.21 0 .16-.01.31-.03.46-.39-.25-.81-.4-1.26-.4-.91 0-1.67.7-1.67 1.58 0 .87.76 1.58 1.67 1.58 1.38 0 2.5-.93 2.63-2.23.24-2.6-.99-4.97-3.03-6.36zM5.21 12c.41 1.21 1.52 2.16 2.89 2.54.27.08.45.3.45.59v1.66c0 .38-.31.7-.7.7-1.22-.07-2.38-.57-3.28-1.37-.43-.41-.68-.99-.68-1.61 0-1.78 1.46-3.22 3.25-3.22.88 0 1.67.39 2.16 1 .5.61.75 1.39.75 2.21 0 .16-.01.31-.03.46-.39-.25-.81-.4-1.26-.4-.91 0-1.67.7-1.67 1.58 0 .87.76 1.58 1.67 1.58 1.38 0 2.5-.93 2.63-2.23.24-2.6-.99-4.97-3.03-6.36 0 0-.53-.42-1.27-.42-.89 0-1.35.67-1.35 1.49 0 .43.14.84.41 1.19-.15.03-.31.04-.46.04-2.03 0-3.68-1.65-3.68-3.68 0-.21.02-.41.05-.61.37.41.86.66 1.39.66.59 0 1.11-.29 1.45-.76-.1-.04-.19-.08-.29-.12C1.46 3.93 1 5.81 1 7.81c0 2.63 2.1 4.77 4.73 4.81h.48zM18.79 12c.41 1.21 1.52 2.16 2.89 2.54.27.08.45.3.45.59v1.66c0 .38-.31.7-.7.7-1.22-.07-2.38-.57-3.28-1.37-.43-.41-.68-.99-.68-1.61 0-1.78 1.46-3.22 3.25-3.22.88 0 1.67.39 2.16 1 .5.61.75 1.39.75 2.21 0 .16-.01.31-.03.46-.39-.25-.81-.4-1.26-.4-.91 0-1.67.7-1.67 1.58 0 .87.76 1.58 1.67 1.58 1.38 0 2.5-.93 2.63-2.23.24-2.6-.99-4.97-3.03-6.36 0 0-.53-.42-1.27-.42-.89 0-1.35.67-1.35 1.49 0 .43.14.84.41 1.19-.15.03-.31.04-.46.04-2.03 0-3.68-1.65-3.68-3.68 0-.21.02-.41.05-.61.37.41.86.66 1.39.66.59 0 1.11-.29 1.45-.76-.1-.04-.19-.08-.29-.12-1.09-.53-1.55-1.7-1.55-2.91 0-2.63 2.1-4.77 4.73-4.81h.48z" />
</symbol>
<symbol id="moon-star" viewBox="0 0 12 12">
<path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z" />
<path d="M12 3c-4.97 0-9 4.03-9 9s4.03 9 9 9c.83 0 1.5-.67 1.5-1.5 0-.39-.15-.74-.39-1.01-.23-.26-.38-.61-.38-.99 0-.83.67-1.5 1.5-1.5H16c2.76 0 5-2.24 5-5 0-4.42-4.03-8-9-8z" />
</symbol>
<symbol id="wave" viewBox="0 0 12 12">
<path d="M2 12c0 2.76 2.24 5 5 5h10c2.76 0 5-2.24 5-5s-2.24-5-5-5H7c-2.76 0-5 2.24-5 5zm19-3h-5c-1.66 0-3 1.34-3 3s1.34 3 3 3h5c1.66 0 3-1.34 3-3s-1.34-3-3-3zm0 5h-5c-.55 0-1-.45-1-1s.45-1 1-1h5c.55 0 1 .45 1 1s-.45 1-1 1zM7 10c.55 0 1 .45 1 1s-.45 1-1 1H2c-.55 0-1-.45-1-1s.45-1 1-1h5zm12 2c0-.55-.45-1-1-1H8c-.55 0-1 .45-1 1s.45 1 1 1h10c.55 0 1-.45 1-1z" />
</symbol>
</svg>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import { useApiStore } from '@/store';
// 类型接口
interface Author {
name: string;
count: number;
icon: string;
}
interface Category {
name: string;
count: number;
color: string;
icon: string;
}
// 3. 初始Tab改为"视频作者"currentTab默认值从'type'改为'author'
const totalVideos = ref<number>(0);
const totalAuthors = ref<number>(0);
const categoryTotal = ref<number>(0);
const fileSizeTotal = ref<number>(0);
const favoriteCount = ref<number>(0);
const collectCount = ref<number>(0);
const categories = ref<Category[]>([]);
const authors = ref<Author[]>([]);
const currentTab = ref<string>('author'); // 默认显示"视频作者"Tab
// 组件名称
defineOptions({
name: 'StatsDashboard',
});
// 加载数据
onMounted(() => {
loadDashboardData();
});
const loadDashboardData = async () => {
try {
const res = await useApiStore().VideoStatics();
totalVideos.value = res.data.videoCount;
totalAuthors.value = res.data.authorCount;
categoryTotal.value = res.data.categoryCount;
favoriteCount.value = res.data.favoriteCount;
collectCount.value = res.data.collectCount;
categories.value = res.data.categories;
fileSizeTotal.value = res.data.viedoSizeTotal; // 注意:原代码"video"拼写错误(viedo),建议修正为"videoSizeTotal"
authors.value = res.data.authors;
// 分类图标和颜色随机分配
const cates = getRandomElements(categoriessss.value, categories.value.length);
const colors = getRandomElements(colorArray, categories.value.length);
categories.value.forEach((item, index) => {
item.icon = cates[index];
item.color = colors[index];
});
} catch (err) {
console.error('加载仪表盘数据失败:', err);
}
};
// 随机获取数组元素方法
const getRandomElements = (arr: any[], n: number) => {
if (n <= 0) return [];
if (n >= arr.length) return [...arr];
return [...arr].sort(() => Math.random() - 0.5).slice(0, n);
};
// 图标列表和颜色数组
const categoriessss = ref<any[]>([
'cup',
'spoon',
'fork',
'knife',
'plate',
'bottle',
'toothbrush',
'comb',
'mirror',
'soap',
'mobile',
'tablet',
'camera',
'headphones',
'speaker',
'tv',
'watch',
'charger',
'router',
'printer',
]);
const colorArray = [
'#3A7CA5',
'#D9A566',
'#6B4226',
'#829399',
'#A63446',
'#4F6367',
'#EEF4ED',
'#FE5F55',
'#C7EFCF',
'#78C0E0',
'#49BEAA',
'#49DCB1',
'#FF9999',
'#66B2FF',
'#99FF99',
];
</script>
<style scoped>
.stats-dashboard {
min-height: 100vh;
background-color: #361f68;
padding: 0 20px;
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans',
'Helvetica Neue', sans-serif;
color: #ffffff;
}
.main-content {
margin: 0 auto;
padding: 20px 0;
/* 增加最大宽度,避免四列在大屏下过宽 max-width: 1400px; */
}
/* 1. 顶部统计卡片:修改为四列布局(核心修改) */
.top-stats {
display: grid;
grid-template-columns: 1fr;
gap: 20px;
margin-bottom: 30px;
}
/* 平板及以上屏幕:四列布局(原3列改为4列) */
@media (min-width: 768px) {
.top-stats {
grid-template-columns: repeat(6, 1fr); /* 关键修改:3 -> 4 */
}
}
/* 小屏适配:保持1列,避免拥挤 */
@media (max-width: 767px) {
.stat-card .stat-value {
font-size: 24px; /* 小屏缩小数值字体 */
}
}
/* 统计卡片基础样式(不变) */
.stat-card {
background-color: rgba(255, 255, 255, 0.05);
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
transition: transform 0.2s, box-shadow 0.2s, background-color 0.2s;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
background-color: rgba(255, 255, 255, 0.1);
}
/* 卡片图标样式(不变) */
.video-card .stat-icon {
background-color: rgba(24, 144, 255, 0.15);
color: #1890ff;
}
.author-card .stat-icon {
background-color: rgba(114, 46, 209, 0.15);
color: #722ed1;
}
.cate-card .stat-icon {
background-color: rgba(194, 238, 51, 0.15);
color: hsl(66, 91%, 49%);
}
.size-card .stat-icon {
background-color: rgba(34, 211, 238, 0.15);
color: #22d3ee;
}
.like-card .stat-icon {
background-color: rgba(227, 19, 234, 0.15);
color: #ef09c5;
}
.collect-card .stat-icon {
background-color: rgba(237, 103, 6, 0.15);
color: #b95406;
}
.stat-info .stat-label {
font-size: 14px;
color: rgba(255, 255, 255, 0.8);
margin: 0 0 8px 0;
}
.stat-info .stat-value {
font-size: 28px;
font-weight: 600;
margin: 0 0 8px 0;
}
.stat-icon {
width: 48px;
height: 48px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
/* 下方Tab区域(核心修改:调整下划线位置) */
.category-stats {
display: grid;
grid-template-columns: 1fr;
gap: 30px;
}
.tab-switcher {
position: relative;
display: flex;
width: 240px;
margin: 0 auto 20px;
border-radius: 4px;
overflow: hidden;
}
.tab-item {
flex: 1;
padding: 12px 0;
text-align: center;
font-size: 16px;
font-weight: 500;
color: rgba(255, 255, 255, 0.7);
cursor: pointer;
transition: color 0.3s ease;
}
.tab-item.active {
color: #ffffff; /* 激活态文字变白,增强辨识度 */
}
/* 2. Tab下划线:调整初始位置(对应"视频作者"Tab */
.tab-underline {
position: absolute;
bottom: 0;
height: 3px;
width: 60px; /* 与文字宽度匹配 */
background-color: #722ed1;
border-radius: 3px 3px 0 0;
transition: all 0.3s ease;
left: 0;
transform: translateX(30px); /* 初始位置:对应第一个Tab(视频作者) */
}
/* 切换到"视频分类"Tab时,下划线位置调整 */
.currentTab-type .tab-underline {
transform: translateX(150px); /* 对应第二个Tab(视频分类) */
}
/* 内容切换动画(不变) */
.content-fade-enter-from,
.content-fade-leave-to {
opacity: 0;
transform: translateY(10px);
}
.content-fade-enter-active,
.content-fade-leave-active {
transition: opacity 0.3s ease, transform 0.3s ease;
}
/* 分类/作者列表样式(不变) */
.category-section {
background-color: rgba(255, 255, 255, 0.05);
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.grid-container {
display: grid;
grid-template-columns: repeat(1, 1fr);
gap: 16px;
}
@media (min-width: 576px) {
.grid-container {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 768px) {
.grid-container {
grid-template-columns: repeat(4, 1fr);
}
}
@media (min-width: 1200px) {
.grid-container {
grid-template-columns: repeat(6, 1fr);
}
}
.grid-item {
background-color: rgba(255, 255, 255, 0.02);
border-radius: 6px;
padding: 16px;
text-align: center;
transition: transform 0.2s, box-shadow 0.2s, background-color 0.2s;
cursor: pointer;
}
.grid-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
background-color: rgba(255, 255, 255, 0.05);
}
.category-icon {
width: 60px;
height: 60px;
border-radius: 50%;
margin: 0 auto 12px;
display: flex;
align-items: center;
justify-content: center;
}
.categories-grid .category-icon {
background-color: var(--category-color);
}
.item-name {
font-size: 16px;
font-weight: 500;
margin: 0 0 8px 0;
}
.item-count {
font-size: 14px;
color: rgba(255, 255, 255, 0.8);
margin: 0;
}
</style>
+128
View File
@@ -0,0 +1,128 @@
import { defineStore } from 'pinia';
import { Plugin } from 'vue';
import './auth.css';
import { alert } from 'stepin';
export type AuthKey = string | number;
export interface AuthState {
authorities: AuthKey[];
}
export type AuthActions = {
setAuthorities: (authorities: AuthKey[]) => void;
hasAuthority: (authority: AuthKey) => boolean;
useAuth: <T extends Function>(key: AuthKey, func: T) => T;
};
export const useAuthStore = defineStore<string, AuthState, {}, AuthActions>('auth', {
state() {
return {
authorities: [],
};
},
actions: {
setAuthorities(authorities) {
this.authorities = authorities;
},
hasAuthority(authority) {
return this.authorities.indexOf(authority) !== -1;
},
/**
* 给函数添加 权限校验
* @param key
* @param func
* @returns
*/
useAuth<T extends Function>(key: AuthKey, func: T): T {
const _this = this;
return function t() {
if (!_this.hasAuthority(key)) {
alert.error(msgFormatter(key));
} else {
return func.apply(undefined, arguments);
}
} as unknown as T;
},
},
});
/**
* 给函数添加 权限校验
* @param key
* @param func
* @returns
*/
export function useAuth<T extends Function>(key: AuthKey, func: T): T {
return function t() {
const authStore = useAuthStore();
if (!authStore.hasAuthority(key)) {
alert.error(msgFormatter(key));
} else {
return func.apply(undefined, arguments);
}
} as unknown as T;
}
/**
* 插件配置
*/
export interface AuthPluginConfig {
disableClass?: string;
action?: 'hide' | 'disable';
formatter?: (access: string) => string;
[key: string | number]: any;
}
function msgFormatter(access: AuthKey) {
return `对不起,您没有 \`${access}\` 权限`;
}
interface Operator {
reject: (el: HTMLElement, access: string, config: AuthPluginConfig) => void;
access: (el: HTMLElement, access: string, config: AuthPluginConfig) => void;
}
const operators = {
hide: {
reject: (el: HTMLElement, access: string, config: AuthPluginConfig) => {
el.setAttribute('_display', el.style.display);
el.style.display = 'none';
},
access: (el: HTMLElement, access: string, config: AuthPluginConfig) => {
if (el.hasAttribute('_display')) {
el.style.display = el.getAttribute('_display');
}
el.removeAttribute('_display');
},
},
disable: {
reject: (el: HTMLElement, access: string, config: AuthPluginConfig) => {
el.classList.add(config.disableClass!);
el.setAttribute('disabled', '');
el.setAttribute('title', config.formatter!(access));
},
access: (el: HTMLElement, access: string, config: AuthPluginConfig) => {
el.classList.remove(config.disableClass!);
},
},
};
const AuthPlugin: Plugin = {
install(app, { disableClass = 'auth-disable', action = 'disable', formatter = msgFormatter }: AuthPluginConfig = {}) {
app.directive('auth', (el: HTMLElement, { value, arg: access, modifiers }, vnode) => {
const { disable, hide } = modifiers;
const _action = hide ? 'hide' : disable ? 'disable' : undefined;
const authConfig = { disableClass, formatter, action: _action ?? action } as AuthPluginConfig;
const operator: Operator = operators[authConfig.action!];
const authorStore = useAuthStore();
if (!authorStore.hasAuthority(access!)) {
operator.reject(el, access!, authConfig);
} else {
operator.access(el, access!, authConfig);
}
});
},
};
export default AuthPlugin;
+6
View File
@@ -0,0 +1,6 @@
#stepin-app .auth-disable {
@apply bg-disabled text-disabled relative cursor-not-allowed border-disabled rounded-sm border border-solid;
}
/* *[disabled] {
@apply bg-disabled text-disabled relative cursor-not-allowed border-disabled rounded-sm border border-solid;
} */
+10
View File
@@ -0,0 +1,10 @@
<script lang="ts" setup>
defineProps({ name: String });
</script>
<template>
<span role="img" style="line-height: 1">
<svg fill="currentColor" style="vertical-align: top" width="1em" height="1em" aria-hidden="true">
<use :xlink:href="`#${name}`"></use>
</svg>
</span>
</template>
+32
View File
@@ -0,0 +1,32 @@
import { Plugin } from 'vue';
import IconFont from './IconFont.vue';
function createScriptUrlElements(scriptUrls: string[]) {
scriptUrls.forEach((url) => {
if (url.length > 0) {
const script = document.createElement('script');
script.setAttribute('src', url);
script.setAttribute('data-namespace', url);
document.body.appendChild(script);
}
});
}
const IconFontPlugin: Plugin = {
install(app, options: { url: string | string[] }) {
if (
typeof document !== 'undefined' &&
typeof window !== 'undefined' &&
typeof document.createElement === 'function'
) {
if (Array.isArray(options.url)) {
createScriptUrlElements(options.url.reverse());
} else {
createScriptUrlElements([options.url]);
}
}
app.component('IconFont', IconFont);
},
};
export default IconFontPlugin;
+2
View File
@@ -0,0 +1,2 @@
export { useAuthStore, default as AuthPlugin } from './auth/auth-plugin';
export { default as IconfontPlugin } from './iconfont';

Some files were not shown because too many files have changed in this diff Show More