Init
0 parents
Showing
127 changed files
with
4033 additions
and
0 deletions
.env.development
0 → 100644
1 | #VITE_API_URL=https://service.hising.orb.local | ||
2 | VITE_API_URL=https://hi-sing-service-dev.hikoon.com | ||
3 | VITE_OSS_ACCESS_KEY=LTAI4GKtcA6yTV6wnapivq7Y | ||
4 | VITE_OSS_ACCESS_SECRET=QPEt0HPEuRe7wIk2wZNtKPF6L0xMmQ | ||
5 | VITE_OSS_BUCKET=hisin-dev | ||
6 | VITE_OSS_REGION=oss-cn-beijing | ||
7 | VITE_OSS_HOST=https://hi-sing-cdn-dev.hikoon.com | ||
8 | VITE_OSS_ENDPOINT=https://hisin-dev.oss-cn-beijing.aliyuncs.com | ||
9 | VITE_SHOW_LOGIN_ACCOUNT=true |
.eslintignore
0 → 100644
.eslintrc.js
0 → 100644
1 | const path = require('path'); | ||
2 | |||
3 | module.exports = { | ||
4 | root: true, | ||
5 | parser: 'vue-eslint-parser', | ||
6 | parserOptions: { | ||
7 | // Parser that checks the content of the <script> tag | ||
8 | parser: '@typescript-eslint/parser', | ||
9 | sourceType: 'module', | ||
10 | ecmaVersion: 2020, | ||
11 | ecmaFeatures: { | ||
12 | jsx: true, | ||
13 | }, | ||
14 | }, | ||
15 | env: { | ||
16 | browser: true, | ||
17 | node: true, | ||
18 | }, | ||
19 | plugins: ['@typescript-eslint'], | ||
20 | extends: [ | ||
21 | // Airbnb JavaScript Style Guide https://github.com/airbnb/javascript | ||
22 | 'airbnb-base', | ||
23 | 'plugin:@typescript-eslint/recommended', | ||
24 | 'plugin:import/recommended', | ||
25 | 'plugin:import/typescript', | ||
26 | 'plugin:vue/vue3-recommended', | ||
27 | 'plugin:prettier/recommended', | ||
28 | ], | ||
29 | settings: { | ||
30 | 'import/resolver': { | ||
31 | typescript: { | ||
32 | project: path.resolve(__dirname, './tsconfig.json'), | ||
33 | }, | ||
34 | }, | ||
35 | }, | ||
36 | globals: { | ||
37 | defineProps: 'readonly', | ||
38 | defineEmits: 'readonly', | ||
39 | defineExpose: 'readonly', | ||
40 | defineModel: 'readonly', | ||
41 | withDefaults: 'readonly', | ||
42 | }, | ||
43 | rules: { | ||
44 | "prettier/prettier": 'off', | ||
45 | // 'prettier/prettier': 1, | ||
46 | 'import/order': 0, | ||
47 | // Vue: Recommended rules to be closed or modify | ||
48 | 'vue/require-default-prop': 0, | ||
49 | 'vue/singleline-html-element-content-newline': 0, | ||
50 | 'vue/max-attributes-per-line': 0, | ||
51 | // Vue: Add extra rules | ||
52 | 'vue/custom-event-name-casing': [2, 'camelCase'], | ||
53 | 'vue/no-v-text': 1, | ||
54 | 'vue/padding-line-between-blocks': 1, | ||
55 | 'vue/require-direct-export': 1, | ||
56 | 'vue/multi-word-component-names': 0, | ||
57 | // Allow @ts-ignore comment | ||
58 | '@typescript-eslint/ban-ts-comment': 0, | ||
59 | '@typescript-eslint/no-unused-vars': 1, | ||
60 | '@typescript-eslint/no-empty-function': 1, | ||
61 | '@typescript-eslint/no-explicit-any': 0, | ||
62 | '@typescript-eslint/no-implicit-any': 0, | ||
63 | 'camelcase': 'off', | ||
64 | 'max-classes-per-file': 'off', | ||
65 | '@typescript-eslint/camelcase': 0, | ||
66 | 'import/extensions': [ | ||
67 | 2, | ||
68 | 'ignorePackages', | ||
69 | { | ||
70 | js: 'never', | ||
71 | jsx: 'never', | ||
72 | ts: 'never', | ||
73 | tsx: 'never', | ||
74 | }, | ||
75 | ], | ||
76 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, | ||
77 | 'no-param-reassign': 0, | ||
78 | 'no-return-assign': 0, | ||
79 | 'no-shadow': 0, | ||
80 | 'prefer-regex-literals': 0, | ||
81 | 'import/no-extraneous-dependencies': 0, | ||
82 | 'class-methods-use-this': 0, | ||
83 | "vue/first-attribute-linebreak": ["error", { | ||
84 | "singleline": "ignore", | ||
85 | "multiline": "ignore" | ||
86 | }] | ||
87 | }, | ||
88 | }; |
.gitignore
0 → 100644
.prettierrc.js
0 → 100644
.stylelintrc.js
0 → 100644
1 | module.exports = { | ||
2 | extends: [ | ||
3 | 'stylelint-config-standard', | ||
4 | 'stylelint-config-rational-order', | ||
5 | 'stylelint-config-prettier', | ||
6 | 'stylelint-config-recommended-vue', | ||
7 | ], | ||
8 | defaultSeverity: 'warning', | ||
9 | plugins: ['stylelint-order'], | ||
10 | rules: { | ||
11 | 'at-rule-no-unknown': [ | ||
12 | true, | ||
13 | { | ||
14 | ignoreAtRules: ['plugin'], | ||
15 | }, | ||
16 | ], | ||
17 | 'rule-empty-line-before': [ | ||
18 | 'always', | ||
19 | { | ||
20 | except: ['after-single-line-comment', 'first-nested'], | ||
21 | }, | ||
22 | ], | ||
23 | 'selector-pseudo-class-no-unknown': [ | ||
24 | true, | ||
25 | { | ||
26 | ignorePseudoClasses: ['deep'], | ||
27 | }, | ||
28 | ], | ||
29 | }, | ||
30 | }; |
babel.config.js
0 → 100644
commitlint.config.js
0 → 100644
components.d.ts
0 → 100644
1 | /* eslint-disable */ | ||
2 | /* prettier-ignore */ | ||
3 | // @ts-nocheck | ||
4 | // Generated by unplugin-vue-components | ||
5 | // Read more: https://github.com/vuejs/core/pull/3399 | ||
6 | import '@vue/runtime-core' | ||
7 | |||
8 | export {} | ||
9 | |||
10 | declare module '@vue/runtime-core' { | ||
11 | export interface GlobalComponents { | ||
12 | RouterLink: typeof import('vue-router')['RouterLink'] | ||
13 | RouterView: typeof import('vue-router')['RouterView'] | ||
14 | } | ||
15 | } |
config/plugin/arcoResolver.ts
0 → 100644
1 | /** | ||
2 | * If you use the template method for development, you can use the unplugin-vue-components plugin to enable on-demand loading support. | ||
3 | * 按需引入 | ||
4 | * https://github.com/antfu/unplugin-vue-components | ||
5 | * https://arco.design/vue/docs/start | ||
6 | * Although the Pro project is full of imported components, this plugin will be used by default. | ||
7 | * 虽然Pro项目中是全量引入组件,但此插件会默认使用。 | ||
8 | */ | ||
9 | import Components from 'unplugin-vue-components/vite'; | ||
10 | import { ArcoResolver } from 'unplugin-vue-components/resolvers'; | ||
11 | |||
12 | export default function configArcoResolverPlugin() { | ||
13 | const arcoResolverPlugin = Components({ | ||
14 | dirs: [], // Avoid parsing src/components. 避免解析到src/components | ||
15 | deep: false, | ||
16 | resolvers: [ArcoResolver()], | ||
17 | }); | ||
18 | return arcoResolverPlugin; | ||
19 | } |
config/plugin/arcoStyleImport.ts
0 → 100644
1 | /** | ||
2 | * Theme import | ||
3 | * 样式按需引入 | ||
4 | * https://github.com/arco-design/arco-plugins/blob/main/packages/plugin-vite-vue/README.md | ||
5 | * https://arco.design/vue/docs/start | ||
6 | */ | ||
7 | import { vitePluginForArco } from '@arco-plugins/vite-vue'; | ||
8 | |||
9 | export default function configArcoStyleImportPlugin() { | ||
10 | const arcoResolverPlugin = vitePluginForArco({}); | ||
11 | return arcoResolverPlugin; | ||
12 | } |
config/plugin/compress.ts
0 → 100644
1 | /** | ||
2 | * Used to package and output gzip. Note that this does not work properly in Vite, the specific reason is still being investigated | ||
3 | * gzip压缩 | ||
4 | * https://github.com/anncwb/vite-plugin-compression | ||
5 | */ | ||
6 | import type { Plugin } from 'vite'; | ||
7 | import compressPlugin from 'vite-plugin-compression'; | ||
8 | |||
9 | export default function configCompressPlugin( | ||
10 | compress: 'gzip' | 'brotli', | ||
11 | deleteOriginFile = false | ||
12 | ): Plugin | Plugin[] { | ||
13 | const plugins: Plugin[] = []; | ||
14 | |||
15 | if (compress === 'gzip') { | ||
16 | plugins.push( | ||
17 | compressPlugin({ | ||
18 | ext: '.gz', | ||
19 | deleteOriginFile, | ||
20 | }) | ||
21 | ); | ||
22 | } | ||
23 | |||
24 | if (compress === 'brotli') { | ||
25 | plugins.push( | ||
26 | compressPlugin({ | ||
27 | ext: '.br', | ||
28 | algorithm: 'brotliCompress', | ||
29 | deleteOriginFile, | ||
30 | }) | ||
31 | ); | ||
32 | } | ||
33 | return plugins; | ||
34 | } |
config/plugin/imagemin.ts
0 → 100644
1 | /** | ||
2 | * Image resource files used to compress the output of the production environment | ||
3 | * 图片压缩 | ||
4 | * https://github.com/anncwb/vite-plugin-imagemin | ||
5 | */ | ||
6 | import viteImagemin from 'vite-plugin-imagemin'; | ||
7 | |||
8 | export default function configImageminPlugin() { | ||
9 | const imageminPlugin = viteImagemin({ | ||
10 | gifsicle: { | ||
11 | optimizationLevel: 7, | ||
12 | interlaced: false, | ||
13 | }, | ||
14 | optipng: { | ||
15 | optimizationLevel: 7, | ||
16 | }, | ||
17 | mozjpeg: { | ||
18 | quality: 20, | ||
19 | }, | ||
20 | pngquant: { | ||
21 | quality: [0.8, 0.9], | ||
22 | speed: 4, | ||
23 | }, | ||
24 | svgo: { | ||
25 | plugins: [ | ||
26 | { | ||
27 | name: 'removeViewBox', | ||
28 | }, | ||
29 | { | ||
30 | name: 'removeEmptyAttrs', | ||
31 | active: false, | ||
32 | }, | ||
33 | ], | ||
34 | }, | ||
35 | }); | ||
36 | return imageminPlugin; | ||
37 | } |
config/plugin/visualizer.ts
0 → 100644
1 | /** | ||
2 | * Generation packaging analysis | ||
3 | * 生成打包分析 | ||
4 | */ | ||
5 | import visualizer from 'rollup-plugin-visualizer'; | ||
6 | import { isReportMode } from '../utils'; | ||
7 | |||
8 | export default function configVisualizerPlugin() { | ||
9 | if (isReportMode()) { | ||
10 | return visualizer({ | ||
11 | filename: './node_modules/.cache/visualizer/stats.html', | ||
12 | open: true, | ||
13 | gzipSize: true, | ||
14 | brotliSize: true, | ||
15 | }); | ||
16 | } | ||
17 | return []; | ||
18 | } |
config/utils/index.ts
0 → 100644
config/vite.config.base.ts
0 → 100644
1 | import { resolve } from 'path'; | ||
2 | import { defineConfig } from 'vite'; | ||
3 | import vue from '@vitejs/plugin-vue'; | ||
4 | import vueJsx from '@vitejs/plugin-vue-jsx'; | ||
5 | import svgLoader from 'vite-svg-loader'; | ||
6 | import configArcoStyleImportPlugin from './plugin/arcoStyleImport'; | ||
7 | |||
8 | export default defineConfig({ | ||
9 | plugins: [vue(), vueJsx(), svgLoader({ svgoConfig: {} }), configArcoStyleImportPlugin()], | ||
10 | resolve: { | ||
11 | alias: [ | ||
12 | { | ||
13 | find: '@', | ||
14 | replacement: resolve(__dirname, '../src'), | ||
15 | }, | ||
16 | { | ||
17 | find: 'assets', | ||
18 | replacement: resolve(__dirname, '../src/assets'), | ||
19 | }, | ||
20 | { | ||
21 | find: 'vue-i18n', | ||
22 | replacement: 'vue-i18n/dist/vue-i18n.cjs.js', // Resolve the i18n warning issue | ||
23 | }, | ||
24 | { | ||
25 | find: 'vue', | ||
26 | replacement: 'vue/dist/vue.esm-bundler.js', // compile template | ||
27 | }, | ||
28 | ], | ||
29 | extensions: ['.ts', '.js'], | ||
30 | }, | ||
31 | define: { | ||
32 | 'process.env': {}, | ||
33 | '__VUE_PROD_HYDRATION_MISMATCH_DETAILS__': false, | ||
34 | }, | ||
35 | css: { | ||
36 | preprocessorOptions: { | ||
37 | less: { | ||
38 | modifyVars: { | ||
39 | hack: `true; @import (reference) "${resolve('src/assets/style/breakpoint.less')}";`, | ||
40 | }, | ||
41 | javascriptEnabled: true, | ||
42 | }, | ||
43 | }, | ||
44 | }, | ||
45 | }); |
config/vite.config.cdn.ts
0 → 100644
1 | import { loadEnv, mergeConfig } from "vite"; | ||
2 | import baseConfig from "./vite.config.base"; | ||
3 | import configCompressPlugin from "./plugin/compress"; | ||
4 | import configVisualizerPlugin from "./plugin/visualizer"; | ||
5 | import configArcoResolverPlugin from "./plugin/arcoResolver"; | ||
6 | import configImageminPlugin from "./plugin/imagemin"; | ||
7 | import VitePluginOss from "vite-plugin-oss"; | ||
8 | |||
9 | const config = loadEnv("production", ""); | ||
10 | |||
11 | const ossPath = `vendor/user/asset${new Date() | ||
12 | .toLocaleString("zh") | ||
13 | .slice(0, 20) | ||
14 | .replace(/[\s/:]/g, "")}`; | ||
15 | export default mergeConfig( | ||
16 | { | ||
17 | mode: "production", | ||
18 | base: config.VITE_OSS_HOST, | ||
19 | server: { | ||
20 | https: true | ||
21 | }, | ||
22 | plugins: [ | ||
23 | configCompressPlugin("gzip"), | ||
24 | configVisualizerPlugin(), | ||
25 | configArcoResolverPlugin(), | ||
26 | configImageminPlugin(), | ||
27 | VitePluginOss({ | ||
28 | from: `./dist/${ossPath}/**`, | ||
29 | accessKeyId: config.VITE_OSS_ACCESS_KEY, | ||
30 | accessKeySecret: config.VITE_OSS_ACCESS_SECRET, | ||
31 | bucket: config.VITE_OSS_BUCKET, | ||
32 | region: config.VITE_OSS_REGION, | ||
33 | quitWpOnError: true, | ||
34 | deleteOrigin: true, | ||
35 | deleteEmptyDir: true | ||
36 | }) | ||
37 | ], | ||
38 | build: { | ||
39 | assetsDir: ossPath, | ||
40 | rollupOptions: { | ||
41 | output: { | ||
42 | manualChunks: { | ||
43 | arco: ["@arco-design/web-vue"], | ||
44 | chart: ["echarts", "vue-echarts"], | ||
45 | vue: ["vue", "vue-router", "pinia", "@vueuse/core", "vue-i18n"] | ||
46 | } | ||
47 | } | ||
48 | }, | ||
49 | chunkSizeWarningLimit: 2000 | ||
50 | } | ||
51 | }, | ||
52 | baseConfig | ||
53 | ); |
config/vite.config.dev.ts
0 → 100644
1 | import { mergeConfig } from 'vite'; | ||
2 | import eslint from 'vite-plugin-eslint'; | ||
3 | import baseConfig from './vite.config.base'; | ||
4 | |||
5 | export default mergeConfig( | ||
6 | { | ||
7 | mode: 'development', | ||
8 | server: { | ||
9 | open: true, | ||
10 | fs: { | ||
11 | strict: false, | ||
12 | }, | ||
13 | }, | ||
14 | plugins: [ | ||
15 | eslint({ | ||
16 | cache: false, | ||
17 | include: ['src/**/*.ts', 'src/**/*.tsx', 'src/**/*.vue'], | ||
18 | exclude: ['node_modules'], | ||
19 | }), | ||
20 | ], | ||
21 | define: { | ||
22 | __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: true, | ||
23 | }, | ||
24 | }, | ||
25 | baseConfig | ||
26 | ); |
config/vite.config.prod.ts
0 → 100644
1 | import { mergeConfig } from 'vite'; | ||
2 | import baseConfig from './vite.config.base'; | ||
3 | import configCompressPlugin from './plugin/compress'; | ||
4 | import configVisualizerPlugin from './plugin/visualizer'; | ||
5 | import configArcoResolverPlugin from './plugin/arcoResolver'; | ||
6 | import configImageminPlugin from './plugin/imagemin'; | ||
7 | |||
8 | export default mergeConfig( | ||
9 | { | ||
10 | mode: 'production', | ||
11 | server: { | ||
12 | https: true, | ||
13 | }, | ||
14 | plugins: [configCompressPlugin('gzip'), configVisualizerPlugin(), configArcoResolverPlugin(), configImageminPlugin()], | ||
15 | build: { | ||
16 | rollupOptions: { | ||
17 | output: { | ||
18 | manualChunks: { | ||
19 | arco: ['@arco-design/web-vue'], | ||
20 | chart: ['echarts', 'vue-echarts'], | ||
21 | vue: ['vue', 'vue-router', 'pinia', '@vueuse/core', 'vue-i18n'], | ||
22 | }, | ||
23 | }, | ||
24 | }, | ||
25 | chunkSizeWarningLimit: 2000, | ||
26 | }, | ||
27 | }, | ||
28 | baseConfig | ||
29 | ); |
index.html
0 → 100644
1 | <!DOCTYPE html> | ||
2 | <html lang="en"> | ||
3 | <head> | ||
4 | <meta charset="UTF-8" /> | ||
5 | <link rel="shortcut icon" type="image/x-icon" href="https://hising-cdn.hikoon.com/file/20231201/nyaagyzd92c1701419974050pv0hv2xlsme.ico"> | ||
6 | <meta name="viewport" content="width=device-width, initial-scale=1.0"/> | ||
7 | <meta name="referrer" content="no-referrer"/> | ||
8 | <title>海星试唱</title> | ||
9 | </head> | ||
10 | <body> | ||
11 | <div id="app"></div> | ||
12 | <script type="module" src="/src/main.ts"></script> | ||
13 | </body> | ||
14 | </html> |
package.json
0 → 100644
1 | { | ||
2 | "name": "arco-design-pro-vue", | ||
3 | "description": "Arco Design Pro for Vue", | ||
4 | "version": "1.0.0", | ||
5 | "private": true, | ||
6 | "author": "ArcoDesign Team", | ||
7 | "license": "MIT", | ||
8 | "scripts": { | ||
9 | "dev": "vue-tsc --noEmit && vite --config ./config/vite.config.dev.ts", | ||
10 | "build": "vue-tsc --noEmit && vite build --config ./config/vite.config.prod.ts", | ||
11 | "build:cdn": "vue-tsc --noEmit && vite build --config ./config/vite.config.cdn.ts", | ||
12 | "report": "cross-env REPORT=true npm run build", | ||
13 | "preview": "npm run build && vite preview --host", | ||
14 | "type:check": "vue-tsc --noEmit --skipLibCheck", | ||
15 | "lint-staged": "npx lint-staged" | ||
16 | }, | ||
17 | "lint-staged": { | ||
18 | "*.{js,ts,jsx,tsx}": [ | ||
19 | "prettier --write", | ||
20 | "eslint --fix" | ||
21 | ], | ||
22 | "*.vue": [ | ||
23 | "stylelint --fix", | ||
24 | "prettier --write", | ||
25 | "eslint --fix" | ||
26 | ], | ||
27 | "*.{less,css}": [ | ||
28 | "stylelint --fix", | ||
29 | "prettier --write" | ||
30 | ] | ||
31 | }, | ||
32 | "dependencies": { | ||
33 | "@arco-design/web-vue": "^2.44.7", | ||
34 | "@vueuse/core": "^7.3.0", | ||
35 | "arco-design-pro-vue": "^2.7.2", | ||
36 | "axios": "^1.6.0", | ||
37 | "dayjs": "^1.11.5", | ||
38 | "echarts": "^5.4.0", | ||
39 | "lodash-es": "^4.17.21", | ||
40 | "mitt": "^3.0.0", | ||
41 | "nprogress": "^0.2.0", | ||
42 | "pinia": "^2.0.23", | ||
43 | "query-string": "^8.0.3", | ||
44 | "sortablejs": "^1.15.0", | ||
45 | "vue": "^3.3.11", | ||
46 | "vue-echarts": "^6.2.3", | ||
47 | "vue-i18n": "^9.2.2", | ||
48 | "vue-router": "^4.0.14", | ||
49 | "ali-oss": "^6.17.1", | ||
50 | "file-saver": "^2.0.5", | ||
51 | "rabbit-lyrics": "^2.1.1" | ||
52 | }, | ||
53 | "devDependencies": { | ||
54 | "@types/ali-oss": "^6.16.3", | ||
55 | "@types/file-saver": "^2.0.5", | ||
56 | "@arco-plugins/vite-vue": "^1.4.5", | ||
57 | "@commitlint/cli": "^17.1.2", | ||
58 | "@commitlint/config-conventional": "^17.1.0", | ||
59 | "@types/lodash": "^4.14.186", | ||
60 | "@types/lodash-es": "^4.17.12", | ||
61 | "@types/nprogress": "^0.2.0", | ||
62 | "@types/sortablejs": "^1.15.0", | ||
63 | "@typescript-eslint/eslint-plugin": "^5.40.0", | ||
64 | "@typescript-eslint/parser": "^5.40.0", | ||
65 | "@vitejs/plugin-vue": "^3.1.2", | ||
66 | "@vitejs/plugin-vue-jsx": "^2.0.1", | ||
67 | "@vue/babel-plugin-jsx": "^1.1.1", | ||
68 | "@vueuse/router": "^10.3.0", | ||
69 | "consola": "^2.15.3", | ||
70 | "cross-env": "^7.0.3", | ||
71 | "eslint": "^8.25.0", | ||
72 | "eslint-config-airbnb-base": "^15.0.0", | ||
73 | "eslint-config-prettier": "^8.5.0", | ||
74 | "eslint-import-resolver-typescript": "^3.5.1", | ||
75 | "eslint-plugin-import": "^2.26.0", | ||
76 | "eslint-plugin-prettier": "^4.2.1", | ||
77 | "eslint-plugin-vue": "^9.6.0", | ||
78 | "less": "^4.1.3", | ||
79 | "lint-staged": "^13.0.3", | ||
80 | "postcss-html": "^1.5.0", | ||
81 | "prettier": "^2.7.1", | ||
82 | "rollup": "^3.9.1", | ||
83 | "rollup-plugin-visualizer": "^5.8.2", | ||
84 | "stylelint": "^14.13.0", | ||
85 | "stylelint-config-prettier": "^9.0.3", | ||
86 | "stylelint-config-rational-order": "^0.1.2", | ||
87 | "stylelint-config-standard": "^29.0.0", | ||
88 | "stylelint-order": "^5.0.0", | ||
89 | "typescript": "^4.8.4", | ||
90 | "unplugin-vue-components": "^0.24.1", | ||
91 | "vite": "^3.2.5", | ||
92 | "vite-plugin-compression": "^0.5.1", | ||
93 | "vite-plugin-eslint": "^1.8.1", | ||
94 | "vite-plugin-imagemin": "^0.6.1", | ||
95 | "vite-plugin-oss": "^1.2.13", | ||
96 | "vite-svg-loader": "^3.6.0", | ||
97 | "vue-tsc": "^1.0.14" | ||
98 | }, | ||
99 | "engines": { | ||
100 | "node": ">=14.0.0" | ||
101 | }, | ||
102 | "resolutions": { | ||
103 | "bin-wrapper": "npm:bin-wrapper-china", | ||
104 | "rollup": "^2.56.3", | ||
105 | "gifsicle": "5.2.0" | ||
106 | }, | ||
107 | "volta": { | ||
108 | "node": "18.3.0", | ||
109 | "yarn": "1.22.19" | ||
110 | } | ||
111 | } |
src/App.vue
0 → 100644
src/api/activity.ts
0 → 100644
1 | // eslint-disable-next-line max-classes-per-file | ||
2 | import { AnyObject, QueryForParams, ServiceResponse } from '@/types/global'; | ||
3 | import axios from 'axios'; | ||
4 | import { ActivityApply } from '@/types/activity-apply'; | ||
5 | import { Activity, ActivityViewUser } from '@/types/activity'; | ||
6 | |||
7 | export default class useActivityApi { | ||
8 | static statusOption = [ | ||
9 | { label: '处理中', value: 0 }, | ||
10 | { label: '已上架', value: 1 }, | ||
11 | { label: '已下架', value: 2 }, | ||
12 | { label: '已匹配', value: 3 }, | ||
13 | { label: '已发行', value: 5 }, | ||
14 | { label: '处理失败', value: 4 }, | ||
15 | ]; | ||
16 | |||
17 | static weightOption = [ | ||
18 | { label: '无', value: 0 }, | ||
19 | { label: '低', value: 30 }, | ||
20 | { label: '中', value: 60 }, | ||
21 | { label: '高', value: 90 }, | ||
22 | ]; | ||
23 | |||
24 | static workSingTypeOption = [ | ||
25 | { label: '自主上传', value: 1 }, | ||
26 | { label: '唱整首', value: 2 }, | ||
27 | { label: '唱片段', value: 3 }, | ||
28 | ]; | ||
29 | |||
30 | static workSingStatusOption = [ | ||
31 | { label: '待采纳', value: 0 }, | ||
32 | { label: '已确认', value: 1 }, | ||
33 | { label: '不合适', value: 2 }, | ||
34 | { label: '未采纳', value: 3 }, | ||
35 | { label: '其他', value: 4 }, | ||
36 | ]; | ||
37 | |||
38 | static songTypeOption = [ | ||
39 | { label: '歌曲', value: 1 }, | ||
40 | { label: 'Demo', value: 2 }, | ||
41 | ]; | ||
42 | |||
43 | static async get(params: QueryForParams): Promise<ServiceResponse<Activity[]>> { | ||
44 | return axios.get('/activities', { params }); | ||
45 | } | ||
46 | |||
47 | static async show(id: number, params?: QueryForParams): Promise<Activity> { | ||
48 | return axios.get(`/activities/${id}`, { params }).then((res) => Promise.resolve(res.data)); | ||
49 | } | ||
50 | |||
51 | static async changeStatus(id: number, data: AnyObject) { | ||
52 | return axios.put<Activity>(`/activities/${id}/change-status`, data).then((res) => Promise.resolve(res.data)); | ||
53 | } | ||
54 | |||
55 | static async update(id: number, data: AnyObject): Promise<Activity> { | ||
56 | return axios.put(`/activities/${id}`, data).then((res) => Promise.resolve(res.data)); | ||
57 | } | ||
58 | |||
59 | static async destroy(id: number) { | ||
60 | return axios.delete(`/activities/${id}`).then((res) => Promise.resolve(res.data)); | ||
61 | } | ||
62 | |||
63 | static async getManageUser(activityId: number, params: QueryForParams): Promise<any> { | ||
64 | return axios.get(`/audition/activities/${activityId}/managers`, { params }); | ||
65 | } | ||
66 | |||
67 | static async createManage(data: AnyObject): Promise<any> { | ||
68 | return axios.post('/audition/activity-managers', data).then((res) => Promise.resolve(res.data)); | ||
69 | } | ||
70 | |||
71 | static async updateManage(id: number, data: AnyObject): Promise<any> { | ||
72 | return axios.put(`/audition/activity-managers/${id}`, data).then((res) => Promise.resolve(res.data)); | ||
73 | } | ||
74 | |||
75 | static async deleteManage(id: number): Promise<any> { | ||
76 | return axios.delete(`/audition/activity-managers/${id}`).then((res) => Promise.resolve(res.data)); | ||
77 | } | ||
78 | |||
79 | static async getViewUser(activityId: number, params: QueryForParams): Promise<ServiceResponse<ActivityViewUser[]>> { | ||
80 | return axios.get(`/activities/${activityId}/views`, { params }); | ||
81 | } | ||
82 | |||
83 | static async getLikeUser(activityId: number, params: QueryForParams): Promise<ServiceResponse<ActivityViewUser[]>> { | ||
84 | return axios.get(`/activities/${activityId}/collects`, { params }); | ||
85 | } | ||
86 | } | ||
87 | |||
88 | export class useApply { | ||
89 | static auditStatusOption = [ | ||
90 | { label: '审核不通过', value: 2 }, | ||
91 | { label: '审核中', value: 0 }, | ||
92 | ]; | ||
93 | |||
94 | static async get(params: QueryForParams): Promise<ServiceResponse<ActivityApply[]>> { | ||
95 | return axios.get('audition/applies', { params }); | ||
96 | } | ||
97 | |||
98 | static async create(data: AnyObject) { | ||
99 | return axios.post('/activities', data).then((res) => Promise.resolve(res.data)); | ||
100 | } | ||
101 | |||
102 | static async update(id: number, data: AnyObject) { | ||
103 | return axios.put(`audition/applies/${id}`, data).then((res) => Promise.resolve(res.data)); | ||
104 | } | ||
105 | |||
106 | static async destroy(id: number) { | ||
107 | return axios.delete(`/audition/applies/${id}`); | ||
108 | } | ||
109 | } | ||
110 |
src/api/auth.ts
0 → 100644
1 | import axios from "axios"; | ||
2 | import { AuthorizedState } from "@/store/modules/authorized/type"; | ||
3 | import FileSaver from "file-saver"; | ||
4 | |||
5 | export default class useAuthApi { | ||
6 | static async info() { | ||
7 | return axios | ||
8 | .get<{ | ||
9 | user: AuthorizedState; | ||
10 | permissions: string[]; | ||
11 | menus: string[]; | ||
12 | can_create_demo: number | ||
13 | }>("auth/info") | ||
14 | .then((res) => Promise.resolve(res.data)); | ||
15 | } | ||
16 | |||
17 | static changePwd(data: { password: string; password_confirmation: string }) { | ||
18 | return axios.put("auth/change-pwd", data); | ||
19 | } | ||
20 | |||
21 | static async downloadFile(url: string, fileName: string) { | ||
22 | // ?response-content-type=Blob | ||
23 | FileSaver.saveAs(`${url}`, fileName + url.substring(url.lastIndexOf("."))); | ||
24 | } | ||
25 | } |
src/api/dashboard.ts
0 → 100644
1 | import axios from 'axios'; | ||
2 | import type { TableData } from '@arco-design/web-vue/es/table/interface'; | ||
3 | |||
4 | export interface ContentDataRecord { | ||
5 | x: string; | ||
6 | y: number; | ||
7 | } | ||
8 | |||
9 | export function queryContentData() { | ||
10 | return axios.get<ContentDataRecord[]>('/api/content-data'); | ||
11 | } | ||
12 | |||
13 | export interface PopularRecord { | ||
14 | key: number; | ||
15 | clickNumber: string; | ||
16 | title: string; | ||
17 | increases: number; | ||
18 | } | ||
19 | |||
20 | export function queryPopularList(params: { type: string }) { | ||
21 | return axios.get<TableData[]>('/api/popular/list', { params }); | ||
22 | } |
src/api/interceptor.ts
0 → 100644
1 | import axios from 'axios'; | ||
2 | import type { AxiosRequestConfig, AxiosResponse } from 'axios'; | ||
3 | import { Message } from '@arco-design/web-vue'; | ||
4 | import { getToken } from '@/utils/auth'; | ||
5 | |||
6 | export interface HttpResponse<T = unknown> { | ||
7 | status: 'success' | 'fail' | 'error'; | ||
8 | msg: string; | ||
9 | code: number; | ||
10 | data: T; | ||
11 | } | ||
12 | |||
13 | export interface ServiceResponse<T = unknown> extends HttpResponse<T> { | ||
14 | meta: { current: number; limit: number; total: number }; | ||
15 | } | ||
16 | |||
17 | if (import.meta.env.VITE_API_URL) { | ||
18 | axios.defaults.baseURL = import.meta.env.VITE_API_URL; | ||
19 | } | ||
20 | |||
21 | axios.interceptors.request.use( | ||
22 | // @ts-ignore | ||
23 | (config: AxiosRequestConfig) => { | ||
24 | if (!config.url?.startsWith('/provider')) { | ||
25 | config.baseURL = `${config.baseURL}/user`; | ||
26 | } | ||
27 | |||
28 | const token = getToken(); | ||
29 | if (token) { | ||
30 | if (!config.headers) { | ||
31 | config.headers = {}; | ||
32 | } | ||
33 | config.headers.Authorization = `Bearer ${token}`; | ||
34 | } | ||
35 | return config; | ||
36 | }, | ||
37 | (error) => { | ||
38 | // do something | ||
39 | return Promise.reject(error); | ||
40 | } | ||
41 | ); | ||
42 | // add response interceptors | ||
43 | axios.interceptors.response.use( | ||
44 | // @ts-ignore | ||
45 | async (response: AxiosResponse<ServiceResponse>): Promise<ServiceResponse | unknown> => { | ||
46 | if (response.data instanceof Blob) { | ||
47 | return Promise.resolve(response); | ||
48 | } | ||
49 | |||
50 | return Promise.resolve(response.data); | ||
51 | }, | ||
52 | (error) => { | ||
53 | if (error.response.status === 401) { | ||
54 | // clearToken(); | ||
55 | window.location.reload(); | ||
56 | Message.warning({ id: 'token_lose', content: '登陆信息已失效,请重新登陆...' }); | ||
57 | } else { | ||
58 | Message.error({ | ||
59 | id: 'service_error', | ||
60 | content: error.response.data.msg || error.msg || 'Request Error', | ||
61 | duration: 5 * 1000, | ||
62 | }); | ||
63 | } | ||
64 | |||
65 | return Promise.reject(error); | ||
66 | } | ||
67 | ); |
src/api/message.ts
0 → 100644
1 | import axios from 'axios'; | ||
2 | |||
3 | export interface MessageRecord { | ||
4 | id: number; | ||
5 | type: string; | ||
6 | title: string; | ||
7 | subTitle: string; | ||
8 | avatar?: string; | ||
9 | content: string; | ||
10 | time: string; | ||
11 | status: 0 | 1; | ||
12 | messageType?: number; | ||
13 | } | ||
14 | export type MessageListType = MessageRecord[]; | ||
15 | |||
16 | export function queryMessageList() { | ||
17 | return axios.post<MessageListType>('/api/message/list'); | ||
18 | } | ||
19 | |||
20 | interface MessageStatus { | ||
21 | ids: number[]; | ||
22 | } | ||
23 | |||
24 | export function setMessageStatus(data: MessageStatus) { | ||
25 | return axios.post<MessageListType>('/api/message/read', data); | ||
26 | } | ||
27 | |||
28 | export interface ChatRecord { | ||
29 | id: number; | ||
30 | username: string; | ||
31 | content: string; | ||
32 | time: string; | ||
33 | isCollect: boolean; | ||
34 | } | ||
35 | |||
36 | export function queryChatList() { | ||
37 | return axios.post<ChatRecord[]>('/api/chat/list'); | ||
38 | } |
src/api/provider.ts
0 → 100644
1 | import { AnyObject } from '@/types/global'; | ||
2 | import axios from 'axios'; | ||
3 | |||
4 | export default class useProviderApi { | ||
5 | static sms(type: string, phone: string, area?: string) { | ||
6 | return axios.post('/provider/sms', { type, phone, area, platform: 'user' }); | ||
7 | } | ||
8 | |||
9 | static async area() { | ||
10 | return axios.get('/provider/area'); | ||
11 | } | ||
12 | |||
13 | static async config(params?: AnyObject) { | ||
14 | return axios.get('/provider/configs', { params }) | ||
15 | } | ||
16 | |||
17 | static async login(type: 'phone' | 'email', data: object) { | ||
18 | return axios.post<{ access_token: string; refresh_token: string; nick_name: string }>('/provider/login', { | ||
19 | platform: 'user', | ||
20 | type, | ||
21 | ...data, | ||
22 | }); | ||
23 | } | ||
24 | } |
src/api/user.ts
0 → 100644
1 | import { QueryForParams } from '@/types/global'; | ||
2 | import axios from 'axios'; | ||
3 | |||
4 | export default class useUserApi { | ||
5 | static statusOption = [ | ||
6 | { label: '启用', value: 1 }, | ||
7 | { label: '禁用', value: 0 }, | ||
8 | { label: '注销', value: 2 }, | ||
9 | ]; | ||
10 | |||
11 | static officialStatusOption = [ | ||
12 | { label: '已关注', value: 1 }, | ||
13 | { label: '未关注', value: 0 }, | ||
14 | ]; | ||
15 | |||
16 | static sexOption = [ | ||
17 | { label: '男', value: 1 }, | ||
18 | { label: '女', value: 2 }, | ||
19 | { label: '无', value: 0 }, | ||
20 | ]; | ||
21 | |||
22 | static scopeOption = [ | ||
23 | { label: '无权限', value: 0 }, | ||
24 | { label: '平台管理员', value: 1 }, | ||
25 | { label: '厂牌管理员', value: 2 }, | ||
26 | ]; | ||
27 | |||
28 | static async manageSongs(id: number, params?: QueryForParams) { | ||
29 | return axios.get(`users/${id}/manage-songs`, { params }); | ||
30 | } | ||
31 | } |
src/assets/image/favicon.ico
0 → 100644
No preview for this file type
src/assets/image/user-login-bg.jpg
0 → 100644

