Commit 8514204c 8514204ca67957cca8641dd8af192aeec128f90e by 杨俊

feat(master): init

0 parents
Showing 284 changed files with 4731 additions and 0 deletions
1 VITE_API_HOST=https://hi-sing-service-dev.hikoon.com
2
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
1 /*.json
2 /*.js
3 dist
...\ No newline at end of file ...\ No newline at end of file
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': 1,
45 'import/order': 0,
46 // Vue: Recommended rules to be closed or modify
47 'vue/require-default-prop': 0,
48 'vue/singleline-html-element-content-newline': 0,
49 'vue/max-attributes-per-line': 0,
50 // Vue: Add extra rules
51 'vue/custom-event-name-casing': [2, 'camelCase'],
52 'vue/no-v-text': 1,
53 'vue/padding-line-between-blocks': 1,
54 'vue/require-direct-export': 1,
55 'vue/multi-word-component-names': 0,
56 // Allow @ts-ignore comment
57 '@typescript-eslint/ban-ts-comment': 0,
58 '@typescript-eslint/no-unused-vars': 1,
59 '@typescript-eslint/no-empty-function': 1,
60 '@typescript-eslint/no-explicit-any': 0,
61 '@typescript-eslint/no-implicit-any': 0,
62 'camelcase': 'off',
63 'max-classes-per-file': 'off',
64 '@typescript-eslint/camelcase': 0,
65 'import/extensions': [
66 2,
67 'ignorePackages',
68 {
69 js: 'never',
70 jsx: 'never',
71 ts: 'never',
72 tsx: 'never',
73 },
74 ],
75 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
76 'no-param-reassign': 0,
77 'no-return-assign': 0,
78 'no-shadow': 0,
79 'prefer-regex-literals': 0,
80 'import/no-extraneous-dependencies': 0,
81 'class-methods-use-this': 0,
82 },
83 };
1 node_modules
2 .DS_Store
3 dist
4 dist-ssr
5 *.local
6 /node_modules
7 /dist
8 /dist-ssr
9 .idea/*
10 yarn.lock
11 .env.production
12 components.d.ts
1 /dist/*
2 /node_modules/**
3
4 **/*.svg
5 **/*.sh
1 module.exports = {
2 tabWidth: 2,
3 semi: true,
4 printWidth: 140,
5 singleQuote: true,
6 quoteProps: 'consistent',
7 htmlWhitespaceSensitivity: 'strict',
8 vueIndentScriptAndStyle: true,
9 };
1 module.exports = {
2 extends: [
3 'stylelint-config-standard',
4 'stylelint-config-rational-order',
5 'stylelint-config-prettier',
6 ],
7 defaultSeverity: 'warning',
8 plugins: ['stylelint-order'],
9 rules: {
10 'at-rule-no-unknown': [
11 true,
12 {
13 ignoreAtRules: ['plugin'],
14 },
15 ],
16 'rule-empty-line-before': [
17 'always',
18 {
19 except: ['after-single-line-comment', 'first-nested'],
20 },
21 ],
22 'selector-pseudo-class-no-unknown': [
23 true,
24 {
25 ignorePseudoClasses: ['deep'],
26 },
27 ],
28 },
29 };
1 module.exports = {
2 plugins: ['@vue/babel-plugin-jsx'],
3 };
1 module.exports = {
2 extends: ['@commitlint/config-conventional'],
3 };
1 !.gitignore
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 // eslint-disable-next-line import/no-unresolved
10 import Components from "unplugin-vue-components/vite";
11 // eslint-disable-next-line import/no-unresolved
12 import { ArcoResolver } from "unplugin-vue-components/resolvers";
13
14 export default function configArcoResolverPlugin() {
15 return Components({
16 dirs: [], // Avoid parsing src/components. 避免解析到src/components
17 deep: false,
18 resolvers: [ArcoResolver()],
19 });
20 }
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 }
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 }
1 /**
2 * Introduces component library styles on demand.
3 * 按需引入组件库样式
4 * https://github.com/anncwb/vite-plugin-style-import
5 */
6
7 import styleImport from 'vite-plugin-style-import';
8
9 export default function configStyleImportPlugin() {
10 return styleImport({
11 libs: [
12 {
13 libraryName: '@arco-design/web-vue',
14 esModule: true,
15 resolveStyle: (name) => {
16 // The use of this part of the component must depend on the parent, so it can be ignored directly.
17 // 这部分组件的使用必须依赖父级,所以直接忽略即可。
18 const ignoreList = [
19 'button-group',
20 'config-provider',
21 'anchor-link',
22 'sub-menu',
23 'menu-item',
24 'menu-item-group',
25 'breadcrumb-item',
26 'form-item',
27 'step',
28 'card-grid',
29 'card-meta',
30 'collapse-panel',
31 'collapse-item',
32 'descriptions-item',
33 'list-item',
34 'list-item-meta',
35 'table-column',
36 'table-column-group',
37 'tab-pane',
38 'tab-content',
39 'timeline-item',
40 'tree-node',
41 'skeleton-line',
42 'skeleton-shape',
43 'grid-item',
44 'carousel-item',
45 'doption',
46 'option',
47 'optgroup',
48 'icon',
49 ];
50 // List of components that need to map imported styles
51 // 需要映射引入样式的组件列表
52 const replaceList = {
53 'typography-text': 'typography',
54 'typography-title': 'typography',
55 'typography-paragraph': 'typography',
56 'typography-link': 'typography',
57 'dropdown-button': 'dropdown',
58 'input-password': 'input',
59 'input-search': 'input',
60 'input-group': 'input',
61 'radio-group': 'radio',
62 'checkbox-group': 'checkbox',
63 'layout-sider': 'layout',
64 'layout-content': 'layout',
65 'layout-footer': 'layout',
66 'layout-header': 'layout',
67 'month-picker': 'date-picker',
68 'range-picker': 'date-picker',
69 'row': 'grid', // 'grid/row.less'
70 'col': 'grid', // 'grid/col.less'
71 'avatar-group': 'avatar',
72 'image-preview': 'image',
73 'image-preview-group': 'image',
74 };
75 if (ignoreList.includes(name)) return '';
76
77 if (name === 'use-form-item') {
78 return `@arco-design/web-vue/es/_hooks/use-form-item.js`;
79 }
80
81 // eslint-disable-next-line no-prototype-builtins
82 // return replaceList.hasOwnProperty(name)
83 // ? // @ts-ignore
84 // `@arco-design/web-vue/es/${replaceList[name]}/style/css.js`
85 // : `@arco-design/web-vue/es/${name}/style/css.js`;
86
87 // less
88 // eslint-disable-next-line no-prototype-builtins
89 return replaceList.hasOwnProperty(name)
90 ? // @ts-ignore
91 `@arco-design/web-vue/es/${replaceList[name]}/style/css.js`
92 : `@arco-design/web-vue/es/${name}/style/index.js`;
93 },
94 },
95 ],
96 });
97 }
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 }
1 /**
2 * Whether to generate package preview
3 * 是否生成打包报告
4 */
5 export default {};
6
7 export function isReportMode(): boolean {
8 return process.env.REPORT === 'true';
9 }
1 import vue from '@vitejs/plugin-vue';
2 import vueJsx from '@vitejs/plugin-vue-jsx';
3 import { resolve } from 'path';
4 import { defineConfig } from 'vite';
5 import svgLoader from 'vite-svg-loader';
6 import vueSetupExtend from 'vite-plugin-vue-setup-extend';
7
8 export default defineConfig({
9 plugins: [vue({ script: { defineModel: true } }), vueJsx(), svgLoader({ svgoConfig: {} }), vueSetupExtend()],
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 build: {
32 rollupOptions: {
33 external: [],
34 },
35 },
36 define: {
37 'process.env': {},
38 '__VUE_PROD_HYDRATION_MISMATCH_DETAILS__': false,
39 },
40 css: {
41 preprocessorOptions: {
42 less: {
43 modifyVars: {
44 hack: `true; @import (reference) "${resolve('src/assets/style/breakpoint.less')}";`,
45 },
46 javascriptEnabled: true,
47 },
48 },
49 },
50 });
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 configStyleImportPlugin from './plugin/styleImport';
7 import configImageminPlugin from './plugin/imagemin';
8 import VitePluginOss from 'vite-plugin-oss';
9
10 const config = loadEnv('production', '');
11
12 const ossPath = `vendor/admin/asset${new Date()
13 .toLocaleString('zh')
14 .slice(0, 20)
15 .replace(/[\s/:]/g, '')}`;
16 export default mergeConfig(
17 {
18 mode: 'production',
19 base: config.VITE_OSS_HOST,
20 server: {
21 https: true,
22 },
23 plugins: [
24 configCompressPlugin('gzip'),
25 configVisualizerPlugin(),
26 configArcoResolverPlugin(),
27 configStyleImportPlugin(),
28 configImageminPlugin(),
29 VitePluginOss({
30 from: `./dist/${ossPath}/**`,
31 accessKeyId: config.VITE_OSS_ACCESS_KEY,
32 accessKeySecret: config.VITE_OSS_ACCESS_SECRET,
33 bucket: config.VITE_OSS_BUCKET,
34 region: config.VITE_OSS_REGION,
35 quitWpOnError: true,
36 deleteOrigin: true,
37 deleteEmptyDir: true,
38 }),
39 ],
40 build: {
41 assetsDir: ossPath,
42 rollupOptions: {
43 output: {
44 manualChunks: {
45 arco: ['@arco-design/web-vue'],
46 chart: ['echarts', 'vue-echarts'],
47 vue: ['vue', 'vue-router', 'pinia', '@vueuse/core', 'vue-i18n'],
48 },
49 },
50 },
51 chunkSizeWarningLimit: 2000,
52 },
53 },
54 baseConfig
55 );
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 https: true,
11 fs: {
12 strict: true,
13 },
14 },
15 plugins: [
16 eslint({
17 cache: false,
18 include: ['src/**/*.ts', 'src/**/*.tsx', 'src/**/*.vue'],
19 exclude: ['node_modules'],
20 }),
21 ],
22 define: {
23 __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: true,
24 },
25 },
26 baseConfig
27 );
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 configStyleImportPlugin from './plugin/styleImport';
7 import configImageminPlugin from './plugin/imagemin';
8
9 export default mergeConfig(
10 {
11 mode: 'production',
12 server: {
13 https: true,
14 },
15 plugins: [
16 configCompressPlugin('gzip'),
17 configVisualizerPlugin(),
18 configArcoResolverPlugin(),
19 configStyleImportPlugin(),
20 configImageminPlugin(),
21 ],
22 build: {
23 rollupOptions: {
24 output: {
25 manualChunks: {
26 arco: ['@arco-design/web-vue'],
27 chart: ['echarts', 'vue-echarts'],
28 vue: ['vue', 'vue-router', 'pinia', '@vueuse/core', 'vue-i18n'],
29 },
30 },
31 },
32 chunkSizeWarningLimit: 2000,
33 },
34 },
35 baseConfig
36 );
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>
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": "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 "prepare": "husky install"
17 },
18 "lint-staged": {
19 "*.{js,ts,jsx,tsx}": [
20 "prettier --write",
21 "eslint --fix"
22 ],
23 "*.vue": [
24 "stylelint --fix",
25 "prettier --write",
26 "eslint --fix"
27 ],
28 "*.{less,css}": [
29 "stylelint --fix",
30 "prettier --write"
31 ]
32 },
33 "dependencies": {
34 "@arco-design/web-vue": "^2.53.3",
35 "@microsoft/fetch-event-source": "^2.0.1",
36 "@vueuse/core": "^7.3.0",
37 "@vueuse/router": "^10.1.2",
38 "@wangeditor/editor": "^5.1.23",
39 "@wangeditor/editor-for-vue": "^5.1.12",
40 "ali-oss": "^6.17.1",
41 "arco-design-pro-vue": "^2.5.10",
42 "axios": "^1.6.5",
43 "dayjs": "^1.11.2",
44 "echarts": "^5.2.2",
45 "file-saver": "^2.0.5",
46 "js-file-download": "^0.4.12",
47 "lodash": "^4.17.21",
48 "mitt": "^3.0.0",
49 "nanoid": "^3.3.4",
50 "nprogress": "^0.2.0",
51 "pinia": "^2.0.9",
52 "query-string": "^7.0.1",
53 "rabbit-lyrics": "^2.1.1",
54 "vite-plugin-vue-setup-extend": "^0.4.0",
55 "vue": "^3.3.11",
56 "vue-echarts": "^6.0.0",
57 "vue-i18n": "^9.2.0-beta.17",
58 "vue-router": "^4.0.14",
59 "vue3-colorpicker": "^2.3.0",
60 "vue3-video-play": "1.3.1-beta.6"
61 },
62 "devDependencies": {
63 "@arco-design/web-vue": "^2.0.0-beta.7",
64 "@commitlint/cli": "^11.0.0",
65 "@commitlint/config-conventional": "^12.0.1",
66 "@types/ali-oss": "^6.16.3",
67 "@types/file-saver": "^2.0.5",
68 "@types/lodash": "^4.14.177",
69 "@types/nprogress": "^0.2.0",
70 "@typescript-eslint/eslint-plugin": "^5.10.0",
71 "@typescript-eslint/parser": "^5.10.0",
72 "@vitejs/plugin-vue": "^1.9.4",
73 "@vitejs/plugin-vue-jsx": "^1.2.0",
74 "@vue/babel-plugin-jsx": "^1.1.1",
75 "cross-env": "^7.0.3",
76 "eslint": "8.22.0",
77 "eslint-config-airbnb-base": "^14.2.1",
78 "eslint-config-prettier": "^8.3.0",
79 "eslint-import-resolver-typescript": "^2.4.0",
80 "eslint-plugin-import": "^2.22.1",
81 "eslint-plugin-prettier": "^3.3.1",
82 "eslint-plugin-vue": "^8.3.0",
83 "husky": "^7.0.4",
84 "less": "^4.1.2",
85 "lint-staged": "^11.2.6",
86 "prettier": "^2.2.1",
87 "rollup-plugin-visualizer": "^5.6.0",
88 "stylelint": "^13.8.0",
89 "stylelint-config-prettier": "^8.0.2",
90 "stylelint-config-rational-order": "^0.1.2",
91 "stylelint-config-standard": "^20.0.0",
92 "stylelint-order": "^4.1.0",
93 "typescript": "^4.4.4",
94 "unplugin-vue-components": "^0.19.3",
95 "vite": "^2.6.4",
96 "vite-plugin-compression": "^0.5.1",
97 "vite-plugin-eslint": "^1.3.0",
98 "vite-plugin-imagemin": "^0.6.1",
99 "vite-plugin-oss": "^1.2.13",
100 "vite-plugin-style-import": "1.4.1",
101 "vite-svg-loader": "^3.1.0",
102 "vue-tsc": "^0.34.15"
103 },
104 "resolutions": {
105 "bin-wrapper": "npm:bin-wrapper-china",
106 "rollup": "^2.56.3",
107 "gifsicle": "5.2.0"
108 },
109 "peerDependencies": {
110 "@arco-design/web-vue": ">=2.0.0-beta.7",
111 "snabbdom": "^3.5.1"
112 },
113 "volta": {
114 "node": "18.3.0",
115 "yarn": "1.22.19"
116 }
117 }
...\ No newline at end of file ...\ No newline at end of file
1 <template>
2 <a-config-provider size="small">
3 <router-view />
4 </a-config-provider>
5 </template>
6
7 <script lang="ts" setup></script>
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>
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;
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
23 /* Note: backdrop-filter has minimal browser support */
24
25 border-radius: 6px !important;
26 backdrop-filter: blur(10px) !important;
27
28 .content-panel {
29 display: flex;
30 justify-content: space-between;
31 width: auto;
32 height: 32px;
33 margin-bottom: 4px;
34 padding: 0 9px;
35 line-height: 32px;
36 background: rgba(255, 255, 255, 0.8);
37 border-radius: 4px;
38 box-shadow: 6px 0 20px rgba(34, 87, 188, 0.1);
39 }
40
41 .tooltip-title {
42 margin: 0 0 10px 0;
43 }
44
45 p {
46 margin: 0;
47 }
48
49 .tooltip-title,
50 .tooltip-value {
51 display: flex;
52 align-items: center;
53 color: #1d2129;
54 font-weight: bold;
55 font-size: 13px;
56 line-height: 15px;
57 text-align: right;
58 }
59
60 .tooltip-value {
61 margin-left: 10px;
62 }
63
64 .tooltip-item-icon {
65 display: inline-block;
66 width: 10px;
67 height: 10px;
68 margin-right: 8px;
69 border-radius: 50%;
70 }
71 }
72
73 .general-card {
74 border: none;
75 border-radius: 4px;
76
77 > .arco-card-header {
78 border: none;
79
80 > .arco-card-header-title {
81 font-size: 14px;
82 color: var(--color-text-2);
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 width: 6px;
99 height: 6px;
100 margin-right: 4px;
101 background-color: rgb(var(--blue-6));
102 border-radius: 50%;
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-20 {
119 margin-top: 20px;
120 }
121
122 .mb-0 {
123 margin-bottom: 0 !important;
124 }
125
126 .mb-8 {
127 margin-bottom: 8px !important;
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 width: 860px !important;
141
142 div.arco-modal-header {
143 border-bottom: unset !important;
144 }
145
146 div.arco-modal-body {
147 padding: 0 24px 20px;
148 }
149 }
150
151
152 .link-hover:hover {
153 cursor: pointer !important;
154 }
This diff could not be displayed because it is too large.
1 <template>
2 <audio
3 v-if="url"
4 v-bind="$attrs"
5 :id="uuid"
6 ref="audioRef"
7 :src="url"
8 class="player"
9 controls
10 controlsList="nodownload noplaybackrate"
11 @play="onPay"
12 />
13 </template>
14
15 <script lang="ts" setup>
16 import { nanoid } from 'nanoid';
17 // import FileSaver from 'file-saver';
18 import { computed, ref } from 'vue';
19
20 const audioRef = ref({});
21
22 defineProps<{ url: string }>();
23
24 const uuid = computed(() => {
25 return `audio-${nanoid()}`;
26 });
27
28 const onPay = (e: any) => {
29 const audios = document.getElementsByTagName('audio');
30 [].forEach.call(audios, (i: HTMLAudioElement) => i !== e.target && i.pause());
31 };
32
33 const getRef = (): HTMLAudioElement => {
34 return audioRef.value as HTMLAudioElement;
35 };
36
37 defineExpose({ getRef });
38 </script>
39
40 <style lang="less" scoped>
41 .player {
42 height: 30px;
43 width: 100%;
44 outline: none;
45 display: flex;
46 }
47
48 audio {
49 &::-webkit-media-controls-volume-control-container {
50 display: none !important;
51 }
52
53 &::-webkit-media-controls-rewind-button {
54 display: none !important;
55 }
56 }
57 </style>
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>
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.name">
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>
1 <template>
2 <VCharts v-if="renderChart" :option="options" :autoresize="autoresize" :style="{ width, height }" />
3 </template>
4
5 <script lang="ts">
6 import { computed, defineComponent, nextTick, ref } 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>
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(defineProps<{ label?: string; onDownload: () => Promise<void> }>(), { label: '导出' });
15
16 const { loading, setLoading } = useLoading(false);
17
18 const onClick = () => {
19 setLoading(true);
20 props.onDownload().finally(() => setLoading(false));
21 };
22 </script>
23
24 <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 { isString, isUndefined } from '@/utils/is';
5 import { get } from 'lodash';
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 userIndex?: string;
18 hideSubTitle?: boolean;
19 }>(),
20 {
21 coverIndex: 'cover',
22 nameIndex: 'song_name',
23 subIndex: 'sub_title',
24 tagIndex: 'tags',
25 projectIndex: 'project',
26 userIndex: 'user',
27 }
28 );
29
30 const getRow = (record: TableData): TableData | undefined => {
31 if (isUndefined(props.row)) {
32 return record;
33 }
34
35 if (isString(props.row)) {
36 return get(record, props.row);
37 }
38
39 return props.row;
40 };
41
42 const getRowColumn = (record: TableData, key: string) => getRow(record)?.[key];
43 </script>
44
45 <template>
46 <TableColumn v-bind="$attrs" :title="title" :data-index="dataIndex">
47 <template #default="{ record }">
48 <Layout>
49 <LayoutSider :width="104" class="left">
50 <Image show-loader :height="100" :width="100" fit="cover" :src="getRowColumn(record, coverIndex)" />
51 </LayoutSider>
52 <LayoutContent>
53 <Form auto-label-width label-align="left" :model="record" style="height: 100%; justify-content: space-between">
54 <FormItem label="歌曲名称" :show-colon="true" row-class="mb-0">
55 <TypographyText :ellipsis="{ rows: 1, showTooltip: true }">
56 {{ getRowColumn(record, nameIndex) }}
57 </TypographyText>
58 </FormItem>
59 <FormItem v-if="!hideSubTitle" label="推荐语" :show-colon="true" row-class="mb-0">
60 <TypographyText :ellipsis="{ rows: 1, showTooltip: true }">
61 {{ getRowColumn(record, subIndex) }}
62 </TypographyText>
63 </FormItem>
64 <FormItem label="歌曲标签" :show-colon="true" row-class="mb-0">
65 <TypographyText :ellipsis="{ rows: 1, showTooltip: true }">
66 {{
67 getRowColumn(record, tagIndex)
68 .map((item: any) => item.name)
69 .join('、')
70 }}
71 </TypographyText>
72 </FormItem>
73 <FormItem v-if="getRowColumn(record, projectIndex)" label="关联厂牌" :show-colon="true" row-class="mb-0">
74 <TypographyText :ellipsis="{ rows: 1, showTooltip: true }">
75 {{ getRowColumn(record, projectIndex)?.name }}
76 </TypographyText>
77 </FormItem>
78 <FormItem v-else label="关联用户" :show-colon="true" row-class="mb-0">
79 <TypographyText :ellipsis="{ rows: 1, showTooltip: true }">
80 {{ getRowColumn(record, userIndex)?.nick_name }}
81 </TypographyText>
82 </FormItem>
83 </Form>
84 </LayoutContent>
85 </Layout>
86 </template>
87 </TableColumn>
88 </template>
89
90 <style lang="less" scoped>
91 :deep(.arco-typography) {
92 margin-bottom: 0;
93 width: 100%;
94 text-align: left;
95 }
96
97 .left {
98 margin-top: 5px;
99 box-shadow: unset;
100 margin-right: 5px;
101 }
102 </style>
1 <script setup lang="ts">
2 import { Avatar } from '@arco-design/web-vue';
3 import TableColumn from '@/components/filter/table-column.vue';
4 import { get } from 'lodash';
5
6 const props = defineProps<{ title?: string; dataIndex?: string; width?: 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" :width="width || 60" :tooltip="false">
15 <template #default="{ record }">
16 <Avatar :size="40" shape="circle" :image-url="getValue(record)" />
17 </template>
18 </TableColumn>
19 </template>
20
21 <style scoped lang="less"></style>
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>
1 <script setup lang="ts">
2 import { Option } 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: Option[]; 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>
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>
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 const getValue = (record: object) => {
10 return get(record, props.dataIndex || '', '');
11 };
12 </script>
13
14 <template>
15 <TableColumn v-bind="$attrs" :title="title" :data-index="dataIndex">
16 <template #default="{ record }">
17 <template v-if="darkValue !== undefined && eq(getValue(record), darkValue)">
18 <span style="color: rgba(44, 44, 44, 0.5)">{{ `${prefix} 0 ${suffix}` }}</span>
19 </template>
20 <template v-else> {{ `${prefix} ${getValue(record)} ${suffix}` }} </template>
21 </template>
22 </TableColumn>
23 </template>
24
25 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { get } from 'lodash';
3 import TableColumn from '@/components/filter/table-column.vue';
4
5 defineProps<{ title?: string; dataIndex?: string; areaIndex?: string }>();
6
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 }"> {{ `(+${getValue(record, areaIndex)}) ${getValue(record, dataIndex)}` }}</template>
15 </TableColumn>
16 </template>
17
18 <style scoped lang="less"></style>
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>
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>
1 <script setup lang="ts">
2 import { Space } 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 }">
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>
1 <script setup lang="ts">
2 import TableColumn from '@/components/filter/table-column.vue';
3
4 const props = withDefaults(
5 defineProps<{
6 title?: string;
7 }>(),
8 {}
9 );
10 </script>
11
12 <template>
13 <TableColumn v-bind="$attrs" :title="title">
14 <template #default="{ record }">
15 <div class="box">
16 <a-image height="60" fit="fill" :src="record.icon" />
17 </div>
18 </template>
19 </TableColumn>
20 </template>
21
22 <style scoped lang="less">
23 .box {
24 height: 60px;
25 }
26 </style>
1 <script setup lang="ts">
2 import TableColumn from '@/components/filter/table-column.vue';
3
4 const props = withDefaults(
5 defineProps<{
6 title?: string;
7 }>(),
8 {}
9 );
10
11 const getIcon = (record): string => {
12 return record.frame?.url;
13 };
14
15 const getBorder = (record): string => {
16 return record.frame?.color;
17 };
18 </script>
19
20 <template>
21 <TableColumn v-bind="$attrs" :title="title">
22 <template #default="{ record }">
23 <div v-if="record.frame" class="box" :style="{ border: `2px solid ${getBorder(record)}` }">
24 <div class="iconBox">
25 <a-image height="22" fit="fill" style="margin-top: -2px" :src="getIcon(record)" />
26 </div>
27 </div>
28 <div v-else class="boxNull"></div>
29 </template>
30 </TableColumn>
31 </template>
32
33 <style scoped lang="less">
34 .box {
35 width: 50px;
36 height: 50px;
37 border-radius: 50%;
38 background-color: #f2f3f5;
39 position: relative;
40
41 .iconBox {
42 display: flex;
43 align-items: center;
44 height: 22px;
45 position: absolute;
46 right: 0;
47 bottom: 0;
48 z-index: 1;
49 overflow: hidden;
50 }
51 }
52
53 .boxNull {
54 width: 50px;
55 height: 50px;
56 display: flex;
57 align-items: center;
58 justify-content: center;
59 }
60 </style>
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>
1 <script setup lang="ts">
2 import { computed, ref } from 'vue';
3 import usePagination from '@/hooks/pagination';
4 import { Table, Row, Col } from '@arco-design/web-vue';
5 import { AnyObject, sizeType } from '@/types/global';
6
7 type PropType = {
8 loading?: boolean;
9 rowKey?: string;
10 hoverType?: string;
11 hidePage?: boolean;
12 simplePage?: boolean;
13 pageSize?: number;
14 size?: sizeType;
15 onQuery: (params?: AnyObject) => Promise<any>;
16 };
17
18 defineEmits<{
19 (e: 'rowSort', dataIndex: string, direction: string): void;
20 }>();
21
22 const props = withDefaults(defineProps<PropType>(), { loading: false, rowKey: 'id', size: 'small', pageSize: 20 });
23 const { pagination, setPage, setPageSize, setTotal } = usePagination({
24 simple: props.simplePage,
25 size: props.size,
26 pageSize: props.pageSize,
27 });
28 const list = ref<any[]>([]);
29 const tableRef = ref();
30
31 const pageQueryParams = computed(() => {
32 return props.hidePage ? {} : { page: pagination.value.current, pageSize: pagination.value.pageSize };
33 });
34
35 const hoverType = computed(() => props.hoverType || 'default');
36
37 const onFetch = () =>
38 props.onQuery(pageQueryParams.value).then(({ data, meta }) => {
39 list.value = data;
40 setPage(props.hidePage ? 1 : meta.current);
41 setTotal(props.hidePage ? data.length : meta.total);
42 setPageSize(props.hidePage ? pagination.value.pageSize : meta.limit);
43 });
44
45 const onPageChange = (page: number) => {
46 setPage(page || 1);
47 onFetch();
48 };
49
50 const onSizeChange = (size: number) => {
51 setPageSize(size);
52 setPage(1);
53 onFetch();
54 };
55
56 const onPush = (row: unknown) => {
57 list.value.unshift(row);
58 // eslint-disable-next-line no-plusplus
59 ++pagination.value.total;
60 };
61
62 const onRemove = (index: number) => {
63 list.value.splice(index, 1);
64 // eslint-disable-next-line no-plusplus
65 --pagination.value.total;
66 };
67
68 const resetSort = () => tableRef.value?.clearSorters();
69
70 const getPagination = () => pagination.value;
71
72 const getCount = () => pagination.value.total;
73
74 const getList = () => list.value;
75
76 const formatSortType = (type: string): string => type?.replace('end', '') ?? '';
77
78 defineExpose({ onFetch, onPageChange, onSizeChange, resetSort, getCount, getPagination, getList, onPush, onRemove });
79 </script>
80
81 <template>
82 <Row justify="space-between" align="center">
83 <Col class="table-tool-item" :span="16" style="text-align: left">
84 <slot name="tool" :size="size" />
85 </Col>
86 <Col class="table-tool-item" :span="8" style="text-align: right">
87 <slot name="tool-right" :size="size" />
88 </Col>
89 </Row>
90 <Table
91 ref="tableRef"
92 v-bind="$attrs"
93 :row-key="rowKey as string"
94 :loading="loading as boolean"
95 :size="size as sizeType"
96 :data="list"
97 :pagination="hidePage ? false : pagination"
98 :bordered="false"
99 :table-layout-fixed="true"
100 @page-change="onPageChange"
101 @page-size-change="onSizeChange"
102 @sorter-change="(dataIndex:string, direction:string) => $emit('rowSort', dataIndex, formatSortType(direction))"
103 >
104 <template #columns>
105 <slot />
106 </template>
107 </Table>
108 </template>
109
110 <style lang="less" scoped>
111 :deep(.arco-table-cell) {
112 padding: 5px 8px !important;
113
114 :hover {
115 cursor: v-bind(hoverType);
116 }
117
118 & > .arco-table-td-content .arco-btn-size-small {
119 padding: 5px !important;
120 }
121 }
122
123 :deep(.table-tool-item) {
124 > * {
125 margin-bottom: 12px !important;
126 }
127 }
128 </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 = defineProps<{ title?: string; dataIndex?: string }>();
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">
14 <template #default="{ record }">
15 <span v-if="eq(getValue(record), 1)">音乐人</span>
16 <span v-else-if="[2, 3].includes(Number(getValue(record)))">经纪人</span>
17 <span v-else style="color: rgba(44, 44, 44, 0.5)">未认证</span>
18 </template>
19 </TableColumn>
20 </template>
21
22 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { Avatar, Link } from '@arco-design/web-vue';
3 import TableColumn from '@/components/filter/table-column.vue';
4 import { User } from '@/utils/model';
5 import { isString, isUndefined } from '@/utils/is';
6 import openNewTab from '@/utils/route-blank';
7 import { get } from 'lodash';
8 import { useRouter } from 'vue-router';
9
10 const props = withDefaults(
11 defineProps<{
12 title?: string;
13 dataIndex?: string;
14 user?: string | Pick<User, 'id' | 'nick_name' | 'real_name' | 'identity'>;
15 showHref?: boolean;
16 showRealName?: boolean;
17 showAvatar?: boolean;
18 darkValue?: string;
19 nickIndex?: string;
20 realIndex?: string;
21 avatarIndex?: string;
22 roleIndex?: string;
23 linkStyle?: object;
24 prefix?: string;
25 }>(),
26 {
27 dataIndex: 'id',
28 nickIndex: 'nick_name',
29 realIndex: 'real_name',
30 avatarIndex: 'avatar',
31 roleIndex: 'identity',
32 prefix: '',
33 }
34 );
35
36 const router = useRouter();
37
38 const getUser = (record: object): Pick<User, 'id' | 'nick_name' | 'real_name' | 'identity'> | undefined => {
39 if (isUndefined(props.user)) {
40 return record as User;
41 }
42
43 if (isString(props.user)) {
44 return get(record, props.user);
45 }
46
47 return get(props, 'user') as User;
48 };
49
50 const getName = (record: object): string => {
51 const user = getUser(record);
52
53 if (!user) {
54 return '';
55 }
56
57 if (props.showRealName) {
58 return `${props.prefix}${get(user, props.nickIndex, '')}(${get(user, props.realIndex)})`;
59 }
60
61 return props.prefix + get(user, props.nickIndex, '');
62 };
63
64 const onHref = (record: object) => {
65 const user = getUser(record);
66 const key = get(record, props.dataIndex);
67 const identity = get(user, props.roleIndex);
68
69 if (identity === 1) {
70 // return router.push({ name: 'user-singer-show', params: { id: key } });
71 openNewTab(router, 'user-singer-show', { id: key });
72 }
73 if ([2, 3].includes(Number(identity))) {
74 // return router.push({ name: 'user-business-show', params: { id: key } });
75 openNewTab(router, 'user-business-show', { id: key });
76 }
77 // return router.push({ name: 'user-register-show', params: { id: key } });
78 openNewTab(router, 'user-register-show', { id: key });
79 };
80 </script>
81
82 <template>
83 <TableColumn v-bind="$attrs" :title="title" :data-index="dataIndex">
84 <template #default="{ record }">
85 <template v-if="darkValue !== undefined && getName(record) === darkValue">
86 <span style="color: rgba(44, 44, 44, 0.5)"></span>
87 </template>
88 <template v-else>
89 <Avatar v-if="showAvatar" style="margin-right: 8px" :size="34" shape="circle" :image-url="getUser(record)?.[avatarIndex]" />
90 <Link v-if="showHref" class="link-hover" :style="linkStyle" :hoverable="false" @click.stop="onHref(record)">
91 {{ getName(record) }}
92 </Link>
93 <template v-else>{{ getName(record) }}</template>
94 </template>
95 </template>
96 </TableColumn>
97 </template>
98
99 <style scoped lang="less"></style>
1 <template>
2 <Button v-bind="$attrs" :type="type" :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'" :size="$attrs['size'] || 'small'" 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 label?: string;
20 icon?: string;
21 iconAlign?: 'left' | 'right';
22 type?: 'primary' | 'secondary' | 'outline' | 'dashed' | 'text';
23 shape?: 'square' | 'round' | 'circle';
24 status?: 'normal' | 'warning' | 'success' | 'danger';
25 }>(),
26 {
27 iconAlign: 'left',
28 type: 'secondary',
29 shape: 'square',
30 status: 'normal',
31 }
32 );
33
34 const emits = defineEmits(['click']);
35
36 const iconName = computed(() => upperFirst(camelCase(`icon-${props.icon}`)));
37 </script>
38
39 <style scoped></style>
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>
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 { DataZoomComponent, GraphicComponent, GridComponent, LegendComponent, TooltipComponent } from 'echarts/components';
6 import Chart from './chart/index.vue';
7 import Breadcrumb from './breadcrumb/index.vue';
8 import AvatarUpload from './avatar-upload/index.vue';
9 import InputUpload from './input-upload/index.vue';
10 import ImageUpload from './image-upload/index.vue';
11 import ExportButton from './export-button/index.vue';
12 import IconButton from './icon-button/index.vue';
13 import AudioPlayer from './audio-player/index.vue';
14 import ProjectSelect from './project-select/index.vue';
15 import UserLink from './user-link/index.vue';
16 import TagSelect from './tag-select/index.vue';
17 import UserSelect from './user-select/index.vue';
18 import PermissionSelect from './permission-select/index.vue';
19 import FilterSearch from './filter/search.vue';
20 import FilterSearchItem from './filter/search-item.vue';
21 import FilterTable from './filter/table.vue';
22 import FilterTableColumn from './filter/table-column.vue';
23 import PageView from './page-view/index.vue';
24
25 // import SvgIcon from './svg-icon/index.vue';
26
27 // Manually introduce ECharts modules to reduce packing size
28
29 use([
30 CanvasRenderer,
31 BarChart,
32 LineChart,
33 PieChart,
34 RadarChart,
35 GridComponent,
36 TooltipComponent,
37 LegendComponent,
38 DataZoomComponent,
39 GraphicComponent,
40 ]);
41
42 export default {
43 install(Vue: App) {
44 Vue.component('Chart', Chart);
45 Vue.component('Breadcrumb', Breadcrumb);
46 Vue.component('AvatarUpload', AvatarUpload);
47 Vue.component('InputUpload', InputUpload);
48 Vue.component('ImageUpload', ImageUpload);
49 Vue.component('ExportButton', ExportButton);
50 Vue.component('IconButton', IconButton);
51 Vue.component('AudioPlayer', AudioPlayer);
52 Vue.component('ProjectSelect', ProjectSelect);
53 Vue.component('UserLink', UserLink);
54 Vue.component('TagSelect', TagSelect);
55 Vue.component('UserSelect', UserSelect);
56 Vue.component('PermissionSelect', PermissionSelect);
57 Vue.component('FilterSearch', FilterSearch);
58 Vue.component('FilterSearchItem', FilterSearchItem);
59 Vue.component('FilterTable', FilterTable);
60 Vue.component('FilterTableColumn', FilterTableColumn);
61 Vue.component('PageView', PageView);
62 // Vue.component('SvgIcon', SvgIcon);
63 },
64 };
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>
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" dot>
11 <a-card v-if="hasCard" :bordered="false" v-bind="$attrs">
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>
1 <template>
2 <TreeSelect
3 :model-value="modelValue"
4 :data="permissionData"
5 :field-names="fieldName"
6 :placeholder="placeholder"
7 :allow-clear="allowClear"
8 :allow-search="allowSearch"
9 :tree-props="treeProp"
10 :filter-tree-node="onFilter"
11 :fallback-option="false"
12 :path-mode="true"
13 @update:model-value="onUpdate"
14 />
15 </template>
16
17 <script lang="ts" setup>
18 import { TreeNodeData, TreeSelect } from '@arco-design/web-vue';
19 import { computed, toRefs } from 'vue';
20 import { SystemPermission } from '@/types/system-permission';
21 import { isUndefined } from '@/utils/is';
22 import { useAppStore } from '@/store';
23 import { storeToRefs } from 'pinia';
24 import { arrayToTree } from '@/utils';
25 import { AnyObject } from '@/types/global';
26
27 type PropType = {
28 modelValue?: number | string | number[];
29 guard?: 'Admin' | 'Manage' | '';
30 allowClear?: boolean;
31 allowSearch?: boolean;
32 placeholder?: string;
33 };
34
35 const props = withDefaults(defineProps<PropType>(), {
36 allowClear: false,
37 allowSearch: false,
38 placeholder: '请选择',
39 modelValue: '',
40 guard: '',
41 });
42
43 const { guard } = toRefs(props);
44
45 const emits = defineEmits<{ (e: 'update:modelValue', value: number | string): void }>();
46 const fieldName = { key: 'name', title: 'label', children: 'children', icon: 'icm' };
47 const treeProp = { virtualListProps: { height: 300 } };
48 const appStore = useAppStore();
49 const { permissions } = storeToRefs(appStore);
50
51 const onUpdate = (val: number | string | undefined) => emits('update:modelValue', isUndefined(val) ? '' : val);
52 const onFilter = (searchKey: string, nodeData: SystemPermission) => nodeData.label.indexOf(searchKey) > -1;
53
54 const permissionData = computed(() => {
55 const list = permissions?.value?.filter((item) => item.type === 'Menu' && item.guard === guard.value) as AnyObject[];
56 return arrayToTree(list) as TreeNodeData[];
57 });
58 </script>
59
60 <style scoped></style>
1 <template>
2 <Select
3 v-bind="$attrs"
4 v-model="formValue"
5 :options="options"
6 :fallback-option="false"
7 :field-names="{ value: 'id', label: 'name' }"
8 :placeholder="placeholder"
9 :allow-search="true"
10 />
11 </template>
12
13 <script lang="ts" setup>
14 import { Select } from '@arco-design/web-vue';
15 import { useSelectionStore } from '@/store';
16 import { useVModels } from '@vueuse/core';
17 import { storeToRefs } from 'pinia';
18 import { computed } from 'vue';
19 import { filter } from 'lodash';
20
21 type propType = { modelValue?: number | string | number[]; placeholder?: string; filtrate?: (value: any) => boolean };
22
23 const props = withDefaults(defineProps<propType>(), {
24 filtrate: () => true,
25 placeholder: '请选择',
26 });
27 const emits = defineEmits(['update:modelValue', 'update:loading']);
28
29 const { getProjectOptions } = storeToRefs(useSelectionStore());
30
31 const options = computed(() => filter(getProjectOptions.value, props.filtrate));
32
33 const { modelValue: formValue } = useVModels(props, emits);
34 </script>
35
36 <style lang="less" scoped></style>
1 <template>
2 <a-button v-if="type === 'Button'" type="text" size="small" :disabled="disabled as boolean" @click="onClick">
3 <slot>详情</slot>
4 </a-button>
5 <a-link v-else class="link" :disabled="disabled as boolean" :hoverable="false" @click="onClick">
6 <a-typography-text
7 type="primary"
8 :disabled="disabled"
9 style="margin-bottom: 0 !important"
10 :ellipsis="{ rows: 1, showTooltip: showTooltip }"
11 >
12 <slot>详情</slot>
13 </a-typography-text>
14 </a-link>
15 </template>
16
17 <script lang="ts" setup>
18 import { RouteParamsRaw, useRouter } from 'vue-router';
19 import { AnyObject } from '@/types/global';
20
21 const props = withDefaults(
22 defineProps<{
23 name?: string;
24 params?: AnyObject;
25 type?: 'Button' | 'link';
26 showTooltip?: boolean;
27 disabled?: boolean;
28 }>(),
29 {
30 name: '',
31 type: 'Button',
32 params: undefined,
33 showTooltip: true,
34 disabled: false,
35 }
36 );
37
38 const router = useRouter();
39 const onClick = () => {
40 router.push({
41 name: props.name,
42 params: (props.params as RouteParamsRaw) || {},
43 });
44 };
45 </script>
46
47 <style lang="less" scoped>
48 .link {
49 width: 100%;
50 padding: 1px 0;
51 display: block !important;
52 }
53
54 .link:hover {
55 cursor: pointer;
56 }
57 </style>
1 <template>
2 <Select
3 v-bind="$attrs"
4 v-model="formValue"
5 :options="activityTagOptions"
6 :fallback-option="false"
7 :placeholder="placeholder"
8 :field-names="{ value: 'id', label: 'name' }"
9 @exceed-limit="onTagExceedLimitError"
10 />
11 </template>
12
13 <script lang="ts" setup>
14 import { Message, Select } from '@arco-design/web-vue';
15 import { computed, onMounted } from 'vue';
16 import { useSelectionStore } from '@/store';
17 import { isArray } from '@/utils/is';
18 import { useVModels } from '@vueuse/core';
19
20 type propType = { modelValue?: number | string | number[]; placeholder?: string; limitErrorMsg?: string };
21 const props = withDefaults(defineProps<propType>(), { placeholder: '请选择', limitErrorMsg: '超出最大选中数' });
22 const emits = defineEmits(['update:modelValue', 'update:loading']);
23
24 const { modelValue: formValue } = useVModels(props, emits);
25
26 const { activityTagOptions } = useSelectionStore();
27
28 const tagIds = computed(() => activityTagOptions.map((item) => item.id));
29
30 const onTagExceedLimitError = () => Message.warning({ content: props.limitErrorMsg, duration: 1500 });
31
32 onMounted(() => {
33 if (isArray(props.modelValue)) {
34 emits('update:modelValue', props.modelValue.filter((item: number) => tagIds.value?.indexOf(item) !== -1) || []);
35 }
36 });
37 </script>
38
39 <style scoped></style>
1 <template>
2 <Link v-if="user" :hoverable="false" class="link" @click.stop="onClick">
3 <TypographyParagraph type="primary" class="name" :ellipsis="{ rows: 1, showTooltip }">
4 <slot>{{ fullName }}</slot>
5 </TypographyParagraph>
6 </Link>
7 </template>
8
9 <script lang="ts" setup>
10 import { computed, toRefs } from 'vue';
11 import { useRouter } from 'vue-router';
12 import { BaseUser } from '@/utils/model';
13 import { Link, TypographyParagraph } from '@arco-design/web-vue';
14
15 type PropType = { user?: BaseUser; showTooltip?: boolean };
16
17 const props = withDefaults(defineProps<PropType>(), { showTooltip: true });
18 const { user } = toRefs(props);
19 const router = useRouter();
20
21 const fullName = computed(() => `${user?.value?.nick_name}`);
22
23 const onClick = () => {
24 if (user?.value?.identity === 1) {
25 return router.push({ name: 'user-singer-show', params: { id: user?.value?.id } });
26 }
27 if ([2, 3].includes(Number(user?.value?.identity))) {
28 return router.push({ name: 'user-business-show', params: { id: user?.value?.id } });
29 }
30 return router.push({ name: 'user-register-show', params: { id: user?.value?.id } });
31 };
32 </script>
33
34 <style lang="less" scoped>
35 .link {
36 display: block !important;
37 width: 100%;
38 padding: 1px 0;
39
40 .name {
41 margin-bottom: 0 !important;
42 min-width: 26px;
43 width: 100%;
44 }
45
46 :hover {
47 cursor: pointer !important;
48 }
49 }
50 </style>
1 <template>
2 <!-- :virtual-list-props="{ height: 240 }"-->
3
4 <Select
5 v-bind="$attrs"
6 v-model="formValue"
7 :fallback-option="false"
8 :field-names="{ value: 'id', label: 'nick_name' }"
9 :options="options"
10 :placeholder="placeholder"
11 :virtual-list-props="{ height: '240px' }"
12 @exceed-limit="onExceedLimitError"
13 >
14 <template #option="{ data }">
15 <Avatar v-if="hasAvatar" :size="28" :image-url="data.avatar" style="margin-right: 8px" />
16 <span>{{ `${data.nick_name}(${data.real_name})` }}</span>
17 </template>
18 </Select>
19 </template>
20
21 <script lang="ts" setup>
22 import { Avatar, Message, Select } from '@arco-design/web-vue';
23
24 import { useSelectionStore } from '@/store';
25 import { storeToRefs } from 'pinia';
26 import { useVModels } from '@vueuse/core';
27 import { computed } from 'vue';
28 import { filter } from 'lodash';
29
30 type PropType = {
31 modelValue?: number | string | number[];
32 placeholder?: string;
33 limitErrorMsg?: string;
34 hasAvatar?: boolean;
35 filtrate?: (value: any) => boolean;
36 };
37 const { getUserOptions } = storeToRefs(useSelectionStore());
38
39 const props = withDefaults(defineProps<PropType>(), {
40 placeholder: '请选择',
41 limitErrorMsg: '超出最大选中数',
42 hasAvatar: true,
43 filtrate: () => true,
44 });
45 const emits = defineEmits(['update:modelValue', 'update:loading']);
46
47 const options = computed(() => filter(getUserOptions.value, props.filtrate));
48 const { modelValue: formValue } = useVModels(props, emits);
49
50 const onExceedLimitError = () => Message.warning({ content: props.limitErrorMsg, duration: 1500 });
51 </script>
52
53 <style scoped></style>
1 import { App } from 'vue';
2 import permission from './permission';
3
4 export default {
5 install(Vue: App) {
6 Vue.directive('permission', permission);
7 },
8 };
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 };
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 }
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 }
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 }
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 = 'file',
52 onProgress?: (percent: number) => void
53 ): Promise<{ url: string; name: string; response: unknown }> {
54 return createOssClient()
55 .multipartUpload(`${prefix}/${getFileDir()}/${getFileName()}.${getFileType(file)}`, file, {
56 progress: onProgress,
57 partSize: 5 * 1024 * 1024,
58 })
59 .then((res) => {
60 return Promise.resolve({ response: res.res, url: `${getHost()}/${res.name}`, name: `${getFileName()}.${getFileType(file)}` });
61 })
62 .catch(() => {
63 Message.error({ content: '上传失败' });
64 // eslint-disable-next-line prefer-promise-reject-errors
65 return Promise.reject(false);
66 });
67 },
68 };
69 }
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 }
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 }
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 }
1 import { computed } from 'vue';
2 import { useAppStore } from '@/store';
3
4 export default function useThemes() {
5 const appStore = useAppStore();
6 const isDark = computed(() => {
7 return appStore.theme === 'dark';
8 });
9 return {
10 isDark,
11 };
12 }
1 import { createVNode, h, ref } from 'vue';
2 import { FormInstance } from '@arco-design/web-vue/es/form';
3 import { Form, FormItem, InputPassword, Message, Modal } from '@arco-design/web-vue';
4 import useAuthApi from '@/http/auth';
5
6 export default function useUser() {
7 const getRoleLabel = (role?: 'Singer' | 'Business') => {
8 switch (role) {
9 case 'Business':
10 return '推荐人';
11 case 'Singer':
12 return '歌手';
13 default:
14 return '未知';
15 }
16 };
17
18 const getSexLabel = (sex: string | number) => {
19 switch (sex) {
20 case 0:
21 return '保密';
22 case 1:
23 return '男';
24 case 2:
25 return '女';
26 default:
27 return '未知';
28 }
29 };
30
31 // eslint-disable-next-line no-shadow
32 const resetPwd = () => {
33 const pwdRef = ref<FormInstance>();
34 const pwdVal = ref({ password: '', password_confirmation: '' });
35 const pwdRules = {
36 password: [{ required: true, message: '请输入密码' }],
37 password_confirmation: [{ required: true, message: '请输入密码' }],
38 };
39
40 const createPwdVNode = (label: string, field: 'password' | 'password_confirmation') =>
41 createVNode(
42 FormItem,
43 { label, field, rowClass: field === 'password_confirmation' ? 'mb-0' : '' },
44 {
45 default: () =>
46 h(InputPassword, {
47 'modelValue': pwdVal.value[field],
48 // eslint-disable-next-line no-return-assign
49 'onUpdate:modelValue': (val?: string) => (pwdVal.value[field] = val || ''),
50 }),
51 }
52 );
53
54 return Modal.open({
55 title: '设置密码',
56 content: () =>
57 h(
58 Form,
59 { ref: pwdRef, model: pwdVal, rules: pwdRules, autoLabelWidth: true },
60 { default: () => [createPwdVNode('新密码', 'password'), createPwdVNode('确认密码', 'password_confirmation')] }
61 ),
62 closable: false,
63 maskClosable: false,
64 escToClose: false,
65 // eslint-disable-next-line no-shadow
66 onBeforeOk: (done: (closed: boolean) => void) => {
67 useAuthApi
68 .changePwd(pwdVal.value)
69 .then(() => {
70 Message.success('更新成功');
71 done(true);
72 })
73 .catch(() => {
74 done(false);
75 });
76 },
77 onClose: () => {
78 pwdVal.value = { password: '', password_confirmation: '' };
79 },
80 });
81 };
82
83 return {
84 getRoleLabel,
85 getSexLabel,
86 resetPwd,
87 };
88 }
1 import { AnyObject, Option, QueryForParams, ServiceResponse } from '@/types/global';
2
3 import { Tag } from '@/types/tag';
4 import axios from 'axios';
5
6 export default class useTagApi {
7 static typeOption = [
8 { label: '歌曲标签', value: 1 },
9 { label: '身份标签', value: 2 },
10 { label: '声音标签', value: 3 },
11 { label: '认证标签', value: 4 },
12 ];
13
14 static showOption = [
15 { label: '显示', value: 1 },
16 { label: '隐藏', value: 0 },
17 ];
18
19 static filterOption = [
20 { label: '允许', value: 1 },
21 { label: '禁止', value: 0 },
22 ];
23
24 static certifyPermissionOption = [
25 { label: '听', value: 1 },
26 { label: '唱', value: 2 },
27 ];
28
29 static async get(params?: QueryForParams): Promise<ServiceResponse<Tag[]>> {
30 return axios.get('system/tags', { params });
31 }
32
33 static async create(data: AnyObject): Promise<Tag> {
34 return axios.post('system/tags', data).then((res) => Promise.resolve(res.data));
35 }
36
37 static async show(id: number, params?: QueryForParams): Promise<Tag> {
38 return axios.get(`system/tags/${id}`, { params }).then((res) => Promise.resolve(res.data));
39 }
40
41 static async update(id: number, data: AnyObject): Promise<Tag> {
42 return axios.put(`system/tags/${id}`, data).then((res) => Promise.resolve(res.data));
43 }
44
45 static async destroy(id: number) {
46 return axios.delete(`system/tags/${id}`).then((res) => Promise.resolve(res.data));
47 }
48
49 static async getOption(params?: QueryForParams): Promise<Option[]> {
50 return useTagApi.get({ ...params, fetchType: 'all' }).then(({ data }) =>
51 data?.map((item) => {
52 return { value: item.id, label: item.name };
53 })
54 );
55 }
56 }
1 // eslint-disable-next-line max-classes-per-file
2 import { AnyObject, AttributeData, QueryForParams, ServiceResponse } from '@/types/global';
3 import { Activity, ActivityApplyRecord, ActivityViewUser } from '@/types/activity';
4 import useUserApi from '@/http/user';
5 import axios, { AxiosRequestConfig } from 'axios';
6 import { UserManageActivity } from '@/utils/model';
7 import { ActivityApply } from '@/types/activity-apply';
8 import FileSaver from 'file-saver';
9 import { ActivityWork } from '@/types/activity-work';
10
11 export default class useActivityApi {
12 static statusOption = [
13 { label: '处理中', value: 0 },
14 { label: '已上架', value: 1 },
15 { label: '已下架', value: 2 },
16 { label: '已匹配', value: 3 },
17 { label: '已发行', value: 5 },
18 { label: '处理失败', value: 4 },
19 ];
20
21 static weightOption = [
22 { label: '无', value: 0 },
23 { label: '低', value: 30 },
24 { label: '中', value: 60 },
25 { label: '高', value: 90 },
26 ];
27
28 static workSingTypeOption = [
29 { label: '自主上传', value: 1 },
30 { label: '唱整首', value: 2 },
31 { label: '唱片段', value: 3 },
32 ];
33
34 static workSingStatusOption = [
35 { label: '待采纳', value: 0 },
36 { label: '已确认', value: 1 },
37 { label: '不合适', value: 2 },
38 { label: '未采纳', value: 3 },
39 { label: '其他', value: 4 },
40 ];
41
42 static songTypeOption = [
43 { label: '歌曲', value: 1 },
44 { label: 'Demo', value: 2 },
45 ];
46
47 static get(params?: QueryForParams): Promise<ServiceResponse<Activity[]>> {
48 return axios.get('audition/activities', { params });
49 }
50
51 static async getExport(fileName: string, params?: QueryForParams) {
52 const config = { params: { ...params, fetchType: 'excel' }, timeout: 60000, responseType: 'blob' } as AxiosRequestConfig;
53 return axios.get('audition/activities', config).then(({ data }) => FileSaver.saveAs(data, `${fileName}.xlsx`));
54 }
55
56 static async create(data: AttributeData): Promise<Activity> {
57 return axios.post('audition/activities', data).then((res) => Promise.resolve(res.data));
58 }
59
60 static async update(id: number, data: AttributeData): Promise<Activity> {
61 return axios.put(`audition/activities/${id}`, data).then((res) => Promise.resolve(res.data));
62 }
63
64 static async changeStatus(id: number, data: AttributeData) {
65 return axios.put<Activity>(`audition/activities/${id}/change-status`, data);
66 }
67
68 static async notify(id: number, data: AttributeData) {
69 return axios.post<Activity>(`audition/activities/${id}/notify`, data);
70 }
71
72 static async show(id: number, params?: QueryForParams): Promise<Activity> {
73 return axios.get(`/audition/activities/${id}`, { params }).then((res) => Promise.resolve(res.data));
74 }
75
76 static async destroy(id: number) {
77 return axios.delete(`/audition/activities/${id}`).then((res) => Promise.resolve(res.data));
78 }
79
80 static async viewUsers(activityId: number, params?: QueryForParams): Promise<ServiceResponse<ActivityViewUser[]>> {
81 return axios.get(`/audition/activities/${activityId}/views`, { params });
82 }
83
84 static async likeUsers(activityId: number, params?: QueryForParams): Promise<ServiceResponse<ActivityViewUser[]>> {
85 return axios.get(`/audition/activities/${activityId}/likes`, { params });
86 }
87
88 static async manageUsers(activityId: number, params?: QueryForParams): Promise<ServiceResponse<UserManageActivity[]>> {
89 return axios.get(`/audition/activities/${activityId}/managers`, { params });
90 }
91
92 static async submitUser(activityId: number, params?: QueryForParams): Promise<ServiceResponse<ActivityWork[]>> {
93 return axios.get(`/audition/activities/${activityId}/submits`, { params });
94 }
95
96 static async getSubmitUserExport(activityId: number, fileName: string, params: QueryForParams) {
97 const config = { params: { ...params, fetchType: 'excel' }, timeout: 60000, responseType: 'blob' } as AxiosRequestConfig;
98 return axios.get(`/audition/activities/${activityId}/submits`, config).then(({ data }) => FileSaver.saveAs(data, `${fileName}.xlsx`));
99 }
100
101 static async changeUserShare(activityId: number, data: AttributeData) {
102 return axios.put<Activity>(`/audition/activities/${activityId}/change-user-share`, data);
103 }
104
105 static async matchUser(activityId: number, params?: QueryForParams): Promise<ServiceResponse<ActivityWork[]>> {
106 return axios.get(`/audition/activities/${activityId}/matches`, { params });
107 }
108 }
109
110 export class useApplyApi {
111 static statusOption = [
112 { label: '审核不通过', value: 2 },
113 { label: '待审核', value: 0 },
114 ];
115
116 static async get(params?: QueryForParams): Promise<ServiceResponse<ActivityApply[]>> {
117 return axios.get('audition/applies', { params });
118 }
119
120 static async create(data: AnyObject): Promise<ActivityApply> {
121 return axios.post(`audition/applies`, data).then((res) => Promise.resolve(res.data));
122 }
123
124 static async audit(id: number, data: AttributeData) {
125 return axios.put(`audition/applies/${id}/audit`, data);
126 }
127
128 static async update(id: number, data: AttributeData) {
129 return axios.put(`audition/applies/${id}`, data);
130 }
131
132 static async record(id: number, params?: QueryForParams): Promise<ServiceResponse<ActivityApplyRecord[]>> {
133 return axios.get(`audition/applies/${id}/records`, { params });
134 }
135
136 static async destroy(id: number) {
137 return axios.delete(`audition/applies/${id}`);
138 }
139 }
140
141 export class useManagerApi {
142 static statusOption = useUserApi.statusOption;
143
144 static sexOption = useUserApi.sexOption;
145
146 static officialStatusOption = useUserApi.officialStatusOption;
147
148 static permissionOption = [
149 { label: '查看试唱', value: 'view' },
150 { label: '查看报价', value: 'price' },
151 { label: '回复结果', value: 'audit' },
152 ];
153
154 static get(params: QueryForParams): Promise<ServiceResponse<UserManageActivity[]>> {
155 return axios.get('audition/activity-managers', { params });
156 }
157
158 static async create(data: AnyObject) {
159 return axios.post('audition/activity-managers', data).then((res) => Promise.resolve(res.data));
160 }
161
162 static async update(id: number, data: AnyObject) {
163 return axios.put(`audition/activity-managers/${id}`, data).then((res) => Promise.resolve(res.data));
164 }
165
166 static async destroy(id: number) {
167 return axios.delete(`audition/activity-managers/${id}`).then((res) => Promise.resolve(res.data));
168 }
169 }
170
171 export class useReListApi {
172 static get(params?: QueryForParams): Promise<ServiceResponse> {
173 return axios.get(`audition/activity-relist-records`, { params });
174 }
175
176 static async getExport(fileName: string, params?: QueryForParams) {
177 const config = { params: { ...params, fetchType: 'excel' }, timeout: 60000, responseType: 'blob' } as AxiosRequestConfig;
178 return axios.get('audition/activity-relist-records', config).then(({ data }) => FileSaver.saveAs(data, `${fileName}.xlsx`));
179 }
180 }
181
182 export class useWorkApi {
183 static get(params?: QueryForParams): Promise<ServiceResponse<ActivityWork[]>> {
184 return axios.put(`audition/activity-works`, { params });
185 }
186
187 static changeStatus(workId: number, data: AttributeData) {
188 return axios.put(`audition/activity-works/${workId}/change-status`, data);
189 }
190 }
1 import { AuthorizedState } from '@/store/modules/authorized/type';
2 import FileSaver from 'file-saver';
3 import axios from 'axios';
4
5 interface LoginData {
6 type: 'email' | 'phone';
7 email?: string;
8 password?: string;
9 phone?: string;
10 code?: string;
11 area?: string;
12 }
13
14 export async function downloadFile(url: string, fileName: string) {
15 FileSaver.saveAs(`${url}`, fileName + url.substring(url.lastIndexOf('.')));
16
17 // ?response-content-type=Blob
18 }
19
20 export default class useAuthApi {
21 static async login(data: LoginData) {
22 return axios
23 .post<{ access_token: string; refresh_token: string; nick_name: string }>('login', data)
24 .then((res) => Promise.resolve(res.data));
25 }
26
27 static async info() {
28 return axios.get<{ user: AuthorizedState; permissions: string[] }>('auth/info').then((res) => Promise.resolve(res.data));
29 }
30
31 static changePwd(data: { password: string; password_confirmation: string }) {
32 return axios.put('auth/change-pwd', data);
33 }
34 }
1 import { AnyObject, QueryForParams, ServiceResponse } from '@/types/global';
2 import { Banner } from '@/utils/model';
3 import axios from 'axios';
4
5 export default class useBannerApi {
6 static roleOption = [
7 { value: 'UnLogin', label: '未登录' },
8 { value: 'Visitor', label: '游客' },
9 { value: 'Singer', label: '音乐人' },
10 { value: 'Business', label: '经纪人' },
11 { value: 'Project', label: '厂牌管理员' },
12 { value: 'System', label: '平台管理员' },
13 ];
14
15 static statusOption = [
16 { value: 0, label: '下架' },
17 { value: 1, label: '上架' },
18 ];
19
20 static scopeOption = [{ value: 1, label: '首页banner' }];
21
22 static typeOption = [
23 { value: 1, label: '普通' },
24 { value: 2, label: '外链' },
25 { value: 3, label: '交互' },
26 { value: 4, label: '自定义H5' },
27 ];
28
29 static get(params?: QueryForParams): Promise<ServiceResponse<Banner[]>> {
30 return axios.get('system/banners', { params });
31 }
32
33 static async create(data: AnyObject): Promise<Banner> {
34 return axios.post('system/banners', data).then((res) => Promise.resolve(res.data));
35 }
36
37 static async update(id: number, data: AnyObject): Promise<Banner> {
38 return axios.put(`system/banners/${id}`, data).then((res) => Promise.resolve(res.data));
39 }
40
41 static async changeStatus(id: number, data: { status: number }) {
42 return axios.put(`system/banners/${id}/change-status`, data);
43 }
44
45 static async destroy(id: number) {
46 return axios.delete(`system/banners/${id}`).then((res) => Promise.resolve(res.data));
47 }
48 }
1 import { AnyObject, QueryForPaginationParams, QueryForParams, ServiceResponse } from '@/types/global';
2 import { User } from '@/utils/model';
3 import axios, { AxiosRequestConfig } from 'axios';
4 import FileSaver from 'file-saver';
5
6 export default class useBrokerApi {}
7
8 export interface BrokerUserConfigItem {
9 user_id: number;
10 user?: User;
11 identifier: '';
12 singer_count: 0;
13 status: 0 | 1;
14 }
15
16 export interface BrokerUserConfig {
17 id: number;
18 title: string;
19 begin_at: string;
20 end_at: string;
21 push_type: number;
22 push_at: string;
23 user_id: number;
24 user?: User;
25 items?: BrokerUserConfigItem[];
26 }
27
28 export class useUserConfigApi {
29 static get(params?: QueryForParams | QueryForPaginationParams): Promise<ServiceResponse<BrokerUserConfig[]>> {
30 return axios.get('system/broker/user-configs', { params });
31 }
32
33 static async create(data: AnyObject): Promise<BrokerUserConfig> {
34 return axios.post('system/broker/user-configs', data).then((res) => Promise.resolve(res.data));
35 }
36
37 static async createLevel(id: number, data: AnyObject) {
38 return axios.post(`system/broker/user-configs/${id}/level`, data).then((res) => Promise.resolve(res.data));
39 }
40
41 static async show(id: number): Promise<BrokerUserConfig> {
42 return axios.get(`system/broker/user-configs/${id}`).then((res) => Promise.resolve(res.data));
43 }
44
45 static async update(id: number, data: AnyObject): Promise<BrokerUserConfig> {
46 return axios.put(`system/broker/user-configs/${id}`, data).then((res) => Promise.resolve(res.data));
47 }
48
49 static async destroy(id: number) {
50 return axios.delete(`system/broker/user-configs/${id}`).then((res) => Promise.resolve(res.data));
51 }
52 }
53
54 export interface BrokerPushConfig {
55 id: number;
56 identifier: string;
57 title: string;
58 intro: string;
59 match_title: string;
60 match_intro: string;
61 user_id: number;
62 user?: User;
63 }
64
65 export class usePushConfigApi {
66 static get(params?: QueryForParams | QueryForPaginationParams): Promise<ServiceResponse<BrokerPushConfig[]>> {
67 return axios.get('system/broker/push-configs', { params });
68 }
69
70 static async create(data: AnyObject): Promise<BrokerPushConfig> {
71 return axios.post('system/broker/push-configs', data).then((res) => Promise.resolve(res.data));
72 }
73
74 static async update(id: number, data: AnyObject): Promise<BrokerPushConfig> {
75 return axios.put(`system/broker/push-configs/${id}`, data).then((res) => Promise.resolve(res.data));
76 }
77
78 static async destroy(id: number) {
79 return axios.delete(`system/broker/push-configs/${id}`).then((res) => Promise.resolve(res.data));
80 }
81 }
82
83 export class usePushMatchRecordApi {
84 static get(params?: QueryForParams | QueryForPaginationParams): Promise<ServiceResponse> {
85 return axios.get('system/broker/push-match-records', { params });
86 }
87
88 static async getExport(fileName: string, params?: QueryForParams) {
89 const config = { params: { ...params, fetchType: 'excel' }, timeout: 60000, responseType: 'blob' } as AxiosRequestConfig;
90 return axios.get('system/broker/push-match-records', config).then(({ data }) => FileSaver.saveAs(data, `${fileName}.xlsx`));
91 }
92
93 static async rollback(recordId: number) {
94 return axios.post(`system/broker/push-match-records/${recordId}/rollback`);
95 }
96
97 static async send(recordId: number) {
98 return axios.post(`system/broker/push-match-records/${recordId}/send`);
99 }
100 }
101
102 export interface BrokerPushLevel {
103 id: number;
104 user_id: number;
105 title: string;
106 is_alert: 1 | 0;
107 begin_at: string;
108 end_at: string;
109 publish_at: string;
110 status: number;
111 }
112
113 export class usePushLevelRecordApi {
114 static statusOption = [
115 { value: 0, label: '待发送' },
116 { value: 1, label: '处理中' },
117 { value: 2, label: '已发送' },
118 { value: 3, label: '已撤销' },
119 { value: -1, label: '发送失败' },
120 ];
121
122 static async get(params?: QueryForParams | QueryForPaginationParams): Promise<ServiceResponse<BrokerPushLevel[]>> {
123 return axios.get('system/broker/push-level-records', { params });
124 }
125
126 static async update(id: number, data: AnyObject): Promise<BrokerPushLevel> {
127 return axios.put(`system/broker/push-level-records/${id}`, data).then((res) => Promise.resolve(res.data));
128 }
129
130 static async send(id: number) {
131 return axios.post(`system/broker/push-level-records/${id}/send`).then((res) => Promise.resolve(res.data));
132 }
133
134 static async rollback(id: number) {
135 return axios.post(`system/broker/push-level-records/${id}/rollback`).then((res) => Promise.resolve(res.data));
136 }
137
138 static async destroy(id: number) {
139 return axios.delete(`system/broker/push-level-records/${id}`).then((res) => Promise.resolve(res.data));
140 }
141
142 static getChildren(id: number, params?: QueryForParams | QueryForPaginationParams) {
143 return axios.get(`system/broker/push-level-records/${id}/children`, { params });
144 }
145 }
1 import { AttributeData, Option, QueryForPaginationParams, QueryForParams, ServiceResponse } from '@/types/global';
2 import { SystemConfig } from '@/types/system-config';
3 import axios from 'axios';
4 import { first } from 'lodash';
5
6 export default class useConfigApi {
7 static statusOptions = [
8 { label: '启用', value: 1 },
9 { label: '禁用', value: 0 },
10 ];
11
12 static contentOptions = [
13 { label: '无内容', value: 'none' },
14 {
15 label: '文本内容',
16 value: 'string',
17 children: [
18 { label: '短文本', value: 'input' },
19 { label: '长文本', value: 'textarea' },
20 ],
21 },
22 {
23 label: '上传链接',
24 value: 'upload',
25 children: [
26 { label: '图片链接', value: 'image' },
27 { label: '视频链接', value: 'video' },
28 { label: '音频链接', value: 'audio' },
29 { label: '文件链接', value: 'file' },
30 ],
31 },
32 ];
33
34 static get(params?: QueryForParams | QueryForPaginationParams): Promise<ServiceResponse<SystemConfig[]>> {
35 return axios.get('system/configs', { params });
36 }
37
38 static async create(data: AttributeData) {
39 return axios.post('system/configs', data).then((res) => Promise.resolve(res.data));
40 }
41
42 static async update(id: number, data: AttributeData) {
43 return axios.put(`system/configs/${id}`, data).then((res) => Promise.resolve(res.data));
44 }
45
46 static async changeStatus(id: number, status: number) {
47 return axios.put(`system/configs/${id}/change-status`, { status });
48 }
49
50 static async getOne(params?: QueryForParams): Promise<SystemConfig | undefined> {
51 return useConfigApi.get({ ...params, parent_id: '', fetchType: 'all' }).then(({ data }) => first(data));
52 }
53
54 static async getOption(params?: QueryForParams): Promise<Option[]> {
55 return useConfigApi.get({ parent_id: '', ...params, fetchType: 'all' }).then(({ data }) =>
56 data?.map((item) => {
57 return {
58 value: item.identifier,
59 label: item.name,
60 };
61 })
62 );
63 }
64 }
1 import { QueryForParams, ServiceResponse } from '@/types/global';
2 import axios, { AxiosRequestConfig } from 'axios';
3 import FileSaver from 'file-saver';
4
5 export default class useDashboardApi {
6 static userCertify() {
7 return axios.get('dashboard/user-certify').then((res) => Promise.resolve(res.data));
8 }
9
10 static userTotal() {
11 return axios.get('dashboard/user-total').then((res) => Promise.resolve(res.data));
12 }
13
14 static userSkill() {
15 return axios.get('dashboard/user-skill').then((res) => Promise.resolve(res.data));
16 }
17
18 static userStyle() {
19 return axios.get('dashboard/user-style').then((res) => Promise.resolve(res.data));
20 }
21
22 static activityTotal() {
23 return axios.get('dashboard/activity-total').then((res) => Promise.resolve(res.data));
24 }
25
26 static activityStyle() {
27 return axios.get('dashboard/activity-style').then((res) => Promise.resolve(res.data));
28 }
29
30 static activityLike() {
31 return axios.get('dashboard/activity-like').then((res) => Promise.resolve(res.data));
32 }
33
34 static activityListen() {
35 return axios.get('dashboard/activity-listen').then((res) => Promise.resolve(res.data));
36 }
37
38 static overview(params?: QueryForParams) {
39 return axios.get('dashboard/overview', { params }).then((res) => Promise.resolve(res.data));
40 }
41
42 static getOverviewExport(fileName: string, params?: QueryForParams) {
43 const config = { params: { ...params, fetchType: 'excel' }, timeout: 60000, responseType: 'blob' } as AxiosRequestConfig;
44 return axios.get('dashboard/overview', config).then(({ data }) => FileSaver.saveAs(data, `${fileName}.xlsx`));
45 }
46
47 static submitWork(params?: QueryForParams): Promise<ServiceResponse> {
48 return axios.get('dashboard/submit-work', { params });
49 }
50
51 static getSubmitWorkExport(fileName: string, params?: QueryForParams) {
52 const config = { params: { ...params, fetchType: 'excel' }, timeout: 60000, responseType: 'blob' } as AxiosRequestConfig;
53 return axios.get('dashboard/submit-work', config).then(({ data }) => FileSaver.saveAs(data, `${fileName}.xlsx`));
54 }
55 }
1 import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
2 import { Message } from '@arco-design/web-vue';
3 import { clearToken, getRefreshToken, getToken } from '@/utils/auth';
4 import { ServiceResponse } from '@/types/global';
5
6 const isRefreshing = false;
7
8 if (import.meta.env.VITE_API_HOST) {
9 axios.defaults.baseURL = import.meta.env.VITE_API_HOST;
10 axios.defaults.timeout = 30000;
11 }
12
13 axios.interceptors.request.use(
14 // @ts-ignore
15 (config: AxiosRequestConfig) => {
16 if (!config.url?.startsWith('/provider')) {
17 config.baseURL = `${config.baseURL}/admin`;
18 }
19 const token = isRefreshing ? getRefreshToken() : getToken();
20 if (token) {
21 config.headers = {
22 Accept: 'application/json',
23 ...config.headers,
24 Authorization: `Bearer ${token}`,
25 Route: sessionStorage.getItem('route') || 'dashboard',
26 };
27 }
28 if (config.params) {
29 Object.keys(config.params).forEach((key) => {
30 if (config.params[key] === undefined) {
31 delete config.params[key];
32 }
33 });
34 }
35
36 return config;
37 },
38 (error) => {
39 return Promise.reject(error);
40 }
41 );
42
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 async (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 );
1 import { QueryForParams, ServiceResponse } from '@/types/global';
2 import { SystemRole } from '@/types/system-role';
3 import axios from 'axios';
4
5 export default class useOperationLog {
6 static async get(params?: QueryForParams): Promise<ServiceResponse<SystemRole[]>> {
7 return axios.get('system/operation-logs', { params });
8 }
9 }
1 import { AnyObject, QueryForParams, ServiceResponse } from '@/types/global';
2 import { Banner } from '@/utils/model';
3 import axios from 'axios';
4
5 export default class useMaterialApi {
6 static typeOption = [
7 { value: 1, label: '用户主页背景' },
8 { value: 2, label: '厂牌主页背景' },
9 { value: 3, label: '音视频封面' },
10 ];
11
12 static get(params?: QueryForParams): Promise<ServiceResponse> {
13 return axios.get('system/materials', { params });
14 }
15
16 static async create(data: AnyObject) {
17 return axios.post(`system/materials`, data).then((res) => Promise.resolve(res.data));
18 }
19
20 static async update(id: number, data: AnyObject): Promise<Banner> {
21 return axios.put(`system/materials/${id}`, data).then((res) => Promise.resolve(res.data));
22 }
23
24 static async destroy(id: number) {
25 return axios.delete(`system/materials/${id}`).then((res) => Promise.resolve(res.data));
26 }
27 }
1 import { AttributeData, QueryForParams, ServiceResponse } from '@/types/global';
2
3 // eslint-disable-next-line import/no-cycle
4 import { Notification } from '@/utils/model';
5 import axios from 'axios';
6
7 export default class useNotificationApi {
8 static typeOption = [
9 { value: 1, label: '文本' },
10 { value: 2, label: '图文' },
11 ];
12
13 static alertOption = [
14 { value: 1, label: '弹出' },
15 { value: 0, label: '不弹出' },
16 ];
17
18 static statusOption = [
19 { value: 0, label: '待发送' },
20 { value: 1, label: '处理中' },
21 { value: 2, label: '已发送' },
22 { value: -1, label: '发送失败' },
23 { value: -2, label: '取消发送' },
24 { value: 3, label: '已撤销' },
25 ];
26
27 static publishTypeOption = [
28 { value: 1, label: '立即发送' },
29 { value: 2, label: '定时发送' },
30 ];
31
32 static linkTypeOption = [
33 { value: 'none', label: '无' },
34 { value: 'user', label: '用户' },
35 { value: 'activity', label: '歌曲' },
36 { value: 'project', label: '厂牌' },
37 { value: 'rich', label: '自定义H5页面' },
38 ];
39
40 static get(params?: QueryForParams): Promise<ServiceResponse<Notification[]>> {
41 return axios.get('system/notification', { params });
42 }
43
44 static async create(data?: AttributeData): Promise<Notification> {
45 return axios.post('system/notification', data).then((res) => Promise.resolve(res.data));
46 }
47
48 static async show(id: number): Promise<Notification> {
49 return axios.get(`system/notification/${id}`).then((res) => Promise.resolve(res.data));
50 }
51
52 static async update(id: number, data = {}): Promise<Notification> {
53 return axios.put(`system/notification/${id}`, data).then((res) => Promise.resolve(res.data));
54 }
55
56 static async user(id: number, params?: QueryForParams) {
57 return axios.get(`system/notification/${id}/users`, { params });
58 }
59
60 static async send(id: number): Promise<unknown> {
61 return axios.put(`system/notification/${id}/send`).then((res) => Promise.resolve(res.data));
62 }
63
64 static async cancel(id: number): Promise<unknown> {
65 return axios.put(`system/notification/${id}/cancel`).then((res) => Promise.resolve(res.data));
66 }
67
68 static async rollback(id: number): Promise<unknown> {
69 return axios.put(`system/notification/${id}/rollback`).then((res) => Promise.resolve(res.data));
70 }
71 }
1 import { AttributeData, QueryForParams, ServiceResponse } from '@/types/global';
2
3 import axios, { AxiosRequestConfig } from 'axios';
4 import FileSaver from 'file-saver';
5 import { Project, User, UserDynamics } from '@/utils/model';
6
7 type ProjectList = Project & {
8 activity_count: number;
9 activity_up_count: number;
10 activity_match_count: number;
11 activity_down_count: number;
12 activity_send_count: number;
13 manage_count: number;
14 };
15
16 export default class useProjectApi {
17 static promoteStatusOption = [
18 { label: '否', value: 0 },
19 { label: '是', value: 1 },
20 ];
21
22 static publicStatusOption = [
23 { label: '否', value: 1 },
24 { label: '是', value: 0 },
25 ];
26
27 static applyStatusOption = [
28 { label: '否', value: 0 },
29 { label: '是', value: 1 },
30 ];
31
32 static manageStatusOption = [
33 { label: '否', value: 0 },
34 { label: '是', value: 1 },
35 ];
36
37 static statusOption = [
38 { label: '启用', value: 1 },
39 { label: '禁用', value: 0 },
40 ];
41
42 static get(params?: QueryForParams): Promise<ServiceResponse<ProjectList[]>> {
43 return axios.get('audition/projects', { params });
44 }
45
46 static async getExport(fileName: string, params?: QueryForParams) {
47 const config = { params: { ...params, fetchType: 'excel' }, timeout: 60000, responseType: 'blob' } as AxiosRequestConfig;
48 return axios.get('audition/projects', config).then(({ data }) => FileSaver.saveAs(data, `${fileName}.xlsx`));
49 }
50
51 static async create(data: Omit<Project, 'id'>): Promise<Project> {
52 return axios.post('audition/projects', data).then((res) => Promise.resolve(res.data));
53 }
54
55 static async show(id: number): Promise<Project> {
56 return axios.get(`/audition/projects/${id}`).then((res) => Promise.resolve(res.data));
57 }
58
59 static async update(id: number, data: AttributeData): Promise<Project> {
60 return axios.put(`audition/projects/${id}`, data).then((res) => Promise.resolve(res.data));
61 }
62
63 static async changeStatus(id: number, status: 1 | 0) {
64 return axios.put(`audition/projects/${id}/change-status`, { status });
65 }
66
67 static destroy(id: number) {
68 return axios.delete(`audition/projects/${id}`);
69 }
70
71 static manageUsers(projectId: number, params?: QueryForParams): Promise<ServiceResponse<User[]>> {
72 return axios.get(`/audition/projects/${projectId}/managers`, { params });
73 }
74
75 static memberUsers(projectId: number, params?: QueryForParams): Promise<ServiceResponse<User[]>> {
76 return axios.get(`/audition/projects/${projectId}/members`, { params });
77 }
78
79 static outManageUsers(projectId: number, params?: QueryForParams): Promise<ServiceResponse<User[]>> {
80 return axios.get(`/audition/projects/${projectId}/out-managers`, { params });
81 }
82
83 static destroyOutManageUsers(projectId: number, data = {}) {
84 return axios.post(`/audition/projects/${projectId}/out-managers`, data);
85 }
86
87 static dynamics(id: number, params?: QueryForParams): Promise<ServiceResponse<UserDynamics[]>> {
88 return axios.get(`/audition/projects/${id}/dynamics`, { params });
89 }
90 }
91
92 export class useManagerApi {
93 static async create(data: { user_id: number; project_id: number }): Promise<Project> {
94 return axios.post('audition/project-managers', data).then((res) => Promise.resolve(res.data));
95 }
96
97 static destroy(id: number) {
98 return axios.delete(`audition/project-managers/${id}`);
99 }
100 }
1 import axios from 'axios';
2
3 type LoginData = { access_token: string; refresh_token: string; nick_name: string };
4
5 export default class useProviderApi {
6 static async area() {
7 return axios.get('/provider/area');
8 }
9
10 static sms(type: string, phone: string, area?: string) {
11 return axios.post('/provider/sms', { type, phone, area, platform: 'admin', scope: 1 });
12 }
13
14 static async login(type: 'phone' | 'email', data: object) {
15 return axios.post<LoginData>('/provider/login', { platform: 'admin', scope: 1, type, ...data });
16 }
17 }
1 import { AnyObject, QueryForParams } from '@/types/global';
2
3 import { Tag } from '@/types/tag';
4 import axios from 'axios';
5
6 export default class useReportApi {
7 static typeOption = [{ value: 3, label: '聊天' }];
8
9 static statusOption = [
10 { value: 0, label: '未处理' },
11 { value: 1, label: '已处理' },
12 ];
13
14 static async get(params?: QueryForParams) {
15 return axios.get('system/reports', { params });
16 }
17
18 static async update(id: number, data: AnyObject): Promise<Tag> {
19 return axios.put(`system/reports/${id}`, data).then((res) => Promise.resolve(res.data));
20 }
21 }
1 import { AnyObject, QueryForParams, ServiceResponse } from '@/types/global';
2
3 import { SystemRole } from '@/types/system-role';
4 import { SystemPermission } from '@/types/system-permission';
5 import axios from 'axios';
6
7 export default class useRoleApi {
8 static statusOption = [
9 { value: 1, label: '启用' },
10 { value: 0, label: '禁用' },
11 ];
12
13 static async get(params?: QueryForParams): Promise<ServiceResponse<SystemRole[]>> {
14 return axios.get('system/roles', { params });
15 }
16
17 static async create(data: AnyObject): Promise<SystemRole> {
18 return axios.post('system/roles', data).then((res) => Promise.resolve(res.data));
19 }
20
21 static async update(id: number, data: AnyObject): Promise<SystemRole> {
22 return axios.put(`system/roles/${id}`, data).then((res) => Promise.resolve(res.data));
23 }
24
25 static async changeStatus(id: number, status: number) {
26 return axios.put(`system/roles/${id}/change-status`, { status });
27 }
28
29 static async changePermission(id: number, permission: number[]) {
30 return axios.put(`system/roles/${id}/change-permission`, { permission });
31 }
32
33 static async destroy(id: number) {
34 return axios.delete(`system/roles/${id}`).then((res) => Promise.resolve(res.data));
35 }
36 }
37
38 export class usePermissionApi {
39 static async get(params?: QueryForParams): Promise<ServiceResponse<SystemPermission[]>> {
40 return axios.get('system/permissions', { params });
41 }
42 }
1 import { AnyObject, QueryForParams, ServiceResponse } from '@/types/global';
2
3 import { Tag } from '@/types/star-tags';
4 import axios from 'axios';
5
6 export default class useTagApi {
7 static frameOption = [
8 { label: '需要边框', value: 1 },
9 { label: '不需要边框', value: 2 },
10 ];
11
12 static indexFilterOption = [
13 { label: '首页可筛选', value: 1 },
14 { label: '首页不可筛选', value: 0 },
15 ];
16
17 static statusOption = [
18 { label: '开启', value: 1 },
19 { label: '禁用', value: 0 },
20 ];
21
22 static async get(params?: QueryForParams): Promise<ServiceResponse<Tag[]>> {
23 return axios.get('system/user_tags', { params });
24 }
25
26 static async create(data: AnyObject): Promise<Tag> {
27 return axios.post('system/user_tags', data).then((res) => Promise.resolve(res.data));
28 }
29
30 static async update(id: number, data: AnyObject): Promise<Tag> {
31 return axios.put(`system/user_tags/${id}`, data).then((res) => Promise.resolve(res.data));
32 }
33
34 static async destroy(id: number) {
35 return axios.delete(`system/user_tags/${id}`).then((res) => Promise.resolve(res.data));
36 }
37 }
1 import { AttributeData, QueryForParams, ServiceResponse } from '@/types/global';
2
3 import { Activity } from '@/types/activity';
4 import axios, { AxiosRequestConfig } from 'axios';
5 import { User, UserCertify, UserDynamics, UserManageActivity, UserMember, UserSubmitActivity } from '@/utils/model';
6 import FileSaver from 'file-saver';
7
8 export default class useUserApi {
9 static statusOption = [
10 { label: '启用', value: 1 },
11 { label: '禁用', value: 0 },
12 { label: '注销', value: 2 },
13 ];
14
15 static officialStatusOption = [
16 { label: '已关注', value: 1 },
17 { label: '未关注', value: 0 },
18 ];
19
20 static sexOption = [
21 { label: '男', value: 1 },
22 { label: '女', value: 2 },
23 { label: '无', value: 0 },
24 ];
25
26 static scopeOption = [
27 { label: '无权限', value: 0 },
28 { label: '平台管理员', value: 1 },
29 { label: '厂牌管理员', value: 2 },
30 ];
31
32 static get(params?: QueryForParams, config = {}): Promise<ServiceResponse<User[]>> {
33 return axios.get(`user`, { params, ...config });
34 }
35
36 static async show(id: number, params?: QueryForParams): Promise<User> {
37 return axios.get(`user/${id}`, { params }).then((res) => Promise.resolve(res.data));
38 }
39
40 static async update(id: number, data: AttributeData): Promise<User> {
41 return axios.put(`user/${id}`, data).then((res) => Promise.resolve(res.data));
42 }
43
44 static destroy(id: number) {
45 return axios.delete(`user/${id}`);
46 }
47
48 static changeStatus(id: number, status: 1 | 0) {
49 return axios.put(`user/${id}/change-status`, { status });
50 }
51
52 static changePwd(id: number, data: { password: string; password_confirmation: string }) {
53 return axios.put(`user/${id}/change-pwd`, data);
54 }
55
56 static listenSongs(id: number, params?: QueryForParams): Promise<ServiceResponse<Activity[]>> {
57 return axios.get(`user/${id}/listen-songs`, { params });
58 }
59
60 static likeSongs(id: number, params?: QueryForParams): Promise<ServiceResponse<Activity[]>> {
61 return axios.get(`user/${id}/like-songs`, { params });
62 }
63
64 static manageSongs(id: number, params?: QueryForParams): Promise<ServiceResponse<UserManageActivity[]>> {
65 return axios.get(`user/${id}/manage-songs`, { params });
66 }
67
68 static submitSongs(id: number, params?: QueryForParams): Promise<ServiceResponse<UserSubmitActivity[]>> {
69 return axios.get(`user/${id}/submit-songs`, { params });
70 }
71
72 static singers(id: number, params?: QueryForParams): Promise<ServiceResponse<User[]>> {
73 return axios.get(`user/${id}/singers`, { params });
74 }
75
76 static dynamics(id: number, params?: QueryForParams): Promise<ServiceResponse<UserDynamics[]>> {
77 return axios.get(`user/${id}/dynamics`, { params });
78 }
79 }
80
81 export class useCertifyApi {
82 static sexOption = useUserApi.sexOption;
83
84 static statusOption = [
85 { label: '待审核', value: 0 },
86 { label: '通过', value: 1 },
87 { label: '拒绝', value: 2 },
88 ];
89
90 // eslint-disable-next-line class-methods-use-this
91 static get(params?: QueryForParams): Promise<ServiceResponse<UserCertify[]>> {
92 return axios.get('user/certifies', { params });
93 }
94
95 static async update(id: number, data: { status: number; reason: string }): Promise<UserCertify> {
96 return axios.put(`user/certifies/${id}`, data).then((res) => Promise.resolve(res.data));
97 }
98 }
99
100 export class useRegisterApi extends useUserApi {
101 static get(params?: QueryForParams): Promise<ServiceResponse<User[]>> {
102 return axios.get('user/registers', { params });
103 }
104
105 static async getExport(fileName: string, params?: QueryForParams) {
106 const config = { params: { ...params, fetchType: 'excel' }, timeout: 60000, responseType: 'blob' } as AxiosRequestConfig;
107 return axios.get('user/registers', config).then(({ data }) => FileSaver.saveAs(data, `${fileName}.xlsx`));
108 }
109 }
110
111 export class useSingerApi extends useUserApi {
112 static get(params?: QueryForParams): Promise<ServiceResponse<User[]>> {
113 return axios.get('user/singers', { params });
114 }
115
116 static async getExport(fileName: string, params?: QueryForParams) {
117 const config = { params: { ...params, fetchType: 'excel' }, timeout: 60000, responseType: 'blob' } as AxiosRequestConfig;
118 return axios.get('user/singers', config).then(({ data }) => FileSaver.saveAs(data, `${fileName}.xlsx`));
119 }
120 }
121
122 export class useBusinessApi extends useUserApi {
123 static get(params?: QueryForParams): Promise<ServiceResponse<User[]>> {
124 return axios.get('user/business', { params });
125 }
126
127 static async getExport(fileName: string, params?: QueryForParams) {
128 const config = { params: { ...params, fetchType: 'excel' }, timeout: 60000, responseType: 'blob' } as AxiosRequestConfig;
129 return axios.get('user/business', config).then(({ data }) => FileSaver.saveAs(data, `${fileName}.xlsx`));
130 }
131 }
132
133 export class useMemberApi {
134 static get(userId: number, params?: QueryForParams): Promise<ServiceResponse<UserMember[]>> {
135 return axios.get(`user/${userId}/members`, { params });
136 }
137
138 static destroy(userId: number) {
139 return axios.delete(`user/${userId}/members`);
140 }
141 }
1 import { AttributeData, QueryForParams, ServiceResponse } from '@/types/global';
2
3 import { AppVersion } from '@/types/app-version';
4 import axios from 'axios';
5
6 export default class useVersionApi {
7 static systemOption = [
8 { label: 'Android', value: 'android' },
9 { label: 'IOS', value: 'ios' },
10 ];
11
12 static forceOption = [
13 { label: '是', value: 1 },
14 { label: '否', value: 0 },
15 ];
16
17 static get(params?: QueryForParams): Promise<ServiceResponse<AppVersion[]>> {
18 return axios.get('system/versions', { params });
19 }
20
21 static async create(data: AttributeData) {
22 return axios.post('system/versions', data).then((res) => Promise.resolve(res.data));
23 }
24
25 static async update(id: number, data: AttributeData) {
26 return axios.put(`system/versions/${id}`, data).then((res) => Promise.resolve(res.data));
27 }
28
29 static async destroy(id: number) {
30 return axios.delete(`system/versions/${id}`).then((res) => Promise.resolve(res.data));
31 }
32 }
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, useAuthorizedStore } from '@/store';
6 import usePermission from '@/hooks/permission';
7 import { last, orderBy } from 'lodash';
8 import { storeToRefs } from 'pinia';
9 import { useIntervalFn } from '@vueuse/core';
10
11 export default defineComponent({
12 emit: ['collapse'],
13 setup() {
14 const appStore = useAppStore();
15 const permission = usePermission();
16 const authorizedStore = useAuthorizedStore();
17 const router = useRouter();
18 const route = useRoute();
19
20 const { appMenu } = storeToRefs(appStore);
21 const { auditUserCount, auditActivityCount, activityApplyFailCount, permissions } = storeToRefs(authorizedStore);
22 const collapsed = ref(false);
23
24 const formatBadgeNum = (num: number) => {
25 return num <= 99 ? num : '99+';
26 };
27
28 const userBadge = computed((): number => {
29 let count = 0;
30 if (permissions.value?.includes('user-certify')) {
31 count += auditUserCount.value as number;
32 }
33 return count;
34 });
35
36 const auditionBadge = computed((): number => {
37 let count = 0;
38 if (permissions.value?.includes('audition-activity-audit')) {
39 count += auditActivityCount.value as number;
40 }
41 if (permissions.value?.includes('audition-activity-apply')) {
42 count += activityApplyFailCount.value as number;
43 }
44 return count;
45 });
46
47 useIntervalFn(() => {
48 authorizedStore.syncAuditUser();
49 authorizedStore.syncAuditActivity();
50 }, 30000);
51
52 const syncServicePermission = (item: RouteRecordRaw) => {
53 if (item.meta) {
54 const serverConfig = appMenu.value.find((menu) => item.name === menu.name);
55 item.meta.title = serverConfig?.label || item.meta?.title || '';
56 item.meta.order = serverConfig?.weight || item.meta?.order || 0;
57 item.meta.icon = serverConfig?.icon || item.meta?.icon || '';
58 }
59 item.children?.map((child) => syncServicePermission(child));
60 return item;
61 };
62
63 const appRoute = computed((): RouteRecordRaw[] => {
64 return router
65 .getRoutes()
66 .find((el) => el.name === 'root')
67 ?.children.filter((element) => element.meta?.hideInMenu !== true)
68 .map((item: RouteRecordRaw) => syncServicePermission(item)) as RouteRecordRaw[];
69 });
70
71 const menuTree = computed((): RouteRecordRaw[] => {
72 function travel(_routes: RouteRecordRaw[], layer: number) {
73 if (!_routes) return null;
74 const collector: any = orderBy(_routes, 'meta.order', 'desc').map((element) => {
75 // no access
76 if (!permission.accessRouter(element)) {
77 return null;
78 }
79
80 // leaf node
81 if (!element.children) {
82 return element;
83 }
84
85 // route filter hideInMenu true
86 element.children = element.children.filter((x) => x.meta?.hideInMenu !== true);
87
88 // Associated child node
89 const subItem = travel(element.children, layer);
90 if (subItem.length) {
91 element.children = subItem;
92 return element;
93 }
94 // the else logic
95 if (layer > 1) {
96 element.children = subItem;
97 return element;
98 }
99
100 if (element.meta?.hideInMenu === false) {
101 return element;
102 }
103
104 return null;
105 });
106
107 return collector.filter(Boolean);
108 }
109
110 return travel(appRoute.value, 0);
111 });
112
113 // In this case only two levels of menus are available
114 // You can expand as needed
115
116 const selectedKey = ref<string[]>([]);
117 const openKey = ref<string[]>([]);
118
119 const goto = (item: RouteRecordRaw) => {
120 router.push({ name: item.name });
121 };
122 watch(
123 route,
124 (newVal) => {
125 if (newVal.meta.requiresAuth) {
126 const key = newVal.meta.hideInMenu ? last(newVal.matched)?.meta?.menuSelectKey : last(newVal.matched)?.name;
127 selectedKey.value = [key as string];
128 openKey.value = newVal.meta.breadcrumb || [];
129 }
130 },
131 { immediate: true }
132 );
133 watch(
134 () => appStore.menuCollapse,
135 (newVal) => {
136 collapsed.value = newVal;
137 },
138 { immediate: true }
139 );
140 const setCollapse = (val: boolean) => appStore.updateSettings({ menuCollapse: val });
141
142 const renderSubMenu = () => {
143 function travel(_route: RouteRecordRaw[], nodes = []) {
144 if (_route) {
145 _route.forEach((element) => {
146 let icon = element?.meta?.icon ? `<${element?.meta?.icon}/>` : ``;
147 let title = `<span>${element.meta?.title}</span>`;
148
149 if (element?.name === 'user' && userBadge.value !== 0) {
150 if (icon && collapsed.value) {
151 icon = `<a-badge :count="${userBadge.value}" :offset="[2, -2]" dot>${icon}</a-badge>`;
152 }
153 title += `<span class="menu-number">${formatBadgeNum(userBadge.value)}</span>`;
154 }
155 if (element?.name === 'audition' && auditionBadge.value !== 0) {
156 if (icon && collapsed.value) {
157 icon = `<a-badge :count="${auditionBadge.value}" :offset="[4, -2]" dot>${icon}</a-badge>`;
158 }
159 title += `<span class="menu-number">${formatBadgeNum(auditionBadge.value)}</span>`;
160 }
161
162 let r;
163
164 if (element && element.children) {
165 r = (
166 <a-sub-menu key={element?.name} v-slots={{ icon: () => h(compile(icon)), title: () => h(compile(title)) }}>
167 {element?.children?.map((elem) => {
168 return (
169 <a-menu-item key={elem.name} onClick={() => goto(elem)}>
170 <span>{elem.meta?.title}</span>
171 {elem.name === 'user-certify' && auditUserCount.value !== 0 ? (
172 <span class="menu-number">{formatBadgeNum(auditUserCount.value as number)}</span>
173 ) : (
174 ''
175 )}
176 {elem.name === 'audition-activity-audit' && auditActivityCount.value !== 0 ? (
177 <span class="menu-number">{formatBadgeNum(auditActivityCount.value as number)}</span>
178 ) : (
179 ''
180 )}
181 {elem.name === 'audition-activity-apply' && activityApplyFailCount.value !== 0 ? (
182 <span class="menu-number">{formatBadgeNum(activityApplyFailCount.value as number)}</span>
183 ) : (
184 ''
185 )}
186 {travel(elem.children ?? [])}
187 </a-menu-item>
188 );
189 })}
190 </a-sub-menu>
191 );
192 } else {
193 r = (
194 <a-menu-item key={element.name} v-slots={{ icon: () => h(compile(icon)) }} onClick={() => goto(element)}>
195 {element.meta?.title}
196 </a-menu-item>
197 );
198 }
199 nodes.push(r as never);
200 });
201 }
202 return nodes;
203 }
204
205 return travel(menuTree.value);
206 };
207
208 return () => (
209 <a-menu
210 v-model:collapsed={collapsed.value}
211 show-collapse-button
212 v-model:selected-keys={selectedKey.value}
213 v-model:open-keys={openKey.value}
214 auto-open-selected={true}
215 level-indent={34}
216 style={{ height: '100%' }}
217 onCollapse={setCollapse}
218 >
219 {renderSubMenu()}
220 </a-menu>
221 );
222 },
223 });
224 </script>
225
226 <style lang="less" scoped>
227 :deep(.arco-menu-inner) {
228 .arco-menu-inline-header {
229 display: flex;
230 align-items: center;
231 }
232
233 .arco-icon {
234 &:not(.arco-icon-down) {
235 font-size: 18px;
236 }
237 }
238 }
239
240 .menu-number {
241 background-color: red;
242 color: #fff;
243 margin-left: 12px;
244 padding: 0 6px;
245 border-radius: 8px;
246 font-size: 12px;
247 font-weight: 500;
248 }
249 </style>
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, IconLock } 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>
1 <template>
2 <a-tooltip :content="theme === 'light' ? '切换为暗黑模式' : '切换为亮色模式'">
3 <a-button :shape="'circle'" class="nav-btn" type="outline" @click="() => onChange()">
4 <template #icon>
5 <icon-moon-fill v-if="theme === 'dark'" />
6 <icon-sun-fill v-else />
7 </template>
8 </a-button>
9 </a-tooltip>
10 </template>
11
12 <script lang="ts">
13 import { computed, defineComponent } from 'vue';
14 import { useAppStore } from '@/store';
15 import { useDark, useToggle } from '@vueuse/core';
16
17 import { IconMoonFill, IconSunFill } from '@arco-design/web-vue/es/icon';
18
19 export default defineComponent({
20 name: 'ToggleThemeButton',
21 components: {
22 IconSunFill,
23 IconMoonFill,
24 },
25 setup() {
26 const appStore = useAppStore();
27
28 const theme = computed(() => {
29 return appStore.theme;
30 });
31
32 const isDark = useDark({
33 selector: 'body',
34 attribute: 'arco-theme',
35 valueDark: 'dark',
36 valueLight: 'light',
37 storageKey: 'arco-theme',
38 onChanged(dark: boolean) {
39 appStore.toggleTheme(dark);
40 },
41 });
42 const onChange = useToggle(isDark);
43
44 return {
45 theme,
46 onChange,
47 };
48 },
49 });
50 </script>
51
52 <style lang="less" scoped>
53 .nav-btn {
54 border-color: rgb(var(--gray-2));
55 color: rgb(var(--gray-8));
56 font-size: 16px;
57 }
58
59 .trigger-btn,
60 .ref-btn {
61 position: absolute;
62 bottom: 14px;
63 }
64
65 .trigger-btn {
66 margin-left: 14px;
67 }
68 </style>
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 Menu from '@/layout/components/menu.vue';
38 import usePermission from '@/hooks/permission';
39 import Navbar from '@/layout/components/navbar.vue';
40
41 export default defineComponent({
42 components: {
43 Navbar,
44 Menu,
45 },
46 setup() {
47 const appStore = useAppStore();
48 const authorizedStore = useAuthorizedStore();
49 const router = useRouter();
50 const route = useRoute();
51 const permission = usePermission();
52 const navbarHeight = `60px`;
53 const menu = computed(() => appStore.menu);
54 const menuWidth = computed(() => {
55 return appStore.menuCollapse ? 48 : appStore.menuWidth;
56 });
57 const collapse = computed(() => {
58 return appStore.menuCollapse;
59 });
60 const paddingStyle = computed(() => {
61 const paddingLeft = menu.value ? { paddingLeft: `${menuWidth.value}px` } : {};
62 const paddingTop = { paddingTop: navbarHeight };
63 return { ...paddingLeft, ...paddingTop };
64 });
65 const setCollapsed = (val: boolean) => {
66 appStore.updateSettings({ menuCollapse: val });
67 };
68 watch(
69 () => authorizedStore.permissions,
70 (roleValue) => {
71 if (roleValue && !permission.accessRouter(route)) router.push({ name: 'notFound' });
72 }
73 );
74 return {
75 menu,
76 menuWidth,
77 paddingStyle,
78 collapse,
79 setCollapsed,
80 };
81 },
82 });
83 </script>
84
85 <style lang="less" scoped>
86 @nav-size-height: 60px;
87 @layout-max-width: 1100px;
88
89 .layout {
90 width: 100%;
91 height: 100%;
92 }
93
94 .layout-navbar {
95 position: fixed;
96 top: 0;
97 left: 0;
98 z-index: 100;
99 width: 100%;
100 min-width: @layout-max-width;
101 height: @nav-size-height;
102 }
103
104 .layout-sider {
105 position: fixed;
106 top: 0;
107 left: 0;
108 z-index: 99;
109 height: 100%;
110
111 &::after {
112 position: absolute;
113 top: 0;
114 right: -1px;
115 display: block;
116 width: 1px;
117 height: 100%;
118 background-color: var(--color-border);
119 content: '';
120 }
121
122 > :deep(.arco-layout-sider-children) {
123 overflow-y: hidden;
124 }
125 }
126
127 .menu-wrapper {
128 height: 100%;
129 overflow: auto;
130 overflow-x: hidden;
131
132 :deep(.arco-menu) {
133 ::-webkit-scrollbar {
134 width: 12px;
135 height: 4px;
136 }
137
138 ::-webkit-scrollbar-thumb {
139 border: 4px solid transparent;
140 background-clip: padding-box;
141 border-radius: 7px;
142 background-color: var(--color-text-4);
143 }
144
145 ::-webkit-scrollbar-thumb:hover {
146 background-color: var(--color-text-3);
147 }
148 }
149 }
150
151 .layout-content {
152 min-width: @layout-max-width;
153 min-height: 100vh;
154 overflow-y: hidden;
155 background-color: var(--color-fill-2);
156 transition: padding-left 0.2s;
157 }
158 </style>
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 import directive from './directive';
6 import App from './App.vue';
7 import '@arco-design/web-vue/dist/arco.css';
8 import '@/assets/style/global.less';
9 import 'vue3-colorpicker/style.css';
10 import '@/http/interceptor';
11
12 import store from './store';
13
14 import router from './router';
15
16 const app = createApp(App);
17
18 app.use(ArcoVue, {});
19 app.use(ArcoVueIcon);
20
21 app.use(store);
22 app.use(router);
23 app.use(globalComponents);
24 app.use(directive);
25
26 app.mount('#app');