5.0
@@ -1,5 +1,5 @@
|
||||
# 后台admin
|
||||
|
||||
- sop-admin-server: admin服务端,使用方式见readme.md
|
||||
- sop-admin-vue: admin前端vue实现
|
||||
- sop-admin-backend: admin服务端
|
||||
- sop-admin-frontend: admin前端实现
|
||||
|
||||
|
@@ -1,14 +0,0 @@
|
||||
# http://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
insert_final_newline = false
|
||||
trim_trailing_whitespace = false
|
@@ -1,14 +1 @@
|
||||
# just a flag
|
||||
ENV = 'development'
|
||||
|
||||
# base api,末尾没有/
|
||||
VUE_APP_BASE_API = 'http://localhost:8082'
|
||||
|
||||
# vue-cli uses the VUE_CLI_BABEL_TRANSPILE_MODULES environment variable,
|
||||
# to control whether the babel-plugin-dynamic-import-node plugin is enabled.
|
||||
# It only does one thing by converting all import() to require().
|
||||
# This configuration can significantly increase the speed of hot updates,
|
||||
# when you have a large number of pages.
|
||||
# Detail: https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/babel-preset-app/index.js
|
||||
|
||||
VUE_CLI_BABEL_TRANSPILE_MODULES = true
|
||||
VITE_API_BASE_URL= 'http://localhost:8080'
|
@@ -1,8 +0,0 @@
|
||||
# just a flag
|
||||
ENV = 'production'
|
||||
|
||||
# base api,
|
||||
# 如果要前后端分离,这里填 http(s)://ip:port 末尾没有/
|
||||
# 如:http://www.aaa.com, http://www.bbb.com:7777
|
||||
VUE_APP_BASE_API = ''
|
||||
|
||||
|
@@ -1,8 +0,0 @@
|
||||
NODE_ENV = production
|
||||
|
||||
# just a flag
|
||||
ENV = 'staging'
|
||||
|
||||
# base api
|
||||
VUE_APP_BASE_API = '/stage-api'
|
||||
|
@@ -1,4 +1,3 @@
|
||||
build/*.js
|
||||
src/assets
|
||||
public
|
||||
dist
|
||||
/*.json
|
||||
/*.js
|
||||
dist
|
@@ -1,198 +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: 'babel-eslint',
|
||||
sourceType: 'module'
|
||||
// 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,
|
||||
es6: true,
|
||||
'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'),
|
||||
},
|
||||
},
|
||||
},
|
||||
extends: ['plugin:vue/recommended', 'eslint:recommended'],
|
||||
|
||||
// add your custom rules here
|
||||
//it is base on https://github.com/vuejs/eslint-config-vue
|
||||
rules: {
|
||||
"vue/max-attributes-per-line": [2, {
|
||||
"singleline": 10,
|
||||
"multiline": {
|
||||
"max": 1,
|
||||
"allowFirstLine": false
|
||||
}
|
||||
}],
|
||||
"vue/singleline-html-element-content-newline": "off",
|
||||
"vue/multiline-html-element-content-newline":"off",
|
||||
"vue/name-property-casing": ["error", "PascalCase"],
|
||||
"vue/no-v-html": "off",
|
||||
'accessor-pairs': 2,
|
||||
'arrow-spacing': [2, {
|
||||
'before': true,
|
||||
'after': true
|
||||
}],
|
||||
'block-spacing': [2, 'always'],
|
||||
'brace-style': [2, '1tbs', {
|
||||
'allowSingleLine': true
|
||||
}],
|
||||
'camelcase': [0, {
|
||||
'properties': 'always'
|
||||
}],
|
||||
'comma-dangle': [2, 'never'],
|
||||
'comma-spacing': [2, {
|
||||
'before': false,
|
||||
'after': true
|
||||
}],
|
||||
'comma-style': [2, 'last'],
|
||||
'constructor-super': 2,
|
||||
'curly': [2, 'multi-line'],
|
||||
'dot-location': [2, 'property'],
|
||||
'eol-last': 2,
|
||||
'eqeqeq': ["error", "always", {"null": "ignore"}],
|
||||
'generator-star-spacing': [2, {
|
||||
'before': true,
|
||||
'after': true
|
||||
}],
|
||||
'handle-callback-err': [2, '^(err|error)$'],
|
||||
'indent': [2, 2, {
|
||||
'SwitchCase': 1
|
||||
}],
|
||||
'jsx-quotes': [2, 'prefer-single'],
|
||||
'key-spacing': [2, {
|
||||
'beforeColon': false,
|
||||
'afterColon': true
|
||||
}],
|
||||
'keyword-spacing': [2, {
|
||||
'before': true,
|
||||
'after': true
|
||||
}],
|
||||
'new-cap': [2, {
|
||||
'newIsCap': true,
|
||||
'capIsNew': false
|
||||
}],
|
||||
'new-parens': 2,
|
||||
'no-array-constructor': 2,
|
||||
'no-caller': 2,
|
||||
'no-console': 'off',
|
||||
'no-class-assign': 2,
|
||||
'no-cond-assign': 2,
|
||||
'no-const-assign': 2,
|
||||
'no-control-regex': 0,
|
||||
'no-delete-var': 2,
|
||||
'no-dupe-args': 2,
|
||||
'no-dupe-class-members': 2,
|
||||
'no-dupe-keys': 2,
|
||||
'no-duplicate-case': 2,
|
||||
'no-empty-character-class': 2,
|
||||
'no-empty-pattern': 2,
|
||||
'no-eval': 2,
|
||||
'no-ex-assign': 2,
|
||||
'no-extend-native': 2,
|
||||
'no-extra-bind': 2,
|
||||
'no-extra-boolean-cast': 2,
|
||||
'no-extra-parens': [2, 'functions'],
|
||||
'no-fallthrough': 2,
|
||||
'no-floating-decimal': 2,
|
||||
'no-func-assign': 2,
|
||||
'no-implied-eval': 2,
|
||||
'no-inner-declarations': [2, 'functions'],
|
||||
'no-invalid-regexp': 2,
|
||||
'no-irregular-whitespace': 2,
|
||||
'no-iterator': 2,
|
||||
'no-label-var': 2,
|
||||
'no-labels': [2, {
|
||||
'allowLoop': false,
|
||||
'allowSwitch': false
|
||||
}],
|
||||
'no-lone-blocks': 2,
|
||||
'no-mixed-spaces-and-tabs': 2,
|
||||
'no-multi-spaces': 2,
|
||||
'no-multi-str': 2,
|
||||
'no-multiple-empty-lines': [2, {
|
||||
'max': 1
|
||||
}],
|
||||
'no-native-reassign': 2,
|
||||
'no-negated-in-lhs': 2,
|
||||
'no-new-object': 2,
|
||||
'no-new-require': 2,
|
||||
'no-new-symbol': 2,
|
||||
'no-new-wrappers': 2,
|
||||
'no-obj-calls': 2,
|
||||
'no-octal': 2,
|
||||
'no-octal-escape': 2,
|
||||
'no-path-concat': 2,
|
||||
'no-proto': 2,
|
||||
'no-redeclare': 2,
|
||||
'no-regex-spaces': 2,
|
||||
'no-return-assign': [2, 'except-parens'],
|
||||
'no-self-assign': 2,
|
||||
'no-self-compare': 2,
|
||||
'no-sequences': 2,
|
||||
'no-shadow-restricted-names': 2,
|
||||
'no-spaced-func': 2,
|
||||
'no-sparse-arrays': 2,
|
||||
'no-this-before-super': 2,
|
||||
'no-throw-literal': 2,
|
||||
'no-trailing-spaces': 2,
|
||||
'no-undef': 2,
|
||||
'no-undef-init': 2,
|
||||
'no-unexpected-multiline': 2,
|
||||
'no-unmodified-loop-condition': 2,
|
||||
'no-unneeded-ternary': [2, {
|
||||
'defaultAssignment': false
|
||||
}],
|
||||
'no-unreachable': 2,
|
||||
'no-unsafe-finally': 2,
|
||||
'no-unused-vars': [2, {
|
||||
'vars': 'all',
|
||||
'args': 'none'
|
||||
}],
|
||||
'no-useless-call': 2,
|
||||
'no-useless-computed-key': 2,
|
||||
'no-useless-constructor': 2,
|
||||
'no-useless-escape': 0,
|
||||
'no-whitespace-before-property': 2,
|
||||
'no-with': 2,
|
||||
'one-var': [2, {
|
||||
'initialized': 'never'
|
||||
}],
|
||||
'operator-linebreak': [2, 'after', {
|
||||
'overrides': {
|
||||
'?': 'before',
|
||||
':': 'before'
|
||||
}
|
||||
}],
|
||||
'padded-blocks': [2, 'never'],
|
||||
'quotes': [2, 'single', {
|
||||
'avoidEscape': true,
|
||||
'allowTemplateLiterals': true
|
||||
}],
|
||||
'semi': [2, 'never'],
|
||||
'semi-spacing': [2, {
|
||||
'before': false,
|
||||
'after': true
|
||||
}],
|
||||
'space-before-blocks': [2, 'always'],
|
||||
'space-before-function-paren': [2, 'never'],
|
||||
'space-in-parens': [2, 'never'],
|
||||
'space-infix-ops': 2,
|
||||
'space-unary-ops': [2, {
|
||||
'words': true,
|
||||
'nonwords': false
|
||||
}],
|
||||
'spaced-comment': [2, 'always', {
|
||||
'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ',']
|
||||
}],
|
||||
'template-curly-spacing': [2, 'never'],
|
||||
'use-isnan': 2,
|
||||
'valid-typeof': 2,
|
||||
'wrap-iife': [2, 'any'],
|
||||
'yield-star-spacing': [2, 'both'],
|
||||
'yoda': [2, 'never'],
|
||||
'prefer-const': 2,
|
||||
'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,
|
||||
'object-curly-spacing': [2, 'always', {
|
||||
objectsInObjects: false
|
||||
}],
|
||||
'array-bracket-spacing': [2, 'never']
|
||||
}
|
||||
}
|
||||
'no-param-reassign': 0,
|
||||
'prefer-regex-literals': 0,
|
||||
'import/no-extraneous-dependencies': 0,
|
||||
},
|
||||
};
|
||||
|
24
sop-admin/sop-admin-frontend/.gitignore
vendored
@@ -1,16 +1,10 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
node_modules/
|
||||
dist/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
package-lock.json
|
||||
tests/**/coverage/
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
4
sop-admin/sop-admin-frontend/.husky/commit-msg
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
pnpm commitlint --edit $1
|
4
sop-admin/sop-admin-frontend/.husky/pre-commit
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npm run lint-staged
|
@@ -1,8 +0,0 @@
|
||||
// https://github.com/michael-ciniawsky/postcss-load-config
|
||||
|
||||
module.exports = {
|
||||
'plugins': {
|
||||
// to edit target browsers: use "browserslist" field in package.json
|
||||
'autoprefixer': {}
|
||||
}
|
||||
}
|
7
sop-admin/sop-admin-frontend/.prettierignore
Normal file
@@ -0,0 +1,7 @@
|
||||
/dist/*
|
||||
.local
|
||||
.output.js
|
||||
/node_modules/**
|
||||
|
||||
**/*.svg
|
||||
**/*.sh
|
9
sop-admin/sop-admin-frontend/.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
sop-admin/sop-admin-frontend/.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'],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
@@ -1,5 +0,0 @@
|
||||
language: node_js
|
||||
node_js: 10
|
||||
script: npm run test
|
||||
notifications:
|
||||
email: false
|
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017-present PanJiaChen
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
@@ -1,26 +0,0 @@
|
||||
# SOP Admin 前端vue实现
|
||||
|
||||
此工程基于前端脚手架:`https://github.com/PanJiaChen/vue-element-admin`
|
||||
|
||||
前提:先安装好npm,[npm安装教程](https://blog.csdn.net/zhangwenwu2/article/details/52778521)
|
||||
|
||||
1. 启动服务端程序,运行`SopAdminServerApplication.java`
|
||||
2. `cd sop-admin-vue`
|
||||
3. 执行`npm install --registry=https://registry.npm.taobao.org`
|
||||
4. 执行`npm run dev`,访问`http://localhost:9528/`,用户名密码:`admin/123456`
|
||||
|
||||
|
||||
- 修改端口号:打开`vue.config.js`,找到`port`属性
|
||||
|
||||
## 打包放入到服务端步骤
|
||||
|
||||
如果想要把vue打包放到服务端,步骤如下:
|
||||
|
||||
- 执行`npm run build:prod`进行打包,结果在`dist`下
|
||||
- 打包完成后,把dest中的所有文件,放到`sop-admin-server/src/main/resources/public`下
|
||||
|
||||
## 前后端分离部署
|
||||
|
||||
- 修改`.env.production`,指定服务端地址
|
||||
- 执行`npm run build:prod`进行打包,结果在`dist`下
|
||||
- 把`dist`中的文件放到静态服务器上
|
@@ -1,5 +1,3 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/app'
|
||||
]
|
||||
}
|
||||
plugins: ['@vue/babel-plugin-jsx'],
|
||||
};
|
||||
|
@@ -1,11 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo "开始构建..."
|
||||
|
||||
rm -rf dist/*
|
||||
npm run build:prod
|
||||
echo "复制dist文件内容到sop-admin-server/src/main/resources/public"
|
||||
rm -rf ../sop-admin-server/src/main/resources/public/*
|
||||
cp -r dist/* ../sop-admin-server/src/main/resources/public
|
||||
|
||||
echo "构建完毕"
|
@@ -1,35 +0,0 @@
|
||||
const { run } = require('runjs')
|
||||
const chalk = require('chalk')
|
||||
const config = require('../vue.config.js')
|
||||
const rawArgv = process.argv.slice(2)
|
||||
const args = rawArgv.join(' ')
|
||||
|
||||
if (process.env.npm_config_preview || rawArgv.includes('--preview')) {
|
||||
const report = rawArgv.includes('--report')
|
||||
|
||||
run(`vue-cli-service build ${args}`)
|
||||
|
||||
const port = 9526
|
||||
const publicPath = config.publicPath
|
||||
|
||||
var connect = require('connect')
|
||||
var serveStatic = require('serve-static')
|
||||
const app = connect()
|
||||
|
||||
app.use(
|
||||
publicPath,
|
||||
serveStatic('./dist', {
|
||||
index: ['index.html', '/']
|
||||
})
|
||||
)
|
||||
|
||||
app.listen(port, function () {
|
||||
console.log(chalk.green(`> Preview at http://localhost:${port}${publicPath}`))
|
||||
if (report) {
|
||||
console.log(chalk.green(`> Report at http://localhost:${port}${publicPath}report.html`))
|
||||
}
|
||||
|
||||
})
|
||||
} else {
|
||||
run(`vue-cli-service build ${args}`)
|
||||
}
|
3
sop-admin/sop-admin-frontend/commitlint.config.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: ['@commitlint/config-conventional'],
|
||||
};
|
15
sop-admin/sop-admin-frontend/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
sop-admin/sop-admin-frontend/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;
|
||||
}
|
@@ -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
sop-admin/sop-admin-frontend/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
sop-admin/sop-admin-frontend/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
sop-admin/sop-admin-frontend/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
sop-admin/sop-admin-frontend/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
sop-admin/sop-admin-frontend/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
sop-admin/sop-admin-frontend/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
sop-admin/sop-admin-frontend/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
sop-admin/sop-admin-frontend/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>
|
@@ -1,24 +0,0 @@
|
||||
module.exports = {
|
||||
moduleFileExtensions: ['js', 'jsx', 'json', 'vue'],
|
||||
transform: {
|
||||
'^.+\\.vue$': 'vue-jest',
|
||||
'.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$':
|
||||
'jest-transform-stub',
|
||||
'^.+\\.jsx?$': 'babel-jest'
|
||||
},
|
||||
moduleNameMapper: {
|
||||
'^@/(.*)$': '<rootDir>/src/$1'
|
||||
},
|
||||
snapshotSerializers: ['jest-serializer-vue'],
|
||||
testMatch: [
|
||||
'**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
|
||||
],
|
||||
collectCoverageFrom: ['src/utils/**/*.{js,vue}', '!src/utils/auth.js', '!src/utils/request.js', 'src/components/**/*.{js,vue}'],
|
||||
coverageDirectory: '<rootDir>/tests/unit/coverage',
|
||||
// 'collectCoverage': true,
|
||||
'coverageReporters': [
|
||||
'lcov',
|
||||
'text-summary'
|
||||
],
|
||||
testURL: 'http://localhost/'
|
||||
}
|
@@ -1,66 +0,0 @@
|
||||
import Mock from 'mockjs'
|
||||
import { param2Obj } from '../src/utils'
|
||||
|
||||
import user from './user'
|
||||
import table from './table'
|
||||
|
||||
const mocks = [
|
||||
...user,
|
||||
...table
|
||||
]
|
||||
|
||||
// for front mock
|
||||
// please use it cautiously, it will redefine XMLHttpRequest,
|
||||
// which will cause many of your third-party libraries to be invalidated(like progress event).
|
||||
export function mockXHR() {
|
||||
// mock patch
|
||||
// https://github.com/nuysoft/Mock/issues/300
|
||||
Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send
|
||||
Mock.XHR.prototype.send = function() {
|
||||
if (this.custom.xhr) {
|
||||
this.custom.xhr.withCredentials = this.withCredentials || false
|
||||
|
||||
if (this.responseType) {
|
||||
this.custom.xhr.responseType = this.responseType
|
||||
}
|
||||
}
|
||||
this.proxy_send(...arguments)
|
||||
}
|
||||
|
||||
function XHR2ExpressReqWrap(respond) {
|
||||
return function(options) {
|
||||
let result = null
|
||||
if (respond instanceof Function) {
|
||||
const { body, type, url } = options
|
||||
// https://expressjs.com/en/4x/api.html#req
|
||||
result = respond({
|
||||
method: type,
|
||||
body: JSON.parse(body),
|
||||
query: param2Obj(url)
|
||||
})
|
||||
} else {
|
||||
result = respond
|
||||
}
|
||||
return Mock.mock(result)
|
||||
}
|
||||
}
|
||||
|
||||
for (const i of mocks) {
|
||||
Mock.mock(new RegExp(i.url), i.type || 'get', XHR2ExpressReqWrap(i.response))
|
||||
}
|
||||
}
|
||||
|
||||
// for mock server
|
||||
const responseFake = (url, type, respond) => {
|
||||
return {
|
||||
url: new RegExp(`/mock${url}`),
|
||||
type: type || 'get',
|
||||
response(req, res) {
|
||||
res.json(Mock.mock(respond instanceof Function ? respond(req, res) : respond))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default mocks.map(route => {
|
||||
return responseFake(route.url, route.type, route.response)
|
||||
})
|
@@ -1,68 +0,0 @@
|
||||
const chokidar = require('chokidar')
|
||||
const bodyParser = require('body-parser')
|
||||
const chalk = require('chalk')
|
||||
const path = require('path')
|
||||
|
||||
const mockDir = path.join(process.cwd(), 'mock')
|
||||
|
||||
function registerRoutes(app) {
|
||||
let mockLastIndex
|
||||
const { default: mocks } = require('./index.js')
|
||||
for (const mock of mocks) {
|
||||
app[mock.type](mock.url, mock.response)
|
||||
mockLastIndex = app._router.stack.length
|
||||
}
|
||||
const mockRoutesLength = Object.keys(mocks).length
|
||||
return {
|
||||
mockRoutesLength: mockRoutesLength,
|
||||
mockStartIndex: mockLastIndex - mockRoutesLength
|
||||
}
|
||||
}
|
||||
|
||||
function unregisterRoutes() {
|
||||
Object.keys(require.cache).forEach(i => {
|
||||
if (i.includes(mockDir)) {
|
||||
delete require.cache[require.resolve(i)]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = app => {
|
||||
// es6 polyfill
|
||||
require('@babel/register')
|
||||
|
||||
// parse app.body
|
||||
// https://expressjs.com/en/4x/api.html#req.body
|
||||
app.use(bodyParser.json())
|
||||
app.use(bodyParser.urlencoded({
|
||||
extended: true
|
||||
}))
|
||||
|
||||
const mockRoutes = registerRoutes(app)
|
||||
var mockRoutesLength = mockRoutes.mockRoutesLength
|
||||
var mockStartIndex = mockRoutes.mockStartIndex
|
||||
|
||||
// watch files, hot reload mock server
|
||||
chokidar.watch(mockDir, {
|
||||
ignored: /mock-server/,
|
||||
ignoreInitial: true
|
||||
}).on('all', (event, path) => {
|
||||
if (event === 'change' || event === 'add') {
|
||||
try {
|
||||
// remove mock routes stack
|
||||
app._router.stack.splice(mockStartIndex, mockRoutesLength)
|
||||
|
||||
// clear routes cache
|
||||
unregisterRoutes()
|
||||
|
||||
const mockRoutes = registerRoutes(app)
|
||||
mockRoutesLength = mockRoutes.mockRoutesLength
|
||||
mockStartIndex = mockRoutes.mockStartIndex
|
||||
|
||||
console.log(chalk.magentaBright(`\n > Mock Server hot reload success! changed ${path}`))
|
||||
} catch (error) {
|
||||
console.log(chalk.redBright(error))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
@@ -1,29 +0,0 @@
|
||||
import Mock from 'mockjs'
|
||||
|
||||
const data = Mock.mock({
|
||||
'items|30': [{
|
||||
id: '@id',
|
||||
title: '@sentence(10, 20)',
|
||||
'status|1': ['published', 'draft', 'deleted'],
|
||||
author: 'name',
|
||||
display_time: '@datetime',
|
||||
pageviews: '@integer(300, 5000)'
|
||||
}]
|
||||
})
|
||||
|
||||
export default [
|
||||
{
|
||||
url: '/table/list',
|
||||
type: 'get',
|
||||
response: config => {
|
||||
const items = data.items
|
||||
return {
|
||||
code: 20000,
|
||||
data: {
|
||||
total: items.length,
|
||||
items: items
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
@@ -1,84 +0,0 @@
|
||||
|
||||
const tokens = {
|
||||
admin: {
|
||||
token: 'admin-token'
|
||||
},
|
||||
editor: {
|
||||
token: 'editor-token'
|
||||
}
|
||||
}
|
||||
|
||||
const users = {
|
||||
'admin-token': {
|
||||
roles: ['admin'],
|
||||
introduction: 'I am a super administrator',
|
||||
avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
|
||||
name: 'Super Admin'
|
||||
},
|
||||
'editor-token': {
|
||||
roles: ['editor'],
|
||||
introduction: 'I am an editor',
|
||||
avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
|
||||
name: 'Normal Editor'
|
||||
}
|
||||
}
|
||||
|
||||
export default [
|
||||
// user login
|
||||
{
|
||||
url: '/user/login',
|
||||
type: 'post',
|
||||
response: config => {
|
||||
const { username } = config.body
|
||||
const token = tokens[username]
|
||||
|
||||
// mock error
|
||||
if (!token) {
|
||||
return {
|
||||
code: 60204,
|
||||
message: 'Account and password are incorrect.'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
code: 20000,
|
||||
data: token
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// get user info
|
||||
{
|
||||
url: '/user/info\.*',
|
||||
type: 'get',
|
||||
response: config => {
|
||||
const { token } = config.query
|
||||
const info = users[token]
|
||||
|
||||
// mock error
|
||||
if (!info) {
|
||||
return {
|
||||
code: 50008,
|
||||
message: 'Login failed, unable to get user details.'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
code: 20000,
|
||||
data: info
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// user logout
|
||||
{
|
||||
url: '/user/logout',
|
||||
type: 'post',
|
||||
response: _ => {
|
||||
return {
|
||||
code: 20000,
|
||||
data: 'success'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
@@ -1,65 +1,103 @@
|
||||
{
|
||||
"name": "sop-admin",
|
||||
"version": "4.1.0",
|
||||
"description": "sop admin",
|
||||
"author": "tanghc",
|
||||
"name": "arco-design-pro-vue",
|
||||
"description": "Arco Design Pro for Vue",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"author": "ArcoDesign Team",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "vue-cli-service serve",
|
||||
"build:prod": "vue-cli-service build",
|
||||
"build:stage": "vue-cli-service build --mode staging",
|
||||
"preview": "node build/index.js --preview",
|
||||
"lint": "eslint --ext .js,.vue src",
|
||||
"test:unit": "jest --clearCache && vue-cli-service test:unit",
|
||||
"test:ci": "npm run lint && npm run test:unit",
|
||||
"svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml"
|
||||
"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": {
|
||||
"axios": "0.18.0",
|
||||
"element-ui": "2.9.1",
|
||||
"js-cookie": "2.2.0",
|
||||
"js-md5": "^0.7.3",
|
||||
"normalize.css": "7.0.0",
|
||||
"nprogress": "0.2.0",
|
||||
"path-to-regexp": "2.4.0",
|
||||
"vue": "2.6.10",
|
||||
"vue-router": "3.0.6",
|
||||
"vuex": "3.1.0"
|
||||
"@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": {
|
||||
"@babel/core": "7.0.0",
|
||||
"@babel/register": "7.0.0",
|
||||
"@vue/cli-plugin-babel": "3.6.0",
|
||||
"@vue/cli-plugin-eslint": "3.6.0",
|
||||
"@vue/cli-plugin-unit-jest": "3.6.3",
|
||||
"@vue/cli-service": "3.6.0",
|
||||
"@vue/test-utils": "1.0.0-beta.29",
|
||||
"babel-core": "7.0.0-bridge.0",
|
||||
"babel-eslint": "10.0.1",
|
||||
"babel-jest": "23.6.0",
|
||||
"chalk": "2.4.2",
|
||||
"connect": "3.6.6",
|
||||
"eslint": "5.15.3",
|
||||
"eslint-plugin-vue": "5.2.2",
|
||||
"html-webpack-plugin": "3.2.0",
|
||||
"mockjs": "1.0.1-beta3",
|
||||
"node-sass": "^4.9.0",
|
||||
"runjs": "^4.3.2",
|
||||
"sass-loader": "^7.1.0",
|
||||
"script-ext-html-webpack-plugin": "2.1.3",
|
||||
"script-loader": "0.7.2",
|
||||
"serve-static": "^1.13.2",
|
||||
"svg-sprite-loader": "4.1.3",
|
||||
"svgo": "1.2.2",
|
||||
"vue-template-compiler": "2.6.10"
|
||||
"@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": ">=8.9",
|
||||
"npm": ">= 3.0.0"
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not ie <= 8"
|
||||
]
|
||||
"resolutions": {
|
||||
"bin-wrapper": "npm:bin-wrapper-china",
|
||||
"rollup": "^2.56.3",
|
||||
"gifsicle": "5.2.0"
|
||||
}
|
||||
}
|
||||
|
10164
sop-admin/sop-admin-frontend/pnpm-lock.yaml
generated
Normal file
Before Width: | Height: | Size: 66 KiB |
@@ -1,17 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title><%= webpackConfig.name %></title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but <%= webpackConfig.name %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"langList": [
|
||||
{
|
||||
"name": "Java"
|
||||
},
|
||||
{
|
||||
"name": "C#"
|
||||
},
|
||||
{
|
||||
"name": "C++"
|
||||
},
|
||||
{
|
||||
"name": "Go"
|
||||
},{
|
||||
"name": "NodeJS"
|
||||
},
|
||||
{
|
||||
"name": "Python"
|
||||
},
|
||||
{
|
||||
"name": "Rust"
|
||||
}
|
||||
]
|
||||
}
|
@@ -1,11 +1,26 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<a-config-provider :locale="locale">
|
||||
<router-view />
|
||||
</div>
|
||||
<global-setting />
|
||||
</a-config-provider>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'App'
|
||||
}
|
||||
<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
sop-admin/sop-admin-frontend/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 });
|
||||
}
|
77
sop-admin/sop-admin-frontend/src/api/interceptor.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
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;
|
||||
msg: 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) => {
|
||||
// let each request carry token
|
||||
// this example using the JWT token
|
||||
// Authorization is a custom headers key
|
||||
// please modify it according to the actual situation
|
||||
const token = getToken();
|
||||
if (token) {
|
||||
if (!config.headers) {
|
||||
config.headers = {};
|
||||
}
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
// do something
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
// add response interceptors
|
||||
axios.interceptors.response.use(
|
||||
(response: AxiosResponse<HttpResponse>) => {
|
||||
const res = response.data;
|
||||
// if the custom code is not 20000, it is judged as an error.
|
||||
if (res.code !== 20000) {
|
||||
Message.error({
|
||||
content: res.msg || '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.msg || 'Error'));
|
||||
}
|
||||
return res;
|
||||
},
|
||||
(error) => {
|
||||
Message.error({
|
||||
content: error.msg || 'Request Error',
|
||||
duration: 5 * 1000,
|
||||
});
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
38
sop-admin/sop-admin-frontend/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');
|
||||
}
|
@@ -1,9 +0,0 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function getList(params) {
|
||||
return request({
|
||||
url: '/table/list',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
@@ -1,24 +0,0 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function login(data) {
|
||||
return request({
|
||||
url: '/user/login',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function getInfo(token) {
|
||||
return request({
|
||||
url: '/user/info',
|
||||
method: 'get',
|
||||
params: { token }
|
||||
})
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
return request({
|
||||
url: '/user/logout',
|
||||
method: 'post'
|
||||
})
|
||||
}
|
27
sop-admin/sop-admin-frontend/src/api/user.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
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;
|
||||
}
|
||||
export function login(data: LoginData) {
|
||||
return axios.post<LoginRes>('/api/user/login', data);
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
return axios.post<LoginRes>('/api/user/logout');
|
||||
}
|
||||
|
||||
export function getUserInfo() {
|
||||
return axios.post<UserState>('/api/user/info');
|
||||
}
|
||||
|
||||
export function getMenuList() {
|
||||
return axios.post<RouteRecordNormalized[]>('/api/user/menu');
|
||||
}
|
12
sop-admin/sop-admin-frontend/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 |
@@ -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
sop-admin/sop-admin-frontend/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
sop-admin/sop-admin-frontend/src/assets/world.json
Normal file
@@ -1,78 +0,0 @@
|
||||
<template>
|
||||
<el-breadcrumb class="app-breadcrumb" separator="/">
|
||||
<transition-group name="breadcrumb">
|
||||
<el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path">
|
||||
<span v-if="item.redirect==='noRedirect'||index==levelList.length-1" class="no-redirect">{{ item.meta.title }}</span>
|
||||
<a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
|
||||
</el-breadcrumb-item>
|
||||
</transition-group>
|
||||
</el-breadcrumb>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import pathToRegexp from 'path-to-regexp'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
levelList: null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
$route() {
|
||||
this.getBreadcrumb()
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getBreadcrumb()
|
||||
},
|
||||
methods: {
|
||||
getBreadcrumb() {
|
||||
// only show routes with meta.title
|
||||
let matched = this.$route.matched.filter(item => item.meta && item.meta.title)
|
||||
const first = matched[0]
|
||||
|
||||
if (!this.isDashboard(first)) {
|
||||
matched = [{ path: '/dashboard', meta: { title: 'Dashboard' }}].concat(matched)
|
||||
}
|
||||
|
||||
this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
|
||||
},
|
||||
isDashboard(route) {
|
||||
const name = route && route.name
|
||||
if (!name) {
|
||||
return false
|
||||
}
|
||||
return name.trim().toLocaleLowerCase() === 'Dashboard'.toLocaleLowerCase()
|
||||
},
|
||||
pathCompile(path) {
|
||||
// To solve this problem https://github.com/PanJiaChen/vue-element-admin/issues/561
|
||||
const { params } = this.$route
|
||||
var toPath = pathToRegexp.compile(path)
|
||||
return toPath(params)
|
||||
},
|
||||
handleLink(item) {
|
||||
const { redirect, path } = item
|
||||
if (redirect) {
|
||||
this.$router.push(redirect)
|
||||
return
|
||||
}
|
||||
this.$router.push(this.pathCompile(path))
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-breadcrumb.el-breadcrumb {
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
line-height: 50px;
|
||||
margin-left: 8px;
|
||||
|
||||
.no-redirect {
|
||||
color: #97a8be;
|
||||
cursor: text;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -1,44 +0,0 @@
|
||||
<template>
|
||||
<div style="padding: 0 15px;" @click="toggleClick">
|
||||
<svg
|
||||
:class="{'is-active':isActive}"
|
||||
class="hamburger"
|
||||
viewBox="0 0 1024 1024"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="64"
|
||||
height="64"
|
||||
>
|
||||
<path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Hamburger',
|
||||
props: {
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleClick() {
|
||||
this.$emit('toggleClick')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.hamburger {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.hamburger.is-active {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
</style>
|
@@ -1,43 +0,0 @@
|
||||
<template>
|
||||
<svg :class="svgClass" aria-hidden="true" v-on="$listeners">
|
||||
<use :xlink:href="iconName" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SvgIcon',
|
||||
props: {
|
||||
iconClass: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
className: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
iconName() {
|
||||
return `#icon-${this.iconClass}`
|
||||
},
|
||||
svgClass() {
|
||||
if (this.className) {
|
||||
return 'svg-icon ' + this.className
|
||||
} else {
|
||||
return 'svg-icon'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.svg-icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
vertical-align: -0.15em;
|
||||
fill: currentColor;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
@@ -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
sop-admin/sop-admin-frontend/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
sop-admin/sop-admin-frontend/src/components/footer/index.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<a-layout-footer class="footer">Arco Pro</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>
|
@@ -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>
|
@@ -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>
|
@@ -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
sop-admin/sop-admin-frontend/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);
|
||||
},
|
||||
};
|
160
sop-admin/sop-admin-frontend/src/components/menu/index.vue
Normal file
@@ -0,0 +1,160 @@
|
||||
<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];
|
||||
|
||||
selectedKey.value = [
|
||||
activeMenu || menuOpenKeys[menuOpenKeys.length - 1],
|
||||
];
|
||||
}
|
||||
}, 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={false}
|
||||
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>
|
@@ -0,0 +1,69 @@
|
||||
import { computed } from 'vue';
|
||||
import { RouteRecordRaw, RouteRecordNormalized } from 'vue-router';
|
||||
import usePermission from '@/hooks/permission';
|
||||
import { useAppStore } from '@/store';
|
||||
import appClientMenus from '@/router/app-menus';
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
export default function useMenuTree() {
|
||||
const permission = usePermission();
|
||||
const appStore = useAppStore();
|
||||
const appRoute = computed(() => {
|
||||
if (appStore.menuFromServer) {
|
||||
return appStore.appAsyncMenus;
|
||||
}
|
||||
return appClientMenus;
|
||||
});
|
||||
const menuTree = computed(() => {
|
||||
const copyRouter = cloneDeep(appRoute.value) as RouteRecordNormalized[];
|
||||
copyRouter.sort((a: RouteRecordNormalized, b: RouteRecordNormalized) => {
|
||||
return (a.meta.order || 0) - (b.meta.order || 0);
|
||||
});
|
||||
function travel(_routes: RouteRecordRaw[], layer: number) {
|
||||
if (!_routes) return null;
|
||||
|
||||
const collector: any = _routes.map((element) => {
|
||||
// no access
|
||||
if (!permission.accessRouter(element)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// leaf node
|
||||
if (element.meta?.hideChildrenInMenu || !element.children) {
|
||||
element.children = [];
|
||||
return element;
|
||||
}
|
||||
|
||||
// route filter hideInMenu true
|
||||
element.children = element.children.filter(
|
||||
(x) => x.meta?.hideInMenu !== true
|
||||
);
|
||||
|
||||
// Associated child node
|
||||
const subItem = travel(element.children, layer + 1);
|
||||
|
||||
if (subItem.length) {
|
||||
element.children = subItem;
|
||||
return element;
|
||||
}
|
||||
// the else logic
|
||||
if (layer > 1) {
|
||||
element.children = subItem;
|
||||
return element;
|
||||
}
|
||||
|
||||
if (element.meta?.hideInMenu === false) {
|
||||
return element;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
return collector.filter(Boolean);
|
||||
}
|
||||
return travel(copyRouter, 0);
|
||||
});
|
||||
|
||||
return {
|
||||
menuTree,
|
||||
};
|
||||
}
|
@@ -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
sop-admin/sop-admin-frontend/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>
|
@@ -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',
|
||||
};
|
@@ -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': '登出登录',
|
||||
};
|
323
sop-admin/sop-admin-frontend/src/components/navbar/index.vue
Normal file
@@ -0,0 +1,323 @@
|
||||
<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"
|
||||
>
|
||||
Arco Pro
|
||||
</a-typography-title>
|
||||
<icon-menu-fold
|
||||
v-if="!topMenu && appStore.device === 'mobile'"
|
||||
style="font-size: 22px; cursor: pointer"
|
||||
@click="toggleDrawerMenu"
|
||||
/>
|
||||
</a-space>
|
||||
</div>
|
||||
<div class="center-side">
|
||||
<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 Menu from '@/components/menu/index.vue';
|
||||
import MessageBox from '../message-box/index.vue';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
.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
sop-admin/sop-admin-frontend/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>
|
@@ -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
sop-admin/sop-admin-frontend/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>
|
17
sop-admin/sop-admin-frontend/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
sop-admin/sop-admin-frontend/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);
|
||||
},
|
||||
};
|
@@ -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
sop-admin/sop-admin-frontend/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
sop-admin/sop-admin-frontend/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
sop-admin/sop-admin-frontend/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
sop-admin/sop-admin-frontend/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,
|
||||
};
|
||||
}
|
33
sop-admin/sop-admin-frontend/src/hooks/permission.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { RouteLocationNormalized, RouteRecordRaw } from 'vue-router';
|
||||
import { useUserStore } from '@/store';
|
||||
|
||||
export default function usePermission() {
|
||||
const userStore = useUserStore();
|
||||
return {
|
||||
accessRouter(route: RouteLocationNormalized | RouteRecordRaw) {
|
||||
return (
|
||||
!route.meta?.requiresAuth ||
|
||||
!route.meta?.roles ||
|
||||
route.meta?.roles?.includes('*') ||
|
||||
route.meta?.roles?.includes(userStore.role)
|
||||
);
|
||||
},
|
||||
findFirstPermissionRoute(_routers: any, role = 'admin') {
|
||||
const cloneRouters = [..._routers];
|
||||
while (cloneRouters.length) {
|
||||
const firstElement = cloneRouters.shift();
|
||||
if (
|
||||
firstElement?.meta?.roles?.find((el: string[]) => {
|
||||
return el.includes('*') || el.includes(role);
|
||||
})
|
||||
)
|
||||
return { name: firstElement.name };
|
||||
if (firstElement?.children) {
|
||||
cloneRouters.push(...firstElement.children);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
// You can add any rules you want
|
||||
};
|
||||
}
|
26
sop-admin/sop-admin-frontend/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
sop-admin/sop-admin-frontend/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
sop-admin/sop-admin-frontend/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
sop-admin/sop-admin-frontend/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
sop-admin/sop-admin-frontend/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,
|
||||
};
|
||||
}
|
@@ -1,9 +0,0 @@
|
||||
import Vue from 'vue'
|
||||
import SvgIcon from '@/components/SvgIcon'// svg component
|
||||
|
||||
// register globally
|
||||
Vue.component('svg-icon', SvgIcon)
|
||||
|
||||
const req = require.context('./svg', false, /\.svg$/)
|
||||
const requireAll = requireContext => requireContext.keys().map(requireContext)
|
||||
requireAll(req)
|
@@ -1 +0,0 @@
|
||||
<svg width="128" height="100" xmlns="http://www.w3.org/2000/svg"><path d="M27.429 63.638c0-2.508-.893-4.65-2.679-6.424-1.786-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.465 2.662-1.785 1.774-2.678 3.916-2.678 6.424 0 2.508.893 4.65 2.678 6.424 1.786 1.775 3.94 2.662 6.465 2.662 2.524 0 4.678-.887 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zm13.714-31.801c0-2.508-.893-4.65-2.679-6.424-1.785-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zM71.714 65.98l7.215-27.116c.285-1.23.107-2.378-.536-3.443-.643-1.064-1.56-1.762-2.75-2.094-1.19-.33-2.333-.177-3.429.462-1.095.639-1.81 1.573-2.143 2.804l-7.214 27.116c-2.857.237-5.405 1.266-7.643 3.088-2.238 1.822-3.738 4.152-4.5 6.992-.952 3.644-.476 7.098 1.429 10.364 1.905 3.265 4.69 5.37 8.357 6.317 3.667.947 7.143.474 10.429-1.42 3.285-1.892 5.404-4.66 6.357-8.305.762-2.84.619-5.607-.429-8.305-1.047-2.697-2.762-4.85-5.143-6.46zm47.143-2.342c0-2.508-.893-4.65-2.678-6.424-1.786-1.775-3.94-2.662-6.465-2.662-2.524 0-4.678.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.786 1.775 3.94 2.662 6.464 2.662 2.524 0 4.679-.887 6.465-2.662 1.785-1.775 2.678-3.916 2.678-6.424zm-45.714-45.43c0-2.509-.893-4.65-2.679-6.425C68.68 10.01 66.524 9.122 64 9.122c-2.524 0-4.679.887-6.464 2.661-1.786 1.775-2.679 3.916-2.679 6.425 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zm32 13.629c0-2.508-.893-4.65-2.679-6.424-1.785-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zM128 63.638c0 12.351-3.357 23.78-10.071 34.286-.905 1.372-2.19 2.058-3.858 2.058H13.93c-1.667 0-2.953-.686-3.858-2.058C3.357 87.465 0 76.037 0 63.638c0-8.613 1.69-16.847 5.071-24.703C8.452 31.08 13 24.312 18.714 18.634c5.715-5.68 12.524-10.199 20.429-13.559C47.048 1.715 55.333.035 64 .035c8.667 0 16.952 1.68 24.857 5.04 7.905 3.36 14.714 7.88 20.429 13.559 5.714 5.678 10.262 12.446 13.643 20.301 3.38 7.856 5.071 16.09 5.071 24.703z"/></svg>
|
Before Width: | Height: | Size: 2.3 KiB |
@@ -1 +0,0 @@
|
||||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M96.258 57.462h31.421C124.794 27.323 100.426 2.956 70.287.07v31.422a32.856 32.856 0 0 1 25.971 25.97zm-38.796-25.97V.07C27.323 2.956 2.956 27.323.07 57.462h31.422a32.856 32.856 0 0 1 25.97-25.97zm12.825 64.766v31.421c30.46-2.885 54.507-27.253 57.713-57.712H96.579c-2.886 13.466-13.146 23.726-26.292 26.291zM31.492 70.287H.07c2.886 30.46 27.253 54.507 57.713 57.713V96.579c-13.466-2.886-23.726-13.146-26.291-26.292z"/></svg>
|
Before Width: | Height: | Size: 497 B |
@@ -1 +0,0 @@
|
||||
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="128" height="128"><defs><style/></defs><path d="M512 128q69.675 0 135.51 21.163t115.498 54.997 93.483 74.837 73.685 82.006 51.67 74.837 32.17 54.827L1024 512q-2.347 4.992-6.315 13.483T998.87 560.17t-31.658 51.669-44.331 59.99-56.832 64.34-69.504 60.16-82.347 51.5-94.848 34.687T512 896q-69.675 0-135.51-21.163t-115.498-54.826-93.483-74.326-73.685-81.493-51.67-74.496-32.17-54.997L0 513.707q2.347-4.992 6.315-13.483t18.816-34.816 31.658-51.84 44.331-60.33 56.832-64.683 69.504-60.331 82.347-51.84 94.848-34.816T512 128.085zm0 85.333q-46.677 0-91.648 12.331t-81.152 31.83-70.656 47.146-59.648 54.485-48.853 57.686-37.675 52.821-26.325 43.99q12.33 21.674 26.325 43.52t37.675 52.351 48.853 57.003 59.648 53.845T339.2 767.02t81.152 31.488T512 810.667t91.648-12.331 81.152-31.659 70.656-46.848 59.648-54.186 48.853-57.344 37.675-52.651T927.957 512q-12.33-21.675-26.325-43.648t-37.675-52.65-48.853-57.345-59.648-54.186-70.656-46.848-81.152-31.659T512 213.334zm0 128q70.656 0 120.661 50.006T682.667 512 632.66 632.661 512 682.667 391.339 632.66 341.333 512t50.006-120.661T512 341.333zm0 85.334q-35.328 0-60.33 25.002T426.666 512t25.002 60.33T512 597.334t60.33-25.002T597.334 512t-25.002-60.33T512 426.666z"/></svg>
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1 +0,0 @@
|
||||
<svg width="128" height="64" xmlns="http://www.w3.org/2000/svg"><path d="M127.072 7.994c1.37-2.208.914-5.152-.914-6.87-2.056-1.717-4.797-1.226-6.396.982-.229.245-25.586 32.382-55.74 32.382-29.24 0-55.74-32.382-55.968-32.627-1.6-1.963-4.57-2.208-6.397-.49C-.17 3.086-.399 6.275 1.2 8.238c.457.736 5.94 7.36 14.62 14.72L4.17 35.96c-1.828 1.963-1.6 5.152.228 6.87.457.98 1.6 1.471 2.742 1.471s2.284-.49 3.198-1.472l12.564-13.983c5.94 4.416 13.021 8.587 20.788 11.53l-4.797 17.418c-.685 2.699.686 5.397 3.198 6.133h1.37c2.057 0 3.884-1.472 4.341-3.68L52.6 42.83c3.655.736 7.538 1.227 11.422 1.227 3.883 0 7.767-.49 11.422-1.227l4.797 17.173c.457 2.208 2.513 3.68 4.34 3.68.457 0 .914 0 1.143-.246 2.513-.736 3.883-3.434 3.198-6.133l-4.797-17.172c7.767-2.944 14.848-7.114 20.788-11.53l12.336 13.738c.913.981 2.056 1.472 3.198 1.472s2.284-.49 3.198-1.472c1.828-1.963 1.828-4.906.228-6.87l-11.65-13.001c9.366-7.36 14.849-14.474 14.849-14.474z"/></svg>
|
Before Width: | Height: | Size: 944 B |
@@ -1 +0,0 @@
|
||||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M84.068 23.784c-1.02 0-1.877-.32-2.572-.96a8.588 8.588 0 0 1-1.738-2.237 11.524 11.524 0 0 1-1.042-2.621c-.232-.895-.348-1.641-.348-2.238V0h.278c.834 0 1.622.085 2.363.256.742.17 1.645.575 2.711 1.214 1.066.64 2.363 1.535 3.892 2.686 1.53 1.15 3.453 2.664 5.77 4.54 2.502 2.045 4.494 3.771 5.977 5.178 1.483 1.406 2.618 2.6 3.406 3.58.787.98 1.274 1.812 1.46 2.494.185.682.277 1.278.277 1.79v2.046H84.068zM127.3 84.01c.278.682.464 1.535.556 2.558.093 1.023-.37 2.003-1.39 2.94-.463.427-.88.832-1.25 1.215-.372.384-.696.704-.974.96a6.69 6.69 0 0 1-.973.767l-11.816-10.741a44.331 44.331 0 0 0 1.877-1.535 31.028 31.028 0 0 1 1.737-1.406c1.112-.938 2.317-1.343 3.615-1.215 1.297.128 2.363.405 3.197.83.927.427 1.923 1.173 2.989 2.239 1.065 1.065 1.876 2.195 2.432 3.388zM78.23 95.902c2.038 0 3.752-.511 5.143-1.534l-26.969 25.83H18.037c-1.761 0-3.684-.47-5.77-1.407a24.549 24.549 0 0 1-5.838-3.709 21.373 21.373 0 0 1-4.518-5.306c-1.204-2.003-1.807-4.07-1.807-6.202V16.495c0-1.79.44-3.665 1.32-5.626A18.41 18.41 0 0 1 5.04 5.562a21.798 21.798 0 0 1 5.213-3.964C12.198.533 14.237 0 16.37 0h53.24v15.984c0 1.62.278 3.367.834 5.242a16.704 16.704 0 0 0 2.572 5.179c1.159 1.577 2.665 2.898 4.518 3.964 1.853 1.066 4.078 1.598 6.673 1.598h20.295v42.325L85.458 92.45c1.02-1.364 1.529-2.856 1.529-4.476 0-2.216-.857-4.113-2.572-5.69-1.714-1.577-3.776-2.366-6.186-2.366H26.1c-2.409 0-4.448.789-6.116 2.366-1.668 1.577-2.502 3.474-2.502 5.69 0 2.217.834 4.092 2.502 5.626 1.668 1.535 3.707 2.302 6.117 2.302h52.13zM26.1 47.951c-2.41 0-4.449.789-6.117 2.366-1.668 1.577-2.502 3.473-2.502 5.69 0 2.216.834 4.092 2.502 5.626 1.668 1.534 3.707 2.302 6.117 2.302h52.13c2.409 0 4.47-.768 6.185-2.302 1.715-1.534 2.572-3.41 2.572-5.626 0-2.217-.857-4.113-2.572-5.69-1.714-1.577-3.776-2.366-6.186-2.366H26.1zm52.407 64.063l1.807-1.663 3.476-3.196a479.75 479.75 0 0 0 4.587-4.284 500.757 500.757 0 0 1 5.004-4.667c3.985-3.666 8.48-7.758 13.485-12.276l11.677 10.741-13.485 12.404-5.004 4.603-4.587 4.22a179.46 179.46 0 0 0-3.267 3.068c-.88.853-1.367 1.322-1.46 1.407-.463.341-.973.703-1.529 1.087-.556.383-1.112.703-1.668.959-.556.256-1.413.575-2.572.959a83.5 83.5 0 0 1-3.545 1.087 72.2 72.2 0 0 1-3.475.895c-1.112.256-1.946.426-2.502.511-1.112.17-1.854.043-2.224-.383-.371-.426-.464-1.151-.278-2.174.092-.511.278-1.279.556-2.302.278-1.023.602-2.067.973-3.132l1.042-3.005c.325-.938.58-1.577.765-1.918a10.157 10.157 0 0 1 2.224-2.941z"/></svg>
|
Before Width: | Height: | Size: 2.4 KiB |
@@ -1 +0,0 @@
|
||||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M115.625 127.937H.063V12.375h57.781v12.374H12.438v90.813h90.813V70.156h12.374z"/><path d="M116.426 2.821l8.753 8.753-56.734 56.734-8.753-8.745z"/><path d="M127.893 37.982h-12.375V12.375H88.706V0h39.187z"/></svg>
|
Before Width: | Height: | Size: 285 B |
@@ -1 +0,0 @@
|
||||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M.002 9.2c0 5.044 3.58 9.133 7.998 9.133 4.417 0 7.997-4.089 7.997-9.133 0-5.043-3.58-9.132-7.997-9.132S.002 4.157.002 9.2zM31.997.066h95.981V18.33H31.997V.066zm0 45.669c0 5.044 3.58 9.132 7.998 9.132 4.417 0 7.997-4.088 7.997-9.132 0-3.263-1.524-6.278-3.998-7.91-2.475-1.63-5.524-1.63-7.998 0-2.475 1.632-4 4.647-4 7.91zM63.992 36.6h63.986v18.265H63.992V36.6zm-31.995 82.2c0 5.043 3.58 9.132 7.998 9.132 4.417 0 7.997-4.089 7.997-9.132 0-5.044-3.58-9.133-7.997-9.133s-7.998 4.089-7.998 9.133zm31.995-9.131h63.986v18.265H63.992V109.67zm0-27.404c0 5.044 3.58 9.133 7.998 9.133 4.417 0 7.997-4.089 7.997-9.133 0-3.263-1.524-6.277-3.998-7.909-2.475-1.631-5.524-1.631-7.998 0-2.475 1.632-4 4.646-4 7.91zm31.995-9.13h31.991V91.4H95.987V73.135z"/></svg>
|
Before Width: | Height: | Size: 821 B |
@@ -1 +0,0 @@
|
||||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M108.8 44.322H89.6v-5.36c0-9.04-3.308-24.163-25.6-24.163-23.145 0-25.6 16.881-25.6 24.162v5.361H19.2v-5.36C19.2 15.281 36.798 0 64 0c27.202 0 44.8 15.281 44.8 38.961v5.361zm-32 39.356c0-5.44-5.763-9.832-12.8-9.832-7.037 0-12.8 4.392-12.8 9.832 0 3.682 2.567 6.808 6.407 8.477v11.205c0 2.718 2.875 4.962 6.4 4.962 3.524 0 6.4-2.244 6.4-4.962V92.155c3.833-1.669 6.393-4.795 6.393-8.477zM128 64v49.201c0 8.158-8.645 14.799-19.2 14.799H19.2C8.651 128 0 121.359 0 113.201V64c0-8.153 8.645-14.799 19.2-14.799h89.6c10.555 0 19.2 6.646 19.2 14.799z"/></svg>
|
Before Width: | Height: | Size: 623 B |
@@ -1 +0,0 @@
|
||||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M.006.064h127.988v31.104H.006V.064zm0 38.016h38.396v41.472H.006V38.08zm0 48.384h38.396v41.472H.006V86.464zM44.802 38.08h38.396v41.472H44.802V38.08zm0 48.384h38.396v41.472H44.802V86.464zM89.598 38.08h38.396v41.472H89.598zm0 48.384h38.396v41.472H89.598z"/><path d="M.006.064h127.988v31.104H.006V.064zm0 38.016h38.396v41.472H.006V38.08zm0 48.384h38.396v41.472H.006V86.464zM44.802 38.08h38.396v41.472H44.802V38.08zm0 48.384h38.396v41.472H44.802V86.464zM89.598 38.08h38.396v41.472H89.598zm0 48.384h38.396v41.472H89.598z"/></svg>
|
Before Width: | Height: | Size: 597 B |
@@ -1 +0,0 @@
|
||||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M126.713 90.023c.858.985 1.287 2.134 1.287 3.447v29.553c0 1.423-.429 2.6-1.287 3.53-.858.93-1.907 1.395-3.146 1.395H97.824c-1.145 0-2.146-.465-3.004-1.395-.858-.93-1.287-2.107-1.287-3.53V93.47c0-.875.19-1.696.572-2.462.382-.766.906-1.368 1.573-1.806a3.84 3.84 0 0 1 2.146-.657h9.725V69.007a3.84 3.84 0 0 0-.43-1.806 3.569 3.569 0 0 0-1.143-1.313 2.714 2.714 0 0 0-1.573-.492h-36.47v23.149h9.725c1.144 0 2.145.492 3.004 1.478.858.985 1.287 2.134 1.287 3.447v29.553c0 .876-.191 1.696-.573 2.463-.38.766-.905 1.368-1.573 1.806a3.84 3.84 0 0 1-2.145.656H51.915a3.84 3.84 0 0 1-2.145-.656c-.668-.438-1.216-1.04-1.645-1.806a4.96 4.96 0 0 1-.644-2.463V93.47c0-1.313.43-2.462 1.288-3.447.858-.986 1.907-1.478 3.146-1.478h9.582v-23.15h-37.9c-.953 0-1.74.356-2.359 1.068-.62.711-.93 1.56-.93 2.544v19.538h9.726c1.239 0 2.264.492 3.074 1.478.81.985 1.216 2.134 1.216 3.447v29.553c0 1.423-.405 2.6-1.216 3.53-.81.93-1.835 1.395-3.074 1.395H4.29c-.476 0-.93-.082-1.358-.246a4.1 4.1 0 0 1-1.144-.657 4.658 4.658 0 0 1-.93-1.067 5.186 5.186 0 0 1-.643-1.395 5.566 5.566 0 0 1-.215-1.56V93.47c0-.437.048-.875.143-1.313a3.95 3.95 0 0 1 .429-1.15c.19-.328.429-.656.715-.984.286-.329.572-.602.858-.821.286-.22.62-.383 1.001-.493.382-.11.763-.164 1.144-.164h9.726V61.619c0-.985.31-1.833.93-2.544.619-.712 1.358-1.068 2.216-1.068h44.335V39.62h-9.582c-1.24 0-2.288-.492-3.146-1.477a5.09 5.09 0 0 1-1.287-3.448V5.14c0-1.423.429-2.627 1.287-3.612.858-.985 1.907-1.477 3.146-1.477h25.743c.763 0 1.478.246 2.145.739a5.17 5.17 0 0 1 1.573 1.888c.382.766.573 1.587.573 2.462v29.553c0 1.313-.43 2.463-1.287 3.448-.859.985-1.86 1.477-3.004 1.477h-9.725v18.389h42.762c.954 0 1.74.355 2.36 1.067.62.711.93 1.56.93 2.545v26.925h9.582c1.239 0 2.288.492 3.146 1.478z"/></svg>
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1 +0,0 @@
|
||||
<svg width="130" height="130" xmlns="http://www.w3.org/2000/svg"><path d="M63.444 64.996c20.633 0 37.359-14.308 37.359-31.953 0-17.649-16.726-31.952-37.359-31.952-20.631 0-37.36 14.303-37.358 31.952 0 17.645 16.727 31.953 37.359 31.953zM80.57 75.65H49.434c-26.652 0-48.26 18.477-48.26 41.27v2.664c0 9.316 21.608 9.325 48.26 9.325H80.57c26.649 0 48.256-.344 48.256-9.325v-2.663c0-22.794-21.605-41.271-48.256-41.271z" stroke="#979797"/></svg>
|
Before Width: | Height: | Size: 440 B |
@@ -1,22 +0,0 @@
|
||||
# replace default config
|
||||
|
||||
# multipass: true
|
||||
# full: true
|
||||
|
||||
plugins:
|
||||
|
||||
# - name
|
||||
#
|
||||
# or:
|
||||
# - name: false
|
||||
# - name: true
|
||||
#
|
||||
# or:
|
||||
# - name:
|
||||
# param1: 1
|
||||
# param2: 2
|
||||
|
||||
- removeAttrs:
|
||||
attrs:
|
||||
- 'fill'
|
||||
- 'fill-rule'
|
@@ -1,31 +0,0 @@
|
||||
<template>
|
||||
<section class="app-main">
|
||||
<transition name="fade-transform" mode="out-in">
|
||||
<router-view :key="key" />
|
||||
</transition>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'AppMain',
|
||||
computed: {
|
||||
key() {
|
||||
return this.$route.fullPath
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-main {
|
||||
/*50 = navbar */
|
||||
min-height: calc(100vh - 50px);
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.fixed-header+.app-main {
|
||||
padding-top: 50px;
|
||||
}
|
||||
</style>
|