init
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"presets": ["es2015"]
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
VITE_BASE_URL=/
|
||||
VITE_API_URL=http://localhost:10101
|
||||
@@ -0,0 +1 @@
|
||||
VITE_BASE_URL=/
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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 版本)
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||

|
||||
|
||||
</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>
|
||||
@@ -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 版本)
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||

|
||||
|
||||
</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>
|
||||
@@ -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
|
||||
);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
"target": "ES6",
|
||||
"module": "ESNext",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
plugins: [require('tailwindcss'), require('autoprefixer')],
|
||||
};
|
||||
@@ -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 |
|
After Width: | Height: | Size: 3.7 KiB |
@@ -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 |
@@ -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>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 58 KiB |
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,4 @@
|
||||
import Guider from './Guider.vue';
|
||||
export type { GuideTarget, GuiderOption } from './interface';
|
||||
|
||||
export default Guider;
|
||||
@@ -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;
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
<template>
|
||||
<router-view></router-view>
|
||||
</template>
|
||||
<script lang="ts" setup></script>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<div>link view</div>
|
||||
</template>
|
||||
@@ -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>
|
||||
@@ -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';
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 {};
|
||||
@@ -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');
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import CookieTable from './CookieTable.vue';
|
||||
</script>
|
||||
<template>
|
||||
<div class="table w-full">
|
||||
<CookieTable />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,2 @@
|
||||
import Table from './Table.vue';
|
||||
export default Table;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import Init from './Init.vue';
|
||||
|
||||
export default Init;
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,3 @@
|
||||
import Login from './Login.vue';
|
||||
export { default as LoginModal } from './LoginModal.vue';
|
||||
export default Login;
|
||||
@@ -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>
|
||||
@@ -0,0 +1,3 @@
|
||||
import Logs from './MyLogs.vue';
|
||||
|
||||
export default Logs;
|
||||
@@ -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="默认465,yeah-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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,4 @@
|
||||
import MyPersonal from './MyPersonal.vue';
|
||||
import EmailSet from './EmailSet.vue';
|
||||
|
||||
export { MyPersonal, EmailSet };
|
||||
@@ -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>
|
||||
@@ -0,0 +1,2 @@
|
||||
import Set from './AppSet.vue';
|
||||
export default Set;
|
||||
@@ -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>
|
||||
@@ -0,0 +1,2 @@
|
||||
import Theme from './Theme.vue';
|
||||
export default Theme;
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,2 @@
|
||||
import DmRecords from './Records.vue';
|
||||
export default DmRecords;
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
} */
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
@@ -0,0 +1,2 @@
|
||||
export { useAuthStore, default as AuthPlugin } from './auth/auth-plugin';
|
||||
export { default as IconfontPlugin } from './iconfont';
|
||||