1.23 MB
src/assets/logo.svg
0 → 100644
1 | <svg width="33" height="33" viewBox="0 0 33 33" fill="none" xmlns="http://www.w3.org/2000/svg"> | ||
2 | <g clip-path="url(#clip0)"> | ||
3 | <path fill-rule="evenodd" clip-rule="evenodd" d="M5.37754 16.9795L12.7498 9.43027C14.7163 7.41663 17.9428 7.37837 19.9564 9.34482C19.9852 9.37297 20.0137 9.40145 20.0418 9.43027L20.1221 9.51243C22.1049 11.5429 22.1049 14.7847 20.1221 16.8152L12.7498 24.3644C10.7834 26.378 7.55686 26.4163 5.54322 24.4498C5.5144 24.4217 5.48592 24.3932 5.45777 24.3644L5.37754 24.2822C3.39468 22.2518 3.39468 19.0099 5.37754 16.9795Z" fill="#12D2AC"/> | ||
4 | <path fill-rule="evenodd" clip-rule="evenodd" d="M20.0479 9.43034L27.3399 16.8974C29.3674 18.9735 29.3674 22.2883 27.3399 24.3644C25.3735 26.3781 22.147 26.4163 20.1333 24.4499C20.1045 24.4217 20.076 24.3933 20.0479 24.3644L12.7558 16.8974C10.7284 14.8213 10.7284 11.5065 12.7558 9.43034C14.7223 7.4167 17.9488 7.37844 19.9624 9.34489C19.9912 9.37304 20.0197 9.40152 20.0479 9.43034Z" fill="#307AF2"/> | ||
5 | <path fill-rule="evenodd" clip-rule="evenodd" d="M20.1321 9.52163L23.6851 13.1599L16.3931 20.627L9.10103 13.1599L12.6541 9.52163C14.6707 7.45664 17.9794 7.4174 20.0444 9.434C20.074 9.46286 20.1032 9.49207 20.1321 9.52163Z" fill="#0057FE"/> | ||
6 | </g> | ||
7 | <defs> | ||
8 | <clipPath id="clip0"> | ||
9 | <rect width="26" height="19" fill="white" transform="translate(3.5 7)"/> | ||
10 | </clipPath> | ||
11 | </defs> | ||
12 | </svg> |
src/assets/style/breakpoint.less
0 → 100644
1 | // ==============breakpoint============ | ||
2 | |||
3 | // Extra small screen / phone | ||
4 | @screen-xs: 480px; | ||
5 | |||
6 | // Small screen / tablet | ||
7 | @screen-sm: 576px; | ||
8 | |||
9 | // Medium screen / desktop | ||
10 | @screen-md: 768px; | ||
11 | |||
12 | // Large screen / wide desktop | ||
13 | @screen-lg: 992px; | ||
14 | |||
15 | // Extra large screen / full hd | ||
16 | @screen-xl: 1200px; | ||
17 | |||
18 | // Extra extra large screen / large desktop | ||
19 | @screen-xxl: 1600px; |
src/assets/style/global.less
0 → 100644
1 | * { | ||
2 | box-sizing: border-box; | ||
3 | } | ||
4 | |||
5 | html, | ||
6 | body { | ||
7 | width: 100%; | ||
8 | height: 100%; | ||
9 | margin: 0; | ||
10 | padding: 0; | ||
11 | font-size: 14px; | ||
12 | background-color: var(--color-bg-1); | ||
13 | -moz-osx-font-smoothing: grayscale; | ||
14 | -webkit-font-smoothing: antialiased; | ||
15 | } | ||
16 | |||
17 | .echarts-tooltip-diy { | ||
18 | background: linear-gradient(304.17deg, | ||
19 | rgba(253, 254, 255, 0.6) -6.04%, | ||
20 | rgba(244, 247, 252, 0.6) 85.2%) !important; | ||
21 | border: none !important; | ||
22 | backdrop-filter: blur(10px) !important; | ||
23 | /* Note: backdrop-filter has minimal browser support */ | ||
24 | |||
25 | border-radius: 6px !important; | ||
26 | |||
27 | .content-panel { | ||
28 | display: flex; | ||
29 | justify-content: space-between; | ||
30 | width: auto; | ||
31 | height: 32px; | ||
32 | margin-bottom: 4px; | ||
33 | padding: 0 9px; | ||
34 | line-height: 32px; | ||
35 | background: rgba(255, 255, 255, 0.8); | ||
36 | border-radius: 4px; | ||
37 | box-shadow: 6px 0 20px rgba(34, 87, 188, 0.1); | ||
38 | } | ||
39 | |||
40 | .tooltip-title { | ||
41 | margin: 0 0 10px 0; | ||
42 | } | ||
43 | |||
44 | p { | ||
45 | margin: 0; | ||
46 | } | ||
47 | |||
48 | .tooltip-title, | ||
49 | .tooltip-value { | ||
50 | font-size: 13px; | ||
51 | line-height: 15px; | ||
52 | display: flex; | ||
53 | align-items: center; | ||
54 | text-align: right; | ||
55 | color: #1d2129; | ||
56 | font-weight: bold; | ||
57 | } | ||
58 | |||
59 | .tooltip-value { | ||
60 | margin-left: 10px; | ||
61 | } | ||
62 | |||
63 | .tooltip-item-icon { | ||
64 | display: inline-block; | ||
65 | margin-right: 8px; | ||
66 | width: 10px; | ||
67 | height: 10px; | ||
68 | border-radius: 50%; | ||
69 | } | ||
70 | } | ||
71 | |||
72 | .general-card { | ||
73 | border-radius: 4px; | ||
74 | border: none; | ||
75 | |||
76 | &>.arco-card-header { | ||
77 | height: auto; | ||
78 | padding: 10px 20px 0 20px; | ||
79 | border: none; | ||
80 | |||
81 | &>.arco-card-header-title { | ||
82 | font-size: 15px; | ||
83 | } | ||
84 | } | ||
85 | |||
86 | &>.arco-card-body { | ||
87 | padding: 0 20px 20px 20px; | ||
88 | } | ||
89 | } | ||
90 | |||
91 | .split-line { | ||
92 | border-color: rgb(var(--gray-2)); | ||
93 | } | ||
94 | |||
95 | .arco-table-cell { | ||
96 | .circle { | ||
97 | display: inline-block; | ||
98 | margin-right: 4px; | ||
99 | width: 6px; | ||
100 | height: 6px; | ||
101 | border-radius: 50%; | ||
102 | background-color: rgb(var(--blue-6)); | ||
103 | |||
104 | &.pass { | ||
105 | background-color: rgb(var(--green-6)); | ||
106 | } | ||
107 | |||
108 | &.warn { | ||
109 | background-color: rgb(var(--yellow-6)); | ||
110 | } | ||
111 | |||
112 | &.err { | ||
113 | background-color: rgb(var(--red-6)); | ||
114 | } | ||
115 | } | ||
116 | } | ||
117 | |||
118 | .mt-0 { | ||
119 | margin-bottom: 0; | ||
120 | } | ||
121 | |||
122 | .mt-20 { | ||
123 | margin-top: 20px; | ||
124 | } | ||
125 | |||
126 | .mb-0 { | ||
127 | margin-bottom: 0; | ||
128 | } | ||
129 | |||
130 | .color-grey { | ||
131 | color: rgba(44, 44, 44, 0.5) | ||
132 | } | ||
133 | |||
134 | .justify-center { | ||
135 | justify-content: center !important; | ||
136 | } | ||
137 | |||
138 | |||
139 | .retry { | ||
140 | div.arco-modal-header { | ||
141 | border-bottom: unset !important; | ||
142 | } | ||
143 | |||
144 | div.arco-modal-body { | ||
145 | padding: 0 24px 20px; | ||
146 | } | ||
147 | } | ||
148 | |||
149 | .link-hover:hover { | ||
150 | cursor: pointer !important; | ||
151 | } | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
src/assets/world.json
0 → 100644
This diff could not be displayed because it is too large.
src/components/audio-player/index.vue
0 → 100644
1 | <template> | ||
2 | <audio v-if="url" :id="uuid" :src="url" class="player" controls controlsList="nodownload noplaybackrate" @play="onPay" /> | ||
3 | </template> | ||
4 | |||
5 | <script lang="ts" setup> | ||
6 | import { nanoid } from 'nanoid'; | ||
7 | import { computed } from 'vue'; | ||
8 | |||
9 | defineProps<{ url: string; name: string }>(); | ||
10 | |||
11 | const uuid = computed(() => `audio-${nanoid()}`); | ||
12 | |||
13 | const onPay = (e: any) => { | ||
14 | const audios = document.getElementsByTagName('audio'); | ||
15 | [].forEach.call(audios, (i: HTMLAudioElement) => i !== e.target && i.pause()); | ||
16 | }; | ||
17 | </script> | ||
18 | |||
19 | <style lang="less" scoped> | ||
20 | .player { | ||
21 | height: 30px; | ||
22 | width: 100%; | ||
23 | } | ||
24 | </style> |
src/components/avatar-upload/index.vue
0 → 100644
1 | <template> | ||
2 | <Upload :accept="accept" :custom-request="onUpload" :on-before-upload="onBeforeUpload" :file-list="[]" :show-file-list="false"> | ||
3 | <template #upload-button> | ||
4 | <Avatar :shape="shape" :size="size" trigger-type="mask"> | ||
5 | <div v-if="!modelValue" class="no-avatar"> | ||
6 | <icon-plus /> | ||
7 | </div> | ||
8 | <img v-else :src="modelValue" style="width: 100%; height: 100%" alt="" /> | ||
9 | <template #trigger-icon> | ||
10 | <icon-camera /> | ||
11 | </template> | ||
12 | </Avatar> | ||
13 | </template> | ||
14 | </Upload> | ||
15 | </template> | ||
16 | |||
17 | <script lang="ts" setup> | ||
18 | import useOss from '@/hooks/oss'; | ||
19 | import { IconCamera, IconPlus } from '@arco-design/web-vue/es/icon'; | ||
20 | import { Avatar, Message, Upload, UploadRequest, useFormItem } from '@arco-design/web-vue'; | ||
21 | import { computed, toRefs } from 'vue'; | ||
22 | |||
23 | const props = withDefaults( | ||
24 | defineProps<{ | ||
25 | modelValue?: string; | ||
26 | size?: number; | ||
27 | shape?: 'circle' | 'square'; | ||
28 | accept?: string; | ||
29 | limit?: number; | ||
30 | }>(), | ||
31 | { | ||
32 | modelValue: '', | ||
33 | size: 80, | ||
34 | shape: 'circle', | ||
35 | accept: 'image/*', | ||
36 | limit: 5, | ||
37 | } | ||
38 | ); | ||
39 | |||
40 | const { shape } = toRefs(props); | ||
41 | |||
42 | const borderRadius = computed(() => (shape.value === 'square' ? 'unset' : 'var(--border-radius-circle)')); | ||
43 | |||
44 | const { eventHandlers } = useFormItem(); | ||
45 | |||
46 | const emits = defineEmits(['update:modelValue']); | ||
47 | |||
48 | const { upload } = useOss(); | ||
49 | |||
50 | const onBeforeUpload = (file: File) => { | ||
51 | if (file.size > props.limit * 1024 * 1024) { | ||
52 | Message.warning(`${file.name} 文件超过${props.limit}MB,无法上传`); | ||
53 | return Promise.resolve(false); | ||
54 | } | ||
55 | |||
56 | return Promise.resolve(file); | ||
57 | }; | ||
58 | |||
59 | const onUpload = (option: any): UploadRequest => { | ||
60 | const { fileItem } = option; | ||
61 | |||
62 | if (fileItem.file) { | ||
63 | upload(fileItem.file, 'image').then((res) => { | ||
64 | emits('update:modelValue', res?.url || ''); | ||
65 | eventHandlers.value?.onChange?.(); | ||
66 | }); | ||
67 | } | ||
68 | |||
69 | return {}; | ||
70 | }; | ||
71 | </script> | ||
72 | |||
73 | <style lang="less" scoped> | ||
74 | :deep(.arco-avatar-text) { | ||
75 | width: 100%; | ||
76 | height: 100%; | ||
77 | overflow: hidden; | ||
78 | display: inline-block; | ||
79 | border-radius: v-bind(borderRadius); | ||
80 | } | ||
81 | |||
82 | .arco-avatar-image { | ||
83 | transform: unset !important; | ||
84 | } | ||
85 | |||
86 | .no-avatar { | ||
87 | top: 0; | ||
88 | left: 0; | ||
89 | display: flex; | ||
90 | position: absolute; | ||
91 | align-content: center; | ||
92 | align-items: center; | ||
93 | justify-content: center; | ||
94 | width: 100%; | ||
95 | height: 100%; | ||
96 | } | ||
97 | </style> |
src/components/breadcrumb/index.vue
0 → 100644
1 | <template> | ||
2 | <a-breadcrumb class="container-breadcrumb"> | ||
3 | <a-breadcrumb-item> | ||
4 | <icon-apps /> | ||
5 | </a-breadcrumb-item> | ||
6 | <a-breadcrumb-item v-for="item in items" :key="item"> | ||
7 | <router-link v-if="getRouteMeta(item)?.isRedirect" :to="{ name: item }"> | ||
8 | {{ getRouteName(item) }} | ||
9 | </router-link> | ||
10 | <span v-else> {{ getRouteName(item) }}</span> | ||
11 | </a-breadcrumb-item> | ||
12 | </a-breadcrumb> | ||
13 | </template> | ||
14 | |||
15 | <script lang="ts" setup> | ||
16 | import { IconApps } from '@arco-design/web-vue/es/icon'; | ||
17 | import { computed } from 'vue'; | ||
18 | import { RouteMeta, RouteRecordNormalized, useRouter } from 'vue-router'; | ||
19 | import { useAppStore } from '@/store'; | ||
20 | import { storeToRefs } from 'pinia'; | ||
21 | |||
22 | defineProps<{ items: string[] }>(); | ||
23 | |||
24 | const router = useRouter(); | ||
25 | const appStore = useAppStore(); | ||
26 | |||
27 | const { appMenu } = storeToRefs(appStore); | ||
28 | |||
29 | const routes = computed(() => { | ||
30 | return router.getRoutes() as RouteRecordNormalized[]; | ||
31 | }); | ||
32 | |||
33 | const getRoute = (name: any) => { | ||
34 | return routes.value.find((el) => el.name === name); | ||
35 | }; | ||
36 | |||
37 | const getRouteMeta = (name: any): RouteMeta | undefined => { | ||
38 | return getRoute(name)?.meta; | ||
39 | }; | ||
40 | |||
41 | const getRouteName = (name: any) => { | ||
42 | return getRouteMeta(name)?.title || appMenu.value.find((item) => item.name === name)?.label || ''; | ||
43 | }; | ||
44 | </script> | ||
45 | |||
46 | <style lang="less" scoped> | ||
47 | .container-breadcrumb { | ||
48 | margin: 16px 0; | ||
49 | |||
50 | :deep(.arco-breadcrumb-item) { | ||
51 | color: rgb(var(--gray-6)); | ||
52 | |||
53 | &:last-child { | ||
54 | color: rgb(var(--gray-8)); | ||
55 | } | ||
56 | } | ||
57 | } | ||
58 | </style> |
src/components/chart/index.vue
0 → 100644
1 | <template> | ||
2 | <VCharts v-if="renderChart" :option="options" :autoresize="autoresize" :style="{ width, height }" /> | ||
3 | </template> | ||
4 | |||
5 | <script lang="ts"> | ||
6 | import { defineComponent, ref, computed, nextTick } from 'vue'; | ||
7 | import VCharts from 'vue-echarts'; | ||
8 | import { useAppStore } from '@/store'; | ||
9 | |||
10 | export default defineComponent({ | ||
11 | components: { | ||
12 | VCharts, | ||
13 | }, | ||
14 | props: { | ||
15 | options: { | ||
16 | type: Object, | ||
17 | default() { | ||
18 | return {}; | ||
19 | }, | ||
20 | }, | ||
21 | autoresize: { | ||
22 | type: Boolean, | ||
23 | default: true, | ||
24 | }, | ||
25 | width: { | ||
26 | type: String, | ||
27 | default: '100%', | ||
28 | }, | ||
29 | height: { | ||
30 | type: String, | ||
31 | default: '100%', | ||
32 | }, | ||
33 | }, | ||
34 | setup() { | ||
35 | const appStore = useAppStore(); | ||
36 | const theme = computed(() => { | ||
37 | if (appStore.theme === 'dark') return 'dark'; | ||
38 | return ''; | ||
39 | }); | ||
40 | const renderChart = ref(false); | ||
41 | // wait container expand | ||
42 | nextTick(() => { | ||
43 | renderChart.value = true; | ||
44 | }); | ||
45 | return { | ||
46 | theme, | ||
47 | renderChart, | ||
48 | }; | ||
49 | }, | ||
50 | }); | ||
51 | </script> | ||
52 | |||
53 | <style scoped lang="less"></style> |
src/components/export-button/index.vue
0 → 100644
1 | <template> | ||
2 | <a-button :loading="loading" @click="onClick"> | ||
3 | <template #icon> | ||
4 | <icon-download /> | ||
5 | </template> | ||
6 | {{ label }} | ||
7 | </a-button> | ||
8 | </template> | ||
9 | |||
10 | <script lang="ts" setup> | ||
11 | import { IconDownload } from '@arco-design/web-vue/es/icon'; | ||
12 | import useLoading from '@/hooks/loading'; | ||
13 | |||
14 | const props = withDefaults( | ||
15 | defineProps<{ | ||
16 | label: string; | ||
17 | onDownload: () => Promise<void>; | ||
18 | }>(), | ||
19 | { | ||
20 | label: '导出', | ||
21 | } | ||
22 | ); | ||
23 | |||
24 | const { loading, setLoading } = useLoading(false); | ||
25 | |||
26 | const onClick = () => { | ||
27 | setLoading(true); | ||
28 | props.onDownload().finally(() => setLoading(false)); | ||
29 | }; | ||
30 | </script> | ||
31 | |||
32 | <style scoped></style> |
1 | <script setup lang="ts"> | ||
2 | import TableColumn from '@/components/filter/table-column.vue'; | ||
3 | import { Layout, LayoutSider, LayoutContent, Image, Form, FormItem, TypographyText, TableData } from '@arco-design/web-vue'; | ||
4 | import { get } from 'lodash'; | ||
5 | import { isString, isUndefined } from '@/utils'; | ||
6 | |||
7 | const props = withDefaults( | ||
8 | defineProps<{ | ||
9 | title?: string; | ||
10 | dataIndex?: string; | ||
11 | row?: string; | ||
12 | coverIndex?: string; | ||
13 | nameIndex?: string; | ||
14 | subIndex?: string; | ||
15 | tagIndex?: string; | ||
16 | projectIndex?: string; | ||
17 | hideSubTitle?: boolean; | ||
18 | }>(), | ||
19 | { | ||
20 | coverIndex: 'cover', | ||
21 | nameIndex: 'song_name', | ||
22 | subIndex: 'sub_title', | ||
23 | tagIndex: 'tags', | ||
24 | projectIndex: 'user', | ||
25 | } | ||
26 | ); | ||
27 | |||
28 | const getRow = (record: TableData): TableData | undefined => { | ||
29 | if (isUndefined(props.row)) { | ||
30 | return record; | ||
31 | } | ||
32 | |||
33 | if (isString(props.row)) { | ||
34 | return get(record, props.row); | ||
35 | } | ||
36 | |||
37 | return props.row; | ||
38 | }; | ||
39 | |||
40 | const getRowColumn = (record: TableData, key: string) => getRow(record)?.[key]; | ||
41 | </script> | ||
42 | |||
43 | <template> | ||
44 | <TableColumn v-bind="$attrs" :title="title" :data-index="dataIndex"> | ||
45 | <template #default="{ record }"> | ||
46 | <Layout> | ||
47 | <LayoutSider :width="104" class="left"> | ||
48 | <Image show-loader :height="100" :width="100" fit="cover" :src="getRowColumn(record, coverIndex)" /> | ||
49 | </LayoutSider> | ||
50 | <LayoutContent> | ||
51 | <Form auto-label-width label-align="left" :model="record" style="height: 100%; justify-content: space-between"> | ||
52 | <FormItem label="歌曲名称" :show-colon="true" row-class="mb-0"> | ||
53 | <TypographyText :ellipsis="{ rows: 1, showTooltip: true }"> | ||
54 | {{ getRowColumn(record, nameIndex) }} | ||
55 | </TypographyText> | ||
56 | </FormItem> | ||
57 | <FormItem v-if="!hideSubTitle" label="推荐语" :show-colon="true" row-class="mb-0"> | ||
58 | <TypographyText :ellipsis="{ rows: 1, showTooltip: true }"> | ||
59 | {{ getRowColumn(record, subIndex) }} | ||
60 | </TypographyText> | ||
61 | </FormItem> | ||
62 | <FormItem label="歌曲标签" :show-colon="true" row-class="mb-0"> | ||
63 | <TypographyText :ellipsis="{ rows: 1, showTooltip: true }"> | ||
64 | {{ | ||
65 | getRowColumn(record, tagIndex) | ||
66 | .map((item: any) => item.name) | ||
67 | .join('、') | ||
68 | }} | ||
69 | </TypographyText> | ||
70 | </FormItem> | ||
71 | <FormItem label="关联用户" :show-colon="true" row-class="mb-0"> | ||
72 | <TypographyText :ellipsis="{ rows: 1, showTooltip: true }"> | ||
73 | {{ getRowColumn(record, projectIndex)?.nick_name }} | ||
74 | </TypographyText> | ||
75 | </FormItem> | ||
76 | </Form> | ||
77 | </LayoutContent> | ||
78 | </Layout> | ||
79 | </template> | ||
80 | </TableColumn> | ||
81 | </template> | ||
82 | |||
83 | <style lang="less" scoped> | ||
84 | :deep(.arco-typography) { | ||
85 | margin-bottom: 0; | ||
86 | width: 100%; | ||
87 | text-align: left; | ||
88 | } | ||
89 | |||
90 | .left { | ||
91 | margin-top: 5px; | ||
92 | box-shadow: unset; | ||
93 | margin-right: 5px; | ||
94 | } | ||
95 | </style> |
1 | <script setup lang="ts"> | ||
2 | import { Avatar, TableColumn } from '@arco-design/web-vue'; | ||
3 | import { get } from 'lodash'; | ||
4 | |||
5 | const props = defineProps<{ title?: string; dataIndex?: string; width?: number }>(); | ||
6 | |||
7 | const getValue = (record: object) => { | ||
8 | return get(record, props.dataIndex || '', ''); | ||
9 | }; | ||
10 | </script> | ||
11 | |||
12 | <template> | ||
13 | <TableColumn v-bind="$attrs" :title="title" :data-index="dataIndex" :width="width || 60"> | ||
14 | <template #cell="{ record }"> | ||
15 | <Avatar :size="40" shape="circle"> | ||
16 | <img alt="" :src="getValue(record)" :width="40" :height="40" /> | ||
17 | </Avatar> | ||
18 | </template> | ||
19 | </TableColumn> | ||
20 | </template> | ||
21 | |||
22 | <style scoped lang="less"></style> |
src/components/filter/date-table-column.vue
0 → 100644
1 | <script setup lang="ts"> | ||
2 | import { get } from "lodash"; | ||
3 | import TableColumn from "@/components/filter/table-column.vue"; | ||
4 | import dayjs from "dayjs"; | ||
5 | |||
6 | withDefaults(defineProps<{ title?: string; dataIndex: string; split?: boolean }>(), { split: true }); | ||
7 | const getValue = (record: object, path: string) => { | ||
8 | return get(record, path, ""); | ||
9 | }; | ||
10 | </script> | ||
11 | |||
12 | <template> | ||
13 | <TableColumn v-bind="$attrs" :title="title" :data-index="dataIndex"> | ||
14 | <template #default="{ record }"> | ||
15 | <template v-if="dataIndex && !getValue(record, dataIndex)"> | ||
16 | <span style="color: rgba(0, 0, 0, 0.3)">无</span> | ||
17 | </template> | ||
18 | <template v-else-if="split"> | ||
19 | <div>{{ dayjs(getValue(record, dataIndex))?.format("YYYY-MM-DD") || "" }}</div> | ||
20 | <div>{{ dayjs(getValue(record, dataIndex))?.format("HH:mm:ss") || "" }}</div> | ||
21 | </template> | ||
22 | <template v-else>{{ getValue(record, dataIndex) }}</template> | ||
23 | </template> | ||
24 | </TableColumn> | ||
25 | </template> | ||
26 | |||
27 | <style scoped lang="less"></style> |
src/components/filter/enum-table-column.vue
0 → 100644
1 | <script setup lang="ts"> | ||
2 | import { Options } from '@/types/global'; | ||
3 | import { eq, get } from 'lodash'; | ||
4 | import TableColumn from '@/components/filter/table-column.vue'; | ||
5 | |||
6 | const props = defineProps<{ title?: string; dataIndex?: string; option: Options[]; darkValue?: string | number }>(); | ||
7 | |||
8 | const getValue = (record: object) => { | ||
9 | return get(record, props.dataIndex || '', ''); | ||
10 | }; | ||
11 | </script> | ||
12 | |||
13 | <template> | ||
14 | <TableColumn v-bind="$attrs" :title="title" :data-index="dataIndex"> | ||
15 | <template #default="{ record }"> | ||
16 | <template v-if="darkValue !== undefined && eq(getValue(record), darkValue)"> | ||
17 | <span style="color: rgba(44, 44, 44, 0.5)"> {{ option.find((item) => item.value === getValue(record))?.label || '未知' }}</span> | ||
18 | </template> | ||
19 | <template v-else> {{ option.find((item) => item.value === getValue(record))?.label || '未知' }}</template> | ||
20 | </template> | ||
21 | </TableColumn> | ||
22 | </template> | ||
23 | |||
24 | <style scoped lang="less"></style> |
src/components/filter/link-table-column.vue
0 → 100644
1 | <script setup lang="ts"> | ||
2 | import TableColumn from '@/components/filter/table-column.vue'; | ||
3 | import { Link } from '@arco-design/web-vue'; | ||
4 | import { AnyObject } from '@/types/global'; | ||
5 | |||
6 | defineProps<{ | ||
7 | title?: string; | ||
8 | dataIndex?: string; | ||
9 | linkStyle?: object; | ||
10 | formatter: (record: AnyObject) => string; | ||
11 | to: (record: AnyObject) => void; | ||
12 | }>(); | ||
13 | </script> | ||
14 | |||
15 | <template> | ||
16 | <TableColumn v-bind="$attrs" :title="title" :data-index="dataIndex"> | ||
17 | <template #default="{ record }"> | ||
18 | <Link v-if="!!formatter(record)" class="link" :style="linkStyle" :hoverable="false" @click.stop="to(record)">{{ | ||
19 | formatter(record) | ||
20 | }}</Link> | ||
21 | <span v-else style="color: rgba(44, 44, 44, 0.5)">无</span> | ||
22 | </template> | ||
23 | </TableColumn> | ||
24 | </template> | ||
25 | |||
26 | <style scoped lang="less"> | ||
27 | .link:hover { | ||
28 | cursor: pointer; | ||
29 | } | ||
30 | </style> |
src/components/filter/name-table-column.vue
0 → 100644
1 | <script setup lang="ts"> | ||
2 | import { get } from 'lodash'; | ||
3 | import TableColumn from '@/components/filter/table-column.vue'; | ||
4 | |||
5 | const props = defineProps<{ title?: string; dataIndex?: string; nickIndex?: string; realIndex?: string }>(); | ||
6 | |||
7 | const fullName = (record: object) => { | ||
8 | let name = ''; | ||
9 | |||
10 | if (props.nickIndex && get(record, props.nickIndex, '')) { | ||
11 | name += get(record, props.nickIndex, ''); | ||
12 | } | ||
13 | |||
14 | if (props.realIndex && get(record, props.realIndex)) { | ||
15 | name += `(${get(record, props.realIndex, '')})`; | ||
16 | } | ||
17 | |||
18 | return name; | ||
19 | }; | ||
20 | </script> | ||
21 | |||
22 | <template> | ||
23 | <TableColumn v-bind="$attrs" :title="title" :data-index="dataIndex"> | ||
24 | <template #default="{ record }"> {{ fullName(record) }}</template> | ||
25 | </TableColumn> | ||
26 | </template> | ||
27 | |||
28 | <style scoped lang="less"></style> |
1 | <script setup lang="ts"> | ||
2 | import { eq, get } from 'lodash'; | ||
3 | import TableColumn from '@/components/filter/table-column.vue'; | ||
4 | |||
5 | const props = withDefaults( | ||
6 | defineProps<{ title?: string; dataIndex?: string; darkValue?: string | number; prefix?: string; suffix?: string }>(), | ||
7 | { prefix: '', suffix: '' } | ||
8 | ); | ||
9 | |||
10 | const getValue = (record: object) => { | ||
11 | return get(record, props.dataIndex || '', ''); | ||
12 | }; | ||
13 | </script> | ||
14 | |||
15 | <template> | ||
16 | <TableColumn v-bind="$attrs" :title="title" :data-index="dataIndex"> | ||
17 | <template #default="{ record }"> | ||
18 | <template v-if="darkValue !== undefined && eq(getValue(record), darkValue)"> | ||
19 | <span style="color: rgba(44, 44, 44, 0.5)">{{ `${prefix} 0 ${suffix}` }}</span> | ||
20 | </template> | ||
21 | <template v-else> {{ `${prefix} ${getValue(record)} ${suffix}` }} </template> | ||
22 | </template> | ||
23 | </TableColumn> | ||
24 | </template> | ||
25 | |||
26 | <style scoped lang="less"></style> |
src/components/filter/phone-table-column.vue
0 → 100644
1 | <script setup lang="ts"> | ||
2 | import { get } from "lodash"; | ||
3 | import TableColumn from "@/components/filter/table-column.vue"; | ||
4 | |||
5 | withDefaults(defineProps<{ title?: string; dataIndex: string; areaIndex?: string }>(), { | ||
6 | areaIndex: "area_code" | ||
7 | }); | ||
8 | |||
9 | const getValue = (record: object, path: string) => { | ||
10 | return get(record, path, ""); | ||
11 | }; | ||
12 | </script> | ||
13 | |||
14 | <template> | ||
15 | <TableColumn v-bind="$attrs" :title="title" :data-index="dataIndex"> | ||
16 | <template #default="{ record }"> {{ `(+${getValue(record, areaIndex)}) ${getValue(record, dataIndex)}` }}</template> | ||
17 | </TableColumn> | ||
18 | </template> | ||
19 | |||
20 | <style scoped lang="less"></style> |
src/components/filter/search-item.vue
0 → 100644
1 | <script setup lang="ts"> | ||
2 | import { GridItem, FormItem } from '@arco-design/web-vue'; | ||
3 | </script> | ||
4 | |||
5 | <template> | ||
6 | <GridItem> | ||
7 | <FormItem class="form-item" row-class="mb-0" v-bind="$attrs" :show-colon="true"> | ||
8 | <slot /> | ||
9 | </FormItem> | ||
10 | </GridItem> | ||
11 | </template> | ||
12 | |||
13 | <style scoped lang="less"> | ||
14 | .form-item { | ||
15 | :deep(.arco-picker) { | ||
16 | width: 100%; | ||
17 | } | ||
18 | } | ||
19 | </style> |
src/components/filter/search.vue
0 → 100644
1 | <script setup lang="ts"> | ||
2 | import { AnyObject } from "@/types/global"; | ||
3 | import { Layout, LayoutContent, LayoutSider, Form, Space, Grid } from "@arco-design/web-vue"; | ||
4 | import IconButton from "@/components/icon-button/index.vue"; | ||
5 | |||
6 | type PropType = { | ||
7 | model?: AnyObject; | ||
8 | loading?: boolean; | ||
9 | searchLabel?: string; | ||
10 | searchIcon?: string; | ||
11 | resetLabel?: string; | ||
12 | resetIcon?: string; | ||
13 | hideSearch?: boolean; | ||
14 | hideReset?: boolean; | ||
15 | inline?: boolean; | ||
16 | split?: number; | ||
17 | size?: "mini" | "small" | "medium" | "large"; | ||
18 | hideDivider?: boolean; | ||
19 | }; | ||
20 | |||
21 | const props = withDefaults(defineProps<PropType>(), { | ||
22 | loading: false, | ||
23 | searchLabel: "搜索", | ||
24 | searchIcon: "search", | ||
25 | resetLabel: "重置", | ||
26 | resetIcon: "refresh", | ||
27 | hideSearch: false, | ||
28 | hideReset: false, | ||
29 | hideDivider: false, | ||
30 | inline: false, | ||
31 | split: 3, | ||
32 | size: "small" | ||
33 | }); | ||
34 | defineEmits<{ (e?: "search"): void; (e?: "reset"): void }>(); | ||
35 | |||
36 | const layoutStyle = { marginBottom: "12px" }; | ||
37 | const layoutRightStyle = props.hideSearch && props.hideReset ? {} : { borderLeft: "1px solid var(--color-neutral-3)" }; | ||
38 | |||
39 | if (!props.hideDivider) { | ||
40 | Object.assign(layoutStyle, { paddingBottom: "12px", borderBottom: "1px solid var(--color-neutral-3)" }); | ||
41 | } | ||
42 | </script> | ||
43 | |||
44 | <template> | ||
45 | <Layout :style="layoutStyle"> | ||
46 | <LayoutContent> | ||
47 | <Form auto-label-width :model="model || {}" label-align="right" :size="size"> | ||
48 | <Grid :cols="split as number" :col-gap="12" :row-gap="12"> | ||
49 | <slot :size="size" /> | ||
50 | </Grid> | ||
51 | </Form> | ||
52 | </LayoutContent> | ||
53 | <LayoutSider class="right" :style="layoutRightStyle"> | ||
54 | <Space :size="12" :direction="inline ? 'horizontal' : 'vertical'"> | ||
55 | <IconButton | ||
56 | v-if="!hideSearch" | ||
57 | :size="size" | ||
58 | :icon="searchIcon" | ||
59 | :label="searchLabel" | ||
60 | type="primary" | ||
61 | :loading="loading" | ||
62 | @click="$emit('search')" | ||
63 | /> | ||
64 | <IconButton v-if="!hideReset" :size="size" :icon="resetIcon" :label="resetLabel" @click="$emit('reset')" /> | ||
65 | <slot name="button" /> | ||
66 | </Space> | ||
67 | </LayoutSider> | ||
68 | </Layout> | ||
69 | </template> | ||
70 | |||
71 | <style scoped lang="less"> | ||
72 | .right { | ||
73 | border-left: 1px solid var(--color-neutral-3); | ||
74 | margin-left: 12px; | ||
75 | padding-left: 12px; | ||
76 | box-shadow: unset; | ||
77 | width: auto !important; | ||
78 | } | ||
79 | </style> |
src/components/filter/space-table-column.vue
0 → 100644
1 | <script setup lang="ts"> | ||
2 | import { Space, TableData } from "@arco-design/web-vue"; | ||
3 | import TableColumn from "@/components/filter/table-column.vue"; | ||
4 | |||
5 | withDefaults(defineProps<{ title?: string; dataIndex?: string; direction?: "vertical" | "horizontal" }>(), { | ||
6 | direction: "horizontal" | ||
7 | }); | ||
8 | </script> | ||
9 | |||
10 | <template> | ||
11 | <TableColumn v-bind="$attrs" :title="title" :data-index="dataIndex" :tooltip="false"> | ||
12 | <template #default="{ record, rowIndex }: { record: TableData, rowIndex?: number }"> | ||
13 | <Space :direction="direction" fill> | ||
14 | <slot name="default" :record="record" :index="rowIndex" /> | ||
15 | </Space> | ||
16 | </template> | ||
17 | </TableColumn> | ||
18 | </template> | ||
19 | |||
20 | <style scoped lang="less"></style> |
src/components/filter/table-column.vue
0 → 100644
1 | <script setup lang="ts"> | ||
2 | // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
3 | import { TableColumn, TableData, TableSortable } from '@arco-design/web-vue'; | ||
4 | |||
5 | type PropType = { title?: string; dataIndex?: string; tooltip?: boolean; ellipsis?: boolean; hasSort?: boolean }; | ||
6 | |||
7 | const sortable = { sortDirections: ['ascend', 'descend'], sorter: true, defaultSortOrder: '' } as TableSortable; | ||
8 | |||
9 | withDefaults(defineProps<PropType>(), { tooltip: true, hasSort: false }); | ||
10 | </script> | ||
11 | |||
12 | <template> | ||
13 | <TableColumn | ||
14 | v-bind="$attrs" | ||
15 | :title="title" | ||
16 | :data-index="dataIndex" | ||
17 | :tooltip="tooltip as boolean" | ||
18 | :ellipsis="ellipsis || tooltip as boolean" | ||
19 | :sortable="hasSort ? sortable : undefined as any" | ||
20 | > | ||
21 | <template v-if="$slots.default" #cell="{ record, rowIndex }: { record: TableData, rowIndex: number }"> | ||
22 | <slot name="default" :record="record" :index="rowIndex" /> | ||
23 | </template> | ||
24 | </TableColumn> | ||
25 | </template> | ||
26 | |||
27 | <style scoped lang="less"></style> |
src/components/filter/table.vue
0 → 100644
1 | <script setup lang="ts"> | ||
2 | import { computed, ref } from 'vue'; | ||
3 | import { Col, Row, Table } from '@arco-design/web-vue'; | ||
4 | import { AnyObject, sizeType } from '@/types/global'; | ||
5 | import usePagination from '@/hooks/pagination'; | ||
6 | |||
7 | type PropType = { | ||
8 | loading?: boolean; | ||
9 | rowKey?: string; | ||
10 | hoverType?: string; | ||
11 | simplePage?: boolean; | ||
12 | pageSize?: number; | ||
13 | size?: sizeType; | ||
14 | onQuery: (params?: AnyObject) => Promise<any>; | ||
15 | }; | ||
16 | |||
17 | defineEmits<{ (e: 'rowSort', dataIndex: string, direction: string): void }>(); | ||
18 | |||
19 | const props = withDefaults(defineProps<PropType>(), { loading: false, rowKey: 'id', size: 'small', pageSize: 20 }); | ||
20 | const { pagination, setPage, setPageSize, setTotal } = usePagination({ | ||
21 | simple: props.simplePage, | ||
22 | size: props.size, | ||
23 | pageSize: props.pageSize, | ||
24 | }); | ||
25 | const list = ref<any[]>([]); | ||
26 | const tableRef = ref(); | ||
27 | |||
28 | const pageQueryParams = computed(() => { | ||
29 | return { page: pagination.value.current, pageSize: pagination.value.pageSize }; | ||
30 | }); | ||
31 | |||
32 | const hoverType = computed(() => props.hoverType || 'default'); | ||
33 | |||
34 | const onFetch = () => | ||
35 | props.onQuery(pageQueryParams.value).then(({ data, meta }) => { | ||
36 | list.value = data; | ||
37 | setPage(meta.current); | ||
38 | setTotal(meta.total); | ||
39 | setPageSize(meta.limit); | ||
40 | }); | ||
41 | |||
42 | const onPageChange = (page: number) => { | ||
43 | setPage(page || 1); | ||
44 | onFetch(); | ||
45 | }; | ||
46 | |||
47 | const onSizeChange = (size: number) => { | ||
48 | setPageSize(size); | ||
49 | setPage(1); | ||
50 | onFetch(); | ||
51 | }; | ||
52 | |||
53 | const onPush = (row: unknown) => { | ||
54 | list.value.unshift(row); | ||
55 | // eslint-disable-next-line no-plusplus | ||
56 | ++pagination.value.total; | ||
57 | }; | ||
58 | |||
59 | const onRemove = (index: number) => { | ||
60 | list.value.splice(index, 1); | ||
61 | // eslint-disable-next-line no-plusplus | ||
62 | --pagination.value.total; | ||
63 | }; | ||
64 | |||
65 | const resetSort = () => tableRef.value?.clearSorters(); | ||
66 | |||
67 | const getCount = () => pagination.value.total; | ||
68 | |||
69 | const formatSortType = (type: string): string => type?.replace('end', '') ?? ''; | ||
70 | |||
71 | defineExpose({ onFetch, onPageChange, onSizeChange, resetSort, getCount, onPush, onRemove }); | ||
72 | </script> | ||
73 | |||
74 | <template> | ||
75 | <Row justify="space-between" align="center"> | ||
76 | <Col flex="1" class="table-tool-item" style="text-align: left"> | ||
77 | <slot name="tool" :size="size" /> | ||
78 | </Col> | ||
79 | <Col flex="auto" class="table-tool-item" style="text-align: right"> | ||
80 | <slot name="tool-right" :size="size" /> | ||
81 | </Col> | ||
82 | </Row> | ||
83 | <Table ref="tableRef" v-bind="$attrs" :row-key="rowKey as string" :loading="loading as boolean" :size="size as sizeType" | ||
84 | :data="list" :pagination="pagination" :bordered="false" :table-layout-fixed="true" @page-change="onPageChange" | ||
85 | @page-size-change="onSizeChange" | ||
86 | @sorter-change="(dataIndex, direction) => $emit('rowSort', dataIndex, formatSortType(direction))"> | ||
87 | <template #columns> | ||
88 | <slot /> | ||
89 | </template> | ||
90 | </Table> | ||
91 | </template> | ||
92 | |||
93 | <style lang="less" scoped> | ||
94 | :deep(.arco-table-cell) { | ||
95 | padding: 5px 8px !important; | ||
96 | |||
97 | :hover { | ||
98 | cursor: v-bind(hoverType); | ||
99 | } | ||
100 | |||
101 | &>.arco-table-td-content .arco-btn-size-small { | ||
102 | padding: 5px !important; | ||
103 | } | ||
104 | } | ||
105 | |||
106 | :deep(.table-tool-item) { | ||
107 | >* { | ||
108 | margin-bottom: 12px !important; | ||
109 | } | ||
110 | } | ||
111 | </style> |
src/components/filter/user-table-column.vue
0 → 100644
1 | <script setup lang="ts"> | ||
2 | import TableColumn from '@/components/filter/table-column.vue'; | ||
3 | import { Avatar } from '@arco-design/web-vue'; | ||
4 | |||
5 | import { get } from 'lodash'; | ||
6 | import { User } from '@/types/user'; | ||
7 | import { isString, isUndefined } from '@/utils'; | ||
8 | |||
9 | const props = withDefaults( | ||
10 | defineProps<{ | ||
11 | title?: string; | ||
12 | dataIndex?: string; | ||
13 | user?: string | Pick<User, 'id' | 'nick_name' | 'real_name' | 'identity'>; | ||
14 | showHref?: boolean; | ||
15 | showAvatar?: boolean; | ||
16 | showRealName?: boolean; | ||
17 | darkValue?: string; | ||
18 | nickIndex?: string; | ||
19 | realIndex?: string; | ||
20 | avatarIndex?: string; | ||
21 | roleIndex?: string; | ||
22 | linkStyle?: object; | ||
23 | }>(), | ||
24 | { | ||
25 | dataIndex: 'id', | ||
26 | nickIndex: 'nick_name', | ||
27 | realIndex: 'real_name', | ||
28 | avatarIndex: 'avatar', | ||
29 | roleIndex: 'identity', | ||
30 | } | ||
31 | ); | ||
32 | |||
33 | const getUser = (record: object): Pick<User, 'id' | 'nick_name' | 'real_name' | 'identity'> | undefined => { | ||
34 | if (isUndefined(props.user)) { | ||
35 | return record as User; | ||
36 | } | ||
37 | |||
38 | if (isString(props.user)) { | ||
39 | return get(record, props.user); | ||
40 | } | ||
41 | |||
42 | return get(props, 'user') as User; | ||
43 | }; | ||
44 | |||
45 | const getName = (record: object): string => { | ||
46 | const user = getUser(record); | ||
47 | |||
48 | if (!user) { | ||
49 | return ''; | ||
50 | } | ||
51 | |||
52 | if (props.showRealName) { | ||
53 | return `${get(user, props.nickIndex, '')}(${get(user, props.realIndex)})`; | ||
54 | } | ||
55 | |||
56 | return get(user, props.nickIndex, ''); | ||
57 | }; | ||
58 | </script> | ||
59 | |||
60 | <template> | ||
61 | <TableColumn v-bind="$attrs" :title="title" :data-index="dataIndex"> | ||
62 | <template #default="{ record }"> | ||
63 | <template v-if="darkValue !== undefined && getName(record) === darkValue"> | ||
64 | <span style="color: rgba(44, 44, 44, 0.5)">无</span> | ||
65 | </template> | ||
66 | <template v-else> | ||
67 | <Avatar v-if="showAvatar" style="margin-right: 8px" :size="34" shape="circle" :image-url="getUser(record)?.[avatarIndex]" /> | ||
68 | {{ getName(record) }} | ||
69 | </template> | ||
70 | </template> | ||
71 | </TableColumn> | ||
72 | </template> | ||
73 | |||
74 | <style scoped lang="less"></style> |
src/components/icon-button/index.vue
0 → 100644
1 | <template> | ||
2 | <Button v-bind="$attrs" :size="size" :type="type" :loading="loading" :status="status" @click="() => emits('click')"> | ||
3 | <template v-if="icon && iconAlign === 'left'" #icon> | ||
4 | <component :is="Icon[iconName]" /> | ||
5 | </template> | ||
6 | {{ label }} | ||
7 | <component :is="Icon[iconName]" v-if="icon && iconAlign === 'right'" style="margin-left: 6px" /> | ||
8 | </Button> | ||
9 | </template> | ||
10 | |||
11 | <script lang="ts" setup> | ||
12 | import { Button } from '@arco-design/web-vue'; | ||
13 | import * as Icon from '@arco-design/web-vue/es/icon'; | ||
14 | import { computed } from 'vue'; | ||
15 | import { camelCase, upperFirst } from 'lodash'; | ||
16 | |||
17 | const props = withDefaults( | ||
18 | defineProps<{ | ||
19 | loading?: boolean; | ||
20 | label?: string; | ||
21 | icon?: string; | ||
22 | iconAlign?: 'left' | 'right'; | ||
23 | type?: 'primary' | 'secondary' | 'outline' | 'dashed' | 'text'; | ||
24 | shape?: 'square' | 'round' | 'circle'; | ||
25 | status?: 'normal' | 'warning' | 'success' | 'danger'; | ||
26 | size?: 'mini' | 'small' | 'medium' | 'large'; | ||
27 | }>(), | ||
28 | { | ||
29 | loading: false, | ||
30 | iconAlign: 'left', | ||
31 | type: 'secondary', | ||
32 | shape: 'square', | ||
33 | status: 'normal', | ||
34 | size: 'small', | ||
35 | } | ||
36 | ); | ||
37 | |||
38 | const emits = defineEmits(['click']); | ||
39 | |||
40 | const iconName = computed(() => upperFirst(camelCase(`icon-${props.icon}`))); | ||
41 | </script> | ||
42 | |||
43 | <style scoped></style> |
src/components/image-upload/index.vue
0 → 100644
1 | <template> | ||
2 | <Upload :accept="accept" :custom-request="onUpload" :file-list="[]" :show-file-list="false"> | ||
3 | <template #upload-button> | ||
4 | <div class="arco-upload-list-item"> | ||
5 | <Image | ||
6 | v-if="modelValue" | ||
7 | :style="{ minHeight }" | ||
8 | :width="width" | ||
9 | :height="height" | ||
10 | :preview="false" | ||
11 | :src="modelValue" | ||
12 | :fit="fit" | ||
13 | show-loader | ||
14 | /> | ||
15 | <div v-else :style="style" class="arco-upload-picture-card"> | ||
16 | <div class="arco-upload-picture-card-text"> | ||
17 | <IconPlus /> | ||
18 | </div> | ||
19 | </div> | ||
20 | </div> | ||
21 | </template> | ||
22 | </Upload> | ||
23 | </template> | ||
24 | |||
25 | <script lang="ts" setup> | ||
26 | import useOss from '@/hooks/oss'; | ||
27 | import { computed } from 'vue'; | ||
28 | import { IconPlus } from '@arco-design/web-vue/es/icon'; | ||
29 | import { UploadRequest, useFormItem, Upload, Image } from '@arco-design/web-vue'; | ||
30 | |||
31 | type PropType = { | ||
32 | modelValue?: string; | ||
33 | accept?: string; | ||
34 | width?: number | string; | ||
35 | height?: number | string; | ||
36 | minHeight?: string; | ||
37 | fit?: 'none' | 'contain' | 'cover' | 'fill' | 'scale-down' | undefined; | ||
38 | }; | ||
39 | |||
40 | const props = withDefaults(defineProps<PropType>(), { | ||
41 | modelValue: '', | ||
42 | accept: 'image/*', | ||
43 | width: 80, | ||
44 | height: 'auto', | ||
45 | minHeight: '100%', | ||
46 | fit: 'cover', | ||
47 | }); | ||
48 | |||
49 | const emits = defineEmits(['update:modelValue']); | ||
50 | const { eventHandlers } = useFormItem(); | ||
51 | |||
52 | const style = computed(() => { | ||
53 | return { | ||
54 | width: `${props.width}px`, | ||
55 | height: props.height.constructor === String ? 'auto' : `${props.height}px`, | ||
56 | maxWidth: '100%', | ||
57 | maxHeight: '100%', | ||
58 | minHeight: '80px', | ||
59 | }; | ||
60 | }); | ||
61 | |||
62 | const { upload } = useOss(); | ||
63 | |||
64 | const onUpload = (option: any): UploadRequest => { | ||
65 | const { fileItem } = option; | ||
66 | |||
67 | if (fileItem.file) { | ||
68 | upload(fileItem.file, 'image').then((res) => { | ||
69 | emits('update:modelValue', res?.url || ''); | ||
70 | eventHandlers.value?.onChange?.(); | ||
71 | }); | ||
72 | } | ||
73 | |||
74 | return {}; | ||
75 | }; | ||
76 | </script> | ||
77 | |||
78 | <style lang="less" scoped> | ||
79 | :deep(.arco-avatar-text) { | ||
80 | width: 100%; | ||
81 | height: 100%; | ||
82 | } | ||
83 | |||
84 | :deep(.arco-upload-list-item) { | ||
85 | margin-top: 0 !important; | ||
86 | } | ||
87 | |||
88 | .no-avatar { | ||
89 | position: absolute; | ||
90 | display: flex; | ||
91 | align-content: center; | ||
92 | align-items: center; | ||
93 | justify-content: center; | ||
94 | width: 100%; | ||
95 | height: 100%; | ||
96 | top: 0; | ||
97 | left: 0; | ||
98 | } | ||
99 | </style> |
src/components/index.ts
0 → 100644
1 | import { App } from 'vue'; | ||
2 | import { use } from 'echarts/core'; | ||
3 | import { CanvasRenderer } from 'echarts/renderers'; | ||
4 | import { BarChart, LineChart, PieChart, RadarChart } from 'echarts/charts'; | ||
5 | import { GridComponent, TooltipComponent, LegendComponent, DataZoomComponent, GraphicComponent } from 'echarts/components'; | ||
6 | import AudioPlayer from '@/components/audio-player/index.vue'; | ||
7 | import Chart from './chart/index.vue'; | ||
8 | import Breadcrumb from './breadcrumb/index.vue'; | ||
9 | import AvatarUpload from './avatar-upload/index.vue'; | ||
10 | import InputUpload from './input-upload/index.vue'; | ||
11 | import ImageUpload from './image-upload/index.vue'; | ||
12 | import RouterButton from './router-button/index.vue'; | ||
13 | import ExportButton from './export-button/index.vue'; | ||
14 | import IconButton from './icon-button/index.vue'; | ||
15 | import TagSelect from './tag-select/index.vue'; | ||
16 | import UserSelect from './user-select/index.vue'; | ||
17 | import FilterSearch from './filter/search.vue'; | ||
18 | import FilterSearchItem from './filter/search-item.vue'; | ||
19 | import FilterTable from './filter/table.vue'; | ||
20 | import FilterTableColumn from './filter/table-column.vue'; | ||
21 | import PageView from './page-view/index.vue'; | ||
22 | |||
23 | // Manually introduce ECharts modules to reduce packing size | ||
24 | |||
25 | use([ | ||
26 | CanvasRenderer, | ||
27 | BarChart, | ||
28 | LineChart, | ||
29 | PieChart, | ||
30 | RadarChart, | ||
31 | GridComponent, | ||
32 | TooltipComponent, | ||
33 | LegendComponent, | ||
34 | DataZoomComponent, | ||
35 | GraphicComponent, | ||
36 | ]); | ||
37 | |||
38 | export default { | ||
39 | install(Vue: App) { | ||
40 | Vue.component('Chart', Chart); | ||
41 | Vue.component('Breadcrumb', Breadcrumb); | ||
42 | Vue.component('AvatarUpload', AvatarUpload); | ||
43 | Vue.component('InputUpload', InputUpload); | ||
44 | Vue.component('ImageUpload', ImageUpload); | ||
45 | Vue.component('RouterButton', RouterButton); | ||
46 | Vue.component('ExportButton', ExportButton); | ||
47 | Vue.component('IconButton', IconButton); | ||
48 | Vue.component('AudioPlayer', AudioPlayer); | ||
49 | Vue.component('TagSelect', TagSelect); | ||
50 | Vue.component('UserSelect', UserSelect); | ||
51 | Vue.component('FilterSearch', FilterSearch); | ||
52 | Vue.component('FilterSearchItem', FilterSearchItem); | ||
53 | Vue.component('FilterTable', FilterTable); | ||
54 | Vue.component('FilterTableColumn', FilterTableColumn); | ||
55 | Vue.component('PageView', PageView); | ||
56 | }, | ||
57 | }; |
src/components/input-upload/index.vue
0 → 100644
1 | <template> | ||
2 | <Upload | ||
3 | v-bind="$attrs" | ||
4 | :on-before-upload="onBeforeUpload" | ||
5 | :custom-request="onUpload" | ||
6 | :file-list="[]" | ||
7 | :show-file-list="false" | ||
8 | style="width: 100%" | ||
9 | > | ||
10 | <template #upload-button> | ||
11 | <Input :model-value="modelValue" :readonly="true" :placeholder="placeholder"> | ||
12 | <template #suffix> | ||
13 | <Progress v-if="loading" :percent="percent" size="mini" /> | ||
14 | <IconUpload v-else /> | ||
15 | </template> | ||
16 | </Input> | ||
17 | </template> | ||
18 | </Upload> | ||
19 | </template> | ||
20 | |||
21 | <script lang="ts" setup> | ||
22 | import { ref } from 'vue'; | ||
23 | import { IconUpload } from '@arco-design/web-vue/es/icon'; | ||
24 | import useLoading from '@/hooks/loading'; | ||
25 | import useOss from '@/hooks/oss'; | ||
26 | import { Input, Message, Progress, Upload, UploadRequest, useFormItem } from '@arco-design/web-vue'; | ||
27 | import { startsWith } from 'lodash'; | ||
28 | |||
29 | type FileType = { name: string; url: string; size: number; type: string; width?: number; height?: number; duration?: number }; | ||
30 | |||
31 | const props = defineProps({ | ||
32 | modelValue: { type: String, default: '' }, | ||
33 | prefix: { type: String, default: 'file' }, | ||
34 | limit: { type: Number, default: 0 }, | ||
35 | placeholder: { type: String, default: '请选择' }, | ||
36 | }); | ||
37 | |||
38 | const emits = defineEmits<{ | ||
39 | (e: 'update:modelValue', value: string): void; | ||
40 | (e: 'success', value: FileType): void; | ||
41 | (e: 'choose-file', value: File): void; | ||
42 | }>(); | ||
43 | const { eventHandlers } = useFormItem(); | ||
44 | |||
45 | const { loading, setLoading } = useLoading(false); | ||
46 | const { upload } = useOss(); | ||
47 | const percent = ref<number>(0); | ||
48 | |||
49 | const onBeforeUpload = (file: File & { width?: number; height?: number; duration?: number }) => { | ||
50 | if (props.limit !== 0 && file.size > props.limit * 1024 * 1024) { | ||
51 | Message.warning(`${file.name} 文件超过${props.limit}MB,无法上传`); | ||
52 | return Promise.resolve(false); | ||
53 | } | ||
54 | |||
55 | if (startsWith(file.type, 'image/')) { | ||
56 | const imgObj = new Image(); | ||
57 | imgObj.src = URL.createObjectURL(file); | ||
58 | imgObj.onload = () => { | ||
59 | file.width = imgObj.width; | ||
60 | file.height = imgObj.height; | ||
61 | }; | ||
62 | } | ||
63 | |||
64 | if (startsWith(file.type, 'audio/') || startsWith(file.type, 'video/')) { | ||
65 | const audioElement = new Audio(URL.createObjectURL(file)); | ||
66 | audioElement.addEventListener('loadedmetadata', () => { | ||
67 | file.duration = audioElement.duration * 1000; | ||
68 | }); | ||
69 | } | ||
70 | |||
71 | return Promise.resolve(file); | ||
72 | }; | ||
73 | |||
74 | const onProgress = (p: number) => { | ||
75 | percent.value = p; | ||
76 | }; | ||
77 | |||
78 | const onUpload = (option: any): UploadRequest => { | ||
79 | const { fileItem } = option; | ||
80 | |||
81 | if (fileItem.file) { | ||
82 | setLoading(true); | ||
83 | // eslint-disable-next-line vue/custom-event-name-casing | ||
84 | emits('choose-file', fileItem.file as File); | ||
85 | upload(fileItem.file, props.prefix, onProgress) | ||
86 | .then((res) => { | ||
87 | emits('update:modelValue', res?.url || ''); | ||
88 | eventHandlers.value?.onChange?.(); | ||
89 | fileItem.percent = 100; | ||
90 | fileItem.url = res?.url || ''; | ||
91 | fileItem.status = 'done'; | ||
92 | emits('success', { | ||
93 | name: fileItem.name, | ||
94 | url: fileItem.url, | ||
95 | size: fileItem.file.size, | ||
96 | type: fileItem.file.type, | ||
97 | width: fileItem.file.width || 0, | ||
98 | height: fileItem.file.height || 0, | ||
99 | duration: fileItem.file.duration || 0, | ||
100 | }); | ||
101 | }) | ||
102 | .finally(() => { | ||
103 | setLoading(false); | ||
104 | }); | ||
105 | } | ||
106 | |||
107 | return {}; | ||
108 | }; | ||
109 | </script> | ||
110 | |||
111 | <style lang="less" scoped> | ||
112 | ::v-deep(.arco-input-append) { | ||
113 | padding: 0; | ||
114 | } | ||
115 | </style> |
src/components/page-view/index.vue
0 → 100644
1 | <script setup lang="ts"> | ||
2 | withDefaults(defineProps<{ hasCard?: boolean; hasBread?: boolean; loading?: boolean }>(), { | ||
3 | loading: false, | ||
4 | }); | ||
5 | </script> | ||
6 | |||
7 | <template> | ||
8 | <div class="container"> | ||
9 | <breadcrumb v-if="hasBread && $route.meta?.breadcrumb" :items="$route.meta?.breadcrumb" /> | ||
10 | <a-spin style="width: 100%" :loading="loading as boolean"> | ||
11 | <a-card v-if="hasCard" :bordered="false"> | ||
12 | <slot /> | ||
13 | </a-card> | ||
14 | <slot v-else /> | ||
15 | </a-spin> | ||
16 | </div> | ||
17 | </template> | ||
18 | |||
19 | <style scoped lang="less"> | ||
20 | .container { | ||
21 | padding: 0 30px 20px 20px; | ||
22 | } | ||
23 | </style> |
src/components/router-button/index.vue
0 → 100644
1 | <template> | ||
2 | <a-button v-if="type === 'Button'" type="text" size="small" @click="onClick"> | ||
3 | <slot>详情</slot> | ||
4 | </a-button> | ||
5 | <a-link v-else class="link" :hoverable="false" @click="onClick"> | ||
6 | <a-typography-text type="primary" :style="textStyle" :ellipsis="{ rows: 1, showTooltip: showTooltip }"> | ||
7 | <slot>详情</slot> | ||
8 | </a-typography-text> | ||
9 | </a-link> | ||
10 | </template> | ||
11 | |||
12 | <script lang="ts" setup> | ||
13 | import { RouteParamsRaw, useRouter } from 'vue-router'; | ||
14 | import { AnyObject } from '@/types/global'; | ||
15 | |||
16 | const props = withDefaults( | ||
17 | defineProps<{ | ||
18 | name: string; | ||
19 | params?: AnyObject; | ||
20 | type: 'Button' | 'link'; | ||
21 | showTooltip: boolean; | ||
22 | }>(), | ||
23 | { | ||
24 | name: '', | ||
25 | type: 'Button', | ||
26 | params: undefined, | ||
27 | showTooltip: true, | ||
28 | } | ||
29 | ); | ||
30 | |||
31 | const textStyle = { marginBottom: 0 }; | ||
32 | |||
33 | const router = useRouter(); | ||
34 | const onClick = () => { | ||
35 | router.push({ | ||
36 | name: props.name, | ||
37 | params: (props.params as RouteParamsRaw) || {}, | ||
38 | }); | ||
39 | }; | ||
40 | </script> | ||
41 | |||
42 | <style lang="less" scoped> | ||
43 | .link { | ||
44 | width: 100%; | ||
45 | padding: 1px 0; | ||
46 | display: block !important; | ||
47 | } | ||
48 | </style> |
src/components/tag-select/index.vue
0 → 100644
1 | <template> | ||
2 | <Select | ||
3 | v-bind="$attrs" | ||
4 | v-model="value" | ||
5 | :options="options" | ||
6 | :fallback-option="false" | ||
7 | :placeholder="placeholder" | ||
8 | :virtual-list-props="{ height: 200 }" | ||
9 | :field-names="{ value: 'id', label: 'name' }" | ||
10 | @exceed-limit="onTagExceedLimitError" | ||
11 | /> | ||
12 | </template> | ||
13 | |||
14 | <script lang="ts" setup> | ||
15 | import { Message, Select } from '@arco-design/web-vue'; | ||
16 | import { computed, onMounted } from 'vue'; | ||
17 | import { Tag } from '@/types/tag'; | ||
18 | import { storeToRefs } from 'pinia'; | ||
19 | import { useSelectionStore } from '@/store'; | ||
20 | import { isArray } from '@arco-design/web-vue/es/_utils/is'; | ||
21 | |||
22 | interface propType { | ||
23 | modelValue?: number | string | number[]; | ||
24 | placeholder?: string; | ||
25 | } | ||
26 | |||
27 | const props = withDefaults(defineProps<propType>(), { placeholder: '请选择' }); | ||
28 | const emits = defineEmits<{ (e: 'update:modelValue', value: unknown): void }>(); | ||
29 | |||
30 | const onTagExceedLimitError = () => Message.warning({ content: '关联标签最多选中3个', duration: 1500 }); | ||
31 | |||
32 | const value = computed({ | ||
33 | get: () => props.modelValue, | ||
34 | set: (val) => emits('update:modelValue', val), | ||
35 | }); | ||
36 | |||
37 | const { getTagOptions } = storeToRefs(useSelectionStore()); | ||
38 | const options = computed(() => getTagOptions.value.filter((item: Tag) => item.type === 1)); | ||
39 | |||
40 | const tagIds = computed(() => options.value?.map((item: Tag) => item.id)); | ||
41 | |||
42 | onMounted(() => { | ||
43 | if (isArray(props.modelValue)) { | ||
44 | value.value = props.modelValue?.filter((item: number) => tagIds.value?.indexOf(item) !== -1) || []; | ||
45 | } | ||
46 | }); | ||
47 | </script> | ||
48 | |||
49 | <style scoped></style> |
src/components/user-select/index.vue
0 → 100644
1 | <script setup lang="ts"> | ||
2 | import { ref } from 'vue'; | ||
3 | |||
4 | import { Select } from '@arco-design/web-vue'; | ||
5 | import { useSelectionStore } from '@/store'; | ||
6 | import { storeToRefs } from 'pinia'; | ||
7 | |||
8 | const { getUserOptions } = storeToRefs(useSelectionStore()); | ||
9 | |||
10 | const loading = ref<boolean>(false); | ||
11 | const fieldName = { value: 'id', label: 'nick_name' }; | ||
12 | </script> | ||
13 | |||
14 | <template> | ||
15 | <Select | ||
16 | v-bind="$attrs" | ||
17 | placeholder="请选择" | ||
18 | :loading="loading" | ||
19 | :multiple="true" | ||
20 | :options="getUserOptions" | ||
21 | :field-names="fieldName" | ||
22 | :allow-search="true" | ||
23 | :virtual-list-props="{ height: 200 }" | ||
24 | /> | ||
25 | </template> | ||
26 | |||
27 | <style scoped lang="less"></style> |
src/directive/index.ts
0 → 100644
src/directive/permission/index.ts
0 → 100644
1 | import { DirectiveBinding } from 'vue'; | ||
2 | import { useAuthorizedStore } from '@/store'; | ||
3 | |||
4 | function checkPermission(el: HTMLElement, binding: DirectiveBinding) { | ||
5 | const { value } = binding; | ||
6 | const userStore = useAuthorizedStore(); | ||
7 | const { permissions } = userStore; | ||
8 | if (Array.isArray(value)) { | ||
9 | if (value.length > 0) { | ||
10 | const hasPermission = value.filter((item: string) => permissions.includes(item)); | ||
11 | if (hasPermission.length === 0 && el.parentNode) { | ||
12 | if (el.parentNode.childElementCount === 1) { | ||
13 | // @ts-ignore | ||
14 | el.parentNode.remove(); | ||
15 | } else { | ||
16 | el.parentNode.removeChild(el); | ||
17 | } | ||
18 | } | ||
19 | } | ||
20 | } else { | ||
21 | throw new Error(`need roles! Like v-permission="['admin','user']"`); | ||
22 | } | ||
23 | } | ||
24 | |||
25 | export default { | ||
26 | mounted(el: HTMLElement, binding: DirectiveBinding) { | ||
27 | checkPermission(el, binding); | ||
28 | }, | ||
29 | updated(el: HTMLElement, binding: DirectiveBinding) { | ||
30 | checkPermission(el, binding); | ||
31 | }, | ||
32 | }; |
src/env.d.ts
0 → 100644
1 | /// <reference types="vite/client" /> | ||
2 | |||
3 | declare module '*.vue' { | ||
4 | import { DefineComponent } from 'vue'; | ||
5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types | ||
6 | const component: DefineComponent<{}, {}, any>; | ||
7 | export default component; | ||
8 | } | ||
9 | interface ImportMetaEnv { | ||
10 | readonly VITE_API_BASE_URL: string; | ||
11 | } |
src/hooks/chart-option.ts
0 → 100644
1 | import { computed } from 'vue'; | ||
2 | import { EChartsOption } from 'echarts'; | ||
3 | import { useAppStore } from '@/store'; | ||
4 | |||
5 | // for code hints | ||
6 | // import { SeriesOption } from 'echarts'; | ||
7 | // Because there are so many configuration items, this provides a relatively convenient code hint. | ||
8 | // 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. | ||
9 | interface optionsFn { | ||
10 | (isDark: boolean): EChartsOption; | ||
11 | } | ||
12 | |||
13 | export default function useChartOption(sourceOption: optionsFn) { | ||
14 | const appStore = useAppStore(); | ||
15 | const isDark = computed(() => { | ||
16 | return appStore.theme === 'dark'; | ||
17 | }); | ||
18 | // echarts support https://echarts.apache.org/zh/theme-builder.html | ||
19 | // It's not used here | ||
20 | // TODO echarts themes | ||
21 | const chartOption = computed<EChartsOption>(() => { | ||
22 | return sourceOption(isDark.value); | ||
23 | }); | ||
24 | return { | ||
25 | chartOption, | ||
26 | }; | ||
27 | } |
src/hooks/loading.ts
0 → 100644
1 | import { ref } from 'vue'; | ||
2 | |||
3 | export default function useLoading(initValue = false) { | ||
4 | const loading = ref(initValue); | ||
5 | const setLoading = (value: boolean) => { | ||
6 | loading.value = value; | ||
7 | }; | ||
8 | const toggle = () => { | ||
9 | loading.value = !loading.value; | ||
10 | }; | ||
11 | return { | ||
12 | loading, | ||
13 | setLoading, | ||
14 | toggle, | ||
15 | }; | ||
16 | } |
src/hooks/oss.ts
0 → 100644
1 | import OSS from "ali-oss"; | ||
2 | import { Message } from "@arco-design/web-vue"; | ||
3 | |||
4 | let ossClient: OSS; | ||
5 | |||
6 | const createOssClient = () => { | ||
7 | if (!ossClient) { | ||
8 | ossClient = new OSS({ | ||
9 | accessKeyId: import.meta.env.VITE_OSS_ACCESS_KEY, | ||
10 | accessKeySecret: import.meta.env.VITE_OSS_ACCESS_SECRET, | ||
11 | bucket: import.meta.env.VITE_OSS_BUCKET, | ||
12 | region: import.meta.env.VITE_OSS_REGION, | ||
13 | endpoint: import.meta.env.VITE_OSS_ENDPOINT, | ||
14 | cname: true, | ||
15 | timeout: 90000 | ||
16 | }); | ||
17 | } | ||
18 | |||
19 | return ossClient; | ||
20 | }; | ||
21 | |||
22 | export default function useOss() { | ||
23 | const getFileName = () => { | ||
24 | function rx() { | ||
25 | return Math.random().toString(36).substring(2); | ||
26 | } | ||
27 | |||
28 | return `${rx()}${new Date().getTime()}${rx()}`; | ||
29 | }; | ||
30 | |||
31 | const getFileDir = () => { | ||
32 | return new Date().toISOString().slice(0, 10).replace(/-/g, ""); | ||
33 | }; | ||
34 | |||
35 | const getFileType = (file: File) => { | ||
36 | return file.name?.split(".")?.pop()?.toLowerCase(); | ||
37 | }; | ||
38 | |||
39 | const getHost = () => { | ||
40 | if (import.meta.env.VITE_OSS_HOST && import.meta.env.VITE_OSS_HOST.length !== 0) { | ||
41 | return import.meta.env.VITE_OSS_HOST; | ||
42 | } | ||
43 | |||
44 | return import.meta.env.VITE_OSS_ENDPOINT; | ||
45 | }; | ||
46 | |||
47 | return { | ||
48 | ossClient, | ||
49 | async upload( | ||
50 | file: File, | ||
51 | prefix: string, | ||
52 | onProgress?: (percent: number) => void | ||
53 | ): Promise<{ url: string; name: string; response: unknown }> { | ||
54 | return createOssClient() | ||
55 | .multipartUpload(`${prefix}/${getFileDir()}/${getFileName()}.${getFileType(file)}`, file, { progress: onProgress }) | ||
56 | .then((res) => { | ||
57 | return Promise.resolve({ | ||
58 | response: res.res, | ||
59 | url: `${getHost()}/${res.name}`, | ||
60 | name: `${getFileName()}.${getFileType(file)}` | ||
61 | }); | ||
62 | }) | ||
63 | .catch(() => { | ||
64 | Message.error({ content: "上传失败" }); | ||
65 | // eslint-disable-next-line prefer-promise-reject-errors | ||
66 | return Promise.reject(false); | ||
67 | }); | ||
68 | } | ||
69 | }; | ||
70 | } |
src/hooks/pagination.ts
0 → 100644
1 | import { ref } from 'vue'; | ||
2 | import { Pagination } from '@/types/global'; | ||
3 | |||
4 | interface Config { | ||
5 | pageSize?: number; | ||
6 | showTotal?: boolean; | ||
7 | showPageSize?: boolean; | ||
8 | size?: 'mini' | 'small' | 'medium' | 'large'; | ||
9 | simple?: boolean; | ||
10 | hideOnSinglePage?: boolean; | ||
11 | } | ||
12 | |||
13 | export default function usePagination(config: Config = {}) { | ||
14 | const defaultPagination = { | ||
15 | total: 0, | ||
16 | current: 1, | ||
17 | pageSize: config.pageSize ?? 20, | ||
18 | pageSizeOptions: [config.pageSize ?? 20, (config.pageSize ?? 20) * 2, (config.pageSize ?? 20) * 3, (config.pageSize ?? 20) * 5], | ||
19 | showTotal: config.showTotal ?? true, | ||
20 | showPageSize: config.showPageSize ?? true, | ||
21 | size: config.size ?? 'medium', | ||
22 | simple: config.simple ?? false, | ||
23 | hideOnSinglePage: config.hideOnSinglePage ?? false, | ||
24 | }; | ||
25 | |||
26 | const pagination = ref<Pagination>(defaultPagination); | ||
27 | |||
28 | // eslint-disable-next-line no-return-assign | ||
29 | const setPage = (page: number) => (pagination.value.current = page); | ||
30 | // eslint-disable-next-line no-return-assign | ||
31 | const setPageSize = (size: number) => (pagination.value.pageSize = size); | ||
32 | // eslint-disable-next-line no-return-assign | ||
33 | const setTotal = (total: number) => (pagination.value.total = total); | ||
34 | // eslint-disable-next-line no-return-assign | ||
35 | const resetPagination = () => (pagination.value = defaultPagination); | ||
36 | |||
37 | const incrementTotal = (increment = 1) => setTotal(pagination.value.total + increment); | ||
38 | |||
39 | const decrementTotal = (increment = 1) => setTotal(pagination.value.total - increment); | ||
40 | |||
41 | return { | ||
42 | pagination, | ||
43 | setPage, | ||
44 | setPageSize, | ||
45 | setTotal, | ||
46 | resetPagination, | ||
47 | incrementTotal, | ||
48 | decrementTotal, | ||
49 | }; | ||
50 | } |
src/hooks/permission.ts
0 → 100644
1 | import { RouteLocationNormalized, RouteRecordRaw } from 'vue-router'; | ||
2 | import { useAuthorizedStore } from '@/store'; | ||
3 | |||
4 | export default function usePermission() { | ||
5 | const userStore = useAuthorizedStore(); | ||
6 | |||
7 | return { | ||
8 | accessRouter(route: RouteLocationNormalized | RouteRecordRaw) { | ||
9 | return ( | ||
10 | !route.meta?.requiresAuth || | ||
11 | !route.meta?.roles || | ||
12 | route.meta?.roles?.includes('*') || | ||
13 | userStore.permissions?.filter((item) => route.meta?.roles?.includes(item))?.length !== 0 | ||
14 | ); | ||
15 | }, | ||
16 | checkPermission(binding: string[] | string): boolean { | ||
17 | const userStore = useAuthorizedStore(); | ||
18 | const { permissions } = userStore; | ||
19 | |||
20 | if (Array.isArray(binding) && binding.length > 0) { | ||
21 | const hasPermission = binding.filter((item: string) => permissions.includes(item)); | ||
22 | return hasPermission.length !== 0; | ||
23 | } | ||
24 | return permissions.includes(binding as string); | ||
25 | }, | ||
26 | // You can add any rules you want | ||
27 | }; | ||
28 | } |
src/hooks/request.ts
0 → 100644
1 | import { ref, UnwrapRef } from 'vue'; | ||
2 | import { AxiosResponse } from 'axios'; | ||
3 | import { HttpResponse } from '@/api/interceptor'; | ||
4 | import useLoading from './loading'; | ||
5 | |||
6 | // use to fetch list | ||
7 | // Don't use async function. It doesn't work in async function. | ||
8 | // Use the bind function to add parameters | ||
9 | // example: useRequest(api.bind(null, {})) | ||
10 | |||
11 | export default function useRequest<T>( | ||
12 | api: () => Promise<AxiosResponse<HttpResponse>>, | ||
13 | defaultValue = [] as unknown as T, | ||
14 | isLoading = true | ||
15 | ) { | ||
16 | const { loading, setLoading } = useLoading(isLoading); | ||
17 | const response = ref<T>(defaultValue); | ||
18 | api() | ||
19 | .then((res) => { | ||
20 | response.value = res.data as unknown as UnwrapRef<T>; | ||
21 | }) | ||
22 | .finally(() => { | ||
23 | setLoading(false); | ||
24 | }); | ||
25 | return { loading, response }; | ||
26 | } |
src/hooks/responsive.ts
0 → 100644
1 | import { onMounted, onBeforeMount, onBeforeUnmount } from 'vue'; | ||
2 | import { useDebounceFn } from '@vueuse/core'; | ||
3 | import { useAppStore } from '@/store'; | ||
4 | import { addEventListen, removeEventListen } from '@/utils/event'; | ||
5 | |||
6 | const WIDTH = 992; // https://arco.design/vue/component/grid#responsivevalue | ||
7 | |||
8 | function queryDevice() { | ||
9 | const rect = document.body.getBoundingClientRect(); | ||
10 | return rect.width - 1 < WIDTH; | ||
11 | } | ||
12 | |||
13 | export default function useResponsive(immediate?: boolean) { | ||
14 | const appStore = useAppStore(); | ||
15 | function resizeHandler() { | ||
16 | if (!document.hidden) { | ||
17 | const isMobile = queryDevice(); | ||
18 | appStore.toggleDevice(isMobile ? 'mobile' : 'desktop'); | ||
19 | appStore.toggleMenu(isMobile); | ||
20 | } | ||
21 | } | ||
22 | const debounceFn = useDebounceFn(resizeHandler, 100); | ||
23 | onMounted(() => { | ||
24 | if (immediate) debounceFn(); | ||
25 | }); | ||
26 | onBeforeMount(() => { | ||
27 | addEventListen(window, 'resize', debounceFn); | ||
28 | }); | ||
29 | onBeforeUnmount(() => { | ||
30 | removeEventListen(window, 'resize', debounceFn); | ||
31 | }); | ||
32 | } |
src/hooks/themes.ts
0 → 100644
src/hooks/visible.ts
0 → 100644
1 | import { ref } from 'vue'; | ||
2 | |||
3 | export default function useVisible(initValue = false) { | ||
4 | const visible = ref(initValue); | ||
5 | const setVisible = (value: boolean) => { | ||
6 | visible.value = value; | ||
7 | }; | ||
8 | const toggle = () => { | ||
9 | visible.value = !visible.value; | ||
10 | }; | ||
11 | return { | ||
12 | visible, | ||
13 | setVisible, | ||
14 | toggle, | ||
15 | }; | ||
16 | } |
src/layout/components/menu.vue
0 → 100644
1 | <script lang="tsx"> | ||
2 | import { compile, computed, defineComponent, h, ref, watch } from "vue"; | ||
3 | |||
4 | import { RouteRecordRaw, useRoute, useRouter } from "vue-router"; | ||
5 | import { useAppStore } from "@/store"; | ||
6 | import usePermission from "@/hooks/permission"; | ||
7 | import { last, orderBy } from "lodash"; | ||
8 | import { storeToRefs } from "pinia"; | ||
9 | |||
10 | export default defineComponent({ | ||
11 | emit: ["collapse"], | ||
12 | setup() { | ||
13 | const appStore = useAppStore(); | ||
14 | const permission = usePermission(); | ||
15 | const router = useRouter(); | ||
16 | const route = useRoute(); | ||
17 | |||
18 | const { appMenu } = storeToRefs(appStore); | ||
19 | |||
20 | const collapsed = ref(false); | ||
21 | const selectedKey = ref<string[]>([]); | ||
22 | const openKey = ref<string[]>([]); | ||
23 | |||
24 | const goto = (item: RouteRecordRaw) => router.push({ name: item.name }); | ||
25 | |||
26 | const syncServicePermission = (item: RouteRecordRaw) => { | ||
27 | if (item.meta) { | ||
28 | const serverConfig = appMenu.value.find((menu) => item.name === menu.name); | ||
29 | item.meta.title = serverConfig?.label || item.meta?.title || ""; | ||
30 | item.meta.order = serverConfig?.weight || item.meta?.order || 0; | ||
31 | item.meta.icon = serverConfig?.icon || item.meta?.icon || ""; | ||
32 | } | ||
33 | item.children?.map((child) => syncServicePermission(child)); | ||
34 | return item; | ||
35 | }; | ||
36 | |||
37 | const appRoute = computed((): RouteRecordRaw[] => { | ||
38 | return router | ||
39 | .getRoutes() | ||
40 | .find((el) => el.name === "root") | ||
41 | ?.children.filter((element) => element.meta?.hideInMenu !== true) | ||
42 | .map((item: RouteRecordRaw) => syncServicePermission(item)) as RouteRecordRaw[]; | ||
43 | }); | ||
44 | |||
45 | const menuTree = computed((): RouteRecordRaw[] => { | ||
46 | function travel(_routes: RouteRecordRaw[], layer: number) { | ||
47 | if (!_routes) return null; | ||
48 | const collector: any = orderBy(_routes, "meta.order", "desc").map((element) => { | ||
49 | // no access | ||
50 | if (!permission.accessRouter(element)) { | ||
51 | return null; | ||
52 | } | ||
53 | |||
54 | // leaf node | ||
55 | if (!element.children) { | ||
56 | return element; | ||
57 | } | ||
58 | |||
59 | // route filter hideInMenu true | ||
60 | element.children = element.children.filter((x) => x.meta?.hideInMenu !== true); | ||
61 | |||
62 | // Associated child node | ||
63 | const subItem = travel(element.children, layer); | ||
64 | if (subItem.length) { | ||
65 | element.children = subItem; | ||
66 | return element; | ||
67 | } | ||
68 | // the else logic | ||
69 | if (layer > 1) { | ||
70 | element.children = subItem; | ||
71 | return element; | ||
72 | } | ||
73 | |||
74 | if (element.meta?.hideInMenu === false) { | ||
75 | return element; | ||
76 | } | ||
77 | |||
78 | return null; | ||
79 | }); | ||
80 | return collector.filter(Boolean); | ||
81 | } | ||
82 | |||
83 | return travel(appRoute.value, 0); | ||
84 | }); | ||
85 | |||
86 | watch( | ||
87 | route, | ||
88 | (newVal) => { | ||
89 | if (newVal.meta.requiresAuth) { | ||
90 | const key = newVal.meta.hideInMenu ? last(newVal.matched)?.meta?.menuSelectKey : last(newVal.matched)?.name; | ||
91 | selectedKey.value = [key as string]; | ||
92 | openKey.value = [...(newVal.meta.breadcrumb || [])]; | ||
93 | } | ||
94 | }, | ||
95 | { immediate: true } | ||
96 | ); | ||
97 | watch( | ||
98 | () => appStore.menuCollapse, | ||
99 | (newVal) => { | ||
100 | collapsed.value = newVal; | ||
101 | }, | ||
102 | { immediate: true } | ||
103 | ); | ||
104 | const setCollapse = (val: boolean) => { | ||
105 | appStore.updateSettings({ menuCollapse: val }); | ||
106 | }; | ||
107 | |||
108 | const renderSubMenu = () => { | ||
109 | function travel(_route: RouteRecordRaw[], nodes = []) { | ||
110 | if (_route) { | ||
111 | _route.forEach((element) => { | ||
112 | // This is demo, modify nodes as needed | ||
113 | const icon = element?.meta?.icon ? `<${element?.meta?.icon}/>` : ``; | ||
114 | let r; | ||
115 | |||
116 | if (element && element.children && element.children.length !== 0) { | ||
117 | r = ( | ||
118 | <a-sub-menu key={element?.name} title={element.meta?.title} v-slots={{ icon: () => h(compile(icon)) }}> | ||
119 | {element?.children?.map((elem) => { | ||
120 | return ( | ||
121 | <a-menu-item key={elem.name} onClick={() => goto(elem)}> | ||
122 | {elem.meta?.title} | ||
123 | {travel(elem.children ?? [])} | ||
124 | </a-menu-item> | ||
125 | ); | ||
126 | })} | ||
127 | </a-sub-menu> | ||
128 | ); | ||
129 | } else { | ||
130 | r = ( | ||
131 | <a-menu-item key={element?.name} v-slots={{ icon: () => h(compile(icon)) }} | ||
132 | onClick={() => goto(element)}> | ||
133 | {element.meta?.title} | ||
134 | </a-menu-item> | ||
135 | ); | ||
136 | } | ||
137 | nodes.push(r as never); | ||
138 | }); | ||
139 | } | ||
140 | return nodes; | ||
141 | } | ||
142 | |||
143 | return travel(menuTree.value); | ||
144 | }; | ||
145 | |||
146 | return () => ( | ||
147 | <a-menu | ||
148 | v-model:collapsed={collapsed.value} | ||
149 | show-collapse-button | ||
150 | auto-open={false} | ||
151 | v-model:selected-keys={selectedKey.value} | ||
152 | v-model:open-keys={openKey.value} | ||
153 | auto-open-selected={true} | ||
154 | auto-scroll-into-view={true} | ||
155 | level-indent={34} | ||
156 | style={{ height: "100%" }} | ||
157 | onCollapse={setCollapse} | ||
158 | > | ||
159 | {renderSubMenu()} | ||
160 | </a-menu> | ||
161 | ); | ||
162 | } | ||
163 | }); | ||
164 | </script> | ||
165 | |||
166 | <style lang="less" scoped> | ||
167 | :deep(.arco-menu-inner) { | ||
168 | .arco-menu-inline-header { | ||
169 | display: flex; | ||
170 | align-items: center; | ||
171 | } | ||
172 | |||
173 | .arco-icon { | ||
174 | &:not(.arco-icon-down) { | ||
175 | font-size: 18px; | ||
176 | } | ||
177 | } | ||
178 | } | ||
179 | </style> |
src/layout/components/navbar.vue
0 → 100644
1 | <template> | ||
2 | <div class="navbar"> | ||
3 | <div class="left-side"> | ||
4 | <a-space> | ||
5 | <img alt="logo" src="//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/dfdba5317c0c20ce20e64fac803d52bc.svg~tplv-49unhts6dw-image.image" /> | ||
6 | <a-typography-title :heading="5" :style="{ margin: 0, fontSize: '18px' }"> 海星试唱</a-typography-title> | ||
7 | </a-space> | ||
8 | </div> | ||
9 | <ul class="right-side"> | ||
10 | <!-- <li>--> | ||
11 | <!-- <toggle-theme-button />--> | ||
12 | <!-- </li>--> | ||
13 | <!-- <li>--> | ||
14 | <!-- <a-tooltip :content="$t('settings.navbar.alerts')">--> | ||
15 | <!-- <div class="message-box-trigger">--> | ||
16 | <!-- <a-badge :count="9" dot>--> | ||
17 | <!-- <a-button--> | ||
18 | <!-- :shape="'circle'"--> | ||
19 | <!-- class="nav-btn"--> | ||
20 | <!-- type="outline"--> | ||
21 | <!-- @click="setPopoverVisible"--> | ||
22 | <!-- >--> | ||
23 | <!-- <icon-notification />--> | ||
24 | <!-- </a-button>--> | ||
25 | <!-- </a-badge>--> | ||
26 | <!-- </div>--> | ||
27 | <!-- </a-tooltip>--> | ||
28 | <!-- <a-popover--> | ||
29 | <!-- :arrow-style="{ display: 'none' }"--> | ||
30 | <!-- :content-style="{ padding: 0, minWidth: '400px' }"--> | ||
31 | <!-- content-class="message-popover"--> | ||
32 | <!-- trigger="click"--> | ||
33 | <!-- >--> | ||
34 | <!-- <div ref="refBtn" class="ref-btn"></div>--> | ||
35 | <!-- <template #content>--> | ||
36 | <!-- <message-box />--> | ||
37 | <!-- </template>--> | ||
38 | <!-- </a-popover>--> | ||
39 | <!-- </li>--> | ||
40 | <li> | ||
41 | <a-dropdown trigger="hover"> | ||
42 | <a-space align="center"> | ||
43 | <a-avatar v-if="avatar" :size="32" :image-url="avatar" /> | ||
44 | <div :style="{ color: theme === 'dark' ? 'white' : 'black', lineHeight: '32px' }"> | ||
45 | <span>{{ name }}</span> | ||
46 | <icon-down /> | ||
47 | </div> | ||
48 | </a-space> | ||
49 | <template #content> | ||
50 | <!-- <a-doption>--> | ||
51 | <!-- <a-space @click="switchRoles">--> | ||
52 | <!-- <icon-tag />--> | ||
53 | <!-- <span>--> | ||
54 | <!-- {{ $t('messageBox.switchRoles') }}--> | ||
55 | <!-- </span>--> | ||
56 | <!-- </a-space>--> | ||
57 | <!-- </a-doption>--> | ||
58 | <!-- <a-doption>--> | ||
59 | <!-- <a-space @click="handleChangePwd">--> | ||
60 | <!-- <icon-lock />--> | ||
61 | <!-- <span>修改密码</span>--> | ||
62 | <!-- </a-space>--> | ||
63 | <!-- </a-doption>--> | ||
64 | <a-doption> | ||
65 | <a-space @click="handleLogout"> | ||
66 | <icon-export /> | ||
67 | <span> 退出登录 </span> | ||
68 | </a-space> | ||
69 | </a-doption> | ||
70 | </template> | ||
71 | </a-dropdown> | ||
72 | </li> | ||
73 | </ul> | ||
74 | </div> | ||
75 | </template> | ||
76 | |||
77 | <script lang="ts" setup> | ||
78 | import { computed } from 'vue'; | ||
79 | import { useAppStore, useAuthorizedStore } from '@/store'; | ||
80 | // import useUser from '@/hooks/user'; | ||
81 | // import ToggleThemeButton from '@/layout/components/toggle-theme-button.vue'; | ||
82 | import { IconDown, IconExport } from '@arco-design/web-vue/es/icon'; | ||
83 | |||
84 | const appStore = useAppStore(); | ||
85 | const authorized = useAuthorizedStore(); | ||
86 | |||
87 | const avatar = computed(() => { | ||
88 | return authorized.avatar; | ||
89 | }); | ||
90 | |||
91 | const name = computed(() => { | ||
92 | return authorized.nick_name; | ||
93 | }); | ||
94 | |||
95 | const { theme } = appStore; | ||
96 | |||
97 | // const theme = computed(() => { | ||
98 | // return appStore.theme; | ||
99 | // }); | ||
100 | |||
101 | // const setVisible = () => { | ||
102 | // appStore.updateSettings({ globalSettings: true }); | ||
103 | // }; | ||
104 | // const refBtn = ref(); | ||
105 | // const triggerBtn = ref(); | ||
106 | // const setPopoverVisible = () => { | ||
107 | // const event = new MouseEvent('click', { | ||
108 | // view: window, | ||
109 | // bubbles: true, | ||
110 | // cancelable: true, | ||
111 | // }); | ||
112 | // refBtn.value.dispatchEvent(event); | ||
113 | // }; | ||
114 | const handleLogout = () => authorized.logout(); | ||
115 | |||
116 | // const handleChangePwd = () => useUser().resetPwd(); | ||
117 | // const setDropDownVisible = () => { | ||
118 | // const event = new MouseEvent('click', { | ||
119 | // view: window, | ||
120 | // bubbles: true, | ||
121 | // cancelable: true, | ||
122 | // }); | ||
123 | // triggerBtn.value.dispatchEvent(event); | ||
124 | // }; | ||
125 | </script> | ||
126 | |||
127 | <style lang="less" scoped> | ||
128 | .navbar { | ||
129 | display: flex; | ||
130 | justify-content: space-between; | ||
131 | height: 100%; | ||
132 | background-color: var(--color-bg-2); | ||
133 | border-bottom: 1px solid var(--color-border); | ||
134 | } | ||
135 | |||
136 | .left-side { | ||
137 | display: flex; | ||
138 | align-items: center; | ||
139 | padding-left: 20px; | ||
140 | } | ||
141 | |||
142 | .right-side { | ||
143 | display: flex; | ||
144 | padding-right: 10px; | ||
145 | list-style: none; | ||
146 | |||
147 | :deep(.locale-select) { | ||
148 | border-radius: 20px; | ||
149 | } | ||
150 | |||
151 | li { | ||
152 | display: flex; | ||
153 | align-items: center; | ||
154 | padding: 0 10px; | ||
155 | } | ||
156 | |||
157 | a { | ||
158 | color: var(--color-text-1); | ||
159 | text-decoration: none; | ||
160 | } | ||
161 | |||
162 | .nav-btn { | ||
163 | border-color: rgb(var(--gray-2)); | ||
164 | color: rgb(var(--gray-8)); | ||
165 | font-size: 16px; | ||
166 | } | ||
167 | |||
168 | .trigger-btn, | ||
169 | .ref-btn { | ||
170 | position: absolute; | ||
171 | bottom: 14px; | ||
172 | } | ||
173 | |||
174 | .trigger-btn { | ||
175 | margin-left: 14px; | ||
176 | } | ||
177 | } | ||
178 | </style> | ||
179 | |||
180 | <style lang="less"> | ||
181 | .message-popover { | ||
182 | .arco-popover-content { | ||
183 | margin-top: 0; | ||
184 | } | ||
185 | } | ||
186 | |||
187 | .arco-dropdown-open .arco-icon-down { | ||
188 | transform: rotate(180deg); | ||
189 | } | ||
190 | </style> |
src/layout/index.vue
0 → 100644
1 | <template> | ||
2 | <a-layout class="layout"> | ||
3 | <div class="layout-navbar"> | ||
4 | <Navbar /> | ||
5 | </div> | ||
6 | <a-layout> | ||
7 | <a-layout> | ||
8 | <a-layout-sider | ||
9 | v-if="menu" | ||
10 | :breakpoint="'xl'" | ||
11 | :collapsed="collapse" | ||
12 | :collapsible="true" | ||
13 | :hide-trigger="true" | ||
14 | :style="{ paddingTop: '60px' }" | ||
15 | :width="menuWidth" | ||
16 | class="layout-sider" | ||
17 | @collapse="setCollapsed" | ||
18 | > | ||
19 | <div class="menu-wrapper"> | ||
20 | <Menu /> | ||
21 | </div> | ||
22 | </a-layout-sider> | ||
23 | <a-layout :style="paddingStyle" class="layout-content"> | ||
24 | <a-layout-content> | ||
25 | <router-view /> | ||
26 | </a-layout-content> | ||
27 | </a-layout> | ||
28 | </a-layout> | ||
29 | </a-layout> | ||
30 | </a-layout> | ||
31 | </template> | ||
32 | |||
33 | <script lang="ts"> | ||
34 | import { computed, defineComponent, watch } from "vue"; | ||
35 | import { useRoute, useRouter } from "vue-router"; | ||
36 | import { useAppStore, useAuthorizedStore } from "@/store"; | ||
37 | import usePermission from "@/hooks/permission"; | ||
38 | import Menu from "@/layout/components/menu.vue"; | ||
39 | import Navbar from "@/layout/components/navbar.vue"; | ||
40 | |||
41 | export default defineComponent({ | ||
42 | // eslint-disable-next-line vue/no-reserved-component-names | ||
43 | components: { Navbar, Menu }, | ||
44 | setup() { | ||
45 | const appStore = useAppStore(); | ||
46 | const authorizedStore = useAuthorizedStore(); | ||
47 | const router = useRouter(); | ||
48 | const route = useRoute(); | ||
49 | const permission = usePermission(); | ||
50 | const navbarHeight = `60px`; | ||
51 | const menu = computed(() => appStore.menu); | ||
52 | const menuWidth = computed(() => { | ||
53 | return appStore.menuCollapse ? 48 : appStore.menuWidth; | ||
54 | }); | ||
55 | const collapse = computed(() => { | ||
56 | return appStore.menuCollapse; | ||
57 | }); | ||
58 | const paddingStyle = computed(() => { | ||
59 | const paddingLeft = menu.value ? { paddingLeft: `${menuWidth.value}px` } : {}; | ||
60 | const paddingTop = { paddingTop: navbarHeight }; | ||
61 | return { ...paddingLeft, ...paddingTop }; | ||
62 | }); | ||
63 | const setCollapsed = (val: boolean) => { | ||
64 | appStore.updateSettings({ menuCollapse: val }); | ||
65 | }; | ||
66 | watch( | ||
67 | () => authorizedStore.permissions, | ||
68 | (roleValue) => { | ||
69 | if (roleValue && !permission.accessRouter(route)) router.push({ name: "notFound" }); | ||
70 | } | ||
71 | ); | ||
72 | return { | ||
73 | menu, | ||
74 | menuWidth, | ||
75 | paddingStyle, | ||
76 | collapse, | ||
77 | setCollapsed | ||
78 | }; | ||
79 | } | ||
80 | }); | ||
81 | </script> | ||
82 | |||
83 | <style lang="less" scoped> | ||
84 | @nav-size-height: 60px; | ||
85 | @layout-max-width: 1100px; | ||
86 | |||
87 | .layout { | ||
88 | width: 100%; | ||
89 | height: 100%; | ||
90 | } | ||
91 | |||
92 | .layout-navbar { | ||
93 | position: fixed; | ||
94 | top: 0; | ||
95 | left: 0; | ||
96 | z-index: 100; | ||
97 | width: 100%; | ||
98 | min-width: @layout-max-width; | ||
99 | height: @nav-size-height; | ||
100 | } | ||
101 | |||
102 | .layout-sider { | ||
103 | position: fixed; | ||
104 | top: 0; | ||
105 | left: 0; | ||
106 | z-index: 99; | ||
107 | height: 100%; | ||
108 | |||
109 | &::after { | ||
110 | position: absolute; | ||
111 | top: 0; | ||
112 | right: -1px; | ||
113 | display: block; | ||
114 | width: 1px; | ||
115 | height: 100%; | ||
116 | background-color: var(--color-border); | ||
117 | content: ''; | ||
118 | } | ||
119 | |||
120 | > :deep(.arco-layout-sider-children) { | ||
121 | overflow-y: hidden; | ||
122 | } | ||
123 | } | ||
124 | |||
125 | .menu-wrapper { | ||
126 | height: 100%; | ||
127 | overflow: auto; | ||
128 | overflow-x: hidden; | ||
129 | |||
130 | :deep(.arco-menu) { | ||
131 | ::-webkit-scrollbar { | ||
132 | width: 12px; | ||
133 | height: 4px; | ||
134 | } | ||
135 | |||
136 | ::-webkit-scrollbar-thumb { | ||
137 | border: 4px solid transparent; | ||
138 | background-clip: padding-box; | ||
139 | border-radius: 7px; | ||
140 | background-color: var(--color-text-4); | ||
141 | } | ||
142 | |||
143 | ::-webkit-scrollbar-thumb:hover { | ||
144 | background-color: var(--color-text-3); | ||
145 | } | ||
146 | } | ||
147 | } | ||
148 | |||
149 | .layout-content { | ||
150 | min-width: @layout-max-width; | ||
151 | min-height: 100vh; | ||
152 | overflow-y: hidden; | ||
153 | background-color: var(--color-fill-2); | ||
154 | transition: padding-left 0.2s; | ||
155 | } | ||
156 | </style> |
src/main.ts
0 → 100644
1 | import { createApp } from 'vue'; | ||
2 | import ArcoVue from '@arco-design/web-vue'; | ||
3 | import ArcoVueIcon from '@arco-design/web-vue/es/icon'; | ||
4 | import globalComponents from '@/components'; | ||
5 | |||
6 | import store from './store'; | ||
7 | import router from './router'; | ||
8 | import directive from './directive'; | ||
9 | import App from './App.vue'; | ||
10 | |||
11 | import '@/assets/style/global.less'; | ||
12 | import '@/api/interceptor'; | ||
13 | |||
14 | const app = createApp(App); | ||
15 | |||
16 | app.use(ArcoVue, {}); | ||
17 | app.use(ArcoVueIcon); | ||
18 | |||
19 | app.use(store); | ||
20 | app.use(router); | ||
21 | app.use(globalComponents); | ||
22 | app.use(directive); | ||
23 | |||
24 | app.mount('#app'); |
src/router/index.ts
0 → 100644
1 | import { createRouter, createWebHistory, LocationQueryRaw } from 'vue-router'; | ||
2 | import NProgress from 'nprogress'; // progress bar | ||
3 | import 'nprogress/nprogress.css'; | ||
4 | |||
5 | import usePermission from '@/hooks/permission'; | ||
6 | import { clearToken, isLogin } from '@/utils/auth'; | ||
7 | import PageLayout from '@/layout/index.vue'; | ||
8 | |||
9 | import { useAuthorizedStore } from '@/store'; | ||
10 | import appRoutes from './modules'; | ||
11 | |||
12 | NProgress.configure({ showSpinner: false }); // NProgress Configuration | ||
13 | |||
14 | const router = createRouter({ | ||
15 | history: createWebHistory(), | ||
16 | routes: [ | ||
17 | { | ||
18 | path: '/login', | ||
19 | name: 'login', | ||
20 | component: () => import('@/views/login/index.vue'), | ||
21 | meta: { | ||
22 | title: '登陆', | ||
23 | requiresAuth: false, | ||
24 | }, | ||
25 | }, | ||
26 | { | ||
27 | name: 'root', | ||
28 | path: '/', | ||
29 | component: PageLayout, | ||
30 | redirect: '/demos', | ||
31 | children: appRoutes, | ||
32 | }, | ||
33 | { | ||
34 | path: '/:pathMatch(.*)*', | ||
35 | name: 'notFound', | ||
36 | redirect: '/exception/404', | ||
37 | meta: { | ||
38 | requiresAuth: true, | ||
39 | }, | ||
40 | }, | ||
41 | ], | ||
42 | scrollBehavior() { | ||
43 | return { top: 0 }; | ||
44 | }, | ||
45 | }); | ||
46 | |||
47 | router.beforeEach(async (to, from, next) => { | ||
48 | NProgress.start(); | ||
49 | const authorizedStore = useAuthorizedStore(); | ||
50 | |||
51 | if (from.name !== undefined) { | ||
52 | to.meta.from = from.name; | ||
53 | } | ||
54 | |||
55 | async function crossroads() { | ||
56 | const Permission = usePermission(); | ||
57 | |||
58 | if (Permission.accessRouter(to)) { | ||
59 | next(); | ||
60 | } else { | ||
61 | next({ name: 'exception-403' }); | ||
62 | } | ||
63 | NProgress.done(); | ||
64 | } | ||
65 | |||
66 | if (isLogin()) { | ||
67 | if (authorizedStore.permissions.length) { | ||
68 | await crossroads(); | ||
69 | } else { | ||
70 | try { | ||
71 | await authorizedStore.syncInfo(); | ||
72 | await crossroads(); | ||
73 | } catch (error) { | ||
74 | clearToken(); | ||
75 | next({ name: 'login', query: { path: to.path } as LocationQueryRaw }); | ||
76 | NProgress.done(); | ||
77 | } | ||
78 | } | ||
79 | } else { | ||
80 | if (to.name === 'login') { | ||
81 | next(); | ||
82 | NProgress.done(); | ||
83 | return; | ||
84 | } | ||
85 | |||
86 | if (to.name === 'root') { | ||
87 | next({ name: 'login' }); | ||
88 | } else { | ||
89 | next({ name: 'login', query: { path: to.path } as LocationQueryRaw }); | ||
90 | } | ||
91 | NProgress.done(); | ||
92 | } | ||
93 | }); | ||
94 | |||
95 | router.afterEach(async (to) => { | ||
96 | sessionStorage.setItem( | ||
97 | 'route', | ||
98 | to.meta?.breadcrumb?.toString() || | ||
99 | to.matched | ||
100 | ?.slice(1) | ||
101 | .map((item) => item.name) | ||
102 | .toString() || | ||
103 | 'dashboard' | ||
104 | ); | ||
105 | }); | ||
106 | |||
107 | export default router; |
src/router/modules/demo.ts
0 → 100644
1 | export default [ | ||
2 | { | ||
3 | path: "demos", | ||
4 | name: "audition-demo", | ||
5 | component: () => import("@/views/demo/index.vue"), | ||
6 | meta: { | ||
7 | title: "Demo管理", | ||
8 | icon: "icon-dashboard", | ||
9 | requiresAuth: true, | ||
10 | hideInMenu: false, | ||
11 | isRedirect: true, | ||
12 | roles: ["*"], | ||
13 | breadcrumb: ["audition-demo"], | ||
14 | reload: true | ||
15 | } | ||
16 | }, | ||
17 | { | ||
18 | path: "demos/:id(\\d+)", | ||
19 | name: "audition-demo-show", | ||
20 | component: () => import("@/views/demo-show/index.vue"), | ||
21 | meta: { | ||
22 | title: "详情", | ||
23 | requiresAuth: false, | ||
24 | hideInMenu: true, | ||
25 | reload: true, | ||
26 | menuSelectKey: "audition-demo", | ||
27 | roles: ["*"], | ||
28 | breadcrumb: ["audition-demo", "audition-demo-show"] | ||
29 | } | ||
30 | } | ||
31 | ]; |
src/router/modules/exception.ts
0 → 100644
1 | export default { | ||
2 | path: 'exception', | ||
3 | name: 'exception', | ||
4 | component: () => import('@/views/exception/index.vue'), | ||
5 | meta: { | ||
6 | title: '异常页', | ||
7 | requiresAuth: true, | ||
8 | icon: 'icon-exclamation-circle', | ||
9 | hideInMenu: true, | ||
10 | }, | ||
11 | children: [ | ||
12 | { | ||
13 | path: '403', | ||
14 | name: 'exception-403', | ||
15 | component: () => import('@/views/exception/403/index.vue'), | ||
16 | meta: { | ||
17 | title: '403', | ||
18 | requiresAuth: true, | ||
19 | roles: ['*'], | ||
20 | }, | ||
21 | }, | ||
22 | { | ||
23 | path: '404', | ||
24 | name: 'exception-404', | ||
25 | component: () => import('@/views/exception/404/index.vue'), | ||
26 | meta: { | ||
27 | title: '404', | ||
28 | requiresAuth: true, | ||
29 | roles: ['*'], | ||
30 | hideInMenu: true, | ||
31 | }, | ||
32 | }, | ||
33 | { | ||
34 | path: '500', | ||
35 | name: 'exception-500', | ||
36 | component: () => import('@/views/exception/500/index.vue'), | ||
37 | meta: { | ||
38 | title: '500', | ||
39 | requiresAuth: true, | ||
40 | roles: ['*'], | ||
41 | }, | ||
42 | }, | ||
43 | ], | ||
44 | }; |
src/router/modules/index.ts
0 → 100644
src/router/typings.d.ts
0 → 100644
1 | import 'vue-router'; | ||
2 | |||
3 | declare module 'vue-router' { | ||
4 | interface RouteMeta { | ||
5 | // options | ||
6 | roles?: string[]; | ||
7 | // every route must declare | ||
8 | requiresAuth: boolean; // need login | ||
9 | icon?: string; | ||
10 | // menu select key | ||
11 | menuSelectKey?: string; | ||
12 | hideInMenu?: boolean; | ||
13 | isRedirect?: boolean; | ||
14 | title?: string; | ||
15 | order?: number; | ||
16 | breadcrumb?: string[]; | ||
17 | } | ||
18 | } |
src/store/index.ts
0 → 100644
1 | import { createPinia } from 'pinia'; | ||
2 | import useAppStore from './modules/app'; | ||
3 | // eslint-disable-next-line import/no-cycle | ||
4 | import useAuthorizedStore from './modules/authorized'; | ||
5 | |||
6 | import useSelectionStore from '@/store/modules/selection'; | ||
7 | |||
8 | const pinia = createPinia(); | ||
9 | |||
10 | export { useAppStore, useSelectionStore, useAuthorizedStore }; | ||
11 | export default pinia; | ||
12 |
src/store/modules/app/index.ts
0 → 100644
1 | import { defineStore } from 'pinia'; | ||
2 | import type { RouteRecordNormalized } from 'vue-router'; | ||
3 | import { AppState } from './types'; | ||
4 | |||
5 | const useAppStore = defineStore('app', { | ||
6 | state: (): AppState => ({ | ||
7 | theme: 'light', | ||
8 | colorWeak: false, | ||
9 | navbar: true, | ||
10 | menu: true, | ||
11 | topMenu: false, | ||
12 | hideMenu: false, | ||
13 | menuCollapse: false, | ||
14 | footer: true, | ||
15 | themeColor: '#165DFF', | ||
16 | menuWidth: 220, | ||
17 | globalSettings: false, | ||
18 | device: 'desktop', | ||
19 | tabBar: false, | ||
20 | permissions: [], | ||
21 | }), | ||
22 | |||
23 | getters: { | ||
24 | appCurrentSetting(state: AppState): AppState { | ||
25 | return { ...state }; | ||
26 | }, | ||
27 | appDevice(state: AppState) { | ||
28 | return state.device; | ||
29 | }, | ||
30 | appAsyncMenus(state: AppState): RouteRecordNormalized[] { | ||
31 | return state.serverMenu as unknown as RouteRecordNormalized[]; | ||
32 | }, | ||
33 | appMenu(state: AppState) { | ||
34 | return state.permissions?.filter((item) => item.type === 'Menu') || []; | ||
35 | }, | ||
36 | }, | ||
37 | |||
38 | actions: { | ||
39 | // Update app settings | ||
40 | updateSettings(partial: Partial<AppState>) { | ||
41 | // @ts-ignore-next-line | ||
42 | this.$patch(partial); | ||
43 | }, | ||
44 | |||
45 | // Change theme color | ||
46 | toggleTheme(dark: boolean) { | ||
47 | if (dark) { | ||
48 | this.theme = 'dark'; | ||
49 | document.body.setAttribute('arco-theme', 'dark'); | ||
50 | } else { | ||
51 | this.theme = 'light'; | ||
52 | document.body.removeAttribute('arco-theme'); | ||
53 | } | ||
54 | }, | ||
55 | toggleDevice(device: string) { | ||
56 | this.device = device; | ||
57 | }, | ||
58 | toggleMenu(value: boolean) { | ||
59 | this.hideMenu = value; | ||
60 | }, | ||
61 | setPermissions(permissions = []) { | ||
62 | this.$patch({ permissions }); | ||
63 | }, | ||
64 | }, | ||
65 | }); | ||
66 | |||
67 | export default useAppStore; |
src/store/modules/app/types.ts
0 → 100644
1 | export interface SystemPermission { | ||
2 | id: number; | ||
3 | name: string; | ||
4 | type: 'Menu' | 'Button'; | ||
5 | label: string; | ||
6 | icon: string; | ||
7 | parent_id: number; | ||
8 | weight: number; | ||
9 | parent?: SystemPermission; | ||
10 | children?: SystemPermission[]; | ||
11 | created_at: string; | ||
12 | updated_at: string; | ||
13 | } | ||
14 | |||
15 | export interface AppState { | ||
16 | theme: string; | ||
17 | colorWeak: boolean; | ||
18 | navbar: boolean; | ||
19 | menu: boolean; | ||
20 | topMenu: boolean; | ||
21 | hideMenu: boolean; | ||
22 | menuCollapse: boolean; | ||
23 | themeColor: string; | ||
24 | menuWidth: number; | ||
25 | globalSettings: boolean; | ||
26 | device: string; | ||
27 | permissions: SystemPermission[]; | ||
28 | [key: string]: unknown; | ||
29 | } |
src/store/modules/authorized/index.ts
0 → 100644
1 | import { defineStore } from "pinia"; | ||
2 | import { clearToken } from "@/utils/auth"; | ||
3 | import useAuthApi from "@/api/auth"; | ||
4 | import { AuthorizedState } from "./type"; | ||
5 | import { Message } from "@arco-design/web-vue"; | ||
6 | import { removeRouteListener } from "@/utils/route-listener"; | ||
7 | // eslint-disable-next-line import/no-cycle | ||
8 | import { useAppStore } from "@/store"; | ||
9 | |||
10 | const useAuthorizedStore = defineStore("authorized", { | ||
11 | state: (): AuthorizedState => ({ | ||
12 | id: undefined, | ||
13 | nick_name: undefined, | ||
14 | avatar: undefined, | ||
15 | permissions: [], | ||
16 | can_create_demo: 0 | ||
17 | }), | ||
18 | getters: { | ||
19 | authorizedInfo(state: AuthorizedState): AuthorizedState { | ||
20 | return { ...state }; | ||
21 | }, | ||
22 | getKey(state: AuthorizedState): number { | ||
23 | return state.id || 0; | ||
24 | }, | ||
25 | isCreateDemo(state: AuthorizedState) { | ||
26 | return state.can_create_demo; | ||
27 | } | ||
28 | }, | ||
29 | actions: { | ||
30 | setInfo(partial: Partial<AuthorizedState>) { | ||
31 | // @ts-ignore | ||
32 | this.$patch(partial); | ||
33 | }, | ||
34 | async syncInfo() { | ||
35 | const { user, permissions, menus, can_create_demo } = await useAuthApi.info(); | ||
36 | this.setInfo({ ...user, can_create_demo, permissions }); | ||
37 | useAppStore().setPermissions(menus as any); | ||
38 | }, | ||
39 | async logout() { | ||
40 | this.$reset(); | ||
41 | clearToken(); | ||
42 | removeRouteListener(); | ||
43 | window.location.reload(); | ||
44 | Message.success("登出成功"); | ||
45 | }, | ||
46 | |||
47 | async syncToken() { | ||
48 | // TODO | ||
49 | // const { data } = await refreshToken(); | ||
50 | // setToken(data.access_token); | ||
51 | } | ||
52 | } | ||
53 | }); | ||
54 | |||
55 | export default useAuthorizedStore; |
src/store/modules/authorized/type.ts
0 → 100644
src/store/modules/selection/index.ts
0 → 100644
1 | import { defineStore } from 'pinia'; | ||
2 | import { Selection } from '@/store/modules/selection/type'; | ||
3 | import { AnyObject } from '@/types/global'; | ||
4 | import { SystemConfig } from '@/types/system-config'; | ||
5 | import axios from 'axios'; | ||
6 | import { Tag } from '@/types/tag'; | ||
7 | |||
8 | const useSelectionStore = defineStore('selection', { | ||
9 | state: (): Selection => ({ | ||
10 | user: [], | ||
11 | project: [], | ||
12 | tag: [], | ||
13 | config: [], | ||
14 | }), | ||
15 | getters: { | ||
16 | getUserOptions(state) { | ||
17 | return state.user; | ||
18 | }, | ||
19 | getTagOptions(state) { | ||
20 | return state.tag; | ||
21 | }, | ||
22 | projectOptions(state) { | ||
23 | return state.project; | ||
24 | }, | ||
25 | lyricTool(state): string { | ||
26 | return state.config.find((item) => item.identifier === 'activity_lyric_tool')?.content || ''; | ||
27 | }, | ||
28 | activityLang(state): SystemConfig | undefined { | ||
29 | return state.config.find((item) => item.identifier === 'activity_lang'); | ||
30 | }, | ||
31 | activityLangOptions(state): SystemConfig[] { | ||
32 | return state.config.filter((item) => item.parent_id === this.activityLang?.id); | ||
33 | }, | ||
34 | activitySpeed(state): SystemConfig | undefined { | ||
35 | return state.config.find((item) => item.identifier === 'activity_speed'); | ||
36 | }, | ||
37 | activitySpeedOptions(state): SystemConfig[] { | ||
38 | return state.config.filter((item) => item.parent_id === this.activitySpeed?.id); | ||
39 | }, | ||
40 | activitySex(state): SystemConfig | undefined { | ||
41 | return state.config.find((item) => item.identifier === 'activity_sex'); | ||
42 | }, | ||
43 | activitySexOptions(state): SystemConfig[] { | ||
44 | return state.config.filter((item) => item.parent_id === this.activitySex?.id); | ||
45 | }, | ||
46 | activityTagOptions(state): Pick<Tag, 'id' | 'name' | 'type'>[] { | ||
47 | return state.tag.filter((item) => item.type === 1); | ||
48 | }, | ||
49 | activityAudioAccept(state): string { | ||
50 | return state.config.find((item) => item.identifier === 'activity_audio_accept')?.content || 'audio/*'; | ||
51 | }, | ||
52 | activityTrackAccept(state): string { | ||
53 | return state.config.find((item) => item.identifier === 'activity_track_accept')?.content || '*'; | ||
54 | }, | ||
55 | appleDemoCover(state): string { | ||
56 | return state.config.find((item) => item.identifier === 'activity_demo_cover')?.content || ''; | ||
57 | }, | ||
58 | }, | ||
59 | actions: { | ||
60 | queryUser(params?: AnyObject) { | ||
61 | axios.get('/provider/users', { params }).then(({ data }) => { | ||
62 | this.user = data; | ||
63 | }); | ||
64 | }, | ||
65 | queryTag(params?: AnyObject) { | ||
66 | axios.get('/provider/tags',{ params }).then(({ data }) => { | ||
67 | this.tag = data; | ||
68 | }); | ||
69 | }, | ||
70 | queryConfig(params?: AnyObject) { | ||
71 | axios.get('/provider/configs', { params }).then(({ data }) => { | ||
72 | this.config = data; | ||
73 | }); | ||
74 | }, | ||
75 | }, | ||
76 | }); | ||
77 | |||
78 | export default useSelectionStore; |
src/store/modules/selection/type.ts
0 → 100644
src/types/activity-apply.ts
0 → 100644
1 | import { Project } from '@/types/project'; | ||
2 | import { Tag } from '@/types/tag'; | ||
3 | import { User } from '@/types/user'; | ||
4 | |||
5 | export interface ActivityExpand { | ||
6 | tag_ids: number[]; | ||
7 | lyricist: { ids: number[]; supplement: string[] }; | ||
8 | composer: { ids: number[]; supplement: string[] }; | ||
9 | arranger: { ids: number[]; supplement: string[] }; | ||
10 | guide_source: { name: string; url: string; size: number }; | ||
11 | karaoke_source: { name: string; url: string; size: number }; | ||
12 | track_source: { name: string; url: string; size: number }; | ||
13 | } | ||
14 | |||
15 | export interface ActivityApplyRecord { | ||
16 | id: number; | ||
17 | created_at: string; | ||
18 | audit_msg: string; | ||
19 | } | ||
20 | |||
21 | export interface ActivityApply { | ||
22 | id: number; | ||
23 | cover: string; | ||
24 | song_name: string; | ||
25 | sub_title?: string; | ||
26 | lang: string[]; | ||
27 | speed: string; | ||
28 | lyric: string; | ||
29 | clip_lyric: string; | ||
30 | sex: string; | ||
31 | project_id: number; | ||
32 | estimate_release_at: string; | ||
33 | audit_status: number; | ||
34 | expand: ActivityExpand; | ||
35 | project?: Project; | ||
36 | tags?: Tag[]; | ||
37 | user?: User; | ||
38 | apply_records?: ActivityApplyRecord[]; | ||
39 | } | ||
40 | |||
41 | export type ActivityApplyFormStep1 = Pick<ActivityApply, 'cover' | 'song_name' | 'sub_title' | 'lang' | 'speed'> & | ||
42 | Pick<ActivityExpand, 'tag_ids'>; | ||
43 | |||
44 | export type ActivityApplyFormStep2 = Pick<ActivityApply, 'project_id' | 'sex' | 'estimate_release_at'> & | ||
45 | Pick<ActivityExpand, 'lyricist' | 'composer' | 'arranger'>; | ||
46 | |||
47 | export type ActivityApplyFormStep3 = Pick<ActivityApply, 'song_name' | 'lyric' | 'clip_lyric'> & | ||
48 | Pick<ActivityExpand, 'guide_source' | 'karaoke_source' | 'track_source'>; | ||
49 | |||
50 | export type ActivityApplyForm = ActivityApplyFormStep1 & ActivityApplyFormStep2 & ActivityApplyFormStep3; |
src/types/activity.ts
0 → 100644
1 | import { Project } from '@/types/project'; | ||
2 | import { Tag } from '@/types/tag'; | ||
3 | import { FileStatus } from '@arco-design/web-vue/es/upload/interfaces'; | ||
4 | import { User } from '@/types/user'; | ||
5 | import { ActivityExpand } from '@/types/activity-apply'; | ||
6 | |||
7 | export type ActivityMaterialType = 'GuideDinging' | 'Accompany'; | ||
8 | export type ActivityMaterialTone = -3 | -2 | -1 | 0 | 1 | 2 | 3; | ||
9 | |||
10 | export interface ActivityMaterialOption { | ||
11 | type: ActivityMaterialType; | ||
12 | tone: ActivityMaterialTone; | ||
13 | label: string; | ||
14 | } | ||
15 | |||
16 | export interface ActivityMaterial { | ||
17 | id: string; | ||
18 | type: ActivityMaterialType; | ||
19 | url: string; | ||
20 | name: string; | ||
21 | tone: ActivityMaterialTone; | ||
22 | file?: File; | ||
23 | percent?: number; | ||
24 | status?: FileStatus; | ||
25 | response?: string; | ||
26 | created_at?: string; | ||
27 | } | ||
28 | |||
29 | export interface ActivitySendLink { | ||
30 | type: string; | ||
31 | url: string; | ||
32 | } | ||
33 | |||
34 | export interface ActivityRecommendIntro { | ||
35 | id: number; | ||
36 | content: string; | ||
37 | } | ||
38 | |||
39 | export interface ActivityApplyRecord { | ||
40 | id: number; | ||
41 | audit_user_id: number; | ||
42 | audit_msg: string; | ||
43 | created_at?: string; | ||
44 | updated_at?: string; | ||
45 | } | ||
46 | |||
47 | export interface Activity { | ||
48 | id: number; | ||
49 | song_name: string; | ||
50 | song_type?: number; | ||
51 | sub_title: string; | ||
52 | cover: string; | ||
53 | user_id: number; | ||
54 | project_id: number; | ||
55 | guide: string; | ||
56 | karaoke: string; | ||
57 | guide_source?: string; | ||
58 | karaoke_source?: string; | ||
59 | lyric: string; | ||
60 | clip_lyric: string; | ||
61 | weight: number; | ||
62 | audit_status: number; | ||
63 | status: number; | ||
64 | match_at: string; | ||
65 | created_at: string; | ||
66 | updated_at: string; | ||
67 | submit_works_count?: number; | ||
68 | views_count?: number; | ||
69 | collections_count?: number; | ||
70 | is_official?: number; | ||
71 | send_url?: ActivitySendLink[]; | ||
72 | user?: Pick<User, 'id' | 'nick_name' | 'real_name' | 'role'>; | ||
73 | project?: Project; | ||
74 | tags?: Tag[]; | ||
75 | materials?: ActivityMaterial[]; | ||
76 | recommend_intros?: ActivityRecommendIntro[]; | ||
77 | apply_records?: ActivityApplyRecord[]; | ||
78 | estimate_release_at: string; | ||
79 | expand?: ActivityExpand; | ||
80 | links?: User[]; | ||
81 | } | ||
82 | |||
83 | export type ActivityViewUser = Pick<User, 'id' | 'avatar' | 'nick_name' | 'real_name' | 'sex' | 'role'> & { | ||
84 | listen_count: number; | ||
85 | collection_count: number; | ||
86 | submit_work: number; | ||
87 | last_listen_at: number; | ||
88 | }; |
src/types/admin.ts
0 → 100644
1 | // eslint-disable-next-line import/no-cycle | ||
2 | import { User } from '@/types/user'; | ||
3 | |||
4 | export interface Admin extends User { | ||
5 | creator?: User; | ||
6 | role?: 'ProjectUser' | 'SystemUser' | 'Admin'; | ||
7 | company: string; | ||
8 | activities_count?: number; | ||
9 | singers_count?: number; | ||
10 | submit_activities_count?: number; | ||
11 | accept_activities_count?: number; | ||
12 | checked_activities_count?: number; | ||
13 | } |
src/types/echarts.ts
0 → 100644
src/types/global.ts
0 → 100644
1 | export interface AnyObject { | ||
2 | [key: string]: unknown; | ||
3 | } | ||
4 | |||
5 | export interface Options { | ||
6 | value: unknown; | ||
7 | label: string; | ||
8 | } | ||
9 | |||
10 | export interface MoreRowValue<T = object> { | ||
11 | type: string; | ||
12 | row: T; | ||
13 | index?: number; | ||
14 | } | ||
15 | |||
16 | export interface QueryForParams { | ||
17 | [key: string]: unknown; | ||
18 | |||
19 | sortBy?: string; | ||
20 | sortType?: 'desc' | 'asc' | ''; | ||
21 | } | ||
22 | |||
23 | export interface QueryForPaginationParams extends QueryForParams { | ||
24 | page?: number; | ||
25 | pageSize?: number; | ||
26 | } | ||
27 | |||
28 | export type sizeType = 'mini' | 'small' | 'medium' | 'large'; | ||
29 | |||
30 | export interface AttributeData { | ||
31 | [key: string]: unknown; | ||
32 | } | ||
33 | |||
34 | export interface Pagination { | ||
35 | current: number; | ||
36 | pageSize: number; | ||
37 | total: number; | ||
38 | showTotal?: boolean; | ||
39 | showPageSize?: boolean; | ||
40 | pageSizeOptions?: number[]; | ||
41 | } | ||
42 | |||
43 | export type SortType = 'descend' | 'ascend' | '' | string; | ||
44 | |||
45 | export interface Sort { | ||
46 | column: string; | ||
47 | type: SortType | string; | ||
48 | } | ||
49 | |||
50 | export interface GeneralChart { | ||
51 | xAxis: string[]; | ||
52 | data: Array<{ name: string; value: number[] }>; | ||
53 | } | ||
54 | |||
55 | export interface HttpResponse<T = unknown> { | ||
56 | status: 'success' | 'fail' | 'error'; | ||
57 | msg: string; | ||
58 | code: number; | ||
59 | data: T; | ||
60 | } | ||
61 | |||
62 | export interface ServiceResponse<T = unknown> extends HttpResponse<T> { | ||
63 | meta: { current: number; limit: number; total: number }; | ||
64 | } |
src/types/mock.ts
0 → 100644
src/types/project.ts
0 → 100644
1 | // eslint-disable-next-line import/no-cycle | ||
2 | import { Admin } from '@/types/admin'; | ||
3 | import { User } from '@/types/user'; | ||
4 | |||
5 | export interface Project { | ||
6 | id: number; | ||
7 | name: string; | ||
8 | user?: Admin; | ||
9 | master?: User; | ||
10 | status: number; | ||
11 | created_at?: string; | ||
12 | updated_at?: string; | ||
13 | up_count?: number; | ||
14 | down_count?: number; | ||
15 | finish_count?: number; | ||
16 | is_promote?: number; | ||
17 | is_official?: number; | ||
18 | |||
19 | is_can_apply?: number; | ||
20 | is_can_manage?: number; | ||
21 | is_can_demo_apply?: number; | ||
22 | |||
23 | head_cover: string; | ||
24 | cover: string; | ||
25 | intro: string; | ||
26 | managers_count?: number; | ||
27 | activities_count?: number; | ||
28 | send_count?: number; | ||
29 | managers?: Admin[]; | ||
30 | } |
src/types/system-config.ts
0 → 100644
src/types/tag.ts
0 → 100644
src/types/user.ts
0 → 100644
This diff is collapsed.
Click to expand it.
src/utils/audioSyncLyric.ts
0 → 100644
This diff is collapsed.
Click to expand it.
src/utils/auth.ts
0 → 100644
This diff is collapsed.
Click to expand it.
src/utils/createVNode.ts
0 → 100644
This diff is collapsed.
Click to expand it.
src/utils/env.ts
0 → 100644
This diff is collapsed.
Click to expand it.
src/utils/event.ts
0 → 100644
This diff is collapsed.
Click to expand it.
src/utils/index.ts
0 → 100644
This diff is collapsed.
Click to expand it.
src/utils/is.ts
0 → 100644
This diff is collapsed.
Click to expand it.
src/utils/monitor.ts
0 → 100644
This diff is collapsed.
Click to expand it.
src/utils/route-listener.ts
0 → 100644
This diff is collapsed.
Click to expand it.
This diff is collapsed.
Click to expand it.
This diff is collapsed.
Click to expand it.
src/views/demo-show/components/user-tag.vue
0 → 100644
This diff is collapsed.
Click to expand it.
src/views/demo-show/index.vue
0 → 100644
This diff is collapsed.
Click to expand it.
src/views/demo/components/audio-preview.vue
0 → 100644
This diff is collapsed.
Click to expand it.
src/views/demo/components/form-content.vue
0 → 100644
This diff is collapsed.
Click to expand it.
This diff is collapsed.
Click to expand it.
This diff is collapsed.
Click to expand it.
This diff is collapsed.
Click to expand it.
src/views/demo/index.vue
0 → 100644
This diff is collapsed.
Click to expand it.
src/views/exception/403/index.vue
0 → 100644
This diff is collapsed.
Click to expand it.
src/views/exception/404/index.vue
0 → 100644
This diff is collapsed.
Click to expand it.
src/views/exception/500/index.vue
0 → 100644
This diff is collapsed.
Click to expand it.
src/views/exception/index.vue
0 → 100644
This diff is collapsed.
Click to expand it.
src/views/login/components/phone-content.vue
0 → 100644
This diff is collapsed.
Click to expand it.
src/views/login/index.vue
0 → 100644
This diff is collapsed.
Click to expand it.
tsconfig.json
0 → 100644
This diff is collapsed.
Click to expand it.
-
Please register or sign in to post a comment