Commit 4c170cb1 4c170cb18f5205639366cb65b33ebf12650beb7d by 杨俊

Init

0 parents
Showing 161 changed files with 4437 additions and 0 deletions
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 withDefaults: 'readonly'
41 },
42 rules: {
43 'prettier/prettier': 1,
44 // Vue: Recommended rules to be closed or modify
45 'vue/require-default-prop': 0,
46 'vue/singleline-html-element-content-newline': 0,
47 'vue/max-attributes-per-line': 0,
48 // Vue: Add extra rules
49 'vue/custom-event-name-casing': [2, 'camelCase'],
50 'vue/no-v-text': 1,
51 'vue/padding-line-between-blocks': 1,
52 'vue/require-direct-export': 1,
53 'vue/multi-word-component-names': 0,
54 // Allow @ts-ignore comment
55 '@typescript-eslint/ban-ts-comment': 0,
56 '@typescript-eslint/no-unused-vars': 1,
57 '@typescript-eslint/no-empty-function': 1,
58 '@typescript-eslint/no-explicit-any': 0,
59 'camelcase': 'off',
60 '@typescript-eslint/camelcase': 0,
61 'import/extensions': [
62 2,
63 'ignorePackages',
64 {
65 js: 'never',
66 jsx: 'never',
67 ts: 'never',
68 tsx: 'never',
69 },
70 ],
71 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
72 'no-param-reassign': 0,
73 'prefer-regex-literals': 0,
74 'import/no-extraneous-dependencies': 0,
75 },
76 };
1 node_modules
2 .DS_Store
3 dist
4 dist-ssr
5 *.local
6 /node_modules
7 /dist-ssr
8 .idea/*
9 yarn.lock
10 /.yarn
1 module.exports = {
2 tabWidth: 2,
3 semi: true,
4 printWidth: 140,
5 singleQuote: true,
6 quoteProps: 'consistent',
7 htmlWhitespaceSensitivity: 'strict',
8 };
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 nodeLinker: node-modules
1 module.exports = {
2 plugins: ['@vue/babel-plugin-jsx'],
3 };
1 module.exports = {
2 extends: ['@commitlint/config-conventional'],
3 };
1 *
2 !.gitignore
3 !vite.config.develop.ts
4 !vite.config.prod.ts
5 !vite.config.cdn.ts
1 import { resolve } from 'path';
2 import { defineConfig } from 'vite';
3 import vue from '@vitejs/plugin-vue';
4 import vueJsx from '@vitejs/plugin-vue-jsx';
5 import svgLoader from 'vite-svg-loader';
6 import vueSetupExtend from 'vite-plugin-vue-setup-extend';
7 import VitePluginOss from 'vite-plugin-oss';
8
9 const config = {
10 VITE_OSS_ACCESS_KEY: 'LTAI5t7LPdTdw9bSY7JUJmyG',
11 VITE_OSS_ACCESS_SECRET: 'qBiMksMt7DakR5cLQc03LCcsdwhPH1',
12 VITE_OSS_BUCKET: 'hising',
13 VITE_OSS_REGION: 'oss-cn-hangzhou',
14 VITE_OSS_HOST: 'https://hising-cdn.hikoon.com',
15 VITE_OSS_ENDPOINT: 'https://hising.oss-cn-hangzhou.aliyuncs.com',
16 };
17
18 const ossPath = `vendor/manage/asset${new Date()
19 .toLocaleString('zh')
20 .slice(0, 20)
21 .replace(/[\s/:]/g, '')}`;
22
23 export default defineConfig({
24 mode: 'production',
25 base: config.VITE_OSS_HOST,
26 server: { https: true },
27 plugins: [
28 vue(),
29 vueJsx(),
30 vueSetupExtend(),
31 svgLoader({ svgoConfig: {} }),
32 VitePluginOss({
33 from: `./dist/${ossPath}/**`,
34 accessKeyId: config.VITE_OSS_ACCESS_KEY,
35 accessKeySecret: config.VITE_OSS_ACCESS_SECRET,
36 bucket: config.VITE_OSS_BUCKET,
37 region: config.VITE_OSS_REGION,
38 quitWpOnError: true,
39 }),
40 ],
41 resolve: {
42 alias: [
43 {
44 find: '@',
45 replacement: resolve(__dirname, '../src'),
46 },
47 {
48 find: 'assets',
49 replacement: resolve(__dirname, '../src/assets'),
50 },
51 {
52 find: 'vue-i18n',
53 replacement: 'vue-i18n/dist/vue-i18n.cjs.js', // Resolve the i18n warning issue
54 },
55 {
56 find: 'vue',
57 replacement: 'vue/dist/vue.esm-bundler.js', // compile template. you can remove it, if you don't need.
58 },
59 ],
60 extensions: ['.ts', '.js'],
61 },
62 define: {
63 'process.env': {
64 VITE_ENV: 'production',
65 VITE_API_HOST: 'https://hising.hikoon.com',
66 VITE_SHOW_LOGIN_ACCOUNT: false,
67 ...config,
68 },
69 },
70 build: {
71 assetsDir: ossPath,
72 rollupOptions: {
73 output: {
74 manualChunks: {
75 arco: ['@arco-design/web-vue'],
76 chart: ['echarts', 'vue-echarts'],
77 vue: ['vue', 'vue-router', 'pinia', '@vueuse/core', 'vue-i18n'],
78 },
79 },
80 },
81 chunkSizeWarningLimit: 2000,
82 },
83 });
1 import { resolve } from 'path';
2 import { defineConfig } from 'vite';
3 import vue from '@vitejs/plugin-vue';
4 import vueJsx from '@vitejs/plugin-vue-jsx';
5 import svgLoader from 'vite-svg-loader';
6 import vueSetupExtend from 'vite-plugin-vue-setup-extend';
7
8 export default defineConfig({
9 mode: 'production',
10 server: { https: true },
11 plugins: [vue(), vueJsx(), vueSetupExtend(), svgLoader({ svgoConfig: {} })],
12 // build:{
13 // rollupOptions: {
14 // output: {
15 // manualChunks: {
16 // echarts: ['echarts']
17 // }
18 // }
19 // }
20 // },
21 resolve: {
22 alias: [
23 {
24 find: '@',
25 replacement: resolve(__dirname, '../src'),
26 },
27 {
28 find: 'assets',
29 replacement: resolve(__dirname, '../src/assets'),
30 },
31 {
32 find: 'vue-i18n',
33 replacement: 'vue-i18n/dist/vue-i18n.cjs.js', // Resolve the i18n warning issue
34 },
35 {
36 find: 'vue',
37 replacement: 'vue/dist/vue.esm-bundler.js', // compile template. you can remove it, if you don't need.
38 },
39 ],
40 extensions: ['.ts', '.js'],
41 },
42 define: {
43 'process.env': {
44 VITE_ENV: 'develop',
45 VITE_API_HOST: 'https://hi-sing-service-dev.hikoon.com',
46 VITE_SHOW_LOGIN_ACCOUNT: true,
47 VITE_OSS_ACCESS_KEY: 'LTAI4GKtcA6yTV6wnapivq7Y',
48 VITE_OSS_ACCESS_SECRET: 'QPEt0HPEuRe7wIk2wZNtKPF6L0xMmQ',
49 VITE_OSS_BUCKET: 'hisin-dev',
50 VITE_OSS_REGION: 'oss-cn-beijing',
51 VITE_OSS_HOST: 'https://hi-sing-cdn-dev.hikoon.com',
52 VITE_OSS_ENDPOINT: 'https://hisin-dev.oss-cn-beijing.aliyuncs.com',
53 },
54 },
55 });
1 import { resolve } from 'path';
2 import { defineConfig } from 'vite';
3 import vue from '@vitejs/plugin-vue';
4 import vueJsx from '@vitejs/plugin-vue-jsx';
5 import svgLoader from 'vite-svg-loader';
6 import vueSetupExtend from 'vite-plugin-vue-setup-extend';
7
8 export default defineConfig({
9 mode: 'production',
10 server: { https: true },
11 plugins: [vue(), vueJsx(), vueSetupExtend(), svgLoader({ svgoConfig: {} })],
12 resolve: {
13 alias: [
14 {
15 find: '@',
16 replacement: resolve(__dirname, '../src'),
17 },
18 {
19 find: 'assets',
20 replacement: resolve(__dirname, '../src/assets'),
21 },
22 {
23 find: 'vue-i18n',
24 replacement: 'vue-i18n/dist/vue-i18n.cjs.js', // Resolve the i18n warning issue
25 },
26 {
27 find: 'vue',
28 replacement: 'vue/dist/vue.esm-bundler.js', // compile template. you can remove it, if you don't need.
29 },
30 ],
31 extensions: ['.ts', '.js'],
32 },
33 define: {
34 'process.env': {
35 VITE_ENV: 'production',
36 VITE_API_HOST: 'https://hising.hikoon.com',
37 VITE_SHOW_LOGIN_ACCOUNT: false,
38 VITE_OSS_ACCESS_KEY: 'LTAI4GKtcA6yTV6wnapivq7Y',
39 VITE_OSS_ACCESS_SECRET: 'QPEt0HPEuRe7wIk2wZNtKPF6L0xMmQ',
40 VITE_OSS_BUCKET: 'hisin',
41 VITE_OSS_REGION: 'oss-cn-hangzhou',
42 VITE_OSS_HOST: 'https://hi-sing-cdn.hikoon.com',
43 VITE_OSS_ENDPOINT: 'https://hisin.oss-cn-hangzhou.aliyuncs.com',
44 },
45 },
46 });
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 <title>海星试唱</title>
8 </head>
9 <body>
10 <div id="app"></div>
11 <script type="module" src="/src/main.ts"></script>
12 </body>
13 </html>
This diff could not be displayed because it is too large.
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": "vite build --config ./config/vite.config.prod.ts",
11 "build:cdn": "vite build --config ./config/vite.config.cdn.ts",
12 "develop": "vite build --config ./config/vite.config.develop.ts",
13 "lint-staged": "npx lint-staged"
14 },
15 "lint-staged": {
16 "*.{js,ts,jsx,tsx}": [
17 "prettier --write",
18 "eslint --fix"
19 ],
20 "*.vue": [
21 "stylelint --fix",
22 "prettier --write",
23 "eslint --fix"
24 ],
25 "*.{less,css}": [
26 "stylelint --fix",
27 "prettier --write"
28 ]
29 },
30 "dependencies": {
31 "@arco-design/web-vue": "^2.36.0",
32 "@types/file-saver": "^2.0.5",
33 "@types/mockjs": "^1.0.4",
34 "@vueuse/core": "^7.3.0",
35 "@vueuse/router": "^10.3.0",
36 "ali-oss": "^6.17.1",
37 "arco-design-pro-vue": "^2.2.3",
38 "axios": "^0.24.0",
39 "dayjs": "^1.11.2",
40 "echarts": "^5.3.3",
41 "file-saver": "^2.0.5",
42 "i": "^0.3.7",
43 "js-file-download": "^0.4.12",
44 "lodash": "^4.17.21",
45 "nanoid": "^3.3.4",
46 "npm": "^9.6.6",
47 "nprogress": "^0.2.0",
48 "pinia": "^2.0.9",
49 "query-string": "^7.0.1",
50 "rabbit-lyrics": "^2.1.1",
51 "vue": "^3.2.31",
52 "vue-echarts": "^6.2.3",
53 "vue-i18n": "^9.2.0-beta.17",
54 "vue-router": "4",
55 "vue3-video-play": "1.3.1-beta.6"
56 },
57 "devDependencies": {
58 "@arco-design/web-vue": "^2.36.0",
59 "@commitlint/cli": "^11.0.0",
60 "@commitlint/config-conventional": "^12.0.1",
61 "@types/ali-oss": "^6.16.3",
62 "@types/lodash": "^4.14.177",
63 "@types/nprogress": "^0.2.0",
64 "@typescript-eslint/eslint-plugin": "^5.10.0",
65 "@typescript-eslint/parser": "^5.10.0",
66 "@vitejs/plugin-vue": "^1.9.4",
67 "@vitejs/plugin-vue-jsx": "^1.2.0",
68 "@vue/babel-plugin-jsx": "^1.1.1",
69 "eslint": "^8.7.0",
70 "eslint-config-airbnb-base": "^14.2.1",
71 "eslint-config-prettier": "^8.3.0",
72 "eslint-import-resolver-typescript": "^2.4.0",
73 "eslint-plugin-import": "^2.22.1",
74 "eslint-plugin-prettier": "^3.3.1",
75 "eslint-plugin-vue": "^8.3.0",
76 "less": "^4.1.2",
77 "lint-staged": "^11.2.6",
78 "mockjs": "^1.1.0",
79 "prettier": "^2.2.1",
80 "stylelint": "^13.8.0",
81 "stylelint-config-prettier": "^8.0.2",
82 "stylelint-config-rational-order": "^0.1.2",
83 "stylelint-config-standard": "^20.0.0",
84 "stylelint-order": "^4.1.0",
85 "typescript": "^4.5.5",
86 "vite": "^2.6.4",
87 "vite-plugin-eslint": "^1.3.0",
88 "vite-plugin-oss": "^1.2.13",
89 "vite-plugin-vue-setup-extend": "^0.4.0",
90 "vite-svg-loader": "^3.1.0",
91 "vue-tsc": "^0.30.5"
92 },
93 "volta": {
94 "node": "18.3.0",
95 "yarn": "1.22.19"
96 }
97 }
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 // eslint-disable-next-line max-classes-per-file
2 import { AnyObject, QueryForParams, ServiceResponse } from '@/types/global';
3 import axios, { AxiosRequestConfig } from 'axios';
4 import { ActivityApply } from '@/types/activity-apply';
5 import FileSaver from 'file-saver';
6 import { Activity, ActivityViewUser } from '@/types/activity';
7 import { ActivityWork } from '@/types/activity-work';
8
9 export default class useActivityApi {
10 static statusOption = [
11 { label: '处理中', value: 0 },
12 { label: '已上架', value: 1 },
13 { label: '已下架', value: 2 },
14 { label: '已匹配', value: 3 },
15 { label: '已发行', value: 5 },
16 { label: '处理失败', value: 4 },
17 ];
18
19 static weightOption = [
20 { label: '无', value: 0 },
21 { label: '低', value: 30 },
22 { label: '中', value: 60 },
23 { label: '高', value: 90 },
24 ];
25
26 static workSingTypeOption = [
27 { label: '自主上传', value: 1 },
28 { label: '唱整首', value: 2 },
29 { label: '唱片段', value: 3 },
30 ];
31
32 static workSingStatusOption = [
33 { label: '待采纳', value: 0 },
34 { label: '已确认', value: 1 },
35 { label: '不合适', value: 2 },
36 { label: '未采纳', value: 3 },
37 { label: '其他', value: 4 },
38 ];
39
40 static songTypeOption = [
41 { label: '歌曲', value: 1 },
42 { label: 'Demo', value: 2 },
43 ];
44
45 static async get(params: QueryForParams): Promise<ServiceResponse<Activity[]>> {
46 return axios.get('audition/activities', { params });
47 }
48
49 static async getExport(fileName: string, params?: QueryForParams) {
50 const config = { params: { ...params, fetchType: 'excel' }, timeout: 60000, responseType: 'blob' } as AxiosRequestConfig;
51 return axios.get('audition/activities', config).then(({ data }) => FileSaver.saveAs(data, `${fileName}.xlsx`));
52 }
53
54 static async show(id: number, params?: QueryForParams): Promise<Activity> {
55 return axios.get(`/audition/activities/${id}`, { params }).then((res) => Promise.resolve(res.data));
56 }
57
58 static async changeStatus(id: number, data: AnyObject) {
59 return axios.put<Activity>(`/audition/activities/${id}/change-status`, data).then((res) => Promise.resolve(res.data));
60 }
61
62 static async update(id: number, data: AnyObject): Promise<Activity> {
63 return axios.put(`/audition/activities/${id}`, data).then((res) => Promise.resolve(res.data));
64 }
65
66 static async destroy(id: number) {
67 return axios.delete(`/audition/activities/${id}`).then((res) => Promise.resolve(res.data));
68 }
69
70 static async getManageUser(activityId: number, params: QueryForParams): Promise<any> {
71 return axios.get(`/audition/activities/${activityId}/managers`, { params });
72 }
73
74 static async createManage(data: AnyObject): Promise<any> {
75 return axios.post('/audition/activity-managers', data).then((res) => Promise.resolve(res.data));
76 }
77
78 static async updateManage(id: number, data: AnyObject): Promise<any> {
79 return axios.put(`/audition/activity-managers/${id}`, data).then((res) => Promise.resolve(res.data));
80 }
81
82 static async deleteManage(id: number): Promise<any> {
83 return axios.delete(`/audition/activity-managers/${id}`).then((res) => Promise.resolve(res.data));
84 }
85
86 static async getViewUser(activityId: number, params: QueryForParams): Promise<ServiceResponse<ActivityViewUser[]>> {
87 return axios.get(`/audition/activities/${activityId}/views`, { params });
88 }
89
90 static async getLikeUser(activityId: number, params: QueryForParams): Promise<ServiceResponse<ActivityViewUser[]>> {
91 return axios.get(`/audition/activities/${activityId}/likes`, { params });
92 }
93 }
94
95 export class useApply {
96 static auditStatusOption = [
97 { label: '审核不通过', value: 2 },
98 { label: '审核中', value: 0 },
99 ];
100
101 static async get(params: QueryForParams): Promise<ServiceResponse<ActivityApply[]>> {
102 return axios.get('audition/applies', { params });
103 }
104
105 static async create(data: AnyObject) {
106 return axios.post('audition/applies', data).then((res) => Promise.resolve(res.data));
107 }
108
109 static async update(id: number, data: AnyObject) {
110 return axios.put(`audition/applies/${id}`, data).then((res) => Promise.resolve(res.data));
111 }
112
113 static async destroy(id: number) {
114 return axios.delete(`/audition/applies/${id}`);
115 }
116 }
117
118 export class useWorkApi {
119 static async get(params?: QueryForParams): Promise<ServiceResponse<ActivityWork[]>> {
120 return axios.get(`/audition/activity-works`, { params });
121 }
122
123 static async getExport(fileName: string, params?: QueryForParams) {
124 const config = { params: { ...params, fetchType: 'excel' }, timeout: 60000, responseType: 'blob' } as AxiosRequestConfig;
125 return axios.get('audition/activity-works', config).then(({ data }) => FileSaver.saveAs(data, `${fileName}.xlsx`));
126 }
127
128 static async changeStatus(workId: number, data: AnyObject) {
129 return axios.put(`/audition/activity-works/${workId}/change-status`, data).then((res) => Promise.resolve(res.data));
130 }
131 }
1 import axios from 'axios';
2 import { SystemPermission } from '@/types/system-permission';
3 import { AttributeData } from '@/types/global';
4 import FileSaver from 'file-saver';
5
6 type AuthResponse = {
7 user: { id?: number; nick_name?: string; real_name?: string; avatar?: string; email?: string };
8 permissions: string[];
9 menus: SystemPermission[];
10 };
11
12 export default class useAuthApi {
13 static async info(): Promise<AuthResponse> {
14 return axios.get('/auth/info').then((res) => Promise.resolve(res.data));
15 }
16
17 static async changePwd(data: AttributeData) {
18 return axios.patch('/auth/change-pwd', data).then((res) => Promise.resolve(res.data));
19 }
20
21 static async downloadFile(url: string, fileName: string) {
22 // ?response-content-type=Blob
23 FileSaver.saveAs(`${url}`, fileName + url.substring(url.lastIndexOf('.')));
24 }
25 }
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 async userStyle() {
7 return axios.get('/dashboard/user-style').then((res) => Promise.resolve(res.data));
8 }
9
10 static async activityStyle() {
11 return axios.get('/dashboard/activity-style').then((res) => Promise.resolve(res.data));
12 }
13
14 static async todo(params?: QueryForParams): Promise<ServiceResponse> {
15 return axios.get('/dashboard/todo', { params });
16 }
17
18 static async submitWork(params?: QueryForParams) {
19 return axios.get('/dashboard/submit-work', { params });
20 }
21
22 static async getSubmitWorkExport(fileName: string, params?: QueryForParams) {
23 const config = { params: { ...params, fetchType: 'excel' }, timeout: 60000, responseType: 'blob' } as AxiosRequestConfig;
24 return axios.get('dashboard/submit-work', config).then(({ data }) => FileSaver.saveAs(data, `${fileName}.xlsx`));
25 }
26 }
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 { apiHost } from '@/utils/env';
5 import { ServiceResponse } from '@/types/global';
6
7 axios.defaults.baseURL = apiHost;
8 axios.defaults.timeout = 30000;
9
10 const isRefreshing = false;
11
12 axios.interceptors.request.use(
13 (config: AxiosRequestConfig) => {
14 if (!config.url?.startsWith('/provider')) {
15 config.baseURL = `${config.baseURL}/manage`;
16 }
17
18 config.headers = {
19 Authorization: `Bearer ${isRefreshing ? getRefreshToken() : getToken()}`,
20 Accept: 'application/json',
21 Route: sessionStorage.getItem('route') || 'dashboard',
22 ...config.headers,
23 };
24
25 if (config.params) {
26 Object.keys(config.params).forEach((key) => {
27 if (config.params[key] === undefined || config.params[key] === '') {
28 delete config.params[key];
29 }
30 });
31 }
32
33 return config;
34 },
35 (error) => {
36 return Promise.reject(error);
37 }
38 );
39
40 axios.interceptors.response.use(
41 async (response: AxiosResponse<ServiceResponse>) => {
42 if (response.data instanceof Blob) {
43 return Promise.resolve(response);
44 }
45
46 return Promise.resolve(response.data);
47 },
48 async (error: any): Promise<any> => {
49 if (error.response?.status === 401) {
50 clearToken();
51 window.location.reload();
52 Message.warning({ content: '登陆信息已失效,请重新登陆...' });
53 }
54
55 if (error.response?.status === 403) {
56 window.location.href = '/exception/403';
57 }
58
59 Message.error({
60 id: 'service_error',
61 content: error.response.data.msg || error.msg || 'Request Error',
62 duration: 5 * 1000,
63 });
64
65 return Promise.reject(error);
66 }
67 );
1 import { AttributeData, QueryForParams, ServiceResponse } from '@/types/global';
2 import axios, { AxiosRequestConfig } from 'axios';
3 import FileSaver from 'file-saver';
4 import { Project } from '@/types/project';
5 import { User } from '@/types/user';
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 statusOption = [
23 { label: '启用', value: 1 },
24 { label: '禁用', value: 0 },
25 ];
26
27 static async get(params?: QueryForParams): Promise<ServiceResponse<ProjectList[]>> {
28 return axios.get('audition/projects', { params });
29 }
30
31 static async getExport(fileName: string, params?: QueryForParams) {
32 const config = { params: { ...params, fetchType: 'excel' }, timeout: 60000, responseType: 'blob' } as AxiosRequestConfig;
33 return axios.get('audition/projects', config).then(({ data }) => FileSaver.saveAs(data, `${fileName}.xlsx`));
34 }
35
36 static async show(id: number): Promise<Project> {
37 return axios.get(`/audition/projects/${id}`).then((res) => Promise.resolve(res.data));
38 }
39
40 static async update(id: number, data: AttributeData): Promise<Project> {
41 return axios.put(`audition/projects/${id}`, data).then((res) => Promise.resolve(res.data));
42 }
43
44 static async manageUsers(projectId: number, params?: QueryForParams): Promise<ServiceResponse<User[]>> {
45 return axios.get(`/audition/projects/${projectId}/managers`, { params });
46 }
47
48 static async memberUsers(projectId: number, params?: QueryForParams): Promise<ServiceResponse<User[]>> {
49 return axios.get(`/audition/projects/${projectId}/members`, { params });
50 }
51
52 static async outManageUsers(projectId: number, params?: QueryForParams): Promise<ServiceResponse<User[]>> {
53 return axios.get(`/audition/projects/${projectId}/out-managers`, { params });
54 }
55
56 static destroyOutManageUsers(projectId: number, data = {}) {
57 return axios.post(`/audition/projects/${projectId}/out-managers`, data);
58 }
59
60 static async dynamics(id: number, params?: QueryForParams): Promise<ServiceResponse> {
61 return axios.get(`/audition/projects/${id}/dynamics`, { params });
62 }
63 }
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: 'manage', scope: 2 });
12 }
13
14 static async login(type: 'phone' | 'email', data: object) {
15 return axios.post<LoginData>('/provider/login', { platform: 'manage', scope: 2, type, ...data });
16 }
17 }
1 import { QueryForParams } from '@/types/global';
2 import axios from 'axios';
3
4 export default class useUserApi {
5 static statusOption = [
6 { label: '启用', value: 1 },
7 { label: '禁用', value: 0 },
8 { label: '注销', value: 2 },
9 ];
10
11 static officialStatusOption = [
12 { label: '已关注', value: 1 },
13 { label: '未关注', value: 0 },
14 ];
15
16 static sexOption = [
17 { label: '男', value: 1 },
18 { label: '女', value: 2 },
19 { label: '无', value: 0 },
20 ];
21
22 static scopeOption = [
23 { label: '无权限', value: 0 },
24 { label: '平台管理员', value: 1 },
25 { label: '厂牌管理员', value: 2 },
26 ];
27
28 static async manageSongs(id: number, params?: QueryForParams) {
29 return axios.get(`users/${id}/manage-songs`, { params });
30 }
31 }
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 * {
2 box-sizing: border-box;
3 }
4
5 html,
6 body {
7 width: 100%;
8 height: 100%;
9 margin: 0;
10 padding: 0;
11 font-size: 14px;
12 background-color: var(--color-bg-1);
13 -moz-osx-font-smoothing: grayscale;
14 -webkit-font-smoothing: antialiased;
15 }
16
17 .echarts-tooltip-diy {
18 background: linear-gradient(304.17deg,
19 rgba(253, 254, 255, 0.6) -6.04%,
20 rgba(244, 247, 252, 0.6) 85.2%) !important;
21 border: none !important;
22 backdrop-filter: blur(10px) !important;
23 /* Note: backdrop-filter has minimal browser support */
24
25 border-radius: 6px !important;
26
27 .content-panel {
28 display: flex;
29 justify-content: space-between;
30 width: auto;
31 height: 32px;
32 margin-bottom: 4px;
33 padding: 0 9px;
34 line-height: 32px;
35 background: rgba(255, 255, 255, 0.8);
36 border-radius: 4px;
37 box-shadow: 6px 0 20px rgba(34, 87, 188, 0.1);
38 }
39
40 .tooltip-title {
41 margin: 0 0 10px 0;
42 }
43
44 p {
45 margin: 0;
46 }
47
48 .tooltip-title,
49 .tooltip-value {
50 font-size: 13px;
51 line-height: 15px;
52 display: flex;
53 align-items: center;
54 text-align: right;
55 color: #1d2129;
56 font-weight: bold;
57 }
58
59 .tooltip-value {
60 margin-left: 10px;
61 }
62
63 .tooltip-item-icon {
64 display: inline-block;
65 margin-right: 8px;
66 width: 10px;
67 height: 10px;
68 border-radius: 50%;
69 }
70 }
71
72 .general-card {
73 border-radius: 4px;
74 border: none;
75
76 & > .arco-card-header {
77 height: auto;
78 padding: 10px 20px 0 20px;
79 border: none;
80
81 & > .arco-card-header-title {
82 font-size: 15px;
83 }
84 }
85
86 & > .arco-card-body {
87 padding: 0 20px 20px 20px;
88 }
89 }
90
91 .split-line {
92 border-color: rgb(var(--gray-2));
93 }
94
95 .arco-table-cell {
96 .circle {
97 display: inline-block;
98 margin-right: 4px;
99 width: 6px;
100 height: 6px;
101 border-radius: 50%;
102 background-color: rgb(var(--blue-6));
103
104 &.pass {
105 background-color: rgb(var(--green-6));
106 }
107
108 &.warn {
109 background-color: rgb(var(--yellow-6));
110 }
111
112 &.err {
113 background-color: rgb(var(--red-6));
114 }
115 }
116 }
117
118 .mt-0 {
119 margin-bottom: 0;
120 }
121
122 .mt-20 {
123 margin-top: 20px;
124 }
125
126 .mb-0 {
127 margin-bottom: 0;
128 }
129
130 .color-grey {
131 color: rgba(44, 44, 44, 0.5)
132 }
133
134 .justify-center {
135 justify-content: center !important;
136 }
137
138
139 .retry {
140 div.arco-modal-header {
141 border-bottom: unset !important;
142 }
143
144 div.arco-modal-body {
145 padding: 0 24px 20px;
146 }
147 }
148
149 .link-hover:hover {
150 cursor: pointer !important;
151 }
152
This diff could not be displayed because it is too large.
1 <template>
2 <audio v-if="url" :id="uuid" :src="url" class="player" controls controlsList="nodownload noplaybackrate" @play="onPay" />
3 </template>
4
5 <script lang="ts" setup>
6 import { nanoid } from 'nanoid';
7 import { computed } from 'vue';
8
9 defineProps<{ url: string; name: string }>();
10
11 const uuid = computed(() => `audio-${nanoid()}`);
12
13 const onPay = (e: any) => {
14 const audios = document.getElementsByTagName('audio');
15 [].forEach.call(audios, (i: HTMLAudioElement) => i !== e.target && i.pause());
16 };
17 </script>
18
19 <style lang="less" scoped>
20 .player {
21 height: 30px;
22 width: 100%;
23 }
24 </style>
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 <template #trigger-icon>
6 <icon-camera />
7 </template>
8 <img v-if="modelValue" :src="modelValue" alt="avatar" style="width: 100%; height: 100%" />
9 <div v-else class="no-avatar">
10 <icon-plus />
11 </div>
12 </Avatar>
13 </template>
14 </Upload>
15 </template>
16
17 <script lang="ts" setup>
18 import { IconCamera, IconPlus } from '@arco-design/web-vue/es/icon';
19 import useOss from '@/hooks/oss';
20 import { Avatar, Message, Upload, UploadRequest, useFormItem } from '@arco-design/web-vue';
21
22 const props = withDefaults(
23 defineProps<{
24 modelValue: string;
25 size: number;
26 shape: 'circle' | 'square';
27 accept: string;
28 limit: number;
29 }>(),
30 {
31 modelValue: '',
32 size: 80,
33 shape: 'circle',
34 accept: 'image/*',
35 limit: 5,
36 }
37 );
38
39 const { eventHandlers } = useFormItem();
40
41 const emits = defineEmits(['update:modelValue']);
42
43 const { upload } = useOss();
44
45 const onBeforeUpload = (file: File) => {
46 if (file.size > props.limit * 1024 * 1024) {
47 Message.warning(`${file.name} 文件超过${props.limit}MB,无法上传`);
48 return Promise.resolve(false);
49 }
50
51 return Promise.resolve(file);
52 };
53
54 const onUpload = (option: any): UploadRequest => {
55 const { fileItem } = option;
56
57 if (fileItem.file) {
58 upload(fileItem.file, 'image').then((res) => {
59 emits('update:modelValue', res.url);
60 eventHandlers.value?.onChange?.();
61 });
62 }
63
64 return {};
65 };
66 </script>
67
68 <style lang="less" scoped>
69 :deep(.arco-avatar-text) {
70 width: 100%;
71 height: 100%;
72 }
73
74 .no-avatar {
75 position: absolute;
76 display: flex;
77 align-content: center;
78 align-items: center;
79 justify-content: center;
80 width: 100%;
81 height: 100%;
82 top: 0;
83 left: 0;
84 }
85 </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">
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 { defineComponent, ref, computed, nextTick } from 'vue';
7 import VCharts from 'vue-echarts';
8 import { useAppStore } from '@/store';
9
10 export default defineComponent({
11 components: {
12 VCharts,
13 },
14 props: {
15 options: {
16 type: Object,
17 default() {
18 return {};
19 },
20 },
21 autoresize: {
22 type: Boolean,
23 default: true,
24 },
25 width: {
26 type: String,
27 default: '100%',
28 },
29 height: {
30 type: String,
31 default: '100%',
32 },
33 },
34 setup() {
35 const appStore = useAppStore();
36 const theme = computed(() => {
37 if (appStore.theme === 'dark') return 'dark';
38 return '';
39 });
40 const renderChart = ref(false);
41 // wait container expand
42 nextTick(() => {
43 renderChart.value = true;
44 });
45 return {
46 theme,
47 renderChart,
48 };
49 },
50 });
51 </script>
52
53 <style scoped lang="less"></style>
1 <template>
2 <a-button :loading="loading" @click="onClick">
3 <template #icon>
4 <icon-download />
5 </template>
6 {{ label }}
7 </a-button>
8 </template>
9
10 <script lang="ts" setup>
11 import { IconDownload } from '@arco-design/web-vue/es/icon';
12 import useLoading from '@/hooks/loading';
13
14 const props = withDefaults(
15 defineProps<{
16 label: string;
17 onDownload: () => Promise<void>;
18 }>(),
19 {
20 label: '导出',
21 }
22 );
23
24 const { loading, setLoading } = useLoading(false);
25
26 const onClick = () => {
27 setLoading(true);
28 props.onDownload().finally(() => setLoading(false));
29 };
30 </script>
31
32 <style scoped></style>
1 <script setup lang="ts">
2 import TableColumn from '@/components/filter/table-column.vue';
3 import { Layout, LayoutSider, LayoutContent, Image, Form, FormItem, TypographyText, TableData } from '@arco-design/web-vue';
4 import { get } from 'lodash';
5 import { isString, isUndefined } from '@/utils';
6
7 const props = withDefaults(
8 defineProps<{
9 title?: string;
10 dataIndex?: string;
11 row?: string;
12 coverIndex?: string;
13 nameIndex?: string;
14 subIndex?: string;
15 tagIndex?: string;
16 projectIndex?: string;
17 hideSubTitle?: boolean;
18 }>(),
19 {
20 coverIndex: 'cover',
21 nameIndex: 'song_name',
22 subIndex: 'sub_title',
23 tagIndex: 'tags',
24 projectIndex: 'project',
25 }
26 );
27
28 const getRow = (record: TableData): TableData | undefined => {
29 if (isUndefined(props.row)) {
30 return record;
31 }
32
33 if (isString(props.row)) {
34 return get(record, props.row);
35 }
36
37 return props.row;
38 };
39
40 const getRowColumn = (record: TableData, key: string) => getRow(record)?.[key];
41 </script>
42
43 <template>
44 <TableColumn v-bind="$attrs" :title="title" :data-index="dataIndex">
45 <template #default="{ record }">
46 <Layout>
47 <LayoutSider :width="104" class="left">
48 <Image show-loader :height="100" :width="100" fit="cover" :src="getRowColumn(record, coverIndex)" />
49 </LayoutSider>
50 <LayoutContent>
51 <Form auto-label-width label-align="left" :model="record" style="height: 100%; justify-content: space-between">
52 <FormItem label="歌曲名称" :show-colon="true" row-class="mb-0">
53 <TypographyText :ellipsis="{ rows: 1, showTooltip: true }">
54 {{ getRowColumn(record, nameIndex) }}
55 </TypographyText>
56 </FormItem>
57 <FormItem v-if="!hideSubTitle" label="推荐语" :show-colon="true" row-class="mb-0">
58 <TypographyText :ellipsis="{ rows: 1, showTooltip: true }">
59 {{ getRowColumn(record, subIndex) }}
60 </TypographyText>
61 </FormItem>
62 <FormItem label="歌曲标签" :show-colon="true" row-class="mb-0">
63 <TypographyText :ellipsis="{ rows: 1, showTooltip: true }">
64 {{
65 getRowColumn(record, tagIndex)
66 .map((item: any) => item.name)
67 .join('、')
68 }}
69 </TypographyText>
70 </FormItem>
71 <FormItem label="关联厂牌" :show-colon="true" row-class="mb-0">
72 <TypographyText :ellipsis="{ rows: 1, showTooltip: true }">
73 {{ getRowColumn(record, projectIndex)?.name }}
74 </TypographyText>
75 </FormItem>
76 </Form>
77 </LayoutContent>
78 </Layout>
79 </template>
80 </TableColumn>
81 </template>
82
83 <style lang="less" scoped>
84 :deep(.arco-typography) {
85 margin-bottom: 0;
86 width: 100%;
87 text-align: left;
88 }
89
90 .left {
91 margin-top: 5px;
92 box-shadow: unset;
93 margin-right: 5px;
94 }
95 </style>
1 <script setup lang="ts">
2 import { Avatar, TableColumn } from '@arco-design/web-vue';
3 import { get } from 'lodash';
4
5 const props = defineProps<{ title?: string; dataIndex?: string; width?: number }>();
6
7 const getValue = (record: object) => {
8 return get(record, props.dataIndex || '', '');
9 };
10 </script>
11
12 <template>
13 <TableColumn v-bind="$attrs" :title="title" :data-index="dataIndex" :width="width || 60">
14 <template #cell="{ record }">
15 <Avatar :size="40" shape="circle">
16 <img alt="" :src="getValue(record)" :width="40" :height="40" />
17 </Avatar>
18 </template>
19 </TableColumn>
20 </template>
21
22 <style scoped lang="less"></style>
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 defineProps<{ title?: string; dataIndex?: string; split?: boolean }>();
7
8 const getValue = (record: object, path: string) => {
9 return get(record, path, '');
10 };
11 </script>
12
13 <template>
14 <TableColumn v-bind="$attrs" :title="title" :data-index="dataIndex">
15 <template #default="{ record }">
16 <template v-if="dataIndex && !getValue(record, dataIndex)">
17 <span style="color: rgba(0, 0, 0, 0.3)"></span>
18 </template>
19 <template v-else-if="split">
20 <div>{{ dayjs(getValue(record, dataIndex))?.format('YYYY-MM-DD') || '' }}</div>
21 <div>{{ dayjs(getValue(record, dataIndex))?.format('HH:mm:ss') || '' }}</div>
22 </template>
23 <template v-else>{{ getValue(record, dataIndex) }}</template>
24 </template>
25 </TableColumn>
26 </template>
27
28 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { Options } from '@/types/global';
3 import { eq, get } from 'lodash';
4 import TableColumn from '@/components/filter/table-column.vue';
5
6 const props = defineProps<{ title?: string; dataIndex?: string; option: Options[]; darkValue?: string | number }>();
7
8 const getValue = (record: object) => {
9 return get(record, props.dataIndex || '', '');
10 };
11 </script>
12
13 <template>
14 <TableColumn v-bind="$attrs" :title="title" :data-index="dataIndex">
15 <template #default="{ record }">
16 <template v-if="darkValue !== undefined && eq(getValue(record), darkValue)">
17 <span style="color: rgba(44, 44, 44, 0.5)"> {{ option.find((item) => item.value === getValue(record))?.label || '未知' }}</span>
18 </template>
19 <template v-else> {{ option.find((item) => item.value === getValue(record))?.label || '未知' }}</template>
20 </template>
21 </TableColumn>
22 </template>
23
24 <style scoped lang="less"></style>
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 { get } from 'lodash';
3 import TableColumn from '@/components/filter/table-column.vue';
4
5 const props = defineProps<{ title?: string; dataIndex?: string; nickIndex?: string; realIndex?: string }>();
6
7 const fullName = (record: object) => {
8 let name = '';
9
10 if (props.nickIndex && get(record, props.nickIndex, '')) {
11 name += get(record, props.nickIndex, '');
12 }
13
14 if (props.realIndex && get(record, props.realIndex)) {
15 name += `(${get(record, props.realIndex, '')})`;
16 }
17
18 return name;
19 };
20 </script>
21
22 <template>
23 <TableColumn v-bind="$attrs" :title="title" :data-index="dataIndex">
24 <template #default="{ record }"> {{ fullName(record) }}</template>
25 </TableColumn>
26 </template>
27
28 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { eq, get } from 'lodash';
3 import TableColumn from '@/components/filter/table-column.vue';
4
5 const props = withDefaults(
6 defineProps<{ title?: string; dataIndex?: string; darkValue?: string | number; prefix?: string; suffix?: string }>(),
7 { prefix: '', suffix: '' }
8 );
9
10 const getValue = (record: object) => {
11 return get(record, props.dataIndex || '', '');
12 };
13 </script>
14
15 <template>
16 <TableColumn v-bind="$attrs" :title="title" :data-index="dataIndex">
17 <template #default="{ record }">
18 <template v-if="darkValue !== undefined && eq(getValue(record), darkValue)">
19 <span style="color: rgba(44, 44, 44, 0.5)">{{ `${prefix} 0 ${suffix}` }}</span>
20 </template>
21 <template v-else> {{ `${prefix} ${getValue(record)} ${suffix}` }} </template>
22 </template>
23 </TableColumn>
24 </template>
25
26 <style scoped lang="less"></style>
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
35 defineEmits<{ (e?: 'search'): void; (e?: 'reset'): void }>();
36
37 const layoutStyle = { marginBottom: '12px' };
38 const layoutRightStyle = props.hideSearch && props.hideReset ? {} : { borderLeft: '1px solid var(--color-neutral-3)' };
39
40 if (!props.hideDivider) {
41 Object.assign(layoutStyle, { paddingBottom: '12px', borderBottom: '1px solid var(--color-neutral-3)' });
42 }
43 </script>
44
45 <template>
46 <Layout :style="layoutStyle">
47 <LayoutContent>
48 <Form ref="formRef" auto-label-width :model="model" label-align="right">
49 <Grid :cols="split as number" :col-gap="12" :row-gap="12">
50 <slot />
51 </Grid>
52 </Form>
53 </LayoutContent>
54 <LayoutSider class="right" width="auto" :style="layoutRightStyle">
55 <Space :size="12" :direction="inline ? 'horizontal' : 'vertical'">
56 <IconButton
57 v-if="!hideSearch"
58 :size="size"
59 :icon="searchIcon"
60 :label="searchLabel"
61 type="primary"
62 :loading="loading"
63 @click="$emit('search')"
64 />
65 <IconButton v-if="!hideReset" :size="size" :icon="resetIcon" :label="resetLabel" @click="$emit('reset')" />
66 <slot name="button" :size="size" />
67 </Space>
68 </LayoutSider>
69 </Layout>
70 </template>
71
72 <style scoped lang="less">
73 .right {
74 border-left: 1px solid var(--color-neutral-3);
75 margin-left: 12px;
76 padding-left: 12px;
77 box-shadow: unset;
78 }
79 </style>
1 <script setup lang="ts">
2 import { Space, TableData } from '@arco-design/web-vue';
3 import TableColumn from '@/components/filter/table-column.vue';
4
5 withDefaults(defineProps<{ title?: string; dataIndex?: string; direction?: 'vertical' | 'horizontal' }>(), {
6 direction: 'horizontal',
7 });
8 </script>
9
10 <template>
11 <TableColumn v-bind="$attrs" :title="title" :data-index="dataIndex" :tooltip="false">
12 <template #default="{ record, rowIndex }">
13 <Space :direction="direction" fill>
14 <slot name="default" :record="record as TableData" :index="rowIndex as Number" />
15 </Space>
16 </template>
17 </TableColumn>
18 </template>
19
20 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { TableColumn, TableSortable } from '@arco-design/web-vue';
3
4 type PropType = { title?: string; dataIndex?: string; tooltip?: boolean; ellipsis?: boolean; hasSort?: boolean };
5
6 const sortable = { sortDirections: ['ascend', 'descend'], sorter: true } as TableSortable;
7
8 withDefaults(defineProps<PropType>(), { tooltip: true, hasSort: false });
9 </script>
10
11 <template>
12 <TableColumn
13 v-bind="$attrs"
14 :title="title"
15 :data-index="dataIndex"
16 :tooltip="tooltip as boolean"
17 :ellipsis="ellipsis || tooltip as boolean"
18 :sortable="hasSort ? sortable : undefined as TableSortable"
19 >
20 <template v-if="$slots.default" #cell="{ record, rowIndex }">
21 <slot name="default" :record="record" :index="rowIndex" />
22 </template>
23 </TableColumn>
24 </template>
25
26 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { computed, ref } from 'vue';
3 import { Col, Row, Table } from '@arco-design/web-vue';
4 import { AnyObject, sizeType } from '@/types/global';
5 import usePagination from '@/hooks/pagination';
6
7 type PropType = {
8 loading?: boolean;
9 rowKey?: string;
10 hoverType?: string;
11 simplePage?: boolean;
12 pageSize?: number;
13 size?: sizeType;
14 onQuery: (params?: AnyObject) => Promise<any>;
15 };
16
17 defineEmits<{ (e: 'rowSort', dataIndex: string, direction: string): void }>();
18
19 const props = withDefaults(defineProps<PropType>(), { loading: false, rowKey: 'id', size: 'small', pageSize: 20 });
20 const { pagination, setPage, setPageSize, setTotal } = usePagination({
21 simple: props.simplePage,
22 size: props.size,
23 pageSize: props.pageSize,
24 });
25 const list = ref<any[]>([]);
26 const tableRef = ref();
27
28 const pageQueryParams = computed(() => {
29 return { page: pagination.value.current, pageSize: pagination.value.pageSize };
30 });
31
32 const hoverType = computed(() => props.hoverType || 'default');
33
34 const onFetch = () =>
35 props.onQuery(pageQueryParams.value).then(({ data, meta }) => {
36 list.value = data;
37 setPage(meta.current);
38 setTotal(meta.total);
39 setPageSize(meta.limit);
40 });
41
42 const onPageChange = (page: number) => {
43 setPage(page || 1);
44 onFetch();
45 };
46
47 const onSizeChange = (size: number) => {
48 setPageSize(size);
49 setPage(1);
50 onFetch();
51 };
52
53 const onPush = (row: unknown) => {
54 list.value.unshift(row);
55 // eslint-disable-next-line no-plusplus
56 ++pagination.value.total;
57 };
58
59 const onRemove = (index: number) => {
60 list.value.splice(index, 1);
61 // eslint-disable-next-line no-plusplus
62 --pagination.value.total;
63 };
64
65 const resetSort = () => tableRef.value?.clearSorters();
66
67 const getCount = () => pagination.value.total;
68
69 const formatSortType = (type: string): string => type?.replace('end', '') ?? '';
70
71 defineExpose({ onFetch, onPageChange, onSizeChange, resetSort, getCount, onPush, onRemove });
72 </script>
73
74 <template>
75 <Row justify="space-between" align="center">
76 <Col class="table-tool-item" :span="16" style="text-align: left">
77 <slot name="tool" :size="size" />
78 </Col>
79 <Col class="table-tool-item" :span="8" style="text-align: right">
80 <slot name="tool-right" :size="size" />
81 </Col>
82 </Row>
83 <Table
84 ref="tableRef"
85 v-bind="$attrs"
86 :row-key="rowKey as string"
87 :loading="loading as boolean"
88 :size="size as sizeType"
89 :data="list"
90 :pagination="pagination"
91 :bordered="false"
92 :table-layout-fixed="true"
93 @page-change="onPageChange"
94 @page-size-change="onSizeChange"
95 @sorter-change="(dataIndex, direction) => $emit('rowSort', dataIndex, formatSortType(direction))"
96 >
97 <template #columns>
98 <slot />
99 </template>
100 </Table>
101 </template>
102
103 <style lang="less" scoped>
104 :deep(.arco-table-cell) {
105 padding: 5px 8px !important;
106
107 :hover {
108 cursor: v-bind(hoverType);
109 }
110
111 & > .arco-table-td-content .arco-btn-size-small {
112 padding: 5px !important;
113 }
114 }
115
116 :deep(.table-tool-item) {
117 > * {
118 margin-bottom: 12px !important;
119 }
120 }
121 </style>
1 <script setup lang="ts">
2 import TableColumn from '@/components/filter/table-column.vue';
3 import { Avatar } from '@arco-design/web-vue';
4
5 import { get } from 'lodash';
6 import { User } from '@/types/user';
7 import { isString, isUndefined } from '@/utils';
8
9 const props = withDefaults(
10 defineProps<{
11 title?: string;
12 dataIndex?: string;
13 user?: string | Pick<User, 'id' | 'nick_name' | 'real_name' | 'identity'>;
14 showHref?: boolean;
15 showAvatar?: boolean;
16 showRealName?: boolean;
17 darkValue?: string;
18 nickIndex?: string;
19 realIndex?: string;
20 avatarIndex?: string;
21 roleIndex?: string;
22 linkStyle?: object;
23 }>(),
24 {
25 dataIndex: 'id',
26 nickIndex: 'nick_name',
27 realIndex: 'real_name',
28 avatarIndex: 'avatar',
29 roleIndex: 'identity',
30 }
31 );
32
33 const getUser = (record: object): Pick<User, 'id' | 'nick_name' | 'real_name' | 'identity'> | undefined => {
34 if (isUndefined(props.user)) {
35 return record as User;
36 }
37
38 if (isString(props.user)) {
39 return get(record, props.user);
40 }
41
42 return get(props, 'user') as User;
43 };
44
45 const getName = (record: object): string => {
46 const user = getUser(record);
47
48 if (!user) {
49 return '';
50 }
51
52 if (props.showRealName) {
53 return `${get(user, props.nickIndex, '')}(${get(user, props.realIndex)})`;
54 }
55
56 return get(user, props.nickIndex, '');
57 };
58 </script>
59
60 <template>
61 <TableColumn v-bind="$attrs" :title="title" :data-index="dataIndex">
62 <template #default="{ record }">
63 <template v-if="darkValue !== undefined && getName(record) === darkValue">
64 <span style="color: rgba(44, 44, 44, 0.5)"></span>
65 </template>
66 <template v-else>
67 <Avatar v-if="showAvatar" style="margin-right: 8px" :size="34" shape="circle" :image-url="getUser(record)?.[avatarIndex]" />
68 {{ getName(record) }}
69 </template>
70 </template>
71 </TableColumn>
72 </template>
73
74 <style scoped lang="less"></style>
1 <template>
2 <Button v-bind="$attrs" :size="size" :type="type" :loading="loading" :status="status" @click="() => emits('click')">
3 <template v-if="icon && iconAlign === 'left'" #icon>
4 <component :is="Icon[iconName]" />
5 </template>
6 {{ label }}
7 <component :is="Icon[iconName]" v-if="icon && iconAlign === 'right'" style="margin-left: 6px" />
8 </Button>
9 </template>
10
11 <script lang="ts" setup>
12 import { Button } from '@arco-design/web-vue';
13 import * as Icon from '@arco-design/web-vue/es/icon';
14 import { computed } from 'vue';
15 import { camelCase, upperFirst } from 'lodash';
16
17 const props = withDefaults(
18 defineProps<{
19 loading?: boolean;
20 label?: string;
21 icon?: string;
22 iconAlign?: 'left' | 'right';
23 type?: 'primary' | 'secondary' | 'outline' | 'dashed' | 'text';
24 shape?: 'square' | 'round' | 'circle';
25 status?: 'normal' | 'warning' | 'success' | 'danger';
26 size?: 'mini' | 'small' | 'medium' | 'large';
27 }>(),
28 {
29 loading: false,
30 iconAlign: 'left',
31 type: 'secondary',
32 shape: 'square',
33 status: 'normal',
34 size: 'small',
35 }
36 );
37
38 const emits = defineEmits(['click']);
39
40 const iconName = computed(() => upperFirst(camelCase(`icon-${props.icon}`)));
41 </script>
42
43 <style scoped></style>
1 <template>
2 <a-upload
3 :accept="accept"
4 :custom-request="onUpload"
5 :file-list="[]"
6 :show-file-list="false"
7 >
8 <template #upload-button>
9 <div class="arco-upload-list-item">
10 <a-image
11 v-if="modelValue"
12 :height="height"
13 :preview="false"
14 :src="modelValue"
15 :width="width"
16 />
17 <div v-else :style="style" class="arco-upload-picture-card">
18 <div class="arco-upload-picture-card-text">
19 <IconPlus />
20 </div>
21 </div>
22 </div>
23 </template>
24 </a-upload>
25 </template>
26
27 <script lang="ts" setup>
28 import useOss from '@/hooks/oss';
29 import { computed } from 'vue';
30 import { IconPlus } from '@arco-design/web-vue/es/icon';
31 import { UploadRequest, useFormItem } from '@arco-design/web-vue';
32
33 const props = defineProps({
34 modelValue: {
35 type: String,
36 default: '',
37 },
38 size: {
39 type: Number,
40 default: 80,
41 },
42 accept: {
43 type: String,
44 default: 'image/*',
45 },
46 width: {
47 type: Number,
48 default: 80,
49 },
50 height: {
51 type: [Number, String],
52 default: 'auto',
53 },
54 });
55
56 const emits = defineEmits(['update:modelValue']);
57 const { eventHandlers } = useFormItem();
58
59 const style = computed(() => {
60 return {
61 width: `${props.width}px`,
62 height: props.height.constructor === String ? 'auto' : `${props.height}px`,
63 maxWidth: '100%',
64 maxHeight: '100%',
65 minHeight: '80px',
66 };
67 });
68
69 const { upload } = useOss();
70
71 const onUpload = (option: any): UploadRequest => {
72 const { fileItem } = option;
73
74 if (fileItem.file) {
75 upload(fileItem.file, 'image').then((res) => {
76 emits('update:modelValue', res.url);
77 eventHandlers.value?.onChange?.();
78 });
79 }
80
81 return {};
82 };
83 </script>
84
85 <style lang="less" scoped>
86 :deep(.arco-avatar-text) {
87 width: 100%;
88 height: 100%;
89 }
90
91 .no-avatar {
92 position: absolute;
93 display: flex;
94 align-content: center;
95 align-items: center;
96 justify-content: center;
97 width: 100%;
98 height: 100%;
99 top: 0;
100 left: 0;
101 }
102 </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 AudioPlayer from '@/components/audio-player/index.vue';
7 import Chart from './chart/index.vue';
8 import Breadcrumb from './breadcrumb/index.vue';
9 import AvatarUpload from './avatar-upload/index.vue';
10 import InputUpload from './input-upload/index.vue';
11 import ImageUpload from './image-upload/index.vue';
12 import RouterButton from './router-button/index.vue';
13 import ExportButton from './export-button/index.vue';
14 import IconButton from './icon-button/index.vue';
15 import ProjectSelect from './project-select/index.vue';
16 import TagSelect from './tag-select/index.vue';
17 import UserSelect from './user-select/index.vue';
18 import FilterSearch from './filter/search.vue';
19 import FilterSearchItem from './filter/search-item.vue';
20 import FilterTable from './filter/table.vue';
21 import FilterTableColumn from './filter/table-column.vue';
22 import PageView from './page-view/index.vue';
23
24 // import SvgIcon from './svg-icon/index.vue';
25
26 // Manually introduce ECharts modules to reduce packing size
27
28 use([
29 CanvasRenderer,
30 BarChart,
31 LineChart,
32 PieChart,
33 RadarChart,
34 GridComponent,
35 TooltipComponent,
36 LegendComponent,
37 DataZoomComponent,
38 GraphicComponent,
39 ]);
40
41 export default {
42 install(Vue: App) {
43 Vue.component('Chart', Chart);
44 Vue.component('Breadcrumb', Breadcrumb);
45 Vue.component('AvatarUpload', AvatarUpload);
46 Vue.component('InputUpload', InputUpload);
47 Vue.component('ImageUpload', ImageUpload);
48 Vue.component('RouterButton', RouterButton);
49 Vue.component('ExportButton', ExportButton);
50 Vue.component('IconButton', IconButton);
51 Vue.component('AudioPlayer', AudioPlayer);
52 Vue.component('ProjectSelect', ProjectSelect);
53 Vue.component('TagSelect', TagSelect);
54 Vue.component('UserSelect', UserSelect);
55 Vue.component('FilterSearch', FilterSearch);
56 Vue.component('FilterSearchItem', FilterSearchItem);
57 Vue.component('FilterTable', FilterTable);
58 Vue.component('FilterTableColumn', FilterTableColumn);
59 Vue.component('PageView', PageView);
60 // Vue.component('SvgIcon', SvgIcon);
61 },
62 };
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, watch } 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
28 type FileType = { name: string; url: string; size: number; type: string };
29
30 const props = defineProps({
31 modelValue: { type: String, default: '' },
32 prefix: { type: String, default: 'file' },
33 limit: { type: Number, default: 0 },
34 placeholder: { type: String, default: '请选择' },
35 });
36
37 const emits = defineEmits<{
38 (e: 'update:modelValue', value: string): void;
39 (e: 'update:loading', value: boolean): 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) => {
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, 'audio/') || startsWith(file.type, 'video/')) {
56 // const audioElement = new Audio(URL.createObjectURL(file));
57 // audioElement.addEventListener('loadedmetadata', () => emits('update:duration', audioElement.duration * 1000));
58 // }
59
60 return Promise.resolve(file);
61 };
62
63 const onProgress = (p: number) => {
64 percent.value = p;
65 };
66
67 const onUpload = (option: any): UploadRequest => {
68 const { fileItem } = option;
69
70 if (fileItem.file) {
71 setLoading(true);
72 // eslint-disable-next-line vue/custom-event-name-casing
73 emits('choose-file', fileItem.file as File);
74 upload(fileItem.file, props.prefix, onProgress)
75 .then((res) => {
76 emits('update:modelValue', res?.url || '');
77 // emits('change', res?.url || '');
78 eventHandlers.value?.onChange?.();
79 fileItem.percent = 100;
80 fileItem.url = res?.url || '';
81 fileItem.status = 'done';
82 emits('success', { name: fileItem.name, url: fileItem.url, size: fileItem.file.size, type: fileItem.file.type });
83 })
84 .finally(() => {
85 setLoading(false);
86 });
87 }
88
89 return {};
90 };
91
92 watch(
93 () => loading.value,
94 (value) => emits('update:loading', value)
95 );
96 </script>
97
98 <style lang="less" scoped>
99 ::v-deep(.arco-input-append) {
100 padding: 0;
101 }
102 </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">
11 <a-card v-if="hasCard" :bordered="false">
12 <slot />
13 </a-card>
14 <slot v-else />
15 </a-spin>
16 </div>
17 </template>
18
19 <style scoped lang="less">
20 .container {
21 padding: 0 30px 20px 20px;
22 }
23 </style>
1 <template>
2 <Select
3 v-bind="$attrs"
4 :model-value="modelValue"
5 :options="projectOptions"
6 :fallback-option="false"
7 :placeholder="placeholder"
8 :field-names="{ value: 'id', label: 'name' }"
9 @update:model-value="onUpdate"
10 />
11 </template>
12
13 <script lang="ts" setup>
14 import { Select } from '@arco-design/web-vue';
15 import { toRefs } from 'vue';
16 import { storeToRefs } from 'pinia';
17 import { useSelectionStore } from '@/store';
18
19 type propType = {
20 modelValue?: number | string | number[];
21 multiple?: boolean;
22 placeholder?: string;
23 maxTagCount?: number;
24 };
25
26 const props = withDefaults(defineProps<propType>(), {
27 multiple: false,
28 maxTagCount: 0,
29 placeholder: '请选择',
30 modelValue: '',
31 });
32
33 const emits = defineEmits<{ (e: 'update:modelValue', value: number | number[]): void }>();
34
35 const { multiple } = toRefs(props);
36
37 const { projectOptions } = storeToRefs(useSelectionStore());
38
39 const onUpdate = (val?: number) => emits('update:modelValue', val || (multiple.value ? [] : 0));
40 </script>
41
42 <style scoped></style>
1 <template>
2 <a-button v-if="type === 'Button'" type="text" size="small" @click="onClick">
3 <slot>详情</slot>
4 </a-button>
5 <a-link v-else class="link" :hoverable="false" @click="onClick">
6 <a-typography-text type="primary" :style="textStyle" :ellipsis="{ rows: 1, showTooltip: showTooltip }">
7 <slot>详情</slot>
8 </a-typography-text>
9 </a-link>
10 </template>
11
12 <script lang="ts" setup>
13 import { RouteParamsRaw, useRouter } from 'vue-router';
14 import { AnyObject } from '@/types/global';
15
16 const props = withDefaults(
17 defineProps<{
18 name: string;
19 params?: AnyObject;
20 type: 'Button' | 'link';
21 showTooltip: boolean;
22 }>(),
23 {
24 name: '',
25 type: 'Button',
26 params: undefined,
27 showTooltip: true,
28 }
29 );
30
31 const textStyle = { marginBottom: 0 };
32
33 const router = useRouter();
34 const onClick = () => {
35 router.push({
36 name: props.name,
37 params: (props.params as RouteParamsRaw) || {},
38 });
39 };
40 </script>
41
42 <style lang="less" scoped>
43 .link {
44 width: 100%;
45 padding: 1px 0;
46 display: block !important;
47 }
48 </style>
1 <template>
2 <Select
3 v-bind="$attrs"
4 v-model="value"
5 :options="options"
6 :fallback-option="false"
7 :placeholder="placeholder"
8 :virtual-list-props="{ height: 200 }"
9 :field-names="{ value: 'id', label: 'name' }"
10 @exceed-limit="onTagExceedLimitError"
11 />
12 </template>
13
14 <script lang="ts" setup>
15 import { Message, Select } from '@arco-design/web-vue';
16 import { computed, onMounted } from 'vue';
17 import { Tag } from '@/types/tag';
18 import { storeToRefs } from 'pinia';
19 import { useSelectionStore } from '@/store';
20 import { isArray } from '@arco-design/web-vue/es/_utils/is';
21
22 interface propType {
23 modelValue?: number | string | number[];
24 placeholder?: string;
25 }
26
27 const props = withDefaults(defineProps<propType>(), { placeholder: '请选择' });
28 const emits = defineEmits<{ (e: 'update:modelValue', value: unknown): void }>();
29
30 const onTagExceedLimitError = () => Message.warning({ content: '关联标签最多选中3个', duration: 1500 });
31
32 const value = computed({
33 get: () => props.modelValue,
34 set: (val) => emits('update:modelValue', val),
35 });
36
37 const { getTagOptions } = storeToRefs(useSelectionStore());
38 const options = computed(() => getTagOptions.value.filter((item: Tag) => item.type === 1));
39
40 const tagIds = computed(() => options.value?.map((item: Tag) => item.id));
41
42 onMounted(() => {
43 if (isArray(props.modelValue)) {
44 value.value = props.modelValue?.filter((item: number) => tagIds.value?.indexOf(item) !== -1) || [];
45 }
46 });
47 </script>
48
49 <style scoped></style>
1 <script setup lang="ts">
2 import { ref } from 'vue';
3
4 import { Select } from '@arco-design/web-vue';
5 import { useSelectionStore } from '@/store';
6 import { storeToRefs } from 'pinia';
7
8 const { getUserOptions } = storeToRefs(useSelectionStore());
9
10 const loading = ref<boolean>(false);
11 const fieldName = { value: 'id', label: 'nick_name' };
12 </script>
13
14 <template>
15 <Select
16 v-bind="$attrs"
17 placeholder="请选择"
18 :loading="loading"
19 :multiple="true"
20 :options="getUserOptions"
21 :field-names="fieldName"
22 :allow-search="true"
23 :virtual-list-props="{ height: 200 }"
24 />
25 </template>
26
27 <style scoped lang="less"></style>
1 {
2 "theme": "light",
3 "colorWeek": false,
4 "navbar": true,
5 "menu": true,
6 "menuCollapse": false,
7 "footer": false,
8 "themeColor": "#165DFF",
9 "menuWidth": 220,
10 "globalSettings": false
11 }
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 { Activity } from '@/types/activity';
2 import { createVNode, ref } from 'vue';
3 import { Button, Form, FormItem, Input, Modal } from '@arco-design/web-vue';
4 import { clone, compact, trim } from 'lodash';
5 import { IconDelete, IconPlus } from '@arco-design/web-vue/es/icon';
6 import { AnyObject } from '@/types/global';
7
8 export default function useActivityHook() {
9 const updateStatusToSend = (activity: Activity, callback: (done: (closed: boolean) => void, attribute: AnyObject) => void) => {
10 const oldLink = clone(activity.send_url?.[0]?.url || '');
11 const link = ref(oldLink);
12 const title = link.value.length === 0 ? '发行' : '编辑发行';
13
14 return Modal.open({
15 title,
16 content: () =>
17 createVNode(
18 Form,
19 { model: link, autoLabelWidth: true },
20 {
21 default: () => [
22 createVNode(
23 'div',
24 {
25 style: {
26 fontFamily: 'PingFangSC-Regular, serif',
27 paddingBottom: '16px',
28 color: '#696969',
29 },
30 },
31 '将歌曲发行平台的url链接回填,会展示在App或小程序应用端。将会给您带来更多播放量 ~'
32 ),
33 createVNode(
34 FormItem,
35 {
36 label: 'QQ音乐或酷狗平台链接',
37 rowClass: 'mb-0',
38 required: true,
39 },
40 {
41 default: () =>
42 createVNode(Input, {
43 'modelValue': link.value,
44 'size': 'small',
45 'onUpdate:modelValue': (val?: string) => {
46 link.value = val || '';
47 },
48 }),
49 }
50 ),
51 ],
52 }
53 ),
54 closable: false,
55 width: 'auto',
56 onBeforeOk: (done) => {
57 if (trim(link.value).length === 0) {
58 Modal.open({ title: '提醒', content: '请输入:QQ音乐或酷狗平台链接', closable: false, hideCancel: true, escToClose: false });
59 return done(false);
60 }
61
62 if (link.value.toString() === oldLink.toString()) {
63 return done(true);
64 }
65
66 if (!/^(((ht|f)tps?):\/\/)?([^!@#$%^&*?.\s-]([^!@#$%^&*?.\s]{0,63}[^!@#$%^&*?.\s])?\.)+[a-z]{2,6}\/?/.test(link.value)) {
67 Modal.open({
68 title: '提醒',
69 content: 'url链接无法被识别,请检查后重试',
70 closable: false,
71 hideCancel: true,
72 escToClose: false,
73 });
74
75 return done(false);
76 }
77 return callback(done, { status: 5, link: link.value });
78 },
79 });
80 };
81
82 const updateStatusToReUp = (activity: Activity, callback: (done: (closed: boolean) => void, attribute: AnyObject) => void) => {
83 return Modal.open({
84 title: '是否确认重新上架',
85 titleAlign: 'start',
86 content: '注意:重新上架会保留原参与试唱的信息',
87 closable: false,
88 escToClose: false,
89 okText: '上架',
90 onBeforeOk: (done) => {
91 return callback(done, { status: 1 });
92 },
93 });
94 };
95
96 const updateStatusToDown = (activity: Activity, callback: (done: (closed: boolean) => void, attribute: AnyObject) => void) => {
97 const msg = ref<string>('');
98
99 return Modal.open({
100 title: '变更状态',
101 titleAlign: 'start',
102 content: () =>
103 createVNode(Input, {
104 'placeholder': '请输入下架原因',
105 'maxLength': 20,
106 'show-word-limit': true,
107 'modelValue': msg.value,
108 'onUpdate:modelValue': (val?: string) => {
109 msg.value = val || '';
110 },
111 }),
112 simple: false,
113 closable: false,
114 escToClose: false,
115 maskClosable: false,
116 okText: '下架',
117 onBeforeOk: (done) => {
118 return callback(done, { status: 2, msg: msg.value });
119 },
120 });
121 };
122
123 const updateStatusToUp = (activity: Activity, callback: (done: (closed: boolean) => void, attribute: AnyObject) => void) => {
124 return Modal.open({
125 title: '变更状态',
126 titleAlign: 'start',
127 content: `请确认是否上架活动:${activity.song_name}`,
128 simple: false,
129 closable: false,
130 escToClose: false,
131 okText: '上架',
132 onBeforeOk: (done) => {
133 return callback(done, { status: 1 });
134 },
135 });
136 };
137
138 const updateRecommendIntro = (activity: Activity, callback: (done: (closed: boolean) => void, attribute: AnyObject) => void) => {
139 const oldIntro = activity.recommend_intros?.map((item) => item.content) || [];
140
141 const intro = ref<string[]>(oldIntro.length === 0 ? [''] : clone(oldIntro));
142
143 const InputVNode = (index: number) =>
144 createVNode(Input, {
145 'modelValue': intro.value[index],
146 'onUpdate:modelValue': (val?: string) => {
147 intro.value[index] = val || '';
148 },
149 });
150
151 const DeleteBtnVNode = (index: number) =>
152 createVNode(
153 Button,
154 {
155 style: { marginLeft: '10px' },
156 onClick: () => intro.value.splice(index, 1),
157 },
158 { icon: () => createVNode(IconDelete) }
159 );
160
161 const InputItemVNode = (index: number) =>
162 createVNode(
163 FormItem,
164 { label: `推荐语${index + 1}`, required: true },
165 {
166 default: () => (intro.value.length === 1 ? InputVNode(index) : [InputVNode(index), DeleteBtnVNode(index)]),
167 }
168 );
169
170 const AddItemVNode = createVNode(
171 FormItem,
172 {},
173 {
174 default: () =>
175 createVNode(
176 Button,
177 {
178 long: false,
179 type: 'primary',
180 size: 'small',
181 onClick: () => intro.value.push(''),
182 },
183 {
184 icon: () => createVNode(IconPlus),
185 default: () => '添加',
186 }
187 ),
188 }
189 );
190
191 return Modal.open({
192 title: '推荐',
193 content: () =>
194 createVNode(
195 Form,
196 { model: intro, autoLabelWidth: true },
197 {
198 default: () => {
199 const children = intro.value.map((value: any, index: number) => InputItemVNode(index));
200 if (children.length < 3) {
201 children.push(AddItemVNode);
202 }
203 return children;
204 },
205 }
206 ),
207 closable: false,
208 // @ts-ignore
209 bodyStyle: { padding: '24px 20px 0' },
210 onBeforeOk: (done) => {
211 if (oldIntro.toString() === intro.value.toString()) {
212 return done(true);
213 }
214
215 if (intro.value.length !== compact(intro.value).length) {
216 return done(false);
217 }
218
219 return callback(done, { intro: intro.value });
220 },
221 });
222 };
223
224 // const activity
225
226 return {
227 updateStatusToReUp,
228 updateStatusToSend,
229 updateStatusToDown,
230 updateStatusToUp,
231 updateRecommendIntro,
232 };
233 }
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 { computed } from 'vue';
2 import { useI18n } from 'vue-i18n';
3 import { Message } from '@arco-design/web-vue';
4
5 export default function useLocale() {
6 const i18 = useI18n();
7 const currentLocale = computed(() => {
8 return i18.locale.value;
9 });
10 const changeLocale = (value: string) => {
11 i18.locale.value = value;
12 localStorage.setItem('arco-locale', value);
13 Message.success(i18.t('navbar.action.locale'));
14 };
15 return {
16 currentLocale,
17 changeLocale,
18 };
19 }
1 import OSS from 'ali-oss';
2 import { ossConfig } from '@/utils/env';
3
4 const oss = new OSS({
5 accessKeyId: ossConfig.accessKeyId,
6 accessKeySecret: ossConfig.accessKeySecret,
7 bucket: ossConfig.bucket,
8 region: ossConfig.region,
9 endpoint: ossConfig.endpoint,
10 cname: true,
11 });
12
13 export default function useOss() {
14 const getFileName = () => {
15 function rx() {
16 return Math.random().toString(36).substring(2);
17 }
18
19 return `${rx()}${new Date().getTime()}${rx()}`;
20 };
21
22 const getFileDir = () => {
23 return new Date().toISOString().slice(0, 10).replace(/-/g, '');
24 };
25
26 const getFileType = (file: File) => {
27 return file.name?.split('.')?.pop()?.toLowerCase();
28 };
29
30 const getHost = () => {
31 if (ossConfig.host && ossConfig.host.length !== 0) {
32 return ossConfig.host;
33 }
34
35 return ossConfig.endpoint;
36 };
37
38 return {
39 upload(file: File, prefix = 'file', onProgress?: (percent: number) => void) {
40 return oss
41 .multipartUpload(`${prefix}/${getFileDir()}/${getFileName()}.${getFileType(file)}`, file, { progress: onProgress })
42 .then((res) => {
43 return { response: res.res, url: `${getHost()}/${res.name}` };
44 });
45 },
46 };
47 }
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 authorizedStore = 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 authorizedStore.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 };
27 }
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 { LocationQueryRaw, useRouter } from 'vue-router';
2 import { useAuthorizedStore } from '@/store';
3 import { createVNode, h, ref } from 'vue';
4 import { FormInstance } from '@arco-design/web-vue/es/form';
5 import { Form, FormItem, InputPassword, Message, Modal } from '@arco-design/web-vue';
6 import useAuthApi from '@/api/auth';
7
8 export default function useUser() {
9 const router = useRouter();
10 const userStore = useAuthorizedStore();
11
12 const logout = async () => {
13 await userStore.logout();
14 const currentRoute = router.currentRoute.value;
15 await router.push({
16 name: 'login',
17 query: { path: currentRoute.path } as LocationQueryRaw,
18 });
19 };
20
21 const resetPwd = () => {
22 const pwdRef = ref<FormInstance>();
23 const pwdVal = ref({ password: '', password_confirmation: '' });
24 const pwdRules = {
25 password: [{ required: true, message: '请输入密码' }],
26 password_confirmation: [{ required: true, message: '请输入密码' }],
27 };
28
29 const createPwdVNode = (label: string, field: 'password' | 'password_confirmation') =>
30 createVNode(
31 FormItem,
32 { label, field, rowClass: field === 'password_confirmation' ? 'mb-0' : '' },
33 {
34 default: () =>
35 h(InputPassword, {
36 'modelValue': pwdVal.value[field],
37 // eslint-disable-next-line no-return-assign
38 'onUpdate:modelValue': (val?: string) => (pwdVal.value[field] = val || ''),
39 }),
40 }
41 );
42
43 return Modal.open({
44 title: '设置密码',
45 content: () =>
46 h(
47 Form,
48 { ref: pwdRef, model: pwdVal, rules: pwdRules, autoLabelWidth: true },
49 { default: () => [createPwdVNode('新密码', 'password'), createPwdVNode('确认密码', 'password_confirmation')] }
50 ),
51 closable: false,
52 maskClosable: false,
53 escToClose: false,
54 // eslint-disable-next-line no-shadow
55 onBeforeOk: (done: (closed: boolean) => void) => {
56 useAuthApi
57 .changePwd(pwdVal.value)
58 .then(() => {
59 Message.success('更新成功');
60 done(true);
61 })
62 .catch(() => {
63 done(false);
64 });
65 },
66 onClose: () => {
67 pwdVal.value = { password: '', password_confirmation: '' };
68 },
69 });
70 };
71
72 return {
73 logout,
74 resetPwd,
75 };
76 }
1 <script lang="tsx">
2 import { compile, computed, defineComponent, h, ref, watch } from 'vue';
3
4 import { RouteRecordRaw, useRoute, useRouter } from 'vue-router';
5 import { useAppStore } from '@/store';
6 import usePermission from '@/hooks/permission';
7 import { last, orderBy } from 'lodash';
8 import { storeToRefs } from 'pinia';
9
10 export default defineComponent({
11 emit: ['collapse'],
12 setup() {
13 const appStore = useAppStore();
14 const permission = usePermission();
15 const router = useRouter();
16 const route = useRoute();
17
18 const { appMenu } = storeToRefs(appStore);
19
20 const collapsed = ref(false);
21 const selectedKey = ref<string[]>([]);
22 const openKey = ref<string[]>([]);
23 const goto = (item: RouteRecordRaw) => router.push({ name: item.name });
24
25 const syncServicePermission = (item: RouteRecordRaw) => {
26 if (item.meta) {
27 const serverConfig = appMenu.value.find((menu) => item.name === menu.name);
28 item.meta.title = serverConfig?.label || item.meta?.title || '';
29 item.meta.order = serverConfig?.weight || item.meta?.order || 0;
30 item.meta.icon = serverConfig?.icon || item.meta?.icon || '';
31 }
32 item.children?.map((child) => syncServicePermission(child));
33 return item;
34 };
35
36 const appRoute = computed((): RouteRecordRaw[] => {
37 return router
38 .getRoutes()
39 .find((el) => el.name === 'root')
40 ?.children.filter((element) => element.meta?.hideInMenu !== true)
41 .map((item: RouteRecordRaw) => syncServicePermission(item)) as RouteRecordRaw[];
42 });
43
44 const menuTree = computed((): RouteRecordRaw[] => {
45 function travel(_routes: RouteRecordRaw[], layer: number) {
46 if (!_routes) return null;
47 const collector: any = orderBy(_routes, 'meta.order', 'desc').map((element) => {
48 // no access
49 if (!permission.accessRouter(element)) {
50 return null;
51 }
52
53 // leaf node
54 if (!element.children) {
55 return element;
56 }
57
58 // route filter hideInMenu true
59 element.children = element.children.filter((x) => x.meta?.hideInMenu !== true);
60
61 // Associated child node
62 const subItem = travel(element.children, layer);
63 if (subItem.length) {
64 element.children = subItem;
65 return element;
66 }
67 // the else logic
68 if (layer > 1) {
69 element.children = subItem;
70 return element;
71 }
72
73 if (element.meta?.hideInMenu === false) {
74 return element;
75 }
76
77 return null;
78 });
79 return collector.filter(Boolean);
80 }
81
82 return travel(appRoute.value, 0);
83 });
84
85 watch(
86 route,
87 (newVal) => {
88 if (newVal.meta.requiresAuth) {
89 const key = newVal.meta.hideInMenu ? last(newVal.matched)?.meta?.menuSelectKey : last(newVal.matched)?.name;
90 selectedKey.value = [key as string];
91 openKey.value = [...(newVal.meta.breadcrumb || [])];
92 }
93 },
94 { immediate: true }
95 );
96 watch(
97 () => appStore.menuCollapse,
98 (newVal) => {
99 collapsed.value = newVal;
100 },
101 { immediate: true }
102 );
103 const setCollapse = (val: boolean) => {
104 appStore.updateSettings({ menuCollapse: val });
105 };
106
107 const renderSubMenu = () => {
108 function travel(_route: RouteRecordRaw[], nodes = []) {
109 if (_route) {
110 _route.forEach((element) => {
111 // This is demo, modify nodes as needed
112 const icon = element?.meta?.icon ? `<${element?.meta?.icon}/>` : ``;
113 let r;
114
115 if (element && element.children && element.children.length !== 0) {
116 r = (
117 <a-sub-menu key={element?.name} title={element.meta?.title} v-slots={{ icon: () => h(compile(icon)) }}>
118 {element?.children?.map((elem) => {
119 return (
120 <a-menu-item key={elem.name} onClick={() => goto(elem)}>
121 {elem.meta?.title}
122 {travel(elem.children ?? [])}
123 </a-menu-item>
124 );
125 })}
126 </a-sub-menu>
127 );
128 } else {
129 r = (
130 <a-menu-item key={element.name} v-slots={{ icon: () => h(compile(icon)) }} onClick={() => goto(element)}>
131 {element.meta?.title}
132 </a-menu-item>
133 );
134 }
135 nodes.push(r as never);
136 });
137 }
138 return nodes;
139 }
140
141 return travel(menuTree.value);
142 };
143
144 return () => (
145 <a-menu
146 v-model:collapsed={collapsed.value}
147 show-collapse-button
148 auto-open={false}
149 v-model:selected-keys={selectedKey.value}
150 v-model:open-keys={openKey.value}
151 auto-open-selected={true}
152 auto-scroll-into-view={true}
153 level-indent={34}
154 style="height: 100%"
155 onCollapse={setCollapse}
156 >
157 {renderSubMenu()}
158 </a-menu>
159 );
160 },
161 });
162 </script>
163
164 <style lang="less" scoped>
165 :deep(.arco-menu-inner) {
166 .arco-menu-inline-header {
167 display: flex;
168 align-items: center;
169 }
170
171 .arco-icon {
172 &:not(.arco-icon-down) {
173 font-size: 18px;
174 }
175 }
176 }
177 </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">
44 <img :src="avatar" alt="avatar" />
45 </a-avatar>
46
47 <span
48 :style="{
49 color: theme === 'dark' ? 'white' : 'black',
50 lineHeight: '31px',
51 }"
52 >
53 {{ name }}
54 <icon-down />
55 </span>
56 </a-space>
57 <template #content>
58 <!-- <a-doption>-->
59 <!-- <a-space @click="switchRoles">-->
60 <!-- <icon-tag />-->
61 <!-- <span>-->
62 <!-- {{ $t('messageBox.switchRoles') }}-->
63 <!-- </span>-->
64 <!-- </a-space>-->
65 <!-- </a-doption>-->
66 <a-doption>
67 <a-space @click="handleChangePwd">
68 <icon-lock />
69 <span>修改密码</span>
70 </a-space>
71 </a-doption>
72 <a-doption>
73 <a-space @click="handleLogout">
74 <icon-export />
75 <span> 退出登录 </span>
76 </a-space>
77 </a-doption>
78 </template>
79 </a-dropdown>
80 </li>
81 </ul>
82 </div>
83 </template>
84
85 <script lang="ts" setup>
86 import { computed } from 'vue';
87 import { useAppStore, useAuthorizedStore } from '@/store';
88 import useUser from '@/hooks/user';
89 // import ToggleThemeButton from '@/layout/components/toggle-theme-button.vue';
90 import { IconDown, IconExport, IconLock } from '@arco-design/web-vue/es/icon';
91
92 const appStore = useAppStore();
93 const authorized = useAuthorizedStore();
94 const { logout } = useUser();
95
96 const avatar = computed(() => {
97 return authorized.avatar;
98 });
99
100 const name = computed(() => {
101 return authorized.nick_name;
102 });
103
104 const theme = computed(() => {
105 return appStore.theme;
106 });
107
108 // const setVisible = () => {
109 // appStore.updateSettings({ globalSettings: true });
110 // };
111 // const refBtn = ref();
112 // const triggerBtn = ref();
113 // const setPopoverVisible = () => {
114 // const event = new MouseEvent('click', {
115 // view: window,
116 // bubbles: true,
117 // cancelable: true,
118 // });
119 // refBtn.value.dispatchEvent(event);
120 // };
121 const handleLogout = () => {
122 logout();
123 };
124
125 const handleChangePwd = () => {
126 useUser().resetPwd();
127 };
128 // const setDropDownVisible = () => {
129 // const event = new MouseEvent('click', {
130 // view: window,
131 // bubbles: true,
132 // cancelable: true,
133 // });
134 // triggerBtn.value.dispatchEvent(event);
135 // };
136 </script>
137
138 <style lang="less" scoped>
139 .navbar {
140 display: flex;
141 justify-content: space-between;
142 height: 100%;
143 background-color: var(--color-bg-2);
144 border-bottom: 1px solid var(--color-border);
145 }
146
147 .left-side {
148 display: flex;
149 align-items: center;
150 padding-left: 20px;
151 }
152
153 .right-side {
154 display: flex;
155 padding-right: 10px;
156 list-style: none;
157
158 :deep(.locale-select) {
159 border-radius: 20px;
160 }
161
162 li {
163 display: flex;
164 align-items: center;
165 padding: 0 10px;
166 }
167
168 a {
169 color: var(--color-text-1);
170 text-decoration: none;
171 }
172
173 .nav-btn {
174 border-color: rgb(var(--gray-2));
175 color: rgb(var(--gray-8));
176 font-size: 16px;
177 }
178
179 .trigger-btn,
180 .ref-btn {
181 position: absolute;
182 bottom: 14px;
183 }
184
185 .trigger-btn {
186 margin-left: 14px;
187 }
188 }
189 </style>
190
191 <style lang="less">
192 .message-popover {
193 .arco-popover-content {
194 margin-top: 0;
195 }
196 }
197
198 .arco-dropdown-open .arco-icon-down {
199 transform: rotate(180deg);
200 }
201 </style>
1 <template>
2 <a-tooltip :content="theme === 'light' ? '切换为暗黑模式' : '切换为亮色模式'">
3 <a-button
4 :shape="'circle'"
5 class="nav-btn"
6 type="outline"
7 @click="() => onChange()"
8 >
9 <template #icon>
10 <icon-moon-fill v-if="theme === 'dark'" />
11 <icon-sun-fill v-else />
12 </template>
13 </a-button>
14 </a-tooltip>
15 </template>
16
17 <script lang="ts">
18 import { computed, defineComponent } from 'vue';
19 import { useAppStore } from '@/store';
20 import { useDark, useToggle } from '@vueuse/core';
21
22 import { IconMoonFill, IconSunFill } from '@arco-design/web-vue/es/icon';
23
24 export default defineComponent({
25 name: 'ToggleThemeButton',
26 components: {
27 IconSunFill,
28 IconMoonFill,
29 },
30 setup() {
31 const appStore = useAppStore();
32
33 const theme = computed(() => {
34 return appStore.theme;
35 });
36
37 const isDark = useDark({
38 selector: 'body',
39 attribute: 'arco-theme',
40 valueDark: 'dark',
41 valueLight: 'light',
42 storageKey: 'arco-theme',
43 onChanged(dark: boolean) {
44 appStore.toggleTheme(dark);
45 },
46 });
47 const onChange = useToggle(isDark);
48
49 return {
50 theme,
51 onChange,
52 };
53 },
54 });
55 </script>
56
57 <style lang="less" scoped>
58 .nav-btn {
59 border-color: rgb(var(--gray-2));
60 color: rgb(var(--gray-8));
61 font-size: 16px;
62 }
63
64 .trigger-btn,
65 .ref-btn {
66 position: absolute;
67 bottom: 14px;
68 }
69
70 .trigger-btn {
71 margin-left: 14px;
72 }
73 </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" name="layout">
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: 'exception-403' });
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 router from './router';
6 import store from './store';
7 import directive from './directive';
8 import App from './App.vue';
9 import '@arco-design/web-vue/dist/arco.css';
10 import '@/assets/style/global.less';
11 import '@/api/interceptor';
12
13 const app = createApp(App);
14
15 app.use(ArcoVue, {});
16 app.use(ArcoVueIcon);
17
18 app.use(store);
19 app.use(router);
20 app.use(globalComponents);
21 app.use(directive);
22
23 app.mount('#app');
1 import { createRouter, createWebHistory, LocationQueryRaw } from 'vue-router';
2 import NProgress from 'nprogress'; // progress bar
3 import 'nprogress/nprogress.css';
4 import usePermission from '@/hooks/permission';
5 import { clearToken, isLogin } from '@/utils/auth';
6 import PageLayout from '@/layout/index.vue';
7 import { useAuthorizedStore } from '@/store';
8 import appRoutes from './modules';
9
10 NProgress.configure({ showSpinner: false }); // NProgress Configuration
11
12 const router = createRouter({
13 history: createWebHistory(),
14 routes: [
15 {
16 path: '/login',
17 name: 'login',
18 component: () => import('@/views/login/index.vue'),
19 meta: {
20 title: '登陆',
21 requiresAuth: false,
22 },
23 },
24 {
25 name: 'root',
26 path: '/',
27 component: PageLayout,
28 redirect: 'dashboard',
29 children: appRoutes,
30 },
31 {
32 path: '/:pathMatch(.*)*',
33 name: 'notFound',
34 redirect: '/exception/404',
35 meta: {
36 requiresAuth: true,
37 },
38 },
39 ],
40 scrollBehavior() {
41 return { top: 0 };
42 },
43 });
44
45 router.beforeEach(async (to, from, next) => {
46 NProgress.start();
47 const authorizedStore = useAuthorizedStore();
48
49 if (from.name !== undefined) {
50 to.meta.from = from.name;
51 }
52
53 async function crossroads() {
54 const Permission = usePermission();
55
56 if (Permission.accessRouter(to)) {
57 next();
58 } else {
59 next({ name: 'exception-403' });
60 }
61 NProgress.done();
62 }
63
64 if (isLogin()) {
65 if (authorizedStore.permissions.length) {
66 await crossroads();
67 } else {
68 try {
69 await authorizedStore.syncInfo();
70 await crossroads();
71 } catch (error) {
72 clearToken();
73 next({ name: 'login', query: { path: to.path } as LocationQueryRaw });
74 NProgress.done();
75 }
76 }
77 } else {
78 if (to.name === 'login') {
79 next();
80 NProgress.done();
81 return;
82 }
83
84 if (to.name === 'root') {
85 next({ name: 'login' });
86 } else {
87 next({ name: 'login', query: { path: to.path } as LocationQueryRaw });
88 }
89 NProgress.done();
90 }
91 });
92
93 router.afterEach(async (to) => {
94 sessionStorage.setItem(
95 'route',
96 to.meta?.breadcrumb?.toString() ||
97 to.matched
98 ?.slice(1)
99 .map((item) => item.name)
100 .toString() ||
101 'dashboard'
102 );
103 });
104
105 export default router;
1 export default {
2 path: 'audition',
3 name: 'audition',
4 component: () => import('@/views/audition/index.vue'),
5 meta: {
6 requiresAuth: true,
7 roles: ['*'],
8 },
9 children: [
10 {
11 path: 'applies',
12 name: 'audition-apply',
13 component: () => import('@/views/audition/activity-apply/index.vue'),
14 meta: {
15 requiresAuth: true,
16 hideInMenu: false,
17 isRedirect: true,
18 reload: true,
19 menuSelectKey: 'audition',
20 roles: ['audition-apply'],
21 breadcrumb: ['audition', 'audition-apply'],
22 },
23 },
24 {
25 path: 'activities',
26 name: 'audition-activity',
27 component: () => import('@/views/audition/activity/index.vue'),
28 meta: {
29 icon: 'icon-apps',
30 requiresAuth: true,
31 hideInMenu: false,
32 isRedirect: true,
33 reload: true,
34 roles: ['audition-activity'],
35 menuSelectKey: 'audition',
36 breadcrumb: ['audition', 'audition-activity'],
37 },
38 },
39 {
40 path: 'activities/:id(\\d+)',
41 name: 'audition-activity-show',
42 component: () => import('@/views/audition/activity-show/index.vue'),
43 meta: {
44 requiresAuth: true,
45 hideInMenu: true,
46 reload: true,
47 menuSelectKey: 'audition-activity',
48 roles: ['audition-activity-show'],
49 breadcrumb: ['audition', 'audition-activity', 'audition-activity-show'],
50 },
51 },
52 // {
53 // path: 'demo-applies',
54 // name: 'audition-demo-apply',
55 // component: () => import('@/views/audition/demo-apply/index.vue'),
56 // meta: {
57 // requiresAuth: true,
58 // hideInMenu: false,
59 // isRedirect: true,
60 // reload: true,
61 // menuSelectKey: 'audition',
62 // roles: ['audition-demo-apply'],
63 // breadcrumb: ['audition', 'audition-demo-apply'],
64 // },
65 // },
66 {
67 path: 'demos',
68 name: 'audition-demo',
69 component: () => import('@/views/audition/demo/index.vue'),
70 meta: {
71 icon: 'icon-apps',
72 requiresAuth: true,
73 hideInMenu: false,
74 isRedirect: true,
75 reload: true,
76 roles: ['audition-demo'],
77 menuSelectKey: 'audition',
78 breadcrumb: ['audition', 'audition-demo'],
79 },
80 },
81 {
82 path: 'demos/:id(\\d+)',
83 name: 'audition-demo-show',
84 component: () => import('@/views/audition/demo-show/index.vue'),
85 meta: {
86 requiresAuth: true,
87 hideInMenu: true,
88 reload: true,
89 menuSelectKey: 'audition-demo',
90 roles: ['audition-demo-show'],
91 breadcrumb: ['audition', 'audition-demo', 'audition-demo-show'],
92 },
93 },
94 ],
95 };
1 export default {
2 path: 'dashboard',
3 name: 'dashboard',
4 component: () => import('@/views/dashboard/index.vue'),
5 meta: {
6 title: '信息概览',
7 requiresAuth: true,
8 icon: 'icon-dashboard',
9 roles: ['*'],
10 order: 99999999,
11 breadcrumb: ['dashboard'],
12 },
13 };
1 export default {
2 path: 'exception',
3 name: 'exception',
4 component: () => import('@/views/exception/index.vue'),
5 meta: {
6 title: '异常页',
7 requiresAuth: true,
8 icon: 'icon-exclamation-circle',
9 hideInMenu: true,
10 },
11 children: [
12 {
13 path: '403',
14 name: 'exception-403',
15 component: () => import('@/views/exception/403/index.vue'),
16 meta: {
17 title: '403',
18 requiresAuth: true,
19 roles: ['*'],
20 hideInMenu: true,
21 },
22 },
23 {
24 path: '404',
25 name: 'exception-404',
26 component: () => import('@/views/exception/404/index.vue'),
27 meta: {
28 title: '404',
29 requiresAuth: true,
30 roles: ['*'],
31 hideInMenu: true,
32 },
33 },
34 {
35 path: '500',
36 name: 'exception-500',
37 component: () => import('@/views/exception/500/index.vue'),
38 meta: {
39 title: '500',
40 requiresAuth: true,
41 roles: ['*'],
42 },
43 },
44 ],
45 };
1 import Project from '@/router/modules/project';
2 import Dashboard from '@/router/modules/dashboard';
3 import Exception from '@/router/modules/exception';
4 import Audition from '@/router/modules/audition';
5
6 export default [Dashboard, Exception, Project, Audition];
1 export default {
2 path: 'projects',
3 name: 'project',
4 component: () => import('@/views/project/index.vue'),
5 meta: {
6 requiresAuth: true,
7 hideInMenu: false,
8 isRedirect: true,
9 roles: ['project'],
10 breadcrumb: ['project'],
11 },
12 children: [
13 {
14 path: ':id(\\d+)',
15 name: 'project-show',
16 component: () => import('@/views/project-show/index.vue'),
17 meta: {
18 requiresAuth: true,
19 hideInMenu: true,
20 roles: ['project-show'],
21 menuSelectKey: 'project',
22 breadcrumb: ['project', 'project-show'],
23 },
24 },
25 ],
26 };
1 import 'vue-router';
2
3 declare module 'vue-router' {
4 interface RouteMeta {
5 // options
6 roles?: string[];
7 // every route must declare
8 requiresAuth: boolean; // need login
9 icon?: string;
10 // menu select key
11 menuSelectKey?: string;
12 breadcrumb?: string[];
13 hideInMenu?: boolean;
14 isRedirect?: boolean;
15 title?: string;
16 order?: number;
17 }
18 }
1 import { createPinia } from 'pinia';
2
3 import useAppStore from '@/store/modules/app';
4 import useAuthorizedStore from '@/store/modules/authorized';
5
6 import useSelectionStore from '@/store/modules/selection';
7
8 const pinia = createPinia();
9
10 export { useAppStore, useSelectionStore, useAuthorizedStore };
11
12 export default pinia;
1 import { defineStore } from 'pinia';
2 import { SystemPermission } from '@/types/system-permission';
3 import { AppState } from './types';
4
5 const useAppStore = defineStore('app', {
6 state: (): AppState => ({
7 theme: 'light',
8 colorWeek: false,
9 navbar: true,
10 menu: true,
11 menuCollapse: false,
12 footer: false,
13 themeColor: '#165DFF',
14 menuWidth: 220,
15 globalSettings: false,
16 permissions: [],
17 }),
18
19 getters: {
20 appCurrentSetting(state: AppState): AppState {
21 return { ...state };
22 },
23 appMenu(state: AppState): SystemPermission[] {
24 return state.permissions?.filter((item) => item.guard === 'Manage' && item.type === 'Menu') || [];
25 },
26 },
27
28 actions: {
29 // Update app settings
30 updateSettings(partial: Partial<AppState>) {
31 // @ts-ignore-next-line
32 this.$patch(partial);
33 },
34
35 // Change theme color
36 toggleTheme(dark: boolean) {
37 if (dark) {
38 this.theme = 'dark';
39 document.body.setAttribute('arco-theme', 'dark');
40 } else {
41 this.theme = 'light';
42 document.body.removeAttribute('arco-theme');
43 }
44 },
45
46 setPermissions(permissions: SystemPermission[]) {
47 this.$patch({ permissions });
48 },
49 },
50 });
51
52 export default useAppStore;
1 import { SystemPermission } from '@/types/system-permission';
2
3 export interface AppState {
4 theme: string;
5 navbar: boolean;
6 menu: boolean;
7 menuCollapse: boolean;
8 themeColor: string;
9 menuWidth: number;
10 globalSettings: boolean;
11 permissions?: SystemPermission[];
12 [key: string]: unknown;
13 }
1 import { defineStore } from 'pinia';
2 import { clearToken } from '@/utils/auth';
3 import useAuthApi from '@/api/auth';
4 // eslint-disable-next-line import/no-cycle
5 import { useAppStore } from '@/store';
6 import { AuthorizedState } from './type';
7
8 const useAuthorizedStore = defineStore('authorized', {
9 state: (): AuthorizedState => ({
10 id: undefined,
11 nick_name: undefined,
12 real_name: undefined,
13 avatar: undefined,
14 email: undefined,
15 phone: undefined,
16 permissions: [],
17 }),
18 getters: {
19 authorizedInfo(state: AuthorizedState): AuthorizedState {
20 return { ...state };
21 },
22 },
23 actions: {
24 setInfo(partial: Partial<AuthorizedState>) {
25 this.$patch(partial);
26 },
27 async syncInfo() {
28 const { user, permissions, menus } = await useAuthApi.info();
29 this.setInfo({ ...user, permissions });
30 useAppStore().setPermissions(menus);
31 },
32 async logout() {
33 this.$reset();
34 clearToken();
35 },
36 async syncToken() {
37 // const { data } = await refreshToken();
38 // setToken(data.access_token);
39 },
40 },
41 });
42
43 export default useAuthorizedStore;
1 export interface AuthorizedState {
2 id?: number;
3 nick_name?: string;
4 real_name?: string;
5 avatar?: string;
6 email?: string;
7 phone?: string;
8 permissions: string[];
9 }
1 import { defineStore } from 'pinia';
2 import { Selection } from '@/store/modules/selection/type';
3 import { AnyObject } from '@/types/global';
4 import { SystemConfig } from '@/types/system-config';
5 import axios from 'axios';
6 import { Tag } from '@/types/tag';
7
8 const useSelectionStore = defineStore('selection', {
9 state: (): Selection => ({
10 user: [],
11 project: [],
12 tag: [],
13 config: [],
14 }),
15 getters: {
16 getUserOptions(state) {
17 return state.user;
18 },
19 getTagOptions(state) {
20 return state.tag;
21 },
22 projectOptions(state) {
23 return state.project;
24 },
25 lyricTool(state): string {
26 return state.config.find((item) => item.identifier === 'activity_lyric_tool')?.content || '';
27 },
28 activityLang(state): SystemConfig | undefined {
29 return state.config.find((item) => item.identifier === 'activity_lang');
30 },
31 activityLangOptions(state): SystemConfig[] {
32 return state.config.filter((item) => item.parent_id === this.activityLang?.id);
33 },
34 activitySpeed(state): SystemConfig | undefined {
35 return state.config.find((item) => item.identifier === 'activity_speed');
36 },
37 activitySpeedOptions(state): SystemConfig[] {
38 return state.config.filter((item) => item.parent_id === this.activitySpeed?.id);
39 },
40 activitySex(state): SystemConfig | undefined {
41 return state.config.find((item) => item.identifier === 'activity_sex');
42 },
43 activitySexOptions(state): SystemConfig[] {
44 return state.config.filter((item) => item.parent_id === this.activitySex?.id);
45 },
46 activityTagOptions(state): Pick<Tag, 'id' | 'name' | 'type'>[] {
47 return state.tag.filter((item) => item.type === 1);
48 },
49 activityAudioAccept(state): string {
50 return state.config.find((item) => item.identifier === 'activity_audio_accept')?.content || 'audio/*';
51 },
52 activityTrackAccept(state): string {
53 return state.config.find((item) => item.identifier === 'activity_track_accept')?.content || '*';
54 },
55 appleDemoCover(state): string {
56 return state.config.find((item) => item.identifier === 'activity_demo_cover')?.content || '';
57 },
58 },
59 actions: {
60 queryUser(params?: AnyObject) {
61 axios.get('selection/user', { params }).then(({ data }) => {
62 this.user = data;
63 });
64 },
65 queryProject(params?: AnyObject) {
66 axios.get('selection/project', { params }).then(({ data }) => {
67 this.project = data;
68 });
69 },
70 queryTag() {
71 axios.get('selection/tag').then(({ data }) => {
72 this.tag = data;
73 });
74 },
75 queryConfig() {
76 axios.get('selection/config').then(({ data }) => {
77 this.config = data;
78 });
79 },
80
81 queryAll() {
82 this.queryConfig();
83 this.queryUser();
84 this.queryProject();
85 this.queryTag();
86 },
87 },
88 });
89
90 export default useSelectionStore;
1 import { User } from '@/types/user';
2 import { Project } from '@/types/project';
3 import { Tag } from '@/types/tag';
4 import { SystemConfig } from '@/types/system-config';
5
6 export interface Selection {
7 user: User[];
8 project: Project[];
9 tag: Tag[];
10 config: SystemConfig[];
11 }
1 import { Project } from '@/types/project';
2 import { Tag } from '@/types/tag';
3 import { User } from '@/types/user';
4
5 export interface ActivityExpand {
6 tag_ids: number[];
7 lyricist: { ids: number[]; supplement: string[] };
8 composer: { ids: number[]; supplement: string[] };
9 arranger: { ids: number[]; supplement: string[] };
10 guide_source: { name: string; url: string; size: number };
11 karaoke_source: { name: string; url: string; size: number };
12 track_source: { name: string; url: string; size: number };
13 }
14
15 export interface ActivityApplyRecord {
16 id: number;
17 created_at: string;
18 audit_msg: string;
19 }
20
21 export interface ActivityApply {
22 id: number;
23 cover: string;
24 song_name: string;
25 sub_title?: string;
26 lang: string[];
27 speed: string;
28 lyric: string;
29 clip_lyric: string;
30 sex: string;
31 project_id: number;
32 estimate_release_at: string;
33 audit_status: number;
34 expand: ActivityExpand;
35 project?: Project;
36 tags?: Tag[];
37 user?: User;
38 apply_records?: ActivityApplyRecord[];
39 }
40
41 export type ActivityApplyFormStep1 = Pick<ActivityApply, 'cover' | 'song_name' | 'sub_title' | 'lang' | 'speed'> &
42 Pick<ActivityExpand, 'tag_ids'>;
43
44 export type ActivityApplyFormStep2 = Pick<ActivityApply, 'project_id' | 'sex' | 'estimate_release_at'> &
45 Pick<ActivityExpand, 'lyricist' | 'composer' | 'arranger'>;
46
47 export type ActivityApplyFormStep3 = Pick<ActivityApply, 'song_name' | 'lyric' | 'clip_lyric'> &
48 Pick<ActivityExpand, 'guide_source' | 'karaoke_source' | 'track_source'>;
49
50 export type ActivityApplyForm = ActivityApplyFormStep1 & ActivityApplyFormStep2 & ActivityApplyFormStep3;
1 import { Activity } from '@/types/activity';
2 import { Singer } from '@/types/singer';
3 import { Business } from '@/types/business';
4
5 export interface ActivityWork {
6 id: number;
7 demo_url: string;
8 version: number;
9 type: '' | 'Submit' | 'Save';
10 status: 0 | 1 | 2;
11 mode: 0 | 1;
12 activity_id: number;
13 user_id: number;
14 open_id: string;
15 user?: Singer;
16 activity?: Activity;
17 activity_name: string;
18 activity_status: number;
19 business?: Business;
20 submit_at: string;
21 created_at: string;
22 business_id: 0;
23 business_nick_name?: string;
24 business_real_name?: string;
25 business_role?: string;
26 children: [];
27
28 user_email: string;
29 user_nick_name: string;
30 user_real_name: string;
31 user_company: string;
32 user_province: string;
33 user_city: string;
34 user_rate: string;
35 user_role: string;
36
37 share_id?: number;
38 share_nick_name?: string;
39 share_real_name?: string;
40 share_role?: string;
41 }
1 import { Project } from '@/types/project';
2 import { Tag } from '@/types/tag';
3 import { FileStatus } from '@arco-design/web-vue/es/upload/interfaces';
4 import { User } from '@/types/user';
5 import { ActivityExpand } from '@/types/activity-apply';
6
7 export type ActivityMaterialType = 'GuideDinging' | 'Accompany';
8 export type ActivityMaterialTone = -3 | -2 | -1 | 0 | 1 | 2 | 3;
9
10 export interface ActivityMaterialOption {
11 type: ActivityMaterialType;
12 tone: ActivityMaterialTone;
13 label: string;
14 }
15
16 export interface ActivityMaterial {
17 id: string;
18 type: ActivityMaterialType;
19 url: string;
20 name: string;
21 tone: ActivityMaterialTone;
22 file?: File;
23 percent?: number;
24 status?: FileStatus;
25 response?: string;
26 created_at?: string;
27 }
28
29 export interface ActivitySendLink {
30 type: string;
31 url: string;
32 }
33
34 export interface ActivityRecommendIntro {
35 id: number;
36 content: string;
37 }
38
39 export interface ActivityApplyRecord {
40 id: number;
41 audit_user_id: number;
42 audit_msg: string;
43 created_at?: string;
44 updated_at?: string;
45 }
46
47 export interface Activity {
48 id: number;
49 song_name: string;
50 song_type?: number;
51 sub_title: string;
52 cover: string;
53 user_id: number;
54 project_id: number;
55 guide: string;
56 karaoke: string;
57 guide_source?: string;
58 karaoke_source?: string;
59 lyric: string;
60 clip_lyric: string;
61 weight: number;
62 audit_status: number;
63 status: number;
64 match_at: string;
65 created_at: string;
66 updated_at: string;
67 submit_works_count?: number;
68 views_count?: number;
69 collections_count?: number;
70 is_official?: number;
71 send_url?: ActivitySendLink[];
72 user?: Pick<User, 'id' | 'nick_name' | 'real_name' | 'role'>;
73 project?: Project;
74 tags?: Tag[];
75 materials?: ActivityMaterial[];
76 recommend_intros?: ActivityRecommendIntro[];
77 apply_records?: ActivityApplyRecord[];
78 estimate_release_at: string;
79 expand?: ActivityExpand;
80 links?: User[];
81 }
82
83 export type ActivityViewUser = Pick<User, 'id' | 'avatar' | 'nick_name' | 'real_name' | 'sex' | 'role'> & {
84 listen_count: number;
85 collection_count: number;
86 submit_work: number;
87 last_listen_at: number;
88 };
1 // eslint-disable-next-line import/no-cycle
2 import { User } from '@/types/user';
3
4 export interface Admin extends User {
5 creator?: User;
6 role?: 'ProjectUser' | 'SystemUser' | 'Admin';
7 company: string;
8 activities_count?: number;
9 singers_count?: number;
10 submit_activities_count?: number;
11 accept_activities_count?: number;
12 checked_activities_count?: number;
13 }
1 export interface AppVersion {
2 id: number;
3 os: string;
4 app_ver: string;
5 app_no: number;
6 url: string;
7 remark?: string;
8 is_force: 1 | 0;
9 }
1 import { User } from '@/types/user';
2
3 export interface Business extends User {
4 role?: 'Business';
5 company?: string;
6 audit_status?: number;
7 singers_count?: number;
8 nick_name: string;
9 real_name: string;
10 email: string;
11 phone: string;
12 submit_activities_count?: number;
13 checked_activities_count?: number;
14 }
1 import { CallbackDataParams } from 'echarts/types/dist/shared';
2
3 export interface ToolTipFormatterParams extends CallbackDataParams {
4 axisDim: string;
5 axisIndex: number;
6 axisType: string;
7 axisId: string;
8 axisValue: string;
9 axisValueLabel: string;
10 }
1 export interface AnyObject {
2 [key: string]: unknown;
3 }
4
5 export interface Options {
6 value: unknown;
7 label: string;
8 }
9
10 export interface MoreRowValue<T = object> {
11 type: string;
12 row: T;
13 index?: number;
14 }
15
16 export interface QueryForParams {
17 [key: string]: unknown;
18
19 sortBy?: string;
20 sortType?: 'desc' | 'asc' | '';
21 }
22
23 export interface QueryForPaginationParams extends QueryForParams {
24 page?: number;
25 pageSize?: number;
26 }
27
28 export type sizeType = 'mini' | 'small' | 'medium' | 'large';
29
30 export interface AttributeData {
31 [key: string]: unknown;
32 }
33
34 export interface Pagination {
35 current: number;
36 pageSize: number;
37 total: number;
38 showTotal?: boolean;
39 showPageSize?: boolean;
40 pageSizeOptions?: number[];
41 }
42
43 export type SortType = 'descend' | 'ascend' | '' | string;
44
45 export interface Sort {
46 column: string;
47 type: SortType | string;
48 }
49
50 export interface GeneralChart {
51 xAxis: string[];
52 data: Array<{ name: string; value: number[] }>;
53 }
54
55 export interface HttpResponse<T = unknown> {
56 status: 'success' | 'fail' | 'error';
57 msg: string;
58 code: number;
59 data: T;
60 }
61
62 export interface ServiceResponse<T = unknown> extends HttpResponse<T> {
63 meta: { current: number; limit: number; total: number };
64 }
1 // eslint-disable-next-line import/no-cycle
2 import { Admin } from '@/types/admin';
3 import { User } from '@/types/user';
4
5 export interface Project {
6 id: number;
7 name: string;
8 user?: Admin;
9 master?: User;
10 status: number;
11 created_at?: string;
12 updated_at?: string;
13 up_count?: number;
14 down_count?: number;
15 finish_count?: number;
16 is_promote?: number;
17 is_official?: number;
18
19 is_can_apply?: number;
20 is_can_manage?: number;
21 is_can_demo_apply?: number;
22
23 head_cover: string;
24 cover: string;
25 intro: string;
26 managers_count?: number;
27 activities_count?: number;
28 send_count?: number;
29 managers?: Admin[];
30 }
1 import { AttributeData } from '@/types/global';
2 import { Business } from '@/types/business';
3 import { User } from '@/types/user';
4
5
6 export interface Singer extends User {
7 avatar: string;
8 remarks: string;
9 province: string;
10 city: string;
11 company: string;
12 rate: string;
13 is_subscribe?: 1 | 0;
14 role?: 'Singer';
15 audit_status?: number;
16 submit_activities_count?: number;
17 accept_activities_count?: number;
18 }
19
20 export interface UpdateAttribute extends AttributeData {
21 nick_name: string;
22 real_name: string;
23 phone: string;
24 business_id: number;
25 }
1 export interface SystemConfig {
2 id: number;
3 parent_id: number;
4 name: string;
5 content: string;
6 remark: string;
7 identifier: string;
8 children?: SystemConfig[];
9 }
1 export interface SystemPermission {
2 id: number;
3 name: string;
4 guard: 'Admin' | 'Manage';
5 type: 'Menu' | 'Button';
6 label: string;
7 icon: string;
8 parent_id: number;
9 weight: number;
10 parent?: SystemPermission;
11 children?: SystemPermission[];
12 created_at: string;
13 updated_at: string;
14 }
1 // eslint-disable-next-line import/no-cycle
2 import { Admin } from '@/types/admin';
3
4 export interface Tag {
5 id: number;
6 name: string;
7 type: number;
8 created_at: string;
9 updated_at: string;
10 user?: Admin;
11 }