mirror of
http://183.237.82.114:10086/kilo/leaser_front.git
synced 2026-10-23 18:35:08 +00:00
feat:初始化项目结构
This commit is contained in:
commit
ac0d6dd5cc
160 changed files with 16402 additions and 0 deletions
1
.env.development
Normal file
1
.env.development
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
VITE_API_BASE_URL= 'https://go.kola.work/api'
|
||||||
1
.env.production
Normal file
1
.env.production
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
VITE_API_BASE_URL= 'https://go.kola.work/api'
|
||||||
3
.eslintignore
Normal file
3
.eslintignore
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
/*.json
|
||||||
|
/*.js
|
||||||
|
dist
|
||||||
70
.eslintrc.js
Normal file
70
.eslintrc.js
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
parser: 'vue-eslint-parser',
|
||||||
|
parserOptions: {
|
||||||
|
// Parser that checks the content of the <script> tag
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
sourceType: 'module',
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
'browser': true,
|
||||||
|
'node': true,
|
||||||
|
'vue/setup-compiler-macros': true,
|
||||||
|
},
|
||||||
|
plugins: ['@typescript-eslint'],
|
||||||
|
extends: [
|
||||||
|
// Airbnb JavaScript Style Guide https://github.com/airbnb/javascript
|
||||||
|
'airbnb-base',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:import/recommended',
|
||||||
|
'plugin:import/typescript',
|
||||||
|
'plugin:vue/vue3-recommended',
|
||||||
|
'plugin:prettier/recommended',
|
||||||
|
],
|
||||||
|
settings: {
|
||||||
|
'import/resolver': {
|
||||||
|
typescript: {
|
||||||
|
project: path.resolve(__dirname, './tsconfig.json'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'prettier/prettier': 1,
|
||||||
|
// Vue: Recommended rules to be closed or modify
|
||||||
|
'vue/require-default-prop': 0,
|
||||||
|
'vue/singleline-html-element-content-newline': 0,
|
||||||
|
'vue/max-attributes-per-line': 0,
|
||||||
|
// Vue: Add extra rules
|
||||||
|
'vue/custom-event-name-casing': [2, 'camelCase'],
|
||||||
|
'vue/no-v-text': 1,
|
||||||
|
'vue/padding-line-between-blocks': 1,
|
||||||
|
'vue/require-direct-export': 1,
|
||||||
|
'vue/multi-word-component-names': 0,
|
||||||
|
// Allow @ts-ignore comment
|
||||||
|
'@typescript-eslint/ban-ts-comment': 0,
|
||||||
|
'@typescript-eslint/no-unused-vars': 1,
|
||||||
|
'@typescript-eslint/no-empty-function': 1,
|
||||||
|
'@typescript-eslint/no-explicit-any': 0,
|
||||||
|
'import/extensions': [
|
||||||
|
2,
|
||||||
|
'ignorePackages',
|
||||||
|
{
|
||||||
|
js: 'never',
|
||||||
|
jsx: 'never',
|
||||||
|
ts: 'never',
|
||||||
|
tsx: 'never',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
|
||||||
|
'no-param-reassign': 0,
|
||||||
|
'prefer-regex-literals': 0,
|
||||||
|
'import/no-extraneous-dependencies': 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
4
.husky/commit-msg
Executable file
4
.husky/commit-msg
Executable file
|
|
@ -0,0 +1,4 @@
|
||||||
|
#!/bin/sh
|
||||||
|
. "$(dirname "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
pnpm commitlint --edit $1
|
||||||
4
.husky/pre-commit
Executable file
4
.husky/pre-commit
Executable file
|
|
@ -0,0 +1,4 @@
|
||||||
|
#!/bin/sh
|
||||||
|
. "$(dirname "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
npm run lint-staged
|
||||||
7
.prettierignore
Normal file
7
.prettierignore
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
/dist/*
|
||||||
|
.local
|
||||||
|
.output.js
|
||||||
|
/node_modules/**
|
||||||
|
|
||||||
|
**/*.svg
|
||||||
|
**/*.sh
|
||||||
9
.prettierrc.js
Normal file
9
.prettierrc.js
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
module.exports = {
|
||||||
|
tabWidth: 2,
|
||||||
|
semi: true,
|
||||||
|
printWidth: 80,
|
||||||
|
singleQuote: true,
|
||||||
|
quoteProps: 'consistent',
|
||||||
|
htmlWhitespaceSensitivity: 'strict',
|
||||||
|
vueIndentScriptAndStyle: true,
|
||||||
|
};
|
||||||
30
.stylelintrc.js
Normal file
30
.stylelintrc.js
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
module.exports = {
|
||||||
|
extends: [
|
||||||
|
'stylelint-config-standard',
|
||||||
|
'stylelint-config-rational-order',
|
||||||
|
'stylelint-config-prettier',
|
||||||
|
'stylelint-config-recommended-vue',
|
||||||
|
],
|
||||||
|
defaultSeverity: 'warning',
|
||||||
|
plugins: ['stylelint-order'],
|
||||||
|
rules: {
|
||||||
|
'at-rule-no-unknown': [
|
||||||
|
true,
|
||||||
|
{
|
||||||
|
ignoreAtRules: ['plugin'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'rule-empty-line-before': [
|
||||||
|
'always',
|
||||||
|
{
|
||||||
|
except: ['after-single-line-comment', 'first-nested'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'selector-pseudo-class-no-unknown': [
|
||||||
|
true,
|
||||||
|
{
|
||||||
|
ignorePseudoClasses: ['deep'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
3
babel.config.js
Normal file
3
babel.config.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
module.exports = {
|
||||||
|
plugins: ['@vue/babel-plugin-jsx'],
|
||||||
|
};
|
||||||
3
commitlint.config.js
Normal file
3
commitlint.config.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
module.exports = {
|
||||||
|
extends: ['@commitlint/config-conventional'],
|
||||||
|
};
|
||||||
15
components.d.ts
vendored
Normal file
15
components.d.ts
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
/* eslint-disable */
|
||||||
|
/* prettier-ignore */
|
||||||
|
// @ts-nocheck
|
||||||
|
// Generated by unplugin-vue-components
|
||||||
|
// Read more: https://github.com/vuejs/core/pull/3399
|
||||||
|
import '@vue/runtime-core'
|
||||||
|
|
||||||
|
export {}
|
||||||
|
|
||||||
|
declare module '@vue/runtime-core' {
|
||||||
|
export interface GlobalComponents {
|
||||||
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
|
}
|
||||||
|
}
|
||||||
19
config/plugin/arcoResolver.ts
Normal file
19
config/plugin/arcoResolver.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
/**
|
||||||
|
* If you use the template method for development, you can use the unplugin-vue-components plugin to enable on-demand loading support.
|
||||||
|
* 按需引入
|
||||||
|
* https://github.com/antfu/unplugin-vue-components
|
||||||
|
* https://arco.design/vue/docs/start
|
||||||
|
* Although the Pro project is full of imported components, this plugin will be used by default.
|
||||||
|
* 虽然Pro项目中是全量引入组件,但此插件会默认使用。
|
||||||
|
*/
|
||||||
|
import Components from 'unplugin-vue-components/vite';
|
||||||
|
import { ArcoResolver } from 'unplugin-vue-components/resolvers';
|
||||||
|
|
||||||
|
export default function configArcoResolverPlugin() {
|
||||||
|
const arcoResolverPlugin = Components({
|
||||||
|
dirs: [], // Avoid parsing src/components. 避免解析到src/components
|
||||||
|
deep: false,
|
||||||
|
resolvers: [ArcoResolver()],
|
||||||
|
});
|
||||||
|
return arcoResolverPlugin;
|
||||||
|
}
|
||||||
12
config/plugin/arcoStyleImport.ts
Normal file
12
config/plugin/arcoStyleImport.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
/**
|
||||||
|
* Theme import
|
||||||
|
* 样式按需引入
|
||||||
|
* https://github.com/arco-design/arco-plugins/blob/main/packages/plugin-vite-vue/README.md
|
||||||
|
* https://arco.design/vue/docs/start
|
||||||
|
*/
|
||||||
|
import { vitePluginForArco } from '@arco-plugins/vite-vue';
|
||||||
|
|
||||||
|
export default function configArcoStyleImportPlugin() {
|
||||||
|
const arcoResolverPlugin = vitePluginForArco({});
|
||||||
|
return arcoResolverPlugin;
|
||||||
|
}
|
||||||
34
config/plugin/compress.ts
Normal file
34
config/plugin/compress.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
/**
|
||||||
|
* Used to package and output gzip. Note that this does not work properly in Vite, the specific reason is still being investigated
|
||||||
|
* gzip压缩
|
||||||
|
* https://github.com/anncwb/vite-plugin-compression
|
||||||
|
*/
|
||||||
|
import type { Plugin } from 'vite';
|
||||||
|
import compressPlugin from 'vite-plugin-compression';
|
||||||
|
|
||||||
|
export default function configCompressPlugin(
|
||||||
|
compress: 'gzip' | 'brotli',
|
||||||
|
deleteOriginFile = false
|
||||||
|
): Plugin | Plugin[] {
|
||||||
|
const plugins: Plugin[] = [];
|
||||||
|
|
||||||
|
if (compress === 'gzip') {
|
||||||
|
plugins.push(
|
||||||
|
compressPlugin({
|
||||||
|
ext: '.gz',
|
||||||
|
deleteOriginFile,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (compress === 'brotli') {
|
||||||
|
plugins.push(
|
||||||
|
compressPlugin({
|
||||||
|
ext: '.br',
|
||||||
|
algorithm: 'brotliCompress',
|
||||||
|
deleteOriginFile,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return plugins;
|
||||||
|
}
|
||||||
37
config/plugin/imagemin.ts
Normal file
37
config/plugin/imagemin.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
/**
|
||||||
|
* Image resource files used to compress the output of the production environment
|
||||||
|
* 图片压缩
|
||||||
|
* https://github.com/anncwb/vite-plugin-imagemin
|
||||||
|
*/
|
||||||
|
import viteImagemin from 'vite-plugin-imagemin';
|
||||||
|
|
||||||
|
export default function configImageminPlugin() {
|
||||||
|
const imageminPlugin = viteImagemin({
|
||||||
|
gifsicle: {
|
||||||
|
optimizationLevel: 7,
|
||||||
|
interlaced: false,
|
||||||
|
},
|
||||||
|
optipng: {
|
||||||
|
optimizationLevel: 7,
|
||||||
|
},
|
||||||
|
mozjpeg: {
|
||||||
|
quality: 20,
|
||||||
|
},
|
||||||
|
pngquant: {
|
||||||
|
quality: [0.8, 0.9],
|
||||||
|
speed: 4,
|
||||||
|
},
|
||||||
|
svgo: {
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
name: 'removeViewBox',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'removeEmptyAttrs',
|
||||||
|
active: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return imageminPlugin;
|
||||||
|
}
|
||||||
18
config/plugin/visualizer.ts
Normal file
18
config/plugin/visualizer.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
/**
|
||||||
|
* Generation packaging analysis
|
||||||
|
* 生成打包分析
|
||||||
|
*/
|
||||||
|
import visualizer from 'rollup-plugin-visualizer';
|
||||||
|
import { isReportMode } from '../utils';
|
||||||
|
|
||||||
|
export default function configVisualizerPlugin() {
|
||||||
|
if (isReportMode()) {
|
||||||
|
return visualizer({
|
||||||
|
filename: './node_modules/.cache/visualizer/stats.html',
|
||||||
|
open: true,
|
||||||
|
gzipSize: true,
|
||||||
|
brotliSize: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
9
config/utils/index.ts
Normal file
9
config/utils/index.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
/**
|
||||||
|
* Whether to generate package preview
|
||||||
|
* 是否生成打包报告
|
||||||
|
*/
|
||||||
|
export default {};
|
||||||
|
|
||||||
|
export function isReportMode(): boolean {
|
||||||
|
return process.env.REPORT === 'true';
|
||||||
|
}
|
||||||
51
config/vite.config.base.ts
Normal file
51
config/vite.config.base.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { resolve } from 'path';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import vue from '@vitejs/plugin-vue';
|
||||||
|
import vueJsx from '@vitejs/plugin-vue-jsx';
|
||||||
|
import svgLoader from 'vite-svg-loader';
|
||||||
|
import configArcoStyleImportPlugin from './plugin/arcoStyleImport';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
vueJsx(),
|
||||||
|
svgLoader({ svgoConfig: {} }),
|
||||||
|
configArcoStyleImportPlugin(),
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: [
|
||||||
|
{
|
||||||
|
find: '@',
|
||||||
|
replacement: resolve(__dirname, '../src'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: 'assets',
|
||||||
|
replacement: resolve(__dirname, '../src/assets'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: 'vue-i18n',
|
||||||
|
replacement: 'vue-i18n/dist/vue-i18n.cjs.js', // Resolve the i18n warning issue
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: 'vue',
|
||||||
|
replacement: 'vue/dist/vue.esm-bundler.js', // compile template
|
||||||
|
},
|
||||||
|
],
|
||||||
|
extensions: ['.ts', '.js'],
|
||||||
|
},
|
||||||
|
define: {
|
||||||
|
'process.env': {},
|
||||||
|
},
|
||||||
|
css: {
|
||||||
|
preprocessorOptions: {
|
||||||
|
less: {
|
||||||
|
modifyVars: {
|
||||||
|
hack: `true; @import (reference) "${resolve(
|
||||||
|
'src/assets/style/breakpoint.less'
|
||||||
|
)}";`,
|
||||||
|
},
|
||||||
|
javascriptEnabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
23
config/vite.config.dev.ts
Normal file
23
config/vite.config.dev.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { mergeConfig } from 'vite';
|
||||||
|
import eslint from 'vite-plugin-eslint';
|
||||||
|
import baseConfig from './vite.config.base';
|
||||||
|
|
||||||
|
export default mergeConfig(
|
||||||
|
{
|
||||||
|
mode: 'development',
|
||||||
|
server: {
|
||||||
|
open: true,
|
||||||
|
fs: {
|
||||||
|
strict: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
// eslint({
|
||||||
|
// cache: false,
|
||||||
|
// include: ['src/**/*.ts', 'src/**/*.tsx', 'src/**/*.vue'],
|
||||||
|
// exclude: ['node_modules'],
|
||||||
|
// }),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
baseConfig
|
||||||
|
);
|
||||||
31
config/vite.config.prod.ts
Normal file
31
config/vite.config.prod.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { mergeConfig } from 'vite';
|
||||||
|
import baseConfig from './vite.config.base';
|
||||||
|
import configCompressPlugin from './plugin/compress';
|
||||||
|
import configVisualizerPlugin from './plugin/visualizer';
|
||||||
|
import configArcoResolverPlugin from './plugin/arcoResolver';
|
||||||
|
import configImageminPlugin from './plugin/imagemin';
|
||||||
|
|
||||||
|
export default mergeConfig(
|
||||||
|
{
|
||||||
|
mode: 'production',
|
||||||
|
plugins: [
|
||||||
|
configCompressPlugin('gzip'),
|
||||||
|
configVisualizerPlugin(),
|
||||||
|
configArcoResolverPlugin(),
|
||||||
|
configImageminPlugin(),
|
||||||
|
],
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks: {
|
||||||
|
arco: ['@arco-design/web-vue'],
|
||||||
|
chart: ['echarts', 'vue-echarts'],
|
||||||
|
vue: ['vue', 'vue-router', 'pinia', '@vueuse/core', 'vue-i18n'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
chunkSizeWarningLimit: 2000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
baseConfig
|
||||||
|
);
|
||||||
13
index.html
Normal file
13
index.html
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="shortcut icon" type="image/x-icon" href="https://unpkg.byted-static.com/latest/byted/arco-config/assets/favicon.ico">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Arco Design Pro - 开箱即用的中台前端/设计解决方案</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
107
package.json
Normal file
107
package.json
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
{
|
||||||
|
"name": "arco-design-pro-vue",
|
||||||
|
"description": "Arco Design Pro for Vue",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"author": "ArcoDesign Team",
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --config ./config/vite.config.dev.ts",
|
||||||
|
"build": "vue-tsc --noEmit && vite build --config ./config/vite.config.prod.ts",
|
||||||
|
"report": "cross-env REPORT=true npm run build",
|
||||||
|
"preview": "npm run build && vite preview --host",
|
||||||
|
"type:check": "vue-tsc --noEmit --skipLibCheck",
|
||||||
|
"lint-staged": "npx lint-staged",
|
||||||
|
"prepare": "husky install"
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*.{js,ts,jsx,tsx}": [
|
||||||
|
"prettier --write",
|
||||||
|
"eslint --fix"
|
||||||
|
],
|
||||||
|
"*.vue": [
|
||||||
|
"stylelint --fix",
|
||||||
|
"prettier --write",
|
||||||
|
"eslint --fix"
|
||||||
|
],
|
||||||
|
"*.{less,css}": [
|
||||||
|
"stylelint --fix",
|
||||||
|
"prettier --write"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@arco-design/web-vue": "^2.44.7",
|
||||||
|
"@vueuse/core": "^9.3.0",
|
||||||
|
"arco-design-pro-vue": "^2.7.3",
|
||||||
|
"axios": "^0.24.0",
|
||||||
|
"dayjs": "^1.11.5",
|
||||||
|
"echarts": "^5.4.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"mitt": "^3.0.0",
|
||||||
|
"nprogress": "^0.2.0",
|
||||||
|
"pinia": "^2.0.23",
|
||||||
|
"query-string": "^8.0.3",
|
||||||
|
"sortablejs": "^1.15.0",
|
||||||
|
"vue": "^3.2.40",
|
||||||
|
"vue-echarts": "^6.2.3",
|
||||||
|
"vue-i18n": "^9.2.2",
|
||||||
|
"vue-router": "^4.0.14"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@arco-design/web-vue": "^2.0.0-beta.7",
|
||||||
|
"@arco-plugins/vite-vue": "^1.4.5",
|
||||||
|
"@commitlint/cli": "^17.1.2",
|
||||||
|
"@commitlint/config-conventional": "^17.1.0",
|
||||||
|
"@types/lodash": "^4.14.186",
|
||||||
|
"@types/mockjs": "^1.0.7",
|
||||||
|
"@types/nprogress": "^0.2.0",
|
||||||
|
"@types/sortablejs": "^1.15.0",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.40.0",
|
||||||
|
"@typescript-eslint/parser": "^5.40.0",
|
||||||
|
"@vitejs/plugin-vue": "^3.1.2",
|
||||||
|
"@vitejs/plugin-vue-jsx": "^2.0.1",
|
||||||
|
"@vue/babel-plugin-jsx": "^1.1.1",
|
||||||
|
"consola": "^2.15.3",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"eslint": "^8.25.0",
|
||||||
|
"eslint-config-airbnb-base": "^15.0.0",
|
||||||
|
"eslint-config-prettier": "^8.5.0",
|
||||||
|
"eslint-import-resolver-typescript": "^3.5.1",
|
||||||
|
"eslint-plugin-import": "^2.26.0",
|
||||||
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
|
"eslint-plugin-vue": "^9.6.0",
|
||||||
|
"husky": "^8.0.1",
|
||||||
|
"less": "^4.1.3",
|
||||||
|
"lint-staged": "^13.0.3",
|
||||||
|
"mockjs": "^1.1.0",
|
||||||
|
"postcss-html": "^1.5.0",
|
||||||
|
"prettier": "^2.7.1",
|
||||||
|
"rollup": "^3.9.1",
|
||||||
|
"rollup-plugin-visualizer": "^5.8.2",
|
||||||
|
"stylelint": "^14.13.0",
|
||||||
|
"stylelint-config-prettier": "^9.0.3",
|
||||||
|
"stylelint-config-rational-order": "^0.1.2",
|
||||||
|
"stylelint-config-recommended-vue": "^1.4.0",
|
||||||
|
"stylelint-config-standard": "^29.0.0",
|
||||||
|
"stylelint-order": "^5.0.0",
|
||||||
|
"typescript": "^4.8.4",
|
||||||
|
"unplugin-vue-components": "^0.24.1",
|
||||||
|
"vite": "^3.2.5",
|
||||||
|
"vite-plugin-compression": "^0.5.1",
|
||||||
|
"vite-plugin-eslint": "^1.8.1",
|
||||||
|
"vite-plugin-imagemin": "^0.6.1",
|
||||||
|
"vite-svg-loader": "^3.6.0",
|
||||||
|
"vue-tsc": "^1.0.14"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
},
|
||||||
|
"resolutions": {
|
||||||
|
"bin-wrapper": "npm:bin-wrapper-china",
|
||||||
|
"rollup": "^2.56.3",
|
||||||
|
"gifsicle": "5.2.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@arco-design/web-vue": ">=2.0.0-beta.7"
|
||||||
|
}
|
||||||
|
}
|
||||||
10663
pnpm-lock.yaml
generated
Normal file
10663
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load diff
26
src/App.vue
Normal file
26
src/App.vue
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
<template>
|
||||||
|
<a-config-provider :locale="locale">
|
||||||
|
<router-view />
|
||||||
|
<global-setting />
|
||||||
|
</a-config-provider>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import enUS from '@arco-design/web-vue/es/locale/lang/en-us';
|
||||||
|
import zhCN from '@arco-design/web-vue/es/locale/lang/zh-cn';
|
||||||
|
import GlobalSetting from '@/components/global-setting/index.vue';
|
||||||
|
import useLocale from '@/hooks/locale';
|
||||||
|
|
||||||
|
const { currentLocale } = useLocale();
|
||||||
|
const locale = computed(() => {
|
||||||
|
switch (currentLocale.value) {
|
||||||
|
case 'zh-CN':
|
||||||
|
return zhCN;
|
||||||
|
case 'en-US':
|
||||||
|
return enUS;
|
||||||
|
default:
|
||||||
|
return enUS;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
22
src/api/dashboard.ts
Normal file
22
src/api/dashboard.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
import type { TableData } from '@arco-design/web-vue/es/table/interface';
|
||||||
|
|
||||||
|
export interface ContentDataRecord {
|
||||||
|
x: string;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function queryContentData() {
|
||||||
|
return axios.get<ContentDataRecord[]>('/api/content-data');
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PopularRecord {
|
||||||
|
key: number;
|
||||||
|
clickNumber: string;
|
||||||
|
title: string;
|
||||||
|
increases: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function queryPopularList(params: { type: string }) {
|
||||||
|
return axios.get<TableData[]>('/api/popular/list', { params });
|
||||||
|
}
|
||||||
21
src/api/form.ts
Normal file
21
src/api/form.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
export interface BaseInfoModel {
|
||||||
|
activityName: string;
|
||||||
|
channelType: string;
|
||||||
|
promotionTime: string[];
|
||||||
|
promoteLink: string;
|
||||||
|
}
|
||||||
|
export interface ChannelInfoModel {
|
||||||
|
advertisingSource: string;
|
||||||
|
advertisingMedia: string;
|
||||||
|
keyword: string[];
|
||||||
|
pushNotify: boolean;
|
||||||
|
advertisingContent: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UnitChannelModel = BaseInfoModel & ChannelInfoModel;
|
||||||
|
|
||||||
|
export function submitChannelForm(data: UnitChannelModel) {
|
||||||
|
return axios.post('/api/channel-form/submit', { data });
|
||||||
|
}
|
||||||
71
src/api/interceptor.ts
Normal file
71
src/api/interceptor.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
import type { AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||||
|
import { Message, Modal } from '@arco-design/web-vue';
|
||||||
|
import { useUserStore } from '@/store';
|
||||||
|
import { getToken } from '@/utils/auth';
|
||||||
|
|
||||||
|
export interface HttpResponse<T = unknown> {
|
||||||
|
// status: number;
|
||||||
|
message: string;
|
||||||
|
code: number;
|
||||||
|
data: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (import.meta.env.VITE_API_BASE_URL) {
|
||||||
|
axios.defaults.baseURL = import.meta.env.VITE_API_BASE_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
axios.interceptors.request.use(
|
||||||
|
(config: AxiosRequestConfig) => {
|
||||||
|
const token = getToken();
|
||||||
|
if (token) {
|
||||||
|
if (!config.headers) {
|
||||||
|
config.headers = {};
|
||||||
|
}
|
||||||
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// add response interceptors
|
||||||
|
axios.interceptors.response.use(
|
||||||
|
(response: AxiosResponse<HttpResponse>) => {
|
||||||
|
const res = response.data;
|
||||||
|
if (res.code !== 200) {
|
||||||
|
Message.error({
|
||||||
|
content: res.message || 'Error',
|
||||||
|
duration: 5 * 1000,
|
||||||
|
});
|
||||||
|
// 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired;
|
||||||
|
// if (
|
||||||
|
// [50008, 50012, 50014].includes(res.code) &&
|
||||||
|
// response.config.url !== '/api/user/info'
|
||||||
|
// ) {
|
||||||
|
// Modal.error({
|
||||||
|
// title: 'Confirm logout',
|
||||||
|
// content:
|
||||||
|
// 'You have been logged out, you can cancel to stay on this page, or log in again',
|
||||||
|
// okText: 'Re-Login',
|
||||||
|
// async onOk() {
|
||||||
|
// const userStore = useUserStore();
|
||||||
|
|
||||||
|
// await userStore.logout();
|
||||||
|
// window.location.reload();
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
return Promise.reject(new Error(res.message || 'Error'));
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
Message.error({
|
||||||
|
content: error.message || 'Request Error',
|
||||||
|
duration: 5 * 1000,
|
||||||
|
});
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
56
src/api/list.ts
Normal file
56
src/api/list.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
import qs from 'query-string';
|
||||||
|
import type { DescData } from '@arco-design/web-vue/es/descriptions/interface';
|
||||||
|
|
||||||
|
export interface PolicyRecord {
|
||||||
|
id: string;
|
||||||
|
number: number;
|
||||||
|
name: string;
|
||||||
|
contentType: 'img' | 'horizontalVideo' | 'verticalVideo';
|
||||||
|
filterType: 'artificial' | 'rules';
|
||||||
|
count: number;
|
||||||
|
status: 'online' | 'offline';
|
||||||
|
createdTime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PolicyParams extends Partial<PolicyRecord> {
|
||||||
|
current: number;
|
||||||
|
pageSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PolicyListRes {
|
||||||
|
list: PolicyRecord[];
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function queryPolicyList(params: PolicyParams) {
|
||||||
|
return axios.get<PolicyListRes>('/api/list/policy', {
|
||||||
|
params,
|
||||||
|
paramsSerializer: (obj) => {
|
||||||
|
return qs.stringify(obj);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServiceRecord {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
name?: string;
|
||||||
|
actionType?: string;
|
||||||
|
icon?: string;
|
||||||
|
data?: DescData[];
|
||||||
|
enable?: boolean;
|
||||||
|
expires?: boolean;
|
||||||
|
}
|
||||||
|
export function queryInspectionList() {
|
||||||
|
return axios.get('/api/list/quality-inspection');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function queryTheServiceList() {
|
||||||
|
return axios.get('/api/list/the-service');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function queryRulesPresetList() {
|
||||||
|
return axios.get('/api/list/rules-preset');
|
||||||
|
}
|
||||||
38
src/api/message.ts
Normal file
38
src/api/message.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
export interface MessageRecord {
|
||||||
|
id: number;
|
||||||
|
type: string;
|
||||||
|
title: string;
|
||||||
|
subTitle: string;
|
||||||
|
avatar?: string;
|
||||||
|
content: string;
|
||||||
|
time: string;
|
||||||
|
status: 0 | 1;
|
||||||
|
messageType?: number;
|
||||||
|
}
|
||||||
|
export type MessageListType = MessageRecord[];
|
||||||
|
|
||||||
|
export function queryMessageList() {
|
||||||
|
return axios.post<MessageListType>('/api/message/list');
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MessageStatus {
|
||||||
|
ids: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setMessageStatus(data: MessageStatus) {
|
||||||
|
return axios.post<MessageListType>('/api/message/read', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatRecord {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
content: string;
|
||||||
|
time: string;
|
||||||
|
isCollect: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function queryChatList() {
|
||||||
|
return axios.post<ChatRecord[]>('/api/chat/list');
|
||||||
|
}
|
||||||
49
src/api/profile.ts
Normal file
49
src/api/profile.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
export interface ProfileBasicRes {
|
||||||
|
status: number;
|
||||||
|
video: {
|
||||||
|
mode: string;
|
||||||
|
acquisition: {
|
||||||
|
resolution: string;
|
||||||
|
frameRate: number;
|
||||||
|
};
|
||||||
|
encoding: {
|
||||||
|
resolution: string;
|
||||||
|
rate: {
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
default: number;
|
||||||
|
};
|
||||||
|
frameRate: number;
|
||||||
|
profile: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
audio: {
|
||||||
|
mode: string;
|
||||||
|
acquisition: {
|
||||||
|
channels: number;
|
||||||
|
};
|
||||||
|
encoding: {
|
||||||
|
channels: number;
|
||||||
|
rate: number;
|
||||||
|
profile: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function queryProfileBasic() {
|
||||||
|
return axios.get<ProfileBasicRes>('/api/profile/basic');
|
||||||
|
}
|
||||||
|
|
||||||
|
export type operationLogRes = Array<{
|
||||||
|
key: string;
|
||||||
|
contentNumber: string;
|
||||||
|
updateContent: string;
|
||||||
|
status: number;
|
||||||
|
updateTime: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export function queryOperationLog() {
|
||||||
|
return axios.get<operationLogRes>('/api/operation/log');
|
||||||
|
}
|
||||||
88
src/api/user-center.ts
Normal file
88
src/api/user-center.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
export interface MyProjectRecord {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
peopleNumber: number;
|
||||||
|
contributors: {
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
avatar: string;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
export function queryMyProjectList() {
|
||||||
|
return axios.post('/api/user/my-project/list');
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MyTeamRecord {
|
||||||
|
id: number;
|
||||||
|
avatar: string;
|
||||||
|
name: string;
|
||||||
|
peopleNumber: number;
|
||||||
|
}
|
||||||
|
export function queryMyTeamList() {
|
||||||
|
return axios.post('/api/user/my-team/list');
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LatestActivity {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
avatar: string;
|
||||||
|
}
|
||||||
|
export function queryLatestActivity() {
|
||||||
|
return axios.post<LatestActivity[]>('/api/user/latest-activity');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveUserInfo() {
|
||||||
|
return axios.post('/api/user/save-info');
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BasicInfoModel {
|
||||||
|
email: string;
|
||||||
|
nickname: string;
|
||||||
|
countryRegion: string;
|
||||||
|
area: string;
|
||||||
|
address: string;
|
||||||
|
profile: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EnterpriseCertificationModel {
|
||||||
|
accountType: number;
|
||||||
|
status: number;
|
||||||
|
time: string;
|
||||||
|
legalPerson: string;
|
||||||
|
certificateType: string;
|
||||||
|
authenticationNumber: string;
|
||||||
|
enterpriseName: string;
|
||||||
|
enterpriseCertificateType: string;
|
||||||
|
organizationCode: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CertificationRecord = Array<{
|
||||||
|
certificationType: number;
|
||||||
|
certificationContent: string;
|
||||||
|
status: number;
|
||||||
|
time: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export interface UnitCertification {
|
||||||
|
enterpriseInfo: EnterpriseCertificationModel;
|
||||||
|
record: CertificationRecord;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function queryCertification() {
|
||||||
|
return axios.post<UnitCertification>('/api/user/certification');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userUploadApi(
|
||||||
|
data: FormData,
|
||||||
|
config: {
|
||||||
|
controller: AbortController;
|
||||||
|
onUploadProgress?: (progressEvent: any) => void;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
// const controller = new AbortController();
|
||||||
|
return axios.post('/api/user/upload', data, config);
|
||||||
|
}
|
||||||
47
src/api/user.ts
Normal file
47
src/api/user.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
import type { RouteRecordNormalized } from 'vue-router';
|
||||||
|
import { UserState } from '@/store/modules/user/types';
|
||||||
|
|
||||||
|
export interface LoginData {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
export interface LoginRes {
|
||||||
|
token: string;
|
||||||
|
user: string;
|
||||||
|
}
|
||||||
|
export interface UserPermission {
|
||||||
|
id: number; // 权限ID
|
||||||
|
parent_id: number; // 上一级ID
|
||||||
|
prem_name: string; // 权限名称
|
||||||
|
level: number; // 层级
|
||||||
|
perm_key: string; // 权限标识
|
||||||
|
prem_type: 1 | 2 | 3 | 4; // 权限类型:1菜单、2按钮、3接口、4页面
|
||||||
|
path: string; // 路由路径
|
||||||
|
sort: number; // 排序
|
||||||
|
visible: 1 | 2; // 是否可见:1显示、2隐藏
|
||||||
|
createdAt: string; // 创建时间
|
||||||
|
updatedAt: string; // 更新时间
|
||||||
|
}
|
||||||
|
export interface UserPermissionList {
|
||||||
|
permission: UserPermission[];
|
||||||
|
}
|
||||||
|
export async function login(data: LoginData) {
|
||||||
|
const result = axios.post<LoginRes>('/auth/login', data);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
export function getUserPermission() {
|
||||||
|
return axios.get<UserPermissionList[]>('/auth/getUserPermission');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logout() {
|
||||||
|
return axios.post<LoginRes>('/user/logout');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUserInfo() {
|
||||||
|
return axios.get<UserState>('/auth/profile');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMenuList() {
|
||||||
|
return axios.post<RouteRecordNormalized[]>('/api/user/menu');
|
||||||
|
}
|
||||||
73
src/api/visualization.ts
Normal file
73
src/api/visualization.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
import { GeneralChart } from '@/types/global';
|
||||||
|
|
||||||
|
export interface ChartDataRecord {
|
||||||
|
x: string;
|
||||||
|
y: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
export interface DataChainGrowth {
|
||||||
|
quota: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DataChainGrowthRes {
|
||||||
|
count: number;
|
||||||
|
growth: number;
|
||||||
|
chartData: {
|
||||||
|
xAxis: string[];
|
||||||
|
data: { name: string; value: number[] };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export function queryDataChainGrowth(data: DataChainGrowth) {
|
||||||
|
return axios.post<DataChainGrowthRes>('/api/data-chain-growth', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PopularAuthorRes {
|
||||||
|
list: {
|
||||||
|
ranking: number;
|
||||||
|
author: string;
|
||||||
|
contentCount: number;
|
||||||
|
clickCount: number;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function queryPopularAuthor() {
|
||||||
|
return axios.get<PopularAuthorRes>('/api/popular-author/list');
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContentPublishRecord {
|
||||||
|
x: string[];
|
||||||
|
y: number[];
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function queryContentPublish() {
|
||||||
|
return axios.get<ContentPublishRecord[]>('/api/content-publish');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function queryContentPeriodAnalysis() {
|
||||||
|
return axios.post<GeneralChart>('/api/content-period-analysis');
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PublicOpinionAnalysis {
|
||||||
|
quota: string;
|
||||||
|
}
|
||||||
|
export interface PublicOpinionAnalysisRes {
|
||||||
|
count: number;
|
||||||
|
growth: number;
|
||||||
|
chartData: ChartDataRecord[];
|
||||||
|
}
|
||||||
|
export function queryPublicOpinionAnalysis(data: DataChainGrowth) {
|
||||||
|
return axios.post<PublicOpinionAnalysisRes>(
|
||||||
|
'/api/public-opinion-analysis',
|
||||||
|
data
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export interface DataOverviewRes {
|
||||||
|
xAxis: string[];
|
||||||
|
data: Array<{ name: string; value: number[]; count: number }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function queryDataOverview() {
|
||||||
|
return axios.post<DataOverviewRes>('/api/data-overview');
|
||||||
|
}
|
||||||
BIN
src/assets/images/building1.png
Normal file
BIN
src/assets/images/building1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
BIN
src/assets/images/login-banner.png
Normal file
BIN
src/assets/images/login-banner.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
12
src/assets/logo.svg
Normal file
12
src/assets/logo.svg
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<svg width="33" height="33" viewBox="0 0 33 33" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0)">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.37754 16.9795L12.7498 9.43027C14.7163 7.41663 17.9428 7.37837 19.9564 9.34482C19.9852 9.37297 20.0137 9.40145 20.0418 9.43027L20.1221 9.51243C22.1049 11.5429 22.1049 14.7847 20.1221 16.8152L12.7498 24.3644C10.7834 26.378 7.55686 26.4163 5.54322 24.4498C5.5144 24.4217 5.48592 24.3932 5.45777 24.3644L5.37754 24.2822C3.39468 22.2518 3.39468 19.0099 5.37754 16.9795Z" fill="#12D2AC"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.0479 9.43034L27.3399 16.8974C29.3674 18.9735 29.3674 22.2883 27.3399 24.3644C25.3735 26.3781 22.147 26.4163 20.1333 24.4499C20.1045 24.4217 20.076 24.3933 20.0479 24.3644L12.7558 16.8974C10.7284 14.8213 10.7284 11.5065 12.7558 9.43034C14.7223 7.4167 17.9488 7.37844 19.9624 9.34489C19.9912 9.37304 20.0197 9.40152 20.0479 9.43034Z" fill="#307AF2"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.1321 9.52163L23.6851 13.1599L16.3931 20.627L9.10103 13.1599L12.6541 9.52163C14.6707 7.45664 17.9794 7.4174 20.0444 9.434C20.074 9.46286 20.1032 9.49207 20.1321 9.52163Z" fill="#0057FE"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0">
|
||||||
|
<rect width="26" height="19" fill="white" transform="translate(3.5 7)"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
19
src/assets/style/breakpoint.less
Normal file
19
src/assets/style/breakpoint.less
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
// ==============breakpoint============
|
||||||
|
|
||||||
|
// Extra small screen / phone
|
||||||
|
@screen-xs: 480px;
|
||||||
|
|
||||||
|
// Small screen / tablet
|
||||||
|
@screen-sm: 576px;
|
||||||
|
|
||||||
|
// Medium screen / desktop
|
||||||
|
@screen-md: 768px;
|
||||||
|
|
||||||
|
// Large screen / wide desktop
|
||||||
|
@screen-lg: 992px;
|
||||||
|
|
||||||
|
// Extra large screen / full hd
|
||||||
|
@screen-xl: 1200px;
|
||||||
|
|
||||||
|
// Extra extra large screen / large desktop
|
||||||
|
@screen-xxl: 1600px;
|
||||||
94
src/assets/style/global.less
Normal file
94
src/assets/style/global.less
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
background-color: var(--color-bg-1);
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
|
||||||
|
.echarts-tooltip-diy {
|
||||||
|
background: linear-gradient(
|
||||||
|
304.17deg,
|
||||||
|
rgba(253, 254, 255, 0.6) -6.04%,
|
||||||
|
rgba(244, 247, 252, 0.6) 85.2%
|
||||||
|
) !important;
|
||||||
|
border: none !important;
|
||||||
|
backdrop-filter: blur(10px) !important;
|
||||||
|
/* Note: backdrop-filter has minimal browser support */
|
||||||
|
|
||||||
|
border-radius: 6px !important;
|
||||||
|
.content-panel {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 9px;
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
width: 164px;
|
||||||
|
height: 32px;
|
||||||
|
line-height: 32px;
|
||||||
|
box-shadow: 6px 0px 20px rgba(34, 87, 188, 0.1);
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.tooltip-title {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.tooltip-title,
|
||||||
|
.tooltip-value {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 15px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
text-align: right;
|
||||||
|
color: #1d2129;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.tooltip-item-icon {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 8px;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.general-card {
|
||||||
|
border-radius: 4px;
|
||||||
|
border: none;
|
||||||
|
& > .arco-card-header {
|
||||||
|
height: auto;
|
||||||
|
padding: 20px;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
& > .arco-card-body {
|
||||||
|
padding: 0 20px 20px 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.split-line {
|
||||||
|
border-color: rgb(var(--gray-2));
|
||||||
|
}
|
||||||
|
|
||||||
|
.arco-table-cell {
|
||||||
|
.circle {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 4px;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: rgb(var(--blue-6));
|
||||||
|
&.pass {
|
||||||
|
background-color: rgb(var(--green-6));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/assets/world.json
Normal file
1
src/assets/world.json
Normal file
File diff suppressed because one or more lines are too long
35
src/components/breadcrumb/index.vue
Normal file
35
src/components/breadcrumb/index.vue
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
<template>
|
||||||
|
<a-breadcrumb class="container-breadcrumb">
|
||||||
|
<a-breadcrumb-item>
|
||||||
|
<icon-apps />
|
||||||
|
</a-breadcrumb-item>
|
||||||
|
<a-breadcrumb-item v-for="item in items" :key="item">
|
||||||
|
{{ $t(item) }}
|
||||||
|
</a-breadcrumb-item>
|
||||||
|
</a-breadcrumb>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { PropType } from 'vue';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
items: {
|
||||||
|
type: Array as PropType<string[]>,
|
||||||
|
default() {
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.container-breadcrumb {
|
||||||
|
margin: 16px 0;
|
||||||
|
:deep(.arco-breadcrumb-item) {
|
||||||
|
color: rgb(var(--gray-6));
|
||||||
|
&:last-child {
|
||||||
|
color: rgb(var(--gray-8));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
47
src/components/chart/index.vue
Normal file
47
src/components/chart/index.vue
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
<template>
|
||||||
|
<VCharts
|
||||||
|
v-if="renderChart"
|
||||||
|
:option="options"
|
||||||
|
:autoresize="autoResize"
|
||||||
|
:style="{ width, height }"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, nextTick } from 'vue';
|
||||||
|
import VCharts from 'vue-echarts';
|
||||||
|
// import { useAppStore } from '@/store';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
options: {
|
||||||
|
type: Object,
|
||||||
|
default() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
autoResize: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
type: String,
|
||||||
|
default: '100%',
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
type: String,
|
||||||
|
default: '100%',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// const appStore = useAppStore();
|
||||||
|
// const theme = computed(() => {
|
||||||
|
// if (appStore.theme === 'dark') return 'dark';
|
||||||
|
// return '';
|
||||||
|
// });
|
||||||
|
const renderChart = ref(false);
|
||||||
|
// wait container expand
|
||||||
|
nextTick(() => {
|
||||||
|
renderChart.value = true;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less"></style>
|
||||||
16
src/components/footer/index.vue
Normal file
16
src/components/footer/index.vue
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
<template>
|
||||||
|
<a-layout-footer class="footer">Leaser.Work</a-layout-footer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup></script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 40px;
|
||||||
|
color: var(--color-text-2);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
79
src/components/global-setting/block.vue
Normal file
79
src/components/global-setting/block.vue
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
<template>
|
||||||
|
<div class="block">
|
||||||
|
<h5 class="title">{{ title }}</h5>
|
||||||
|
<div v-for="option in options" :key="option.name" class="switch-wrapper">
|
||||||
|
<span>{{ $t(option.name) }}</span>
|
||||||
|
<form-wrapper
|
||||||
|
:type="option.type || 'switch'"
|
||||||
|
:name="option.key"
|
||||||
|
:default-value="option.defaultVal"
|
||||||
|
@input-change="handleChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { PropType } from 'vue';
|
||||||
|
import { useAppStore } from '@/store';
|
||||||
|
import FormWrapper from './form-wrapper.vue';
|
||||||
|
|
||||||
|
interface OptionsProps {
|
||||||
|
name: string;
|
||||||
|
key: string;
|
||||||
|
type?: string;
|
||||||
|
defaultVal?: boolean | string | number;
|
||||||
|
}
|
||||||
|
defineProps({
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
type: Array as PropType<OptionsProps[]>,
|
||||||
|
default() {
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const appStore = useAppStore();
|
||||||
|
const handleChange = async ({
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
}: {
|
||||||
|
key: string;
|
||||||
|
value: unknown;
|
||||||
|
}) => {
|
||||||
|
if (key === 'colorWeak') {
|
||||||
|
document.body.style.filter = value ? 'invert(80%)' : 'none';
|
||||||
|
}
|
||||||
|
if (key === 'menuFromServer' && value) {
|
||||||
|
await appStore.fetchServerMenuConfig();
|
||||||
|
}
|
||||||
|
if (key === 'topMenu') {
|
||||||
|
appStore.updateSettings({
|
||||||
|
menuCollapse: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
appStore.updateSettings({ [key]: value });
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.block {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin: 10px 0;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
39
src/components/global-setting/form-wrapper.vue
Normal file
39
src/components/global-setting/form-wrapper.vue
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
<template>
|
||||||
|
<a-input-number
|
||||||
|
v-if="type === 'number'"
|
||||||
|
:style="{ width: '80px' }"
|
||||||
|
size="small"
|
||||||
|
:default-value="(defaultValue as number)"
|
||||||
|
@change="handleChange"
|
||||||
|
/>
|
||||||
|
<a-switch
|
||||||
|
v-else
|
||||||
|
:default-checked="(defaultValue as boolean)"
|
||||||
|
size="small"
|
||||||
|
@change="handleChange"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
const props = defineProps({
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
defaultValue: {
|
||||||
|
type: [String, Boolean, Number],
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const emit = defineEmits(['inputChange']);
|
||||||
|
const handleChange = (value: unknown) => {
|
||||||
|
emit('inputChange', {
|
||||||
|
value,
|
||||||
|
key: props.name,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
98
src/components/global-setting/index.vue
Normal file
98
src/components/global-setting/index.vue
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
<template>
|
||||||
|
<div v-if="!appStore.navbar" class="fixed-settings" @click="setVisible">
|
||||||
|
<a-button type="primary">
|
||||||
|
<template #icon>
|
||||||
|
<icon-settings />
|
||||||
|
</template>
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
<a-drawer
|
||||||
|
:width="300"
|
||||||
|
unmount-on-close
|
||||||
|
:visible="visible"
|
||||||
|
:cancel-text="$t('settings.close')"
|
||||||
|
:ok-text="$t('settings.copySettings')"
|
||||||
|
@ok="copySettings"
|
||||||
|
@cancel="cancel"
|
||||||
|
>
|
||||||
|
<template #title> {{ $t('settings.title') }} </template>
|
||||||
|
<Block :options="contentOpts" :title="$t('settings.content')" />
|
||||||
|
<Block :options="othersOpts" :title="$t('settings.otherSettings')" />
|
||||||
|
<a-alert>{{ $t('settings.alertContent') }}</a-alert>
|
||||||
|
</a-drawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { Message } from '@arco-design/web-vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useClipboard } from '@vueuse/core';
|
||||||
|
import { useAppStore } from '@/store';
|
||||||
|
import Block from './block.vue';
|
||||||
|
|
||||||
|
const emit = defineEmits(['cancel']);
|
||||||
|
|
||||||
|
const appStore = useAppStore();
|
||||||
|
const { t } = useI18n();
|
||||||
|
const { copy } = useClipboard();
|
||||||
|
const visible = computed(() => appStore.globalSettings);
|
||||||
|
const contentOpts = computed(() => [
|
||||||
|
{ name: 'settings.navbar', key: 'navbar', defaultVal: appStore.navbar },
|
||||||
|
{
|
||||||
|
name: 'settings.menu',
|
||||||
|
key: 'menu',
|
||||||
|
defaultVal: appStore.menu,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'settings.topMenu',
|
||||||
|
key: 'topMenu',
|
||||||
|
defaultVal: appStore.topMenu,
|
||||||
|
},
|
||||||
|
{ name: 'settings.footer', key: 'footer', defaultVal: appStore.footer },
|
||||||
|
{ name: 'settings.tabBar', key: 'tabBar', defaultVal: appStore.tabBar },
|
||||||
|
{
|
||||||
|
name: 'settings.menuFromServer',
|
||||||
|
key: 'menuFromServer',
|
||||||
|
defaultVal: appStore.menuFromServer,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'settings.menuWidth',
|
||||||
|
key: 'menuWidth',
|
||||||
|
defaultVal: appStore.menuWidth,
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const othersOpts = computed(() => [
|
||||||
|
{
|
||||||
|
name: 'settings.colorWeak',
|
||||||
|
key: 'colorWeak',
|
||||||
|
defaultVal: appStore.colorWeak,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const cancel = () => {
|
||||||
|
appStore.updateSettings({ globalSettings: false });
|
||||||
|
emit('cancel');
|
||||||
|
};
|
||||||
|
const copySettings = async () => {
|
||||||
|
const text = JSON.stringify(appStore.$state, null, 2);
|
||||||
|
await copy(text);
|
||||||
|
Message.success(t('settings.copySettings.message'));
|
||||||
|
};
|
||||||
|
const setVisible = () => {
|
||||||
|
appStore.updateSettings({ globalSettings: true });
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.fixed-settings {
|
||||||
|
position: fixed;
|
||||||
|
top: 280px;
|
||||||
|
right: 0;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
font-size: 18px;
|
||||||
|
vertical-align: -4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
35
src/components/index.ts
Normal file
35
src/components/index.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { App } from 'vue';
|
||||||
|
import { use } from 'echarts/core';
|
||||||
|
import { CanvasRenderer } from 'echarts/renderers';
|
||||||
|
import { BarChart, LineChart, PieChart, RadarChart } from 'echarts/charts';
|
||||||
|
import {
|
||||||
|
GridComponent,
|
||||||
|
TooltipComponent,
|
||||||
|
LegendComponent,
|
||||||
|
DataZoomComponent,
|
||||||
|
GraphicComponent,
|
||||||
|
} from 'echarts/components';
|
||||||
|
import Chart from './chart/index.vue';
|
||||||
|
import Breadcrumb from './breadcrumb/index.vue';
|
||||||
|
|
||||||
|
// Manually introduce ECharts modules to reduce packing size
|
||||||
|
|
||||||
|
use([
|
||||||
|
CanvasRenderer,
|
||||||
|
BarChart,
|
||||||
|
LineChart,
|
||||||
|
PieChart,
|
||||||
|
RadarChart,
|
||||||
|
GridComponent,
|
||||||
|
TooltipComponent,
|
||||||
|
LegendComponent,
|
||||||
|
DataZoomComponent,
|
||||||
|
GraphicComponent,
|
||||||
|
]);
|
||||||
|
|
||||||
|
export default {
|
||||||
|
install(Vue: App) {
|
||||||
|
Vue.component('Chart', Chart);
|
||||||
|
Vue.component('Breadcrumb', Breadcrumb);
|
||||||
|
},
|
||||||
|
};
|
||||||
162
src/components/menu/index.vue
Normal file
162
src/components/menu/index.vue
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
<script lang="tsx">
|
||||||
|
import { defineComponent, ref, h, compile, computed } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useRoute, useRouter, RouteRecordRaw } from 'vue-router';
|
||||||
|
import type { RouteMeta } from 'vue-router';
|
||||||
|
import { useAppStore } from '@/store';
|
||||||
|
import { listenerRouteChange } from '@/utils/route-listener';
|
||||||
|
import { openWindow, regexUrl } from '@/utils';
|
||||||
|
import useMenuTree from './use-menu-tree';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
emit: ['collapse'],
|
||||||
|
setup() {
|
||||||
|
const { t } = useI18n();
|
||||||
|
const appStore = useAppStore();
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
const { menuTree } = useMenuTree();
|
||||||
|
const collapsed = computed({
|
||||||
|
get() {
|
||||||
|
if (appStore.device === 'desktop') return appStore.menuCollapse;
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
set(value: boolean) {
|
||||||
|
appStore.updateSettings({ menuCollapse: value });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const topMenu = computed(() => appStore.topMenu);
|
||||||
|
const openKeys = ref<string[]>([]);
|
||||||
|
const selectedKey = ref<string[]>([]);
|
||||||
|
|
||||||
|
const goto = (item: RouteRecordRaw) => {
|
||||||
|
// Open external link
|
||||||
|
if (regexUrl.test(item.path)) {
|
||||||
|
openWindow(item.path);
|
||||||
|
selectedKey.value = [item.name as string];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Eliminate external link side effects
|
||||||
|
const { hideInMenu, activeMenu } = item.meta as RouteMeta;
|
||||||
|
if (route.name === item.name && !hideInMenu && !activeMenu) {
|
||||||
|
selectedKey.value = [item.name as string];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Trigger router change
|
||||||
|
router.push({
|
||||||
|
name: item.name,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const findMenuOpenKeys = (target: string) => {
|
||||||
|
const result: string[] = [];
|
||||||
|
let isFind = false;
|
||||||
|
const backtrack = (item: RouteRecordRaw, keys: string[]) => {
|
||||||
|
if (item.name === target) {
|
||||||
|
isFind = true;
|
||||||
|
result.push(...keys);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (item.children?.length) {
|
||||||
|
item.children.forEach((el) => {
|
||||||
|
backtrack(el, [...keys, el.name as string]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
menuTree.value.forEach((el: RouteRecordRaw) => {
|
||||||
|
if (isFind) return; // Performance optimization
|
||||||
|
backtrack(el, [el.name as string]);
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
listenerRouteChange((newRoute) => {
|
||||||
|
const { requiresAuth, activeMenu, hideInMenu } = newRoute.meta;
|
||||||
|
if (requiresAuth && (!hideInMenu || activeMenu)) {
|
||||||
|
const menuOpenKeys = findMenuOpenKeys(
|
||||||
|
(activeMenu || newRoute.name) as string
|
||||||
|
);
|
||||||
|
|
||||||
|
const keySet = new Set([...menuOpenKeys, ...openKeys.value]);
|
||||||
|
openKeys.value = [...keySet];
|
||||||
|
|
||||||
|
// 修复选中逻辑:优先使用 activeMenu,否则使用当前路由名称
|
||||||
|
const targetKey = activeMenu || newRoute.name;
|
||||||
|
if (targetKey) {
|
||||||
|
selectedKey.value = [targetKey as string];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
const setCollapse = (val: boolean) => {
|
||||||
|
if (appStore.device === 'desktop')
|
||||||
|
appStore.updateSettings({ menuCollapse: val });
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderSubMenu = () => {
|
||||||
|
function travel(_route: RouteRecordRaw[], nodes = []) {
|
||||||
|
if (_route) {
|
||||||
|
_route.forEach((element) => {
|
||||||
|
// This is demo, modify nodes as needed
|
||||||
|
const icon = element?.meta?.icon
|
||||||
|
? () => h(compile(`<${element?.meta?.icon}/>`))
|
||||||
|
: null;
|
||||||
|
const node =
|
||||||
|
element?.children && element?.children.length !== 0 ? (
|
||||||
|
<a-sub-menu
|
||||||
|
key={element?.name}
|
||||||
|
v-slots={{
|
||||||
|
icon,
|
||||||
|
title: () => h(compile(t(element?.meta?.locale || ''))),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{travel(element?.children)}
|
||||||
|
</a-sub-menu>
|
||||||
|
) : (
|
||||||
|
<a-menu-item
|
||||||
|
key={element?.name}
|
||||||
|
v-slots={{ icon }}
|
||||||
|
onClick={() => goto(element)}
|
||||||
|
>
|
||||||
|
{t(element?.meta?.locale || '')}
|
||||||
|
</a-menu-item>
|
||||||
|
);
|
||||||
|
nodes.push(node as never);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
return travel(menuTree.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return () => (
|
||||||
|
<a-menu
|
||||||
|
mode={topMenu.value ? 'horizontal' : 'vertical'}
|
||||||
|
v-model:collapsed={collapsed.value}
|
||||||
|
v-model:open-keys={openKeys.value}
|
||||||
|
show-collapse-button={appStore.device !== 'mobile'}
|
||||||
|
auto-open={true}
|
||||||
|
selected-keys={selectedKey.value}
|
||||||
|
auto-open-selected={true}
|
||||||
|
level-indent={34}
|
||||||
|
style="height: 100%;width:100%;"
|
||||||
|
onCollapse={setCollapse}
|
||||||
|
>
|
||||||
|
{renderSubMenu()}
|
||||||
|
</a-menu>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
:deep(.arco-menu-inner) {
|
||||||
|
.arco-menu-inline-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.arco-icon {
|
||||||
|
&:not(.arco-icon-down) {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
14
src/components/menu/use-menu-tree.ts
Normal file
14
src/components/menu/use-menu-tree.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { RouteRecordRaw } from 'vue-router';
|
||||||
|
import { useAppStore } from '@/store';
|
||||||
|
|
||||||
|
export default function useMenuTree() {
|
||||||
|
const appStore = useAppStore();
|
||||||
|
const menuTree = computed(() => {
|
||||||
|
return appStore.appAsyncMenus;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
menuTree,
|
||||||
|
};
|
||||||
|
}
|
||||||
129
src/components/message-box/index.vue
Normal file
129
src/components/message-box/index.vue
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
<template>
|
||||||
|
<a-spin style="display: block" :loading="loading">
|
||||||
|
<a-tabs v-model:activeKey="messageType" type="rounded" destroy-on-hide>
|
||||||
|
<a-tab-pane v-for="item in tabList" :key="item.key">
|
||||||
|
<template #title>
|
||||||
|
<span> {{ item.title }}{{ formatUnreadLength(item.key) }} </span>
|
||||||
|
</template>
|
||||||
|
<a-result v-if="!renderList.length" status="404">
|
||||||
|
<template #subtitle> {{ $t('messageBox.noContent') }} </template>
|
||||||
|
</a-result>
|
||||||
|
<List
|
||||||
|
:render-list="renderList"
|
||||||
|
:unread-count="unreadCount"
|
||||||
|
@item-click="handleItemClick"
|
||||||
|
/>
|
||||||
|
</a-tab-pane>
|
||||||
|
<template #extra>
|
||||||
|
<a-button type="text" @click="emptyList">
|
||||||
|
{{ $t('messageBox.tab.button') }}
|
||||||
|
</a-button>
|
||||||
|
</template>
|
||||||
|
</a-tabs>
|
||||||
|
</a-spin>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, reactive, toRefs, computed } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import {
|
||||||
|
queryMessageList,
|
||||||
|
setMessageStatus,
|
||||||
|
MessageRecord,
|
||||||
|
MessageListType,
|
||||||
|
} from '@/api/message';
|
||||||
|
import useLoading from '@/hooks/loading';
|
||||||
|
import List from './list.vue';
|
||||||
|
|
||||||
|
interface TabItem {
|
||||||
|
key: string;
|
||||||
|
title: string;
|
||||||
|
avatar?: string;
|
||||||
|
}
|
||||||
|
const { loading, setLoading } = useLoading(true);
|
||||||
|
const messageType = ref('message');
|
||||||
|
const { t } = useI18n();
|
||||||
|
const messageData = reactive<{
|
||||||
|
renderList: MessageRecord[];
|
||||||
|
messageList: MessageRecord[];
|
||||||
|
}>({
|
||||||
|
renderList: [],
|
||||||
|
messageList: [],
|
||||||
|
});
|
||||||
|
toRefs(messageData);
|
||||||
|
const tabList: TabItem[] = [
|
||||||
|
{
|
||||||
|
key: 'message',
|
||||||
|
title: t('messageBox.tab.title.message'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'notice',
|
||||||
|
title: t('messageBox.tab.title.notice'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'todo',
|
||||||
|
title: t('messageBox.tab.title.todo'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
async function fetchSourceData() {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const { data } = await queryMessageList();
|
||||||
|
messageData.messageList = data;
|
||||||
|
} catch (err) {
|
||||||
|
// you can report use errorHandler or other
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function readMessage(data: MessageListType) {
|
||||||
|
const ids = data.map((item) => item.id);
|
||||||
|
await setMessageStatus({ ids });
|
||||||
|
fetchSourceData();
|
||||||
|
}
|
||||||
|
const renderList = computed(() => {
|
||||||
|
return messageData.messageList.filter(
|
||||||
|
(item) => messageType.value === item.type
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const unreadCount = computed(() => {
|
||||||
|
return renderList.value.filter((item) => !item.status).length;
|
||||||
|
});
|
||||||
|
const getUnreadList = (type: string) => {
|
||||||
|
const list = messageData.messageList.filter(
|
||||||
|
(item) => item.type === type && !item.status
|
||||||
|
);
|
||||||
|
return list;
|
||||||
|
};
|
||||||
|
const formatUnreadLength = (type: string) => {
|
||||||
|
const list = getUnreadList(type);
|
||||||
|
return list.length ? `(${list.length})` : ``;
|
||||||
|
};
|
||||||
|
const handleItemClick = (items: MessageListType) => {
|
||||||
|
if (renderList.value.length) readMessage([...items]);
|
||||||
|
};
|
||||||
|
const emptyList = () => {
|
||||||
|
messageData.messageList = [];
|
||||||
|
};
|
||||||
|
fetchSourceData();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
:deep(.arco-popover-popup-content) {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.arco-list-item-meta) {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
:deep(.arco-tabs-nav) {
|
||||||
|
padding: 14px 0 12px 16px;
|
||||||
|
border-bottom: 1px solid var(--color-neutral-3);
|
||||||
|
}
|
||||||
|
:deep(.arco-tabs-content) {
|
||||||
|
padding-top: 0;
|
||||||
|
.arco-result-subtitle {
|
||||||
|
color: rgb(var(--gray-6));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
149
src/components/message-box/list.vue
Normal file
149
src/components/message-box/list.vue
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
<template>
|
||||||
|
<a-list :bordered="false">
|
||||||
|
<a-list-item
|
||||||
|
v-for="item in renderList"
|
||||||
|
:key="item.id"
|
||||||
|
action-layout="vertical"
|
||||||
|
:style="{
|
||||||
|
opacity: item.status ? 0.5 : 1,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #extra>
|
||||||
|
<a-tag v-if="item.messageType === 0" color="gray">未开始</a-tag>
|
||||||
|
<a-tag v-else-if="item.messageType === 1" color="green">已开通</a-tag>
|
||||||
|
<a-tag v-else-if="item.messageType === 2" color="blue">进行中</a-tag>
|
||||||
|
<a-tag v-else-if="item.messageType === 3" color="red">即将到期</a-tag>
|
||||||
|
</template>
|
||||||
|
<div class="item-wrap" @click="onItemClick(item)">
|
||||||
|
<a-list-item-meta>
|
||||||
|
<template v-if="item.avatar" #avatar>
|
||||||
|
<a-avatar shape="circle">
|
||||||
|
<img v-if="item.avatar" :src="item.avatar" />
|
||||||
|
<icon-desktop v-else />
|
||||||
|
</a-avatar>
|
||||||
|
</template>
|
||||||
|
<template #title>
|
||||||
|
<a-space :size="4">
|
||||||
|
<span>{{ item.title }}</span>
|
||||||
|
<a-typography-text type="secondary">
|
||||||
|
{{ item.subTitle }}
|
||||||
|
</a-typography-text>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
|
<template #description>
|
||||||
|
<div>
|
||||||
|
<a-typography-paragraph
|
||||||
|
:ellipsis="{
|
||||||
|
rows: 1,
|
||||||
|
}"
|
||||||
|
>{{ item.content }}</a-typography-paragraph
|
||||||
|
>
|
||||||
|
<a-typography-text
|
||||||
|
v-if="item.type === 'message'"
|
||||||
|
class="time-text"
|
||||||
|
>
|
||||||
|
{{ item.time }}
|
||||||
|
</a-typography-text>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</a-list-item-meta>
|
||||||
|
</div>
|
||||||
|
</a-list-item>
|
||||||
|
<template #footer>
|
||||||
|
<a-space
|
||||||
|
fill
|
||||||
|
:size="0"
|
||||||
|
:class="{ 'add-border-top': renderList.length < showMax }"
|
||||||
|
>
|
||||||
|
<div class="footer-wrap">
|
||||||
|
<a-link @click="allRead">{{ $t('messageBox.allRead') }}</a-link>
|
||||||
|
</div>
|
||||||
|
<div class="footer-wrap">
|
||||||
|
<a-link>{{ $t('messageBox.viewMore') }}</a-link>
|
||||||
|
</div>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
|
<div
|
||||||
|
v-if="renderList.length && renderList.length < 3"
|
||||||
|
:style="{ height: (showMax - renderList.length) * 86 + 'px' }"
|
||||||
|
></div>
|
||||||
|
</a-list>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { PropType } from 'vue';
|
||||||
|
import { MessageRecord, MessageListType } from '@/api/message';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
renderList: {
|
||||||
|
type: Array as PropType<MessageListType>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
unreadCount: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const emit = defineEmits(['itemClick']);
|
||||||
|
const allRead = () => {
|
||||||
|
emit('itemClick', [...props.renderList]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onItemClick = (item: MessageRecord) => {
|
||||||
|
if (!item.status) {
|
||||||
|
emit('itemClick', [item]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const showMax = 3;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
:deep(.arco-list) {
|
||||||
|
.arco-list-item {
|
||||||
|
min-height: 86px;
|
||||||
|
border-bottom: 1px solid rgb(var(--gray-3));
|
||||||
|
}
|
||||||
|
.arco-list-item-extra {
|
||||||
|
position: absolute;
|
||||||
|
right: 20px;
|
||||||
|
}
|
||||||
|
.arco-list-item-meta-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.item-wrap {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.time-text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgb(var(--gray-6));
|
||||||
|
}
|
||||||
|
.arco-empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.arco-list-footer {
|
||||||
|
padding: 0;
|
||||||
|
height: 50px;
|
||||||
|
line-height: 50px;
|
||||||
|
border-top: none;
|
||||||
|
.arco-space-item {
|
||||||
|
width: 100%;
|
||||||
|
border-right: 1px solid rgb(var(--gray-3));
|
||||||
|
&:last-child {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.add-border-top {
|
||||||
|
border-top: 1px solid rgb(var(--gray-3));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.footer-wrap {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.arco-typography {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.add-border {
|
||||||
|
border-top: 1px solid rgb(var(--gray-3));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
13
src/components/message-box/locale/en-US.ts
Normal file
13
src/components/message-box/locale/en-US.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
export default {
|
||||||
|
'messageBox.tab.title.message': 'Message',
|
||||||
|
'messageBox.tab.title.notice': 'Notice',
|
||||||
|
'messageBox.tab.title.todo': 'Todo',
|
||||||
|
'messageBox.tab.button': 'empty',
|
||||||
|
'messageBox.allRead': 'All Read',
|
||||||
|
'messageBox.viewMore': 'View More',
|
||||||
|
'messageBox.noContent': 'No Content',
|
||||||
|
'messageBox.switchRoles': 'Switch Roles',
|
||||||
|
'messageBox.userCenter': 'User Center',
|
||||||
|
'messageBox.userSettings': 'User Settings',
|
||||||
|
'messageBox.logout': 'Logout',
|
||||||
|
};
|
||||||
13
src/components/message-box/locale/zh-CN.ts
Normal file
13
src/components/message-box/locale/zh-CN.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
export default {
|
||||||
|
'messageBox.tab.title.message': '消息',
|
||||||
|
'messageBox.tab.title.notice': '通知',
|
||||||
|
'messageBox.tab.title.todo': '待办',
|
||||||
|
'messageBox.tab.button': '清空',
|
||||||
|
'messageBox.allRead': '全部已读',
|
||||||
|
'messageBox.viewMore': '查看更多',
|
||||||
|
'messageBox.noContent': '暂无内容',
|
||||||
|
'messageBox.switchRoles': '切换角色',
|
||||||
|
'messageBox.userCenter': '用户中心',
|
||||||
|
'messageBox.userSettings': '用户设置',
|
||||||
|
'messageBox.logout': '登出登录',
|
||||||
|
};
|
||||||
339
src/components/navbar/index.vue
Normal file
339
src/components/navbar/index.vue
Normal file
|
|
@ -0,0 +1,339 @@
|
||||||
|
<template>
|
||||||
|
<div class="navbar">
|
||||||
|
<div class="left-side">
|
||||||
|
<a-space>
|
||||||
|
<img
|
||||||
|
alt="logo"
|
||||||
|
src="//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/dfdba5317c0c20ce20e64fac803d52bc.svg~tplv-49unhts6dw-image.image"
|
||||||
|
/>
|
||||||
|
<a-typography-title
|
||||||
|
:style="{ margin: 0, fontSize: '18px' }"
|
||||||
|
:heading="5"
|
||||||
|
>
|
||||||
|
Leaser.Work
|
||||||
|
</a-typography-title>
|
||||||
|
<icon-menu-fold
|
||||||
|
v-if="appStore.device === 'mobile'"
|
||||||
|
style="font-size: 22px; cursor: pointer"
|
||||||
|
@click="toggleDrawerMenu"
|
||||||
|
/>
|
||||||
|
</a-space>
|
||||||
|
</div>
|
||||||
|
<div class="center-side">
|
||||||
|
<TopMenu :menus="menus" :selected-key="selectedKey" @menu-click="handleMenuClick" />
|
||||||
|
<!-- <Menu v-if="topMenu" /> -->
|
||||||
|
</div>
|
||||||
|
<ul class="right-side">
|
||||||
|
<li>
|
||||||
|
<a-tooltip :content="$t('settings.search')">
|
||||||
|
<a-button class="nav-btn" type="outline" :shape="'circle'">
|
||||||
|
<template #icon>
|
||||||
|
<icon-search />
|
||||||
|
</template>
|
||||||
|
</a-button>
|
||||||
|
</a-tooltip>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a-tooltip :content="$t('settings.language')">
|
||||||
|
<a-button
|
||||||
|
class="nav-btn"
|
||||||
|
type="outline"
|
||||||
|
:shape="'circle'"
|
||||||
|
@click="setDropDownVisible"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<icon-language />
|
||||||
|
</template>
|
||||||
|
</a-button>
|
||||||
|
</a-tooltip>
|
||||||
|
<a-dropdown trigger="click" @select="changeLocale as any">
|
||||||
|
<div ref="triggerBtn" class="trigger-btn"></div>
|
||||||
|
<template #content>
|
||||||
|
<a-doption
|
||||||
|
v-for="item in locales"
|
||||||
|
:key="item.value"
|
||||||
|
:value="item.value"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<icon-check v-show="item.value === currentLocale" />
|
||||||
|
</template>
|
||||||
|
{{ item.label }}
|
||||||
|
</a-doption>
|
||||||
|
</template>
|
||||||
|
</a-dropdown>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a-tooltip
|
||||||
|
:content="
|
||||||
|
theme === 'light'
|
||||||
|
? $t('settings.navbar.theme.toDark')
|
||||||
|
: $t('settings.navbar.theme.toLight')
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<a-button
|
||||||
|
class="nav-btn"
|
||||||
|
type="outline"
|
||||||
|
:shape="'circle'"
|
||||||
|
@click="handleToggleTheme"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<icon-moon-fill v-if="theme === 'dark'" />
|
||||||
|
<icon-sun-fill v-else />
|
||||||
|
</template>
|
||||||
|
</a-button>
|
||||||
|
</a-tooltip>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a-tooltip :content="$t('settings.navbar.alerts')">
|
||||||
|
<div class="message-box-trigger">
|
||||||
|
<a-badge :count="9" dot>
|
||||||
|
<a-button
|
||||||
|
class="nav-btn"
|
||||||
|
type="outline"
|
||||||
|
:shape="'circle'"
|
||||||
|
@click="setPopoverVisible"
|
||||||
|
>
|
||||||
|
<icon-notification />
|
||||||
|
</a-button>
|
||||||
|
</a-badge>
|
||||||
|
</div>
|
||||||
|
</a-tooltip>
|
||||||
|
<a-popover
|
||||||
|
trigger="click"
|
||||||
|
:arrow-style="{ display: 'none' }"
|
||||||
|
:content-style="{ padding: 0, minWidth: '400px' }"
|
||||||
|
content-class="message-popover"
|
||||||
|
>
|
||||||
|
<div ref="refBtn" class="ref-btn"></div>
|
||||||
|
<template #content>
|
||||||
|
<message-box />
|
||||||
|
</template>
|
||||||
|
</a-popover>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a-tooltip
|
||||||
|
:content="
|
||||||
|
isFullscreen
|
||||||
|
? $t('settings.navbar.screen.toExit')
|
||||||
|
: $t('settings.navbar.screen.toFull')
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<a-button
|
||||||
|
class="nav-btn"
|
||||||
|
type="outline"
|
||||||
|
:shape="'circle'"
|
||||||
|
@click="toggleFullScreen"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<icon-fullscreen-exit v-if="isFullscreen" />
|
||||||
|
<icon-fullscreen v-else />
|
||||||
|
</template>
|
||||||
|
</a-button>
|
||||||
|
</a-tooltip>
|
||||||
|
</li>
|
||||||
|
<!-- 页面配置 -->
|
||||||
|
<!-- <li>
|
||||||
|
<a-tooltip :content="$t('settings.title')">
|
||||||
|
<a-button
|
||||||
|
class="nav-btn"
|
||||||
|
type="outline"
|
||||||
|
:shape="'circle'"
|
||||||
|
@click="setVisible"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<icon-settings />
|
||||||
|
</template>
|
||||||
|
</a-button>
|
||||||
|
</a-tooltip>
|
||||||
|
</li> -->
|
||||||
|
<li>
|
||||||
|
<a-dropdown trigger="click">
|
||||||
|
<a-avatar
|
||||||
|
:size="32"
|
||||||
|
:style="{ marginRight: '8px', cursor: 'pointer' }"
|
||||||
|
>
|
||||||
|
<img alt="avatar" :src="avatar" />
|
||||||
|
</a-avatar>
|
||||||
|
<template #content>
|
||||||
|
<a-doption>
|
||||||
|
<a-space @click="switchRoles">
|
||||||
|
<icon-tag />
|
||||||
|
<span>
|
||||||
|
{{ $t('messageBox.switchRoles') }}
|
||||||
|
</span>
|
||||||
|
</a-space>
|
||||||
|
</a-doption>
|
||||||
|
<a-doption>
|
||||||
|
<a-space @click="$router.push({ name: 'Info' })">
|
||||||
|
<icon-user />
|
||||||
|
<span>
|
||||||
|
{{ $t('messageBox.userCenter') }}
|
||||||
|
</span>
|
||||||
|
</a-space>
|
||||||
|
</a-doption>
|
||||||
|
<a-doption>
|
||||||
|
<a-space @click="$router.push({ name: 'Setting' })">
|
||||||
|
<icon-settings />
|
||||||
|
<span>
|
||||||
|
{{ $t('messageBox.userSettings') }}
|
||||||
|
</span>
|
||||||
|
</a-space>
|
||||||
|
</a-doption>
|
||||||
|
<a-doption>
|
||||||
|
<a-space @click="handleLogout">
|
||||||
|
<icon-export />
|
||||||
|
<span>
|
||||||
|
{{ $t('messageBox.logout') }}
|
||||||
|
</span>
|
||||||
|
</a-space>
|
||||||
|
</a-doption>
|
||||||
|
</template>
|
||||||
|
</a-dropdown>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, ref, inject } from 'vue';
|
||||||
|
import { Message } from '@arco-design/web-vue';
|
||||||
|
import { useDark, useToggle, useFullscreen } from '@vueuse/core';
|
||||||
|
import { useAppStore, useUserStore } from '@/store';
|
||||||
|
import { LOCALE_OPTIONS } from '@/locale';
|
||||||
|
import useLocale from '@/hooks/locale';
|
||||||
|
import useUser from '@/hooks/user';
|
||||||
|
import TopMenu from '@/components/top-menu/index.vue';
|
||||||
|
// import Menu from '@/components/menu/index.vue';
|
||||||
|
import MessageBox from '../message-box/index.vue';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
menus: any[],
|
||||||
|
selectedKey: string,
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits(['menu-click']);
|
||||||
|
const handleMenuClick = (key: string) => {
|
||||||
|
emit('menu-click', key);
|
||||||
|
};
|
||||||
|
|
||||||
|
const appStore = useAppStore();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const { logout } = useUser();
|
||||||
|
const { changeLocale, currentLocale } = useLocale();
|
||||||
|
const { isFullscreen, toggle: toggleFullScreen } = useFullscreen();
|
||||||
|
const locales = [...LOCALE_OPTIONS];
|
||||||
|
const avatar = computed(() => {
|
||||||
|
return userStore.avatar;
|
||||||
|
});
|
||||||
|
const theme = computed(() => {
|
||||||
|
return appStore.theme;
|
||||||
|
});
|
||||||
|
// const topMenu = computed(() => appStore.topMenu && appStore.menu);
|
||||||
|
const isDark = useDark({
|
||||||
|
selector: 'body',
|
||||||
|
attribute: 'arco-theme',
|
||||||
|
valueDark: 'dark',
|
||||||
|
valueLight: 'light',
|
||||||
|
storageKey: 'arco-theme',
|
||||||
|
onChanged(dark: boolean) {
|
||||||
|
// overridden default behavior
|
||||||
|
appStore.toggleTheme(dark);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const toggleTheme = useToggle(isDark);
|
||||||
|
const handleToggleTheme = () => {
|
||||||
|
toggleTheme();
|
||||||
|
};
|
||||||
|
const setVisible = () => {
|
||||||
|
appStore.updateSettings({ globalSettings: true });
|
||||||
|
};
|
||||||
|
const refBtn = ref();
|
||||||
|
const triggerBtn = ref();
|
||||||
|
const setPopoverVisible = () => {
|
||||||
|
const event = new MouseEvent('click', {
|
||||||
|
view: window,
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
});
|
||||||
|
refBtn.value.dispatchEvent(event);
|
||||||
|
};
|
||||||
|
const handleLogout = () => {
|
||||||
|
logout();
|
||||||
|
};
|
||||||
|
const setDropDownVisible = () => {
|
||||||
|
const event = new MouseEvent('click', {
|
||||||
|
view: window,
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
});
|
||||||
|
triggerBtn.value.dispatchEvent(event);
|
||||||
|
};
|
||||||
|
const switchRoles = async () => {
|
||||||
|
const res = await userStore.switchRoles();
|
||||||
|
Message.success(res as string);
|
||||||
|
};
|
||||||
|
const toggleDrawerMenu = inject('toggleDrawerMenu') as () => void;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.navbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--color-bg-2);
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-side {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-side {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-side {
|
||||||
|
display: flex;
|
||||||
|
padding-right: 20px;
|
||||||
|
list-style: none;
|
||||||
|
:deep(.locale-select) {
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--color-text-1);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.nav-btn {
|
||||||
|
border-color: rgb(var(--gray-2));
|
||||||
|
color: rgb(var(--gray-8));
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.trigger-btn,
|
||||||
|
.ref-btn {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 14px;
|
||||||
|
}
|
||||||
|
.trigger-btn {
|
||||||
|
margin-left: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
.message-popover {
|
||||||
|
.arco-popover-content {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
101
src/components/tab-bar/index.vue
Normal file
101
src/components/tab-bar/index.vue
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
<template>
|
||||||
|
<div class="tab-bar-container">
|
||||||
|
<a-affix ref="affixRef" :offset-top="offsetTop">
|
||||||
|
<div class="tab-bar-box">
|
||||||
|
<div class="tab-bar-scroll">
|
||||||
|
<div class="tags-wrap">
|
||||||
|
<tab-item
|
||||||
|
v-for="(tag, index) in tagList"
|
||||||
|
:key="tag.fullPath"
|
||||||
|
:index="index"
|
||||||
|
:item-data="tag"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tag-bar-operation"></div>
|
||||||
|
</div>
|
||||||
|
</a-affix>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, computed, watch, onUnmounted } from 'vue';
|
||||||
|
import type { RouteLocationNormalized } from 'vue-router';
|
||||||
|
import {
|
||||||
|
listenerRouteChange,
|
||||||
|
removeRouteListener,
|
||||||
|
} from '@/utils/route-listener';
|
||||||
|
import { useAppStore, useTabBarStore } from '@/store';
|
||||||
|
import tabItem from './tab-item.vue';
|
||||||
|
|
||||||
|
const appStore = useAppStore();
|
||||||
|
const tabBarStore = useTabBarStore();
|
||||||
|
|
||||||
|
const affixRef = ref();
|
||||||
|
const tagList = computed(() => {
|
||||||
|
return tabBarStore.getTabList;
|
||||||
|
});
|
||||||
|
const offsetTop = computed(() => {
|
||||||
|
return appStore.navbar ? 60 : 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => appStore.navbar,
|
||||||
|
() => {
|
||||||
|
affixRef.value.updatePosition();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
listenerRouteChange((route: RouteLocationNormalized) => {
|
||||||
|
if (
|
||||||
|
!route.meta.noAffix &&
|
||||||
|
!tagList.value.some((tag) => tag.fullPath === route.fullPath)
|
||||||
|
) {
|
||||||
|
tabBarStore.updateTabList(route);
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
removeRouteListener();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.tab-bar-container {
|
||||||
|
position: relative;
|
||||||
|
background-color: var(--color-bg-2);
|
||||||
|
.tab-bar-box {
|
||||||
|
display: flex;
|
||||||
|
padding: 0 0 0 20px;
|
||||||
|
background-color: var(--color-bg-2);
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
.tab-bar-scroll {
|
||||||
|
height: 32px;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
.tags-wrap {
|
||||||
|
padding: 4px 0;
|
||||||
|
height: 48px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow-x: auto;
|
||||||
|
|
||||||
|
:deep(.arco-tag) {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
&:first-child {
|
||||||
|
.arco-tag-close-btn {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-bar-operation {
|
||||||
|
width: 100px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
12
src/components/tab-bar/readme.md
Normal file
12
src/components/tab-bar/readme.md
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
## 组件说明
|
||||||
|
|
||||||
|
该组件非官方最终设计规范,以单独组件存在。
|
||||||
|
|
||||||
|
同时仅仅提供最基本的功能,后续进行优化及更改。
|
||||||
|
|
||||||
|
|
||||||
|
## Component description
|
||||||
|
|
||||||
|
The component unofficial final design specification exists as a separate component.
|
||||||
|
|
||||||
|
At the same time, only the most basic functions are provided, and subsequent optimizations and changes will be made.
|
||||||
200
src/components/tab-bar/tab-item.vue
Normal file
200
src/components/tab-bar/tab-item.vue
Normal file
|
|
@ -0,0 +1,200 @@
|
||||||
|
<template>
|
||||||
|
<a-dropdown
|
||||||
|
trigger="contextMenu"
|
||||||
|
:popup-max-height="false"
|
||||||
|
@select="actionSelect"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="arco-tag arco-tag-size-medium arco-tag-checked"
|
||||||
|
:class="{ 'link-activated': itemData.fullPath === $route.fullPath }"
|
||||||
|
@click="goto(itemData)"
|
||||||
|
>
|
||||||
|
<span class="tag-link">
|
||||||
|
{{ $t(itemData.title) }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="arco-icon-hover arco-tag-icon-hover arco-icon-hover-size-medium arco-tag-close-btn"
|
||||||
|
@click.stop="tagClose(itemData, index)"
|
||||||
|
>
|
||||||
|
<icon-close />
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<template #content>
|
||||||
|
<a-doption :disabled="disabledReload" :value="Eaction.reload">
|
||||||
|
<icon-refresh />
|
||||||
|
<span>重新加载</span>
|
||||||
|
</a-doption>
|
||||||
|
<a-doption
|
||||||
|
class="sperate-line"
|
||||||
|
:disabled="disabledCurrent"
|
||||||
|
:value="Eaction.current"
|
||||||
|
>
|
||||||
|
<icon-close />
|
||||||
|
<span>关闭当前标签页</span>
|
||||||
|
</a-doption>
|
||||||
|
<a-doption :disabled="disabledLeft" :value="Eaction.left">
|
||||||
|
<icon-to-left />
|
||||||
|
<span>关闭左侧标签页</span>
|
||||||
|
</a-doption>
|
||||||
|
<a-doption
|
||||||
|
class="sperate-line"
|
||||||
|
:disabled="disabledRight"
|
||||||
|
:value="Eaction.right"
|
||||||
|
>
|
||||||
|
<icon-to-right />
|
||||||
|
<span>关闭右侧标签页</span>
|
||||||
|
</a-doption>
|
||||||
|
<a-doption :value="Eaction.others">
|
||||||
|
<icon-swap />
|
||||||
|
<span>关闭其它标签页</span>
|
||||||
|
</a-doption>
|
||||||
|
<a-doption :value="Eaction.all">
|
||||||
|
<icon-folder-delete />
|
||||||
|
<span>关闭全部标签页</span>
|
||||||
|
</a-doption>
|
||||||
|
</template>
|
||||||
|
</a-dropdown>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { PropType, computed } from 'vue';
|
||||||
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
|
import { useTabBarStore } from '@/store';
|
||||||
|
import type { TagProps } from '@/store/modules/tab-bar/types';
|
||||||
|
import { DEFAULT_ROUTE_NAME, REDIRECT_ROUTE_NAME } from '@/router/constants';
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-shadow
|
||||||
|
enum Eaction {
|
||||||
|
reload = 'reload',
|
||||||
|
current = 'current',
|
||||||
|
left = 'left',
|
||||||
|
right = 'right',
|
||||||
|
others = 'others',
|
||||||
|
all = 'all',
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
itemData: {
|
||||||
|
type: Object as PropType<TagProps>,
|
||||||
|
default() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
index: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
const tabBarStore = useTabBarStore();
|
||||||
|
|
||||||
|
const goto = (tag: TagProps) => {
|
||||||
|
router.push({ ...tag });
|
||||||
|
};
|
||||||
|
const tagList = computed(() => {
|
||||||
|
return tabBarStore.getTabList;
|
||||||
|
});
|
||||||
|
|
||||||
|
const disabledReload = computed(() => {
|
||||||
|
return props.itemData.fullPath !== route.fullPath;
|
||||||
|
});
|
||||||
|
|
||||||
|
const disabledCurrent = computed(() => {
|
||||||
|
return props.index === 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
const disabledLeft = computed(() => {
|
||||||
|
return [0, 1].includes(props.index);
|
||||||
|
});
|
||||||
|
|
||||||
|
const disabledRight = computed(() => {
|
||||||
|
return props.index === tagList.value.length - 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
const tagClose = (tag: TagProps, idx: number) => {
|
||||||
|
tabBarStore.deleteTag(idx, tag);
|
||||||
|
if (props.itemData.fullPath === route.fullPath) {
|
||||||
|
const latest = tagList.value[idx - 1]; // 获取队列的前一个tab
|
||||||
|
router.push({ name: latest.name });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findCurrentRouteIndex = () => {
|
||||||
|
return tagList.value.findIndex((el) => el.fullPath === route.fullPath);
|
||||||
|
};
|
||||||
|
const actionSelect = async (value: any) => {
|
||||||
|
const { itemData, index } = props;
|
||||||
|
const copyTagList = [...tagList.value];
|
||||||
|
if (value === Eaction.current) {
|
||||||
|
tagClose(itemData, index);
|
||||||
|
} else if (value === Eaction.left) {
|
||||||
|
const currentRouteIdx = findCurrentRouteIndex();
|
||||||
|
copyTagList.splice(1, props.index - 1);
|
||||||
|
|
||||||
|
tabBarStore.freshTabList(copyTagList);
|
||||||
|
if (currentRouteIdx < index) {
|
||||||
|
router.push({ name: itemData.name });
|
||||||
|
}
|
||||||
|
} else if (value === Eaction.right) {
|
||||||
|
const currentRouteIdx = findCurrentRouteIndex();
|
||||||
|
copyTagList.splice(props.index + 1);
|
||||||
|
|
||||||
|
tabBarStore.freshTabList(copyTagList);
|
||||||
|
if (currentRouteIdx > index) {
|
||||||
|
router.push({ name: itemData.name });
|
||||||
|
}
|
||||||
|
} else if (value === Eaction.others) {
|
||||||
|
const filterList = tagList.value.filter((el, idx) => {
|
||||||
|
return idx === 0 || idx === props.index;
|
||||||
|
});
|
||||||
|
tabBarStore.freshTabList(filterList);
|
||||||
|
router.push({ name: itemData.name });
|
||||||
|
} else if (value === Eaction.reload) {
|
||||||
|
tabBarStore.deleteCache(itemData);
|
||||||
|
await router.push({
|
||||||
|
name: REDIRECT_ROUTE_NAME,
|
||||||
|
params: {
|
||||||
|
path: route.fullPath,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
tabBarStore.addCache(itemData.name);
|
||||||
|
} else {
|
||||||
|
tabBarStore.resetTabList();
|
||||||
|
router.push({ name: DEFAULT_ROUTE_NAME });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.tag-link {
|
||||||
|
color: var(--color-text-2);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.link-activated {
|
||||||
|
color: rgb(var(--link-6));
|
||||||
|
.tag-link {
|
||||||
|
color: rgb(var(--link-6));
|
||||||
|
}
|
||||||
|
& + .arco-tag-close-btn {
|
||||||
|
color: rgb(var(--link-6));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:deep(.arco-dropdown-option-content) {
|
||||||
|
span {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.arco-dropdown-open {
|
||||||
|
.tag-link {
|
||||||
|
color: rgb(var(--danger-6));
|
||||||
|
}
|
||||||
|
.arco-tag-close-btn {
|
||||||
|
color: rgb(var(--danger-6));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sperate-line {
|
||||||
|
border-bottom: 1px solid var(--color-neutral-3);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
135
src/components/top-menu/index.vue
Normal file
135
src/components/top-menu/index.vue
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
<template>
|
||||||
|
<div class="top-menu">
|
||||||
|
<a-select :style="{ width: '200px' }" placeholder="请选择" :bordered="false">
|
||||||
|
<template #label="{ data }">
|
||||||
|
<div class="current-building">
|
||||||
|
<img class="img" src="@/assets/images/building1.png" alt="">
|
||||||
|
<p class="building-name">星河商务中心</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<a-option>
|
||||||
|
<p class="building-name">星河商务中心</p>
|
||||||
|
</a-option>
|
||||||
|
</a-select>
|
||||||
|
<a-menu :style="{ width: '100%' }" mode="horizontal" :selected-keys="[selectedKey]"
|
||||||
|
@menu-item-click="onMenuClick" style="width: 100%">
|
||||||
|
<a-menu-item v-for="item in topLevelMenus" :key="item.path">
|
||||||
|
{{ t(item.meta?.locale || '') }}
|
||||||
|
</a-menu-item>
|
||||||
|
</a-menu>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import type { RouteRecordNormalized } from 'vue-router';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
// 获取一级路由(顶级菜单)
|
||||||
|
const topLevelMenus = computed(() => {
|
||||||
|
return router.getRoutes()
|
||||||
|
.filter(route => {
|
||||||
|
// 过滤出一级路由:有 meta 且有 children
|
||||||
|
return route.meta &&
|
||||||
|
route.children?.length > 0 &&
|
||||||
|
route.meta.isFirst &&
|
||||||
|
route.meta.locale;
|
||||||
|
})
|
||||||
|
.map(route => ({
|
||||||
|
path: route.path,
|
||||||
|
name: route.name,
|
||||||
|
meta: route.meta
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
const buildingList = ref([
|
||||||
|
{ logo: new URL('@/assets/images/building1.png', import.meta.url).href, title: '星河商务中心', id: 1 },
|
||||||
|
{ logo: new URL('@/assets/images/building1.png', import.meta.url).href, title: '南山科学科学园', id: 2 },
|
||||||
|
{ logo: new URL('@/assets/images/building1.png', import.meta.url).href, title: '金融科技大厦', id: 3 },
|
||||||
|
{ logo: new URL('@/assets/images/building1.png', import.meta.url).href, title: '福田平安大厦', id: 4 },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const currentBuildingId = ref(1);
|
||||||
|
|
||||||
|
const getCurrentBuilding = (id: number) => {
|
||||||
|
return buildingList.value.find(building => building.id === id);
|
||||||
|
};
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
selectedKey: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits(['menu-click']);
|
||||||
|
|
||||||
|
const onMenuClick = (key: string) => {
|
||||||
|
// 查找对应的路由
|
||||||
|
const targetRoute = topLevelMenus.value.find(item => item.path === key);
|
||||||
|
if (targetRoute) {
|
||||||
|
// 跳转到对应路由
|
||||||
|
router.push(targetRoute.path);
|
||||||
|
}
|
||||||
|
// 触发父组件事件
|
||||||
|
emit('menu-click', key);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="less">
|
||||||
|
.top-menu {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.current-building,
|
||||||
|
.building {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.img {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.building-name {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.building-item {
|
||||||
|
.building {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.img {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.building-name {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.arco-select-option-content) {
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
|
||||||
|
.current-building,
|
||||||
|
.building {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.img {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.building-name {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
17
src/config/settings.json
Normal file
17
src/config/settings.json
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"theme": "light",
|
||||||
|
"colorWeak": false,
|
||||||
|
"navbar": true,
|
||||||
|
"menu": true,
|
||||||
|
"topMenu": false,
|
||||||
|
"hideMenu": false,
|
||||||
|
"menuCollapse": false,
|
||||||
|
"footer": true,
|
||||||
|
"themeColor": "#165DFF",
|
||||||
|
"menuWidth": 220,
|
||||||
|
"globalSettings": false,
|
||||||
|
"device": "desktop",
|
||||||
|
"tabBar": false,
|
||||||
|
"menuFromServer": false,
|
||||||
|
"serverMenu": []
|
||||||
|
}
|
||||||
8
src/directive/index.ts
Normal file
8
src/directive/index.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { App } from 'vue';
|
||||||
|
import permission from './permission';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
install(Vue: App) {
|
||||||
|
Vue.directive('permission', permission);
|
||||||
|
},
|
||||||
|
};
|
||||||
30
src/directive/permission/index.ts
Normal file
30
src/directive/permission/index.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { DirectiveBinding } from 'vue';
|
||||||
|
import { useUserStore } from '@/store';
|
||||||
|
|
||||||
|
function checkPermission(el: HTMLElement, binding: DirectiveBinding) {
|
||||||
|
const { value } = binding;
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const { role } = userStore;
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
if (value.length > 0) {
|
||||||
|
const permissionValues = value;
|
||||||
|
|
||||||
|
const hasPermission = permissionValues.includes(role);
|
||||||
|
if (!hasPermission && el.parentNode) {
|
||||||
|
el.parentNode.removeChild(el);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(`need roles! Like v-permission="['admin','user']"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mounted(el: HTMLElement, binding: DirectiveBinding) {
|
||||||
|
checkPermission(el, binding);
|
||||||
|
},
|
||||||
|
updated(el: HTMLElement, binding: DirectiveBinding) {
|
||||||
|
checkPermission(el, binding);
|
||||||
|
},
|
||||||
|
};
|
||||||
11
src/env.d.ts
vendored
Normal file
11
src/env.d.ts
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
declare module '*.vue' {
|
||||||
|
import { DefineComponent } from 'vue';
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
|
||||||
|
const component: DefineComponent<{}, {}, any>;
|
||||||
|
export default component;
|
||||||
|
}
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
readonly VITE_API_BASE_URL: string;
|
||||||
|
}
|
||||||
27
src/hooks/chart-option.ts
Normal file
27
src/hooks/chart-option.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { EChartsOption } from 'echarts';
|
||||||
|
import { useAppStore } from '@/store';
|
||||||
|
|
||||||
|
// for code hints
|
||||||
|
// import { SeriesOption } from 'echarts';
|
||||||
|
// Because there are so many configuration items, this provides a relatively convenient code hint.
|
||||||
|
// When using vue, pay attention to the reactive issues. It is necessary to ensure that corresponding functions can be triggered, TypeScript does not report errors, and code writing is convenient.
|
||||||
|
interface optionsFn {
|
||||||
|
(isDark: boolean): EChartsOption;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useChartOption(sourceOption: optionsFn) {
|
||||||
|
const appStore = useAppStore();
|
||||||
|
const isDark = computed(() => {
|
||||||
|
return appStore.theme === 'dark';
|
||||||
|
});
|
||||||
|
// echarts support https://echarts.apache.org/zh/theme-builder.html
|
||||||
|
// It's not used here
|
||||||
|
// TODO echarts themes
|
||||||
|
const chartOption = computed<EChartsOption>(() => {
|
||||||
|
return sourceOption(isDark.value);
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
chartOption,
|
||||||
|
};
|
||||||
|
}
|
||||||
16
src/hooks/loading.ts
Normal file
16
src/hooks/loading.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
export default function useLoading(initValue = false) {
|
||||||
|
const loading = ref(initValue);
|
||||||
|
const setLoading = (value: boolean) => {
|
||||||
|
loading.value = value;
|
||||||
|
};
|
||||||
|
const toggle = () => {
|
||||||
|
loading.value = !loading.value;
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
loading,
|
||||||
|
setLoading,
|
||||||
|
toggle,
|
||||||
|
};
|
||||||
|
}
|
||||||
22
src/hooks/locale.ts
Normal file
22
src/hooks/locale.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { Message } from '@arco-design/web-vue';
|
||||||
|
|
||||||
|
export default function useLocale() {
|
||||||
|
const i18 = useI18n();
|
||||||
|
const currentLocale = computed(() => {
|
||||||
|
return i18.locale.value;
|
||||||
|
});
|
||||||
|
const changeLocale = (value: string) => {
|
||||||
|
if (i18.locale.value === value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
i18.locale.value = value;
|
||||||
|
localStorage.setItem('arco-locale', value);
|
||||||
|
Message.success(i18.t('navbar.action.locale'));
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
currentLocale,
|
||||||
|
changeLocale,
|
||||||
|
};
|
||||||
|
}
|
||||||
52
src/hooks/permission.ts
Normal file
52
src/hooks/permission.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { RouteLocationNormalized, RouteRecordRaw } from 'vue-router';
|
||||||
|
import { useUserStore } from '@/store';
|
||||||
|
|
||||||
|
export default function usePermission() {
|
||||||
|
const userStore = useUserStore();
|
||||||
|
return {
|
||||||
|
accessRouter(route: RouteLocationNormalized | RouteRecordRaw) {
|
||||||
|
return true
|
||||||
|
// 如果路由不需要认证,直接允许访问
|
||||||
|
// if (!route.meta?.requiresAuth) {
|
||||||
|
// return true;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 如果用户没有权限数据,拒绝访问
|
||||||
|
// if (!userStore.permission || userStore.permission.length === 0) {
|
||||||
|
// return false;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 检查用户是否有该路由的权限
|
||||||
|
// return userStore.permission.some(
|
||||||
|
// (p: any) =>
|
||||||
|
// p.perm_key === route.meta?.permissionKey && p.visible === 1
|
||||||
|
// );
|
||||||
|
},
|
||||||
|
findFirstPermissionRoute(
|
||||||
|
_routers: any,
|
||||||
|
permissions = userStore.permission
|
||||||
|
) {
|
||||||
|
if (!permissions || permissions.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cloneRouters = [..._routers];
|
||||||
|
while (cloneRouters.length) {
|
||||||
|
const firstElement = cloneRouters.shift();
|
||||||
|
|
||||||
|
if (
|
||||||
|
permissions.some(
|
||||||
|
(p: any) => p.perm_key === firstElement.meta?.permissionKey && p.visible === 1
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return { name: firstElement.name };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstElement?.children) {
|
||||||
|
cloneRouters.push(...firstElement.children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
26
src/hooks/request.ts
Normal file
26
src/hooks/request.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { ref, UnwrapRef } from 'vue';
|
||||||
|
import { AxiosResponse } from 'axios';
|
||||||
|
import { HttpResponse } from '@/api/interceptor';
|
||||||
|
import useLoading from './loading';
|
||||||
|
|
||||||
|
// use to fetch list
|
||||||
|
// Don't use async function. It doesn't work in async function.
|
||||||
|
// Use the bind function to add parameters
|
||||||
|
// example: useRequest(api.bind(null, {}))
|
||||||
|
|
||||||
|
export default function useRequest<T>(
|
||||||
|
api: () => Promise<AxiosResponse<HttpResponse>>,
|
||||||
|
defaultValue = [] as unknown as T,
|
||||||
|
isLoading = true
|
||||||
|
) {
|
||||||
|
const { loading, setLoading } = useLoading(isLoading);
|
||||||
|
const response = ref<T>(defaultValue);
|
||||||
|
api()
|
||||||
|
.then((res) => {
|
||||||
|
response.value = res.data as unknown as UnwrapRef<T>;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
return { loading, response };
|
||||||
|
}
|
||||||
32
src/hooks/responsive.ts
Normal file
32
src/hooks/responsive.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { onMounted, onBeforeMount, onBeforeUnmount } from 'vue';
|
||||||
|
import { useDebounceFn } from '@vueuse/core';
|
||||||
|
import { useAppStore } from '@/store';
|
||||||
|
import { addEventListen, removeEventListen } from '@/utils/event';
|
||||||
|
|
||||||
|
const WIDTH = 992; // https://arco.design/vue/component/grid#responsivevalue
|
||||||
|
|
||||||
|
function queryDevice() {
|
||||||
|
const rect = document.body.getBoundingClientRect();
|
||||||
|
return rect.width - 1 < WIDTH;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useResponsive(immediate?: boolean) {
|
||||||
|
const appStore = useAppStore();
|
||||||
|
function resizeHandler() {
|
||||||
|
if (!document.hidden) {
|
||||||
|
const isMobile = queryDevice();
|
||||||
|
appStore.toggleDevice(isMobile ? 'mobile' : 'desktop');
|
||||||
|
appStore.toggleMenu(isMobile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const debounceFn = useDebounceFn(resizeHandler, 100);
|
||||||
|
onMounted(() => {
|
||||||
|
if (immediate) debounceFn();
|
||||||
|
});
|
||||||
|
onBeforeMount(() => {
|
||||||
|
addEventListen(window, 'resize', debounceFn);
|
||||||
|
});
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
removeEventListen(window, 'resize', debounceFn);
|
||||||
|
});
|
||||||
|
}
|
||||||
12
src/hooks/themes.ts
Normal file
12
src/hooks/themes.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useAppStore } from '@/store';
|
||||||
|
|
||||||
|
export default function useThemes() {
|
||||||
|
const appStore = useAppStore();
|
||||||
|
const isDark = computed(() => {
|
||||||
|
return appStore.theme === 'dark';
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
isDark,
|
||||||
|
};
|
||||||
|
}
|
||||||
24
src/hooks/user.ts
Normal file
24
src/hooks/user.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { Message } from '@arco-design/web-vue';
|
||||||
|
|
||||||
|
import { useUserStore } from '@/store';
|
||||||
|
|
||||||
|
export default function useUser() {
|
||||||
|
const router = useRouter();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const logout = async (logoutTo?: string) => {
|
||||||
|
await userStore.logout();
|
||||||
|
const currentRoute = router.currentRoute.value;
|
||||||
|
Message.success('登出成功');
|
||||||
|
router.push({
|
||||||
|
name: logoutTo && typeof logoutTo === 'string' ? logoutTo : 'login',
|
||||||
|
query: {
|
||||||
|
...router.currentRoute.value.query,
|
||||||
|
redirect: currentRoute.name as string,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
logout,
|
||||||
|
};
|
||||||
|
}
|
||||||
16
src/hooks/visible.ts
Normal file
16
src/hooks/visible.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
export default function useVisible(initValue = false) {
|
||||||
|
const visible = ref(initValue);
|
||||||
|
const setVisible = (value: boolean) => {
|
||||||
|
visible.value = value;
|
||||||
|
};
|
||||||
|
const toggle = () => {
|
||||||
|
visible.value = !visible.value;
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
visible,
|
||||||
|
setVisible,
|
||||||
|
toggle,
|
||||||
|
};
|
||||||
|
}
|
||||||
215
src/layout/default-layout.vue
Normal file
215
src/layout/default-layout.vue
Normal file
|
|
@ -0,0 +1,215 @@
|
||||||
|
<template>
|
||||||
|
<a-layout class="layout" :class="{ mobile: appStore.hideMenu }">
|
||||||
|
<div v-if="navbar" class="layout-navbar">
|
||||||
|
<NavBar :menus="topMenus" :selected-key="topSelectedKey" @menu-click="handleMenuClick" />
|
||||||
|
</div>
|
||||||
|
<a-layout>
|
||||||
|
<a-layout>
|
||||||
|
<a-layout-sider
|
||||||
|
v-if="renderMenu"
|
||||||
|
v-show="!hideMenu"
|
||||||
|
class="layout-sider"
|
||||||
|
breakpoint="xl"
|
||||||
|
:collapsed="collapsed"
|
||||||
|
:collapsible="true"
|
||||||
|
:width="menuWidth"
|
||||||
|
:style="{ paddingTop: navbar ? '60px' : '' }"
|
||||||
|
:hide-trigger="true"
|
||||||
|
@collapse="setCollapsed"
|
||||||
|
>
|
||||||
|
<div class="menu-wrapper">
|
||||||
|
<Menu />
|
||||||
|
</div>
|
||||||
|
</a-layout-sider>
|
||||||
|
<a-drawer
|
||||||
|
v-if="hideMenu"
|
||||||
|
:visible="drawerVisible"
|
||||||
|
placement="left"
|
||||||
|
:footer="false"
|
||||||
|
mask-closable
|
||||||
|
:closable="false"
|
||||||
|
@cancel="drawerCancel"
|
||||||
|
>
|
||||||
|
<Menu />
|
||||||
|
</a-drawer>
|
||||||
|
<a-layout class="layout-content" :style="paddingStyle">
|
||||||
|
<TabBar v-if="appStore.tabBar" />
|
||||||
|
<a-layout-content>
|
||||||
|
<PageLayout />
|
||||||
|
</a-layout-content>
|
||||||
|
<Footer v-if="footer" />
|
||||||
|
</a-layout>
|
||||||
|
</a-layout>
|
||||||
|
</a-layout>
|
||||||
|
</a-layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, computed, watch, provide, onMounted } from 'vue';
|
||||||
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
|
import { useAppStore, useUserStore } from '@/store';
|
||||||
|
import NavBar from '@/components/navbar/index.vue';
|
||||||
|
import Menu from '@/components/menu/index.vue';
|
||||||
|
import Footer from '@/components/footer/index.vue';
|
||||||
|
import TabBar from '@/components/tab-bar/index.vue';
|
||||||
|
import usePermission from '@/hooks/permission';
|
||||||
|
import useResponsive from '@/hooks/responsive';
|
||||||
|
import PageLayout from './page-layout.vue';
|
||||||
|
|
||||||
|
const isInit = ref(false);
|
||||||
|
const appStore = useAppStore();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
const permission = usePermission();
|
||||||
|
useResponsive(true);
|
||||||
|
|
||||||
|
// 动态获取顶栏和侧边栏菜单
|
||||||
|
const topMenus = computed(() => {
|
||||||
|
return router.getRoutes().filter(el => el.meta.isFirst && el.children?.length);
|
||||||
|
});
|
||||||
|
const topSelectedKey = ref('');
|
||||||
|
|
||||||
|
const findTopMenu = (currentRoute: any) => {
|
||||||
|
return topMenus.value.find(el => currentRoute.path.startsWith(el.path));
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(() => route.path, (newPath) => {
|
||||||
|
const currentTopMenu = findTopMenu(route);
|
||||||
|
if (currentTopMenu) {
|
||||||
|
topSelectedKey.value = currentTopMenu.path;
|
||||||
|
appStore.updateSettings({ serverMenu: currentTopMenu.children });
|
||||||
|
|
||||||
|
// 确保左侧菜单能正确识别当前路由
|
||||||
|
// 延迟一下,确保菜单数据更新后再触发路由监听
|
||||||
|
setTimeout(() => {
|
||||||
|
// 强制触发菜单组件的路由监听
|
||||||
|
const currentRoute = router.currentRoute.value;
|
||||||
|
if (currentRoute.name) {
|
||||||
|
// 这里可以添加一些调试信息
|
||||||
|
console.log('Current route:', currentRoute.name, 'Path:', currentRoute.path);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
const navbarHeight = `60px`;
|
||||||
|
const navbar = computed(() => appStore.navbar);
|
||||||
|
const renderMenu = computed(() => appStore.menu && !appStore.topMenu);
|
||||||
|
const hideMenu = computed(() => appStore.hideMenu);
|
||||||
|
const footer = computed(() => appStore.footer);
|
||||||
|
const menuWidth = computed(() => {
|
||||||
|
return appStore.menuCollapse ? 48 : appStore.menuWidth;
|
||||||
|
});
|
||||||
|
const collapsed = computed(() => {
|
||||||
|
return appStore.menuCollapse;
|
||||||
|
});
|
||||||
|
const paddingStyle = computed(() => {
|
||||||
|
const paddingLeft =
|
||||||
|
renderMenu.value && !hideMenu.value
|
||||||
|
? { paddingLeft: `${menuWidth.value}px` }
|
||||||
|
: {};
|
||||||
|
const paddingTop = navbar.value ? { paddingTop: navbarHeight } : {};
|
||||||
|
return { ...paddingLeft, ...paddingTop };
|
||||||
|
});
|
||||||
|
const setCollapsed = (val: boolean) => {
|
||||||
|
if (!isInit.value) return; // for page initialization menu state problem
|
||||||
|
appStore.updateSettings({ menuCollapse: val });
|
||||||
|
};
|
||||||
|
watch(
|
||||||
|
() => userStore.role,
|
||||||
|
(roleValue) => {
|
||||||
|
if (roleValue && !permission.accessRouter(route))
|
||||||
|
router.push({ name: 'notFound' });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const drawerVisible = ref(false);
|
||||||
|
const drawerCancel = () => {
|
||||||
|
drawerVisible.value = false;
|
||||||
|
};
|
||||||
|
provide('toggleDrawerMenu', () => {
|
||||||
|
drawerVisible.value = !drawerVisible.value;
|
||||||
|
});
|
||||||
|
const handleMenuClick = (key: string) => {
|
||||||
|
const currentTopMenu = topMenus.value.find(el => el.path === key);
|
||||||
|
if (currentTopMenu?.children?.length) {
|
||||||
|
router.push({ name: currentTopMenu.children[0].name });
|
||||||
|
appStore.updateSettings({ serverMenu: currentTopMenu.children });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
onMounted(() => {
|
||||||
|
isInit.value = true;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
@nav-size-height: 60px;
|
||||||
|
@layout-max-width: 1100px;
|
||||||
|
|
||||||
|
.layout {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-navbar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 100;
|
||||||
|
width: 100%;
|
||||||
|
height: @nav-size-height;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-sider {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 99;
|
||||||
|
height: 100%;
|
||||||
|
transition: all 0.2s cubic-bezier(0.34, 0.69, 0.1, 1);
|
||||||
|
&::after {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: -1px;
|
||||||
|
display: block;
|
||||||
|
width: 1px;
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--color-border);
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
> :deep(.arco-layout-sider-children) {
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-wrapper {
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
:deep(.arco-menu) {
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 12px;
|
||||||
|
height: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
border: 4px solid transparent;
|
||||||
|
background-clip: padding-box;
|
||||||
|
border-radius: 7px;
|
||||||
|
background-color: var(--color-text-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: var(--color-text-3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-content {
|
||||||
|
min-height: 100vh;
|
||||||
|
overflow-y: hidden;
|
||||||
|
background-color: var(--color-fill-2);
|
||||||
|
transition: padding 0.2s cubic-bezier(0.34, 0.69, 0.1, 1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
25
src/layout/page-layout.vue
Normal file
25
src/layout/page-layout.vue
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
<template>
|
||||||
|
<router-view v-slot="{ Component, route }">
|
||||||
|
<transition name="fade" mode="out-in" appear>
|
||||||
|
<component
|
||||||
|
:is="Component"
|
||||||
|
v-if="route.meta.ignoreCache"
|
||||||
|
:key="route.fullPath"
|
||||||
|
/>
|
||||||
|
<keep-alive v-else :include="cacheList">
|
||||||
|
<component :is="Component" :key="route.fullPath" />
|
||||||
|
</keep-alive>
|
||||||
|
</transition>
|
||||||
|
</router-view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useTabBarStore } from '@/store';
|
||||||
|
|
||||||
|
const tabBarStore = useTabBarStore();
|
||||||
|
|
||||||
|
const cacheList = computed(() => tabBarStore.getCacheList);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less"></style>
|
||||||
40
src/locale/en-US.ts
Normal file
40
src/locale/en-US.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import localeMessageBox from '@/components/message-box/locale/en-US';
|
||||||
|
import localeLogin from '@/views/login/locale/en-US';
|
||||||
|
|
||||||
|
import localeWorkplace from '@/views/dashboard/workplace/locale/en-US';
|
||||||
|
|
||||||
|
import localeMonitor from '@/views/dashboard/monitor/locale/en-US';
|
||||||
|
|
||||||
|
import locale403 from '@/views/exception/403/locale/en-US';
|
||||||
|
import locale404 from '@/views/exception/404/locale/en-US';
|
||||||
|
import locale500 from '@/views/exception/500/locale/en-US';
|
||||||
|
|
||||||
|
|
||||||
|
import localeSettings from './en-US/settings';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
'menu.dashboard': 'Dashboard',
|
||||||
|
'menu.server.dashboard': 'Dashboard-Server',
|
||||||
|
'menu.server.workplace': 'Workplace-Server',
|
||||||
|
'menu.server.monitor': 'Monitor-Server',
|
||||||
|
'menu.list': 'List',
|
||||||
|
'menu.result': 'Result',
|
||||||
|
'menu.exception': 'Exception',
|
||||||
|
'menu.form': 'Form',
|
||||||
|
'menu.profile': 'Profile',
|
||||||
|
'menu.visualization': 'Data Visualization',
|
||||||
|
'menu.user': 'User Center',
|
||||||
|
'menu.arcoWebsite': 'Arco Design',
|
||||||
|
'menu.faq': 'FAQ',
|
||||||
|
'navbar.docs': 'Docs',
|
||||||
|
'navbar.action.locale': 'Switch to English',
|
||||||
|
...localeSettings,
|
||||||
|
...localeMessageBox,
|
||||||
|
...localeLogin,
|
||||||
|
...localeWorkplace,
|
||||||
|
|
||||||
|
...localeMonitor,
|
||||||
|
...locale403,
|
||||||
|
...locale404,
|
||||||
|
...locale500,
|
||||||
|
};
|
||||||
29
src/locale/en-US/settings.ts
Normal file
29
src/locale/en-US/settings.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
export default {
|
||||||
|
'settings.title': 'Settings',
|
||||||
|
'settings.themeColor': 'Theme Color',
|
||||||
|
'settings.content': 'Content Setting',
|
||||||
|
'settings.search': 'Search',
|
||||||
|
'settings.language': 'Language',
|
||||||
|
'settings.navbar': 'Navbar',
|
||||||
|
'settings.menuWidth': 'Menu Width (px)',
|
||||||
|
'settings.navbar.theme.toLight': 'Click to use light mode',
|
||||||
|
'settings.navbar.theme.toDark': 'Click to use dark mode',
|
||||||
|
'settings.navbar.screen.toFull': 'Click to switch to full screen mode',
|
||||||
|
'settings.navbar.screen.toExit': 'Click to exit the full screen mode',
|
||||||
|
'settings.navbar.alerts': 'alerts',
|
||||||
|
'settings.menu': 'Menu',
|
||||||
|
'settings.topMenu': 'Top Menu',
|
||||||
|
'settings.tabBar': 'Tab Bar',
|
||||||
|
'settings.footer': 'Footer',
|
||||||
|
'settings.otherSettings': 'Other Settings',
|
||||||
|
'settings.colorWeak': 'Color Weak',
|
||||||
|
'settings.alertContent':
|
||||||
|
'After the configuration is only temporarily effective, if you want to really affect the project, click the "Copy Settings" button below and replace the configuration in settings.json.',
|
||||||
|
'settings.copySettings': 'Copy Settings',
|
||||||
|
'settings.copySettings.message':
|
||||||
|
'Copy succeeded, please paste to file src/settings.json.',
|
||||||
|
'settings.close': 'Close',
|
||||||
|
'settings.color.tooltip':
|
||||||
|
'10 gradient colors generated according to the theme color',
|
||||||
|
'settings.menuFromServer': 'Menu From Server',
|
||||||
|
};
|
||||||
22
src/locale/index.ts
Normal file
22
src/locale/index.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { createI18n } from 'vue-i18n';
|
||||||
|
import en from './en-US';
|
||||||
|
import cn from './zh-CN';
|
||||||
|
|
||||||
|
export const LOCALE_OPTIONS = [
|
||||||
|
{ label: '中文', value: 'zh-CN' },
|
||||||
|
{ label: 'English', value: 'en-US' },
|
||||||
|
];
|
||||||
|
const defaultLocale = localStorage.getItem('arco-locale') || 'zh-CN';
|
||||||
|
|
||||||
|
const i18n = createI18n({
|
||||||
|
locale: defaultLocale,
|
||||||
|
fallbackLocale: 'en-US',
|
||||||
|
legacy: false,
|
||||||
|
allowComposition: true,
|
||||||
|
messages: {
|
||||||
|
'en-US': en,
|
||||||
|
'zh-CN': cn,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default i18n;
|
||||||
57
src/locale/zh-CN.ts
Normal file
57
src/locale/zh-CN.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
import localeMessageBox from '@/components/message-box/locale/zh-CN';
|
||||||
|
import localeLogin from '@/views/login/locale/zh-CN';
|
||||||
|
|
||||||
|
import localeWorkplace from '@/views/dashboard/workplace/locale/zh-CN';
|
||||||
|
|
||||||
|
import localeMonitor from '@/views/dashboard/monitor/locale/zh-CN';
|
||||||
|
|
||||||
|
|
||||||
|
import locale403 from '@/views/exception/403/locale/zh-CN';
|
||||||
|
import locale404 from '@/views/exception/404/locale/zh-CN';
|
||||||
|
import locale500 from '@/views/exception/500/locale/zh-CN';
|
||||||
|
|
||||||
|
|
||||||
|
import localeSettings from './zh-CN/settings';
|
||||||
|
|
||||||
|
import localeBusiness from '@/views/business/locale/zh-CN';
|
||||||
|
import localeFinance from '@/views/finance/locale/zh-CN';
|
||||||
|
import localeProperty from '@/views/property/locale/zh-CN';
|
||||||
|
import localeSetting from '@/views/setting/locale/zh-CN';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
'menu.dashboard': '仪表盘',
|
||||||
|
'menu.server.dashboard': '仪表盘-服务端',
|
||||||
|
'menu.server.workplace': '工作台-服务端',
|
||||||
|
'menu.server.monitor': '实时监控-服务端',
|
||||||
|
'menu.list': '列表页',
|
||||||
|
'menu.result': '结果页',
|
||||||
|
'menu.exception': '异常页',
|
||||||
|
'menu.form': '表单页',
|
||||||
|
'menu.profile': '详情页',
|
||||||
|
'menu.visualization': '数据可视化',
|
||||||
|
'menu.user': '个人中心',
|
||||||
|
'menu.arcoWebsite': 'Arco Design',
|
||||||
|
'menu.faq': '常见问题',
|
||||||
|
'navbar.docs': '文档中心',
|
||||||
|
'navbar.action.locale': '切换为中文',
|
||||||
|
|
||||||
|
'menu.business': '业务',
|
||||||
|
'menu.finance': '财务',
|
||||||
|
'menu.property': '物业',
|
||||||
|
'menu.setting': '设置',
|
||||||
|
|
||||||
|
...localeSettings,
|
||||||
|
...localeMessageBox,
|
||||||
|
...localeLogin,
|
||||||
|
...localeWorkplace,
|
||||||
|
|
||||||
|
...localeMonitor,
|
||||||
|
...locale403,
|
||||||
|
...locale404,
|
||||||
|
...locale500,
|
||||||
|
|
||||||
|
...localeBusiness,
|
||||||
|
...localeFinance,
|
||||||
|
...localeProperty,
|
||||||
|
...localeSetting,
|
||||||
|
};
|
||||||
29
src/locale/zh-CN/settings.ts
Normal file
29
src/locale/zh-CN/settings.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
export default {
|
||||||
|
'settings.title': '页面配置',
|
||||||
|
'settings.themeColor': '主题色',
|
||||||
|
'settings.content': '内容区域',
|
||||||
|
'settings.search': '搜索',
|
||||||
|
'settings.language': '语言',
|
||||||
|
'settings.navbar': '导航栏',
|
||||||
|
'settings.menuWidth': '菜单宽度 (px)',
|
||||||
|
'settings.navbar.theme.toLight': '点击切换为亮色模式',
|
||||||
|
'settings.navbar.theme.toDark': '点击切换为暗黑模式',
|
||||||
|
'settings.navbar.screen.toFull': '点击切换全屏模式',
|
||||||
|
'settings.navbar.screen.toExit': '点击退出全屏模式',
|
||||||
|
'settings.navbar.alerts': '消息通知',
|
||||||
|
'settings.menu': '菜单栏',
|
||||||
|
'settings.topMenu': '顶部菜单栏',
|
||||||
|
'settings.tabBar': '多页签',
|
||||||
|
'settings.footer': '底部',
|
||||||
|
'settings.otherSettings': '其他设置',
|
||||||
|
'settings.colorWeak': '色弱模式',
|
||||||
|
'settings.alertContent':
|
||||||
|
'配置之后仅是临时生效,要想真正作用于项目,点击下方的 "复制配置" 按钮,将配置替换到 settings.json 中即可。',
|
||||||
|
'settings.copySettings': '复制配置',
|
||||||
|
'settings.copySettings.message':
|
||||||
|
'复制成功,请粘贴到 src/settings.json 文件中',
|
||||||
|
'settings.close': '关闭',
|
||||||
|
'settings.color.tooltip':
|
||||||
|
'根据主题颜色生成的 10 个梯度色(将配置复制到项目中,主题色才能对亮色 / 暗黑模式同时生效)',
|
||||||
|
'settings.menuFromServer': '菜单来源于后台',
|
||||||
|
};
|
||||||
27
src/main.ts
Normal file
27
src/main.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { createApp } from 'vue';
|
||||||
|
import ArcoVue from '@arco-design/web-vue';
|
||||||
|
import ArcoVueIcon from '@arco-design/web-vue/es/icon';
|
||||||
|
import globalComponents from '@/components';
|
||||||
|
import router from './router';
|
||||||
|
import store from './store';
|
||||||
|
import i18n from './locale';
|
||||||
|
import directive from './directive';
|
||||||
|
import App from './App.vue';
|
||||||
|
// Styles are imported via arco-plugin. See config/plugin/arcoStyleImport.ts in the directory for details
|
||||||
|
// 样式通过 arco-plugin 插件导入。详见目录文件 config/plugin/arcoStyleImport.ts
|
||||||
|
// https://arco.design/docs/designlab/use-theme-package
|
||||||
|
import '@/assets/style/global.less';
|
||||||
|
import '@/api/interceptor';
|
||||||
|
|
||||||
|
const app = createApp(App);
|
||||||
|
|
||||||
|
app.use(ArcoVue, {});
|
||||||
|
app.use(ArcoVueIcon);
|
||||||
|
|
||||||
|
app.use(router);
|
||||||
|
app.use(store);
|
||||||
|
app.use(i18n);
|
||||||
|
app.use(globalComponents);
|
||||||
|
app.use(directive);
|
||||||
|
|
||||||
|
app.mount('#app');
|
||||||
16
src/router/app-menus/index.ts
Normal file
16
src/router/app-menus/index.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { appRoutes, appExternalRoutes } from '../routes';
|
||||||
|
|
||||||
|
const mixinRoutes = [...appRoutes, ...appExternalRoutes];
|
||||||
|
|
||||||
|
const appClientMenus = mixinRoutes.map((el) => {
|
||||||
|
const { name, path, meta, redirect, children } = el;
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
path,
|
||||||
|
meta,
|
||||||
|
redirect,
|
||||||
|
children,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export default appClientMenus;
|
||||||
18
src/router/constants.ts
Normal file
18
src/router/constants.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
export const WHITE_LIST = [
|
||||||
|
{ name: 'notFound', children: [] },
|
||||||
|
{ name: 'login', children: [] },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const NOT_FOUND = {
|
||||||
|
name: 'notFound',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const REDIRECT_ROUTE_NAME = 'Redirect';
|
||||||
|
|
||||||
|
export const DEFAULT_ROUTE_NAME = 'business';
|
||||||
|
|
||||||
|
export const DEFAULT_ROUTE = {
|
||||||
|
title: 'menu.business.project',
|
||||||
|
name: DEFAULT_ROUTE_NAME,
|
||||||
|
fullPath: '/business/project',
|
||||||
|
};
|
||||||
17
src/router/guard/index.ts
Normal file
17
src/router/guard/index.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import type { Router } from 'vue-router';
|
||||||
|
import { setRouteEmitter } from '@/utils/route-listener';
|
||||||
|
import setupUserLoginInfoGuard from './userLoginInfo';
|
||||||
|
import setupPermissionGuard from './permission';
|
||||||
|
|
||||||
|
function setupPageGuard(router: Router) {
|
||||||
|
router.beforeEach(async (to) => {
|
||||||
|
// emit route change
|
||||||
|
setRouteEmitter(to);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function createRouteGuard(router: Router) {
|
||||||
|
setupPageGuard(router);
|
||||||
|
setupUserLoginInfoGuard(router);
|
||||||
|
setupPermissionGuard(router);
|
||||||
|
}
|
||||||
55
src/router/guard/permission.ts
Normal file
55
src/router/guard/permission.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
import type { Router, RouteRecordNormalized } from 'vue-router';
|
||||||
|
import NProgress from 'nprogress'; // progress bar
|
||||||
|
|
||||||
|
import usePermission from '@/hooks/permission';
|
||||||
|
import { useUserStore, useAppStore } from '@/store';
|
||||||
|
import { appRoutes } from '../routes';
|
||||||
|
import { WHITE_LIST, NOT_FOUND } from '../constants';
|
||||||
|
|
||||||
|
export default function setupPermissionGuard(router: Router) {
|
||||||
|
router.beforeEach(async (to, from, next) => {
|
||||||
|
const appStore = useAppStore();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const Permission = usePermission();
|
||||||
|
const permissionsAllow = Permission.accessRouter(to);
|
||||||
|
if (appStore.menuFromServer) {
|
||||||
|
// 针对来自服务端的菜单配置进行处理
|
||||||
|
// Handle routing configuration from the server
|
||||||
|
|
||||||
|
// 根据需要自行完善来源于服务端的菜单配置的permission逻辑
|
||||||
|
// Refine the permission logic from the server's menu configuration as needed
|
||||||
|
if (
|
||||||
|
!appStore.appAsyncMenus.length &&
|
||||||
|
!WHITE_LIST.find((el) => el.name === to.name)
|
||||||
|
) {
|
||||||
|
await appStore.fetchServerMenuConfig();
|
||||||
|
}
|
||||||
|
const serverMenuConfig = [...appStore.appAsyncMenus, ...WHITE_LIST];
|
||||||
|
|
||||||
|
let exist = false;
|
||||||
|
while (serverMenuConfig.length && !exist) {
|
||||||
|
const element = serverMenuConfig.shift();
|
||||||
|
if (element?.name === to.name) exist = true;
|
||||||
|
|
||||||
|
if (element?.children) {
|
||||||
|
serverMenuConfig.push(
|
||||||
|
...(element.children as unknown as RouteRecordNormalized[])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (exist && permissionsAllow) {
|
||||||
|
next();
|
||||||
|
} else next(NOT_FOUND);
|
||||||
|
} else {
|
||||||
|
// eslint-disable-next-line no-lonely-if
|
||||||
|
if (permissionsAllow) next();
|
||||||
|
else {
|
||||||
|
const destination =
|
||||||
|
Permission.findFirstPermissionRoute(appRoutes, userStore.role) ||
|
||||||
|
NOT_FOUND;
|
||||||
|
next(destination);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NProgress.done();
|
||||||
|
});
|
||||||
|
}
|
||||||
43
src/router/guard/userLoginInfo.ts
Normal file
43
src/router/guard/userLoginInfo.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import type { Router, LocationQueryRaw } from 'vue-router';
|
||||||
|
import NProgress from 'nprogress'; // progress bar
|
||||||
|
|
||||||
|
import { useUserStore } from '@/store';
|
||||||
|
import { isLogin } from '@/utils/auth';
|
||||||
|
|
||||||
|
export default function setupUserLoginInfoGuard(router: Router) {
|
||||||
|
router.beforeEach(async (to, from, next) => {
|
||||||
|
NProgress.start();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
if (isLogin()) {
|
||||||
|
if (userStore.role) {
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
await userStore.info();
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
await userStore.logout();
|
||||||
|
next({
|
||||||
|
name: 'login',
|
||||||
|
query: {
|
||||||
|
redirect: to.name,
|
||||||
|
...to.query,
|
||||||
|
} as LocationQueryRaw,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (to.name === 'login') {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
next({
|
||||||
|
name: 'login',
|
||||||
|
query: {
|
||||||
|
redirect: to.name,
|
||||||
|
...to.query,
|
||||||
|
} as LocationQueryRaw,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
37
src/router/index.ts
Normal file
37
src/router/index.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { createRouter, createWebHistory } from 'vue-router';
|
||||||
|
import NProgress from 'nprogress'; // progress bar
|
||||||
|
import 'nprogress/nprogress.css';
|
||||||
|
|
||||||
|
import { appRoutes } from './routes';
|
||||||
|
import { REDIRECT_MAIN, NOT_FOUND_ROUTE } from './routes/base';
|
||||||
|
import createRouteGuard from './guard';
|
||||||
|
|
||||||
|
NProgress.configure({ showSpinner: false }); // NProgress Configuration
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(),
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
redirect: 'login',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
name: 'login',
|
||||||
|
component: () => import('@/views/login/index.vue'),
|
||||||
|
meta: {
|
||||||
|
requiresAuth: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...appRoutes,
|
||||||
|
REDIRECT_MAIN,
|
||||||
|
NOT_FOUND_ROUTE,
|
||||||
|
],
|
||||||
|
scrollBehavior() {
|
||||||
|
return { top: 0 };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
createRouteGuard(router);
|
||||||
|
|
||||||
|
export default router;
|
||||||
31
src/router/routes/base.ts
Normal file
31
src/router/routes/base.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import type { RouteRecordRaw } from 'vue-router';
|
||||||
|
import { REDIRECT_ROUTE_NAME } from '@/router/constants';
|
||||||
|
|
||||||
|
export const DEFAULT_LAYOUT = () => import('@/layout/default-layout.vue');
|
||||||
|
|
||||||
|
export const REDIRECT_MAIN: RouteRecordRaw = {
|
||||||
|
path: '/redirect',
|
||||||
|
name: 'redirectWrapper',
|
||||||
|
component: DEFAULT_LAYOUT,
|
||||||
|
meta: {
|
||||||
|
requiresAuth: true,
|
||||||
|
hideInMenu: true,
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/redirect/:path',
|
||||||
|
name: REDIRECT_ROUTE_NAME,
|
||||||
|
component: () => import('@/views/redirect/index.vue'),
|
||||||
|
meta: {
|
||||||
|
requiresAuth: true,
|
||||||
|
hideInMenu: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NOT_FOUND_ROUTE: RouteRecordRaw = {
|
||||||
|
path: '/:pathMatch(.*)*',
|
||||||
|
name: 'notFound',
|
||||||
|
component: () => import('@/views/not-found/index.vue'),
|
||||||
|
};
|
||||||
10
src/router/routes/externalModules/arco.ts
Normal file
10
src/router/routes/externalModules/arco.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
export default {
|
||||||
|
path: 'https://arco.design',
|
||||||
|
name: 'arcoWebsite',
|
||||||
|
meta: {
|
||||||
|
locale: 'menu.arcoWebsite',
|
||||||
|
icon: 'icon-link',
|
||||||
|
requiresAuth: true,
|
||||||
|
order: 8,
|
||||||
|
},
|
||||||
|
};
|
||||||
10
src/router/routes/externalModules/faq.ts
Normal file
10
src/router/routes/externalModules/faq.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
export default {
|
||||||
|
path: 'https://arco.design/vue/docs/pro/faq',
|
||||||
|
name: 'faq',
|
||||||
|
meta: {
|
||||||
|
locale: 'menu.faq',
|
||||||
|
icon: 'icon-question-circle',
|
||||||
|
requiresAuth: true,
|
||||||
|
order: 9,
|
||||||
|
},
|
||||||
|
};
|
||||||
40
src/router/routes/externalModules/result.ts
Normal file
40
src/router/routes/externalModules/result.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { DEFAULT_LAYOUT } from '../base';
|
||||||
|
import { AppRouteRecordRaw } from '../types';
|
||||||
|
|
||||||
|
const RESULT: AppRouteRecordRaw = {
|
||||||
|
path: '/result',
|
||||||
|
name: 'result',
|
||||||
|
component: DEFAULT_LAYOUT,
|
||||||
|
redirect:'/result/success',
|
||||||
|
meta: {
|
||||||
|
locale: 'menu.result',
|
||||||
|
icon: 'icon-check-circle',
|
||||||
|
requiresAuth: true,
|
||||||
|
order: 5,
|
||||||
|
isFirst:true,
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'success',
|
||||||
|
name: 'Success',
|
||||||
|
component: () => import('@/views/result/success/index.vue'),
|
||||||
|
meta: {
|
||||||
|
locale: 'menu.result.success',
|
||||||
|
requiresAuth: true,
|
||||||
|
roles: ['admin'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'error',
|
||||||
|
name: 'Error',
|
||||||
|
component: () => import('@/views/result/error/index.vue'),
|
||||||
|
meta: {
|
||||||
|
locale: 'menu.result.error',
|
||||||
|
requiresAuth: true,
|
||||||
|
roles: ['admin'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RESULT;
|
||||||
40
src/router/routes/externalModules/user.ts
Normal file
40
src/router/routes/externalModules/user.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { DEFAULT_LAYOUT } from '../base';
|
||||||
|
import { AppRouteRecordRaw } from '../types';
|
||||||
|
|
||||||
|
const USER: AppRouteRecordRaw = {
|
||||||
|
path: '/user',
|
||||||
|
name: 'user',
|
||||||
|
component: DEFAULT_LAYOUT,
|
||||||
|
redirect:'/user/info',
|
||||||
|
meta: {
|
||||||
|
locale: 'menu.user',
|
||||||
|
icon: 'icon-user',
|
||||||
|
requiresAuth: true,
|
||||||
|
order: 7,
|
||||||
|
isFirst:true,
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'info',
|
||||||
|
name: 'Info',
|
||||||
|
component: () => import('@/views/user/info/index.vue'),
|
||||||
|
meta: {
|
||||||
|
locale: 'menu.user.info',
|
||||||
|
requiresAuth: true,
|
||||||
|
roles: ['*'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'setting',
|
||||||
|
name: 'Setting',
|
||||||
|
component: () => import('@/views/user/setting/index.vue'),
|
||||||
|
meta: {
|
||||||
|
locale: 'menu.user.setting',
|
||||||
|
requiresAuth: true,
|
||||||
|
roles: ['*'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default USER;
|
||||||
41
src/router/routes/externalModules/visualization.ts
Normal file
41
src/router/routes/externalModules/visualization.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { DEFAULT_LAYOUT } from '../base';
|
||||||
|
import { AppRouteRecordRaw } from '../types';
|
||||||
|
|
||||||
|
const VISUALIZATION: AppRouteRecordRaw = {
|
||||||
|
path: '/visualization',
|
||||||
|
name: 'visualization',
|
||||||
|
component: DEFAULT_LAYOUT,
|
||||||
|
redirect:'/visualization/data-analysis',
|
||||||
|
meta: {
|
||||||
|
locale: 'menu.visualization',
|
||||||
|
requiresAuth: true,
|
||||||
|
icon: 'icon-apps',
|
||||||
|
order: 1,
|
||||||
|
isFirst:true,
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'data-analysis',
|
||||||
|
name: 'DataAnalysis',
|
||||||
|
component: () => import('@/views/visualization/data-analysis/index.vue'),
|
||||||
|
meta: {
|
||||||
|
locale: 'menu.visualization.dataAnalysis',
|
||||||
|
requiresAuth: true,
|
||||||
|
roles: ['admin'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'multi-dimension-data-analysis',
|
||||||
|
name: 'MultiDimensionDataAnalysis',
|
||||||
|
component: () =>
|
||||||
|
import('@/views/visualization/multi-dimension-data-analysis/index.vue'),
|
||||||
|
meta: {
|
||||||
|
locale: 'menu.visualization.multiDimensionDataAnalysis',
|
||||||
|
requiresAuth: true,
|
||||||
|
roles: ['admin'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VISUALIZATION;
|
||||||
25
src/router/routes/index.ts
Normal file
25
src/router/routes/index.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import type { RouteRecordNormalized } from 'vue-router';
|
||||||
|
|
||||||
|
const modules = import.meta.glob('./modules/*.ts', { eager: true });
|
||||||
|
const externalModules = import.meta.glob('./externalModules/*.ts', {
|
||||||
|
eager: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
function formatModules(_modules: any, result: RouteRecordNormalized[]) {
|
||||||
|
Object.keys(_modules).forEach((key) => {
|
||||||
|
const defaultModule = _modules[key].default;
|
||||||
|
if (!defaultModule) return;
|
||||||
|
const moduleList = Array.isArray(defaultModule)
|
||||||
|
? [...defaultModule]
|
||||||
|
: [defaultModule];
|
||||||
|
result.push(...moduleList);
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const appRoutes: RouteRecordNormalized[] = formatModules(modules, []);
|
||||||
|
|
||||||
|
export const appExternalRoutes: RouteRecordNormalized[] = formatModules(
|
||||||
|
externalModules,
|
||||||
|
[]
|
||||||
|
);
|
||||||
173
src/router/routes/modules/business.ts
Normal file
173
src/router/routes/modules/business.ts
Normal file
|
|
@ -0,0 +1,173 @@
|
||||||
|
import { DEFAULT_LAYOUT } from '../base';
|
||||||
|
import { AppRouteRecordRaw } from '../types';
|
||||||
|
import PageLayout from '@/layout/page-layout.vue';
|
||||||
|
// 业务路由
|
||||||
|
const BUSINESS: AppRouteRecordRaw = {
|
||||||
|
path: '/business',
|
||||||
|
name: 'business',
|
||||||
|
component: DEFAULT_LAYOUT,
|
||||||
|
redirect: '/business/project',
|
||||||
|
meta: {
|
||||||
|
locale: 'menu.business',
|
||||||
|
requiresAuth: true,
|
||||||
|
icon: 'icon-heart-fill',
|
||||||
|
permissionKey: 'business',
|
||||||
|
order: 2,
|
||||||
|
isFirst: true,
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
// 项目管理
|
||||||
|
{
|
||||||
|
path: 'project',
|
||||||
|
name: 'business-project',
|
||||||
|
component: () => import('@/views/business/project/index.vue'),
|
||||||
|
meta: {
|
||||||
|
locale: 'menu.business.project',
|
||||||
|
requiresAuth: true,
|
||||||
|
roles: ['admin'],
|
||||||
|
icon: 'icon-attachment',
|
||||||
|
permissionKey: 'businessProject',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 空间管理
|
||||||
|
{
|
||||||
|
path: 'space',
|
||||||
|
name: 'business-space',
|
||||||
|
component: PageLayout,
|
||||||
|
redirect: '/business/space/house-view',
|
||||||
|
meta: {
|
||||||
|
locale: 'menu.business.space',
|
||||||
|
requiresAuth: true,
|
||||||
|
roles: ['*'],
|
||||||
|
icon: 'icon-brush',
|
||||||
|
permissionKey: 'businessSpace',
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
// 房源剖视
|
||||||
|
{
|
||||||
|
path: 'house-view',
|
||||||
|
name: 'business-space-house-view',
|
||||||
|
component: () => import('@/views/business/space/house-view.vue'),
|
||||||
|
meta: {
|
||||||
|
locale: 'menu.business.house-view',
|
||||||
|
requiresAuth: true,
|
||||||
|
roles: ['*'],
|
||||||
|
icon: 'icon-brush',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 房源列表
|
||||||
|
{
|
||||||
|
path: 'house-list',
|
||||||
|
name: 'business-space-house-list',
|
||||||
|
component: () => import('@/views/business/space/house-list.vue'),
|
||||||
|
meta: {
|
||||||
|
locale: 'menu.business.house-list',
|
||||||
|
requiresAuth: true,
|
||||||
|
roles: ['*'],
|
||||||
|
icon: 'icon-brush',
|
||||||
|
permissionKey: 'businessHouseList',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// 合同管理
|
||||||
|
{
|
||||||
|
path: 'contract',
|
||||||
|
name: 'business-contract',
|
||||||
|
component: PageLayout,
|
||||||
|
redirect: '/business/contract/contract-list',
|
||||||
|
meta: {
|
||||||
|
locale: 'menu.business.contract',
|
||||||
|
requiresAuth: true,
|
||||||
|
roles: ['*'],
|
||||||
|
icon: 'icon-brush',
|
||||||
|
permissionKey: 'businessContract',
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
// 合同列表
|
||||||
|
{
|
||||||
|
path: 'contract-list',
|
||||||
|
name: 'business-contract-list',
|
||||||
|
component: () => import('@/views/business/contract/list.vue'),
|
||||||
|
meta: {
|
||||||
|
locale: 'menu.business.contract-list',
|
||||||
|
requiresAuth: true,
|
||||||
|
roles: ['*'],
|
||||||
|
icon: 'icon-brush',
|
||||||
|
permissionKey: 'businessContractList',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 合同归档
|
||||||
|
{
|
||||||
|
path: 'contract-file',
|
||||||
|
name: 'business-contract-file',
|
||||||
|
component: () => import('@/views/business/contract/file.vue'),
|
||||||
|
meta: {
|
||||||
|
locale: 'menu.business.contract-file',
|
||||||
|
requiresAuth: true,
|
||||||
|
roles: ['*'],
|
||||||
|
icon: 'icon-brush',
|
||||||
|
permissionKey: 'businessContractFile',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 合同模板
|
||||||
|
{
|
||||||
|
path: 'contract-template',
|
||||||
|
name: 'business-contract-template',
|
||||||
|
component: () => import('@/views/business/contract/template.vue'),
|
||||||
|
meta: {
|
||||||
|
locale: 'menu.business.contract-template',
|
||||||
|
requiresAuth: true,
|
||||||
|
roles: ['*'],
|
||||||
|
icon: 'icon-brush',
|
||||||
|
permissionKey: 'businessContractTemplate',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// 租客管理
|
||||||
|
{
|
||||||
|
path: 'tenant',
|
||||||
|
name: 'business-tenant',
|
||||||
|
component: PageLayout,
|
||||||
|
redirect: '/business/tenant/list',
|
||||||
|
meta: {
|
||||||
|
locale: 'menu.business.tenant',
|
||||||
|
requiresAuth: true,
|
||||||
|
roles: ['*'],
|
||||||
|
icon: 'icon-font-colors',
|
||||||
|
permissionKey: 'businessTenant',
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
// 租客列表
|
||||||
|
{
|
||||||
|
path: 'tenant-list',
|
||||||
|
name: 'business-tenant-list',
|
||||||
|
component: () => import('@/views/business/tenant/list.vue'),
|
||||||
|
meta: {
|
||||||
|
locale: 'menu.business.tenant-list',
|
||||||
|
requiresAuth: true,
|
||||||
|
roles: ['*'],
|
||||||
|
icon: 'icon-font-colors',
|
||||||
|
permissionKey: 'businessTenantList',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 租客联系人
|
||||||
|
{
|
||||||
|
path: 'tenant-contacts',
|
||||||
|
name: 'business-tenant-contacts',
|
||||||
|
component: () => import('@/views/business/tenant/contacts.vue'),
|
||||||
|
meta: {
|
||||||
|
locale: 'menu.business.tenant.contacts',
|
||||||
|
requiresAuth: true,
|
||||||
|
roles: ['*'],
|
||||||
|
icon: 'icon-font-colors',
|
||||||
|
permissionKey: 'businessTenantContacts',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BUSINESS;
|
||||||
63
src/router/routes/modules/dashboard.ts
Normal file
63
src/router/routes/modules/dashboard.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { DEFAULT_LAYOUT } from '../base';
|
||||||
|
import { AppRouteRecordRaw } from '../types';
|
||||||
|
import PageLayout from '@/layout/page-layout.vue';
|
||||||
|
const DASHBOARD: AppRouteRecordRaw = {
|
||||||
|
path: '/dashboard',
|
||||||
|
name: 'dashboard',
|
||||||
|
component: DEFAULT_LAYOUT,
|
||||||
|
redirect: '/dashboard/workplace',
|
||||||
|
meta: {
|
||||||
|
locale: 'menu.dashboard.workplace',
|
||||||
|
requiresAuth: true,
|
||||||
|
icon: 'icon-dashboard',
|
||||||
|
order: 1,
|
||||||
|
isFirst: true,
|
||||||
|
permissionKey: 'dashboard',
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'workplace',
|
||||||
|
name: 'Workplace',
|
||||||
|
component: PageLayout,
|
||||||
|
redirect: '/dashboard/workplace/step',
|
||||||
|
meta: {
|
||||||
|
locale: 'menu.dashboard.workplace',
|
||||||
|
requiresAuth: true,
|
||||||
|
roles: ['*'],
|
||||||
|
icon: 'icon-dashboard',
|
||||||
|
permissionKey: 'workplace',
|
||||||
|
// activeMenu: 'dashboard',
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'step',
|
||||||
|
name: 'Step',
|
||||||
|
component: () => import('@/views/dashboard/workplace/index.vue'),
|
||||||
|
meta: {
|
||||||
|
locale: 'menu.form.step',
|
||||||
|
requiresAuth: true,
|
||||||
|
roles: ['*'],
|
||||||
|
icon: 'icon-exclamation-circle',
|
||||||
|
permissionKey: 'step',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: 'monitor',
|
||||||
|
name: 'Monitor',
|
||||||
|
component: () => import('@/views/dashboard/monitor/index.vue'),
|
||||||
|
meta: {
|
||||||
|
locale: 'menu.dashboard.monitor',
|
||||||
|
requiresAuth: true,
|
||||||
|
roles: ['*'],
|
||||||
|
icon: 'icon-settings',
|
||||||
|
permissionKey: 'monitor',
|
||||||
|
// activeMenu: 'dashboard',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DASHBOARD;
|
||||||
40
src/router/routes/modules/finance.ts
Normal file
40
src/router/routes/modules/finance.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { DEFAULT_LAYOUT } from '../base';
|
||||||
|
import { AppRouteRecordRaw } from '../types';
|
||||||
|
|
||||||
|
const FINANCE: AppRouteRecordRaw = {
|
||||||
|
path: '/finance',
|
||||||
|
name: 'finance',
|
||||||
|
component: DEFAULT_LAYOUT,
|
||||||
|
redirect: '/finance/bill',
|
||||||
|
meta: {
|
||||||
|
locale: 'menu.finance',
|
||||||
|
icon: 'icon-settings',
|
||||||
|
requiresAuth: true,
|
||||||
|
order: 3,
|
||||||
|
isFirst:true,
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'bill',
|
||||||
|
name: 'bill',
|
||||||
|
component: () => import('@/views/finance/bill/index.vue'),
|
||||||
|
meta: {
|
||||||
|
locale: 'menu.finance.bill',
|
||||||
|
requiresAuth: true,
|
||||||
|
roles: ['admin'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'money',
|
||||||
|
name: 'money',
|
||||||
|
component: () => import('@/views/finance/money/index.vue'),
|
||||||
|
meta: {
|
||||||
|
locale: 'menu.finance.money',
|
||||||
|
requiresAuth: true,
|
||||||
|
roles: ['admin'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FINANCE;
|
||||||
40
src/router/routes/modules/property.ts
Normal file
40
src/router/routes/modules/property.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { DEFAULT_LAYOUT } from '../base';
|
||||||
|
import { AppRouteRecordRaw } from '../types';
|
||||||
|
|
||||||
|
const PROPERTY: AppRouteRecordRaw = {
|
||||||
|
path: '/property',
|
||||||
|
name: 'property',
|
||||||
|
component: DEFAULT_LAYOUT,
|
||||||
|
redirect: '/property/hydropower',
|
||||||
|
meta: {
|
||||||
|
locale: 'menu.property',
|
||||||
|
requiresAuth: true,
|
||||||
|
icon: 'icon-list',
|
||||||
|
order: 4,
|
||||||
|
isFirst:true,
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'hydropower', // The midline path complies with SEO specifications
|
||||||
|
name: 'hydropower',
|
||||||
|
component: () => import('@/views/property/hydropower/index.vue'),
|
||||||
|
meta: {
|
||||||
|
locale: 'menu.property.hydropower',
|
||||||
|
requiresAuth: true,
|
||||||
|
roles: ['*'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'reservation',
|
||||||
|
name: 'reservation',
|
||||||
|
component: () => import('@/views/property/reservation/index.vue'),
|
||||||
|
meta: {
|
||||||
|
locale: 'menu.property.reservation',
|
||||||
|
requiresAuth: true,
|
||||||
|
roles: ['*'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PROPERTY;
|
||||||
69
src/router/routes/modules/setting.ts
Normal file
69
src/router/routes/modules/setting.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
import { DEFAULT_LAYOUT } from '../base';
|
||||||
|
import { AppRouteRecordRaw } from '../types';
|
||||||
|
import PageLayout from '@/layout/page-layout.vue';
|
||||||
|
// 设置
|
||||||
|
const SETTING: AppRouteRecordRaw = {
|
||||||
|
path: '/setting',
|
||||||
|
name: 'setting',
|
||||||
|
component: DEFAULT_LAYOUT,
|
||||||
|
redirect: '/setting/basic',
|
||||||
|
meta: {
|
||||||
|
locale: 'menu.setting',
|
||||||
|
requiresAuth: true,
|
||||||
|
order: 5,
|
||||||
|
// hideChildrenInMenu:true,
|
||||||
|
isFirst: true,
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
// 用户设置
|
||||||
|
{
|
||||||
|
path: 'user',
|
||||||
|
name: 'user-setting',
|
||||||
|
component: PageLayout,
|
||||||
|
meta: {
|
||||||
|
locale: 'menu.setting.user',
|
||||||
|
requiresAuth: true,
|
||||||
|
roles: ['admin'],
|
||||||
|
icon: 'icon-file',
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
// 用户列表
|
||||||
|
{
|
||||||
|
path: 'list',
|
||||||
|
name: 'setting-user-list',
|
||||||
|
component: () => import('@/views/setting/user/list.vue'),
|
||||||
|
meta: {
|
||||||
|
locale: 'menu.setting.user-list',
|
||||||
|
requiresAuth: true,
|
||||||
|
roles: ['*'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 角色权限
|
||||||
|
{
|
||||||
|
path: 'role',
|
||||||
|
name: 'setting-user-role',
|
||||||
|
component: () => import('@/views/setting/user/role.vue'),
|
||||||
|
meta: {
|
||||||
|
locale: 'menu.setting.role',
|
||||||
|
requiresAuth: true,
|
||||||
|
roles: ['*'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// 系统配置
|
||||||
|
{
|
||||||
|
path: 'system',
|
||||||
|
name: 'system-setting',
|
||||||
|
component: () => import('@/views/setting/system/index.vue'),
|
||||||
|
meta: {
|
||||||
|
locale: 'menu.setting.system',
|
||||||
|
requiresAuth: true,
|
||||||
|
roles: ['admin'],
|
||||||
|
icon: 'icon-settings',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SETTING;
|
||||||
22
src/router/routes/types.ts
Normal file
22
src/router/routes/types.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import type { RouteMeta, NavigationGuard } from 'vue-router';
|
||||||
|
|
||||||
|
export type Component<T = any> =
|
||||||
|
| ReturnType<typeof defineComponent>
|
||||||
|
| (() => Promise<typeof import('*.vue')>)
|
||||||
|
| (() => Promise<T>);
|
||||||
|
|
||||||
|
export interface AppRouteRecordRaw {
|
||||||
|
path: string;
|
||||||
|
name?: string | symbol;
|
||||||
|
meta?: RouteMeta & {
|
||||||
|
permissionKey?: string; // 权限标识,与接口返回的perm_key对应
|
||||||
|
};
|
||||||
|
redirect?: string;
|
||||||
|
component: Component | string;
|
||||||
|
children?: AppRouteRecordRaw[];
|
||||||
|
alias?: string | string[];
|
||||||
|
props?: Record<string, any>;
|
||||||
|
beforeEnter?: NavigationGuard | NavigationGuard[];
|
||||||
|
fullPath?: string;
|
||||||
|
}
|
||||||
16
src/router/typings.d.ts
vendored
Normal file
16
src/router/typings.d.ts
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import 'vue-router';
|
||||||
|
|
||||||
|
declare module 'vue-router' {
|
||||||
|
interface RouteMeta {
|
||||||
|
roles?: string[]; // Controls roles that have access to the page
|
||||||
|
requiresAuth: boolean; // Whether login is required to access the current page (every route must declare)
|
||||||
|
icon?: string; // The icon show in the side menu
|
||||||
|
locale?: string; // The locale name show in side menu and breadcrumb
|
||||||
|
hideInMenu?: boolean; // If true, it is not displayed in the side menu
|
||||||
|
hideChildrenInMenu?: boolean; // if set true, the children are not displayed in the side menu
|
||||||
|
activeMenu?: string; // if set name, the menu will be highlighted according to the name you set
|
||||||
|
order?: number; // Sort routing menu items. If set key, the higher the value, the more forward it is
|
||||||
|
noAffix?: boolean; // if set true, the tag will not affix in the tab-bar
|
||||||
|
ignoreCache?: boolean; // if set true, the page will not be cached
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue