commit
fbc1b7a9a8
@ -0,0 +1 @@ |
|||||||
|
VITE_API_BASE_URL= 'http://localhost:8080' |
@ -0,0 +1,3 @@ |
|||||||
|
/*.json |
||||||
|
/*.js |
||||||
|
dist |
@ -0,0 +1,70 @@ |
|||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const path = require('path'); |
||||||
|
|
||||||
|
module.exports = { |
||||||
|
root: true, |
||||||
|
parser: 'vue-eslint-parser', |
||||||
|
parserOptions: { |
||||||
|
// Parser that checks the content of the <script> tag
|
||||||
|
parser: '@typescript-eslint/parser', |
||||||
|
sourceType: 'module', |
||||||
|
ecmaVersion: 2020, |
||||||
|
ecmaFeatures: { |
||||||
|
jsx: true, |
||||||
|
}, |
||||||
|
}, |
||||||
|
env: { |
||||||
|
'browser': true, |
||||||
|
'node': true, |
||||||
|
'vue/setup-compiler-macros': true, |
||||||
|
}, |
||||||
|
plugins: ['@typescript-eslint'], |
||||||
|
extends: [ |
||||||
|
// Airbnb JavaScript Style Guide https://github.com/airbnb/javascript
|
||||||
|
'airbnb-base', |
||||||
|
'plugin:@typescript-eslint/recommended', |
||||||
|
'plugin:import/recommended', |
||||||
|
'plugin:import/typescript', |
||||||
|
'plugin:vue/vue3-recommended', |
||||||
|
'plugin:prettier/recommended', |
||||||
|
], |
||||||
|
settings: { |
||||||
|
'import/resolver': { |
||||||
|
typescript: { |
||||||
|
project: path.resolve(__dirname, './tsconfig.json'), |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
rules: { |
||||||
|
'prettier/prettier': 1, |
||||||
|
// Vue: Recommended rules to be closed or modify
|
||||||
|
'vue/require-default-prop': 0, |
||||||
|
'vue/singleline-html-element-content-newline': 0, |
||||||
|
'vue/max-attributes-per-line': 0, |
||||||
|
// Vue: Add extra rules
|
||||||
|
'vue/custom-event-name-casing': [2, 'camelCase'], |
||||||
|
'vue/no-v-text': 1, |
||||||
|
'vue/padding-line-between-blocks': 1, |
||||||
|
'vue/require-direct-export': 1, |
||||||
|
'vue/multi-word-component-names': 0, |
||||||
|
// Allow @ts-ignore comment
|
||||||
|
'@typescript-eslint/ban-ts-comment': 0, |
||||||
|
'@typescript-eslint/no-unused-vars': 1, |
||||||
|
'@typescript-eslint/no-empty-function': 1, |
||||||
|
'@typescript-eslint/no-explicit-any': 0, |
||||||
|
'import/extensions': [ |
||||||
|
2, |
||||||
|
'ignorePackages', |
||||||
|
{ |
||||||
|
js: 'never', |
||||||
|
jsx: 'never', |
||||||
|
ts: 'never', |
||||||
|
tsx: 'never', |
||||||
|
}, |
||||||
|
], |
||||||
|
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, |
||||||
|
'no-param-reassign': 0, |
||||||
|
'prefer-regex-literals': 0, |
||||||
|
'import/no-extraneous-dependencies': 0, |
||||||
|
}, |
||||||
|
}; |
@ -0,0 +1,10 @@ |
|||||||
|
node_modules |
||||||
|
.DS_Store |
||||||
|
dist |
||||||
|
dist-ssr |
||||||
|
*.local |
||||||
|
node_modules |
||||||
|
.DS_Store |
||||||
|
dist |
||||||
|
dist-ssr |
||||||
|
*.local |
@ -0,0 +1,4 @@ |
|||||||
|
#!/bin/sh |
||||||
|
. "$(dirname "$0")/_/husky.sh" |
||||||
|
|
||||||
|
yarn commitlint --edit $1 |
@ -0,0 +1,4 @@ |
|||||||
|
#!/bin/sh |
||||||
|
. "$(dirname "$0")/_/husky.sh" |
||||||
|
|
||||||
|
npm run lint-staged |
@ -0,0 +1,7 @@ |
|||||||
|
/dist/* |
||||||
|
.local |
||||||
|
.output.js |
||||||
|
/node_modules/** |
||||||
|
|
||||||
|
**/*.svg |
||||||
|
**/*.sh |
@ -0,0 +1,9 @@ |
|||||||
|
module.exports = { |
||||||
|
tabWidth: 2, |
||||||
|
semi: true, |
||||||
|
printWidth: 80, |
||||||
|
singleQuote: true, |
||||||
|
quoteProps: 'consistent', |
||||||
|
htmlWhitespaceSensitivity: 'strict', |
||||||
|
vueIndentScriptAndStyle: true, |
||||||
|
}; |
@ -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'], |
||||||
|
}, |
||||||
|
], |
||||||
|
}, |
||||||
|
}; |
@ -0,0 +1,3 @@ |
|||||||
|
module.exports = { |
||||||
|
plugins: ['@vue/babel-plugin-jsx'], |
||||||
|
}; |
@ -0,0 +1,3 @@ |
|||||||
|
module.exports = { |
||||||
|
extends: ['@commitlint/config-conventional'], |
||||||
|
}; |
@ -0,0 +1,81 @@ |
|||||||
|
// generated by unplugin-vue-components
|
||||||
|
// We suggest you to commit this file into source control
|
||||||
|
// Read more: https://github.com/vuejs/core/pull/3399
|
||||||
|
import '@vue/runtime-core' |
||||||
|
|
||||||
|
export {} |
||||||
|
|
||||||
|
declare module '@vue/runtime-core' { |
||||||
|
export interface GlobalComponents { |
||||||
|
AAffix: typeof import('@arco-design/web-vue')['Affix'] |
||||||
|
AAlert: typeof import('@arco-design/web-vue')['Alert'] |
||||||
|
AAvatar: typeof import('@arco-design/web-vue')['Avatar'] |
||||||
|
AAvatarGroup: typeof import('@arco-design/web-vue')['AvatarGroup'] |
||||||
|
ABadge: typeof import('@arco-design/web-vue')['Badge'] |
||||||
|
ABreadcrumb: typeof import('@arco-design/web-vue')['Breadcrumb'] |
||||||
|
ABreadcrumbItem: typeof import('@arco-design/web-vue')['BreadcrumbItem'] |
||||||
|
AButton: typeof import('@arco-design/web-vue')['Button'] |
||||||
|
ACard: typeof import('@arco-design/web-vue')['Card'] |
||||||
|
ACardMeta: typeof import('@arco-design/web-vue')['CardMeta'] |
||||||
|
ACarousel: typeof import('@arco-design/web-vue')['Carousel'] |
||||||
|
ACarouselItem: typeof import('@arco-design/web-vue')['CarouselItem'] |
||||||
|
ACascader: typeof import('@arco-design/web-vue')['Cascader'] |
||||||
|
ACheckbox: typeof import('@arco-design/web-vue')['Checkbox'] |
||||||
|
ACol: typeof import('@arco-design/web-vue')['Col'] |
||||||
|
AConfigProvider: typeof import('@arco-design/web-vue')['ConfigProvider'] |
||||||
|
ADescriptions: typeof import('@arco-design/web-vue')['Descriptions'] |
||||||
|
ADivider: typeof import('@arco-design/web-vue')['Divider'] |
||||||
|
ADoption: typeof import('@arco-design/web-vue')['Doption'] |
||||||
|
ADrawer: typeof import('@arco-design/web-vue')['Drawer'] |
||||||
|
ADropdown: typeof import('@arco-design/web-vue')['Dropdown'] |
||||||
|
AForm: typeof import('@arco-design/web-vue')['Form'] |
||||||
|
AFormItem: typeof import('@arco-design/web-vue')['FormItem'] |
||||||
|
AGrid: typeof import('@arco-design/web-vue')['Grid'] |
||||||
|
AGridItem: typeof import('@arco-design/web-vue')['GridItem'] |
||||||
|
AInput: typeof import('@arco-design/web-vue')['Input'] |
||||||
|
AInputNumber: typeof import('@arco-design/web-vue')['InputNumber'] |
||||||
|
AInputPassword: typeof import('@arco-design/web-vue')['InputPassword'] |
||||||
|
AInputSearch: typeof import('@arco-design/web-vue')['InputSearch'] |
||||||
|
ALayout: typeof import('@arco-design/web-vue')['Layout'] |
||||||
|
ALayoutContent: typeof import('@arco-design/web-vue')['LayoutContent'] |
||||||
|
ALayoutFooter: typeof import('@arco-design/web-vue')['LayoutFooter'] |
||||||
|
ALayoutSider: typeof import('@arco-design/web-vue')['LayoutSider'] |
||||||
|
ALink: typeof import('@arco-design/web-vue')['Link'] |
||||||
|
AList: typeof import('@arco-design/web-vue')['List'] |
||||||
|
AListItem: typeof import('@arco-design/web-vue')['ListItem'] |
||||||
|
AListItemMeta: typeof import('@arco-design/web-vue')['ListItemMeta'] |
||||||
|
AMenu: typeof import('@arco-design/web-vue')['Menu'] |
||||||
|
AMenuItem: typeof import('@arco-design/web-vue')['MenuItem'] |
||||||
|
AOption: typeof import('@arco-design/web-vue')['Option'] |
||||||
|
APopover: typeof import('@arco-design/web-vue')['Popover'] |
||||||
|
ARadio: typeof import('@arco-design/web-vue')['Radio'] |
||||||
|
ARadioGroup: typeof import('@arco-design/web-vue')['RadioGroup'] |
||||||
|
ARangePicker: typeof import('@arco-design/web-vue')['RangePicker'] |
||||||
|
AResult: typeof import('@arco-design/web-vue')['Result'] |
||||||
|
ARow: typeof import('@arco-design/web-vue')['Row'] |
||||||
|
ASelect: typeof import('@arco-design/web-vue')['Select'] |
||||||
|
ASkeleton: typeof import('@arco-design/web-vue')['Skeleton'] |
||||||
|
ASkeletonLine: typeof import('@arco-design/web-vue')['SkeletonLine'] |
||||||
|
ASkeletonShape: typeof import('@arco-design/web-vue')['SkeletonShape'] |
||||||
|
ASpace: typeof import('@arco-design/web-vue')['Space'] |
||||||
|
ASpin: typeof import('@arco-design/web-vue')['Spin'] |
||||||
|
AStatistic: typeof import('@arco-design/web-vue')['Statistic'] |
||||||
|
AStep: typeof import('@arco-design/web-vue')['Step'] |
||||||
|
ASteps: typeof import('@arco-design/web-vue')['Steps'] |
||||||
|
ASubMenu: typeof import('@arco-design/web-vue')['SubMenu'] |
||||||
|
ASwitch: typeof import('@arco-design/web-vue')['Switch'] |
||||||
|
ATable: typeof import('@arco-design/web-vue')['Table'] |
||||||
|
ATableColumn: typeof import('@arco-design/web-vue')['TableColumn'] |
||||||
|
ATabPane: typeof import('@arco-design/web-vue')['TabPane'] |
||||||
|
ATabs: typeof import('@arco-design/web-vue')['Tabs'] |
||||||
|
ATag: typeof import('@arco-design/web-vue')['Tag'] |
||||||
|
ATextarea: typeof import('@arco-design/web-vue')['Textarea'] |
||||||
|
ATooltip: typeof import('@arco-design/web-vue')['Tooltip'] |
||||||
|
ATypographyParagraph: typeof import('@arco-design/web-vue')['TypographyParagraph'] |
||||||
|
ATypographyText: typeof import('@arco-design/web-vue')['TypographyText'] |
||||||
|
ATypographyTitle: typeof import('@arco-design/web-vue')['TypographyTitle'] |
||||||
|
AUpload: typeof import('@arco-design/web-vue')['Upload'] |
||||||
|
RouterLink: typeof import('vue-router')['RouterLink'] |
||||||
|
RouterView: typeof import('vue-router')['RouterView'] |
||||||
|
} |
||||||
|
} |
@ -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,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; |
||||||
|
} |
@ -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; |
||||||
|
} |
@ -0,0 +1,87 @@ |
|||||||
|
/** |
||||||
|
* Introduces component library styles on demand. |
||||||
|
* 按需引入组件库样式 |
||||||
|
* https://github.com/anncwb/vite-plugin-style-import
|
||||||
|
*/ |
||||||
|
|
||||||
|
import styleImport from 'vite-plugin-style-import'; |
||||||
|
|
||||||
|
export default function configStyleImportPlugin() { |
||||||
|
const styleImportPlugin = styleImport({ |
||||||
|
libs: [ |
||||||
|
{ |
||||||
|
libraryName: '@arco-design/web-vue', |
||||||
|
esModule: true, |
||||||
|
resolveStyle: (name) => { |
||||||
|
// The use of this part of the component must depend on the parent, so it can be ignored directly.
|
||||||
|
// 这部分组件的使用必须依赖父级,所以直接忽略即可。
|
||||||
|
const ignoreList = [ |
||||||
|
'config-provider', |
||||||
|
'anchor-link', |
||||||
|
'sub-menu', |
||||||
|
'menu-item', |
||||||
|
'menu-item-group', |
||||||
|
'breadcrumb-item', |
||||||
|
'form-item', |
||||||
|
'step', |
||||||
|
'card-grid', |
||||||
|
'card-meta', |
||||||
|
'collapse-panel', |
||||||
|
'collapse-item', |
||||||
|
'descriptions-item', |
||||||
|
'list-item', |
||||||
|
'list-item-meta', |
||||||
|
'table-column', |
||||||
|
'table-column-group', |
||||||
|
'tab-pane', |
||||||
|
'tab-content', |
||||||
|
'timeline-item', |
||||||
|
'tree-node', |
||||||
|
'skeleton-line', |
||||||
|
'skeleton-shape', |
||||||
|
'grid-item', |
||||||
|
'carousel-item', |
||||||
|
'doption', |
||||||
|
'option', |
||||||
|
'optgroup', |
||||||
|
'icon', |
||||||
|
]; |
||||||
|
// List of components that need to map imported styles
|
||||||
|
// 需要映射引入样式的组件列表
|
||||||
|
const replaceList = { |
||||||
|
'typography-text': 'typography', |
||||||
|
'typography-title': 'typography', |
||||||
|
'typography-paragraph': 'typography', |
||||||
|
'typography-link': 'typography', |
||||||
|
'dropdown-button': 'dropdown', |
||||||
|
'input-password': 'input', |
||||||
|
'input-search': 'input', |
||||||
|
'input-group': 'input', |
||||||
|
'radio-group': 'radio', |
||||||
|
'checkbox-group': 'checkbox', |
||||||
|
'layout-sider': 'layout', |
||||||
|
'layout-content': 'layout', |
||||||
|
'layout-footer': 'layout', |
||||||
|
'layout-header': 'layout', |
||||||
|
'month-picker': 'date-picker', |
||||||
|
'range-picker': 'date-picker', |
||||||
|
'row': 'grid', // 'grid/row.less'
|
||||||
|
'col': 'grid', // 'grid/col.less'
|
||||||
|
'avatar-group': 'avatar', |
||||||
|
'image-preview': 'image', |
||||||
|
'image-preview-group': 'image', |
||||||
|
'cascader-panel': 'cascader', |
||||||
|
}; |
||||||
|
if (ignoreList.includes(name)) return ''; |
||||||
|
// eslint-disable-next-line no-prototype-builtins
|
||||||
|
return replaceList.hasOwnProperty(name) |
||||||
|
? `@arco-design/web-vue/es/${replaceList[name]}/style/css.js` |
||||||
|
: `@arco-design/web-vue/es/${name}/style/css.js`; |
||||||
|
// less
|
||||||
|
// return `@arco-design/web-vue/es/${name}/style/index.js`;
|
||||||
|
}, |
||||||
|
}, |
||||||
|
], |
||||||
|
}); |
||||||
|
return styleImportPlugin; |
||||||
|
} |
@ -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 []; |
||||||
|
} |
@ -0,0 +1,9 @@ |
|||||||
|
/** |
||||||
|
* Whether to generate package preview |
||||||
|
* 是否生成打包报告 |
||||||
|
*/ |
||||||
|
export default {}; |
||||||
|
|
||||||
|
export function isReportMode(): boolean { |
||||||
|
return process.env.REPORT === 'true'; |
||||||
|
} |
@ -0,0 +1,45 @@ |
|||||||
|
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'; |
||||||
|
|
||||||
|
export default defineConfig({ |
||||||
|
plugins: [vue(), vueJsx(), svgLoader({ svgoConfig: {} })], |
||||||
|
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, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}); |
@ -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 |
||||||
|
); |
@ -0,0 +1,33 @@ |
|||||||
|
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 configStyleImportPlugin from './plugin/styleImport'; |
||||||
|
import configImageminPlugin from './plugin/imagemin'; |
||||||
|
|
||||||
|
export default mergeConfig( |
||||||
|
{ |
||||||
|
mode: 'production', |
||||||
|
plugins: [ |
||||||
|
configCompressPlugin('gzip'), |
||||||
|
configVisualizerPlugin(), |
||||||
|
configArcoResolverPlugin(), |
||||||
|
configStyleImportPlugin(), |
||||||
|
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 |
||||||
|
); |
@ -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> |
@ -0,0 +1,103 @@ |
|||||||
|
{ |
||||||
|
"name": "arco-design-pro-vue", |
||||||
|
"description": "Arco Design Pro for Vue", |
||||||
|
"version": "1.0.0", |
||||||
|
"private": true, |
||||||
|
"author": "ArcoDesign Team", |
||||||
|
"license": "MIT", |
||||||
|
"scripts": { |
||||||
|
"dev": "vite --config ./config/vite.config.dev.ts", |
||||||
|
"build": "vue-tsc --noEmit && vite build --config ./config/vite.config.prod.ts", |
||||||
|
"report": "cross-env REPORT=true npm run build", |
||||||
|
"preview": "npm run build && vite preview --host", |
||||||
|
"type:check": "vue-tsc --noEmit --skipLibCheck", |
||||||
|
"lint-staged": "npx lint-staged", |
||||||
|
"prepare": "husky install" |
||||||
|
}, |
||||||
|
"lint-staged": { |
||||||
|
"*.{js,ts,jsx,tsx}": [ |
||||||
|
"prettier --write", |
||||||
|
"eslint --fix" |
||||||
|
], |
||||||
|
"*.vue": [ |
||||||
|
"stylelint --fix", |
||||||
|
"prettier --write", |
||||||
|
"eslint --fix" |
||||||
|
], |
||||||
|
"*.{less,css}": [ |
||||||
|
"stylelint --fix", |
||||||
|
"prettier --write" |
||||||
|
] |
||||||
|
}, |
||||||
|
"dependencies": { |
||||||
|
"@arco-design/web-vue": "^2.40.0", |
||||||
|
"@vueuse/core": "^9.3.0", |
||||||
|
"arco-design-pro-vue": "^2.6.0", |
||||||
|
"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": { |
||||||
|
"@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": "^4.0.0", |
||||||
|
"@vitejs/plugin-vue-jsx": "^3.0.0", |
||||||
|
"@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.22.8", |
||||||
|
"vite": "^3.2.5", |
||||||
|
"vite-plugin-compression": "^0.5.1", |
||||||
|
"vite-plugin-eslint": "^1.8.1", |
||||||
|
"vite-plugin-imagemin": "^0.6.1", |
||||||
|
"vite-plugin-style-import": "1.4.1", |
||||||
|
"vite-svg-loader": "^3.6.0", |
||||||
|
"vue-tsc": "^1.0.14" |
||||||
|
}, |
||||||
|
"engines": { |
||||||
|
"node": ">=14.0.0" |
||||||
|
}, |
||||||
|
"resolutions": { |
||||||
|
"bin-wrapper": "npm:bin-wrapper-china", |
||||||
|
"rollup": "^2.56.3", |
||||||
|
"gifsicle": "5.2.0" |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,26 @@ |
|||||||
|
<template> |
||||||
|
<a-config-provider :locale="locale"> |
||||||
|
<router-view /> |
||||||
|
<global-setting /> |
||||||
|
</a-config-provider> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script lang="ts" setup> |
||||||
|
import { computed } from 'vue'; |
||||||
|
import enUS from '@arco-design/web-vue/es/locale/lang/en-us'; |
||||||
|
import zhCN from '@arco-design/web-vue/es/locale/lang/zh-cn'; |
||||||
|
import GlobalSetting from '@/components/global-setting/index.vue'; |
||||||
|
import useLocale from '@/hooks/locale'; |
||||||
|
|
||||||
|
const { currentLocale } = useLocale(); |
||||||
|
const locale = computed(() => { |
||||||
|
switch (currentLocale.value) { |
||||||
|
case 'zh-CN': |
||||||
|
return zhCN; |
||||||
|
case 'en-US': |
||||||
|
return enUS; |
||||||
|
default: |
||||||
|
return enUS; |
||||||
|
} |
||||||
|
}); |
||||||
|
</script> |
@ -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 }); |
||||||
|
} |
@ -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); |
||||||
|
} |
||||||
|
); |
@ -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'); |
||||||
|
} |
@ -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'); |
||||||
|
} |
After Width: | Height: | Size: 62 KiB |
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; |
@ -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)); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
File diff suppressed because one or more lines are too long
@ -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> |
@ -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> |
@ -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> |
@ -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); |
||||||
|
}, |
||||||
|
}; |
@ -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> |
@ -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': '登出登录', |
||||||
|
}; |
@ -0,0 +1,320 @@ |
|||||||
|
<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" |
||||||
|
> |
||||||
|
{{ 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 } = 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> |
@ -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. |
@ -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> |
@ -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": [] |
||||||
|
} |
@ -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); |
||||||
|
}, |
||||||
|
}; |
@ -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; |
||||||
|
} |
@ -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, |
||||||
|
}; |
||||||
|
} |
@ -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, |
||||||
|
}; |
||||||
|
} |
@ -0,0 +1,19 @@ |
|||||||
|
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) => { |
||||||
|
i18.locale.value = value; |
||||||
|
localStorage.setItem('arco-locale', value); |
||||||
|
Message.success(i18.t('navbar.action.locale')); |
||||||
|
}; |
||||||
|
return { |
||||||
|
currentLocale, |
||||||
|
changeLocale, |
||||||
|
}; |
||||||
|
} |
@ -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
|
||||||
|
}; |
||||||
|
} |
@ -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 }; |
||||||
|
} |
@ -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); |
||||||
|
}); |
||||||
|
} |
@ -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, |
||||||
|
}; |
||||||
|
} |
@ -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, |
||||||
|
}; |
||||||
|
} |
@ -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, |
||||||
|
}; |
||||||
|
} |
@ -0,0 +1,173 @@ |
|||||||
|
<template> |
||||||
|
<a-layout class="layout" :class="{ mobile: appStore.hideMenu }"> |
||||||
|
<div v-if="navbar" class="layout-navbar"> |
||||||
|
<NavBar /> |
||||||
|
</div> |
||||||
|
<a-layout> |
||||||
|
<a-layout> |
||||||
|
<a-layout-sider |
||||||
|
v-if="renderMenu" |
||||||
|
v-show="!hideMenu" |
||||||
|
class="layout-sider" |
||||||
|
breakpoint="xl" |
||||||
|
:collapsed="collapsed" |
||||||
|
:collapsible="true" |
||||||
|
:width="menuWidth" |
||||||
|
:style="{ paddingTop: navbar ? '60px' : '' }" |
||||||
|
:hide-trigger="true" |
||||||
|
@collapse="setCollapsed" |
||||||
|
> |
||||||
|
<div class="menu-wrapper"> |
||||||
|
<Menu /> |
||||||
|
</div> |
||||||
|
</a-layout-sider> |
||||||
|
<a-drawer |
||||||
|
v-if="hideMenu" |
||||||
|
:visible="drawerVisible" |
||||||
|
placement="left" |
||||||
|
:footer="false" |
||||||
|
mask-closable |
||||||
|
:closable="false" |
||||||
|
@cancel="drawerCancel" |
||||||
|
> |
||||||
|
<Menu /> |
||||||
|
</a-drawer> |
||||||
|
<a-layout class="layout-content" :style="paddingStyle"> |
||||||
|
<TabBar v-if="appStore.tabBar" /> |
||||||
|
<a-layout-content> |
||||||
|
<PageLayout /> |
||||||
|
</a-layout-content> |
||||||
|
<Footer v-if="footer" /> |
||||||
|
</a-layout> |
||||||
|
</a-layout> |
||||||
|
</a-layout> |
||||||
|
</a-layout> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script lang="ts" setup> |
||||||
|
import { ref, computed, watch, provide } from 'vue'; |
||||||
|
import { useRouter, useRoute } from 'vue-router'; |
||||||
|
import { useAppStore, useUserStore } from '@/store'; |
||||||
|
import NavBar from '@/components/navbar/index.vue'; |
||||||
|
import Menu from '@/components/menu/index.vue'; |
||||||
|
import Footer from '@/components/footer/index.vue'; |
||||||
|
import TabBar from '@/components/tab-bar/index.vue'; |
||||||
|
import usePermission from '@/hooks/permission'; |
||||||
|
import useResponsive from '@/hooks/responsive'; |
||||||
|
import PageLayout from './page-layout.vue'; |
||||||
|
|
||||||
|
const appStore = useAppStore(); |
||||||
|
const userStore = useUserStore(); |
||||||
|
const router = useRouter(); |
||||||
|
const route = useRoute(); |
||||||
|
const permission = usePermission(); |
||||||
|
useResponsive(true); |
||||||
|
const navbarHeight = `60px`; |
||||||
|
const navbar = computed(() => appStore.navbar); |
||||||
|
const renderMenu = computed(() => appStore.menu && !appStore.topMenu); |
||||||
|
const hideMenu = computed(() => appStore.hideMenu); |
||||||
|
const footer = computed(() => appStore.footer); |
||||||
|
const menuWidth = computed(() => { |
||||||
|
return appStore.menuCollapse ? 48 : appStore.menuWidth; |
||||||
|
}); |
||||||
|
const collapsed = computed(() => { |
||||||
|
return appStore.menuCollapse; |
||||||
|
}); |
||||||
|
const paddingStyle = computed(() => { |
||||||
|
const paddingLeft = |
||||||
|
renderMenu.value && !hideMenu.value |
||||||
|
? { paddingLeft: `${menuWidth.value}px` } |
||||||
|
: {}; |
||||||
|
const paddingTop = navbar.value ? { paddingTop: navbarHeight } : {}; |
||||||
|
return { ...paddingLeft, ...paddingTop }; |
||||||
|
}); |
||||||
|
const setCollapsed = (val: boolean) => { |
||||||
|
appStore.updateSettings({ menuCollapse: val }); |
||||||
|
}; |
||||||
|
watch( |
||||||
|
() => userStore.role, |
||||||
|
(roleValue) => { |
||||||
|
if (roleValue && !permission.accessRouter(route)) |
||||||
|
router.push({ name: 'notFound' }); |
||||||
|
} |
||||||
|
); |
||||||
|
const drawerVisible = ref(false); |
||||||
|
const drawerCancel = () => { |
||||||
|
drawerVisible.value = false; |
||||||
|
}; |
||||||
|
provide('toggleDrawerMenu', () => { |
||||||
|
drawerVisible.value = !drawerVisible.value; |
||||||
|
}); |
||||||
|
</script> |
||||||
|
|
||||||
|
<style scoped lang="less"> |
||||||
|
@nav-size-height: 60px; |
||||||
|
@layout-max-width: 1100px; |
||||||
|
|
||||||
|
.layout { |
||||||
|
width: 100%; |
||||||
|
height: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
.layout-navbar { |
||||||
|
position: fixed; |
||||||
|
top: 0; |
||||||
|
left: 0; |
||||||
|
z-index: 100; |
||||||
|
width: 100%; |
||||||
|
height: @nav-size-height; |
||||||
|
} |
||||||
|
|
||||||
|
.layout-sider { |
||||||
|
position: fixed; |
||||||
|
top: 0; |
||||||
|
left: 0; |
||||||
|
z-index: 99; |
||||||
|
height: 100%; |
||||||
|
transition: all 0.2s cubic-bezier(0.34, 0.69, 0.1, 1); |
||||||
|
&::after { |
||||||
|
position: absolute; |
||||||
|
top: 0; |
||||||
|
right: -1px; |
||||||
|
display: block; |
||||||
|
width: 1px; |
||||||
|
height: 100%; |
||||||
|
background-color: var(--color-border); |
||||||
|
content: ''; |
||||||
|
} |
||||||
|
|
||||||
|
> :deep(.arco-layout-sider-children) { |
||||||
|
overflow-y: hidden; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.menu-wrapper { |
||||||
|
height: 100%; |
||||||
|
overflow: auto; |
||||||
|
overflow-x: hidden; |
||||||
|
:deep(.arco-menu) { |
||||||
|
::-webkit-scrollbar { |
||||||
|
width: 12px; |
||||||
|
height: 4px; |
||||||
|
} |
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb { |
||||||
|
border: 4px solid transparent; |
||||||
|
background-clip: padding-box; |
||||||
|
border-radius: 7px; |
||||||
|
background-color: var(--color-text-4); |
||||||
|
} |
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover { |
||||||
|
background-color: var(--color-text-3); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.layout-content { |
||||||
|
min-height: 100vh; |
||||||
|
overflow-y: hidden; |
||||||
|
background-color: var(--color-fill-2); |
||||||
|
transition: padding 0.2s cubic-bezier(0.34, 0.69, 0.1, 1); |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,25 @@ |
|||||||
|
<template> |
||||||
|
<router-view v-slot="{ Component, route }"> |
||||||
|
<transition name="fade" mode="out-in" appear> |
||||||
|
<component |
||||||
|
:is="Component" |
||||||
|
v-if="route.meta.ignoreCache" |
||||||
|
:key="route.fullPath" |
||||||
|
/> |
||||||
|
<keep-alive v-else :include="cacheList"> |
||||||
|
<component :is="Component" :key="route.fullPath" /> |
||||||
|
</keep-alive> |
||||||
|
</transition> |
||||||
|
</router-view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script lang="ts" setup> |
||||||
|
import { computed } from 'vue'; |
||||||
|
import { useTabBarStore } from '@/store'; |
||||||
|
|
||||||
|
const tabBarStore = useTabBarStore(); |
||||||
|
|
||||||
|
const cacheList = computed(() => tabBarStore.getCacheList); |
||||||
|
</script> |
||||||
|
|
||||||
|
<style scoped lang="less"></style> |
@ -0,0 +1,28 @@ |
|||||||
|
import localeMessageBox from '@/components/message-box/locale/en-US'; |
||||||
|
import localeLogin from '@/views/login/locale/en-US'; |
||||||
|
|
||||||
|
import localeWorkplace from '@/views/dashboard/workplace/locale/en-US'; |
||||||
|
|
||||||
|
import localeSettings from './en-US/settings'; |
||||||
|
|
||||||
|
export default { |
||||||
|
'menu.dashboard': 'Dashboard', |
||||||
|
'menu.server.dashboard': 'Dashboard-Server', |
||||||
|
'menu.server.workplace': 'Workplace-Server', |
||||||
|
'menu.server.monitor': 'Monitor-Server', |
||||||
|
'menu.list': 'List', |
||||||
|
'menu.result': 'Result', |
||||||
|
'menu.exception': 'Exception', |
||||||
|
'menu.form': 'Form', |
||||||
|
'menu.profile': 'Profile', |
||||||
|
'menu.visualization': 'Data Visualization', |
||||||
|
'menu.user': 'User Center', |
||||||
|
'menu.arcoWebsite': 'Arco Design', |
||||||
|
'menu.faq': 'FAQ', |
||||||
|
'navbar.docs': 'Docs', |
||||||
|
'navbar.action.locale': 'Switch to English', |
||||||
|
...localeSettings, |
||||||
|
...localeMessageBox, |
||||||
|
...localeLogin, |
||||||
|
...localeWorkplace, |
||||||
|
}; |
@ -0,0 +1,29 @@ |
|||||||
|
export default { |
||||||
|
'settings.title': 'Settings', |
||||||
|
'settings.themeColor': 'Theme Color', |
||||||
|
'settings.content': 'Content Setting', |
||||||
|
'settings.search': 'Search', |
||||||
|
'settings.language': 'Language', |
||||||
|
'settings.navbar': 'Navbar', |
||||||
|
'settings.menuWidth': 'Menu Width (px)', |
||||||
|
'settings.navbar.theme.toLight': 'Click to use light mode', |
||||||
|
'settings.navbar.theme.toDark': 'Click to use dark mode', |
||||||
|
'settings.navbar.screen.toFull': 'Click to switch to full screen mode', |
||||||
|
'settings.navbar.screen.toExit': 'Click to exit the full screen mode', |
||||||
|
'settings.navbar.alerts': 'alerts', |
||||||
|
'settings.menu': 'Menu', |
||||||
|
'settings.topMenu': 'Top Menu', |
||||||
|
'settings.tabBar': 'Tab Bar', |
||||||
|
'settings.footer': 'Footer', |
||||||
|
'settings.otherSettings': 'Other Settings', |
||||||
|
'settings.colorWeak': 'Color Weak', |
||||||
|
'settings.alertContent': |
||||||
|
'After the configuration is only temporarily effective, if you want to really affect the project, click the "Copy Settings" button below and replace the configuration in settings.json.', |
||||||
|
'settings.copySettings': 'Copy Settings', |
||||||
|
'settings.copySettings.message': |
||||||
|
'Copy succeeded, please paste to file src/settings.json.', |
||||||
|
'settings.close': 'Close', |
||||||
|
'settings.color.tooltip': |
||||||
|
'10 gradient colors generated according to the theme color', |
||||||
|
'settings.menuFromServer': 'Menu From Server', |
||||||
|
}; |
@ -0,0 +1,21 @@ |
|||||||
|
import { createI18n } from 'vue-i18n'; |
||||||
|
import en from './en-US'; |
||||||
|
import cn from './zh-CN'; |
||||||
|
|
||||||
|
export const LOCALE_OPTIONS = [ |
||||||
|
{ label: '中文', value: 'zh-CN' }, |
||||||
|
{ label: 'English', value: 'en-US' }, |
||||||
|
]; |
||||||
|
const defaultLocale = localStorage.getItem('arco-locale') || 'zh-CN'; |
||||||
|
|
||||||
|
const i18n = createI18n({ |
||||||
|
locale: defaultLocale, |
||||||
|
fallbackLocale: 'en-US', |
||||||
|
allowComposition: true, |
||||||
|
messages: { |
||||||
|
'en-US': en, |
||||||
|
'zh-CN': cn, |
||||||
|
}, |
||||||
|
}); |
||||||
|
|
||||||
|
export default i18n; |
@ -0,0 +1,28 @@ |
|||||||
|
import localeMessageBox from '@/components/message-box/locale/zh-CN'; |
||||||
|
import localeLogin from '@/views/login/locale/zh-CN'; |
||||||
|
|
||||||
|
import localeWorkplace from '@/views/dashboard/workplace/locale/zh-CN'; |
||||||
|
|
||||||
|
import localeSettings from './zh-CN/settings'; |
||||||
|
|
||||||
|
export default { |
||||||
|
'menu.dashboard': '仪表盘', |
||||||
|
'menu.server.dashboard': '仪表盘-服务端', |
||||||
|
'menu.server.workplace': '工作台-服务端', |
||||||
|
'menu.server.monitor': '实时监控-服务端', |
||||||
|
'menu.list': '列表页', |
||||||
|
'menu.result': '结果页', |
||||||
|
'menu.exception': '异常页', |
||||||
|
'menu.form': '表单页', |
||||||
|
'menu.profile': '详情页', |
||||||
|
'menu.visualization': '数据可视化', |
||||||
|
'menu.user': '个人中心', |
||||||
|
'menu.arcoWebsite': 'Arco Design', |
||||||
|
'menu.faq': '常见问题', |
||||||
|
'navbar.docs': '文档中心', |
||||||
|
'navbar.action.locale': '切换为中文', |
||||||
|
...localeSettings, |
||||||
|
...localeMessageBox, |
||||||
|
...localeLogin, |
||||||
|
...localeWorkplace, |
||||||
|
}; |
@ -0,0 +1,29 @@ |
|||||||
|
export default { |
||||||
|
'settings.title': '页面配置', |
||||||
|
'settings.themeColor': '主题色', |
||||||
|
'settings.content': '内容区域', |
||||||
|
'settings.search': '搜索', |
||||||
|
'settings.language': '语言', |
||||||
|
'settings.navbar': '导航栏', |
||||||
|
'settings.menuWidth': '菜单宽度 (px)', |
||||||
|
'settings.navbar.theme.toLight': '点击切换为亮色模式', |
||||||
|
'settings.navbar.theme.toDark': '点击切换为暗黑模式', |
||||||
|
'settings.navbar.screen.toFull': '点击切换全屏模式', |
||||||
|
'settings.navbar.screen.toExit': '点击退出全屏模式', |
||||||
|
'settings.navbar.alerts': '消息通知', |
||||||
|
'settings.menu': '菜单栏', |
||||||
|
'settings.topMenu': '顶部菜单栏', |
||||||
|
'settings.tabBar': '多页签', |
||||||
|
'settings.footer': '底部', |
||||||
|
'settings.otherSettings': '其他设置', |
||||||
|
'settings.colorWeak': '色弱模式', |
||||||
|
'settings.alertContent': |
||||||
|
'配置之后仅是临时生效,要想真正作用于项目,点击下方的 "复制配置" 按钮,将配置替换到 settings.json 中即可。', |
||||||
|
'settings.copySettings': '复制配置', |
||||||
|
'settings.copySettings.message': |
||||||
|
'复制成功,请粘贴到 src/settings.json 文件中', |
||||||
|
'settings.close': '关闭', |
||||||
|
'settings.color.tooltip': |
||||||
|
'根据主题颜色生成的 10 个梯度色(将配置复制到项目中,主题色才能对亮色 / 暗黑模式同时生效)', |
||||||
|
'settings.menuFromServer': '菜单来源于后台', |
||||||
|
}; |
@ -0,0 +1,26 @@ |
|||||||
|
import { createApp } from 'vue'; |
||||||
|
import ArcoVue from '@arco-design/web-vue'; |
||||||
|
import ArcoVueIcon from '@arco-design/web-vue/es/icon'; |
||||||
|
import globalComponents from '@/components'; |
||||||
|
import router from './router'; |
||||||
|
import store from './store'; |
||||||
|
import i18n from './locale'; |
||||||
|
import directive from './directive'; |
||||||
|
import './mock'; |
||||||
|
import App from './App.vue'; |
||||||
|
import '@arco-design/web-vue/dist/arco.css'; |
||||||
|
import '@/assets/style/global.less'; |
||||||
|
import '@/api/interceptor'; |
||||||
|
|
||||||
|
const app = createApp(App); |
||||||
|
|
||||||
|
app.use(ArcoVue, {}); |
||||||
|
app.use(ArcoVueIcon); |
||||||
|
|
||||||
|
app.use(router); |
||||||
|
app.use(store); |
||||||
|
app.use(i18n); |
||||||
|
app.use(globalComponents); |
||||||
|
app.use(directive); |
||||||
|
|
||||||
|
app.mount('#app'); |
@ -0,0 +1,10 @@ |
|||||||
|
import Mock from 'mockjs'; |
||||||
|
|
||||||
|
import './user'; |
||||||
|
import './message-box'; |
||||||
|
|
||||||
|
import '@/views/dashboard/workplace/mock'; |
||||||
|
|
||||||
|
Mock.setup({ |
||||||
|
timeout: '600-1000', |
||||||
|
}); |
@ -0,0 +1,85 @@ |
|||||||
|
import Mock from 'mockjs'; |
||||||
|
import setupMock, { successResponseWrap } from '@/utils/setup-mock'; |
||||||
|
|
||||||
|
const haveReadIds: number[] = []; |
||||||
|
const getMessageList = () => { |
||||||
|
return [ |
||||||
|
{ |
||||||
|
id: 1, |
||||||
|
type: 'message', |
||||||
|
title: '郑曦月', |
||||||
|
subTitle: '的私信', |
||||||
|
avatar: |
||||||
|
'//p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/8361eeb82904210b4f55fab888fe8416.png~tplv-uwbnlip3yd-webp.webp', |
||||||
|
content: '审批请求已发送,请查收', |
||||||
|
time: '今天 12:30:01', |
||||||
|
}, |
||||||
|
{ |
||||||
|
id: 2, |
||||||
|
type: 'message', |
||||||
|
title: '宁波', |
||||||
|
subTitle: '的回复', |
||||||
|
avatar: |
||||||
|
'//p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/3ee5f13fb09879ecb5185e440cef6eb9.png~tplv-uwbnlip3yd-webp.webp', |
||||||
|
content: '此处 bug 已经修复', |
||||||
|
time: '今天 12:30:01', |
||||||
|
}, |
||||||
|
{ |
||||||
|
id: 3, |
||||||
|
type: 'message', |
||||||
|
title: '宁波', |
||||||
|
subTitle: '的回复', |
||||||
|
avatar: |
||||||
|
'//p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/3ee5f13fb09879ecb5185e440cef6eb9.png~tplv-uwbnlip3yd-webp.webp', |
||||||
|
content: '此处 bug 已经修复', |
||||||
|
time: '今天 12:20:01', |
||||||
|
}, |
||||||
|
{ |
||||||
|
id: 4, |
||||||
|
type: 'notice', |
||||||
|
title: '续费通知', |
||||||
|
subTitle: '', |
||||||
|
avatar: '', |
||||||
|
content: '您的产品使用期限即将截止,如需继续使用产品请前往购…', |
||||||
|
time: '今天 12:20:01', |
||||||
|
messageType: 3, |
||||||
|
}, |
||||||
|
{ |
||||||
|
id: 5, |
||||||
|
type: 'notice', |
||||||
|
title: '规则开通成功', |
||||||
|
subTitle: '', |
||||||
|
avatar: '', |
||||||
|
content: '内容屏蔽规则于 2021-12-01 开通成功并生效', |
||||||
|
time: '今天 12:20:01', |
||||||
|
messageType: 1, |
||||||
|
}, |
||||||
|
{ |
||||||
|
id: 6, |
||||||
|
type: 'todo', |
||||||
|
title: '质检队列变更', |
||||||
|
subTitle: '', |
||||||
|
avatar: '', |
||||||
|
content: '内容质检队列于 2021-12-01 19:50:23 进行变更,请重新…', |
||||||
|
time: '今天 12:20:01', |
||||||
|
messageType: 0, |
||||||
|
}, |
||||||
|
].map((item) => ({ |
||||||
|
...item, |
||||||
|
status: haveReadIds.indexOf(item.id) === -1 ? 0 : 1, |
||||||
|
})); |
||||||
|
}; |
||||||
|
|
||||||
|
setupMock({ |
||||||
|
setup: () => { |
||||||
|
Mock.mock(new RegExp('/api/message/list'), () => { |
||||||
|
return successResponseWrap(getMessageList()); |
||||||
|
}); |
||||||
|
|
||||||
|
Mock.mock(new RegExp('/api/message/read'), (params: { body: string }) => { |
||||||
|
const { ids } = JSON.parse(params.body); |
||||||
|
haveReadIds.push(...(ids || [])); |
||||||
|
return successResponseWrap(true); |
||||||
|
}); |
||||||
|
}, |
||||||
|
}); |
@ -0,0 +1,105 @@ |
|||||||
|
import Mock from 'mockjs'; |
||||||
|
import setupMock, { |
||||||
|
successResponseWrap, |
||||||
|
failResponseWrap, |
||||||
|
} from '@/utils/setup-mock'; |
||||||
|
|
||||||
|
import { MockParams } from '@/types/mock'; |
||||||
|
import { isLogin } from '@/utils/auth'; |
||||||
|
|
||||||
|
setupMock({ |
||||||
|
setup() { |
||||||
|
// Mock.XHR.prototype.withCredentials = true;
|
||||||
|
|
||||||
|
// 用户信息
|
||||||
|
Mock.mock(new RegExp('/api/user/info'), () => { |
||||||
|
if (isLogin()) { |
||||||
|
const role = window.localStorage.getItem('userRole') || 'admin'; |
||||||
|
return successResponseWrap({ |
||||||
|
name: '王立群', |
||||||
|
avatar: |
||||||
|
'//lf1-xgcdn-tos.pstatp.com/obj/vcloud/vadmin/start.8e0e4855ee346a46ccff8ff3e24db27b.png', |
||||||
|
email: 'wangliqun@email.com', |
||||||
|
job: 'frontend', |
||||||
|
jobName: '前端艺术家', |
||||||
|
organization: 'Frontend', |
||||||
|
organizationName: '前端', |
||||||
|
location: 'beijing', |
||||||
|
locationName: '北京', |
||||||
|
introduction: '人潇洒,性温存', |
||||||
|
personalWebsite: 'https://www.arco.design', |
||||||
|
phone: '150****0000', |
||||||
|
registrationDate: '2013-05-10 12:10:00', |
||||||
|
accountId: '15012312300', |
||||||
|
certification: 1, |
||||||
|
role, |
||||||
|
}); |
||||||
|
} |
||||||
|
return failResponseWrap(null, '未登录', 50008); |
||||||
|
}); |
||||||
|
|
||||||
|
// 登录
|
||||||
|
Mock.mock(new RegExp('/api/user/login'), (params: MockParams) => { |
||||||
|
const { username, password } = JSON.parse(params.body); |
||||||
|
if (!username) { |
||||||
|
return failResponseWrap(null, '用户名不能为空', 50000); |
||||||
|
} |
||||||
|
if (!password) { |
||||||
|
return failResponseWrap(null, '密码不能为空', 50000); |
||||||
|
} |
||||||
|
if (username === 'admin' && password === 'admin') { |
||||||
|
window.localStorage.setItem('userRole', 'admin'); |
||||||
|
return successResponseWrap({ |
||||||
|
token: '12345', |
||||||
|
}); |
||||||
|
} |
||||||
|
if (username === 'user' && password === 'user') { |
||||||
|
window.localStorage.setItem('userRole', 'user'); |
||||||
|
return successResponseWrap({ |
||||||
|
token: '54321', |
||||||
|
}); |
||||||
|
} |
||||||
|
return failResponseWrap(null, '账号或者密码错误', 50000); |
||||||
|
}); |
||||||
|
|
||||||
|
// 登出
|
||||||
|
Mock.mock(new RegExp('/api/user/logout'), () => { |
||||||
|
return successResponseWrap(null); |
||||||
|
}); |
||||||
|
|
||||||
|
// 用户的服务端菜单
|
||||||
|
Mock.mock(new RegExp('/api/user/menu'), () => { |
||||||
|
const menuList = [ |
||||||
|
{ |
||||||
|
path: '/dashboard', |
||||||
|
name: 'dashboard', |
||||||
|
meta: { |
||||||
|
locale: 'menu.server.dashboard', |
||||||
|
requiresAuth: true, |
||||||
|
icon: 'icon-dashboard', |
||||||
|
order: 1, |
||||||
|
}, |
||||||
|
children: [ |
||||||
|
{ |
||||||
|
path: 'workplace', |
||||||
|
name: 'Workplace', |
||||||
|
meta: { |
||||||
|
locale: 'menu.server.workplace', |
||||||
|
requiresAuth: true, |
||||||
|
}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
path: 'https://arco.design', |
||||||
|
name: 'arcoWebsite', |
||||||
|
meta: { |
||||||
|
locale: 'menu.arcoWebsite', |
||||||
|
requiresAuth: true, |
||||||
|
}, |
||||||
|
}, |
||||||
|
], |
||||||
|
}, |
||||||
|
]; |
||||||
|
return successResponseWrap(menuList); |
||||||
|
}); |
||||||
|
}, |
||||||
|
}); |
@ -0,0 +1,16 @@ |
|||||||
|
import { appRoutes, appExternalRoutes } from '../routes'; |
||||||
|
|
||||||
|
const mixinRoutes = [...appRoutes, ...appExternalRoutes]; |
||||||
|
|
||||||
|
const appClientMenus = mixinRoutes.map((el) => { |
||||||
|
const { name, path, meta, redirect, children } = el; |
||||||
|
return { |
||||||
|
name, |
||||||
|
path, |
||||||
|
meta, |
||||||
|
redirect, |
||||||
|
children, |
||||||
|
}; |
||||||
|
}); |
||||||
|
|
||||||
|
export default appClientMenus; |
@ -0,0 +1,18 @@ |
|||||||
|
export const WHITE_LIST = [ |
||||||
|
{ name: 'notFound', children: [] }, |
||||||
|
{ name: 'login', children: [] }, |
||||||
|
]; |
||||||
|
|
||||||
|
export const NOT_FOUND = { |
||||||
|
name: 'notFound', |
||||||
|
}; |
||||||
|
|
||||||
|
export const REDIRECT_ROUTE_NAME = 'Redirect'; |
||||||
|
|
||||||
|
export const DEFAULT_ROUTE_NAME = 'Workplace'; |
||||||
|
|
||||||
|
export const DEFAULT_ROUTE = { |
||||||
|
title: 'menu.dashboard.workplace', |
||||||
|
name: DEFAULT_ROUTE_NAME, |
||||||
|
fullPath: '/dashboard/workplace', |
||||||
|
}; |
@ -0,0 +1,17 @@ |
|||||||
|
import type { Router } from 'vue-router'; |
||||||
|
import { setRouteEmitter } from '@/utils/route-listener'; |
||||||
|
import setupUserLoginInfoGuard from './userLoginInfo'; |
||||||
|
import setupPermissionGuard from './permission'; |
||||||
|
|
||||||
|
function setupPageGuard(router: Router) { |
||||||
|
router.beforeEach(async (to) => { |
||||||
|
// emit route change
|
||||||
|
setRouteEmitter(to); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
export default function createRouteGuard(router: Router) { |
||||||
|
setupPageGuard(router); |
||||||
|
setupUserLoginInfoGuard(router); |
||||||
|
setupPermissionGuard(router); |
||||||
|
} |
@ -0,0 +1,55 @@ |
|||||||
|
import type { Router, RouteRecordNormalized } from 'vue-router'; |
||||||
|
import NProgress from 'nprogress'; // progress bar
|
||||||
|
|
||||||
|
import usePermission from '@/hooks/permission'; |
||||||
|
import { useUserStore, useAppStore } from '@/store'; |
||||||
|
import { appRoutes } from '../routes'; |
||||||
|
import { WHITE_LIST, NOT_FOUND } from '../constants'; |
||||||
|
|
||||||
|
export default function setupPermissionGuard(router: Router) { |
||||||
|
router.beforeEach(async (to, from, next) => { |
||||||
|
const appStore = useAppStore(); |
||||||
|
const userStore = useUserStore(); |
||||||
|
const Permission = usePermission(); |
||||||
|
const permissionsAllow = Permission.accessRouter(to); |
||||||
|
if (appStore.menuFromServer) { |
||||||
|
// 针对来自服务端的菜单配置进行处理
|
||||||
|
// Handle routing configuration from the server
|
||||||
|
|
||||||
|
// 根据需要自行完善来源于服务端的菜单配置的permission逻辑
|
||||||
|
// Refine the permission logic from the server's menu configuration as needed
|
||||||
|
if ( |
||||||
|
!appStore.appAsyncMenus.length && |
||||||
|
!WHITE_LIST.find((el) => el.name === to.name) |
||||||
|
) { |
||||||
|
await appStore.fetchServerMenuConfig(); |
||||||
|
} |
||||||
|
const serverMenuConfig = [...appStore.appAsyncMenus, ...WHITE_LIST]; |
||||||
|
|
||||||
|
let exist = false; |
||||||
|
while (serverMenuConfig.length && !exist) { |
||||||
|
const element = serverMenuConfig.shift(); |
||||||
|
if (element?.name === to.name) exist = true; |
||||||
|
|
||||||
|
if (element?.children) { |
||||||
|
serverMenuConfig.push( |
||||||
|
...(element.children as unknown as RouteRecordNormalized[]) |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
if (exist && permissionsAllow) { |
||||||
|
next(); |
||||||
|
} else next(NOT_FOUND); |
||||||
|
} else { |
||||||
|
// eslint-disable-next-line no-lonely-if
|
||||||
|
if (permissionsAllow) next(); |
||||||
|
else { |
||||||
|
const destination = |
||||||
|
Permission.findFirstPermissionRoute(appRoutes, userStore.role) || |
||||||
|
NOT_FOUND; |
||||||
|
next(destination); |
||||||
|
} |
||||||
|
} |
||||||
|
NProgress.done(); |
||||||
|
}); |
||||||
|
} |
@ -0,0 +1,43 @@ |
|||||||
|
import type { Router, LocationQueryRaw } from 'vue-router'; |
||||||
|
import NProgress from 'nprogress'; // progress bar
|
||||||
|
|
||||||
|
import { useUserStore } from '@/store'; |
||||||
|
import { isLogin } from '@/utils/auth'; |
||||||
|
|
||||||
|
export default function setupUserLoginInfoGuard(router: Router) { |
||||||
|
router.beforeEach(async (to, from, next) => { |
||||||
|
NProgress.start(); |
||||||
|
const userStore = useUserStore(); |
||||||
|
if (isLogin()) { |
||||||
|
if (userStore.role) { |
||||||
|
next(); |
||||||
|
} else { |
||||||
|
try { |
||||||
|
await userStore.info(); |
||||||
|
next(); |
||||||
|
} catch (error) { |
||||||
|
await userStore.logout(); |
||||||
|
next({ |
||||||
|
name: 'login', |
||||||
|
query: { |
||||||
|
redirect: to.name, |
||||||
|
...to.query, |
||||||
|
} as LocationQueryRaw, |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
} else { |
||||||
|
if (to.name === 'login') { |
||||||
|
next(); |
||||||
|
return; |
||||||
|
} |
||||||
|
next({ |
||||||
|
name: 'login', |
||||||
|
query: { |
||||||
|
redirect: to.name, |
||||||
|
...to.query, |
||||||
|
} as LocationQueryRaw, |
||||||
|
}); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
@ -0,0 +1,37 @@ |
|||||||
|
import { createRouter, createWebHistory } from 'vue-router'; |
||||||
|
import NProgress from 'nprogress'; // progress bar
|
||||||
|
import 'nprogress/nprogress.css'; |
||||||
|
|
||||||
|
import { appRoutes } from './routes'; |
||||||
|
import { REDIRECT_MAIN, NOT_FOUND_ROUTE } from './routes/base'; |
||||||
|
import createRouteGuard from './guard'; |
||||||
|
|
||||||
|
NProgress.configure({ showSpinner: false }); // NProgress Configuration
|
||||||
|
|
||||||
|
const router = createRouter({ |
||||||
|
history: createWebHistory(), |
||||||
|
routes: [ |
||||||
|
{ |
||||||
|
path: '/', |
||||||
|
redirect: 'login', |
||||||
|
}, |
||||||
|
{ |
||||||
|
path: '/login', |
||||||
|
name: 'login', |
||||||
|
component: () => import('@/views/login/index.vue'), |
||||||
|
meta: { |
||||||
|
requiresAuth: false, |
||||||
|
}, |
||||||
|
}, |
||||||
|
...appRoutes, |
||||||
|
REDIRECT_MAIN, |
||||||
|
NOT_FOUND_ROUTE, |
||||||
|
], |
||||||
|
scrollBehavior() { |
||||||
|
return { top: 0 }; |
||||||
|
}, |
||||||
|
}); |
||||||
|
|
||||||
|
createRouteGuard(router); |
||||||
|
|
||||||
|
export default router; |
@ -0,0 +1,31 @@ |
|||||||
|
import type { RouteRecordRaw } from 'vue-router'; |
||||||
|
import { REDIRECT_ROUTE_NAME } from '@/router/constants'; |
||||||
|
|
||||||
|
export const DEFAULT_LAYOUT = () => import('@/layout/default-layout.vue'); |
||||||
|
|
||||||
|
export const REDIRECT_MAIN: RouteRecordRaw = { |
||||||
|
path: '/redirect', |
||||||
|
name: 'redirectWrapper', |
||||||
|
component: DEFAULT_LAYOUT, |
||||||
|
meta: { |
||||||
|
requiresAuth: true, |
||||||
|
hideInMenu: true, |
||||||
|
}, |
||||||
|
children: [ |
||||||
|
{ |
||||||
|
path: '/redirect/:path', |
||||||
|
name: REDIRECT_ROUTE_NAME, |
||||||
|
component: () => import('@/views/redirect/index.vue'), |
||||||
|
meta: { |
||||||
|
requiresAuth: true, |
||||||
|
hideInMenu: true, |
||||||
|
}, |
||||||
|
}, |
||||||
|
], |
||||||
|
}; |
||||||
|
|
||||||
|
export const NOT_FOUND_ROUTE: RouteRecordRaw = { |
||||||
|
path: '/:pathMatch(.*)*', |
||||||
|
name: 'notFound', |
||||||
|
component: () => import('@/views/not-found/index.vue'), |
||||||
|
}; |
@ -0,0 +1,10 @@ |
|||||||
|
export default { |
||||||
|
path: 'https://arco.design', |
||||||
|
name: 'arcoWebsite', |
||||||
|
meta: { |
||||||
|
locale: 'menu.arcoWebsite', |
||||||
|
icon: 'icon-link', |
||||||
|
requiresAuth: true, |
||||||
|
order: 8, |
||||||
|
}, |
||||||
|
}; |
@ -0,0 +1,10 @@ |
|||||||
|
export default { |
||||||
|
path: 'https://arco.design/vue/docs/pro/faq', |
||||||
|
name: 'faq', |
||||||
|
meta: { |
||||||
|
locale: 'menu.faq', |
||||||
|
icon: 'icon-question-circle', |
||||||
|
requiresAuth: true, |
||||||
|
order: 9, |
||||||
|
}, |
||||||
|
}; |
@ -0,0 +1,25 @@ |
|||||||
|
import type { RouteRecordNormalized } from 'vue-router'; |
||||||
|
|
||||||
|
const modules = import.meta.glob('./modules/*.ts', { eager: true }); |
||||||
|
const externalModules = import.meta.glob('./externalModules/*.ts', { |
||||||
|
eager: true, |
||||||
|
}); |
||||||
|
|
||||||
|
function formatModules(_modules: any, result: RouteRecordNormalized[]) { |
||||||
|
Object.keys(_modules).forEach((key) => { |
||||||
|
const defaultModule = _modules[key].default; |
||||||
|
if (!defaultModule) return; |
||||||
|
const moduleList = Array.isArray(defaultModule) |
||||||
|
? [...defaultModule] |
||||||
|
: [defaultModule]; |
||||||
|
result.push(...moduleList); |
||||||
|
}); |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
export const appRoutes: RouteRecordNormalized[] = formatModules(modules, []); |
||||||
|
|
||||||
|
export const appExternalRoutes: RouteRecordNormalized[] = formatModules( |
||||||
|
externalModules, |
||||||
|
[] |
||||||
|
); |
@ -0,0 +1,28 @@ |
|||||||
|
import { DEFAULT_LAYOUT } from '../base'; |
||||||
|
import { AppRouteRecordRaw } from '../types'; |
||||||
|
|
||||||
|
const DASHBOARD: AppRouteRecordRaw = { |
||||||
|
path: '/dashboard', |
||||||
|
name: 'dashboard', |
||||||
|
component: DEFAULT_LAYOUT, |
||||||
|
meta: { |
||||||
|
locale: 'menu.dashboard', |
||||||
|
requiresAuth: true, |
||||||
|
icon: 'icon-dashboard', |
||||||
|
order: 0, |
||||||
|
}, |
||||||
|
children: [ |
||||||
|
{ |
||||||
|
path: 'workplace', |
||||||
|
name: 'Workplace', |
||||||
|
component: () => import('@/views/dashboard/workplace/index.vue'), |
||||||
|
meta: { |
||||||
|
locale: 'menu.dashboard.workplace', |
||||||
|
requiresAuth: true, |
||||||
|
roles: ['*'], |
||||||
|
}, |
||||||
|
}, |
||||||
|
], |
||||||
|
}; |
||||||
|
|
||||||
|
export default DASHBOARD; |
@ -0,0 +1,20 @@ |
|||||||
|
import { defineComponent } from 'vue'; |
||||||
|
import type { RouteMeta, NavigationGuard } from 'vue-router'; |
||||||
|
|
||||||
|
export type Component<T = any> = |
||||||
|
| ReturnType<typeof defineComponent> |
||||||
|
| (() => Promise<typeof import('*.vue')>) |
||||||
|
| (() => Promise<T>); |
||||||
|
|
||||||
|
export interface AppRouteRecordRaw { |
||||||
|
path: string; |
||||||
|
name?: string | symbol; |
||||||
|
meta?: RouteMeta; |
||||||
|
redirect?: string; |
||||||
|
component: Component | string; |
||||||
|
children?: AppRouteRecordRaw[]; |
||||||
|
alias?: string | string[]; |
||||||
|
props?: Record<string, any>; |
||||||
|
beforeEnter?: NavigationGuard | NavigationGuard[]; |
||||||
|
fullPath?: string; |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
import 'vue-router'; |
||||||
|
|
||||||
|
declare module 'vue-router' { |
||||||
|
interface RouteMeta { |
||||||
|
roles?: string[]; // Controls roles that have access to the page
|
||||||
|
requiresAuth: boolean; // Whether login is required to access the current page (every route must declare)
|
||||||
|
icon?: string; // The icon show in the side menu
|
||||||
|
locale?: string; // The locale name show in side menu and breadcrumb
|
||||||
|
hideInMenu?: boolean; // If true, it is not displayed in the side menu
|
||||||
|
hideChildrenInMenu?: boolean; // if set true, the children are not displayed in the side menu
|
||||||
|
activeMenu?: string; // if set name, the menu will be highlighted according to the name you set
|
||||||
|
order?: number; // Sort routing menu items. If set key, the higher the value, the more forward it is
|
||||||
|
noAffix?: boolean; // if set true, the tag will not affix in the tab-bar
|
||||||
|
ignoreCache?: boolean; // if set true, the page will not be cached
|
||||||
|
} |
||||||
|
} |
@ -0,0 +1,9 @@ |
|||||||
|
import { createPinia } from 'pinia'; |
||||||
|
import useAppStore from './modules/app'; |
||||||
|
import useUserStore from './modules/user'; |
||||||
|
import useTabBarStore from './modules/tab-bar'; |
||||||
|
|
||||||
|
const pinia = createPinia(); |
||||||
|
|
||||||
|
export { useAppStore, useUserStore, useTabBarStore }; |
||||||
|
export default pinia; |
@ -0,0 +1,77 @@ |
|||||||
|
import { defineStore } from 'pinia'; |
||||||
|
import { Notification } from '@arco-design/web-vue'; |
||||||
|
import type { NotificationReturn } from '@arco-design/web-vue/es/notification/interface'; |
||||||
|
import type { RouteRecordNormalized } from 'vue-router'; |
||||||
|
import defaultSettings from '@/config/settings.json'; |
||||||
|
import { getMenuList } from '@/api/user'; |
||||||
|
import { AppState } from './types'; |
||||||
|
|
||||||
|
const useAppStore = defineStore('app', { |
||||||
|
state: (): AppState => ({ ...defaultSettings }), |
||||||
|
|
||||||
|
getters: { |
||||||
|
appCurrentSetting(state: AppState): AppState { |
||||||
|
return { ...state }; |
||||||
|
}, |
||||||
|
appDevice(state: AppState) { |
||||||
|
return state.device; |
||||||
|
}, |
||||||
|
appAsyncMenus(state: AppState): RouteRecordNormalized[] { |
||||||
|
return state.serverMenu as unknown as RouteRecordNormalized[]; |
||||||
|
}, |
||||||
|
}, |
||||||
|
|
||||||
|
actions: { |
||||||
|
// Update app settings
|
||||||
|
updateSettings(partial: Partial<AppState>) { |
||||||
|
// @ts-ignore-next-line
|
||||||
|
this.$patch(partial); |
||||||
|
}, |
||||||
|
|
||||||
|
// Change theme color
|
||||||
|
toggleTheme(dark: boolean) { |
||||||
|
if (dark) { |
||||||
|
this.theme = 'dark'; |
||||||
|
document.body.setAttribute('arco-theme', 'dark'); |
||||||
|
} else { |
||||||
|
this.theme = 'light'; |
||||||
|
document.body.removeAttribute('arco-theme'); |
||||||
|
} |
||||||
|
}, |
||||||
|
toggleDevice(device: string) { |
||||||
|
this.device = device; |
||||||
|
}, |
||||||
|
toggleMenu(value: boolean) { |
||||||
|
this.hideMenu = value; |
||||||
|
}, |
||||||
|
async fetchServerMenuConfig() { |
||||||
|
let notifyInstance: NotificationReturn | null = null; |
||||||
|
try { |
||||||
|
notifyInstance = Notification.info({ |
||||||
|
id: 'menuNotice', // Keep the instance id the same
|
||||||
|
content: 'loading', |
||||||
|
closable: true, |
||||||
|
}); |
||||||
|
const { data } = await getMenuList(); |
||||||
|
this.serverMenu = data; |
||||||
|
notifyInstance = Notification.success({ |
||||||
|
id: 'menuNotice', |
||||||
|
content: 'success', |
||||||
|
closable: true, |
||||||
|
}); |
||||||
|
} catch (error) { |
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
notifyInstance = Notification.error({ |
||||||
|
id: 'menuNotice', |
||||||
|
content: 'error', |
||||||
|
closable: true, |
||||||
|
}); |
||||||
|
} |
||||||
|
}, |
||||||
|
clearServerMenu() { |
||||||
|
this.serverMenu = []; |
||||||
|
}, |
||||||
|
}, |
||||||
|
}); |
||||||
|
|
||||||
|
export default useAppStore; |
@ -0,0 +1,20 @@ |
|||||||
|
import type { RouteRecordNormalized } from 'vue-router'; |
||||||
|
|
||||||
|
export interface AppState { |
||||||
|
theme: string; |
||||||
|
colorWeak: boolean; |
||||||
|
navbar: boolean; |
||||||
|
menu: boolean; |
||||||
|
topMenu: boolean; |
||||||
|
hideMenu: boolean; |
||||||
|
menuCollapse: boolean; |
||||||
|
footer: boolean; |
||||||
|
themeColor: string; |
||||||
|
menuWidth: number; |
||||||
|
globalSettings: boolean; |
||||||
|
device: string; |
||||||
|
tabBar: boolean; |
||||||
|
menuFromServer: boolean; |
||||||
|
serverMenu: RouteRecordNormalized[]; |
||||||
|
[key: string]: unknown; |
||||||
|
} |
@ -0,0 +1,74 @@ |
|||||||
|
import type { RouteLocationNormalized } from 'vue-router'; |
||||||
|
import { defineStore } from 'pinia'; |
||||||
|
import { |
||||||
|
DEFAULT_ROUTE, |
||||||
|
DEFAULT_ROUTE_NAME, |
||||||
|
REDIRECT_ROUTE_NAME, |
||||||
|
} from '@/router/constants'; |
||||||
|
import { isString } from '@/utils/is'; |
||||||
|
import { TabBarState, TagProps } from './types'; |
||||||
|
|
||||||
|
const formatTag = (route: RouteLocationNormalized): TagProps => { |
||||||
|
const { name, meta, fullPath, query } = route; |
||||||
|
return { |
||||||
|
title: meta.locale || '', |
||||||
|
name: String(name), |
||||||
|
fullPath, |
||||||
|
query, |
||||||
|
ignoreCache: meta.ignoreCache, |
||||||
|
}; |
||||||
|
}; |
||||||
|
|
||||||
|
const BAN_LIST = [REDIRECT_ROUTE_NAME]; |
||||||
|
|
||||||
|
const useAppStore = defineStore('tabBar', { |
||||||
|
state: (): TabBarState => ({ |
||||||
|
cacheTabList: new Set([DEFAULT_ROUTE_NAME]), |
||||||
|
tagList: [DEFAULT_ROUTE], |
||||||
|
}), |
||||||
|
|
||||||
|
getters: { |
||||||
|
getTabList(): TagProps[] { |
||||||
|
return this.tagList; |
||||||
|
}, |
||||||
|
getCacheList(): string[] { |
||||||
|
return Array.from(this.cacheTabList); |
||||||
|
}, |
||||||
|
}, |
||||||
|
|
||||||
|
actions: { |
||||||
|
updateTabList(route: RouteLocationNormalized) { |
||||||
|
if (BAN_LIST.includes(route.name as string)) return; |
||||||
|
this.tagList.push(formatTag(route)); |
||||||
|
if (!route.meta.ignoreCache) { |
||||||
|
this.cacheTabList.add(route.name as string); |
||||||
|
} |
||||||
|
}, |
||||||
|
deleteTag(idx: number, tag: TagProps) { |
||||||
|
this.tagList.splice(idx, 1); |
||||||
|
this.cacheTabList.delete(tag.name); |
||||||
|
}, |
||||||
|
addCache(name: string) { |
||||||
|
if (isString(name) && name !== '') this.cacheTabList.add(name); |
||||||
|
}, |
||||||
|
deleteCache(tag: TagProps) { |
||||||
|
this.cacheTabList.delete(tag.name); |
||||||
|
}, |
||||||
|
freshTabList(tags: TagProps[]) { |
||||||
|
this.tagList = tags; |
||||||
|
this.cacheTabList.clear(); |
||||||
|
// 要先判断ignoreCache
|
||||||
|
this.tagList |
||||||
|
.filter((el) => !el.ignoreCache) |
||||||
|
.map((el) => el.name) |
||||||
|
.forEach((x) => this.cacheTabList.add(x)); |
||||||
|
}, |
||||||
|
resetTabList() { |
||||||
|
this.tagList = [DEFAULT_ROUTE]; |
||||||
|
this.cacheTabList.clear(); |
||||||
|
this.cacheTabList.add(DEFAULT_ROUTE_NAME); |
||||||
|
}, |
||||||
|
}, |
||||||
|
}); |
||||||
|
|
||||||
|
export default useAppStore; |
@ -0,0 +1,12 @@ |
|||||||
|
export interface TagProps { |
||||||
|
title: string; |
||||||
|
name: string; |
||||||
|
fullPath: string; |
||||||
|
query?: any; |
||||||
|
ignoreCache?: boolean; |
||||||
|
} |
||||||
|
|
||||||
|
export interface TabBarState { |
||||||
|
tagList: TagProps[]; |
||||||
|
cacheTabList: Set<string>; |
||||||
|
} |
@ -0,0 +1,91 @@ |
|||||||
|
import { defineStore } from 'pinia'; |
||||||
|
import { |
||||||
|
login as userLogin, |
||||||
|
logout as userLogout, |
||||||
|
getUserInfo, |
||||||
|
LoginData, |
||||||
|
} from '@/api/user'; |
||||||
|
import { setToken, clearToken } from '@/utils/auth'; |
||||||
|
import { removeRouteListener } from '@/utils/route-listener'; |
||||||
|
import { UserState } from './types'; |
||||||
|
import useAppStore from '../app'; |
||||||
|
|
||||||
|
const useUserStore = defineStore('user', { |
||||||
|
state: (): UserState => ({ |
||||||
|
name: undefined, |
||||||
|
avatar: undefined, |
||||||
|
job: undefined, |
||||||
|
organization: undefined, |
||||||
|
location: undefined, |
||||||
|
email: undefined, |
||||||
|
introduction: undefined, |
||||||
|
personalWebsite: undefined, |
||||||
|
jobName: undefined, |
||||||
|
organizationName: undefined, |
||||||
|
locationName: undefined, |
||||||
|
phone: undefined, |
||||||
|
registrationDate: undefined, |
||||||
|
accountId: undefined, |
||||||
|
certification: undefined, |
||||||
|
role: '', |
||||||
|
}), |
||||||
|
|
||||||
|
getters: { |
||||||
|
userInfo(state: UserState): UserState { |
||||||
|
return { ...state }; |
||||||
|
}, |
||||||
|
}, |
||||||
|
|
||||||
|
actions: { |
||||||
|
switchRoles() { |
||||||
|
return new Promise((resolve) => { |
||||||
|
this.role = this.role === 'user' ? 'admin' : 'user'; |
||||||
|
resolve(this.role); |
||||||
|
}); |
||||||
|
}, |
||||||
|
// Set user's information
|
||||||
|
setInfo(partial: Partial<UserState>) { |
||||||
|
this.$patch(partial); |
||||||
|
}, |
||||||
|
|
||||||
|
// Reset user's information
|
||||||
|
resetInfo() { |
||||||
|
this.$reset(); |
||||||
|
}, |
||||||
|
|
||||||
|
// Get user's information
|
||||||
|
async info() { |
||||||
|
const res = await getUserInfo(); |
||||||
|
|
||||||
|
this.setInfo(res.data); |
||||||
|
}, |
||||||
|
|
||||||
|
// Login
|
||||||
|
async login(loginForm: LoginData) { |
||||||
|
try { |
||||||
|
const res = await userLogin(loginForm); |
||||||
|
setToken(res.data.token); |
||||||
|
} catch (err) { |
||||||
|
clearToken(); |
||||||
|
throw err; |
||||||
|
} |
||||||
|
}, |
||||||
|
logoutCallBack() { |
||||||
|
const appStore = useAppStore(); |
||||||
|
this.resetInfo(); |
||||||
|
clearToken(); |
||||||
|
removeRouteListener(); |
||||||
|
appStore.clearServerMenu(); |
||||||
|
}, |
||||||
|
// Logout
|
||||||
|
async logout() { |
||||||
|
try { |
||||||
|
await userLogout(); |
||||||
|
} finally { |
||||||
|
this.logoutCallBack(); |
||||||
|
} |
||||||
|
}, |
||||||
|
}, |
||||||
|
}); |
||||||
|
|
||||||
|
export default useUserStore; |
@ -0,0 +1,19 @@ |
|||||||
|
export type RoleType = '' | '*' | 'admin' | 'user'; |
||||||
|
export interface UserState { |
||||||
|
name?: string; |
||||||
|
avatar?: string; |
||||||
|
job?: string; |
||||||
|
organization?: string; |
||||||
|
location?: string; |
||||||
|
email?: string; |
||||||
|
introduction?: string; |
||||||
|
personalWebsite?: string; |
||||||
|
jobName?: string; |
||||||
|
organizationName?: string; |
||||||
|
locationName?: string; |
||||||
|
phone?: string; |
||||||
|
registrationDate?: string; |
||||||
|
accountId?: string; |
||||||
|
certification?: number; |
||||||
|
role: RoleType; |
||||||
|
} |
@ -0,0 +1,10 @@ |
|||||||
|
import { CallbackDataParams } from 'echarts/types/dist/shared'; |
||||||
|
|
||||||
|
export interface ToolTipFormatterParams extends CallbackDataParams { |
||||||
|
axisDim: string; |
||||||
|
axisIndex: number; |
||||||
|
axisType: string; |
||||||
|
axisId: string; |
||||||
|
axisValue: string; |
||||||
|
axisValueLabel: string; |
||||||
|
} |
@ -0,0 +1,37 @@ |
|||||||
|
export interface AnyObject { |
||||||
|
[key: string]: unknown; |
||||||
|
} |
||||||
|
|
||||||
|
export interface Options { |
||||||
|
value: unknown; |
||||||
|
label: string; |
||||||
|
} |
||||||
|
|
||||||
|
export interface NodeOptions extends Options { |
||||||
|
children?: NodeOptions[]; |
||||||
|
} |
||||||
|
|
||||||
|
export interface GetParams { |
||||||
|
body: null; |
||||||
|
type: string; |
||||||
|
url: string; |
||||||
|
} |
||||||
|
|
||||||
|
export interface PostData { |
||||||
|
body: string; |
||||||
|
type: string; |
||||||
|
url: string; |
||||||
|
} |
||||||
|
|
||||||
|
export interface Pagination { |
||||||
|
current: number; |
||||||
|
pageSize: number; |
||||||
|
total?: number; |
||||||
|
} |
||||||
|
|
||||||
|
export type TimeRanger = [string, string]; |
||||||
|
|
||||||
|
export interface GeneralChart { |
||||||
|
xAxis: string[]; |
||||||
|
data: Array<{ name: string; value: number[] }>; |
||||||
|
} |
@ -0,0 +1,5 @@ |
|||||||
|
export interface MockParams { |
||||||
|
url: string; |
||||||
|
type: string; |
||||||
|
body: string; |
||||||
|
} |
@ -0,0 +1,19 @@ |
|||||||
|
const TOKEN_KEY = 'token'; |
||||||
|
|
||||||
|
const isLogin = () => { |
||||||
|
return !!localStorage.getItem(TOKEN_KEY); |
||||||
|
}; |
||||||
|
|
||||||
|
const getToken = () => { |
||||||
|
return localStorage.getItem(TOKEN_KEY); |
||||||
|
}; |
||||||
|
|
||||||
|
const setToken = (token: string) => { |
||||||
|
localStorage.setItem(TOKEN_KEY, token); |
||||||
|
}; |
||||||
|
|
||||||
|
const clearToken = () => { |
||||||
|
localStorage.removeItem(TOKEN_KEY); |
||||||
|
}; |
||||||
|
|
||||||
|
export { isLogin, getToken, setToken, clearToken }; |
@ -0,0 +1,3 @@ |
|||||||
|
const debug = process.env.NODE_ENV !== 'production'; |
||||||
|
|
||||||
|
export default debug; |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue