Commit 8514204c 8514204ca67957cca8641dd8af192aeec128f90e by 杨俊

feat(master): init

0 parents
Showing 284 changed files with 23014 additions and 0 deletions
1 VITE_API_HOST=https://hi-sing-service-dev.hikoon.com
2
3 VITE_OSS_ACCESS_KEY=LTAI4GKtcA6yTV6wnapivq7Y
4 VITE_OSS_ACCESS_SECRET=QPEt0HPEuRe7wIk2wZNtKPF6L0xMmQ
5 VITE_OSS_BUCKET=hisin-dev
6 VITE_OSS_REGION=oss-cn-beijing
7 VITE_OSS_HOST=https://hi-sing-cdn-dev.hikoon.com
8 VITE_OSS_ENDPOINT=https://hisin-dev.oss-cn-beijing.aliyuncs.com
9 VITE_SHOW_LOGIN_ACCOUNT=true
1 /*.json
2 /*.js
3 dist
...\ No newline at end of file ...\ No newline at end of file
1 const path = require('path');
2
3 module.exports = {
4 root: true,
5 parser: 'vue-eslint-parser',
6 parserOptions: {
7 // Parser that checks the content of the <script> tag
8 parser: '@typescript-eslint/parser',
9 sourceType: 'module',
10 ecmaVersion: 2020,
11 ecmaFeatures: {
12 jsx: true,
13 },
14 },
15 env: {
16 browser: true,
17 node: true,
18 },
19 plugins: ['@typescript-eslint'],
20 extends: [
21 // Airbnb JavaScript Style Guide https://github.com/airbnb/javascript
22 'airbnb-base',
23 'plugin:@typescript-eslint/recommended',
24 'plugin:import/recommended',
25 'plugin:import/typescript',
26 'plugin:vue/vue3-recommended',
27 'plugin:prettier/recommended',
28 ],
29 settings: {
30 'import/resolver': {
31 typescript: {
32 project: path.resolve(__dirname, './tsconfig.json'),
33 },
34 },
35 },
36 globals: {
37 defineProps: 'readonly',
38 defineEmits: 'readonly',
39 defineExpose: 'readonly',
40 defineModel: 'readonly',
41 withDefaults: 'readonly',
42 },
43 rules: {
44 'prettier/prettier': 1,
45 'import/order': 0,
46 // Vue: Recommended rules to be closed or modify
47 'vue/require-default-prop': 0,
48 'vue/singleline-html-element-content-newline': 0,
49 'vue/max-attributes-per-line': 0,
50 // Vue: Add extra rules
51 'vue/custom-event-name-casing': [2, 'camelCase'],
52 'vue/no-v-text': 1,
53 'vue/padding-line-between-blocks': 1,
54 'vue/require-direct-export': 1,
55 'vue/multi-word-component-names': 0,
56 // Allow @ts-ignore comment
57 '@typescript-eslint/ban-ts-comment': 0,
58 '@typescript-eslint/no-unused-vars': 1,
59 '@typescript-eslint/no-empty-function': 1,
60 '@typescript-eslint/no-explicit-any': 0,
61 '@typescript-eslint/no-implicit-any': 0,
62 'camelcase': 'off',
63 'max-classes-per-file': 'off',
64 '@typescript-eslint/camelcase': 0,
65 'import/extensions': [
66 2,
67 'ignorePackages',
68 {
69 js: 'never',
70 jsx: 'never',
71 ts: 'never',
72 tsx: 'never',
73 },
74 ],
75 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
76 'no-param-reassign': 0,
77 'no-return-assign': 0,
78 'no-shadow': 0,
79 'prefer-regex-literals': 0,
80 'import/no-extraneous-dependencies': 0,
81 'class-methods-use-this': 0,
82 },
83 };
1 node_modules
2 .DS_Store
3 dist
4 dist-ssr
5 *.local
6 /node_modules
7 /dist
8 /dist-ssr
9 .idea/*
10 yarn.lock
11 .env.production
12 components.d.ts
1 /dist/*
2 /node_modules/**
3
4 **/*.svg
5 **/*.sh
1 module.exports = {
2 tabWidth: 2,
3 semi: true,
4 printWidth: 140,
5 singleQuote: true,
6 quoteProps: 'consistent',
7 htmlWhitespaceSensitivity: 'strict',
8 vueIndentScriptAndStyle: true,
9 };
1 module.exports = {
2 extends: [
3 'stylelint-config-standard',
4 'stylelint-config-rational-order',
5 'stylelint-config-prettier',
6 ],
7 defaultSeverity: 'warning',
8 plugins: ['stylelint-order'],
9 rules: {
10 'at-rule-no-unknown': [
11 true,
12 {
13 ignoreAtRules: ['plugin'],
14 },
15 ],
16 'rule-empty-line-before': [
17 'always',
18 {
19 except: ['after-single-line-comment', 'first-nested'],
20 },
21 ],
22 'selector-pseudo-class-no-unknown': [
23 true,
24 {
25 ignorePseudoClasses: ['deep'],
26 },
27 ],
28 },
29 };
1 module.exports = {
2 plugins: ['@vue/babel-plugin-jsx'],
3 };
1 module.exports = {
2 extends: ['@commitlint/config-conventional'],
3 };
1 !.gitignore
1 /**
2 * If you use the template method for development, you can use the unplugin-vue-components plugin to enable on-demand loading support.
3 * 按需引入
4 * https://github.com/antfu/unplugin-vue-components
5 * https://arco.design/vue/docs/start
6 * Although the Pro project is full of imported components, this plugin will be used by default.
7 * 虽然Pro项目中是全量引入组件,但此插件会默认使用。
8 */
9 // eslint-disable-next-line import/no-unresolved
10 import Components from "unplugin-vue-components/vite";
11 // eslint-disable-next-line import/no-unresolved
12 import { ArcoResolver } from "unplugin-vue-components/resolvers";
13
14 export default function configArcoResolverPlugin() {
15 return Components({
16 dirs: [], // Avoid parsing src/components. 避免解析到src/components
17 deep: false,
18 resolvers: [ArcoResolver()],
19 });
20 }
1 /**
2 * Used to package and output gzip. Note that this does not work properly in Vite, the specific reason is still being investigated
3 * gzip压缩
4 * https://github.com/anncwb/vite-plugin-compression
5 */
6 import type { Plugin } from 'vite';
7 import compressPlugin from 'vite-plugin-compression';
8
9 export default function configCompressPlugin(
10 compress: 'gzip' | 'brotli',
11 deleteOriginFile = false
12 ): Plugin | Plugin[] {
13 const plugins: Plugin[] = [];
14
15 if (compress === 'gzip') {
16 plugins.push(
17 compressPlugin({
18 ext: '.gz',
19 deleteOriginFile,
20 })
21 );
22 }
23
24 if (compress === 'brotli') {
25 plugins.push(
26 compressPlugin({
27 ext: '.br',
28 algorithm: 'brotliCompress',
29 deleteOriginFile,
30 })
31 );
32 }
33 return plugins;
34 }
1 /**
2 * Image resource files used to compress the output of the production environment
3 * 图片压缩
4 * https://github.com/anncwb/vite-plugin-imagemin
5 */
6 import viteImagemin from 'vite-plugin-imagemin';
7
8 export default function configImageminPlugin() {
9 const imageminPlugin = viteImagemin({
10 gifsicle: {
11 optimizationLevel: 7,
12 interlaced: false,
13 },
14 optipng: {
15 optimizationLevel: 7,
16 },
17 mozjpeg: {
18 quality: 20,
19 },
20 pngquant: {
21 quality: [0.8, 0.9],
22 speed: 4,
23 },
24 svgo: {
25 plugins: [
26 {
27 name: 'removeViewBox',
28 },
29 {
30 name: 'removeEmptyAttrs',
31 active: false,
32 },
33 ],
34 },
35 });
36 return imageminPlugin;
37 }
1 /**
2 * Introduces component library styles on demand.
3 * 按需引入组件库样式
4 * https://github.com/anncwb/vite-plugin-style-import
5 */
6
7 import styleImport from 'vite-plugin-style-import';
8
9 export default function configStyleImportPlugin() {
10 return styleImport({
11 libs: [
12 {
13 libraryName: '@arco-design/web-vue',
14 esModule: true,
15 resolveStyle: (name) => {
16 // The use of this part of the component must depend on the parent, so it can be ignored directly.
17 // 这部分组件的使用必须依赖父级,所以直接忽略即可。
18 const ignoreList = [
19 'button-group',
20 'config-provider',
21 'anchor-link',
22 'sub-menu',
23 'menu-item',
24 'menu-item-group',
25 'breadcrumb-item',
26 'form-item',
27 'step',
28 'card-grid',
29 'card-meta',
30 'collapse-panel',
31 'collapse-item',
32 'descriptions-item',
33 'list-item',
34 'list-item-meta',
35 'table-column',
36 'table-column-group',
37 'tab-pane',
38 'tab-content',
39 'timeline-item',
40 'tree-node',
41 'skeleton-line',
42 'skeleton-shape',
43 'grid-item',
44 'carousel-item',
45 'doption',
46 'option',
47 'optgroup',
48 'icon',
49 ];
50 // List of components that need to map imported styles
51 // 需要映射引入样式的组件列表
52 const replaceList = {
53 'typography-text': 'typography',
54 'typography-title': 'typography',
55 'typography-paragraph': 'typography',
56 'typography-link': 'typography',
57 'dropdown-button': 'dropdown',
58 'input-password': 'input',
59 'input-search': 'input',
60 'input-group': 'input',
61 'radio-group': 'radio',
62 'checkbox-group': 'checkbox',
63 'layout-sider': 'layout',
64 'layout-content': 'layout',
65 'layout-footer': 'layout',
66 'layout-header': 'layout',
67 'month-picker': 'date-picker',
68 'range-picker': 'date-picker',
69 'row': 'grid', // 'grid/row.less'
70 'col': 'grid', // 'grid/col.less'
71 'avatar-group': 'avatar',
72 'image-preview': 'image',
73 'image-preview-group': 'image',
74 };
75 if (ignoreList.includes(name)) return '';
76
77 if (name === 'use-form-item') {
78 return `@arco-design/web-vue/es/_hooks/use-form-item.js`;
79 }
80
81 // eslint-disable-next-line no-prototype-builtins
82 // return replaceList.hasOwnProperty(name)
83 // ? // @ts-ignore
84 // `@arco-design/web-vue/es/${replaceList[name]}/style/css.js`
85 // : `@arco-design/web-vue/es/${name}/style/css.js`;
86
87 // less
88 // eslint-disable-next-line no-prototype-builtins
89 return replaceList.hasOwnProperty(name)
90 ? // @ts-ignore
91 `@arco-design/web-vue/es/${replaceList[name]}/style/css.js`
92 : `@arco-design/web-vue/es/${name}/style/index.js`;
93 },
94 },
95 ],
96 });
97 }
1 /**
2 * Generation packaging analysis
3 * 生成打包分析
4 */
5 import visualizer from 'rollup-plugin-visualizer';
6 import { isReportMode } from '../utils';
7
8 export default function configVisualizerPlugin() {
9 if (isReportMode()) {
10 return visualizer({
11 filename: './node_modules/.cache/visualizer/stats.html',
12 open: true,
13 gzipSize: true,
14 brotliSize: true,
15 });
16 }
17 return [];
18 }
1 /**
2 * Whether to generate package preview
3 * 是否生成打包报告
4 */
5 export default {};
6
7 export function isReportMode(): boolean {
8 return process.env.REPORT === 'true';
9 }
1 import vue from '@vitejs/plugin-vue';
2 import vueJsx from '@vitejs/plugin-vue-jsx';
3 import { resolve } from 'path';
4 import { defineConfig } from 'vite';
5 import svgLoader from 'vite-svg-loader';
6 import vueSetupExtend from 'vite-plugin-vue-setup-extend';
7
8 export default defineConfig({
9 plugins: [vue({ script: { defineModel: true } }), vueJsx(), svgLoader({ svgoConfig: {} }), vueSetupExtend()],
10 resolve: {
11 alias: [
12 {
13 find: '@',
14 replacement: resolve(__dirname, '../src'),
15 },
16 {
17 find: 'assets',
18 replacement: resolve(__dirname, '../src/assets'),
19 },
20 {
21 find: 'vue-i18n',
22 replacement: 'vue-i18n/dist/vue-i18n.cjs.js', // Resolve the i18n warning issue
23 },
24 {
25 find: 'vue',
26 replacement: 'vue/dist/vue.esm-bundler.js', // compile template
27 },
28 ],
29 extensions: ['.ts', '.js'],
30 },
31 build: {
32 rollupOptions: {
33 external: [],
34 },
35 },
36 define: {
37 'process.env': {},
38 '__VUE_PROD_HYDRATION_MISMATCH_DETAILS__': false,
39 },
40 css: {
41 preprocessorOptions: {
42 less: {
43 modifyVars: {
44 hack: `true; @import (reference) "${resolve('src/assets/style/breakpoint.less')}";`,
45 },
46 javascriptEnabled: true,
47 },
48 },
49 },
50 });
1 import { loadEnv, mergeConfig } from 'vite';
2 import baseConfig from './vite.config.base';
3 import configCompressPlugin from './plugin/compress';
4 import configVisualizerPlugin from './plugin/visualizer';
5 import configArcoResolverPlugin from './plugin/arcoResolver';
6 import configStyleImportPlugin from './plugin/styleImport';
7 import configImageminPlugin from './plugin/imagemin';
8 import VitePluginOss from 'vite-plugin-oss';
9
10 const config = loadEnv('production', '');
11
12 const ossPath = `vendor/admin/asset${new Date()
13 .toLocaleString('zh')
14 .slice(0, 20)
15 .replace(/[\s/:]/g, '')}`;
16 export default mergeConfig(
17 {
18 mode: 'production',
19 base: config.VITE_OSS_HOST,
20 server: {
21 https: true,
22 },
23 plugins: [
24 configCompressPlugin('gzip'),
25 configVisualizerPlugin(),
26 configArcoResolverPlugin(),
27 configStyleImportPlugin(),
28 configImageminPlugin(),
29 VitePluginOss({
30 from: `./dist/${ossPath}/**`,
31 accessKeyId: config.VITE_OSS_ACCESS_KEY,
32 accessKeySecret: config.VITE_OSS_ACCESS_SECRET,
33 bucket: config.VITE_OSS_BUCKET,
34 region: config.VITE_OSS_REGION,
35 quitWpOnError: true,
36 deleteOrigin: true,
37 deleteEmptyDir: true,
38 }),
39 ],
40 build: {
41 assetsDir: ossPath,
42 rollupOptions: {
43 output: {
44 manualChunks: {
45 arco: ['@arco-design/web-vue'],
46 chart: ['echarts', 'vue-echarts'],
47 vue: ['vue', 'vue-router', 'pinia', '@vueuse/core', 'vue-i18n'],
48 },
49 },
50 },
51 chunkSizeWarningLimit: 2000,
52 },
53 },
54 baseConfig
55 );
1 import { mergeConfig } from 'vite';
2 import eslint from 'vite-plugin-eslint';
3 import baseConfig from './vite.config.base';
4
5 export default mergeConfig(
6 {
7 mode: 'development',
8 server: {
9 open: true,
10 https: true,
11 fs: {
12 strict: true,
13 },
14 },
15 plugins: [
16 eslint({
17 cache: false,
18 include: ['src/**/*.ts', 'src/**/*.tsx', 'src/**/*.vue'],
19 exclude: ['node_modules'],
20 }),
21 ],
22 define: {
23 __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: true,
24 },
25 },
26 baseConfig
27 );
1 import { mergeConfig } from 'vite';
2 import baseConfig from './vite.config.base';
3 import configCompressPlugin from './plugin/compress';
4 import configVisualizerPlugin from './plugin/visualizer';
5 import configArcoResolverPlugin from './plugin/arcoResolver';
6 import configStyleImportPlugin from './plugin/styleImport';
7 import configImageminPlugin from './plugin/imagemin';
8
9 export default mergeConfig(
10 {
11 mode: 'production',
12 server: {
13 https: true,
14 },
15 plugins: [
16 configCompressPlugin('gzip'),
17 configVisualizerPlugin(),
18 configArcoResolverPlugin(),
19 configStyleImportPlugin(),
20 configImageminPlugin(),
21 ],
22 build: {
23 rollupOptions: {
24 output: {
25 manualChunks: {
26 arco: ['@arco-design/web-vue'],
27 chart: ['echarts', 'vue-echarts'],
28 vue: ['vue', 'vue-router', 'pinia', '@vueuse/core', 'vue-i18n'],
29 },
30 },
31 },
32 chunkSizeWarningLimit: 2000,
33 },
34 },
35 baseConfig
36 );
1 <!DOCTYPE html>
2 <html lang="en">
3 <head>
4 <meta charset="UTF-8"/>
5 <link rel="shortcut icon" type="image/x-icon" href="https://hising-cdn.hikoon.com/file/20231201/nyaagyzd92c1701419974050pv0hv2xlsme.ico">
6 <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
7 <meta name="referrer" content="no-referrer"/>
8 <title>海星试唱</title>
9 </head>
10 <body>
11 <div id="app"></div>
12 <script type="module" src="/src/main.ts"></script>
13 </body>
14 </html>
1 {
2 "name": "arco-design-pro-vue",
3 "description": "Arco Design Pro for Vue",
4 "version": "1.0.0",
5 "private": true,
6 "author": "ArcoDesign Team",
7 "license": "MIT",
8 "scripts": {
9 "dev": "vite --config ./config/vite.config.dev.ts",
10 "build": "vue-tsc --noEmit && vite build --config ./config/vite.config.prod.ts",
11 "build:cdn": "vue-tsc --noEmit && vite build --config ./config/vite.config.cdn.ts",
12 "report": "cross-env REPORT=true npm run build",
13 "preview": "npm run build && vite preview --host",
14 "type:check": "vue-tsc --noEmit --skipLibCheck",
15 "lint-staged": "npx lint-staged",
16 "prepare": "husky install"
17 },
18 "lint-staged": {
19 "*.{js,ts,jsx,tsx}": [
20 "prettier --write",
21 "eslint --fix"
22 ],
23 "*.vue": [
24 "stylelint --fix",
25 "prettier --write",
26 "eslint --fix"
27 ],
28 "*.{less,css}": [
29 "stylelint --fix",
30 "prettier --write"
31 ]
32 },
33 "dependencies": {
34 "@arco-design/web-vue": "^2.53.3",
35 "@microsoft/fetch-event-source": "^2.0.1",
36 "@vueuse/core": "^7.3.0",
37 "@vueuse/router": "^10.1.2",
38 "@wangeditor/editor": "^5.1.23",
39 "@wangeditor/editor-for-vue": "^5.1.12",
40 "ali-oss": "^6.17.1",
41 "arco-design-pro-vue": "^2.5.10",
42 "axios": "^1.6.5",
43 "dayjs": "^1.11.2",
44 "echarts": "^5.2.2",
45 "file-saver": "^2.0.5",
46 "js-file-download": "^0.4.12",
47 "lodash": "^4.17.21",
48 "mitt": "^3.0.0",
49 "nanoid": "^3.3.4",
50 "nprogress": "^0.2.0",
51 "pinia": "^2.0.9",
52 "query-string": "^7.0.1",
53 "rabbit-lyrics": "^2.1.1",
54 "vite-plugin-vue-setup-extend": "^0.4.0",
55 "vue": "^3.3.11",
56 "vue-echarts": "^6.0.0",
57 "vue-i18n": "^9.2.0-beta.17",
58 "vue-router": "^4.0.14",
59 "vue3-colorpicker": "^2.3.0",
60 "vue3-video-play": "1.3.1-beta.6"
61 },
62 "devDependencies": {
63 "@arco-design/web-vue": "^2.0.0-beta.7",
64 "@commitlint/cli": "^11.0.0",
65 "@commitlint/config-conventional": "^12.0.1",
66 "@types/ali-oss": "^6.16.3",
67 "@types/file-saver": "^2.0.5",
68 "@types/lodash": "^4.14.177",
69 "@types/nprogress": "^0.2.0",
70 "@typescript-eslint/eslint-plugin": "^5.10.0",
71 "@typescript-eslint/parser": "^5.10.0",
72 "@vitejs/plugin-vue": "^1.9.4",
73 "@vitejs/plugin-vue-jsx": "^1.2.0",
74 "@vue/babel-plugin-jsx": "^1.1.1",
75 "cross-env": "^7.0.3",
76 "eslint": "8.22.0",
77 "eslint-config-airbnb-base": "^14.2.1",
78 "eslint-config-prettier": "^8.3.0",
79 "eslint-import-resolver-typescript": "^2.4.0",
80 "eslint-plugin-import": "^2.22.1",
81 "eslint-plugin-prettier": "^3.3.1",
82 "eslint-plugin-vue": "^8.3.0",
83 "husky": "^7.0.4",
84 "less": "^4.1.2",
85 "lint-staged": "^11.2.6",
86 "prettier": "^2.2.1",
87 "rollup-plugin-visualizer": "^5.6.0",
88 "stylelint": "^13.8.0",
89 "stylelint-config-prettier": "^8.0.2",
90 "stylelint-config-rational-order": "^0.1.2",
91 "stylelint-config-standard": "^20.0.0",
92 "stylelint-order": "^4.1.0",
93 "typescript": "^4.4.4",
94 "unplugin-vue-components": "^0.19.3",
95 "vite": "^2.6.4",
96 "vite-plugin-compression": "^0.5.1",
97 "vite-plugin-eslint": "^1.3.0",
98 "vite-plugin-imagemin": "^0.6.1",
99 "vite-plugin-oss": "^1.2.13",
100 "vite-plugin-style-import": "1.4.1",
101 "vite-svg-loader": "^3.1.0",
102 "vue-tsc": "^0.34.15"
103 },
104 "resolutions": {
105 "bin-wrapper": "npm:bin-wrapper-china",
106 "rollup": "^2.56.3",
107 "gifsicle": "5.2.0"
108 },
109 "peerDependencies": {
110 "@arco-design/web-vue": ">=2.0.0-beta.7",
111 "snabbdom": "^3.5.1"
112 },
113 "volta": {
114 "node": "18.3.0",
115 "yarn": "1.22.19"
116 }
117 }
...\ No newline at end of file ...\ No newline at end of file
1 <template>
2 <a-config-provider size="small">
3 <router-view />
4 </a-config-provider>
5 </template>
6
7 <script lang="ts" setup></script>
1 <svg width="33" height="33" viewBox="0 0 33 33" fill="none" xmlns="http://www.w3.org/2000/svg">
2 <g clip-path="url(#clip0)">
3 <path fill-rule="evenodd" clip-rule="evenodd" d="M5.37754 16.9795L12.7498 9.43027C14.7163 7.41663 17.9428 7.37837 19.9564 9.34482C19.9852 9.37297 20.0137 9.40145 20.0418 9.43027L20.1221 9.51243C22.1049 11.5429 22.1049 14.7847 20.1221 16.8152L12.7498 24.3644C10.7834 26.378 7.55686 26.4163 5.54322 24.4498C5.5144 24.4217 5.48592 24.3932 5.45777 24.3644L5.37754 24.2822C3.39468 22.2518 3.39468 19.0099 5.37754 16.9795Z" fill="#12D2AC"/>
4 <path fill-rule="evenodd" clip-rule="evenodd" d="M20.0479 9.43034L27.3399 16.8974C29.3674 18.9735 29.3674 22.2883 27.3399 24.3644C25.3735 26.3781 22.147 26.4163 20.1333 24.4499C20.1045 24.4217 20.076 24.3933 20.0479 24.3644L12.7558 16.8974C10.7284 14.8213 10.7284 11.5065 12.7558 9.43034C14.7223 7.4167 17.9488 7.37844 19.9624 9.34489C19.9912 9.37304 20.0197 9.40152 20.0479 9.43034Z" fill="#307AF2"/>
5 <path fill-rule="evenodd" clip-rule="evenodd" d="M20.1321 9.52163L23.6851 13.1599L16.3931 20.627L9.10103 13.1599L12.6541 9.52163C14.6707 7.45664 17.9794 7.4174 20.0444 9.434C20.074 9.46286 20.1032 9.49207 20.1321 9.52163Z" fill="#0057FE"/>
6 </g>
7 <defs>
8 <clipPath id="clip0">
9 <rect width="26" height="19" fill="white" transform="translate(3.5 7)"/>
10 </clipPath>
11 </defs>
12 </svg>
1 // ==============breakpoint============
2
3 // Extra small screen / phone
4 @screen-xs: 480px;
5
6 // Small screen / tablet
7 @screen-sm: 576px;
8
9 // Medium screen / desktop
10 @screen-md: 768px;
11
12 // Large screen / wide desktop
13 @screen-lg: 992px;
14
15 // Extra large screen / full hd
16 @screen-xl: 1200px;
17
18 // Extra extra large screen / large desktop
19 @screen-xxl: 1600px;
1 * {
2 box-sizing: border-box;
3 }
4
5 html,
6 body {
7 width: 100%;
8 height: 100%;
9 margin: 0;
10 padding: 0;
11 font-size: 14px;
12 background-color: var(--color-bg-1);
13 -moz-osx-font-smoothing: grayscale;
14 -webkit-font-smoothing: antialiased;
15 }
16
17 .echarts-tooltip-diy {
18 background: linear-gradient(304.17deg,
19 rgba(253, 254, 255, 0.6) -6.04%,
20 rgba(244, 247, 252, 0.6) 85.2%) !important;
21 border: none !important;
22
23 /* Note: backdrop-filter has minimal browser support */
24
25 border-radius: 6px !important;
26 backdrop-filter: blur(10px) !important;
27
28 .content-panel {
29 display: flex;
30 justify-content: space-between;
31 width: auto;
32 height: 32px;
33 margin-bottom: 4px;
34 padding: 0 9px;
35 line-height: 32px;
36 background: rgba(255, 255, 255, 0.8);
37 border-radius: 4px;
38 box-shadow: 6px 0 20px rgba(34, 87, 188, 0.1);
39 }
40
41 .tooltip-title {
42 margin: 0 0 10px 0;
43 }
44
45 p {
46 margin: 0;
47 }
48
49 .tooltip-title,
50 .tooltip-value {
51 display: flex;
52 align-items: center;
53 color: #1d2129;
54 font-weight: bold;
55 font-size: 13px;
56 line-height: 15px;
57 text-align: right;
58 }
59
60 .tooltip-value {
61 margin-left: 10px;
62 }
63
64 .tooltip-item-icon {
65 display: inline-block;
66 width: 10px;
67 height: 10px;
68 margin-right: 8px;
69 border-radius: 50%;
70 }
71 }
72
73 .general-card {
74 border: none;
75 border-radius: 4px;
76
77 > .arco-card-header {
78 border: none;
79
80 > .arco-card-header-title {
81 font-size: 14px;
82 color: var(--color-text-2);
83 }
84 }
85
86 > .arco-card-body {
87 padding: 0 20px 20px 20px;
88 }
89 }
90
91 .split-line {
92 border-color: rgb(var(--gray-2));
93 }
94
95 .arco-table-cell {
96 .circle {
97 display: inline-block;
98 width: 6px;
99 height: 6px;
100 margin-right: 4px;
101 background-color: rgb(var(--blue-6));
102 border-radius: 50%;
103
104 &.pass {
105 background-color: rgb(var(--green-6));
106 }
107
108 &.warn {
109 background-color: rgb(var(--yellow-6));
110 }
111
112 &.err {
113 background-color: rgb(var(--red-6));
114 }
115 }
116 }
117
118 .mt-20 {
119 margin-top: 20px;
120 }
121
122 .mb-0 {
123 margin-bottom: 0 !important;
124 }
125
126 .mb-8 {
127 margin-bottom: 8px !important;
128 }
129
130 .color-grey {
131 color: rgba(44, 44, 44, 0.5)
132 }
133
134 .justify-center {
135 justify-content: center !important;
136 }
137
138
139 .retry {
140 width: 860px !important;
141
142 div.arco-modal-header {
143 border-bottom: unset !important;
144 }
145
146 div.arco-modal-body {
147 padding: 0 24px 20px;
148 }
149 }
150
151
152 .link-hover:hover {
153 cursor: pointer !important;
154 }
This diff could not be displayed because it is too large.
1 <template>
2 <audio
3 v-if="url"
4 v-bind="$attrs"
5 :id="uuid"
6 ref="audioRef"
7 :src="url"
8 class="player"
9 controls
10 controlsList="nodownload noplaybackrate"
11 @play="onPay"
12 />
13 </template>
14
15 <script lang="ts" setup>
16 import { nanoid } from 'nanoid';
17 // import FileSaver from 'file-saver';
18 import { computed, ref } from 'vue';
19
20 const audioRef = ref({});
21
22 defineProps<{ url: string }>();
23
24 const uuid = computed(() => {
25 return `audio-${nanoid()}`;
26 });
27
28 const onPay = (e: any) => {
29 const audios = document.getElementsByTagName('audio');
30 [].forEach.call(audios, (i: HTMLAudioElement) => i !== e.target && i.pause());
31 };
32
33 const getRef = (): HTMLAudioElement => {
34 return audioRef.value as HTMLAudioElement;
35 };
36
37 defineExpose({ getRef });
38 </script>
39
40 <style lang="less" scoped>
41 .player {
42 height: 30px;
43 width: 100%;
44 outline: none;
45 display: flex;
46 }
47
48 audio {
49 &::-webkit-media-controls-volume-control-container {
50 display: none !important;
51 }
52
53 &::-webkit-media-controls-rewind-button {
54 display: none !important;
55 }
56 }
57 </style>
1 <template>
2 <Upload :accept="accept" :custom-request="onUpload" :on-before-upload="onBeforeUpload" :file-list="[]" :show-file-list="false">
3 <template #upload-button>
4 <Avatar :shape="shape" :size="size" trigger-type="mask">
5 <div v-if="!modelValue" class="no-avatar">
6 <icon-plus />
7 </div>
8 <img v-else :src="modelValue" style="width: 100%; height: 100%" alt="" />
9 <template #trigger-icon>
10 <icon-camera />
11 </template>
12 </Avatar>
13 </template>
14 </Upload>
15 </template>
16
17 <script lang="ts" setup>
18 import useOss from '@/hooks/oss';
19 import { IconCamera, IconPlus } from '@arco-design/web-vue/es/icon';
20 import { Avatar, Message, Upload, UploadRequest, useFormItem } from '@arco-design/web-vue';
21 import { computed, toRefs } from 'vue';
22
23 const props = withDefaults(
24 defineProps<{
25 modelValue?: string;
26 size?: number;
27 shape?: 'circle' | 'square';
28 accept?: string;
29 limit?: number;
30 }>(),
31 {
32 modelValue: '',
33 size: 80,
34 shape: 'circle',
35 accept: 'image/*',
36 limit: 5,
37 }
38 );
39
40 const { shape } = toRefs(props);
41
42 const borderRadius = computed(() => (shape.value === 'square' ? 'unset' : 'var(--border-radius-circle)'));
43
44 const { eventHandlers } = useFormItem();
45
46 const emits = defineEmits(['update:modelValue']);
47
48 const { upload } = useOss();
49
50 const onBeforeUpload = (file: File) => {
51 if (file.size > props.limit * 1024 * 1024) {
52 Message.warning(`${file.name} 文件超过${props.limit}MB,无法上传`);
53 return Promise.resolve(false);
54 }
55
56 return Promise.resolve(file);
57 };
58
59 const onUpload = (option: any): UploadRequest => {
60 const { fileItem } = option;
61
62 if (fileItem.file) {
63 upload(fileItem.file, 'image').then((res) => {
64 emits('update:modelValue', res?.url || '');
65 eventHandlers.value?.onChange?.();
66 });
67 }
68
69 return {};
70 };
71 </script>
72
73 <style lang="less" scoped>
74 :deep(.arco-avatar-text) {
75 width: 100%;
76 height: 100%;
77 overflow: hidden;
78 display: inline-block;
79 border-radius: v-bind(borderRadius);
80 }
81
82 .arco-avatar-image {
83 transform: unset !important;
84 }
85
86 .no-avatar {
87 top: 0;
88 left: 0;
89 display: flex;
90 position: absolute;
91 align-content: center;
92 align-items: center;
93 justify-content: center;
94 width: 100%;
95 height: 100%;
96 }
97 </style>
1 <template>
2 <a-breadcrumb class="container-breadcrumb">
3 <a-breadcrumb-item>
4 <icon-apps />
5 </a-breadcrumb-item>
6 <a-breadcrumb-item v-for="item in items" :key="item.name">
7 <router-link v-if="getRouteMeta(item)?.isRedirect" :to="{ name: item }">
8 {{ getRouteName(item) }}
9 </router-link>
10 <span v-else> {{ getRouteName(item) }}</span>
11 </a-breadcrumb-item>
12 </a-breadcrumb>
13 </template>
14
15 <script lang="ts" setup>
16 import { IconApps } from '@arco-design/web-vue/es/icon';
17 import { computed } from 'vue';
18 import { RouteMeta, RouteRecordNormalized, useRouter } from 'vue-router';
19 import { useAppStore } from '@/store';
20 import { storeToRefs } from 'pinia';
21
22 defineProps<{ items: string[] }>();
23
24 const router = useRouter();
25 const appStore = useAppStore();
26
27 const { appMenu } = storeToRefs(appStore);
28
29 const routes = computed(() => {
30 return router.getRoutes() as RouteRecordNormalized[];
31 });
32
33 const getRoute = (name: any) => {
34 return routes.value.find((el) => el.name === name);
35 };
36
37 const getRouteMeta = (name: any): RouteMeta | undefined => {
38 return getRoute(name)?.meta;
39 };
40
41 const getRouteName = (name: any) => {
42 return getRouteMeta(name)?.title || appMenu.value.find((item) => item.name === name)?.label || '';
43 };
44 </script>
45
46 <style lang="less" scoped>
47 .container-breadcrumb {
48 margin: 16px 0;
49
50 :deep(.arco-breadcrumb-item) {
51 color: rgb(var(--gray-6));
52
53 &:last-child {
54 color: rgb(var(--gray-8));
55 }
56 }
57 }
58 </style>
1 <template>
2 <VCharts v-if="renderChart" :option="options" :autoresize="autoresize" :style="{ width, height }" />
3 </template>
4
5 <script lang="ts">
6 import { computed, defineComponent, nextTick, ref } from 'vue';
7 import VCharts from 'vue-echarts';
8 import { useAppStore } from '@/store';
9
10 export default defineComponent({
11 components: {
12 VCharts,
13 },
14 props: {
15 options: {
16 type: Object,
17 default() {
18 return {};
19 },
20 },
21 autoresize: {
22 type: Boolean,
23 default: true,
24 },
25 width: {
26 type: String,
27 default: '100%',
28 },
29 height: {
30 type: String,
31 default: '100%',
32 },
33 },
34 setup() {
35 const appStore = useAppStore();
36 const theme = computed(() => {
37 if (appStore.theme === 'dark') return 'dark';
38 return '';
39 });
40 const renderChart = ref(false);
41 // wait container expand
42 nextTick(() => {
43 renderChart.value = true;
44 });
45 return {
46 theme,
47 renderChart,
48 };
49 },
50 });
51 </script>
52
53 <style scoped lang="less"></style>
1 <template>
2 <a-button :loading="loading" @click="onClick">
3 <template #icon>
4 <icon-download />
5 </template>
6 {{ label }}
7 </a-button>
8 </template>
9
10 <script lang="ts" setup>
11 import { IconDownload } from '@arco-design/web-vue/es/icon';
12 import useLoading from '@/hooks/loading';
13
14 const props = withDefaults(defineProps<{ label?: string; onDownload: () => Promise<void> }>(), { label: '导出' });
15
16 const { loading, setLoading } = useLoading(false);
17
18 const onClick = () => {
19 setLoading(true);
20 props.onDownload().finally(() => setLoading(false));
21 };
22 </script>
23
24 <style scoped></style>
1 <script setup lang="ts">
2 import TableColumn from '@/components/filter/table-column.vue';
3 import { Layout, LayoutSider, LayoutContent, Image, Form, FormItem, TypographyText, TableData } from '@arco-design/web-vue';
4 import { isString, isUndefined } from '@/utils/is';
5 import { get } from 'lodash';
6
7 const props = withDefaults(
8 defineProps<{
9 title?: string;
10 dataIndex?: string;
11 row?: string;
12 coverIndex?: string;
13 nameIndex?: string;
14 subIndex?: string;
15 tagIndex?: string;
16 projectIndex?: string;
17 userIndex?: string;
18 hideSubTitle?: boolean;
19 }>(),
20 {
21 coverIndex: 'cover',
22 nameIndex: 'song_name',
23 subIndex: 'sub_title',
24 tagIndex: 'tags',
25 projectIndex: 'project',
26 userIndex: 'user',
27 }
28 );
29
30 const getRow = (record: TableData): TableData | undefined => {
31 if (isUndefined(props.row)) {
32 return record;
33 }
34
35 if (isString(props.row)) {
36 return get(record, props.row);
37 }
38
39 return props.row;
40 };
41
42 const getRowColumn = (record: TableData, key: string) => getRow(record)?.[key];
43 </script>
44
45 <template>
46 <TableColumn v-bind="$attrs" :title="title" :data-index="dataIndex">
47 <template #default="{ record }">
48 <Layout>
49 <LayoutSider :width="104" class="left">
50 <Image show-loader :height="100" :width="100" fit="cover" :src="getRowColumn(record, coverIndex)" />
51 </LayoutSider>
52 <LayoutContent>
53 <Form auto-label-width label-align="left" :model="record" style="height: 100%; justify-content: space-between">
54 <FormItem label="歌曲名称" :show-colon="true" row-class="mb-0">
55 <TypographyText :ellipsis="{ rows: 1, showTooltip: true }">
56 {{ getRowColumn(record, nameIndex) }}
57 </TypographyText>
58 </FormItem>
59 <FormItem v-if="!hideSubTitle" label="推荐语" :show-colon="true" row-class="mb-0">
60 <TypographyText :ellipsis="{ rows: 1, showTooltip: true }">
61 {{ getRowColumn(record, subIndex) }}
62 </TypographyText>
63 </FormItem>
64 <FormItem label="歌曲标签" :show-colon="true" row-class="mb-0">
65 <TypographyText :ellipsis="{ rows: 1, showTooltip: true }">
66 {{
67 getRowColumn(record, tagIndex)
68 .map((item: any) => item.name)
69 .join('、')
70 }}
71 </TypographyText>
72 </FormItem>
73 <FormItem v-if="getRowColumn(record, projectIndex)" label="关联厂牌" :show-colon="true" row-class="mb-0">
74 <TypographyText :ellipsis="{ rows: 1, showTooltip: true }">
75 {{ getRowColumn(record, projectIndex)?.name }}
76 </TypographyText>
77 </FormItem>
78 <FormItem v-else label="关联用户" :show-colon="true" row-class="mb-0">
79 <TypographyText :ellipsis="{ rows: 1, showTooltip: true }">
80 {{ getRowColumn(record, userIndex)?.nick_name }}
81 </TypographyText>
82 </FormItem>
83 </Form>
84 </LayoutContent>
85 </Layout>
86 </template>
87 </TableColumn>
88 </template>
89
90 <style lang="less" scoped>
91 :deep(.arco-typography) {
92 margin-bottom: 0;
93 width: 100%;
94 text-align: left;
95 }
96
97 .left {
98 margin-top: 5px;
99 box-shadow: unset;
100 margin-right: 5px;
101 }
102 </style>
1 <script setup lang="ts">
2 import { Avatar } from '@arco-design/web-vue';
3 import TableColumn from '@/components/filter/table-column.vue';
4 import { get } from 'lodash';
5
6 const props = defineProps<{ title?: string; dataIndex?: string; width?: number }>();
7
8 const getValue = (record: object) => {
9 return get(record, props.dataIndex || '', '');
10 };
11 </script>
12
13 <template>
14 <TableColumn v-bind="$attrs" :title="title" :data-index="dataIndex" :width="width || 60" :tooltip="false">
15 <template #default="{ record }">
16 <Avatar :size="40" shape="circle" :image-url="getValue(record)" />
17 </template>
18 </TableColumn>
19 </template>
20
21 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { get } from 'lodash';
3 import TableColumn from '@/components/filter/table-column.vue';
4 import dayjs from 'dayjs';
5
6 withDefaults(defineProps<{ title?: string; dataIndex?: string; split?: boolean }>(), { split: true });
7 const getValue = (record: object, path: string) => {
8 return get(record, path, '');
9 };
10 </script>
11
12 <template>
13 <TableColumn v-bind="$attrs" :title="title" :data-index="dataIndex">
14 <template #default="{ record }">
15 <template v-if="dataIndex && !getValue(record, dataIndex)">
16 <span style="color: rgba(0, 0, 0, 0.3)"></span>
17 </template>
18 <template v-else-if="split">
19 <div>{{ dayjs(getValue(record, dataIndex))?.format('YYYY-MM-DD') || '' }}</div>
20 <div>{{ dayjs(getValue(record, dataIndex))?.format('HH:mm:ss') || '' }}</div>
21 </template>
22 <template v-else>{{ getValue(record, dataIndex) }}</template>
23 </template>
24 </TableColumn>
25 </template>
26
27 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { Option } from '@/types/global';
3 import { eq, get } from 'lodash';
4 import TableColumn from '@/components/filter/table-column.vue';
5
6 const props = defineProps<{ title?: string; dataIndex?: string; option: Option[]; darkValue?: string | number }>();
7
8 const getValue = (record: object) => {
9 return get(record, props.dataIndex || '', '');
10 };
11 </script>
12
13 <template>
14 <TableColumn v-bind="$attrs" :title="title" :data-index="dataIndex">
15 <template #default="{ record }">
16 <template v-if="darkValue !== undefined && eq(getValue(record), darkValue)">
17 <span style="color: rgba(44, 44, 44, 0.5)"> {{ option.find((item) => item.value === getValue(record))?.label || '未知' }}</span>
18 </template>
19 <template v-else> {{ option.find((item) => item.value === getValue(record))?.label || '未知' }}</template>
20 </template>
21 </TableColumn>
22 </template>
23
24 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import TableColumn from '@/components/filter/table-column.vue';
3 import { Link } from '@arco-design/web-vue';
4 import { AnyObject } from '@/types/global';
5
6 defineProps<{
7 title?: string;
8 dataIndex?: string;
9 linkStyle?: object;
10 formatter: (record: AnyObject) => string;
11 to: (record: AnyObject) => void;
12 }>();
13 </script>
14
15 <template>
16 <TableColumn v-bind="$attrs" :title="title" :data-index="dataIndex">
17 <template #default="{ record }">
18 <Link v-if="!!formatter(record)" class="link" :style="linkStyle" :hoverable="false" @click.stop="to(record)">
19 {{ formatter(record) }}
20 </Link>
21 <span v-else style="color: rgba(44, 44, 44, 0.5)"></span>
22 </template>
23 </TableColumn>
24 </template>
25
26 <style scoped lang="less">
27 .link:hover {
28 cursor: pointer;
29 }
30 </style>
1 <script setup lang="ts">
2 import { eq, get } from 'lodash';
3 import TableColumn from '@/components/filter/table-column.vue';
4
5 const props = withDefaults(
6 defineProps<{ title?: string; dataIndex?: string; darkValue?: string | number; prefix?: string; suffix?: string }>(),
7 { prefix: '', suffix: '' }
8 );
9 const getValue = (record: object) => {
10 return get(record, props.dataIndex || '', '');
11 };
12 </script>
13
14 <template>
15 <TableColumn v-bind="$attrs" :title="title" :data-index="dataIndex">
16 <template #default="{ record }">
17 <template v-if="darkValue !== undefined && eq(getValue(record), darkValue)">
18 <span style="color: rgba(44, 44, 44, 0.5)">{{ `${prefix} 0 ${suffix}` }}</span>
19 </template>
20 <template v-else> {{ `${prefix} ${getValue(record)} ${suffix}` }} </template>
21 </template>
22 </TableColumn>
23 </template>
24
25 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { get } from 'lodash';
3 import TableColumn from '@/components/filter/table-column.vue';
4
5 defineProps<{ title?: string; dataIndex?: string; areaIndex?: string }>();
6
7 const getValue = (record: object, path: string) => {
8 return get(record, path, '');
9 };
10 </script>
11
12 <template>
13 <TableColumn v-bind="$attrs" :title="title" :data-index="dataIndex">
14 <template #default="{ record }"> {{ `(+${getValue(record, areaIndex)}) ${getValue(record, dataIndex)}` }}</template>
15 </TableColumn>
16 </template>
17
18 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { GridItem, FormItem } from '@arco-design/web-vue';
3 </script>
4
5 <template>
6 <GridItem>
7 <FormItem class="form-item" row-class="mb-0" v-bind="$attrs" :show-colon="true">
8 <slot />
9 </FormItem>
10 </GridItem>
11 </template>
12
13 <style scoped lang="less">
14 .form-item {
15 :deep(.arco-picker) {
16 width: 100%;
17 }
18 }
19 </style>
1 <script setup lang="ts">
2 import { AnyObject } from '@/types/global';
3 import { Layout, LayoutContent, LayoutSider, Form, Space, Grid } from '@arco-design/web-vue';
4 import IconButton from '@/components/icon-button/index.vue';
5
6 type PropType = {
7 model?: AnyObject;
8 loading?: boolean;
9 searchLabel?: string;
10 searchIcon?: string;
11 resetLabel?: string;
12 resetIcon?: string;
13 hideSearch?: boolean;
14 hideReset?: boolean;
15 inline?: boolean;
16 split?: number;
17 size?: 'mini' | 'small' | 'medium' | 'large';
18 hideDivider?: boolean;
19 };
20
21 const props = withDefaults(defineProps<PropType>(), {
22 loading: false,
23 searchLabel: '搜索',
24 searchIcon: 'search',
25 resetLabel: '重置',
26 resetIcon: 'refresh',
27 hideSearch: false,
28 hideReset: false,
29 hideDivider: false,
30 inline: false,
31 split: 3,
32 size: 'small',
33 });
34 defineEmits<{ (e?: 'search'): void; (e?: 'reset'): void }>();
35
36 const layoutStyle = { marginBottom: '12px' };
37 const layoutRightStyle = props.hideSearch && props.hideReset ? {} : { borderLeft: '1px solid var(--color-neutral-3)' };
38
39 if (!props.hideDivider) {
40 Object.assign(layoutStyle, { paddingBottom: '12px', borderBottom: '1px solid var(--color-neutral-3)' });
41 }
42 </script>
43
44 <template>
45 <Layout :style="layoutStyle">
46 <LayoutContent>
47 <Form auto-label-width :model="model" label-align="right" :size="size">
48 <Grid :cols="split as number" :col-gap="12" :row-gap="12">
49 <slot :size="size" />
50 </Grid>
51 </Form>
52 </LayoutContent>
53 <LayoutSider class="right" :style="layoutRightStyle">
54 <Space :size="12" :direction="inline ? 'horizontal' : 'vertical'">
55 <IconButton
56 v-if="!hideSearch"
57 :size="size"
58 :icon="searchIcon"
59 :label="searchLabel"
60 type="primary"
61 :loading="loading"
62 @click="$emit('search')"
63 />
64 <IconButton v-if="!hideReset" :size="size" :icon="resetIcon" :label="resetLabel" @click="$emit('reset')" />
65 <slot name="button" />
66 </Space>
67 </LayoutSider>
68 </Layout>
69 </template>
70
71 <style scoped lang="less">
72 .right {
73 border-left: 1px solid var(--color-neutral-3);
74 margin-left: 12px;
75 padding-left: 12px;
76 box-shadow: unset;
77 width: auto !important;
78 }
79 </style>
1 <script setup lang="ts">
2 import { Space } from '@arco-design/web-vue';
3 import TableColumn from '@/components/filter/table-column.vue';
4
5 withDefaults(defineProps<{ title?: string; dataIndex?: string; direction?: 'vertical' | 'horizontal' }>(), {
6 direction: 'horizontal',
7 });
8 </script>
9
10 <template>
11 <TableColumn v-bind="$attrs" :title="title" :data-index="dataIndex" :tooltip="false">
12 <template #default="{ record, rowIndex }">
13 <Space :direction="direction" fill>
14 <slot name="default" :record="record" :index="rowIndex" />
15 </Space>
16 </template>
17 </TableColumn>
18 </template>
19
20 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import TableColumn from '@/components/filter/table-column.vue';
3
4 const props = withDefaults(
5 defineProps<{
6 title?: string;
7 }>(),
8 {}
9 );
10 </script>
11
12 <template>
13 <TableColumn v-bind="$attrs" :title="title">
14 <template #default="{ record }">
15 <div class="box">
16 <a-image height="60" fit="fill" :src="record.icon" />
17 </div>
18 </template>
19 </TableColumn>
20 </template>
21
22 <style scoped lang="less">
23 .box {
24 height: 60px;
25 }
26 </style>
1 <script setup lang="ts">
2 import TableColumn from '@/components/filter/table-column.vue';
3
4 const props = withDefaults(
5 defineProps<{
6 title?: string;
7 }>(),
8 {}
9 );
10
11 const getIcon = (record): string => {
12 return record.frame?.url;
13 };
14
15 const getBorder = (record): string => {
16 return record.frame?.color;
17 };
18 </script>
19
20 <template>
21 <TableColumn v-bind="$attrs" :title="title">
22 <template #default="{ record }">
23 <div v-if="record.frame" class="box" :style="{ border: `2px solid ${getBorder(record)}` }">
24 <div class="iconBox">
25 <a-image height="22" fit="fill" style="margin-top: -2px" :src="getIcon(record)" />
26 </div>
27 </div>
28 <div v-else class="boxNull"></div>
29 </template>
30 </TableColumn>
31 </template>
32
33 <style scoped lang="less">
34 .box {
35 width: 50px;
36 height: 50px;
37 border-radius: 50%;
38 background-color: #f2f3f5;
39 position: relative;
40
41 .iconBox {
42 display: flex;
43 align-items: center;
44 height: 22px;
45 position: absolute;
46 right: 0;
47 bottom: 0;
48 z-index: 1;
49 overflow: hidden;
50 }
51 }
52
53 .boxNull {
54 width: 50px;
55 height: 50px;
56 display: flex;
57 align-items: center;
58 justify-content: center;
59 }
60 </style>
1 <script setup lang="ts">
2 // eslint-disable-next-line @typescript-eslint/no-unused-vars
3 import { TableColumn, TableData, TableSortable } from '@arco-design/web-vue';
4
5 type PropType = { title?: string; dataIndex?: string; tooltip?: boolean; ellipsis?: boolean; hasSort?: boolean };
6
7 const sortable = { sortDirections: ['ascend', 'descend'], sorter: true, defaultSortOrder: '' } as TableSortable;
8
9 withDefaults(defineProps<PropType>(), { tooltip: true, hasSort: false });
10 </script>
11
12 <template>
13 <TableColumn
14 v-bind="$attrs"
15 :title="title"
16 :data-index="dataIndex"
17 :tooltip="tooltip as boolean"
18 :ellipsis="ellipsis || tooltip as boolean"
19 :sortable="hasSort ? sortable : undefined as any"
20 >
21 <template v-if="$slots.default" #cell="{ record, rowIndex }: { record: TableData, rowIndex: number }">
22 <slot name="default" :record="record" :index="rowIndex" />
23 </template>
24 </TableColumn>
25 </template>
26
27 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { computed, ref } from 'vue';
3 import usePagination from '@/hooks/pagination';
4 import { Table, Row, Col } from '@arco-design/web-vue';
5 import { AnyObject, sizeType } from '@/types/global';
6
7 type PropType = {
8 loading?: boolean;
9 rowKey?: string;
10 hoverType?: string;
11 hidePage?: boolean;
12 simplePage?: boolean;
13 pageSize?: number;
14 size?: sizeType;
15 onQuery: (params?: AnyObject) => Promise<any>;
16 };
17
18 defineEmits<{
19 (e: 'rowSort', dataIndex: string, direction: string): void;
20 }>();
21
22 const props = withDefaults(defineProps<PropType>(), { loading: false, rowKey: 'id', size: 'small', pageSize: 20 });
23 const { pagination, setPage, setPageSize, setTotal } = usePagination({
24 simple: props.simplePage,
25 size: props.size,
26 pageSize: props.pageSize,
27 });
28 const list = ref<any[]>([]);
29 const tableRef = ref();
30
31 const pageQueryParams = computed(() => {
32 return props.hidePage ? {} : { page: pagination.value.current, pageSize: pagination.value.pageSize };
33 });
34
35 const hoverType = computed(() => props.hoverType || 'default');
36
37 const onFetch = () =>
38 props.onQuery(pageQueryParams.value).then(({ data, meta }) => {
39 list.value = data;
40 setPage(props.hidePage ? 1 : meta.current);
41 setTotal(props.hidePage ? data.length : meta.total);
42 setPageSize(props.hidePage ? pagination.value.pageSize : meta.limit);
43 });
44
45 const onPageChange = (page: number) => {
46 setPage(page || 1);
47 onFetch();
48 };
49
50 const onSizeChange = (size: number) => {
51 setPageSize(size);
52 setPage(1);
53 onFetch();
54 };
55
56 const onPush = (row: unknown) => {
57 list.value.unshift(row);
58 // eslint-disable-next-line no-plusplus
59 ++pagination.value.total;
60 };
61
62 const onRemove = (index: number) => {
63 list.value.splice(index, 1);
64 // eslint-disable-next-line no-plusplus
65 --pagination.value.total;
66 };
67
68 const resetSort = () => tableRef.value?.clearSorters();
69
70 const getPagination = () => pagination.value;
71
72 const getCount = () => pagination.value.total;
73
74 const getList = () => list.value;
75
76 const formatSortType = (type: string): string => type?.replace('end', '') ?? '';
77
78 defineExpose({ onFetch, onPageChange, onSizeChange, resetSort, getCount, getPagination, getList, onPush, onRemove });
79 </script>
80
81 <template>
82 <Row justify="space-between" align="center">
83 <Col class="table-tool-item" :span="16" style="text-align: left">
84 <slot name="tool" :size="size" />
85 </Col>
86 <Col class="table-tool-item" :span="8" style="text-align: right">
87 <slot name="tool-right" :size="size" />
88 </Col>
89 </Row>
90 <Table
91 ref="tableRef"
92 v-bind="$attrs"
93 :row-key="rowKey as string"
94 :loading="loading as boolean"
95 :size="size as sizeType"
96 :data="list"
97 :pagination="hidePage ? false : pagination"
98 :bordered="false"
99 :table-layout-fixed="true"
100 @page-change="onPageChange"
101 @page-size-change="onSizeChange"
102 @sorter-change="(dataIndex:string, direction:string) => $emit('rowSort', dataIndex, formatSortType(direction))"
103 >
104 <template #columns>
105 <slot />
106 </template>
107 </Table>
108 </template>
109
110 <style lang="less" scoped>
111 :deep(.arco-table-cell) {
112 padding: 5px 8px !important;
113
114 :hover {
115 cursor: v-bind(hoverType);
116 }
117
118 & > .arco-table-td-content .arco-btn-size-small {
119 padding: 5px !important;
120 }
121 }
122
123 :deep(.table-tool-item) {
124 > * {
125 margin-bottom: 12px !important;
126 }
127 }
128 </style>
1 <script setup lang="ts">
2 import { eq, get } from 'lodash';
3 import TableColumn from '@/components/filter/table-column.vue';
4
5 const props = defineProps<{ title?: string; dataIndex?: string }>();
6
7 const getValue = (record: object) => {
8 return get(record, props.dataIndex || '', '');
9 };
10 </script>
11
12 <template>
13 <TableColumn v-bind="$attrs" :title="title" :data-index="dataIndex">
14 <template #default="{ record }">
15 <span v-if="eq(getValue(record), 1)">音乐人</span>
16 <span v-else-if="[2, 3].includes(Number(getValue(record)))">经纪人</span>
17 <span v-else style="color: rgba(44, 44, 44, 0.5)">未认证</span>
18 </template>
19 </TableColumn>
20 </template>
21
22 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { Avatar, Link } from '@arco-design/web-vue';
3 import TableColumn from '@/components/filter/table-column.vue';
4 import { User } from '@/utils/model';
5 import { isString, isUndefined } from '@/utils/is';
6 import openNewTab from '@/utils/route-blank';
7 import { get } from 'lodash';
8 import { useRouter } from 'vue-router';
9
10 const props = withDefaults(
11 defineProps<{
12 title?: string;
13 dataIndex?: string;
14 user?: string | Pick<User, 'id' | 'nick_name' | 'real_name' | 'identity'>;
15 showHref?: boolean;
16 showRealName?: boolean;
17 showAvatar?: boolean;
18 darkValue?: string;
19 nickIndex?: string;
20 realIndex?: string;
21 avatarIndex?: string;
22 roleIndex?: string;
23 linkStyle?: object;
24 prefix?: string;
25 }>(),
26 {
27 dataIndex: 'id',
28 nickIndex: 'nick_name',
29 realIndex: 'real_name',
30 avatarIndex: 'avatar',
31 roleIndex: 'identity',
32 prefix: '',
33 }
34 );
35
36 const router = useRouter();
37
38 const getUser = (record: object): Pick<User, 'id' | 'nick_name' | 'real_name' | 'identity'> | undefined => {
39 if (isUndefined(props.user)) {
40 return record as User;
41 }
42
43 if (isString(props.user)) {
44 return get(record, props.user);
45 }
46
47 return get(props, 'user') as User;
48 };
49
50 const getName = (record: object): string => {
51 const user = getUser(record);
52
53 if (!user) {
54 return '';
55 }
56
57 if (props.showRealName) {
58 return `${props.prefix}${get(user, props.nickIndex, '')}(${get(user, props.realIndex)})`;
59 }
60
61 return props.prefix + get(user, props.nickIndex, '');
62 };
63
64 const onHref = (record: object) => {
65 const user = getUser(record);
66 const key = get(record, props.dataIndex);
67 const identity = get(user, props.roleIndex);
68
69 if (identity === 1) {
70 // return router.push({ name: 'user-singer-show', params: { id: key } });
71 openNewTab(router, 'user-singer-show', { id: key });
72 }
73 if ([2, 3].includes(Number(identity))) {
74 // return router.push({ name: 'user-business-show', params: { id: key } });
75 openNewTab(router, 'user-business-show', { id: key });
76 }
77 // return router.push({ name: 'user-register-show', params: { id: key } });
78 openNewTab(router, 'user-register-show', { id: key });
79 };
80 </script>
81
82 <template>
83 <TableColumn v-bind="$attrs" :title="title" :data-index="dataIndex">
84 <template #default="{ record }">
85 <template v-if="darkValue !== undefined && getName(record) === darkValue">
86 <span style="color: rgba(44, 44, 44, 0.5)"></span>
87 </template>
88 <template v-else>
89 <Avatar v-if="showAvatar" style="margin-right: 8px" :size="34" shape="circle" :image-url="getUser(record)?.[avatarIndex]" />
90 <Link v-if="showHref" class="link-hover" :style="linkStyle" :hoverable="false" @click.stop="onHref(record)">
91 {{ getName(record) }}
92 </Link>
93 <template v-else>{{ getName(record) }}</template>
94 </template>
95 </template>
96 </TableColumn>
97 </template>
98
99 <style scoped lang="less"></style>
1 <template>
2 <Button v-bind="$attrs" :type="type" :status="status" @click="() => emits('click')">
3 <template v-if="icon && iconAlign === 'left'" #icon>
4 <component :is="Icon[iconName]" />
5 </template>
6 {{ label }}
7 <component :is="Icon[iconName]" v-if="icon && iconAlign === 'right'" :size="$attrs['size'] || 'small'" style="margin-left: 6px" />
8 </Button>
9 </template>
10
11 <script lang="ts" setup>
12 import { Button } from '@arco-design/web-vue';
13 import * as Icon from '@arco-design/web-vue/es/icon';
14 import { computed } from 'vue';
15 import { camelCase, upperFirst } from 'lodash';
16
17 const props = withDefaults(
18 defineProps<{
19 label?: string;
20 icon?: string;
21 iconAlign?: 'left' | 'right';
22 type?: 'primary' | 'secondary' | 'outline' | 'dashed' | 'text';
23 shape?: 'square' | 'round' | 'circle';
24 status?: 'normal' | 'warning' | 'success' | 'danger';
25 }>(),
26 {
27 iconAlign: 'left',
28 type: 'secondary',
29 shape: 'square',
30 status: 'normal',
31 }
32 );
33
34 const emits = defineEmits(['click']);
35
36 const iconName = computed(() => upperFirst(camelCase(`icon-${props.icon}`)));
37 </script>
38
39 <style scoped></style>
1 <template>
2 <Upload :accept="accept" :custom-request="onUpload" :file-list="[]" :show-file-list="false">
3 <template #upload-button>
4 <div class="arco-upload-list-item">
5 <Image
6 v-if="modelValue"
7 :style="{ minHeight }"
8 :width="width"
9 :height="height"
10 :preview="false"
11 :src="modelValue"
12 :fit="fit"
13 show-loader
14 />
15 <div v-else :style="style" class="arco-upload-picture-card">
16 <div class="arco-upload-picture-card-text">
17 <IconPlus />
18 </div>
19 </div>
20 </div>
21 </template>
22 </Upload>
23 </template>
24
25 <script lang="ts" setup>
26 import useOss from '@/hooks/oss';
27 import { computed } from 'vue';
28 import { IconPlus } from '@arco-design/web-vue/es/icon';
29 import { UploadRequest, useFormItem, Upload, Image } from '@arco-design/web-vue';
30
31 type PropType = {
32 modelValue?: string;
33 accept?: string;
34 width?: number | string;
35 height?: number | string;
36 minHeight?: string;
37 fit?: 'none' | 'contain' | 'cover' | 'fill' | 'scale-down' | undefined;
38 };
39
40 const props = withDefaults(defineProps<PropType>(), {
41 modelValue: '',
42 accept: 'image/*',
43 width: 80,
44 height: 'auto',
45 minHeight: '100%',
46 fit: 'cover',
47 });
48
49 const emits = defineEmits(['update:modelValue']);
50 const { eventHandlers } = useFormItem();
51
52 const style = computed(() => {
53 return {
54 width: `${props.width}px`,
55 height: props.height.constructor === String ? 'auto' : `${props.height}px`,
56 maxWidth: '100%',
57 maxHeight: '100%',
58 minHeight: '80px',
59 };
60 });
61
62 const { upload } = useOss();
63
64 const onUpload = (option: any): UploadRequest => {
65 const { fileItem } = option;
66
67 if (fileItem.file) {
68 upload(fileItem.file, 'image').then((res) => {
69 emits('update:modelValue', res?.url || '');
70 eventHandlers.value?.onChange?.();
71 });
72 }
73
74 return {};
75 };
76 </script>
77
78 <style lang="less" scoped>
79 :deep(.arco-avatar-text) {
80 width: 100%;
81 height: 100%;
82 }
83
84 :deep(.arco-upload-list-item) {
85 margin-top: 0 !important;
86 }
87
88 .no-avatar {
89 position: absolute;
90 display: flex;
91 align-content: center;
92 align-items: center;
93 justify-content: center;
94 width: 100%;
95 height: 100%;
96 top: 0;
97 left: 0;
98 }
99 </style>
1 import { App } from 'vue';
2 import { use } from 'echarts/core';
3 import { CanvasRenderer } from 'echarts/renderers';
4 import { BarChart, LineChart, PieChart, RadarChart } from 'echarts/charts';
5 import { DataZoomComponent, GraphicComponent, GridComponent, LegendComponent, TooltipComponent } from 'echarts/components';
6 import Chart from './chart/index.vue';
7 import Breadcrumb from './breadcrumb/index.vue';
8 import AvatarUpload from './avatar-upload/index.vue';
9 import InputUpload from './input-upload/index.vue';
10 import ImageUpload from './image-upload/index.vue';
11 import ExportButton from './export-button/index.vue';
12 import IconButton from './icon-button/index.vue';
13 import AudioPlayer from './audio-player/index.vue';
14 import ProjectSelect from './project-select/index.vue';
15 import UserLink from './user-link/index.vue';
16 import TagSelect from './tag-select/index.vue';
17 import UserSelect from './user-select/index.vue';
18 import PermissionSelect from './permission-select/index.vue';
19 import FilterSearch from './filter/search.vue';
20 import FilterSearchItem from './filter/search-item.vue';
21 import FilterTable from './filter/table.vue';
22 import FilterTableColumn from './filter/table-column.vue';
23 import PageView from './page-view/index.vue';
24
25 // import SvgIcon from './svg-icon/index.vue';
26
27 // Manually introduce ECharts modules to reduce packing size
28
29 use([
30 CanvasRenderer,
31 BarChart,
32 LineChart,
33 PieChart,
34 RadarChart,
35 GridComponent,
36 TooltipComponent,
37 LegendComponent,
38 DataZoomComponent,
39 GraphicComponent,
40 ]);
41
42 export default {
43 install(Vue: App) {
44 Vue.component('Chart', Chart);
45 Vue.component('Breadcrumb', Breadcrumb);
46 Vue.component('AvatarUpload', AvatarUpload);
47 Vue.component('InputUpload', InputUpload);
48 Vue.component('ImageUpload', ImageUpload);
49 Vue.component('ExportButton', ExportButton);
50 Vue.component('IconButton', IconButton);
51 Vue.component('AudioPlayer', AudioPlayer);
52 Vue.component('ProjectSelect', ProjectSelect);
53 Vue.component('UserLink', UserLink);
54 Vue.component('TagSelect', TagSelect);
55 Vue.component('UserSelect', UserSelect);
56 Vue.component('PermissionSelect', PermissionSelect);
57 Vue.component('FilterSearch', FilterSearch);
58 Vue.component('FilterSearchItem', FilterSearchItem);
59 Vue.component('FilterTable', FilterTable);
60 Vue.component('FilterTableColumn', FilterTableColumn);
61 Vue.component('PageView', PageView);
62 // Vue.component('SvgIcon', SvgIcon);
63 },
64 };
1 <template>
2 <Upload
3 v-bind="$attrs"
4 :on-before-upload="onBeforeUpload"
5 :custom-request="onUpload"
6 :file-list="[]"
7 :show-file-list="false"
8 style="width: 100%"
9 >
10 <template #upload-button>
11 <Input :model-value="modelValue" :readonly="true" :placeholder="placeholder">
12 <template #suffix>
13 <Progress v-if="loading" :percent="percent" size="mini" />
14 <IconUpload v-else />
15 </template>
16 </Input>
17 </template>
18 </Upload>
19 </template>
20
21 <script lang="ts" setup>
22 import { ref } from 'vue';
23 import { IconUpload } from '@arco-design/web-vue/es/icon';
24 import useLoading from '@/hooks/loading';
25 import useOss from '@/hooks/oss';
26 import { Input, Message, Progress, Upload, UploadRequest, useFormItem } from '@arco-design/web-vue';
27 import { startsWith } from 'lodash';
28
29 type FileType = { name: string; url: string; size: number; type: string; width?: number; height?: number; duration?: number };
30
31 const props = defineProps({
32 modelValue: { type: String, default: '' },
33 prefix: { type: String, default: 'file' },
34 limit: { type: Number, default: 0 },
35 placeholder: { type: String, default: '请选择' },
36 });
37
38 const emits = defineEmits<{
39 (e: 'update:modelValue', value: string): void;
40 (e: 'success', value: FileType): void;
41 (e: 'choose-file', value: File): void;
42 }>();
43 const { eventHandlers } = useFormItem();
44
45 const { loading, setLoading } = useLoading(false);
46 const { upload } = useOss();
47 const percent = ref<number>(0);
48
49 const onBeforeUpload = (file: File & { width?: number; height?: number; duration?: number }) => {
50 if (props.limit !== 0 && file.size > props.limit * 1024 * 1024) {
51 Message.warning(`${file.name} 文件超过${props.limit}MB,无法上传`);
52 return Promise.resolve(false);
53 }
54
55 if (startsWith(file.type, 'image/')) {
56 const imgObj = new Image();
57 imgObj.src = URL.createObjectURL(file);
58 imgObj.onload = () => {
59 file.width = imgObj.width;
60 file.height = imgObj.height;
61 };
62 }
63
64 if (startsWith(file.type, 'audio/') || startsWith(file.type, 'video/')) {
65 const audioElement = new Audio(URL.createObjectURL(file));
66 audioElement.addEventListener('loadedmetadata', () => {
67 file.duration = audioElement.duration * 1000;
68 });
69 }
70
71 return Promise.resolve(file);
72 };
73
74 const onProgress = (p: number) => {
75 percent.value = p;
76 };
77
78 const onUpload = (option: any): UploadRequest => {
79 const { fileItem } = option;
80
81 if (fileItem.file) {
82 setLoading(true);
83 // eslint-disable-next-line vue/custom-event-name-casing
84 emits('choose-file', fileItem.file as File);
85 upload(fileItem.file, props.prefix, onProgress)
86 .then((res) => {
87 emits('update:modelValue', res?.url || '');
88 eventHandlers.value?.onChange?.();
89 fileItem.percent = 100;
90 fileItem.url = res?.url || '';
91 fileItem.status = 'done';
92 emits('success', {
93 name: fileItem.name,
94 url: fileItem.url,
95 size: fileItem.file.size,
96 type: fileItem.file.type,
97 width: fileItem.file.width || 0,
98 height: fileItem.file.height || 0,
99 duration: fileItem.file.duration || 0,
100 });
101 })
102 .finally(() => {
103 setLoading(false);
104 });
105 }
106
107 return {};
108 };
109 </script>
110
111 <style lang="less" scoped>
112 ::v-deep(.arco-input-append) {
113 padding: 0;
114 }
115 </style>
1 <script setup lang="ts">
2 withDefaults(defineProps<{ hasCard?: boolean; hasBread?: boolean; loading?: boolean }>(), {
3 loading: false,
4 });
5 </script>
6
7 <template>
8 <div class="container">
9 <breadcrumb v-if="hasBread && $route.meta?.breadcrumb" :items="$route.meta?.breadcrumb" />
10 <a-spin style="width: 100%" :loading="loading as boolean" dot>
11 <a-card v-if="hasCard" :bordered="false" v-bind="$attrs">
12 <slot />
13 </a-card>
14 <slot v-else />
15 </a-spin>
16 </div>
17 </template>
18
19 <style scoped lang="less">
20 .container {
21 padding: 0 30px 20px 20px;
22 }
23 </style>
1 <template>
2 <TreeSelect
3 :model-value="modelValue"
4 :data="permissionData"
5 :field-names="fieldName"
6 :placeholder="placeholder"
7 :allow-clear="allowClear"
8 :allow-search="allowSearch"
9 :tree-props="treeProp"
10 :filter-tree-node="onFilter"
11 :fallback-option="false"
12 :path-mode="true"
13 @update:model-value="onUpdate"
14 />
15 </template>
16
17 <script lang="ts" setup>
18 import { TreeNodeData, TreeSelect } from '@arco-design/web-vue';
19 import { computed, toRefs } from 'vue';
20 import { SystemPermission } from '@/types/system-permission';
21 import { isUndefined } from '@/utils/is';
22 import { useAppStore } from '@/store';
23 import { storeToRefs } from 'pinia';
24 import { arrayToTree } from '@/utils';
25 import { AnyObject } from '@/types/global';
26
27 type PropType = {
28 modelValue?: number | string | number[];
29 guard?: 'Admin' | 'Manage' | '';
30 allowClear?: boolean;
31 allowSearch?: boolean;
32 placeholder?: string;
33 };
34
35 const props = withDefaults(defineProps<PropType>(), {
36 allowClear: false,
37 allowSearch: false,
38 placeholder: '请选择',
39 modelValue: '',
40 guard: '',
41 });
42
43 const { guard } = toRefs(props);
44
45 const emits = defineEmits<{ (e: 'update:modelValue', value: number | string): void }>();
46 const fieldName = { key: 'name', title: 'label', children: 'children', icon: 'icm' };
47 const treeProp = { virtualListProps: { height: 300 } };
48 const appStore = useAppStore();
49 const { permissions } = storeToRefs(appStore);
50
51 const onUpdate = (val: number | string | undefined) => emits('update:modelValue', isUndefined(val) ? '' : val);
52 const onFilter = (searchKey: string, nodeData: SystemPermission) => nodeData.label.indexOf(searchKey) > -1;
53
54 const permissionData = computed(() => {
55 const list = permissions?.value?.filter((item) => item.type === 'Menu' && item.guard === guard.value) as AnyObject[];
56 return arrayToTree(list) as TreeNodeData[];
57 });
58 </script>
59
60 <style scoped></style>
1 <template>
2 <Select
3 v-bind="$attrs"
4 v-model="formValue"
5 :options="options"
6 :fallback-option="false"
7 :field-names="{ value: 'id', label: 'name' }"
8 :placeholder="placeholder"
9 :allow-search="true"
10 />
11 </template>
12
13 <script lang="ts" setup>
14 import { Select } from '@arco-design/web-vue';
15 import { useSelectionStore } from '@/store';
16 import { useVModels } from '@vueuse/core';
17 import { storeToRefs } from 'pinia';
18 import { computed } from 'vue';
19 import { filter } from 'lodash';
20
21 type propType = { modelValue?: number | string | number[]; placeholder?: string; filtrate?: (value: any) => boolean };
22
23 const props = withDefaults(defineProps<propType>(), {
24 filtrate: () => true,
25 placeholder: '请选择',
26 });
27 const emits = defineEmits(['update:modelValue', 'update:loading']);
28
29 const { getProjectOptions } = storeToRefs(useSelectionStore());
30
31 const options = computed(() => filter(getProjectOptions.value, props.filtrate));
32
33 const { modelValue: formValue } = useVModels(props, emits);
34 </script>
35
36 <style lang="less" scoped></style>
1 <template>
2 <a-button v-if="type === 'Button'" type="text" size="small" :disabled="disabled as boolean" @click="onClick">
3 <slot>详情</slot>
4 </a-button>
5 <a-link v-else class="link" :disabled="disabled as boolean" :hoverable="false" @click="onClick">
6 <a-typography-text
7 type="primary"
8 :disabled="disabled"
9 style="margin-bottom: 0 !important"
10 :ellipsis="{ rows: 1, showTooltip: showTooltip }"
11 >
12 <slot>详情</slot>
13 </a-typography-text>
14 </a-link>
15 </template>
16
17 <script lang="ts" setup>
18 import { RouteParamsRaw, useRouter } from 'vue-router';
19 import { AnyObject } from '@/types/global';
20
21 const props = withDefaults(
22 defineProps<{
23 name?: string;
24 params?: AnyObject;
25 type?: 'Button' | 'link';
26 showTooltip?: boolean;
27 disabled?: boolean;
28 }>(),
29 {
30 name: '',
31 type: 'Button',
32 params: undefined,
33 showTooltip: true,
34 disabled: false,
35 }
36 );
37
38 const router = useRouter();
39 const onClick = () => {
40 router.push({
41 name: props.name,
42 params: (props.params as RouteParamsRaw) || {},
43 });
44 };
45 </script>
46
47 <style lang="less" scoped>
48 .link {
49 width: 100%;
50 padding: 1px 0;
51 display: block !important;
52 }
53
54 .link:hover {
55 cursor: pointer;
56 }
57 </style>
1 <template>
2 <Select
3 v-bind="$attrs"
4 v-model="formValue"
5 :options="activityTagOptions"
6 :fallback-option="false"
7 :placeholder="placeholder"
8 :field-names="{ value: 'id', label: 'name' }"
9 @exceed-limit="onTagExceedLimitError"
10 />
11 </template>
12
13 <script lang="ts" setup>
14 import { Message, Select } from '@arco-design/web-vue';
15 import { computed, onMounted } from 'vue';
16 import { useSelectionStore } from '@/store';
17 import { isArray } from '@/utils/is';
18 import { useVModels } from '@vueuse/core';
19
20 type propType = { modelValue?: number | string | number[]; placeholder?: string; limitErrorMsg?: string };
21 const props = withDefaults(defineProps<propType>(), { placeholder: '请选择', limitErrorMsg: '超出最大选中数' });
22 const emits = defineEmits(['update:modelValue', 'update:loading']);
23
24 const { modelValue: formValue } = useVModels(props, emits);
25
26 const { activityTagOptions } = useSelectionStore();
27
28 const tagIds = computed(() => activityTagOptions.map((item) => item.id));
29
30 const onTagExceedLimitError = () => Message.warning({ content: props.limitErrorMsg, duration: 1500 });
31
32 onMounted(() => {
33 if (isArray(props.modelValue)) {
34 emits('update:modelValue', props.modelValue.filter((item: number) => tagIds.value?.indexOf(item) !== -1) || []);
35 }
36 });
37 </script>
38
39 <style scoped></style>
1 <template>
2 <Link v-if="user" :hoverable="false" class="link" @click.stop="onClick">
3 <TypographyParagraph type="primary" class="name" :ellipsis="{ rows: 1, showTooltip }">
4 <slot>{{ fullName }}</slot>
5 </TypographyParagraph>
6 </Link>
7 </template>
8
9 <script lang="ts" setup>
10 import { computed, toRefs } from 'vue';
11 import { useRouter } from 'vue-router';
12 import { BaseUser } from '@/utils/model';
13 import { Link, TypographyParagraph } from '@arco-design/web-vue';
14
15 type PropType = { user?: BaseUser; showTooltip?: boolean };
16
17 const props = withDefaults(defineProps<PropType>(), { showTooltip: true });
18 const { user } = toRefs(props);
19 const router = useRouter();
20
21 const fullName = computed(() => `${user?.value?.nick_name}`);
22
23 const onClick = () => {
24 if (user?.value?.identity === 1) {
25 return router.push({ name: 'user-singer-show', params: { id: user?.value?.id } });
26 }
27 if ([2, 3].includes(Number(user?.value?.identity))) {
28 return router.push({ name: 'user-business-show', params: { id: user?.value?.id } });
29 }
30 return router.push({ name: 'user-register-show', params: { id: user?.value?.id } });
31 };
32 </script>
33
34 <style lang="less" scoped>
35 .link {
36 display: block !important;
37 width: 100%;
38 padding: 1px 0;
39
40 .name {
41 margin-bottom: 0 !important;
42 min-width: 26px;
43 width: 100%;
44 }
45
46 :hover {
47 cursor: pointer !important;
48 }
49 }
50 </style>
1 <template>
2 <!-- :virtual-list-props="{ height: 240 }"-->
3
4 <Select
5 v-bind="$attrs"
6 v-model="formValue"
7 :fallback-option="false"
8 :field-names="{ value: 'id', label: 'nick_name' }"
9 :options="options"
10 :placeholder="placeholder"
11 :virtual-list-props="{ height: '240px' }"
12 @exceed-limit="onExceedLimitError"
13 >
14 <template #option="{ data }">
15 <Avatar v-if="hasAvatar" :size="28" :image-url="data.avatar" style="margin-right: 8px" />
16 <span>{{ `${data.nick_name}(${data.real_name})` }}</span>
17 </template>
18 </Select>
19 </template>
20
21 <script lang="ts" setup>
22 import { Avatar, Message, Select } from '@arco-design/web-vue';
23
24 import { useSelectionStore } from '@/store';
25 import { storeToRefs } from 'pinia';
26 import { useVModels } from '@vueuse/core';
27 import { computed } from 'vue';
28 import { filter } from 'lodash';
29
30 type PropType = {
31 modelValue?: number | string | number[];
32 placeholder?: string;
33 limitErrorMsg?: string;
34 hasAvatar?: boolean;
35 filtrate?: (value: any) => boolean;
36 };
37 const { getUserOptions } = storeToRefs(useSelectionStore());
38
39 const props = withDefaults(defineProps<PropType>(), {
40 placeholder: '请选择',
41 limitErrorMsg: '超出最大选中数',
42 hasAvatar: true,
43 filtrate: () => true,
44 });
45 const emits = defineEmits(['update:modelValue', 'update:loading']);
46
47 const options = computed(() => filter(getUserOptions.value, props.filtrate));
48 const { modelValue: formValue } = useVModels(props, emits);
49
50 const onExceedLimitError = () => Message.warning({ content: props.limitErrorMsg, duration: 1500 });
51 </script>
52
53 <style scoped></style>
1 import { App } from 'vue';
2 import permission from './permission';
3
4 export default {
5 install(Vue: App) {
6 Vue.directive('permission', permission);
7 },
8 };
1 import { DirectiveBinding } from 'vue';
2 import { useAuthorizedStore } from '@/store';
3
4 function checkPermission(el: HTMLElement, binding: DirectiveBinding) {
5 const { value } = binding;
6 const userStore = useAuthorizedStore();
7 const { permissions } = userStore;
8 if (Array.isArray(value)) {
9 if (value.length > 0) {
10 const hasPermission = value.filter((item: string) => permissions.includes(item));
11 if (hasPermission.length === 0 && el.parentNode) {
12 if (el.parentNode.childElementCount === 1) {
13 // @ts-ignore
14 el.parentNode.remove();
15 } else {
16 el.parentNode.removeChild(el);
17 }
18 }
19 }
20 } else {
21 throw new Error(`need roles! Like v-permission="['admin','user']"`);
22 }
23 }
24
25 export default {
26 mounted(el: HTMLElement, binding: DirectiveBinding) {
27 checkPermission(el, binding);
28 },
29 updated(el: HTMLElement, binding: DirectiveBinding) {
30 checkPermission(el, binding);
31 },
32 };
1 /// <reference types="vite/client" />
2
3 declare module '*.vue' {
4 import { DefineComponent } from 'vue';
5 // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
6 const component: DefineComponent<{}, {}, any>;
7 export default component;
8 }
1 import { computed } from 'vue';
2 import { EChartsOption } from 'echarts';
3 import { useAppStore } from '@/store';
4
5 // for code hints
6 // import { SeriesOption } from 'echarts';
7 // Because there are so many configuration items, this provides a relatively convenient code hint.
8 // When using vue, pay attention to the reactive issues. It is necessary to ensure that corresponding functions can be triggered, Typescript does not report errors, and code writing is convenient.
9 interface optionsFn {
10 (isDark: boolean): EChartsOption;
11 }
12
13 export default function useChartOption(sourceOption: optionsFn) {
14 const appStore = useAppStore();
15 const isDark = computed(() => {
16 return appStore.theme === 'dark';
17 });
18 // echarts support https://echarts.apache.org/zh/theme-builder.html
19 // It's not used here
20 // TODO echarts themes
21 const chartOption = computed<EChartsOption>(() => {
22 return sourceOption(isDark.value);
23 });
24 return {
25 chartOption,
26 };
27 }
1 import { ref } from 'vue';
2
3 export default function useLoading(initValue = false) {
4 const loading = ref(initValue);
5 const setLoading = (value: boolean) => {
6 loading.value = value;
7 };
8 const toggle = () => {
9 loading.value = !loading.value;
10 };
11 return {
12 loading,
13 setLoading,
14 toggle,
15 };
16 }
1 import OSS from 'ali-oss';
2 import { Message } from '@arco-design/web-vue';
3
4 let ossClient: OSS;
5
6 const createOssClient = () => {
7 if (!ossClient) {
8 ossClient = new OSS({
9 accessKeyId: import.meta.env.VITE_OSS_ACCESS_KEY,
10 accessKeySecret: import.meta.env.VITE_OSS_ACCESS_SECRET,
11 bucket: import.meta.env.VITE_OSS_BUCKET,
12 region: import.meta.env.VITE_OSS_REGION,
13 endpoint: import.meta.env.VITE_OSS_ENDPOINT,
14 cname: true,
15 timeout: 90000,
16 });
17 }
18
19 return ossClient;
20 };
21
22 export default function useOss() {
23 const getFileName = () => {
24 function rx() {
25 return Math.random().toString(36).substring(2);
26 }
27
28 return `${rx()}${new Date().getTime()}${rx()}`;
29 };
30
31 const getFileDir = () => {
32 return new Date().toISOString().slice(0, 10).replace(/-/g, '');
33 };
34
35 const getFileType = (file: File) => {
36 return file.name?.split('.')?.pop()?.toLowerCase();
37 };
38
39 const getHost = () => {
40 if (import.meta.env.VITE_OSS_HOST && import.meta.env.VITE_OSS_HOST.length !== 0) {
41 return import.meta.env.VITE_OSS_HOST;
42 }
43
44 return import.meta.env.VITE_OSS_ENDPOINT;
45 };
46
47 return {
48 ossClient,
49 async upload(
50 file: File,
51 prefix = 'file',
52 onProgress?: (percent: number) => void
53 ): Promise<{ url: string; name: string; response: unknown }> {
54 return createOssClient()
55 .multipartUpload(`${prefix}/${getFileDir()}/${getFileName()}.${getFileType(file)}`, file, {
56 progress: onProgress,
57 partSize: 5 * 1024 * 1024,
58 })
59 .then((res) => {
60 return Promise.resolve({ response: res.res, url: `${getHost()}/${res.name}`, name: `${getFileName()}.${getFileType(file)}` });
61 })
62 .catch(() => {
63 Message.error({ content: '上传失败' });
64 // eslint-disable-next-line prefer-promise-reject-errors
65 return Promise.reject(false);
66 });
67 },
68 };
69 }
1 import { ref } from 'vue';
2 import { Pagination } from '@/types/global';
3
4 interface Config {
5 pageSize?: number;
6 showTotal?: boolean;
7 showPageSize?: boolean;
8 size?: 'mini' | 'small' | 'medium' | 'large';
9 simple?: boolean;
10 hideOnSinglePage?: boolean;
11 }
12
13 export default function usePagination(config: Config = {}) {
14 const defaultPagination = {
15 total: 0,
16 current: 1,
17 pageSize: config.pageSize ?? 20,
18 pageSizeOptions: [config.pageSize ?? 20, (config.pageSize ?? 20) * 2, (config.pageSize ?? 20) * 3, (config.pageSize ?? 20) * 5],
19 showTotal: config.showTotal ?? true,
20 showPageSize: config.showPageSize ?? true,
21 size: config.size ?? 'medium',
22 simple: config.simple ?? false,
23 hideOnSinglePage: config.hideOnSinglePage ?? false,
24 };
25
26 const pagination = ref<Pagination>(defaultPagination);
27
28 // eslint-disable-next-line no-return-assign
29 const setPage = (page: number) => (pagination.value.current = page);
30 // eslint-disable-next-line no-return-assign
31 const setPageSize = (size: number) => (pagination.value.pageSize = size);
32 // eslint-disable-next-line no-return-assign
33 const setTotal = (total: number) => (pagination.value.total = total);
34 // eslint-disable-next-line no-return-assign
35 const resetPagination = () => (pagination.value = defaultPagination);
36
37 const incrementTotal = (increment = 1) => setTotal(pagination.value.total + increment);
38
39 const decrementTotal = (increment = 1) => setTotal(pagination.value.total - increment);
40
41 return {
42 pagination,
43 setPage,
44 setPageSize,
45 setTotal,
46 resetPagination,
47 incrementTotal,
48 decrementTotal,
49 };
50 }
1 import { RouteLocationNormalized, RouteRecordRaw } from 'vue-router';
2 import { useAuthorizedStore } from '@/store';
3
4 export default function usePermission() {
5 const userStore = useAuthorizedStore();
6
7 return {
8 accessRouter(route: RouteLocationNormalized | RouteRecordRaw) {
9 return (
10 !route.meta?.requiresAuth ||
11 !route.meta?.roles ||
12 route.meta?.roles?.includes('*') ||
13 userStore.permissions?.filter((item) => route.meta?.roles?.includes(item))?.length !== 0
14 );
15 },
16 checkPermission(binding: string[] | string): boolean {
17 const userStore = useAuthorizedStore();
18 const { permissions } = userStore;
19
20 if (Array.isArray(binding) && binding.length > 0) {
21 const hasPermission = binding.filter((item: string) => permissions.includes(item));
22 return hasPermission.length !== 0;
23 }
24 return permissions.includes(binding as string);
25 },
26 // You can add any rules you want
27 };
28 }
1 import { onMounted, onBeforeMount, onBeforeUnmount } from 'vue';
2 import { useDebounceFn } from '@vueuse/core';
3 import { useAppStore } from '@/store';
4 import { addEventListen, removeEventListen } from '@/utils/event';
5
6 const WIDTH = 992; // https://arco.design/vue/component/grid#responsivevalue
7
8 function queryDevice() {
9 const rect = document.body.getBoundingClientRect();
10 return rect.width - 1 < WIDTH;
11 }
12
13 export default function useResponsive(immediate?: boolean) {
14 const appStore = useAppStore();
15 function resizeHandler() {
16 if (!document.hidden) {
17 const isMobile = queryDevice();
18 appStore.toggleDevice(isMobile ? 'mobile' : 'desktop');
19 appStore.toggleMenu(isMobile);
20 }
21 }
22 const debounceFn = useDebounceFn(resizeHandler, 100);
23 onMounted(() => {
24 if (immediate) debounceFn();
25 });
26 onBeforeMount(() => {
27 addEventListen(window, 'resize', debounceFn);
28 });
29 onBeforeUnmount(() => {
30 removeEventListen(window, 'resize', debounceFn);
31 });
32 }
1 import { computed } from 'vue';
2 import { useAppStore } from '@/store';
3
4 export default function useThemes() {
5 const appStore = useAppStore();
6 const isDark = computed(() => {
7 return appStore.theme === 'dark';
8 });
9 return {
10 isDark,
11 };
12 }
1 import { createVNode, h, ref } from 'vue';
2 import { FormInstance } from '@arco-design/web-vue/es/form';
3 import { Form, FormItem, InputPassword, Message, Modal } from '@arco-design/web-vue';
4 import useAuthApi from '@/http/auth';
5
6 export default function useUser() {
7 const getRoleLabel = (role?: 'Singer' | 'Business') => {
8 switch (role) {
9 case 'Business':
10 return '推荐人';
11 case 'Singer':
12 return '歌手';
13 default:
14 return '未知';
15 }
16 };
17
18 const getSexLabel = (sex: string | number) => {
19 switch (sex) {
20 case 0:
21 return '保密';
22 case 1:
23 return '男';
24 case 2:
25 return '女';
26 default:
27 return '未知';
28 }
29 };
30
31 // eslint-disable-next-line no-shadow
32 const resetPwd = () => {
33 const pwdRef = ref<FormInstance>();
34 const pwdVal = ref({ password: '', password_confirmation: '' });
35 const pwdRules = {
36 password: [{ required: true, message: '请输入密码' }],
37 password_confirmation: [{ required: true, message: '请输入密码' }],
38 };
39
40 const createPwdVNode = (label: string, field: 'password' | 'password_confirmation') =>
41 createVNode(
42 FormItem,
43 { label, field, rowClass: field === 'password_confirmation' ? 'mb-0' : '' },
44 {
45 default: () =>
46 h(InputPassword, {
47 'modelValue': pwdVal.value[field],
48 // eslint-disable-next-line no-return-assign
49 'onUpdate:modelValue': (val?: string) => (pwdVal.value[field] = val || ''),
50 }),
51 }
52 );
53
54 return Modal.open({
55 title: '设置密码',
56 content: () =>
57 h(
58 Form,
59 { ref: pwdRef, model: pwdVal, rules: pwdRules, autoLabelWidth: true },
60 { default: () => [createPwdVNode('新密码', 'password'), createPwdVNode('确认密码', 'password_confirmation')] }
61 ),
62 closable: false,
63 maskClosable: false,
64 escToClose: false,
65 // eslint-disable-next-line no-shadow
66 onBeforeOk: (done: (closed: boolean) => void) => {
67 useAuthApi
68 .changePwd(pwdVal.value)
69 .then(() => {
70 Message.success('更新成功');
71 done(true);
72 })
73 .catch(() => {
74 done(false);
75 });
76 },
77 onClose: () => {
78 pwdVal.value = { password: '', password_confirmation: '' };
79 },
80 });
81 };
82
83 return {
84 getRoleLabel,
85 getSexLabel,
86 resetPwd,
87 };
88 }
1 import { AnyObject, Option, QueryForParams, ServiceResponse } from '@/types/global';
2
3 import { Tag } from '@/types/tag';
4 import axios from 'axios';
5
6 export default class useTagApi {
7 static typeOption = [
8 { label: '歌曲标签', value: 1 },
9 { label: '身份标签', value: 2 },
10 { label: '声音标签', value: 3 },
11 { label: '认证标签', value: 4 },
12 ];
13
14 static showOption = [
15 { label: '显示', value: 1 },
16 { label: '隐藏', value: 0 },
17 ];
18
19 static filterOption = [
20 { label: '允许', value: 1 },
21 { label: '禁止', value: 0 },
22 ];
23
24 static certifyPermissionOption = [
25 { label: '听', value: 1 },
26 { label: '唱', value: 2 },
27 ];
28
29 static async get(params?: QueryForParams): Promise<ServiceResponse<Tag[]>> {
30 return axios.get('system/tags', { params });
31 }
32
33 static async create(data: AnyObject): Promise<Tag> {
34 return axios.post('system/tags', data).then((res) => Promise.resolve(res.data));
35 }
36
37 static async show(id: number, params?: QueryForParams): Promise<Tag> {
38 return axios.get(`system/tags/${id}`, { params }).then((res) => Promise.resolve(res.data));
39 }
40
41 static async update(id: number, data: AnyObject): Promise<Tag> {
42 return axios.put(`system/tags/${id}`, data).then((res) => Promise.resolve(res.data));
43 }
44
45 static async destroy(id: number) {
46 return axios.delete(`system/tags/${id}`).then((res) => Promise.resolve(res.data));
47 }
48
49 static async getOption(params?: QueryForParams): Promise<Option[]> {
50 return useTagApi.get({ ...params, fetchType: 'all' }).then(({ data }) =>
51 data?.map((item) => {
52 return { value: item.id, label: item.name };
53 })
54 );
55 }
56 }
1 // eslint-disable-next-line max-classes-per-file
2 import { AnyObject, AttributeData, QueryForParams, ServiceResponse } from '@/types/global';
3 import { Activity, ActivityApplyRecord, ActivityViewUser } from '@/types/activity';
4 import useUserApi from '@/http/user';
5 import axios, { AxiosRequestConfig } from 'axios';
6 import { UserManageActivity } from '@/utils/model';
7 import { ActivityApply } from '@/types/activity-apply';
8 import FileSaver from 'file-saver';
9 import { ActivityWork } from '@/types/activity-work';
10
11 export default class useActivityApi {
12 static statusOption = [
13 { label: '处理中', value: 0 },
14 { label: '已上架', value: 1 },
15 { label: '已下架', value: 2 },
16 { label: '已匹配', value: 3 },
17 { label: '已发行', value: 5 },
18 { label: '处理失败', value: 4 },
19 ];
20
21 static weightOption = [
22 { label: '无', value: 0 },
23 { label: '低', value: 30 },
24 { label: '中', value: 60 },
25 { label: '高', value: 90 },
26 ];
27
28 static workSingTypeOption = [
29 { label: '自主上传', value: 1 },
30 { label: '唱整首', value: 2 },
31 { label: '唱片段', value: 3 },
32 ];
33
34 static workSingStatusOption = [
35 { label: '待采纳', value: 0 },
36 { label: '已确认', value: 1 },
37 { label: '不合适', value: 2 },
38 { label: '未采纳', value: 3 },
39 { label: '其他', value: 4 },
40 ];
41
42 static songTypeOption = [
43 { label: '歌曲', value: 1 },
44 { label: 'Demo', value: 2 },
45 ];
46
47 static get(params?: QueryForParams): Promise<ServiceResponse<Activity[]>> {
48 return axios.get('audition/activities', { params });
49 }
50
51 static async getExport(fileName: string, params?: QueryForParams) {
52 const config = { params: { ...params, fetchType: 'excel' }, timeout: 60000, responseType: 'blob' } as AxiosRequestConfig;
53 return axios.get('audition/activities', config).then(({ data }) => FileSaver.saveAs(data, `${fileName}.xlsx`));
54 }
55
56 static async create(data: AttributeData): Promise<Activity> {
57 return axios.post('audition/activities', data).then((res) => Promise.resolve(res.data));
58 }
59
60 static async update(id: number, data: AttributeData): Promise<Activity> {
61 return axios.put(`audition/activities/${id}`, data).then((res) => Promise.resolve(res.data));
62 }
63
64 static async changeStatus(id: number, data: AttributeData) {
65 return axios.put<Activity>(`audition/activities/${id}/change-status`, data);
66 }
67
68 static async notify(id: number, data: AttributeData) {
69 return axios.post<Activity>(`audition/activities/${id}/notify`, data);
70 }
71
72 static async show(id: number, params?: QueryForParams): Promise<Activity> {
73 return axios.get(`/audition/activities/${id}`, { params }).then((res) => Promise.resolve(res.data));
74 }
75
76 static async destroy(id: number) {
77 return axios.delete(`/audition/activities/${id}`).then((res) => Promise.resolve(res.data));
78 }
79
80 static async viewUsers(activityId: number, params?: QueryForParams): Promise<ServiceResponse<ActivityViewUser[]>> {
81 return axios.get(`/audition/activities/${activityId}/views`, { params });
82 }
83
84 static async likeUsers(activityId: number, params?: QueryForParams): Promise<ServiceResponse<ActivityViewUser[]>> {
85 return axios.get(`/audition/activities/${activityId}/likes`, { params });
86 }
87
88 static async manageUsers(activityId: number, params?: QueryForParams): Promise<ServiceResponse<UserManageActivity[]>> {
89 return axios.get(`/audition/activities/${activityId}/managers`, { params });
90 }
91
92 static async submitUser(activityId: number, params?: QueryForParams): Promise<ServiceResponse<ActivityWork[]>> {
93 return axios.get(`/audition/activities/${activityId}/submits`, { params });
94 }
95
96 static async getSubmitUserExport(activityId: number, fileName: string, params: QueryForParams) {
97 const config = { params: { ...params, fetchType: 'excel' }, timeout: 60000, responseType: 'blob' } as AxiosRequestConfig;
98 return axios.get(`/audition/activities/${activityId}/submits`, config).then(({ data }) => FileSaver.saveAs(data, `${fileName}.xlsx`));
99 }
100
101 static async changeUserShare(activityId: number, data: AttributeData) {
102 return axios.put<Activity>(`/audition/activities/${activityId}/change-user-share`, data);
103 }
104
105 static async matchUser(activityId: number, params?: QueryForParams): Promise<ServiceResponse<ActivityWork[]>> {
106 return axios.get(`/audition/activities/${activityId}/matches`, { params });
107 }
108 }
109
110 export class useApplyApi {
111 static statusOption = [
112 { label: '审核不通过', value: 2 },
113 { label: '待审核', value: 0 },
114 ];
115
116 static async get(params?: QueryForParams): Promise<ServiceResponse<ActivityApply[]>> {
117 return axios.get('audition/applies', { params });
118 }
119
120 static async create(data: AnyObject): Promise<ActivityApply> {
121 return axios.post(`audition/applies`, data).then((res) => Promise.resolve(res.data));
122 }
123
124 static async audit(id: number, data: AttributeData) {
125 return axios.put(`audition/applies/${id}/audit`, data);
126 }
127
128 static async update(id: number, data: AttributeData) {
129 return axios.put(`audition/applies/${id}`, data);
130 }
131
132 static async record(id: number, params?: QueryForParams): Promise<ServiceResponse<ActivityApplyRecord[]>> {
133 return axios.get(`audition/applies/${id}/records`, { params });
134 }
135
136 static async destroy(id: number) {
137 return axios.delete(`audition/applies/${id}`);
138 }
139 }
140
141 export class useManagerApi {
142 static statusOption = useUserApi.statusOption;
143
144 static sexOption = useUserApi.sexOption;
145
146 static officialStatusOption = useUserApi.officialStatusOption;
147
148 static permissionOption = [
149 { label: '查看试唱', value: 'view' },
150 { label: '查看报价', value: 'price' },
151 { label: '回复结果', value: 'audit' },
152 ];
153
154 static get(params: QueryForParams): Promise<ServiceResponse<UserManageActivity[]>> {
155 return axios.get('audition/activity-managers', { params });
156 }
157
158 static async create(data: AnyObject) {
159 return axios.post('audition/activity-managers', data).then((res) => Promise.resolve(res.data));
160 }
161
162 static async update(id: number, data: AnyObject) {
163 return axios.put(`audition/activity-managers/${id}`, data).then((res) => Promise.resolve(res.data));
164 }
165
166 static async destroy(id: number) {
167 return axios.delete(`audition/activity-managers/${id}`).then((res) => Promise.resolve(res.data));
168 }
169 }
170
171 export class useReListApi {
172 static get(params?: QueryForParams): Promise<ServiceResponse> {
173 return axios.get(`audition/activity-relist-records`, { params });
174 }
175
176 static async getExport(fileName: string, params?: QueryForParams) {
177 const config = { params: { ...params, fetchType: 'excel' }, timeout: 60000, responseType: 'blob' } as AxiosRequestConfig;
178 return axios.get('audition/activity-relist-records', config).then(({ data }) => FileSaver.saveAs(data, `${fileName}.xlsx`));
179 }
180 }
181
182 export class useWorkApi {
183 static get(params?: QueryForParams): Promise<ServiceResponse<ActivityWork[]>> {
184 return axios.put(`audition/activity-works`, { params });
185 }
186
187 static changeStatus(workId: number, data: AttributeData) {
188 return axios.put(`audition/activity-works/${workId}/change-status`, data);
189 }
190 }
1 import { AuthorizedState } from '@/store/modules/authorized/type';
2 import FileSaver from 'file-saver';
3 import axios from 'axios';
4
5 interface LoginData {
6 type: 'email' | 'phone';
7 email?: string;
8 password?: string;
9 phone?: string;
10 code?: string;
11 area?: string;
12 }
13
14 export async function downloadFile(url: string, fileName: string) {
15 FileSaver.saveAs(`${url}`, fileName + url.substring(url.lastIndexOf('.')));
16
17 // ?response-content-type=Blob
18 }
19
20 export default class useAuthApi {
21 static async login(data: LoginData) {
22 return axios
23 .post<{ access_token: string; refresh_token: string; nick_name: string }>('login', data)
24 .then((res) => Promise.resolve(res.data));
25 }
26
27 static async info() {
28 return axios.get<{ user: AuthorizedState; permissions: string[] }>('auth/info').then((res) => Promise.resolve(res.data));
29 }
30
31 static changePwd(data: { password: string; password_confirmation: string }) {
32 return axios.put('auth/change-pwd', data);
33 }
34 }
1 import { AnyObject, QueryForParams, ServiceResponse } from '@/types/global';
2 import { Banner } from '@/utils/model';
3 import axios from 'axios';
4
5 export default class useBannerApi {
6 static roleOption = [
7 { value: 'UnLogin', label: '未登录' },
8 { value: 'Visitor', label: '游客' },
9 { value: 'Singer', label: '音乐人' },
10 { value: 'Business', label: '经纪人' },
11 { value: 'Project', label: '厂牌管理员' },
12 { value: 'System', label: '平台管理员' },
13 ];
14
15 static statusOption = [
16 { value: 0, label: '下架' },
17 { value: 1, label: '上架' },
18 ];
19
20 static scopeOption = [{ value: 1, label: '首页banner' }];
21
22 static typeOption = [
23 { value: 1, label: '普通' },
24 { value: 2, label: '外链' },
25 { value: 3, label: '交互' },
26 { value: 4, label: '自定义H5' },
27 ];
28
29 static get(params?: QueryForParams): Promise<ServiceResponse<Banner[]>> {
30 return axios.get('system/banners', { params });
31 }
32
33 static async create(data: AnyObject): Promise<Banner> {
34 return axios.post('system/banners', data).then((res) => Promise.resolve(res.data));
35 }
36
37 static async update(id: number, data: AnyObject): Promise<Banner> {
38 return axios.put(`system/banners/${id}`, data).then((res) => Promise.resolve(res.data));
39 }
40
41 static async changeStatus(id: number, data: { status: number }) {
42 return axios.put(`system/banners/${id}/change-status`, data);
43 }
44
45 static async destroy(id: number) {
46 return axios.delete(`system/banners/${id}`).then((res) => Promise.resolve(res.data));
47 }
48 }
1 import { AnyObject, QueryForPaginationParams, QueryForParams, ServiceResponse } from '@/types/global';
2 import { User } from '@/utils/model';
3 import axios, { AxiosRequestConfig } from 'axios';
4 import FileSaver from 'file-saver';
5
6 export default class useBrokerApi {}
7
8 export interface BrokerUserConfigItem {
9 user_id: number;
10 user?: User;
11 identifier: '';
12 singer_count: 0;
13 status: 0 | 1;
14 }
15
16 export interface BrokerUserConfig {
17 id: number;
18 title: string;
19 begin_at: string;
20 end_at: string;
21 push_type: number;
22 push_at: string;
23 user_id: number;
24 user?: User;
25 items?: BrokerUserConfigItem[];
26 }
27
28 export class useUserConfigApi {
29 static get(params?: QueryForParams | QueryForPaginationParams): Promise<ServiceResponse<BrokerUserConfig[]>> {
30 return axios.get('system/broker/user-configs', { params });
31 }
32
33 static async create(data: AnyObject): Promise<BrokerUserConfig> {
34 return axios.post('system/broker/user-configs', data).then((res) => Promise.resolve(res.data));
35 }
36
37 static async createLevel(id: number, data: AnyObject) {
38 return axios.post(`system/broker/user-configs/${id}/level`, data).then((res) => Promise.resolve(res.data));
39 }
40
41 static async show(id: number): Promise<BrokerUserConfig> {
42 return axios.get(`system/broker/user-configs/${id}`).then((res) => Promise.resolve(res.data));
43 }
44
45 static async update(id: number, data: AnyObject): Promise<BrokerUserConfig> {
46 return axios.put(`system/broker/user-configs/${id}`, data).then((res) => Promise.resolve(res.data));
47 }
48
49 static async destroy(id: number) {
50 return axios.delete(`system/broker/user-configs/${id}`).then((res) => Promise.resolve(res.data));
51 }
52 }
53
54 export interface BrokerPushConfig {
55 id: number;
56 identifier: string;
57 title: string;
58 intro: string;
59 match_title: string;
60 match_intro: string;
61 user_id: number;
62 user?: User;
63 }
64
65 export class usePushConfigApi {
66 static get(params?: QueryForParams | QueryForPaginationParams): Promise<ServiceResponse<BrokerPushConfig[]>> {
67 return axios.get('system/broker/push-configs', { params });
68 }
69
70 static async create(data: AnyObject): Promise<BrokerPushConfig> {
71 return axios.post('system/broker/push-configs', data).then((res) => Promise.resolve(res.data));
72 }
73
74 static async update(id: number, data: AnyObject): Promise<BrokerPushConfig> {
75 return axios.put(`system/broker/push-configs/${id}`, data).then((res) => Promise.resolve(res.data));
76 }
77
78 static async destroy(id: number) {
79 return axios.delete(`system/broker/push-configs/${id}`).then((res) => Promise.resolve(res.data));
80 }
81 }
82
83 export class usePushMatchRecordApi {
84 static get(params?: QueryForParams | QueryForPaginationParams): Promise<ServiceResponse> {
85 return axios.get('system/broker/push-match-records', { params });
86 }
87
88 static async getExport(fileName: string, params?: QueryForParams) {
89 const config = { params: { ...params, fetchType: 'excel' }, timeout: 60000, responseType: 'blob' } as AxiosRequestConfig;
90 return axios.get('system/broker/push-match-records', config).then(({ data }) => FileSaver.saveAs(data, `${fileName}.xlsx`));
91 }
92
93 static async rollback(recordId: number) {
94 return axios.post(`system/broker/push-match-records/${recordId}/rollback`);
95 }
96
97 static async send(recordId: number) {
98 return axios.post(`system/broker/push-match-records/${recordId}/send`);
99 }
100 }
101
102 export interface BrokerPushLevel {
103 id: number;
104 user_id: number;
105 title: string;
106 is_alert: 1 | 0;
107 begin_at: string;
108 end_at: string;
109 publish_at: string;
110 status: number;
111 }
112
113 export class usePushLevelRecordApi {
114 static statusOption = [
115 { value: 0, label: '待发送' },
116 { value: 1, label: '处理中' },
117 { value: 2, label: '已发送' },
118 { value: 3, label: '已撤销' },
119 { value: -1, label: '发送失败' },
120 ];
121
122 static async get(params?: QueryForParams | QueryForPaginationParams): Promise<ServiceResponse<BrokerPushLevel[]>> {
123 return axios.get('system/broker/push-level-records', { params });
124 }
125
126 static async update(id: number, data: AnyObject): Promise<BrokerPushLevel> {
127 return axios.put(`system/broker/push-level-records/${id}`, data).then((res) => Promise.resolve(res.data));
128 }
129
130 static async send(id: number) {
131 return axios.post(`system/broker/push-level-records/${id}/send`).then((res) => Promise.resolve(res.data));
132 }
133
134 static async rollback(id: number) {
135 return axios.post(`system/broker/push-level-records/${id}/rollback`).then((res) => Promise.resolve(res.data));
136 }
137
138 static async destroy(id: number) {
139 return axios.delete(`system/broker/push-level-records/${id}`).then((res) => Promise.resolve(res.data));
140 }
141
142 static getChildren(id: number, params?: QueryForParams | QueryForPaginationParams) {
143 return axios.get(`system/broker/push-level-records/${id}/children`, { params });
144 }
145 }
1 import { AttributeData, Option, QueryForPaginationParams, QueryForParams, ServiceResponse } from '@/types/global';
2 import { SystemConfig } from '@/types/system-config';
3 import axios from 'axios';
4 import { first } from 'lodash';
5
6 export default class useConfigApi {
7 static statusOptions = [
8 { label: '启用', value: 1 },
9 { label: '禁用', value: 0 },
10 ];
11
12 static contentOptions = [
13 { label: '无内容', value: 'none' },
14 {
15 label: '文本内容',
16 value: 'string',
17 children: [
18 { label: '短文本', value: 'input' },
19 { label: '长文本', value: 'textarea' },
20 ],
21 },
22 {
23 label: '上传链接',
24 value: 'upload',
25 children: [
26 { label: '图片链接', value: 'image' },
27 { label: '视频链接', value: 'video' },
28 { label: '音频链接', value: 'audio' },
29 { label: '文件链接', value: 'file' },
30 ],
31 },
32 ];
33
34 static get(params?: QueryForParams | QueryForPaginationParams): Promise<ServiceResponse<SystemConfig[]>> {
35 return axios.get('system/configs', { params });
36 }
37
38 static async create(data: AttributeData) {
39 return axios.post('system/configs', data).then((res) => Promise.resolve(res.data));
40 }
41
42 static async update(id: number, data: AttributeData) {
43 return axios.put(`system/configs/${id}`, data).then((res) => Promise.resolve(res.data));
44 }
45
46 static async changeStatus(id: number, status: number) {
47 return axios.put(`system/configs/${id}/change-status`, { status });
48 }
49
50 static async getOne(params?: QueryForParams): Promise<SystemConfig | undefined> {
51 return useConfigApi.get({ ...params, parent_id: '', fetchType: 'all' }).then(({ data }) => first(data));
52 }
53
54 static async getOption(params?: QueryForParams): Promise<Option[]> {
55 return useConfigApi.get({ parent_id: '', ...params, fetchType: 'all' }).then(({ data }) =>
56 data?.map((item) => {
57 return {
58 value: item.identifier,
59 label: item.name,
60 };
61 })
62 );
63 }
64 }
1 import { QueryForParams, ServiceResponse } from '@/types/global';
2 import axios, { AxiosRequestConfig } from 'axios';
3 import FileSaver from 'file-saver';
4
5 export default class useDashboardApi {
6 static userCertify() {
7 return axios.get('dashboard/user-certify').then((res) => Promise.resolve(res.data));
8 }
9
10 static userTotal() {
11 return axios.get('dashboard/user-total').then((res) => Promise.resolve(res.data));
12 }
13
14 static userSkill() {
15 return axios.get('dashboard/user-skill').then((res) => Promise.resolve(res.data));
16 }
17
18 static userStyle() {
19 return axios.get('dashboard/user-style').then((res) => Promise.resolve(res.data));
20 }
21
22 static activityTotal() {
23 return axios.get('dashboard/activity-total').then((res) => Promise.resolve(res.data));
24 }
25
26 static activityStyle() {
27 return axios.get('dashboard/activity-style').then((res) => Promise.resolve(res.data));
28 }
29
30 static activityLike() {
31 return axios.get('dashboard/activity-like').then((res) => Promise.resolve(res.data));
32 }
33
34 static activityListen() {
35 return axios.get('dashboard/activity-listen').then((res) => Promise.resolve(res.data));
36 }
37
38 static overview(params?: QueryForParams) {
39 return axios.get('dashboard/overview', { params }).then((res) => Promise.resolve(res.data));
40 }
41
42 static getOverviewExport(fileName: string, params?: QueryForParams) {
43 const config = { params: { ...params, fetchType: 'excel' }, timeout: 60000, responseType: 'blob' } as AxiosRequestConfig;
44 return axios.get('dashboard/overview', config).then(({ data }) => FileSaver.saveAs(data, `${fileName}.xlsx`));
45 }
46
47 static submitWork(params?: QueryForParams): Promise<ServiceResponse> {
48 return axios.get('dashboard/submit-work', { params });
49 }
50
51 static getSubmitWorkExport(fileName: string, params?: QueryForParams) {
52 const config = { params: { ...params, fetchType: 'excel' }, timeout: 60000, responseType: 'blob' } as AxiosRequestConfig;
53 return axios.get('dashboard/submit-work', config).then(({ data }) => FileSaver.saveAs(data, `${fileName}.xlsx`));
54 }
55 }
1 import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
2 import { Message } from '@arco-design/web-vue';
3 import { clearToken, getRefreshToken, getToken } from '@/utils/auth';
4 import { ServiceResponse } from '@/types/global';
5
6 const isRefreshing = false;
7
8 if (import.meta.env.VITE_API_HOST) {
9 axios.defaults.baseURL = import.meta.env.VITE_API_HOST;
10 axios.defaults.timeout = 30000;
11 }
12
13 axios.interceptors.request.use(
14 // @ts-ignore
15 (config: AxiosRequestConfig) => {
16 if (!config.url?.startsWith('/provider')) {
17 config.baseURL = `${config.baseURL}/admin`;
18 }
19 const token = isRefreshing ? getRefreshToken() : getToken();
20 if (token) {
21 config.headers = {
22 Accept: 'application/json',
23 ...config.headers,
24 Authorization: `Bearer ${token}`,
25 Route: sessionStorage.getItem('route') || 'dashboard',
26 };
27 }
28 if (config.params) {
29 Object.keys(config.params).forEach((key) => {
30 if (config.params[key] === undefined) {
31 delete config.params[key];
32 }
33 });
34 }
35
36 return config;
37 },
38 (error) => {
39 return Promise.reject(error);
40 }
41 );
42
43 axios.interceptors.response.use(
44 // @ts-ignore
45 async (response: AxiosResponse<ServiceResponse>): Promise<ServiceResponse | unknown> => {
46 if (response.data instanceof Blob) {
47 return Promise.resolve(response);
48 }
49
50 return Promise.resolve(response.data);
51 },
52 async (error) => {
53 if (error.response.status === 401) {
54 clearToken();
55 window.location.reload();
56 Message.warning({ id: 'token_lose', content: '登陆信息已失效,请重新登陆...' });
57 } else {
58 Message.error({
59 id: 'service_error',
60 content: error.response.data.msg || error.msg || 'Request Error',
61 duration: 5 * 1000,
62 });
63 }
64
65 return Promise.reject(error);
66 }
67 );
1 import { QueryForParams, ServiceResponse } from '@/types/global';
2 import { SystemRole } from '@/types/system-role';
3 import axios from 'axios';
4
5 export default class useOperationLog {
6 static async get(params?: QueryForParams): Promise<ServiceResponse<SystemRole[]>> {
7 return axios.get('system/operation-logs', { params });
8 }
9 }
1 import { AnyObject, QueryForParams, ServiceResponse } from '@/types/global';
2 import { Banner } from '@/utils/model';
3 import axios from 'axios';
4
5 export default class useMaterialApi {
6 static typeOption = [
7 { value: 1, label: '用户主页背景' },
8 { value: 2, label: '厂牌主页背景' },
9 { value: 3, label: '音视频封面' },
10 ];
11
12 static get(params?: QueryForParams): Promise<ServiceResponse> {
13 return axios.get('system/materials', { params });
14 }
15
16 static async create(data: AnyObject) {
17 return axios.post(`system/materials`, data).then((res) => Promise.resolve(res.data));
18 }
19
20 static async update(id: number, data: AnyObject): Promise<Banner> {
21 return axios.put(`system/materials/${id}`, data).then((res) => Promise.resolve(res.data));
22 }
23
24 static async destroy(id: number) {
25 return axios.delete(`system/materials/${id}`).then((res) => Promise.resolve(res.data));
26 }
27 }
1 import { AttributeData, QueryForParams, ServiceResponse } from '@/types/global';
2
3 // eslint-disable-next-line import/no-cycle
4 import { Notification } from '@/utils/model';
5 import axios from 'axios';
6
7 export default class useNotificationApi {
8 static typeOption = [
9 { value: 1, label: '文本' },
10 { value: 2, label: '图文' },
11 ];
12
13 static alertOption = [
14 { value: 1, label: '弹出' },
15 { value: 0, label: '不弹出' },
16 ];
17
18 static statusOption = [
19 { value: 0, label: '待发送' },
20 { value: 1, label: '处理中' },
21 { value: 2, label: '已发送' },
22 { value: -1, label: '发送失败' },
23 { value: -2, label: '取消发送' },
24 { value: 3, label: '已撤销' },
25 ];
26
27 static publishTypeOption = [
28 { value: 1, label: '立即发送' },
29 { value: 2, label: '定时发送' },
30 ];
31
32 static linkTypeOption = [
33 { value: 'none', label: '无' },
34 { value: 'user', label: '用户' },
35 { value: 'activity', label: '歌曲' },
36 { value: 'project', label: '厂牌' },
37 { value: 'rich', label: '自定义H5页面' },
38 ];
39
40 static get(params?: QueryForParams): Promise<ServiceResponse<Notification[]>> {
41 return axios.get('system/notification', { params });
42 }
43
44 static async create(data?: AttributeData): Promise<Notification> {
45 return axios.post('system/notification', data).then((res) => Promise.resolve(res.data));
46 }
47
48 static async show(id: number): Promise<Notification> {
49 return axios.get(`system/notification/${id}`).then((res) => Promise.resolve(res.data));
50 }
51
52 static async update(id: number, data = {}): Promise<Notification> {
53 return axios.put(`system/notification/${id}`, data).then((res) => Promise.resolve(res.data));
54 }
55
56 static async user(id: number, params?: QueryForParams) {
57 return axios.get(`system/notification/${id}/users`, { params });
58 }
59
60 static async send(id: number): Promise<unknown> {
61 return axios.put(`system/notification/${id}/send`).then((res) => Promise.resolve(res.data));
62 }
63
64 static async cancel(id: number): Promise<unknown> {
65 return axios.put(`system/notification/${id}/cancel`).then((res) => Promise.resolve(res.data));
66 }
67
68 static async rollback(id: number): Promise<unknown> {
69 return axios.put(`system/notification/${id}/rollback`).then((res) => Promise.resolve(res.data));
70 }
71 }
1 import { AttributeData, QueryForParams, ServiceResponse } from '@/types/global';
2
3 import axios, { AxiosRequestConfig } from 'axios';
4 import FileSaver from 'file-saver';
5 import { Project, User, UserDynamics } from '@/utils/model';
6
7 type ProjectList = Project & {
8 activity_count: number;
9 activity_up_count: number;
10 activity_match_count: number;
11 activity_down_count: number;
12 activity_send_count: number;
13 manage_count: number;
14 };
15
16 export default class useProjectApi {
17 static promoteStatusOption = [
18 { label: '否', value: 0 },
19 { label: '是', value: 1 },
20 ];
21
22 static publicStatusOption = [
23 { label: '否', value: 1 },
24 { label: '是', value: 0 },
25 ];
26
27 static applyStatusOption = [
28 { label: '否', value: 0 },
29 { label: '是', value: 1 },
30 ];
31
32 static manageStatusOption = [
33 { label: '否', value: 0 },
34 { label: '是', value: 1 },
35 ];
36
37 static statusOption = [
38 { label: '启用', value: 1 },
39 { label: '禁用', value: 0 },
40 ];
41
42 static get(params?: QueryForParams): Promise<ServiceResponse<ProjectList[]>> {
43 return axios.get('audition/projects', { params });
44 }
45
46 static async getExport(fileName: string, params?: QueryForParams) {
47 const config = { params: { ...params, fetchType: 'excel' }, timeout: 60000, responseType: 'blob' } as AxiosRequestConfig;
48 return axios.get('audition/projects', config).then(({ data }) => FileSaver.saveAs(data, `${fileName}.xlsx`));
49 }
50
51 static async create(data: Omit<Project, 'id'>): Promise<Project> {
52 return axios.post('audition/projects', data).then((res) => Promise.resolve(res.data));
53 }
54
55 static async show(id: number): Promise<Project> {
56 return axios.get(`/audition/projects/${id}`).then((res) => Promise.resolve(res.data));
57 }
58
59 static async update(id: number, data: AttributeData): Promise<Project> {
60 return axios.put(`audition/projects/${id}`, data).then((res) => Promise.resolve(res.data));
61 }
62
63 static async changeStatus(id: number, status: 1 | 0) {
64 return axios.put(`audition/projects/${id}/change-status`, { status });
65 }
66
67 static destroy(id: number) {
68 return axios.delete(`audition/projects/${id}`);
69 }
70
71 static manageUsers(projectId: number, params?: QueryForParams): Promise<ServiceResponse<User[]>> {
72 return axios.get(`/audition/projects/${projectId}/managers`, { params });
73 }
74
75 static memberUsers(projectId: number, params?: QueryForParams): Promise<ServiceResponse<User[]>> {
76 return axios.get(`/audition/projects/${projectId}/members`, { params });
77 }
78
79 static outManageUsers(projectId: number, params?: QueryForParams): Promise<ServiceResponse<User[]>> {
80 return axios.get(`/audition/projects/${projectId}/out-managers`, { params });
81 }
82
83 static destroyOutManageUsers(projectId: number, data = {}) {
84 return axios.post(`/audition/projects/${projectId}/out-managers`, data);
85 }
86
87 static dynamics(id: number, params?: QueryForParams): Promise<ServiceResponse<UserDynamics[]>> {
88 return axios.get(`/audition/projects/${id}/dynamics`, { params });
89 }
90 }
91
92 export class useManagerApi {
93 static async create(data: { user_id: number; project_id: number }): Promise<Project> {
94 return axios.post('audition/project-managers', data).then((res) => Promise.resolve(res.data));
95 }
96
97 static destroy(id: number) {
98 return axios.delete(`audition/project-managers/${id}`);
99 }
100 }
1 import axios from 'axios';
2
3 type LoginData = { access_token: string; refresh_token: string; nick_name: string };
4
5 export default class useProviderApi {
6 static async area() {
7 return axios.get('/provider/area');
8 }
9
10 static sms(type: string, phone: string, area?: string) {
11 return axios.post('/provider/sms', { type, phone, area, platform: 'admin', scope: 1 });
12 }
13
14 static async login(type: 'phone' | 'email', data: object) {
15 return axios.post<LoginData>('/provider/login', { platform: 'admin', scope: 1, type, ...data });
16 }
17 }
1 import { AnyObject, QueryForParams } from '@/types/global';
2
3 import { Tag } from '@/types/tag';
4 import axios from 'axios';
5
6 export default class useReportApi {
7 static typeOption = [{ value: 3, label: '聊天' }];
8
9 static statusOption = [
10 { value: 0, label: '未处理' },
11 { value: 1, label: '已处理' },
12 ];
13
14 static async get(params?: QueryForParams) {
15 return axios.get('system/reports', { params });
16 }
17
18 static async update(id: number, data: AnyObject): Promise<Tag> {
19 return axios.put(`system/reports/${id}`, data).then((res) => Promise.resolve(res.data));
20 }
21 }
1 import { AnyObject, QueryForParams, ServiceResponse } from '@/types/global';
2
3 import { SystemRole } from '@/types/system-role';
4 import { SystemPermission } from '@/types/system-permission';
5 import axios from 'axios';
6
7 export default class useRoleApi {
8 static statusOption = [
9 { value: 1, label: '启用' },
10 { value: 0, label: '禁用' },
11 ];
12
13 static async get(params?: QueryForParams): Promise<ServiceResponse<SystemRole[]>> {
14 return axios.get('system/roles', { params });
15 }
16
17 static async create(data: AnyObject): Promise<SystemRole> {
18 return axios.post('system/roles', data).then((res) => Promise.resolve(res.data));
19 }
20
21 static async update(id: number, data: AnyObject): Promise<SystemRole> {
22 return axios.put(`system/roles/${id}`, data).then((res) => Promise.resolve(res.data));
23 }
24
25 static async changeStatus(id: number, status: number) {
26 return axios.put(`system/roles/${id}/change-status`, { status });
27 }
28
29 static async changePermission(id: number, permission: number[]) {
30 return axios.put(`system/roles/${id}/change-permission`, { permission });
31 }
32
33 static async destroy(id: number) {
34 return axios.delete(`system/roles/${id}`).then((res) => Promise.resolve(res.data));
35 }
36 }
37
38 export class usePermissionApi {
39 static async get(params?: QueryForParams): Promise<ServiceResponse<SystemPermission[]>> {
40 return axios.get('system/permissions', { params });
41 }
42 }
1 import { AnyObject, QueryForParams, ServiceResponse } from '@/types/global';
2
3 import { Tag } from '@/types/star-tags';
4 import axios from 'axios';
5
6 export default class useTagApi {
7 static frameOption = [
8 { label: '需要边框', value: 1 },
9 { label: '不需要边框', value: 2 },
10 ];
11
12 static indexFilterOption = [
13 { label: '首页可筛选', value: 1 },
14 { label: '首页不可筛选', value: 0 },
15 ];
16
17 static statusOption = [
18 { label: '开启', value: 1 },
19 { label: '禁用', value: 0 },
20 ];
21
22 static async get(params?: QueryForParams): Promise<ServiceResponse<Tag[]>> {
23 return axios.get('system/user_tags', { params });
24 }
25
26 static async create(data: AnyObject): Promise<Tag> {
27 return axios.post('system/user_tags', data).then((res) => Promise.resolve(res.data));
28 }
29
30 static async update(id: number, data: AnyObject): Promise<Tag> {
31 return axios.put(`system/user_tags/${id}`, data).then((res) => Promise.resolve(res.data));
32 }
33
34 static async destroy(id: number) {
35 return axios.delete(`system/user_tags/${id}`).then((res) => Promise.resolve(res.data));
36 }
37 }
1 import { AttributeData, QueryForParams, ServiceResponse } from '@/types/global';
2
3 import { Activity } from '@/types/activity';
4 import axios, { AxiosRequestConfig } from 'axios';
5 import { User, UserCertify, UserDynamics, UserManageActivity, UserMember, UserSubmitActivity } from '@/utils/model';
6 import FileSaver from 'file-saver';
7
8 export default class useUserApi {
9 static statusOption = [
10 { label: '启用', value: 1 },
11 { label: '禁用', value: 0 },
12 { label: '注销', value: 2 },
13 ];
14
15 static officialStatusOption = [
16 { label: '已关注', value: 1 },
17 { label: '未关注', value: 0 },
18 ];
19
20 static sexOption = [
21 { label: '男', value: 1 },
22 { label: '女', value: 2 },
23 { label: '无', value: 0 },
24 ];
25
26 static scopeOption = [
27 { label: '无权限', value: 0 },
28 { label: '平台管理员', value: 1 },
29 { label: '厂牌管理员', value: 2 },
30 ];
31
32 static get(params?: QueryForParams, config = {}): Promise<ServiceResponse<User[]>> {
33 return axios.get(`user`, { params, ...config });
34 }
35
36 static async show(id: number, params?: QueryForParams): Promise<User> {
37 return axios.get(`user/${id}`, { params }).then((res) => Promise.resolve(res.data));
38 }
39
40 static async update(id: number, data: AttributeData): Promise<User> {
41 return axios.put(`user/${id}`, data).then((res) => Promise.resolve(res.data));
42 }
43
44 static destroy(id: number) {
45 return axios.delete(`user/${id}`);
46 }
47
48 static changeStatus(id: number, status: 1 | 0) {
49 return axios.put(`user/${id}/change-status`, { status });
50 }
51
52 static changePwd(id: number, data: { password: string; password_confirmation: string }) {
53 return axios.put(`user/${id}/change-pwd`, data);
54 }
55
56 static listenSongs(id: number, params?: QueryForParams): Promise<ServiceResponse<Activity[]>> {
57 return axios.get(`user/${id}/listen-songs`, { params });
58 }
59
60 static likeSongs(id: number, params?: QueryForParams): Promise<ServiceResponse<Activity[]>> {
61 return axios.get(`user/${id}/like-songs`, { params });
62 }
63
64 static manageSongs(id: number, params?: QueryForParams): Promise<ServiceResponse<UserManageActivity[]>> {
65 return axios.get(`user/${id}/manage-songs`, { params });
66 }
67
68 static submitSongs(id: number, params?: QueryForParams): Promise<ServiceResponse<UserSubmitActivity[]>> {
69 return axios.get(`user/${id}/submit-songs`, { params });
70 }
71
72 static singers(id: number, params?: QueryForParams): Promise<ServiceResponse<User[]>> {
73 return axios.get(`user/${id}/singers`, { params });
74 }
75
76 static dynamics(id: number, params?: QueryForParams): Promise<ServiceResponse<UserDynamics[]>> {
77 return axios.get(`user/${id}/dynamics`, { params });
78 }
79 }
80
81 export class useCertifyApi {
82 static sexOption = useUserApi.sexOption;
83
84 static statusOption = [
85 { label: '待审核', value: 0 },
86 { label: '通过', value: 1 },
87 { label: '拒绝', value: 2 },
88 ];
89
90 // eslint-disable-next-line class-methods-use-this
91 static get(params?: QueryForParams): Promise<ServiceResponse<UserCertify[]>> {
92 return axios.get('user/certifies', { params });
93 }
94
95 static async update(id: number, data: { status: number; reason: string }): Promise<UserCertify> {
96 return axios.put(`user/certifies/${id}`, data).then((res) => Promise.resolve(res.data));
97 }
98 }
99
100 export class useRegisterApi extends useUserApi {
101 static get(params?: QueryForParams): Promise<ServiceResponse<User[]>> {
102 return axios.get('user/registers', { params });
103 }
104
105 static async getExport(fileName: string, params?: QueryForParams) {
106 const config = { params: { ...params, fetchType: 'excel' }, timeout: 60000, responseType: 'blob' } as AxiosRequestConfig;
107 return axios.get('user/registers', config).then(({ data }) => FileSaver.saveAs(data, `${fileName}.xlsx`));
108 }
109 }
110
111 export class useSingerApi extends useUserApi {
112 static get(params?: QueryForParams): Promise<ServiceResponse<User[]>> {
113 return axios.get('user/singers', { params });
114 }
115
116 static async getExport(fileName: string, params?: QueryForParams) {
117 const config = { params: { ...params, fetchType: 'excel' }, timeout: 60000, responseType: 'blob' } as AxiosRequestConfig;
118 return axios.get('user/singers', config).then(({ data }) => FileSaver.saveAs(data, `${fileName}.xlsx`));
119 }
120 }
121
122 export class useBusinessApi extends useUserApi {
123 static get(params?: QueryForParams): Promise<ServiceResponse<User[]>> {
124 return axios.get('user/business', { params });
125 }
126
127 static async getExport(fileName: string, params?: QueryForParams) {
128 const config = { params: { ...params, fetchType: 'excel' }, timeout: 60000, responseType: 'blob' } as AxiosRequestConfig;
129 return axios.get('user/business', config).then(({ data }) => FileSaver.saveAs(data, `${fileName}.xlsx`));
130 }
131 }
132
133 export class useMemberApi {
134 static get(userId: number, params?: QueryForParams): Promise<ServiceResponse<UserMember[]>> {
135 return axios.get(`user/${userId}/members`, { params });
136 }
137
138 static destroy(userId: number) {
139 return axios.delete(`user/${userId}/members`);
140 }
141 }
1 import { AttributeData, QueryForParams, ServiceResponse } from '@/types/global';
2
3 import { AppVersion } from '@/types/app-version';
4 import axios from 'axios';
5
6 export default class useVersionApi {
7 static systemOption = [
8 { label: 'Android', value: 'android' },
9 { label: 'IOS', value: 'ios' },
10 ];
11
12 static forceOption = [
13 { label: '是', value: 1 },
14 { label: '否', value: 0 },
15 ];
16
17 static get(params?: QueryForParams): Promise<ServiceResponse<AppVersion[]>> {
18 return axios.get('system/versions', { params });
19 }
20
21 static async create(data: AttributeData) {
22 return axios.post('system/versions', data).then((res) => Promise.resolve(res.data));
23 }
24
25 static async update(id: number, data: AttributeData) {
26 return axios.put(`system/versions/${id}`, data).then((res) => Promise.resolve(res.data));
27 }
28
29 static async destroy(id: number) {
30 return axios.delete(`system/versions/${id}`).then((res) => Promise.resolve(res.data));
31 }
32 }
1 <script lang="tsx">
2 import { compile, computed, defineComponent, h, ref, watch } from 'vue';
3
4 import { RouteRecordRaw, useRoute, useRouter } from 'vue-router';
5 import { useAppStore, useAuthorizedStore } from '@/store';
6 import usePermission from '@/hooks/permission';
7 import { last, orderBy } from 'lodash';
8 import { storeToRefs } from 'pinia';
9 import { useIntervalFn } from '@vueuse/core';
10
11 export default defineComponent({
12 emit: ['collapse'],
13 setup() {
14 const appStore = useAppStore();
15 const permission = usePermission();
16 const authorizedStore = useAuthorizedStore();
17 const router = useRouter();
18 const route = useRoute();
19
20 const { appMenu } = storeToRefs(appStore);
21 const { auditUserCount, auditActivityCount, activityApplyFailCount, permissions } = storeToRefs(authorizedStore);
22 const collapsed = ref(false);
23
24 const formatBadgeNum = (num: number) => {
25 return num <= 99 ? num : '99+';
26 };
27
28 const userBadge = computed((): number => {
29 let count = 0;
30 if (permissions.value?.includes('user-certify')) {
31 count += auditUserCount.value as number;
32 }
33 return count;
34 });
35
36 const auditionBadge = computed((): number => {
37 let count = 0;
38 if (permissions.value?.includes('audition-activity-audit')) {
39 count += auditActivityCount.value as number;
40 }
41 if (permissions.value?.includes('audition-activity-apply')) {
42 count += activityApplyFailCount.value as number;
43 }
44 return count;
45 });
46
47 useIntervalFn(() => {
48 authorizedStore.syncAuditUser();
49 authorizedStore.syncAuditActivity();
50 }, 30000);
51
52 const syncServicePermission = (item: RouteRecordRaw) => {
53 if (item.meta) {
54 const serverConfig = appMenu.value.find((menu) => item.name === menu.name);
55 item.meta.title = serverConfig?.label || item.meta?.title || '';
56 item.meta.order = serverConfig?.weight || item.meta?.order || 0;
57 item.meta.icon = serverConfig?.icon || item.meta?.icon || '';
58 }
59 item.children?.map((child) => syncServicePermission(child));
60 return item;
61 };
62
63 const appRoute = computed((): RouteRecordRaw[] => {
64 return router
65 .getRoutes()
66 .find((el) => el.name === 'root')
67 ?.children.filter((element) => element.meta?.hideInMenu !== true)
68 .map((item: RouteRecordRaw) => syncServicePermission(item)) as RouteRecordRaw[];
69 });
70
71 const menuTree = computed((): RouteRecordRaw[] => {
72 function travel(_routes: RouteRecordRaw[], layer: number) {
73 if (!_routes) return null;
74 const collector: any = orderBy(_routes, 'meta.order', 'desc').map((element) => {
75 // no access
76 if (!permission.accessRouter(element)) {
77 return null;
78 }
79
80 // leaf node
81 if (!element.children) {
82 return element;
83 }
84
85 // route filter hideInMenu true
86 element.children = element.children.filter((x) => x.meta?.hideInMenu !== true);
87
88 // Associated child node
89 const subItem = travel(element.children, layer);
90 if (subItem.length) {
91 element.children = subItem;
92 return element;
93 }
94 // the else logic
95 if (layer > 1) {
96 element.children = subItem;
97 return element;
98 }
99
100 if (element.meta?.hideInMenu === false) {
101 return element;
102 }
103
104 return null;
105 });
106
107 return collector.filter(Boolean);
108 }
109
110 return travel(appRoute.value, 0);
111 });
112
113 // In this case only two levels of menus are available
114 // You can expand as needed
115
116 const selectedKey = ref<string[]>([]);
117 const openKey = ref<string[]>([]);
118
119 const goto = (item: RouteRecordRaw) => {
120 router.push({ name: item.name });
121 };
122 watch(
123 route,
124 (newVal) => {
125 if (newVal.meta.requiresAuth) {
126 const key = newVal.meta.hideInMenu ? last(newVal.matched)?.meta?.menuSelectKey : last(newVal.matched)?.name;
127 selectedKey.value = [key as string];
128 openKey.value = newVal.meta.breadcrumb || [];
129 }
130 },
131 { immediate: true }
132 );
133 watch(
134 () => appStore.menuCollapse,
135 (newVal) => {
136 collapsed.value = newVal;
137 },
138 { immediate: true }
139 );
140 const setCollapse = (val: boolean) => appStore.updateSettings({ menuCollapse: val });
141
142 const renderSubMenu = () => {
143 function travel(_route: RouteRecordRaw[], nodes = []) {
144 if (_route) {
145 _route.forEach((element) => {
146 let icon = element?.meta?.icon ? `<${element?.meta?.icon}/>` : ``;
147 let title = `<span>${element.meta?.title}</span>`;
148
149 if (element?.name === 'user' && userBadge.value !== 0) {
150 if (icon && collapsed.value) {
151 icon = `<a-badge :count="${userBadge.value}" :offset="[2, -2]" dot>${icon}</a-badge>`;
152 }
153 title += `<span class="menu-number">${formatBadgeNum(userBadge.value)}</span>`;
154 }
155 if (element?.name === 'audition' && auditionBadge.value !== 0) {
156 if (icon && collapsed.value) {
157 icon = `<a-badge :count="${auditionBadge.value}" :offset="[4, -2]" dot>${icon}</a-badge>`;
158 }
159 title += `<span class="menu-number">${formatBadgeNum(auditionBadge.value)}</span>`;
160 }
161
162 let r;
163
164 if (element && element.children) {
165 r = (
166 <a-sub-menu key={element?.name} v-slots={{ icon: () => h(compile(icon)), title: () => h(compile(title)) }}>
167 {element?.children?.map((elem) => {
168 return (
169 <a-menu-item key={elem.name} onClick={() => goto(elem)}>
170 <span>{elem.meta?.title}</span>
171 {elem.name === 'user-certify' && auditUserCount.value !== 0 ? (
172 <span class="menu-number">{formatBadgeNum(auditUserCount.value as number)}</span>
173 ) : (
174 ''
175 )}
176 {elem.name === 'audition-activity-audit' && auditActivityCount.value !== 0 ? (
177 <span class="menu-number">{formatBadgeNum(auditActivityCount.value as number)}</span>
178 ) : (
179 ''
180 )}
181 {elem.name === 'audition-activity-apply' && activityApplyFailCount.value !== 0 ? (
182 <span class="menu-number">{formatBadgeNum(activityApplyFailCount.value as number)}</span>
183 ) : (
184 ''
185 )}
186 {travel(elem.children ?? [])}
187 </a-menu-item>
188 );
189 })}
190 </a-sub-menu>
191 );
192 } else {
193 r = (
194 <a-menu-item key={element.name} v-slots={{ icon: () => h(compile(icon)) }} onClick={() => goto(element)}>
195 {element.meta?.title}
196 </a-menu-item>
197 );
198 }
199 nodes.push(r as never);
200 });
201 }
202 return nodes;
203 }
204
205 return travel(menuTree.value);
206 };
207
208 return () => (
209 <a-menu
210 v-model:collapsed={collapsed.value}
211 show-collapse-button
212 v-model:selected-keys={selectedKey.value}
213 v-model:open-keys={openKey.value}
214 auto-open-selected={true}
215 level-indent={34}
216 style={{ height: '100%' }}
217 onCollapse={setCollapse}
218 >
219 {renderSubMenu()}
220 </a-menu>
221 );
222 },
223 });
224 </script>
225
226 <style lang="less" scoped>
227 :deep(.arco-menu-inner) {
228 .arco-menu-inline-header {
229 display: flex;
230 align-items: center;
231 }
232
233 .arco-icon {
234 &:not(.arco-icon-down) {
235 font-size: 18px;
236 }
237 }
238 }
239
240 .menu-number {
241 background-color: red;
242 color: #fff;
243 margin-left: 12px;
244 padding: 0 6px;
245 border-radius: 8px;
246 font-size: 12px;
247 font-weight: 500;
248 }
249 </style>
1 <template>
2 <div class="navbar">
3 <div class="left-side">
4 <a-space>
5 <img alt="logo" src="//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/dfdba5317c0c20ce20e64fac803d52bc.svg~tplv-49unhts6dw-image.image" />
6 <a-typography-title :heading="5" :style="{ margin: 0, fontSize: '18px' }"> 海星试唱</a-typography-title>
7 </a-space>
8 </div>
9 <ul class="right-side">
10 <!-- <li>-->
11 <!-- <toggle-theme-button />-->
12 <!-- </li>-->
13 <!-- <li>-->
14 <!-- <a-tooltip :content="$t('settings.navbar.alerts')">-->
15 <!-- <div class="message-box-trigger">-->
16 <!-- <a-badge :count="9" dot>-->
17 <!-- <a-button-->
18 <!-- :shape="'circle'"-->
19 <!-- class="nav-btn"-->
20 <!-- type="outline"-->
21 <!-- @click="setPopoverVisible"-->
22 <!-- >-->
23 <!-- <icon-notification />-->
24 <!-- </a-button>-->
25 <!-- </a-badge>-->
26 <!-- </div>-->
27 <!-- </a-tooltip>-->
28 <!-- <a-popover-->
29 <!-- :arrow-style="{ display: 'none' }"-->
30 <!-- :content-style="{ padding: 0, minWidth: '400px' }"-->
31 <!-- content-class="message-popover"-->
32 <!-- trigger="click"-->
33 <!-- >-->
34 <!-- <div ref="refBtn" class="ref-btn"></div>-->
35 <!-- <template #content>-->
36 <!-- <message-box />-->
37 <!-- </template>-->
38 <!-- </a-popover>-->
39 <!-- </li>-->
40 <li>
41 <a-dropdown trigger="hover">
42 <a-space align="center">
43 <a-avatar v-if="avatar" :size="32" :image-url="avatar" />
44 <div :style="{ color: theme === 'dark' ? 'white' : 'black', lineHeight: '32px' }">
45 <span>{{ name }}</span>
46 <icon-down />
47 </div>
48 </a-space>
49 <template #content>
50 <!-- <a-doption>-->
51 <!-- <a-space @click="switchRoles">-->
52 <!-- <icon-tag />-->
53 <!-- <span>-->
54 <!-- {{ $t('messageBox.switchRoles') }}-->
55 <!-- </span>-->
56 <!-- </a-space>-->
57 <!-- </a-doption>-->
58 <a-doption>
59 <a-space @click="handleChangePwd">
60 <icon-lock />
61 <span>修改密码</span>
62 </a-space>
63 </a-doption>
64 <a-doption>
65 <a-space @click="handleLogout">
66 <icon-export />
67 <span> 退出登录 </span>
68 </a-space>
69 </a-doption>
70 </template>
71 </a-dropdown>
72 </li>
73 </ul>
74 </div>
75 </template>
76
77 <script lang="ts" setup>
78 import { computed } from 'vue';
79 import { useAppStore, useAuthorizedStore } from '@/store';
80 import useUser from '@/hooks/user';
81 // import ToggleThemeButton from '@/layout/components/toggle-theme-button.vue';
82 import { IconDown, IconExport, IconLock } from '@arco-design/web-vue/es/icon';
83
84 const appStore = useAppStore();
85 const authorized = useAuthorizedStore();
86
87 const avatar = computed(() => {
88 return authorized.avatar;
89 });
90
91 const name = computed(() => {
92 return authorized.nick_name;
93 });
94
95 const { theme } = appStore;
96
97 // const theme = computed(() => {
98 // return appStore.theme;
99 // });
100
101 // const setVisible = () => {
102 // appStore.updateSettings({ globalSettings: true });
103 // };
104 // const refBtn = ref();
105 // const triggerBtn = ref();
106 // const setPopoverVisible = () => {
107 // const event = new MouseEvent('click', {
108 // view: window,
109 // bubbles: true,
110 // cancelable: true,
111 // });
112 // refBtn.value.dispatchEvent(event);
113 // };
114 const handleLogout = () => authorized.logout();
115
116 const handleChangePwd = () => useUser().resetPwd();
117 // const setDropDownVisible = () => {
118 // const event = new MouseEvent('click', {
119 // view: window,
120 // bubbles: true,
121 // cancelable: true,
122 // });
123 // triggerBtn.value.dispatchEvent(event);
124 // };
125 </script>
126
127 <style lang="less" scoped>
128 .navbar {
129 display: flex;
130 justify-content: space-between;
131 height: 100%;
132 background-color: var(--color-bg-2);
133 border-bottom: 1px solid var(--color-border);
134 }
135
136 .left-side {
137 display: flex;
138 align-items: center;
139 padding-left: 20px;
140 }
141
142 .right-side {
143 display: flex;
144 padding-right: 10px;
145 list-style: none;
146
147 :deep(.locale-select) {
148 border-radius: 20px;
149 }
150
151 li {
152 display: flex;
153 align-items: center;
154 padding: 0 10px;
155 }
156
157 a {
158 color: var(--color-text-1);
159 text-decoration: none;
160 }
161
162 .nav-btn {
163 border-color: rgb(var(--gray-2));
164 color: rgb(var(--gray-8));
165 font-size: 16px;
166 }
167
168 .trigger-btn,
169 .ref-btn {
170 position: absolute;
171 bottom: 14px;
172 }
173
174 .trigger-btn {
175 margin-left: 14px;
176 }
177 }
178 </style>
179
180 <style lang="less">
181 .message-popover {
182 .arco-popover-content {
183 margin-top: 0;
184 }
185 }
186
187 .arco-dropdown-open .arco-icon-down {
188 transform: rotate(180deg);
189 }
190 </style>
1 <template>
2 <a-tooltip :content="theme === 'light' ? '切换为暗黑模式' : '切换为亮色模式'">
3 <a-button :shape="'circle'" class="nav-btn" type="outline" @click="() => onChange()">
4 <template #icon>
5 <icon-moon-fill v-if="theme === 'dark'" />
6 <icon-sun-fill v-else />
7 </template>
8 </a-button>
9 </a-tooltip>
10 </template>
11
12 <script lang="ts">
13 import { computed, defineComponent } from 'vue';
14 import { useAppStore } from '@/store';
15 import { useDark, useToggle } from '@vueuse/core';
16
17 import { IconMoonFill, IconSunFill } from '@arco-design/web-vue/es/icon';
18
19 export default defineComponent({
20 name: 'ToggleThemeButton',
21 components: {
22 IconSunFill,
23 IconMoonFill,
24 },
25 setup() {
26 const appStore = useAppStore();
27
28 const theme = computed(() => {
29 return appStore.theme;
30 });
31
32 const isDark = useDark({
33 selector: 'body',
34 attribute: 'arco-theme',
35 valueDark: 'dark',
36 valueLight: 'light',
37 storageKey: 'arco-theme',
38 onChanged(dark: boolean) {
39 appStore.toggleTheme(dark);
40 },
41 });
42 const onChange = useToggle(isDark);
43
44 return {
45 theme,
46 onChange,
47 };
48 },
49 });
50 </script>
51
52 <style lang="less" scoped>
53 .nav-btn {
54 border-color: rgb(var(--gray-2));
55 color: rgb(var(--gray-8));
56 font-size: 16px;
57 }
58
59 .trigger-btn,
60 .ref-btn {
61 position: absolute;
62 bottom: 14px;
63 }
64
65 .trigger-btn {
66 margin-left: 14px;
67 }
68 </style>
1 <template>
2 <a-layout class="layout">
3 <div class="layout-navbar">
4 <Navbar />
5 </div>
6 <a-layout>
7 <a-layout>
8 <a-layout-sider
9 v-if="menu"
10 :breakpoint="'xl'"
11 :collapsed="collapse"
12 :collapsible="true"
13 :hide-trigger="true"
14 :style="{ paddingTop: '60px' }"
15 :width="menuWidth"
16 class="layout-sider"
17 @collapse="setCollapsed"
18 >
19 <div class="menu-wrapper">
20 <Menu />
21 </div>
22 </a-layout-sider>
23 <a-layout :style="paddingStyle" class="layout-content">
24 <a-layout-content>
25 <router-view />
26 </a-layout-content>
27 </a-layout>
28 </a-layout>
29 </a-layout>
30 </a-layout>
31 </template>
32
33 <script lang="ts">
34 import { computed, defineComponent, watch } from 'vue';
35 import { useRoute, useRouter } from 'vue-router';
36 import { useAppStore, useAuthorizedStore } from '@/store';
37 import Menu from '@/layout/components/menu.vue';
38 import usePermission from '@/hooks/permission';
39 import Navbar from '@/layout/components/navbar.vue';
40
41 export default defineComponent({
42 components: {
43 Navbar,
44 Menu,
45 },
46 setup() {
47 const appStore = useAppStore();
48 const authorizedStore = useAuthorizedStore();
49 const router = useRouter();
50 const route = useRoute();
51 const permission = usePermission();
52 const navbarHeight = `60px`;
53 const menu = computed(() => appStore.menu);
54 const menuWidth = computed(() => {
55 return appStore.menuCollapse ? 48 : appStore.menuWidth;
56 });
57 const collapse = computed(() => {
58 return appStore.menuCollapse;
59 });
60 const paddingStyle = computed(() => {
61 const paddingLeft = menu.value ? { paddingLeft: `${menuWidth.value}px` } : {};
62 const paddingTop = { paddingTop: navbarHeight };
63 return { ...paddingLeft, ...paddingTop };
64 });
65 const setCollapsed = (val: boolean) => {
66 appStore.updateSettings({ menuCollapse: val });
67 };
68 watch(
69 () => authorizedStore.permissions,
70 (roleValue) => {
71 if (roleValue && !permission.accessRouter(route)) router.push({ name: 'notFound' });
72 }
73 );
74 return {
75 menu,
76 menuWidth,
77 paddingStyle,
78 collapse,
79 setCollapsed,
80 };
81 },
82 });
83 </script>
84
85 <style lang="less" scoped>
86 @nav-size-height: 60px;
87 @layout-max-width: 1100px;
88
89 .layout {
90 width: 100%;
91 height: 100%;
92 }
93
94 .layout-navbar {
95 position: fixed;
96 top: 0;
97 left: 0;
98 z-index: 100;
99 width: 100%;
100 min-width: @layout-max-width;
101 height: @nav-size-height;
102 }
103
104 .layout-sider {
105 position: fixed;
106 top: 0;
107 left: 0;
108 z-index: 99;
109 height: 100%;
110
111 &::after {
112 position: absolute;
113 top: 0;
114 right: -1px;
115 display: block;
116 width: 1px;
117 height: 100%;
118 background-color: var(--color-border);
119 content: '';
120 }
121
122 > :deep(.arco-layout-sider-children) {
123 overflow-y: hidden;
124 }
125 }
126
127 .menu-wrapper {
128 height: 100%;
129 overflow: auto;
130 overflow-x: hidden;
131
132 :deep(.arco-menu) {
133 ::-webkit-scrollbar {
134 width: 12px;
135 height: 4px;
136 }
137
138 ::-webkit-scrollbar-thumb {
139 border: 4px solid transparent;
140 background-clip: padding-box;
141 border-radius: 7px;
142 background-color: var(--color-text-4);
143 }
144
145 ::-webkit-scrollbar-thumb:hover {
146 background-color: var(--color-text-3);
147 }
148 }
149 }
150
151 .layout-content {
152 min-width: @layout-max-width;
153 min-height: 100vh;
154 overflow-y: hidden;
155 background-color: var(--color-fill-2);
156 transition: padding-left 0.2s;
157 }
158 </style>
1 import { createApp } from 'vue';
2 import ArcoVue from '@arco-design/web-vue';
3 import ArcoVueIcon from '@arco-design/web-vue/es/icon';
4 import globalComponents from '@/components';
5 import directive from './directive';
6 import App from './App.vue';
7 import '@arco-design/web-vue/dist/arco.css';
8 import '@/assets/style/global.less';
9 import 'vue3-colorpicker/style.css';
10 import '@/http/interceptor';
11
12 import store from './store';
13
14 import router from './router';
15
16 const app = createApp(App);
17
18 app.use(ArcoVue, {});
19 app.use(ArcoVueIcon);
20
21 app.use(store);
22 app.use(router);
23 app.use(globalComponents);
24 app.use(directive);
25
26 app.mount('#app');
1 import { createRouter, createWebHistory, LocationQueryRaw } from 'vue-router';
2 import NProgress from 'nprogress'; // progress bar
3 import 'nprogress/nprogress.css';
4
5 import usePermission from '@/hooks/permission';
6 import { clearToken, isLogin } from '@/utils/auth';
7 import PageLayout from '@/layout/index.vue';
8
9 import { useAuthorizedStore } from '@/store';
10 import appRoutes from './modules';
11
12 NProgress.configure({ showSpinner: false }); // NProgress Configuration
13
14 const router = createRouter({
15 history: createWebHistory(),
16 routes: [
17 {
18 path: '/login',
19 name: 'login',
20 component: () => import('@/views/login/index.vue'),
21 meta: {
22 title: '登陆',
23 requiresAuth: false,
24 },
25 },
26 {
27 name: 'root',
28 path: '/',
29 component: PageLayout,
30 redirect: 'dashboard',
31 children: appRoutes,
32 },
33 {
34 path: '/:pathMatch(.*)*',
35 name: 'notFound',
36 redirect: '/exception/404',
37 meta: {
38 requiresAuth: true,
39 },
40 },
41 ],
42 scrollBehavior() {
43 return { top: 0 };
44 },
45 });
46
47 router.beforeEach(async (to, from, next) => {
48 NProgress.start();
49 const authorizedStore = useAuthorizedStore();
50
51 if (from.name !== undefined) {
52 to.meta.from = from.name;
53 }
54
55 async function crossroads() {
56 const Permission = usePermission();
57
58 if (Permission.accessRouter(to)) {
59 next();
60 } else {
61 next({ name: 'exception-403' });
62 }
63 NProgress.done();
64 }
65
66 if (isLogin()) {
67 if (authorizedStore.permissions.length) {
68 await crossroads();
69 } else {
70 try {
71 await authorizedStore.syncInfo();
72 await crossroads();
73 } catch (error) {
74 clearToken();
75 next({ name: 'login', query: { path: to.path } as LocationQueryRaw });
76 NProgress.done();
77 }
78 }
79 } else {
80 if (to.name === 'login') {
81 next();
82 NProgress.done();
83 return;
84 }
85
86 if (to.name === 'root') {
87 next({ name: 'login' });
88 } else {
89 next({ name: 'login', query: { path: to.path } as LocationQueryRaw });
90 }
91 NProgress.done();
92 }
93 });
94
95 router.afterEach(async (to) => {
96 sessionStorage.setItem(
97 'route',
98 to.meta?.breadcrumb?.toString() ||
99 to.matched
100 ?.slice(1)
101 .map((item) => item.name)
102 .toString() ||
103 'dashboard'
104 );
105 });
106
107 export default router;
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: 'projects',
12 name: 'audition-project',
13 component: () => import('@/views/audition/project/index.vue'),
14 meta: {
15 requiresAuth: true,
16 hideInMenu: false,
17 isRedirect: true,
18 roles: ['audition-project'],
19 breadcrumb: ['audition', 'audition-project'],
20 reload: true,
21 },
22 },
23 {
24 path: 'projects/:id(\\d+)',
25 name: 'audition-project-show',
26 component: () => import('@/views/audition/project-show/index.vue'),
27 meta: {
28 title: '详情',
29 requiresAuth: true,
30 hideInMenu: true,
31 menuSelectKey: 'audition-project',
32 roles: ['audition-project-show'],
33 breadcrumb: ['audition', 'audition-project', 'audition-project-show'],
34 reload: true,
35 },
36 },
37 {
38 path: 'activity-apply',
39 name: 'audition-activity-apply',
40 component: () => import('@/views/audition/activity-apply/index.vue'),
41 meta: {
42 requiresAuth: true,
43 hideInMenu: false,
44 isRedirect: true,
45 roles: ['audition-activity-apply'],
46 breadcrumb: ['audition', 'audition-activity-apply'],
47 reload: true,
48 },
49 },
50 {
51 path: 'activity-audit',
52 name: 'audition-activity-audit',
53 component: () => import('@/views/audition/activity-audit/index.vue'),
54 meta: {
55 requiresAuth: true,
56 hideInMenu: false,
57 isRedirect: true,
58 roles: ['audition-activity-audit'],
59 breadcrumb: ['audition', 'audition-activity-audit'],
60 reload: true,
61 },
62 },
63 {
64 path: 'activity-relist-log',
65 name: 'audition-activity-relist-log',
66 component: () => import('@/views/audition/activity-relist-log/index.vue'),
67 meta: {
68 requiresAuth: true,
69 hideInMenu: false,
70 isRedirect: true,
71 roles: ['audition-activity-relist-log'],
72 breadcrumb: ['audition', 'audition-activity-relist-log'],
73 reload: true,
74 },
75 },
76 {
77 path: 'activities',
78 name: 'audition-activity',
79 component: () => import('@/views/audition/activity/index.vue'),
80 meta: {
81 requiresAuth: true,
82 hideInMenu: false,
83 isRedirect: true,
84 roles: ['audition-activity'],
85 breadcrumb: ['audition', 'audition-activity'],
86 reload: true,
87 },
88 },
89 {
90 path: 'activities/:id(\\d+)',
91 name: 'audition-activity-show',
92 component: () => import('@/views/audition/activity-show/index.vue'),
93 meta: {
94 title: '详情',
95 requiresAuth: true,
96 hideInMenu: true,
97 menuSelectKey: 'audition-activity',
98 roles: ['audition-activity'],
99 breadcrumb: ['audition', 'audition-activity', 'audition-activity-show'],
100 reload: true,
101 },
102 },
103 {
104 path: 'demos',
105 name: 'audition-demo',
106 component: () => import('@/views/audition/demo/index.vue'),
107 meta: {
108 requiresAuth: true,
109 hideInMenu: false,
110 isRedirect: true,
111 roles: ['audition-demo'],
112 breadcrumb: ['audition', 'audition-demo'],
113 reload: true,
114 },
115 },
116 {
117 path: 'demos/:id(\\d+)',
118 name: 'audition-demo-show',
119 component: () => import('@/views/audition/demo-show/index.vue'),
120 meta: {
121 title: '详情',
122 requiresAuth: true,
123 hideInMenu: true,
124 menuSelectKey: 'audition-demo',
125 roles: ['audition-demo'],
126 breadcrumb: ['audition', 'audition-demo', 'audition-demo-show'],
127 reload: true,
128 },
129 },
130 ],
131 };
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 },
21 },
22 {
23 path: '404',
24 name: 'exception-404',
25 component: () => import('@/views/exception/404/index.vue'),
26 meta: {
27 title: '404',
28 requiresAuth: true,
29 roles: ['*'],
30 hideInMenu: true,
31 },
32 },
33 {
34 path: '500',
35 name: 'exception-500',
36 component: () => import('@/views/exception/500/index.vue'),
37 meta: {
38 title: '500',
39 requiresAuth: true,
40 roles: ['*'],
41 },
42 },
43 ],
44 };
1 import Dashboard from './dashboard';
2
3 import Exception from './exception';
4 import User from './user';
5 import Operation from './operation';
6 import Audition from './audition';
7 import System from './system';
8
9 export default [Dashboard, Exception, User, Operation, Audition, System];
1 export default {
2 path: 'operation',
3 name: 'operation',
4 component: () => import('@/views/operation/index.vue'),
5 meta: {
6 requiresAuth: true,
7 roles: ['*'],
8 },
9 children: [
10 {
11 path: 'banners',
12 name: 'operation-banner',
13 component: () => import('@/views/operation/banner/index.vue'),
14 meta: {
15 requiresAuth: true,
16 hideInMenu: false,
17 isRedirect: true,
18 reload: true,
19 roles: ['operation-banner'],
20 breadcrumb: ['operation', 'operation-banner'],
21 },
22 },
23 {
24 path: 'notification',
25 name: 'operation-notification',
26 component: () => import('@/views/operation/notification/index.vue'),
27 meta: {
28 requiresAuth: true,
29 hideInMenu: false,
30 isRedirect: true,
31 reload: true,
32 roles: ['operation-notification'],
33 breadcrumb: ['operation', 'operation-notification'],
34 },
35 },
36 {
37 path: 'notification',
38 name: 'operation-notification-create',
39 component: () => import('@/views/operation/notification/create.vue'),
40 meta: {
41 title: '创建',
42 requiresAuth: true,
43 hideInMenu: true,
44 reload: true,
45 isRedirect: true,
46 menuSelectKey: 'operation-notification',
47 roles: ['operation-notification-create'],
48 breadcrumb: ['operation', 'operation-notification'],
49 },
50 },
51 {
52 path: 'notification/:id(\\d+)',
53 name: 'operation-notification-show',
54 component: () => import('@/views/operation/notification/show.vue'),
55 meta: {
56 title: '详情',
57 requiresAuth: true,
58 hideInMenu: true,
59 reload: true,
60 isRedirect: true,
61 menuSelectKey: 'operation-notification',
62 roles: ['operation-notification-show'],
63 breadcrumb: ['operation', 'operation-notification', 'operation-notification-show'],
64 },
65 },
66 {
67 path: 'notification/:id(\\d+)/edit',
68 name: 'operation-notification-update',
69 component: () => import('@/views/operation/notification/update.vue'),
70 meta: {
71 title: '详情',
72 requiresAuth: true,
73 hideInMenu: true,
74 reload: true,
75 isRedirect: true,
76 menuSelectKey: 'operation-notification',
77 roles: ['operation-notification-edit'],
78 breadcrumb: ['operation', 'operation-notification', 'operation-notification-update'],
79 },
80 },
81 {
82 path: 'broker',
83 name: 'operation-broker',
84 component: () => import('@/views/operation/broker/index.vue'),
85 meta: {
86 requiresAuth: true,
87 hideInMenu: false,
88 isRedirect: true,
89 reload: true,
90 roles: ['operation-broker'],
91 breadcrumb: ['operation', 'operation-broker'],
92 },
93 },
94 ],
95 };
1 export default {
2 path: 'system',
3 name: 'system',
4 component: () => import('@/views/system/index.vue'),
5 meta: {
6 requiresAuth: true,
7 roles: ['*'],
8 },
9 children: [
10 {
11 path: 'tags',
12 name: 'system-tag',
13 component: () => import('@/views/system/tag/index.vue'),
14 meta: {
15 requiresAuth: true,
16 hideInMenu: false,
17 roles: ['system-tag'],
18 breadcrumb: ['system', 'system-tag'],
19 },
20 },
21 {
22 path: 'star-tags',
23 name: 'system-star-tag',
24 component: () => import('@/views/system/star-tag/index.vue'),
25 meta: {
26 requiresAuth: true,
27 hideInMenu: false,
28 roles: ['system-star-tag'],
29 breadcrumb: ['system', 'system-star-tag'],
30 },
31 },
32 {
33 path: 'configs',
34 name: 'system-config',
35 component: () => import('@/views/system/config/index.vue'),
36 meta: {
37 requiresAuth: true,
38 hideInMenu: false,
39 roles: ['system-config'],
40 breadcrumb: ['system', 'system-config'],
41 },
42 },
43 {
44 path: 'materials',
45 name: 'system-material',
46 component: () => import('@/views/system/material/index.vue'),
47 meta: {
48 requiresAuth: true,
49 hideInMenu: false,
50 roles: ['system-material'],
51 breadcrumb: ['system', 'system-material'],
52 },
53 },
54 {
55 path: 'roles',
56 name: 'system-role',
57 component: () => import('@/views/system/role/index.vue'),
58 meta: {
59 requiresAuth: true,
60 hideInMenu: false,
61 roles: ['system-role'],
62 breadcrumb: ['system', 'system-role'],
63 },
64 },
65 {
66 path: 'versions',
67 name: 'system-version',
68 component: () => import('@/views/system/version/index.vue'),
69 meta: {
70 requiresAuth: true,
71 hideInMenu: false,
72 roles: ['system-version'],
73 breadcrumb: ['system', 'system-version'],
74 },
75 },
76 {
77 path: 'customer',
78 name: 'system-customer',
79 component: () => import('@/views/system/customer/index.vue'),
80 meta: {
81 requiresAuth: true,
82 hideInMenu: false,
83 isRedirect: true,
84 roles: ['system-customer'],
85 breadcrumb: ['system', 'system-customer'],
86 },
87 },
88 {
89 path: 'logs',
90 name: 'system-log',
91 component: () => import('@/views/system/logs/index.vue'),
92 meta: {
93 requiresAuth: true,
94 hideInMenu: false,
95 roles: ['system-log'],
96 breadcrumb: ['system', 'system-log'],
97 },
98 },
99 {
100 path: 'reports',
101 name: 'system-report',
102 component: () => import('@/views/system/report/index.vue'),
103 meta: {
104 requiresAuth: true,
105 hideInMenu: false,
106 roles: ['system-report'],
107 breadcrumb: ['system', 'system-report'],
108 },
109 },
110 ],
111 };
1 export default {
2 path: 'user',
3 name: 'user',
4 component: () => import('@/views/user/index.vue'),
5 meta: {
6 requiresAuth: true,
7 roles: ['*'],
8 },
9 children: [
10 {
11 path: 'registers',
12 name: 'user-register',
13 component: () => import('@/views/user/register/index.vue'),
14 meta: {
15 requiresAuth: true,
16 hideInMenu: false,
17 isRedirect: true,
18 roles: ['user-register'],
19 breadcrumb: ['user', 'user-register'],
20 reload: true,
21 },
22 },
23 {
24 path: 'registers/:id(\\d+)',
25 name: 'user-register-show',
26 component: () => import('@/views/user/info/index.vue'),
27 meta: {
28 title: '详情',
29 requiresAuth: true,
30 hideInMenu: true,
31 menuSelectKey: 'user-register',
32 roles: ['user-register-show'],
33 breadcrumb: ['user', 'user-register', 'user-register-show'],
34 },
35 },
36 {
37 path: 'certify',
38 name: 'user-certify',
39 component: () => import('@/views/user/certify/index.vue'),
40 meta: {
41 requiresAuth: true,
42 hideInMenu: false,
43 isRedirect: true,
44 roles: ['user-certify'],
45 breadcrumb: ['user', 'user-certify'],
46 reload: true,
47 },
48 },
49 {
50 path: 'singers',
51 name: 'user-singer',
52 component: () => import('@/views/user/singer/index.vue'),
53 meta: {
54 requiresAuth: true,
55 hideInMenu: false,
56 isRedirect: true,
57 roles: ['user-singer'],
58 breadcrumb: ['user', 'user-singer'],
59 reload: true,
60 },
61 },
62 {
63 path: 'singers/:id(\\d+)',
64 name: 'user-singer-show',
65 component: () => import('@/views/user/info/index.vue'),
66 meta: {
67 title: '详情',
68 requiresAuth: true,
69 hideInMenu: true,
70 menuSelectKey: 'user-singer',
71 roles: ['user-singer-show'],
72 breadcrumb: ['user', 'user-singer', 'user-singer-show'],
73 },
74 },
75 {
76 path: 'business',
77 name: 'user-business',
78 component: () => import('@/views/user/business/index.vue'),
79 meta: {
80 requiresAuth: true,
81 hideInMenu: false,
82 isRedirect: true,
83 reload: true,
84 roles: ['user-business'],
85 breadcrumb: ['user', 'user-business'],
86 },
87 },
88 {
89 path: 'business/:id(\\d+)',
90 name: 'user-business-show',
91 component: () => import('@/views/user/info/index.vue'),
92 meta: {
93 title: '详情',
94 requiresAuth: true,
95 hideInMenu: true,
96 menuSelectKey: 'user-business',
97 roles: ['user-business-show'],
98 breadcrumb: ['user', 'user-business', 'user-business-show'],
99 },
100 },
101 ],
102 };
1 import 'vue-router';
2
3 declare module 'vue-router' {
4 interface RouteMeta {
5 // options
6 roles?: string[];
7 // every route must declare
8 requiresAuth: boolean; // need login
9 icon?: string;
10 // menu select key
11 menuSelectKey?: string;
12 hideInMenu?: boolean;
13 isRedirect?: boolean;
14 title?: string;
15 order?: number;
16 breadcrumb?: string[];
17 }
18 }
1 import { QueryForPaginationParams, Sort } from '@/types/global';
2
3 export function getSort(state: any): Sort {
4 return state.sort;
5 }
6
7 export function getQuery(state: any): QueryForPaginationParams {
8 const { current, pageSize } = state.pagination;
9 const { column, type } = state.sort;
10
11 return {
12 ...state.filter,
13 page: current,
14 pageSize,
15 sortBy: column,
16 sortType: type.replace('end', '') as 'desc' | 'asc',
17 };
18 }
1 import { createPinia } from 'pinia';
2
3 import useAppStore from '@/store/modules/app';
4 // eslint-disable-next-line import/no-cycle
5 import useAuthorizedStore from '@/store/modules/authorized';
6
7 import useSelectionStore from '@/store/modules/selection';
8
9 const pinia = createPinia();
10
11 export { useAppStore, useSelectionStore, useAuthorizedStore };
12
13 export default pinia;
1 import { defineStore } from 'pinia';
2 import { AppState } from './types';
3 import { usePermissionApi } from '@/http/role';
4 import { SystemPermission } from '@/types/system-permission';
5
6 const useAppStore = defineStore('app', {
7 state: (): AppState => ({
8 theme: 'light',
9 colorWeak: false,
10 navbar: true,
11 menu: true,
12 hideMenu: false,
13 menuCollapse: false,
14 footer: true,
15 themeColor: '#165DFF',
16 menuWidth: 210,
17 globalSettings: false,
18 device: 'desktop',
19 tabBar: false,
20 permissions: [],
21 }),
22
23 getters: {
24 appCurrentSetting(state: AppState): AppState {
25 return { ...state };
26 },
27 appDevice(state: AppState) {
28 return state.device;
29 },
30 appMenu(state: AppState): SystemPermission[] {
31 return state.permissions?.filter((item) => item.guard === 'Admin' && item.type === 'Menu') || [];
32 },
33 },
34
35 actions: {
36 // Update app settings
37 updateSettings(partial: Partial<AppState>) {
38 // @ts-ignore-next-line
39 this.$patch(partial);
40 },
41
42 // Change theme color
43 toggleTheme(dark: boolean) {
44 if (dark) {
45 this.theme = 'dark';
46 document.body.setAttribute('arco-theme', 'dark');
47 } else {
48 this.theme = 'light';
49 document.body.removeAttribute('arco-theme');
50 }
51 },
52 toggleDevice(device: string) {
53 this.device = device;
54 },
55 toggleMenu(value: boolean) {
56 this.hideMenu = value;
57 },
58 queryPermissionList() {
59 usePermissionApi
60 .get({
61 setColumn: ['id', 'parent_id', 'guard', 'name', 'label', 'icon', 'weight', 'type'],
62 fetchType: 'all',
63 sortBy: 'weight',
64 sortType: 'desc',
65 })
66 .then(({ data }) => (this.permissions = data));
67 },
68 },
69 });
70
71 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
13 [key: string]: unknown;
14 }
1 import { defineStore } from 'pinia';
2 import { clearToken } from '@/utils/auth';
3 import useAuthApi from '@/http/auth';
4 import { AuthorizedState } from './type';
5 // eslint-disable-next-line import/no-cycle
6 import { useAppStore } from '@/store';
7 import { useCertifyApi } from '@/http/user';
8 import { useApplyApi } from '@/http/activity';
9
10 const useAuthorizedStore = defineStore('authorized', {
11 state: (): AuthorizedState => ({
12 id: undefined,
13 nick_name: undefined,
14 avatar: undefined,
15 permissions: [],
16 auditUserCount: 0,
17 auditActivityCount: 0,
18 activityApplyFailCount: 0,
19 }),
20 getters: {
21 authorizedInfo(state: AuthorizedState): AuthorizedState {
22 return { ...state };
23 },
24 getKey(state: AuthorizedState): number {
25 return state.id || 0;
26 },
27 },
28 actions: {
29 setInfo(partial: Partial<AuthorizedState>) {
30 // @ts-ignore
31 this.$patch(partial);
32 },
33 syncAuditUser() {
34 useCertifyApi.get({ status: 0, page: 1, pageSize: 1 }).then(({ meta }) => this.setInfo({ auditUserCount: meta.total }));
35 },
36 syncAuditActivity() {
37 useApplyApi
38 .get({ auditStatus: 0, songType: 1, page: 1, pageSize: 1 })
39 .then(({ meta }) => this.setInfo({ auditActivityCount: meta.total }));
40
41 useApplyApi
42 .get({ auditStatus: 2, songType: 1, createdForm: 1, page: 1, pageSize: 1 })
43 .then(({ meta }) => this.setInfo({ activityApplyFailCount: meta.total }));
44 },
45 async syncInfo() {
46 const { user, permissions } = await useAuthApi.info();
47 this.setInfo({ ...user, permissions });
48 useAppStore().queryPermissionList();
49 this.syncAuditUser();
50 this.syncAuditActivity();
51 },
52 logout() {
53 clearToken();
54 this.$reset();
55 window.location.reload();
56 },
57
58 async syncToken() {
59 // TODO
60 // const { data } = await refreshToken();
61 // setToken(data.access_token);
62 },
63 },
64 });
65
66 export default useAuthorizedStore;
1 export interface AuthorizedState {
2 id?: number;
3 nick_name?: string;
4 avatar?: string;
5 permissions: string[];
6
7 [key: string]: unknown;
8 }
1 import { defineStore } from 'pinia';
2 import { Selection } from '@/store/modules/selection/type';
3
4 import { SystemConfig } from '@/types/system-config';
5 import { Tag } from '@/types/tag';
6 import { Project } from '@/types/project';
7 import useProjectApi from '@/http/project';
8 import useUserApi from '@/http/user';
9 import useTagApi from '@/http/Tag';
10 import useConfigApi from '@/http/config';
11 import { User } from '@/utils/model';
12
13 const useSelectionStore = defineStore('selection', {
14 state: (): Selection => ({ user: [], project: [], tag: [], config: [] }),
15 getters: {
16 getUserOptions(state): User[] {
17 return state.user;
18 },
19 getProjectOptions(state) {
20 return state.project;
21 },
22 getTagOptions(state) {
23 return state.tag;
24 },
25 projectOptions(state) {
26 return state.project;
27 },
28 getConfigOption(state) {
29 return state.config;
30 },
31 lyricTool(state): string {
32 return state.config.find((item) => item.identifier === 'activity_lyric_tool')?.content || '';
33 },
34 activityLang(state): SystemConfig | undefined {
35 return state.config.find((item) => item.identifier === 'activity_lang');
36 },
37 activityLangOptions(state): SystemConfig[] {
38 return state.config.filter((item) => item.parent_id === this.activityLang?.id);
39 },
40 activitySpeed(state): SystemConfig | undefined {
41 return state.config.find((item) => item.identifier === 'activity_speed');
42 },
43 activitySpeedOptions(state): SystemConfig[] {
44 return state.config.filter((item) => item.parent_id === this.activitySpeed?.id);
45 },
46 activitySex(state): SystemConfig | undefined {
47 return state.config.find((item) => item.identifier === 'activity_sex');
48 },
49 activitySexOptions(state): SystemConfig[] {
50 return state.config.filter((item) => item.parent_id === this.activitySex?.id);
51 },
52 activityMark(state): SystemConfig | undefined {
53 return state.config.find((item) => item.identifier === 'activity_mark');
54 },
55 activityMarkOptions(state): SystemConfig[] {
56 return state.config.filter((item) => item.parent_id === this.activityMark?.id);
57 },
58 activityAudioAccept(state): string {
59 return state.config.find((item) => item.identifier === 'activity_audio_accept')?.content || 'audio/*';
60 },
61 activityTrackAccept(state): string {
62 return state.config.find((item) => item.identifier === 'activity_track_accept')?.content || '*';
63 },
64 activityTagOptions(state): Pick<Tag, 'id' | 'name' | 'type'>[] {
65 return state.tag.filter((item) => item.type === 1);
66 },
67 activityProjectOptions(state): Pick<Project, 'id' | 'name'>[] {
68 return state.project;
69 },
70 activityAuditIds(state): string[] {
71 return state.config.find((item) => item.identifier === 'activity_audit_ids')?.content?.split(',') || [];
72 },
73 appleDemoCover(state): string {
74 return state.config.find((item) => item.identifier === 'activity_demo_cover')?.content || '';
75 },
76
77 userAuthTag(state) {
78 return state.tag.filter((item) => item.type === 4);
79 },
80 },
81 actions: {
82 queryUser() {
83 useUserApi
84 .get({ fetchType: 'all', setColumn: ['id', 'nick_name', 'real_name', 'avatar'], sortBy: 'id', sortType: 'desc' })
85 .then(({ data }) => {
86 this.user = data;
87 });
88 },
89 queryProject() {
90 useProjectApi
91 .get({ fetchType: 'all', setColumn: ['id', 'is_can_apply', 'is_can_demo_apply', 'name'], sortBy: 'id', sortType: 'desc' })
92 .then(({ data }) => {
93 this.project = data;
94 });
95 },
96 queryTag() {
97 useTagApi.get({ fetchType: 'all', setColumn: ['id', 'name', 'type'], sortBy: 'weight', sortType: 'desc' }).then(({ data }) => {
98 this.tag = data;
99 });
100 },
101 queryConfig() {
102 useConfigApi
103 .get({
104 fetchType: 'all',
105 parent_id: '',
106 setColumn: ['id', 'parent_id', 'name', 'identifier', 'content'],
107 sortBy: 'weight',
108 sortType: 'desc',
109 })
110 .then(({ data }) => {
111 this.config = data;
112 });
113 },
114 queryAll() {
115 this.queryConfig();
116 this.queryUser();
117 this.queryProject();
118 this.queryTag();
119 },
120 },
121 });
122 export default useSelectionStore;
1 import { Project } from '@/types/project';
2 import { Tag } from '@/types/tag';
3 import { SystemConfig } from '@/types/system-config';
4 import { User } from '@/utils/model';
5
6 export interface Selection {
7 user: User[];
8 project: Pick<Project, 'id' | 'name'>[];
9 tag: Pick<Tag, 'id' | 'name' | 'type'>[];
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 push_type: ['tag', 'user'];
14 push_user: number[];
15 }
16
17 export interface ActivityApplyRecord {
18 id: number;
19 user: User;
20 created_at: string;
21 audit_msg: string;
22 }
23
24 export interface ActivityApply {
25 id: number;
26 cover: string;
27 song_name: string;
28 song_type: number;
29 sub_title?: string;
30 lang: string[];
31 speed: string;
32 is_official: number;
33 mark: string;
34 lyric: string;
35 clip_lyric: string;
36 sex: string;
37 project_id: number;
38 estimate_release_at: string;
39 audit_status: number;
40 record_count?: number;
41 is_push: number;
42 expand: ActivityExpand;
43 project?: Project;
44 tags?: Tag[];
45 user?: User;
46 out_side_manages?: { user_id: number; permission: string[] }[];
47 apply_records?: ActivityApplyRecord[];
48 last_record?: ActivityApplyRecord;
49 created_form: number;
50 weight: number;
51 }
52
53 export type ActivityApplyFormStep1 = Pick<
54 ActivityApply,
55 'cover' | 'song_name' | 'song_type' | 'sub_title' | 'lang' | 'speed' | 'weight'
56 > & {
57 expand: Pick<ActivityExpand, 'tag_ids'>;
58 };
59
60 export type ActivityApplyFormStep2 = Pick<
61 ActivityApply,
62 'project_id' | 'sex' | 'estimate_release_at' | 'is_official' | 'mark' | 'out_side_manages'
63 > & {
64 expand: Pick<ActivityExpand, 'lyricist' | 'composer' | 'arranger'>;
65 };
66
67 export type ActivityApplyFormStep3 = Pick<ActivityApply, 'song_name' | 'lyric' | 'clip_lyric'> & {
68 expand: Pick<ActivityExpand, 'guide_source' | 'karaoke_source' | 'track_source'>;
69 };
70
71 export type ActivityApplyFormStep4 = Pick<ActivityApply, 'is_push'> & {
72 expand: Pick<ActivityExpand, 'push_type' | 'push_user'>;
73 };
74
75 export type ActivityApplyForm = ActivityApplyFormStep1 & ActivityApplyFormStep2 & ActivityApplyFormStep3 & ActivityApplyFormStep4;
1 import { Tag } from '@/types/tag';
2 import { Project } from '@/types/project';
3
4 export interface ActivityRegister {
5 id: number;
6 song_name: string;
7 sub_title: string;
8 cover: string;
9 user_id: number;
10 project_id: number;
11 guide: string;
12 karaoke: string;
13 guide_source?: string;
14 karaoke_source?: string;
15 lyric: string;
16 clip_lyric: string;
17 is_official: number;
18 is_recommend: number;
19 weight: number;
20 status: number;
21 created_at: string;
22 tags?: Tag[];
23 project?: Project;
24 record?: {
25 cover: string;
26 song_name: string;
27 sub_title: string;
28 tagIds: number[];
29 project_id: number;
30 guide: string;
31 karaoke: string;
32 lyric: string;
33 clip_lyric: string;
34 };
35 }
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 sing_type: '' | 'Full' | 'Part';
13 activity_id: number;
14 activity_status: number;
15 user_id: number;
16 open_id: string;
17 user?: Singer;
18 activity?: Activity;
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 { Admin } from '@/types/admin';
4 import { FileStatus } from '@arco-design/web-vue/es/upload/interfaces';
5 import { User } from '@/types/user';
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?: User;
42 audit_user_id: number;
43 audit_msg: string;
44 current?: {
45 cover: string;
46 song_name: string;
47 sub_title: string;
48 tagIds: number[];
49 project_id: number;
50 guide: string;
51 karaoke: string;
52 lyric: string;
53 clip_lyric: string;
54 };
55 created_at?: string;
56 updated_at?: string;
57 }
58
59 export interface ActivityExpand {
60 tag_ids: number[];
61 lyricist: { ids: number[]; supplement: string[] };
62 composer: { ids: number[]; supplement: string[] };
63 arranger: { ids: number[]; supplement: string[] };
64 guide_source: { name: string; url: string; size: number };
65 karaoke_source: { name: string; url: string; size: number };
66 track_source: { name: string; url: string; size: number };
67 }
68
69 export interface Activity {
70 id: number;
71 song_name: string;
72 sub_title: string;
73 cover: string;
74 user_id: number;
75 project_id: number;
76 guide: string;
77 guide_source?: string;
78 karaoke: string;
79 karaoke_source?: string;
80 lyric: string;
81 clip_lyric: string;
82 weight: number;
83 audit_status: number;
84 status: number;
85 created_at: string;
86 updated_at: string;
87 submit_works_count?: number;
88 views_count?: number;
89 collections_count?: number;
90 send_url?: ActivitySendLink[];
91 user?: Admin;
92 project?: Project;
93 tags?: Tag[];
94 materials?: ActivityMaterial[];
95 recommend_intros?: ActivityRecommendIntro[];
96 apply_records?: ActivityApplyRecord[];
97 is_push?: 0 | 1;
98 is_official?: 0 | 1;
99 is_recommend?: 0 | 1;
100 push_type?: number[];
101
102 listen_count?: number;
103 like_count?: number;
104 submit_count?: number;
105
106 estimate_release_at?: string;
107 expand?: ActivityExpand;
108 links?: User[];
109 publish_at: string;
110 lang: string[];
111 sex: string;
112 }
113
114 export type ActivityViewUser = Pick<User, 'id' | 'avatar' | 'nick_name' | 'real_name' | 'sex' | 'role'> & {
115 listen_count: number;
116 collection_count: number;
117 submit_work: number;
118 last_listen_at: number;
119 };
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 company: string;
7 activities_count?: number;
8 singers_count?: number;
9 submit_activities_count?: number;
10 accept_activities_count?: number;
11 checked_activities_count?: number;
12 }
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 singers_count?: number;
5 submit_activities_count?: number;
6 checked_activities_count?: number;
7 }
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 Option {
6 value: unknown;
7 label: string;
8 disabled?: boolean;
9 }
10
11 export interface QueryForParams {
12 [key: string]: unknown;
13
14 setColumn?: string[];
15 setWith?: string | string[];
16 setSort?: string | string[];
17 querySelect?: string[];
18 withRelation?: string[];
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 QueryForPaginationRes<T = object> {
35 list: T[];
36 current: number;
37 pageSize: number;
38 total: number;
39 }
40
41 export interface Pagination {
42 current: number;
43 pageSize: number;
44 total: number;
45 showTotal?: boolean;
46 showPageSize?: boolean;
47 pageSizeOptions?: number[];
48 }
49
50 export type SortType = 'descend' | 'ascend' | '' | string;
51
52 export interface Sort {
53 column: string;
54 type: SortType | string;
55 }
56
57 export interface HttpResponse<T = unknown> {
58 status: 'success' | 'fail' | 'error';
59 msg: string;
60 code: number;
61 data: T;
62 }
63
64 export interface ServiceResponse<T = unknown> extends HttpResponse<T> {
65 meta: { current: number; limit: number; total: number };
66 }
1 import { Admin } from '@/types/admin';
2
3 export interface OperateLog {
4 id: number;
5 user?: Admin;
6 action: string;
7 content: string;
8 created_at: string;
9 }
1 // eslint-disable-next-line import/no-cycle
2 import { Admin } from '@/types/admin';
3
4 export interface Project {
5 id: number;
6 cover: string;
7 name: string;
8 intro: string;
9 user?: Admin;
10 status: number;
11 is_promote?: number;
12 is_public?: number;
13 created_at?: string;
14 updated_at?: string;
15 up_count?: number;
16 down_count?: number;
17 finish_count?: number;
18 managers_count?: number;
19 activities_count?: number;
20 send_count?: number;
21 managers?: Admin[];
22
23 activity_count?: number;
24 activity_up_count?: number;
25 activity_match_count?: number;
26 activity_down_count?: number;
27 activity_send_count?: number;
28 manage_count?: number;
29 }
1 import { Business } from '@/types/business';
2 import { User } from '@/types/user';
3
4 export interface Singer extends User {
5 business_id?: number;
6 business?: Business;
7 submit_activities_count?: number;
8 accept_activities_count?: number;
9 }
1 import { User } from '@/types/user';
2
3 export interface Square {
4 title: string;
5 is_top: 0 | 1;
6 status: 0 | 1;
7 type?: number;
8 created_at?: string;
9 deleted_at?: string | null;
10 view_count?: number;
11 interact_count?: number;
12 user?: User;
13
14 attach_users?: [];
15 attach_activities?: [];
16 attach_images?: [];
17 attach_musics?: [];
18 }
19
20 export interface SquareImage {
21 id: number;
22 value: string;
23 }
24
25 export interface SquareComment {
26 id: number;
27 content: string;
28 created_at: string;
29 }
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 frame?: object;
8 status: number;
9 created_at: string;
10 updated_at: string;
11 user?: Admin;
12 }
1 export interface SystemConfig {
2 id: number;
3 parent_id: number;
4 name: string;
5 content: string;
6 remark: string;
7 status: 1 | 0;
8 weight: number;
9 identifier: string;
10 children?: SystemConfig[];
11 expand: {
12 type: string;
13 [key: string]: unknown;
14 };
15 }
1 export interface SystemPermission {
2 id: number;
3 name: string;
4 guard: 'Admin' | 'Manage' | 'App';
5 type: 'Menu' | 'Button';
6 label: string;
7 icon?: string;
8 is_show_line?: 1 | 0;
9 parent_id: number;
10 weight: number;
11 parent?: SystemPermission;
12 children?: SystemPermission[];
13 created_at: string;
14 updated_at: string;
15 }
1 // eslint-disable-next-line import/no-cycle
2 import { User } from '@/types/user';
3 import { SystemPermission } from '@/types/system-permission';
4
5 export interface SystemRole {
6 id: number;
7 name: string;
8 guard: '' | 'Admin' | 'Manage';
9 status: 0 | 1;
10 intro?: string;
11 user_id?: number;
12 user?: User;
13 permissions?: SystemPermission[];
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 weight: number;
9 is_show: number;
10 index_filter: number;
11 created_at: string;
12 updated_at: string;
13 user?: Admin;
14 expand?: {
15 permission?: string[];
16 };
17 }
1 import { User } from '@/types/user';
2
3 export interface UserGroup {
4 id: number;
5 title: string;
6 intro: string;
7 user_id: number;
8 status: 0 | 1;
9 user?: User;
10 created_at: string;
11 }
1 import { User } from '@/types/user';
2 import { AttributeData } from '@/types/global';
3
4 export interface UserRegister {
5 id: number;
6 role: 'Singer' | 'Business';
7 nick_name: string;
8 real_name: string;
9 phone: string;
10 company: string;
11 remark: string;
12 sound: string;
13 referrer_id: number;
14 business_id: number;
15 admin_id: number;
16 examine_id: number;
17 status: 0 | 1 | 2;
18 audit_info: string;
19 examine_at: string;
20 audit_at: string;
21 admin?: User;
22 business?: User;
23 }
24
25 export interface UserRegisterAuditInfo extends AttributeData {
26 status: number;
27 business_id: number;
28 examine_info: string;
29 }
30
31 export interface UserRegisterRecord {
32 id: number;
33 admin_id: number;
34 examine_info: string;
35 updated_at: string;
36 admin?: User;
37 }
1 // eslint-disable-next-line import/no-cycle
2 import { Project } from '@/types/project';
3
4 // eslint-disable-next-line import/no-cycle
5 import { Tag } from '@/types/tag';
6 // eslint-disable-next-line import/no-cycle
7 import { SystemRole } from '@/types/system-role';
8
9 export interface User {
10 id: number;
11 business_id?: number;
12 open_id?: string;
13 avatar?: string;
14 nick_name: string;
15 real_name: string;
16 phone?: string;
17 email?: string;
18 intro?: string;
19 company?: string;
20 rate?: string;
21 province?: string;
22 city?: string;
23 sound?: string;
24 role?: 'Singer' | 'Business';
25 sex?: number;
26 scope?: number;
27 like_activities_count?: number;
28 status?: number;
29 official_status?: number;
30 audit_status?: number;
31 last_login?: string;
32 created_at?: string;
33 updated_at?: string;
34 projects?: Project[];
35 manage_projects?: Project[];
36 roles?: SystemRole[];
37 styles?: Tag[];
38 tags?: Tag[];
39 voices?: Tag[];
40 skills?: Tag[];
41 identities?: Tag[];
42 business?: User;
43 style_tags?: Tag[];
44 auth_tags?: Tag[];
45 identity: number;
46 area_code: string;
47 }
1 import RabbitLyrics from 'rabbit-lyrics';
2 import parseLyrics from 'rabbit-lyrics/src/parseLyrics';
3
4 // @ts-ignore
5 export default class AudioSyncLyric extends RabbitLyrics {
6 public startTime = 0;
7
8 public setStartTime(time: number) {
9 this.startTime = time || 0;
10 this.render();
11 this.mediaElement.addEventListener('timeupdate', this.synchronize);
12 }
13
14 private render(): void {
15 // Add class names
16 this.lyricsElement.classList.add('rabbit-lyrics');
17 this.lyricsElement.classList.add(`rabbit-lyrics--${this.viewMode}`);
18 this.lyricsElement.classList.add(`rabbit-lyrics--${this.alignment}`);
19 this.lyricsElement.textContent = null;
20
21 // Render lyrics lines
22 this.lyricsLines = parseLyrics(this.lyrics).map((line) => {
23 const lineElement = document.createElement('div');
24 lineElement.className = 'rabbit-lyrics__line';
25 lineElement.addEventListener('click', () => {
26 this.mediaElement.currentTime = line.startsAt - this.startTime;
27 this.synchronize();
28 });
29 const lineContent = line.content.map((inline) => {
30 const inlineElement = document.createElement('span');
31 inlineElement.className = 'rabbit-lyrics__inline';
32 inlineElement.textContent = inline.content;
33 lineElement.append(inlineElement);
34 return { ...inline, element: inlineElement };
35 });
36 this.lyricsElement.append(lineElement);
37 return { ...line, content: lineContent, element: lineElement };
38 });
39 this.synchronize();
40 }
41
42 private synchronize = () => {
43 const time = this.startTime + this.mediaElement.currentTime;
44 let changed = false; // If here are active lines changed
45 const activeLines = this.lyricsLines.filter((line) => {
46 if (time >= line.startsAt && time < line.endsAt) {
47 // If line should be active
48 if (!line.element.classList.contains('rabbit-lyrics__line--active')) {
49 // If it hasn't been activated
50 changed = true;
51 line.element.classList.add('rabbit-lyrics__line--active');
52 }
53 line.content.forEach((inline) => {
54 if (time >= inline.startsAt) {
55 inline.element.classList.add('rabbit-lyrics__inline--active');
56 } else {
57 inline.element.classList.remove('rabbit-lyrics__inline--active');
58 }
59 });
60 return true;
61 }
62 // If line should be inactive
63 if (line.element.classList.contains('rabbit-lyrics__line--active')) {
64 // If it hasn't been deactivated
65 changed = true;
66 line.element.classList.remove('rabbit-lyrics__line--active');
67 line.content.forEach((inline) => {
68 inline.element.classList.remove('rabbit-lyrics__inline--active');
69 });
70 }
71 return false;
72 });
73
74 if (changed && activeLines.length > 0) {
75 // Calculate scroll top. Vertically align active lines in middle
76 const activeLinesOffsetTop =
77 (activeLines[0].element.offsetTop +
78 activeLines[activeLines.length - 1].element.offsetTop +
79 activeLines[activeLines.length - 1].element.offsetHeight) /
80 2;
81 this.lyricsElement.scrollTop = activeLinesOffsetTop - this.lyricsElement.clientHeight / 2;
82 }
83 };
84 }
1 const isLogin = () => {
2 return !!localStorage.getItem('access_token');
3 };
4
5 const getToken = () => {
6 return localStorage.getItem('access_token') || localStorage.getItem('refresh_token');
7 };
8
9 const setToken = (access_token: string, refresh_token: string) => {
10 localStorage.setItem('access_token', access_token);
11 localStorage.setItem('refresh_token', refresh_token);
12 };
13
14 const getRefreshToken = () => {
15 return localStorage.getItem('refresh_token');
16 };
17
18 const clearToken = () => {
19 localStorage.removeItem('access_token');
20 localStorage.removeItem('refresh_token');
21 };
22
23 export { isLogin, getToken, setToken, getRefreshToken, clearToken };
1 import { AnyObject } from '@/types/global';
2 import { createVNode, Ref, VNode } from 'vue';
3 import { Form, FormItem, Input, InputNumber, Textarea, Modal, Select } from '@arco-design/web-vue';
4 import InputUpload from '@/components/input-upload/index.vue';
5
6 import { RenderContent } from '@arco-design/web-vue/es/_utils/types';
7 import { ModalConfig } from '@arco-design/web-vue/es/modal/interface';
8 import { set } from 'lodash';
9
10 export function createModalVNode(content: RenderContent, props?: object & Omit<ModalConfig, 'content'>) {
11 return Modal.open({
12 content,
13 titleAlign: 'start',
14 closable: false,
15 escToClose: false,
16 maskClosable: false,
17 okButtonProps: { size: 'small' },
18 cancelButtonProps: { size: 'small' },
19 ...props,
20 });
21 }
22
23 export function createFormVNode(props?: AnyObject, children?: VNode | VNode[] | undefined): VNode {
24 return createVNode(Form, { autoLabelWidth: true, ...props }, () => children);
25 }
26
27 export function createFormItemVNode(props?: AnyObject, children?: string | VNode | VNode[]): VNode {
28 return createVNode(FormItem, { showColon: true, ...props }, () => children);
29 }
30
31 export function createSelectVNode(value: Ref, options: AnyObject[], props?: AnyObject): VNode {
32 return createVNode(Select, {
33 options,
34 'modelValue': value.value,
35 'placeholder': '请选择',
36 'onUpdate:modelValue': (val?: unknown) => set(value, 'value', val),
37 ...props,
38 });
39 }
40
41 export function createSelectionFormItemVNode(value: Ref, options: AnyObject[], itemProps?: AnyObject, props?: AnyObject): VNode {
42 return createFormItemVNode(itemProps, createSelectVNode(value, options, props));
43 }
44
45 export function createInputVNode(value: Ref, props?: AnyObject): VNode {
46 return createVNode(Input, {
47 'modelValue': value.value,
48 'placeholder': '请输入',
49 'onUpdate:modelValue': (val?: string) => set(value, 'value', val || ''),
50 ...props,
51 });
52 }
53
54 export function createInputFormItemVNode(value: Ref, itemProps?: AnyObject, props?: AnyObject): VNode {
55 return createFormItemVNode(itemProps, createInputVNode(value, props));
56 }
57
58 export function createInputNumberVNode(value: Ref, props?: AnyObject): VNode {
59 return createVNode(InputNumber, {
60 'modelValue': value.value,
61 'placeholder': '请输入',
62 'onUpdate:modelValue': (val?: string) => set(value, 'value', val || 0),
63 'min': 0,
64 'max': 255,
65 ...props,
66 });
67 }
68
69 export function createInputNumberFormItemVNode(value: Ref, itemProps?: AnyObject, props?: AnyObject): VNode {
70 return createFormItemVNode(itemProps, createInputNumberVNode(value, props));
71 }
72
73 export function createTextareaVNode(value: Ref, props?: AnyObject) {
74 return createVNode(Textarea, {
75 'modelValue': value.value,
76 'placeholder': '请输入',
77 'onUpdate:modelValue': (val?: string) => set(value, 'value', val || 0),
78 'max-length': 500,
79 'show-word-limit': true,
80 'auto-size': { minRows: 3, maxRows: 6 },
81 ...props,
82 });
83 }
84
85 export function createTextareaFormItemVNode(value: Ref, itemProps?: AnyObject, props?: AnyObject) {
86 return createFormItemVNode(itemProps, createTextareaVNode(value, props));
87 }
88
89 export function createInputUploadVNode(value: Ref, props?: AnyObject): VNode {
90 return createVNode(InputUpload, {
91 'modelValue': value.value,
92 'placeholder': '请选择',
93 'onUpdate:modelValue': (val?: string) => set(value, 'value', val || 0),
94 ...props,
95 });
96 }
97
98 export function createInputUploadFormItemVNode(value: Ref, itemProps?: AnyObject, props?: AnyObject): VNode {
99 return createFormItemVNode(itemProps, createInputUploadVNode(value, props));
100 }
1 export const isProduction = process.env.NODE_ENV === 'production';
2
3 export const debug = !isProduction;
4
5 export const defaultProjectCover = 'https://hisin.oss-cn-hangzhou.aliyuncs.com/tmp/project_cover.png';
1 export function addEventListen(target: Window | HTMLElement, event: string, handler: EventListenerOrEventListenerObject, capture = false) {
2 if (target.addEventListener && typeof target.addEventListener === 'function') {
3 target.addEventListener(event, handler, capture);
4 }
5 }
6
7 export function removeEventListen(
8 target: Window | HTMLElement,
9 event: string,
10 handler: EventListenerOrEventListenerObject,
11 capture = false
12 ) {
13 if (target.removeEventListener && typeof target.removeEventListener === 'function') {
14 target.removeEventListener(event, handler, capture);
15 }
16 }
1 import { createVNode, VNode } from 'vue';
2 import { Form, FormItem, Modal } from '@arco-design/web-vue';
3 import { AnyObject } from '@/types/global';
4 import { RenderContent } from '@arco-design/web-vue/es/_utils/types';
5 import { ModalConfig } from '@arco-design/web-vue/es/modal/interface';
6 import { compact, get } from 'lodash';
7
8 type TargetContext = '_self' | '_parent' | '_blank' | '_top';
9
10 export const openWindow = (url: string, opts?: { target?: TargetContext; [key: string]: any }) => {
11 const { target = '_blank', ...others } = opts || {};
12 window.open(
13 url,
14 target,
15 Object.entries(others)
16 .reduce((preValue: string[], curValue) => {
17 const [key, value] = curValue;
18 return [...preValue, `${key}=${value}`];
19 }, [])
20 .join(',')
21 );
22 };
23
24 export const regexUrl = new RegExp(
25 '^(?!mailto:)(?:(?:http|https|ftp)://)(?:\\S+(?::\\S*)?@)?(?:(?:(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}(?:\\.(?:[0-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))|(?:(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)(?:\\.(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))|localhost)(?::\\d{2,5})?(?:(/|\\?|#)[^\\s]*)?$',
26 'i'
27 );
28
29 export const createFormVNode = (props: AnyObject, children: VNode | VNode[]) =>
30 createVNode(Form, { autoLabelWidth: true, ...props }, () => children);
31
32 export const createFormItemVNode = (props: AnyObject, children: VNode | VNode[]) =>
33 createVNode(FormItem, props, { default: () => children });
34
35 export const createModalVNode = (content: RenderContent, props: Omit<ModalConfig, 'content'>) =>
36 Modal.open({ content, closable: false, maskClosable: false, escToClose: false, ...props });
37
38 export const arrayToTree = (list: any[], parentId: unknown = 0, config = {}) => {
39 const idKey = get(config, 'idKey', 'id');
40 const parentKey = get(config, 'parentKey', 'parent_id');
41 const childrenKey = get(config, 'childrenKey', 'children');
42 const removeEmpty = get(config, 'removeEmpty', false);
43
44 return list
45 .filter((item) => item[parentKey] === parentId)
46 .map((item) => {
47 item[childrenKey] = arrayToTree(list, item[idKey], config);
48 if (removeEmpty && item[childrenKey].length === 0) {
49 delete item[childrenKey];
50 }
51 return item;
52 });
53 };
54
55 export const findTreeParentIds = (nodeData: AnyObject[], parentId: any, config = {}): any[] => {
56 const ids: any[] = [];
57 const idKey = get(config, 'idKey', 'id');
58 const parentKey = get(config, 'parentKey', 'parent_id');
59
60 nodeData
61 ?.filter((item) => item[idKey] === parentId)
62 ?.forEach((item) => ids.push(item[idKey], ...findTreeParentIds(nodeData, item[parentKey])));
63
64 return ids;
65 };
66
67 export const findTreeChildIds = (nodeData: AnyObject[], nodeId: any, config = {}): any[] => {
68 const ids: any[] = [];
69 const idKey = get(config, 'idKey', 'id');
70 const parentKey = get(config, 'parentKey', 'parent_id');
71
72 nodeData
73 ?.filter((item) => item[parentKey] === nodeId)
74 ?.forEach((item) => ids.push(item[idKey], ...findTreeChildIds(nodeData, item[idKey])));
75
76 return ids;
77 };
78
79 export const bytesForHuman = (bytes: number, decimals = 2) => {
80 const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
81
82 let i = 0;
83
84 // eslint-disable-next-line no-plusplus
85 for (i; bytes > 1024; i++) {
86 bytes /= 1024;
87 }
88
89 return `${parseFloat(bytes.toFixed(decimals))} ${units[i]}`;
90 };
91
92 export const audioBufferToWav = (audioBuffer: AudioBuffer, len: number) => {
93 const numOfChan = audioBuffer.numberOfChannels;
94 const length = len * numOfChan * 2 + 44;
95 const buffer = new ArrayBuffer(length);
96 const view = new DataView(buffer);
97 const channels: Float32Array[] = [];
98 let i: number;
99 let sample: number;
100 let offset = 0;
101 let pos = 0;
102
103 // write WAVE header
104 // eslint-disable-next-line no-use-before-define
105 setUint32(0x46464952); // "RIFF"
106 // eslint-disable-next-line no-use-before-define
107 setUint32(length - 8); // file length - 8
108 // eslint-disable-next-line no-use-before-define
109 setUint32(0x45564157); // "WAVE"
110
111 // eslint-disable-next-line no-use-before-define
112 setUint32(0x20746d66); // "fmt " chunk
113 // eslint-disable-next-line no-use-before-define
114 setUint32(16); // length = 16
115 // eslint-disable-next-line no-use-before-define
116 setUint16(1); // PCM (uncompressed)
117 // eslint-disable-next-line no-use-before-define
118 setUint16(numOfChan);
119 // eslint-disable-next-line no-use-before-define
120 setUint32(audioBuffer.sampleRate);
121 // eslint-disable-next-line no-use-before-define
122 setUint32(audioBuffer.sampleRate * 2 * numOfChan); // avg. bytes/sec
123 // eslint-disable-next-line no-use-before-define
124 setUint16(numOfChan * 2); // block-align
125 // eslint-disable-next-line no-use-before-define
126 setUint16(16); // 16-bit (hardcoded in this demo)
127
128 // eslint-disable-next-line no-use-before-define
129 setUint32(0x61746164); // "data" - chunk
130 // eslint-disable-next-line no-use-before-define
131 setUint32(length - pos - 4); // chunk length
132
133 // write interleaved data
134 // eslint-disable-next-line no-plusplus
135 for (i = 0; i < audioBuffer.numberOfChannels; i++) {
136 // @ts-ignore
137 channels.push(audioBuffer.getChannelData(i));
138 }
139
140 while (pos < length) {
141 // eslint-disable-next-line no-plusplus
142 for (i = 0; i < numOfChan; i++) {
143 // interleave channels
144 sample = Math.max(-1, Math.min(1, channels[i][offset])); // clamp
145 // eslint-disable-next-line no-bitwise
146 sample = (0.5 + sample < 0 ? sample * 32768 : sample * 32767) | 0; // scale to 16-bit signed int
147 view.setInt16(pos, sample, true); // write 16-bit sample
148 pos += 2;
149 }
150 // eslint-disable-next-line no-plusplus
151 offset++; // next source sample
152 }
153
154 // create Blob
155 return new Blob([buffer], { type: 'audio/wav' });
156
157 function setUint16(data: number) {
158 view.setUint16(pos, data, true);
159 pos += 2;
160 }
161
162 function setUint32(data: number) {
163 view.setUint32(pos, data, true);
164 pos += 4;
165 }
166 };
167
168 export const getLyricTimeArr = (lyric: string): string[] => {
169 const times: string[] = [];
170
171 lyric?.split('\n').forEach((item) => {
172 item = item.replace(/(^\s*)|(\s*$)/g, '');
173 times.push(item.substring(item.indexOf('[') + 1, item.indexOf(']')));
174 });
175
176 return compact(times);
177 };
178
179 export const promiseToBoolean = (callback: Promise<any> | undefined) => callback?.then(() => true).catch(() => false) || false;
180
181 export default null;
1 const opt = Object.prototype.toString;
2
3 export function isArray(obj: any): obj is any[] {
4 return opt.call(obj) === '[object Array]';
5 }
6
7 export function isObject(obj: any): obj is { [key: string]: any } {
8 return opt.call(obj) === '[object Object]';
9 }
10
11 export function isString(obj: any): obj is string {
12 return opt.call(obj) === '[object String]';
13 }
14
15 export function isNumber(obj: any): obj is number {
16 return opt.call(obj) === '[object Number]' && obj === obj; // eslint-disable-line
17 }
18
19 export function isRegExp(obj: any) {
20 return opt.call(obj) === '[object RegExp]';
21 }
22
23 export function isFile(obj: any): obj is File {
24 return opt.call(obj) === '[object File]';
25 }
26
27 export function isBlob(obj: any): obj is Blob {
28 return opt.call(obj) === '[object Blob]';
29 }
30
31 export function isUndefined(obj: any): obj is undefined {
32 return obj === undefined;
33 }
34
35 export function isNull(obj: any): obj is null {
36 return obj === null;
37 }
38
39 export function isFunction(obj: any): obj is (...args: any[]) => any {
40 return typeof obj === 'function';
41 }
42
43 export function isEmptyObject(obj: any): boolean {
44 return isObject(obj) && Object.keys(obj).length === 0;
45 }
46
47 export function isExist(obj: any): boolean {
48 return obj || obj === 0;
49 }
50
51 export function isWindow(el: any): el is Window {
52 return el === window;
53 }
1 import { Tag } from '@/types/tag';
2 import { SystemRole } from '@/types/system-role';
3 import { Activity } from '@/types/activity';
4 import { AnyObject } from '@/types/global';
5
6 export interface BaseUser {
7 id: number;
8 avatar: string;
9 nick_name: string;
10 real_name: string;
11 identity: number;
12 }
13
14 export interface User extends BaseUser {
15 cover: string;
16 sex: number;
17 email: string;
18 lang: string;
19 area_code: string;
20 phone: string;
21 created_at: string;
22 scope: number;
23 demo_type: number;
24 user_tag_id: number;
25 official_status: number;
26 status: number;
27 company: string;
28 province: string;
29 city: string;
30 intro: string;
31
32 register_type?: number;
33 inviter?: any;
34 register_remark?: string;
35
36 business_singer_limit: number;
37
38 business?: BaseUser & { [K in keyof User]: User[K] };
39 auth_tags?: Pick<Tag, 'id' | 'name'>[];
40 style_tags?: Pick<Tag, 'id' | 'name'>[];
41
42 // eslint-disable-next-line no-use-before-define
43 projects?: Project[];
44 roles?: SystemRole[];
45 // eslint-disable-next-line no-use-before-define
46 manage_projects?: Project[];
47 opus?: Activity[];
48 expand?: {
49 recommend?: {
50 activity?: string;
51 };
52 };
53 }
54
55 export interface UserDynamics {
56 id: number;
57 type: 'image' | 'audio' | 'video';
58 intro: string;
59 properties: object;
60 is_top: number;
61 created_at: string;
62 updated_at: string;
63 }
64
65 export interface UserImageDynamics extends UserDynamics {
66 properties: {
67 url: string;
68 [key: string]: unknown;
69 };
70 }
71
72 export interface UserAudioDynamics extends UserDynamics {
73 properties: {
74 cover: {
75 url: string;
76 width: number;
77 height: number;
78 };
79 url: string;
80 [key: string]: unknown;
81 };
82 }
83
84 export interface UserManageActivity {
85 id: number;
86 user: Pick<
87 User,
88 | 'id'
89 | 'avatar'
90 | 'nick_name'
91 | 'real_name'
92 | 'area_code'
93 | 'phone'
94 | 'email'
95 | 'sex'
96 | 'status'
97 | 'register_type'
98 | 'official_status'
99 | 'inviter'
100 | 'register_remark'
101 >;
102 permission: string[];
103 activity_id: number;
104 activity_name: string;
105 activity_publish_at: string;
106 activity_status: string;
107 }
108
109 export interface UserSubmitActivity {
110 activity_cover: string;
111 activity_name: string;
112 activity_title: string;
113 activity_status: number;
114 status: number;
115 sing_type: string;
116 mode: number;
117 demo_url: string;
118 submit_at: string;
119 // eslint-disable-next-line no-use-before-define
120 project?: Project;
121 tags?: Tag[];
122 }
123
124 export interface UserSubmitPrice {
125 id: number;
126 value: {
127 year: string;
128 ratio: number;
129 amounts: number;
130 is_reward: 1 | 0;
131 is_dividend: 1 | 0;
132 };
133 is_deduct: 1 | 0;
134 is_talk: 1 | 0;
135 is_accept_address: 1 | 0;
136 address: {
137 name: string;
138 parent: {
139 name: string;
140 };
141 };
142 }
143
144 export interface UserMember {
145 id: number;
146 role: string;
147 is_top: number;
148 status: number;
149 member_id: number;
150 member: User;
151 }
152
153 interface UserCertifyRecord {
154 id: number;
155 status: number;
156 reason?: string;
157 operator_id: number;
158 operator?: BaseUser & { [K in keyof User]: User[K] };
159 updated_at: string;
160 }
161
162 export interface UserCertify {
163 id: number;
164 user_id: number;
165 user?: Pick<
166 User,
167 | 'id'
168 | 'avatar'
169 | 'nick_name'
170 | 'real_name'
171 | 'sex'
172 | 'identity'
173 | 'auth_tags'
174 | 'official_status'
175 | 'inviter'
176 | 'register_remark'
177 | 'register_type'
178 >;
179 nick_name: string;
180 register_type?: number;
181 inviter?: any;
182 register_remark?: string;
183 works: string;
184 reason?: string;
185 audio_info?: {
186 local?: string;
187 online?: string;
188 };
189 img: string[];
190 platform: string[];
191 tags: number[];
192 status: number;
193 records: UserCertifyRecord[];
194 created_at: string;
195 }
196
197 export interface BannerLink {
198 title: string;
199 cover: string;
200 content: string;
201 link_id: number;
202 link_type: 1 | 2 | 3;
203 link_name: string;
204 desc1: string;
205 desc2: string;
206 desc3: string;
207 }
208
209 export interface Banner {
210 id: number;
211 name: string;
212 cover: string;
213 type: number;
214 scope: number;
215 permission: string[];
216 content_picture: string;
217 content: BannerLink[];
218 status: number;
219 weight: number;
220 }
221
222 export interface Project {
223 id: number;
224 name: string;
225 cover: string;
226 head_cover: string;
227 intro: string;
228 master_id: number;
229 is_promote: number;
230 is_public: number;
231 is_can_apply: number;
232 is_can_manage: number;
233 is_can_demo_apply: number;
234 is_demo_audit: number;
235 status: number;
236
237 user?: BaseUser & { [K in keyof User]: User[K] };
238 master?: BaseUser & { [K in keyof User]: User[K] };
239
240 // eslint-disable-next-line no-use-before-define
241 managers: User[];
242
243 activity_count?: number;
244 activity_up_count?: number;
245 activity_match_count?: number;
246 activity_send_count?: number;
247
248 demo_count?: number;
249 demo_up_count?: number;
250 }
251
252 export interface Notification {
253 id: number;
254 title: string;
255 type: number;
256 content: string;
257 cover: string;
258 publish_type: number;
259 publish_at?: string;
260 publish_to: string[];
261 link_type: 'none' | 'user' | 'activity' | 'project' | 'rich';
262 link_id: number;
263 is_alert: number;
264 rich_content?: string;
265 status: number;
266
267 items_count?: number;
268 }
1 import { App, ComponentPublicInstance } from 'vue';
2 import axios from 'axios';
3
4 export default function handleError(Vue: App, baseUrl: string) {
5 if (!baseUrl) {
6 return;
7 }
8 Vue.config.errorHandler = (err: unknown, instance: ComponentPublicInstance | null, info: string) => {
9 // send error info
10 axios.post(`${baseUrl}/report-error`, {
11 err,
12 instance,
13 info,
14 // location: window.location.href,
15 // message: err.message,
16 // stack: err.stack,
17 // browserInfo: getBrowserInfo(),
18 // user info
19 // dom info
20 // url info
21 // ...
22 });
23 };
24 }
1 /**
2 * 跳转时,新打开标签页
3 */
4
5 export default function openNewTab(router: any, name: string, params: object) {
6 const routeUrl = router.resolve({
7 name,
8 params,
9 });
10 window.open(routeUrl.href, '_blank');
11 }
1 /**
2 * Listening to routes alone would waste rendering performance. Use the publish-subscribe model for distribution management
3 * 单独监听路由会浪费渲染性能。使用发布订阅模式去进行分发管理。
4 */
5 import mitt, { Handler } from 'mitt';
6 import type { RouteLocationNormalized } from 'vue-router';
7
8 const emitter = mitt();
9
10 const key = Symbol('ROUTE_CHANGE');
11
12 let latestRoute: RouteLocationNormalized;
13
14 export function setRouteEmitter(to: RouteLocationNormalized) {
15 emitter.emit(key, to);
16 latestRoute = to;
17 }
18
19 export function listenerRouteChange(handler: (route: RouteLocationNormalized) => void, immediate = true) {
20 emitter.on(key, handler as Handler);
21 if (immediate && latestRoute) {
22 handler(latestRoute);
23 }
24 }
25
26 export function removeRouteListener() {
27 emitter.off(key);
28 }
1 <script setup lang="ts">
2 import { Space } from '@arco-design/web-vue';
3 import { computed, onMounted, ref } from 'vue';
4 import axios from 'axios';
5 import { audioBufferToWav } from '@/utils';
6 import AudioSyncLyric from '@/utils/audioSyncLyric';
7
8 const props = defineProps<{
9 src: string | File | ArrayBuffer;
10 lyric: string;
11 startWithLyric?: boolean;
12 startTime?: string;
13 endTime?: string;
14 }>();
15
16 const audioRef = ref();
17 const lyricRef = ref();
18
19 const source = ref();
20
21 const onPay = (e: any) => {
22 const audios = document.getElementsByTagName('audio');
23 [].forEach.call(audios, (i: HTMLAudioElement) => i !== e.target && i.pause());
24 };
25
26 const convertDurationToSeconds = (duration: string) => {
27 const timeArray = duration.split(':'); // 将时间字符串拆分为时、分、秒的数组
28
29 switch (timeArray.length) {
30 case 1:
31 return parseFloat(timeArray[0]);
32 case 2:
33 return parseInt(timeArray[0], 10) * 60 + parseFloat(timeArray[1]);
34 case 3:
35 return parseInt(timeArray[0], 10) * 3600 + parseInt(timeArray[1], 10) * 60 + parseFloat(timeArray[2]);
36 default:
37 return 0;
38 }
39 };
40
41 const startTime = computed((): number => convertDurationToSeconds(props.startTime ?? '00:00.00'));
42 const endTime = computed((): number | undefined => (props.endTime ? convertDurationToSeconds(props.endTime) : undefined));
43
44 const getResult = async (): Promise<ArrayBuffer> => {
45 if (props.src instanceof Blob) {
46 return props.src.arrayBuffer();
47 }
48 if (props.src instanceof ArrayBuffer) {
49 return Promise.resolve(props.src);
50 }
51
52 return axios
53 .get(`${props.src}?response-content-type=Blob`, { responseType: 'blob', timeout: 60000 })
54 .then(({ data }) => Promise.resolve(data.arrayBuffer()));
55 };
56
57 onMounted(async () => {
58 // eslint-disable-next-line no-new
59 new AudioSyncLyric(lyricRef.value, audioRef.value).setStartTime(startTime.value);
60 const result: ArrayBuffer = await getResult();
61
62 const audioCtx = new AudioContext();
63 const audioBuffer = await audioCtx.decodeAudioData(result);
64 const { numberOfChannels, sampleRate, duration } = audioBuffer;
65 const start = startTime.value; // 从第几秒开始复制
66 const end = endTime.value || duration; // 复制到第几秒结束
67
68 // eslint-disable-next-line no-bitwise
69 const startOffset = (start * sampleRate) >> 0; // 起始位置 = 开始时间 * 采样率
70 // eslint-disable-next-line no-bitwise
71 const endOffset = (end * sampleRate) >> 0; // 结束位置 = 结束时间 * 采样率
72 const frameCount = endOffset - startOffset; // 音频帧数/长度 = 结束位置 - 起始位置
73 const newAudioBuffer = audioCtx.createBuffer(numberOfChannels, frameCount, sampleRate);
74
75 // eslint-disable-next-line no-plusplus
76 for (let i = 0; i < numberOfChannels; i++) {
77 newAudioBuffer.getChannelData(i).set(audioBuffer.getChannelData(i).slice(startOffset, endOffset));
78 }
79
80 const blob = audioBufferToWav(newAudioBuffer, frameCount);
81 source.value = URL.createObjectURL(blob);
82 });
83 </script>
84
85 <template>
86 <Space direction="vertical" fill>
87 <audio ref="audioRef" :src="source" class="audio" controls controlsList="nodownload noplaybackrate" @play="onPay" />
88 <div ref="lyricRef" class="lyric">{{ lyric }}</div>
89 </Space>
90 </template>
91
92 <style scoped lang="less">
93 .audio {
94 height: 30px;
95 width: 100%;
96 outline: none;
97 }
98
99 .lyric {
100 border: none !important;
101 background-color: #f7f8fa;
102
103 :deep(.rabbit-lyrics__line) {
104 padding: 0.3em 1em !important;
105 }
106
107 :deep(.rabbit-lyrics__inline) {
108 color: #818181;
109 }
110
111 :deep(.rabbit-lyrics__inline.rabbit-lyrics__inline--active) {
112 font-size: 16px;
113 font-weight: 500;
114 color: black;
115 }
116 }
117 </style>
1 <template>
2 <Layout>
3 <Layout has-sider>
4 <LayoutSider :width="120" class="aside">
5 <Steps :current="currentStep" direction="vertical" :small="true">
6 <Step v-for="item in stepItems" :key="item.value">{{ item.label }}</Step>
7 </Steps>
8 </LayoutSider>
9 <LayoutContent class="main">
10 <component :is="currentContent" ref="formRef" v-model="formValue" :loading="loading" :filter-project="filterProject" />
11 </LayoutContent>
12 </Layout>
13 <LayoutFooter>
14 <Divider style="margin-top: 8px; margin-bottom: 20px" />
15 <Link :href="useSelectionStore().lyricTool" :hoverable="false" class="link-hover" icon>歌词制作工具</Link>
16 <Space style="float: right">
17 <IconButton v-show="currentStep !== 1" icon="left" label="上一步" @click="onPrev" />
18 <IconButton icon="right" icon-align="right" :loading="loading" :label="nextBtnLabel" @click="onNext" />
19 </Space>
20 </LayoutFooter>
21 </Layout>
22 </template>
23
24 <script setup lang="ts">
25 import { Layout, LayoutSider, LayoutContent, LayoutFooter, Step, Steps, Space, Divider, Link } from '@arco-design/web-vue';
26 import { computed, markRaw, ref } from 'vue';
27 import Step1FormContent from '@/views/audition/activity-apply/components/step1-form-content.vue';
28 import Step2FormContent from '@/views/audition/activity-apply/components/step2-form-content.vue';
29 import Step3FormContent from '@/views/audition/activity-apply/components/step3-form-content.vue';
30 import IconButton from '@/components/icon-button/index.vue';
31
32 import { useSelectionStore } from '@/store';
33
34 import { AnyObject } from '@/types/global';
35 import useLoading from '@/hooks/loading';
36 import { cloneDeep } from 'lodash';
37
38 const props = defineProps<{
39 initValue?: AnyObject;
40 filterProject?: (value: unknown) => boolean;
41 onSubmit: (data: AnyObject) => Promise<any>;
42 }>();
43
44 const { loading, setLoading } = useLoading(false);
45
46 const formRef = ref();
47 const formValue = ref({ ...cloneDeep(props.initValue) });
48
49 const stepItems = [
50 { value: 1, label: '基本信息', template: markRaw(Step1FormContent) },
51 { value: 2, label: '补充信息', template: markRaw(Step2FormContent) },
52 { value: 3, label: '上传文件', template: markRaw(Step3FormContent) },
53 ];
54
55 const currentStep = ref(1);
56 const currentContent = computed(() => stepItems.find((item) => item.value === currentStep.value)?.template);
57 const nextBtnLabel = computed(() => (currentStep.value === 3 ? '提交' : '下一步'));
58
59 const onPrev = (): void => {
60 currentStep.value = Math.max(1, currentStep.value - 1);
61 };
62
63 const onNext = () => {
64 formRef.value.onValid(async () => {
65 if (currentStep.value === stepItems.length) {
66 setLoading(true);
67 props.onSubmit(formValue.value).finally(() => setLoading(false));
68 }
69 currentStep.value = Math.min(stepItems.length, currentStep.value + 1);
70 });
71 };
72 </script>
73
74 <style scoped lang="less">
75 .aside {
76 box-shadow: unset !important;
77 border-right: 1px solid var(--color-border);
78 margin-right: 20px;
79 background-color: transparent;
80 }
81
82 .main {
83 min-width: 640px;
84 }
85 </style>
1 <script setup lang="ts">
2 import { Form, FormItem, Input, Message, Select } from '@arco-design/web-vue';
3 import { computed, ref } from 'vue';
4 import AvatarUpload from '@/components/avatar-upload/index.vue';
5 import TagSelect from '@/components/tag-select/index.vue';
6 import { FieldRule, FormInstance } from '@arco-design/web-vue/es/form';
7 import { useSelectionStore } from '@/store';
8 import { get, set } from 'lodash';
9 import { useVModels } from '@vueuse/core';
10 import ProjectSelect from '@/components/project-select/index.vue';
11
12 const props = withDefaults(defineProps<{ loading?: boolean; modelValue?: any; filterProject?: (value: any) => boolean }>(), {
13 filterProject: () => true,
14 });
15
16 const emits = defineEmits(['update:modelValue', 'update:loading']);
17
18 const formRef = ref<FormInstance>();
19 const { modelValue: formValue } = useVModels(props, emits);
20
21 const formRule = {
22 'cover': [{ type: 'string', required: true, message: '请上传活动封面' }],
23 'song_name': [{ type: 'string', required: true, message: '请输入歌曲名称' }],
24 'sub_title': [{ type: 'string', required: true, message: '请输入简介' }],
25 'expand.tag_ids': [
26 { type: 'array', required: true, message: '请选择关联标签' },
27 { type: 'array', maxLength: 3, message: '关联标签最多选中3个' },
28 ],
29 'lang': [
30 { type: 'string', required: true, message: '请选择语种' },
31 { type: 'array', maxLength: 2, message: '语种最多选中2个' },
32 ],
33 'speed': [{ type: 'string', required: true, message: '请选择语速' }],
34 'project_id': [{ type: 'number', min: 1, required: true, message: '请选择关联厂牌' }],
35 } as Record<string, FieldRule[]>;
36
37 const tagIds = computed({
38 get: () => get(formValue.value, 'expand.tag_ids', []),
39 set: (val) => set(formValue.value, 'expand.tag_ids', val),
40 });
41
42 const { activityLangOptions, activitySpeedOptions } = useSelectionStore();
43
44 const onTagExceedLimitError = (content: string, duration = 1000) => Message.warning({ content, duration });
45
46 defineExpose({
47 onValid: (callback?: () => void) => formRef.value?.validate((errors) => !errors && callback?.()),
48 });
49 </script>
50
51 <template>
52 <Form ref="formRef" :model="formValue" :rules="formRule" :auto-label-width="true" size="small">
53 <FormItem label="封面图片" field="cover" :show-colon="true">
54 <AvatarUpload v-model="formValue.cover" :size="100" shape="square" />
55 </FormItem>
56 <FormItem label="歌曲名称" field="song_name" :show-colon="true">
57 <Input v-model="formValue.song_name" :max-length="100" :show-word-limit="true" placeholder="请输入" />
58 </FormItem>
59 <FormItem label="曲风标签" field="expand.tag_ids" :show-colon="true">
60 <TagSelect v-model="tagIds" :multiple="true" :limit="3" placeholder="请选择" limit-error-msg="关联标签最多选中3个" />
61 </FormItem>
62 <FormItem label="歌曲语种" field="lang" :show-colon="true">
63 <Select
64 v-model="formValue.lang"
65 :options="activityLangOptions"
66 :limit="2"
67 :field-names="{ value: 'identifier', label: 'name' }"
68 :fallback-option="false"
69 :multiple="true"
70 placeholder="请选择"
71 @exceed-limit="onTagExceedLimitError('关联语种最多选中2个')"
72 />
73 </FormItem>
74 <FormItem label="歌曲速度" field="speed" :show-colon="true">
75 <Select
76 v-model="formValue.speed"
77 :options="activitySpeedOptions"
78 :field-names="{ value: 'identifier', label: 'name' }"
79 :fallback-option="false"
80 placeholder="请选择"
81 />
82 </FormItem>
83 <FormItem label="歌曲简介" field="sub_title" :show-colon="true">
84 <Input v-model.trim="formValue.sub_title" :max-length="20" :show-word-limit="true" placeholder="举例:影视OST歌曲,急寻歌手" />
85 </FormItem>
86 <FormItem label="关联厂牌" field="project_id" :show-colon="true">
87 <ProjectSelect v-model="formValue.project_id" :filtrate="filterProject" />
88 </FormItem>
89 </Form>
90 </template>
91
92 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { DatePicker, Form, FormItem, InputTag, Select } from '@arco-design/web-vue';
3 import UserSelect from '@/components/user-select/index.vue';
4
5 import { ref, computed } from 'vue';
6 import { FieldRule, FormInstance } from '@arco-design/web-vue/es/form';
7 import { useVModels } from '@vueuse/core';
8 import { useSelectionStore } from '@/store';
9 import { get, set } from 'lodash';
10 import { storeToRefs } from 'pinia';
11
12 const props = defineProps<{ loading?: boolean; modelValue?: any }>();
13 const emits = defineEmits(['update:modelValue', 'update:loading']);
14
15 const formRef = ref<FormInstance>();
16 const { modelValue: formValue } = useVModels(props, emits);
17
18 const { activitySexOptions } = storeToRefs(useSelectionStore());
19
20 const lyricistIds = computed({
21 get: () => get(formValue.value, 'expand.lyricist.ids', []),
22 set: (val) => set(formValue.value, 'expand.lyricist.ids', val),
23 });
24
25 const lyricistSupplement = computed({
26 get: () => get(formValue.value, 'expand.lyricist.supplement', []),
27 set: (val) => set(formValue.value, 'expand.lyricist.supplement', val),
28 });
29
30 const composerIds = computed({
31 get: () => get(formValue.value, 'expand.composer.ids', []),
32 set: (val) => set(formValue.value, 'expand.composer.ids', val),
33 });
34
35 const composerSupplement = computed({
36 get: () => get(formValue.value, 'expand.composer.supplement', []),
37 set: (val) => set(formValue.value, 'expand.composer.supplement', val),
38 });
39
40 const arrangerIds = computed({
41 get: () => get(formValue.value, 'expand.arranger.ids', []),
42 set: (val) => set(formValue.value, 'expand.arranger.ids', val),
43 });
44
45 const arrangerSupplement = computed({
46 get: () => get(formValue.value, 'expand.arranger.supplement', []),
47 set: (val) => set(formValue.value, 'expand.arranger.supplement', val),
48 });
49
50 const checkMaxUser = (value: any, cb: (error?: string) => void) => (value && value.length > 2 ? cb('最大选择2人') : {});
51
52 const checkNeedOne = (value: any, cb: (error?: string) => void, condition: boolean, message: string) => {
53 checkMaxUser(value, cb);
54 if (condition) {
55 cb(message);
56 }
57 };
58
59 const formRule = {
60 'sex': [{ required: true, message: '请选择性别要求' }],
61 'expand.lyricist.supplement': [
62 {
63 type: 'array',
64 validator: (value, cb) =>
65 checkNeedOne(
66 value,
67 cb,
68 lyricistIds.value.length === 0 && lyricistSupplement.value.length === 0,
69 '词作者(用户)或词作者(未注册),必填一个'
70 ),
71 },
72 ],
73 'expand.composer.supplement': [
74 {
75 type: 'array',
76 validator: (value, cb) =>
77 checkNeedOne(
78 value,
79 cb,
80 composerIds.value.length === 0 && composerSupplement.value.length === 0,
81 '曲作者(用户)或曲作者(未注册),必填一个'
82 ),
83 },
84 ],
85 'expand.arranger.supplement': [
86 {
87 type: 'array',
88 validator: (value, cb) =>
89 checkNeedOne(
90 value,
91 cb,
92 arrangerIds.value.length === 0 && arrangerSupplement.value.length === 0,
93 '编曲(用户)或编曲(未注册),必填一个'
94 ),
95 },
96 ],
97 'estimate_release_at': [{ required: true, message: '请选择预计发布时间' }],
98 } as Record<string, FieldRule[]>;
99
100 defineExpose({
101 onValid: (callback?: () => void) => formRef.value?.validate((errors) => !errors && callback?.()),
102 });
103 </script>
104
105 <template>
106 <Form ref="formRef" :model="formValue" :rules="formRule" :auto-label-width="true" size="small">
107 <FormItem label="词作者(用户)" field="expand.lyricist.ids" :show-colon="true">
108 <UserSelect v-model="lyricistIds" placeholder="请选择用户" :multiple="true" :allow-search="true" :limit="2" />
109 </FormItem>
110 <FormItem label="词作者(未注册)" field="expand.lyricist.supplement" :show-colon="true">
111 <InputTag v-model="lyricistSupplement" placeholder="输入完成回车键分隔" :max-tag-count="2" :unique-value="true" />
112 <template #extra>
113 <div>注意:上方填1项即可,歌曲信息中【用户】可点击跳转用户主页,【未注册】仅显示;</div>
114 </template>
115 </FormItem>
116 <FormItem label="曲作者(用户)" field="expand.composer.ids" :show-colon="true">
117 <UserSelect v-model="composerIds" placeholder="请选择用户" :multiple="true" :allow-search="true" :limit="2" />
118 </FormItem>
119 <FormItem label="曲作者(未注册)" field="expand.composer.supplement" :show-colon="true">
120 <InputTag v-model="composerSupplement" placeholder="输入完成回车键分隔" :max-tag-count="2" :unique-value="true" />
121 <template #extra>
122 <div>注意:上方填1项即可,歌曲信息中【用户】可点击跳转用户主页,【未注册】仅显示;</div>
123 </template>
124 </FormItem>
125 <FormItem label="编曲(用户)" field="expand.arranger.ids" :show-colon="true">
126 <UserSelect v-model="arrangerIds" placeholder="请选择用户" :multiple="true" :allow-search="true" :limit="2" />
127 </FormItem>
128 <FormItem label="编曲(未注册)" field="expand.arranger.supplement" :show-colon="true">
129 <InputTag v-model="arrangerSupplement" placeholder="输入完成回车键分隔" :max-tag-count="2" :unique-value="true" />
130 <template #extra>
131 <div>注意:上方填1项即可,歌曲信息中【用户】可点击跳转用户主页,【未注册】仅显示;</div>
132 </template>
133 </FormItem>
134 <FormItem label="性别要求" field="sex" :show-colon="true">
135 <Select
136 v-model="formValue.sex"
137 :options="activitySexOptions"
138 :field-names="{ value: 'identifier', label: 'name' }"
139 :fallback-option="false"
140 placeholder="请选择"
141 />
142 </FormItem>
143 <FormItem label="预计发行日期" field="estimate_release_at" :show-colon="true">
144 <DatePicker
145 v-model="formValue.estimate_release_at"
146 placeholder="请选择"
147 style="width: 100%"
148 :show-now-btn="false"
149 :allow-clear="false"
150 />
151 </FormItem>
152 </Form>
153 </template>
154
155 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { ref, computed, createVNode } from 'vue';
3 import { FieldRule, FormInstance } from '@arco-design/web-vue/es/form';
4 import { useVModels } from '@vueuse/core';
5 import { Form, FormItem, Link, Textarea } from '@arco-design/web-vue';
6 import InputUpload from '@/components/input-upload/index.vue';
7 import { useSelectionStore } from '@/store';
8 import { first, get, last, set } from 'lodash';
9 import { bytesForHuman, getLyricTimeArr } from '@/utils';
10 import axios from 'axios';
11 import AudioPreview from '@/views/audition/activity-apply/components/audio-preview.vue';
12 import AudioPlayer from '@/components/audio-player/index.vue';
13 import { createModalVNode } from '@/utils/createVNode';
14 import { downloadFile } from '@/http/auth';
15
16 const props = defineProps<{ loading?: boolean; modelValue?: any }>();
17 const emits = defineEmits(['update:modelValue', 'update:loading']);
18
19 const formRef = ref<FormInstance>();
20 const { loading, modelValue: formValue } = useVModels(props, emits);
21
22 const guideSourceUrl = computed({
23 get: () => get(formValue.value, 'expand.guide_source.url', ''),
24 set: (val) => set(formValue.value, 'expand.guide_source.url', val),
25 });
26
27 const karaokeSourceUrl = computed({
28 get: () => get(formValue.value, 'expand.karaoke_source.url', ''),
29 set: (val) => set(formValue.value, 'expand.karaoke_source.url', val),
30 });
31
32 const trackSourceUrl = computed({
33 get: () => get(formValue.value, 'expand.track_source.url', ''),
34 set: (val) => set(formValue.value, 'expand.track_source.url', val),
35 });
36
37 const trackSourceName = computed(() => get(formValue.value, 'expand.track_source.name', ''));
38 const trackSourceSize = computed((): number => get(formValue.value, 'expand.track_source.size', 0));
39
40 const fullLyricTime = computed(() => getLyricTimeArr(formValue.value.lyric));
41 const clipLyricTime = computed(() => getLyricTimeArr(formValue.value.clip_lyric));
42 const checkClip = computed(
43 () => clipLyricTime.value.length !== 0 && fullLyricTime.value.toString().indexOf(clipLyricTime.value.toString()) !== -1
44 );
45
46 const onUpdateGuide = (file: { name: string; url: string; size: number }) => {
47 set(formValue.value, 'expand.guide_source', { name: file.name, url: file.url, size: file.size });
48 };
49
50 const onUpdateKaraoke = (file: { name: string; url: string; size: number }) => {
51 set(formValue.value, 'expand.karaoke_source', { name: file.name, url: file.url, size: file.size });
52 };
53
54 const onUpdateTrack = (file: { name: string; url: string; size: number }) => {
55 set(formValue.value, 'expand.track_source', { name: file.name, url: file.url, size: file.size });
56 };
57
58 const { activityAudioAccept, activityTrackAccept } = useSelectionStore();
59
60 const formRule = {
61 'expand.guide_source.url': [{ required: true, message: '请上传导唱文件' }],
62 'expand.karaoke_source.url': [{ required: true, message: '请上传伴奏文件' }],
63 'lyric': [
64 { type: 'string', required: true, message: '请填写歌词内容' },
65 {
66 type: 'string',
67 validator: (value: any, cb: (error?: string) => void) => (fullLyricTime.value.length === 0 ? cb('歌词文本格式不正确') : {}),
68 },
69 ],
70 'clip_lyric': [
71 { type: 'string', required: true, message: '请填写推荐歌词内容' },
72 { type: 'string', validator: (value: any, cb: (error?: string) => void) => (checkClip.value ? {} : cb('推荐片段不在歌词中')) },
73 ],
74 } as Record<string, FieldRule[]>;
75
76 const onDownload = (url: string, fileName: string) => downloadFile(url, `${formValue.value.song_name}(${fileName})`);
77
78 const onFormatLyric = (key: string) => {
79 formValue.value[key] = formValue.value[key]?.replace(/(\n[\s\t]*\r*\n)/g, '\n').replace(/^[\n\r\t]*|[\n\r\t]*$/g, '');
80 };
81
82 const guideFile = ref<File | undefined>();
83
84 const getGuideFile = async () => {
85 if (!guideFile.value) {
86 guideFile.value = await axios
87 .get(`${guideSourceUrl.value}`, { responseType: 'blob', timeout: 60000 })
88 .then(({ data }) => Promise.resolve(data));
89 }
90
91 return guideFile.value;
92 };
93
94 const onAudioPreview = async () => {
95 const src = await getGuideFile();
96 createModalVNode(() => createVNode(AudioPreview, { src, lyric: formValue.value.lyric }), {
97 title: '预览歌词-整首',
98 footer: false,
99 closable: true,
100 });
101 };
102
103 const onAudioClipPreview = async () => {
104 const src = await getGuideFile();
105 const startTime = first(clipLyricTime.value) as string;
106 const fullEndIndex = fullLyricTime.value.indexOf(last(clipLyricTime.value) as string);
107 const endTime = fullEndIndex === -1 ? undefined : fullLyricTime.value[fullEndIndex + 1] || undefined;
108
109 createModalVNode(() => createVNode(AudioPreview, { src, lyric: formValue.value.clip_lyric, startTime, endTime }), {
110 title: '预览歌词-片段',
111 footer: false,
112 closable: true,
113 });
114 };
115
116 defineExpose({
117 onValid: (callback?: () => void) => formRef.value?.validate((errors) => !errors && callback?.()),
118 });
119 </script>
120
121 <template>
122 <Form ref="formRef" :model="formValue" :rules="formRule" :auto-label-width="true" size="small">
123 <FormItem label="导唱文件(带伴奏)" field="expand.guide_source.url" :show-colon="true">
124 <InputUpload
125 v-model="guideSourceUrl"
126 :accept="activityAudioAccept"
127 :limit="100"
128 @success="onUpdateGuide"
129 @choose-file="(file:File) => (guideFile = file)"
130 @update:loading="(value:boolean) => (loading = value)"
131 />
132 </FormItem>
133 <FormItem v-if="guideSourceUrl">
134 <template #label>
135 <Link icon @click="onDownload(guideSourceUrl, '导唱文件')">下载导唱</Link>
136 </template>
137 <AudioPlayer name="导唱文件" :url="guideSourceUrl" />
138 </FormItem>
139 <FormItem label="伴奏文件" field="expand.karaoke_source.url" :show-colon="true">
140 <InputUpload
141 v-model="karaokeSourceUrl"
142 :accept="activityAudioAccept"
143 :limit="100"
144 @success="onUpdateKaraoke"
145 @update:loading="(value:boolean) => (loading = value)"
146 />
147 </FormItem>
148 <FormItem v-if="karaokeSourceUrl">
149 <template #label>
150 <Link icon @click="onDownload(karaokeSourceUrl, '伴奏文件')">下载伴奏</Link>
151 </template>
152 <AudioPlayer name="伴奏文件" :url="karaokeSourceUrl" />
153 </FormItem>
154 <FormItem label="分轨文件" :show-colon="true">
155 <InputUpload v-model="trackSourceUrl" :accept="activityTrackAccept" @success="onUpdateTrack" />
156 </FormItem>
157 <FormItem v-if="trackSourceUrl">
158 <template #label>
159 <Link icon @click="onDownload(karaokeSourceUrl, '分轨文件')">下载分轨文件</Link>
160 </template>
161 {{ trackSourceName }} {{ bytesForHuman(trackSourceSize) }}
162 </FormItem>
163 <FormItem class="lyric" label="歌词文本" field="lyric" :show-colon="true">
164 <Textarea
165 v-model="formValue.lyric"
166 :auto-size="{ minRows: 6, maxRows: 6 }"
167 placeholder="请粘贴带时间的lrc歌词文本至输入框"
168 @blur="() => onFormatLyric('lyric')"
169 />
170 <template #extra>
171 <Link :hoverable="false" :disabled="!guideSourceUrl || !formValue.lyric" @click="onAudioPreview()"> 预览歌词</Link>
172 </template>
173 </FormItem>
174 <FormItem class="lyric" label="片段歌词" field="clip_lyric" :show-colon="true">
175 <Textarea
176 v-model="formValue.clip_lyric"
177 :auto-size="{ minRows: 6, maxRows: 6 }"
178 placeholder="请粘贴带时间的lrc歌词文本至输入框"
179 @blur="() => onFormatLyric('clip_lyric')"
180 />
181 <template #extra>
182 <Link :hoverable="false" :disabled="!guideSourceUrl || !checkClip" @click="onAudioClipPreview()"> 预览歌词</Link>
183 </template>
184 </FormItem>
185 </Form>
186 </template>
187
188 <style scoped lang="less">
189 .lyric {
190 :deep(.arco-form-item-extra) {
191 width: 100%;
192 text-align: right;
193 }
194 }
195 </style>
1 <script setup lang="ts">
2 import { computed, createVNode, onMounted, ref } from 'vue';
3 import { AnyObject } from '@/types/global';
4 import useLoading from '@/hooks/loading';
5 import { useApplyApi } from '@/http/activity';
6 import { useAuthorizedStore, useSelectionStore } from '@/store';
7 import { createModalVNode } from '@/utils/createVNode';
8 import { Message, TableData, TabPane, Tabs } from '@arco-design/web-vue';
9 import { promiseToBoolean } from '@/utils';
10 import UserTableColumn from '@/components/filter/user-table-column.vue';
11 import EnumTableColumn from '@/components/filter/enum-table-column.vue';
12 import { tryOnMounted } from '@vueuse/core';
13 import SpaceTableColumn from '@/components/filter/space-table-column.vue';
14 import usePermission from '@/hooks/permission';
15 import DateTableColumn from '@/components/filter/date-table-column.vue';
16 import ActivityTableColumn from '@/components/filter/activity-table-column.vue';
17 import FormContent from '@/views/audition/activity-apply/components/form-content.vue';
18 import RecordTable from '@/views/audition/activity-audit/components/record-table.vue';
19
20 const { loading, setLoading } = useLoading(false);
21 const filter = ref<AnyObject>({});
22 const tableRef = ref();
23
24 const queryParams = computed(() => {
25 return {
26 ...filter.value,
27 createdForm: 1,
28 songType: 1,
29 setColumn: [
30 'id',
31 'song_name',
32 'cover',
33 'lang',
34 'speed',
35 'sub_title',
36 'sex',
37 'user_id',
38 'project_id',
39 'lyric',
40 'clip_lyric',
41 'expand',
42 'audit_status',
43 'created_at',
44 'estimate_release_at',
45 ],
46 setWith: [
47 'project:id,name',
48 'user:id,real_name,nick_name,role',
49 'tags:id,name',
50 'applyRecords:id,activity_id,current,audit_msg,created_at',
51 ],
52 setSort: ['-updated_at'],
53 };
54 });
55
56 const onQuery = async (params?: AnyObject) => {
57 setLoading(true);
58 return useApplyApi.get({ ...queryParams.value, ...params }).finally(() => setLoading(false));
59 };
60
61 const onSearch = () => tableRef.value?.onPageChange(1);
62
63 const onReset = () => {
64 filter.value = { songName: '', userName: '', projectName: '', tagName: '', auditStatus: '', createBetween: [] };
65 onSearch();
66 };
67
68 tryOnMounted(() => useSelectionStore().queryAll());
69
70 onMounted(() => {
71 onReset();
72 useAuthorizedStore().syncAuditActivity();
73 });
74
75 const onCreate = () => {
76 const dialog = createModalVNode(
77 () =>
78 createVNode(FormContent, {
79 onSubmit: (data: AnyObject) =>
80 useApplyApi.create(Object.assign(data, { song_type: 1 })).then(() => {
81 Message.success(`申请上架:${data.song_name}`);
82 tableRef.value?.onFetch();
83 dialog.close();
84 }),
85 }),
86 { title: '申请歌曲上架', footer: false, closable: true, width: 'auto' }
87 );
88 };
89
90 const onUpdate = (row: TableData) => {
91 const dialog = createModalVNode(
92 () =>
93 createVNode(Tabs, { type: 'rounded', size: 'mini' }, [
94 createVNode(TabPane, { title: '试唱歌曲上架', key: 'applyRef' }, () =>
95 createVNode(FormContent, {
96 initValue: row,
97 onSubmit: (data: AnyObject) =>
98 useApplyApi.update(row.id, Object.assign(data, { song_type: 1 })).then(() => {
99 Message.success(`重新申请上架活动:${row.song_name}`);
100 tableRef.value?.onFetch();
101 useAuthorizedStore().syncAuditActivity();
102 dialog.close();
103 }),
104 })
105 ),
106 createVNode(TabPane, { title: `审核驳回记录(${row.apply_records?.length || 0}`, key: 'record' }, () =>
107 createVNode(RecordTable, { row })
108 ),
109 ]),
110 { title: '编辑歌曲', footer: false, closable: true, width: '860px' }
111 );
112 };
113
114 const onDelete = (row: TableData) =>
115 createModalVNode(`确认要将活动:${row.song_name} 删除吗?`, {
116 title: '删除操作',
117 onBeforeOk: () => promiseToBoolean(useApplyApi.destroy(row.id)),
118 onOk: () => tableRef.value?.onFetch(),
119 });
120 </script>
121
122 <template>
123 <page-view has-bread has-card>
124 <filter-search :model="filter" :loading="loading" @search="onSearch" @reset="onReset">
125 <filter-search-item field="song_name" label="歌曲名称">
126 <a-input v-model="filter.songName" :allow-clear="true" placeholder="请输入" />
127 </filter-search-item>
128 <filter-search-item field="projectName" label="厂牌名称">
129 <a-input v-model="filter.projectName" :allow-clear="true" placeholder="请输入" />
130 </filter-search-item>
131 <filter-search-item field="tagName" label="标签名称">
132 <a-input v-model="filter.tagName" :allow-clear="true" placeholder="请输入" />
133 </filter-search-item>
134 <filter-search-item field="userName" label="创建人">
135 <a-input v-model="filter.userName" :allow-clear="true" placeholder="请输入" />
136 </filter-search-item>
137 <filter-search-item field="createBetween" label="创建时间">
138 <a-range-picker v-model="filter.createBetween" :allow-clear="true" />
139 </filter-search-item>
140 <filter-search-item field="audit_status" label="状态">
141 <a-select v-model="filter.auditStatus" :options="useApplyApi.statusOption" placeholder="请选择" />
142 </filter-search-item>
143 </filter-search>
144 <filter-table ref="tableRef" :loading="loading" :on-query="onQuery">
145 <template #tool>
146 <a-button v-permission="['audition-activity-apply-create']" type="primary" @click="onCreate">试唱歌曲上架</a-button>
147 </template>
148 <activity-table-column :width="480" data-index="id" title="试唱歌曲" />
149 <user-table-column :width="160" data-index="user_id" user="user" title="创建人" show-href />
150 <date-table-column data-index="created_at" title="创建时间" :width="100" split />
151 <enum-table-column :width="100" data-index="audit_status" title="状态" :option="useApplyApi.statusOption" />
152 <space-table-column
153 v-if="usePermission().checkPermission(['audition-activity-apply-edit', 'audition-activity-apply-delete'])"
154 :width="80"
155 data-index="operations"
156 direction="vertical"
157 title="操作"
158 >
159 <template #default="{ record }">
160 <span v-if="record.audit_status === 0" style="color: rgba(44, 44, 44, 0.5)"></span>
161 <a-link
162 v-if="record.audit_status === 2"
163 v-permission="['audition-activity-apply-edit']"
164 :hoverable="false"
165 class="link-hover"
166 @click="onUpdate(record)"
167 >
168 编辑
169 </a-link>
170 <a-link
171 v-if="record.audit_status === 2"
172 v-permission="['audition-activity-apply-delete']"
173 :hoverable="false"
174 class="link-hover"
175 @click="onDelete(record)"
176 >
177 删除
178 </a-link>
179 </template>
180 </space-table-column>
181 </filter-table>
182 </page-view>
183 </template>
184
185 <style scoped lang="less"></style>
1 <template>
2 <Layout>
3 <Layout has-sider>
4 <LayoutSider :width="120" class="aside">
5 <Steps :current="currentStep" direction="vertical" :small="true">
6 <template v-for="item in stepItems">
7 <Step v-if="item.value <= maxStep" :key="item.value">{{ item.label }}</Step>
8 </template>
9 </Steps>
10 </LayoutSider>
11 <LayoutContent class="main">
12 <component
13 :is="currentContent"
14 ref="formRef"
15 v-model="formValue"
16 :loading="loading"
17 :disabled="disabled"
18 :hide-field="hideField"
19 :current-attribute="currentAttribute"
20 :last-attribute="lastAttribute"
21 :filter-project="filterProject"
22 />
23 </LayoutContent>
24 </Layout>
25 <LayoutFooter>
26 <Divider style="margin-top: 8px; margin-bottom: 20px" />
27 <Link :href="useSelectionStore().lyricTool" :hoverable="false" class="link-hover" icon>歌词制作工具</Link>
28 <Space style="float: right">
29 <IconButton v-show="currentStep !== 1" size="small" icon="left" label="上一步" @click="onPrev" />
30 <IconButton
31 v-if="!isAudit || (isAudit && currentStep !== maxStep)"
32 size="small"
33 icon="right"
34 icon-align="right"
35 :loading="loading"
36 :label="nextBtnLabel"
37 @click="onNext"
38 />
39
40 <template v-if="isAudit && currentStep === maxStep">
41 <IconButton size="small" type="primary" status="danger" icon="close" label="拒绝" :loading="loading" @click="onFail" />
42 <IconButton size="small" type="primary" status="success" icon="check" label="确认" :loading="loading" @click="onSuccess" />
43 </template>
44 </Space>
45 </LayoutFooter>
46 </Layout>
47 </template>
48
49 <script setup lang="ts">
50 import { Layout, LayoutSider, LayoutContent, LayoutFooter, Step, Steps, Space, Divider, Link } from '@arco-design/web-vue';
51 import { computed, createVNode, markRaw, ref } from 'vue';
52 import Step1FormContent from '@/views/audition/activity-audit/components/step1-form-content.vue';
53 import Step2FormContent from '@/views/audition/activity-audit/components/step2-form-content.vue';
54 import Step3FormContent from '@/views/audition/activity-audit/components/step3-form-content.vue';
55 import Step4FormContent from '@/views/audition/activity-audit/components/step4-form-content.vue';
56 import IconButton from '@/components/icon-button/index.vue';
57
58 import { useSelectionStore } from '@/store';
59
60 import { AnyObject } from '@/types/global';
61 import useLoading from '@/hooks/loading';
62 import { cloneDeep } from 'lodash';
63 import { createFormItemVNode, createInputVNode, createModalVNode } from '@/utils/createVNode';
64
65 const props = withDefaults(
66 defineProps<{
67 initValue?: AnyObject;
68 currentAttribute?: AnyObject;
69 lastAttribute?: AnyObject;
70 filterProject?: (value: unknown) => boolean;
71 maxStep?: number;
72 isAudit?: boolean;
73 disabled?: boolean;
74 submitText?: string;
75 submit: (data: AnyObject) => Promise<any>;
76 hideField?: string[];
77 }>(),
78 { maxStep: 4, isAudit: false, submitText: '提交' }
79 );
80
81 const { loading, setLoading } = useLoading(false);
82
83 const formRef = ref();
84 const formValue = ref({ ...cloneDeep(props.initValue) });
85
86 const stepItems = [
87 { value: 1, label: '基本信息', template: markRaw(Step1FormContent) },
88 { value: 2, label: '补充信息', template: markRaw(Step2FormContent) },
89 { value: 3, label: '上传文件', template: markRaw(Step3FormContent) },
90 { value: 4, label: '推送配置', template: markRaw(Step4FormContent) },
91 ];
92
93 const currentStep = ref(1);
94 const currentContent = computed(() => stepItems.find((item) => item.value === currentStep.value)?.template);
95 const nextBtnLabel = computed(() => (currentStep.value === props.maxStep ? props.submitText : '下一步'));
96
97 const onPrev = (): void => {
98 currentStep.value = Math.max(1, currentStep.value - 1);
99 };
100
101 const onNext = () => {
102 formRef.value.onValid(async () => {
103 if (currentStep.value === props.maxStep) {
104 setLoading(true);
105 props.submit(formValue.value).finally(() => setLoading(false));
106 }
107 currentStep.value = Math.min(props.maxStep, currentStep.value + 1);
108 });
109 };
110
111 const onFail = () => {
112 const formValue = ref<string>('');
113 const modelProp = { title: '审核不通过', footer: false, closable: true };
114 const buttonProp = { size: 'small', icon: 'message', type: 'primary', label: '提交', style: { marginLeft: '5px' } };
115
116 const dialog = createModalVNode(
117 () =>
118 createFormItemVNode({ hideLabel: true, rowClass: 'mb-0' }, [
119 createInputVNode(formValue, { size: 'small', maxLength: 100, showWordLimit: true, placeholder: '请输入拒绝理由' }),
120 createVNode(IconButton, {
121 ...buttonProp,
122 loading: loading.value,
123 disabled: formValue.value.length === 0,
124 onClick: () => {
125 setLoading(true);
126 props
127 .submit({ audit_status: 2, msg: formValue.value })
128 .then(() => dialog.close())
129 .finally(() => setLoading(false));
130 },
131 }),
132 ]),
133 modelProp
134 );
135 };
136
137 const onSuccess = () => {
138 setLoading(true);
139 props.submit({ ...formValue.value, audit_status: 1 }).finally(() => setLoading(false));
140 };
141 </script>
142
143 <style scoped lang="less">
144 .aside {
145 box-shadow: unset !important;
146 border-right: 1px solid var(--color-border);
147 margin-right: 20px;
148 background-color: transparent;
149 }
150
151 .main {
152 min-width: 640px;
153 }
154 </style>
1 <script setup lang="ts">
2 import { Tabs, TabPane } from '@arco-design/web-vue';
3 import RecordTable from '@/views/audition/activity-audit/components/record-table.vue';
4 import FormContent from '@/views/audition/activity-audit/components/form-content.vue';
5 import { ActivityApply, ActivityApplyForm } from '@/types/activity-apply';
6 import { Activity } from '@/types/activity';
7 import { AnyObject } from '@/types/global';
8
9 withDefaults(
10 defineProps<{
11 activeKey: string;
12 row: ActivityApply;
13 submitText?: string;
14 type?: 'view' | 'audit';
15 disabled?: boolean;
16 currentAttribute?: ActivityApplyForm;
17 lastAttribute?: ActivityApplyForm;
18 submit: (value: AnyObject) => Promise<any>;
19 }>(),
20 {
21 type: 'audit',
22 }
23 );
24 </script>
25
26 <template>
27 <Tabs :default-active-key="activeKey || 'audit'" type="rounded" size="mini" style="width: 800px">
28 <TabPane key="audit" title="试唱上架审核">
29 <FormContent
30 :init-value="row"
31 :current-attribute="currentAttribute"
32 :last-attribute="lastAttribute"
33 :submit-text="submitText"
34 :is-audit="type === 'audit'"
35 :disabled="type === 'view'"
36 :submit="submit"
37 />
38 </TabPane>
39 <TabPane key="record" :title="`审核驳回记录(${row.record_count})`">
40 <RecordTable :row="row as Activity" />
41 </TabPane>
42 </Tabs>
43 </template>
44
45 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { CheckboxGroup, Col, Row, Select } from '@arco-design/web-vue';
3 import { useSelectionStore } from '@/store';
4 import { computed } from 'vue';
5 import { findIndex, fromPairs } from 'lodash';
6
7 type ManageValue = { user_id: number; permission: string[] };
8
9 const props = withDefaults(defineProps<{ modelValue?: ManageValue[] }>(), { modelValue: () => [] });
10 const emits = defineEmits<{ (e: 'update:modelValue', value: ManageValue[]): void }>();
11
12 const { getUserOptions } = useSelectionStore();
13
14 const permissionOption = [
15 { label: '查看报价', value: 'price' },
16 { label: '回复结果', value: 'audit' },
17 ];
18
19 const formValue = computed({
20 get: () => props.modelValue as ManageValue[],
21 set: (val: ManageValue[]) => emits('update:modelValue', val),
22 });
23
24 const selectPermission = computed(() => {
25 return fromPairs(props.modelValue?.map((item) => [item.user_id, item.permission]));
26 });
27
28 const selectIds = computed({
29 get: () => props.modelValue?.map((item) => item.user_id) as number[],
30 set: (val: number[]) => {
31 emits(
32 'update:modelValue',
33 val.map((item) => {
34 const permission = selectPermission.value[item] || [];
35 if (!permission.includes('view')) {
36 permission.unshift('view');
37 }
38
39 return { user_id: item, permission };
40 })
41 );
42 },
43 });
44
45 const onClick = (e: Event, key: number) => {
46 if (!selectIds.value.includes(key)) {
47 selectIds.value = [...selectIds.value, key];
48 }
49 e.stopPropagation();
50 };
51
52 const onUpdatePermission = (key: number, val: string[]) => {
53 const index = findIndex(formValue.value, { user_id: key });
54 formValue.value?.splice(index, 1, { user_id: key, permission: val });
55 };
56 </script>
57
58 <template>
59 <Select
60 v-model="selectIds"
61 :options="getUserOptions"
62 :virtual-list-props="{ height: 240, fixedSize: true, buffer: 60 }"
63 :field-names="{ value: 'id', label: 'nick_name' }"
64 placeholder="请选择用户"
65 :fallback-option="false"
66 :multiple="true"
67 >
68 <template #option="{ data }">
69 <Row :wrap="false" :gutter="16" :justify="'space-between'">
70 <Col flex="auto">{{ data.nick_name }}</Col>
71 <Col flex="200px" @click="(e: Event) => onClick(e,data.id)">
72 <CheckboxGroup
73 class="p-checkbox"
74 :model-value="selectPermission[data.id]"
75 :options="permissionOption"
76 @update:model-value="(value:string[]) => onUpdatePermission(data.id, value)"
77 />
78 </Col>
79 </Row>
80 </template>
81 </Select>
82 </template>
83
84 <style scoped lang="less"></style>
1 <template>
2 <FilterTable ref="tableRef" :loading="loading" :scroll="{ y: 540 }" :on-query="onQuery" :page-size="10">
3 <FilterTableColumn data-index="id" title="序号" :width="60">
4 <template #default="{ index }">{{ getIndexNumber(index) }}</template>
5 </FilterTableColumn>
6 <FilterTableColumn data-index="audit_user_id" title="操作人" :width="160">
7 <template #default="{ record }">
8 <template v-if="record.audit_user">
9 {{ `${record.audit_user.nick_name}(${record.audit_user.real_name})` }}
10 </template>
11 </template>
12 </FilterTableColumn>
13 <DateTableColumn data-index="updated_at" title="操作时间" :width="180" :split="false" />
14 <FilterTableColumn data-index="audit_msg" title="驳回说明" :width="400" />
15 </FilterTable>
16 </template>
17
18 <script lang="ts" setup>
19 import useLoading from '@/hooks/loading';
20 import { computed, onMounted, ref } from 'vue';
21 import { Activity } from '@/types/activity';
22 import { AnyObject } from '@/types/global';
23 import { useApplyApi } from '@/http/activity';
24 import FilterTable from '@/components/filter/table.vue';
25 import FilterTableColumn from '@/components/filter/table-column.vue';
26 import DateTableColumn from '@/components/filter/date-table-column.vue';
27
28 const { loading, setLoading } = useLoading(false);
29
30 const props = defineProps<{ row: Activity }>();
31 const tableRef = ref();
32
33 const onQuery = async (params: AnyObject) => {
34 setLoading(true);
35 return useApplyApi
36 .record(props.row.id, {
37 ...params,
38 setWith: ['auditUser:id,nick_name,real_name,role'],
39 setSort: ['-id'],
40 })
41 .finally(() => setLoading(false));
42 };
43
44 defineExpose({ total: computed(() => tableRef.value?.getCount()) });
45
46 const getIndexNumber = (rowIndex: number) => {
47 const pagination = tableRef.value?.getPagination();
48 return pagination.total - (pagination.current - 1) * pagination.pageSize - rowIndex;
49 };
50
51 // eslint-disable-next-line no-return-await
52 onMounted(() => tableRef.value?.onPageChange(1));
53 </script>
54
55 <style scoped></style>
1 <script setup lang="ts">
2 import { Form, FormItem, Input, Message, Select } from '@arco-design/web-vue';
3 import { computed, ref } from 'vue';
4 import AvatarUpload from '@/components/avatar-upload/index.vue';
5 import TagSelect from '@/components/tag-select/index.vue';
6 import ProjectSelect from '@/components/project-select/index.vue';
7 import { FieldRule, FormInstance } from '@arco-design/web-vue/es/form';
8 import { useSelectionStore } from '@/store';
9 import { get, set } from 'lodash';
10 import { useVModels } from '@vueuse/core';
11 import useActivityApi from '@/http/activity';
12 import { isEqual, isEqualArray } from '@arco-design/web-vue/es/_utils/is-equal';
13 import { IconFile } from '@arco-design/web-vue/es/icon';
14
15 const props = withDefaults(
16 defineProps<{
17 loading?: boolean;
18 modelValue?: any;
19 currentAttribute?: any;
20 lastAttribute?: any;
21 hideField?: string[];
22 filterProject?: (value: any) => boolean;
23 }>(),
24 {
25 filterProject: () => true,
26 }
27 );
28
29 const emits = defineEmits(['update:modelValue', 'update:loading']);
30
31 const formRef = ref<FormInstance>();
32 const { modelValue: formValue } = useVModels(props, emits);
33
34 const formRule = {
35 'cover': [{ type: 'string', required: true, message: '请上传活动封面' }],
36 'song_name': [{ type: 'string', required: true, message: '请输入歌曲名称' }],
37 'sub_title': [{ type: 'string', required: true, message: '请输入简介' }],
38 'expand.tag_ids': [
39 { type: 'array', required: true, message: '请选择关联标签' },
40 { type: 'array', maxLength: 3, message: '关联标签最多选中3个' },
41 ],
42 'lang': [
43 { type: 'string', required: true, message: '请选择语种' },
44 { type: 'array', maxLength: 2, message: '语种最多选中2个' },
45 ],
46 'speed': [{ type: 'string', required: true, message: '请选择语速' }],
47 'project_id': [{ type: 'number', min: 1, required: true, message: '请选择关联厂牌' }],
48 'weight': [{ type: 'string', required: true, message: '请选择权重' }],
49 } as Record<string, FieldRule[]>;
50
51 const tagIds = computed({
52 get: () => get(formValue.value, 'expand.tag_ids', []),
53 set: (val) => set(formValue.value, 'expand.tag_ids', val),
54 });
55
56 const { activityLangOptions, activitySpeedOptions } = useSelectionStore();
57
58 const onTagExceedLimitError = (content: string, duration = 1000) => Message.warning({ content, duration });
59
60 defineExpose({
61 onValid: (callback?: () => void) => formRef.value?.validate((errors) => !errors && callback?.()),
62 });
63 </script>
64
65 <template>
66 <Form ref="formRef" :model="formValue" :rules="formRule" :auto-label-width="true" size="small">
67 <FormItem label="封面图片" field="cover" :show-colon="true">
68 <AvatarUpload v-model="formValue.cover" :size="100" shape="square" />
69 <span v-if="currentAttribute && lastAttribute" style="width: 20px; margin-left: 10px">
70 <IconFile v-show="!isEqual(currentAttribute.cover, lastAttribute.cover)" />
71 </span>
72 </FormItem>
73 <FormItem label="歌曲名称" field="song_name" :show-colon="true">
74 <Input v-model="formValue.song_name" :max-length="100" :show-word-limit="true" placeholder="请输入" />
75 <span v-if="currentAttribute && lastAttribute" style="width: 20px; margin-left: 10px">
76 <IconFile v-show="!isEqual(currentAttribute.song_name, lastAttribute.song_name)" />
77 </span>
78 </FormItem>
79 <FormItem label="曲风标签" field="expand.tag_ids" :show-colon="true">
80 <TagSelect v-model="tagIds" :multiple="true" :limit="3" placeholder="请选择" limit-error-msg="关联标签最多选中3个" />
81 <span v-if="currentAttribute && lastAttribute" style="width: 20px; margin-left: 10px">
82 <IconFile v-show="!isEqualArray(currentAttribute.expand?.tag_ids, lastAttribute.expand?.tag_ids)" />
83 </span>
84 </FormItem>
85 <FormItem label="歌曲语种" field="lang" :show-colon="true">
86 <Select
87 v-model="formValue.lang"
88 :options="activityLangOptions"
89 :limit="2"
90 :field-names="{ value: 'identifier', label: 'name' }"
91 :fallback-option="false"
92 :multiple="true"
93 placeholder="请选择"
94 @exceed-limit="onTagExceedLimitError('关联语种最多选中2个')"
95 />
96 <span v-if="currentAttribute && lastAttribute" style="width: 20px; margin-left: 10px">
97 <IconFile v-show="!isEqualArray(currentAttribute.lang as [], lastAttribute.lang as [])" />
98 </span>
99 </FormItem>
100 <FormItem label="歌曲速度" field="speed" :show-colon="true">
101 <Select
102 v-model="formValue.speed"
103 :options="activitySpeedOptions"
104 :field-names="{ value: 'identifier', label: 'name' }"
105 :fallback-option="false"
106 placeholder="请选择"
107 />
108 <span v-if="currentAttribute && lastAttribute" style="width: 20px; margin-left: 10px">
109 <IconFile v-show="!isEqual(currentAttribute.speed, lastAttribute.speed)" />
110 </span>
111 </FormItem>
112 <FormItem label="歌曲简介" field="sub_title" :show-colon="true">
113 <Input v-model.trim="formValue.sub_title" :max-length="20" :show-word-limit="true" placeholder="举例:影视OST歌曲,急寻歌手" />
114 <span v-if="currentAttribute && lastAttribute" style="width: 20px; margin-left: 10px">
115 <IconFile v-show="!isEqual(currentAttribute.sub_title, lastAttribute.sub_title)" />
116 </span>
117 </FormItem>
118 <FormItem v-if="!hideField?.includes('project_id')" label="关联厂牌" field="project_id" :show-colon="true">
119 <ProjectSelect v-model="formValue.project_id" :filtrate="filterProject" />
120 <span v-if="currentAttribute && lastAttribute" style="width: 20px; margin-left: 10px">
121 <IconFile v-show="!isEqual(currentAttribute.project_id, lastAttribute.project_id)" />
122 </span>
123 </FormItem>
124 <FormItem label="推荐权重" field="weight" :show-colon="true">
125 <Select v-model="formValue.weight" :options="useActivityApi.weightOption" placeholder="请选择" />
126 <span v-if="currentAttribute && lastAttribute" style="width: 20px; margin-left: 10px">
127 <IconFile v-show="!isEqual(currentAttribute.weight, lastAttribute.weight)" />
128 </span>
129 </FormItem>
130 </Form>
131 </template>
132
133 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { DatePicker, Form, FormItem, InputTag, Select } from '@arco-design/web-vue';
3 import UserSelect from '@/components/user-select/index.vue';
4 import OutManageSelect from '@/views/audition/activity-audit/components/out-manage-select.vue';
5
6 import { ref, computed } from 'vue';
7 import { FieldRule, FormInstance } from '@arco-design/web-vue/es/form';
8 import { useVModels } from '@vueuse/core';
9 import { useSelectionStore } from '@/store';
10 import { get, set } from 'lodash';
11 import { storeToRefs } from 'pinia';
12 import { isEqual, isEqualArray } from '@arco-design/web-vue/es/_utils/is-equal';
13 import { IconFile } from '@arco-design/web-vue/es/icon';
14
15 const props = defineProps<{ loading?: boolean; modelValue?: any; currentAttribute?: any; lastAttribute?: any }>();
16 const emits = defineEmits(['update:modelValue', 'update:loading']);
17
18 const formRef = ref<FormInstance>();
19 const { modelValue: formValue } = useVModels(props, emits);
20
21 const { activitySexOptions, activityMarkOptions } = storeToRefs(useSelectionStore());
22
23 const booleanOptions = [
24 { label: '否', value: 0 },
25 { label: '是', value: 1 },
26 ];
27
28 const lyricistIds = computed({
29 get: () => get(formValue.value, 'expand.lyricist.ids', []),
30 set: (val) => set(formValue.value, 'expand.lyricist.ids', val),
31 });
32
33 const lyricistSupplement = computed({
34 get: () => get(formValue.value, 'expand.lyricist.supplement', []),
35 set: (val) => set(formValue.value, 'expand.lyricist.supplement', val),
36 });
37
38 const composerIds = computed({
39 get: () => get(formValue.value, 'expand.composer.ids', []),
40 set: (val) => set(formValue.value, 'expand.composer.ids', val),
41 });
42
43 const composerSupplement = computed({
44 get: () => get(formValue.value, 'expand.composer.supplement', []),
45 set: (val) => set(formValue.value, 'expand.composer.supplement', val),
46 });
47
48 const arrangerIds = computed({
49 get: () => get(formValue.value, 'expand.arranger.ids', []),
50 set: (val) => set(formValue.value, 'expand.arranger.ids', val),
51 });
52
53 const arrangerSupplement = computed({
54 get: () => get(formValue.value, 'expand.arranger.supplement', []),
55 set: (val) => set(formValue.value, 'expand.arranger.supplement', val),
56 });
57
58 const checkMaxUser = (value: any, cb: (error?: string) => void) => (value && value.length > 2 ? cb('最大选择2人') : {});
59
60 const checkNeedOne = (value: any, cb: (error?: string) => void, condition: boolean, message: string) => {
61 checkMaxUser(value, cb);
62 if (condition) {
63 cb(message);
64 }
65 };
66
67 const formRule = {
68 'sex': [{ required: true, message: '请选择性别要求' }],
69 'expand.lyricist.supplement': [
70 {
71 type: 'array',
72 validator: (value, cb) =>
73 checkNeedOne(
74 value,
75 cb,
76 lyricistIds.value.length === 0 && lyricistSupplement.value.length === 0,
77 '词作者(用户)或词作者(未注册),必填一个'
78 ),
79 },
80 ],
81 'expand.composer.supplement': [
82 {
83 type: 'array',
84 validator: (value, cb) =>
85 checkNeedOne(
86 value,
87 cb,
88 composerIds.value.length === 0 && composerSupplement.value.length === 0,
89 '曲作者(用户)或曲作者(未注册),必填一个'
90 ),
91 },
92 ],
93 'expand.arranger.supplement': [
94 {
95 type: 'array',
96 validator: (value, cb) =>
97 checkNeedOne(
98 value,
99 cb,
100 arrangerIds.value.length === 0 && arrangerSupplement.value.length === 0,
101 '编曲(用户)或编曲(未注册),必填一个'
102 ),
103 },
104 ],
105 'is_official': [{ required: true, message: '请选择是否有奖励标识' }],
106 'estimate_release_at': [{ required: true, message: '请选择预计发布时间' }],
107 } as Record<string, FieldRule[]>;
108
109 defineExpose({
110 onValid: (callback?: () => void) => formRef.value?.validate((errors) => !errors && callback?.()),
111 });
112 </script>
113
114 <template>
115 <Form ref="formRef" :model="formValue" :rules="formRule" :auto-label-width="true" size="small">
116 <FormItem label="词作者(用户)" field="expand.lyricist.ids" :show-colon="true">
117 <UserSelect v-model="lyricistIds" placeholder="请选择用户" :multiple="true" :allow-search="true" :limit="2" />
118 <span v-if="currentAttribute && lastAttribute" style="width: 20px; margin-left: 10px">
119 <IconFile v-show="!isEqualArray(currentAttribute.expand?.lyricist?.ids, lastAttribute.expand?.lyricist?.ids)" />
120 </span>
121 </FormItem>
122 <FormItem label="词作者(未注册)" field="expand.lyricist.supplement" :show-colon="true">
123 <InputTag v-model="lyricistSupplement" placeholder="输入完成回车键分隔" :max-tag-count="2" :unique-value="true" />
124 <span v-if="currentAttribute && lastAttribute" style="width: 20px; margin-left: 10px">
125 <IconFile v-show="!isEqualArray(currentAttribute.expand?.lyricist?.supplement, lastAttribute.expand?.lyricist?.supplement)" />
126 </span>
127 <template #extra>
128 <div>注意:上方填1项即可,歌曲信息中【用户】可点击跳转用户主页,【未注册】仅显示;</div>
129 </template>
130 </FormItem>
131 <FormItem label="曲作者(用户)" field="expand.composer.ids" :show-colon="true">
132 <UserSelect v-model="composerIds" placeholder="请选择用户" :multiple="true" :allow-search="true" :limit="2" />
133 <span v-if="currentAttribute && lastAttribute" style="width: 20px; margin-left: 10px">
134 <IconFile v-show="!isEqualArray(currentAttribute.expand?.composer?.ids, lastAttribute.expand?.composer?.ids)" />
135 </span>
136 </FormItem>
137 <FormItem label="曲作者(未注册)" field="expand.composer.supplement" :show-colon="true">
138 <InputTag v-model="composerSupplement" placeholder="输入完成回车键分隔" :max-tag-count="2" :unique-value="true" />
139 <span v-if="currentAttribute && lastAttribute" style="width: 20px; margin-left: 10px">
140 <IconFile v-show="!isEqualArray(currentAttribute.expand?.composer?.supplement, lastAttribute.expand?.composer?.supplement)" />
141 </span>
142 <template #extra>
143 <div>注意:上方填1项即可,歌曲信息中【用户】可点击跳转用户主页,【未注册】仅显示;</div>
144 </template>
145 </FormItem>
146 <FormItem label="编曲(用户)" field="expand.arranger.ids" :show-colon="true">
147 <UserSelect v-model="arrangerIds" placeholder="请选择用户" :multiple="true" :allow-search="true" :limit="2" />
148 <span v-if="currentAttribute && lastAttribute" style="width: 20px; margin-left: 10px">
149 <IconFile v-show="!isEqualArray(currentAttribute.expand?.arranger?.ids, lastAttribute.expand?.arranger?.ids)" />
150 </span>
151 </FormItem>
152 <FormItem label="编曲(未注册)" field="expand.arranger.supplement" :show-colon="true">
153 <InputTag v-model="arrangerSupplement" placeholder="输入完成回车键分隔" :max-tag-count="2" :unique-value="true" />
154 <span v-if="currentAttribute && lastAttribute" style="width: 20px; margin-left: 10px">
155 <IconFile v-show="!isEqualArray(currentAttribute.expand?.arranger?.supplement, lastAttribute.expand?.arranger?.supplement)" />
156 </span>
157 <template #extra>
158 <div>注意:上方填1项即可,歌曲信息中【用户】可点击跳转用户主页,【未注册】仅显示;</div>
159 </template>
160 </FormItem>
161 <FormItem label="奖励标识" field="is_official" :show-colon="true">
162 <Select v-model="formValue.is_official" :options="booleanOptions" placeholder="请选择" />
163 <span v-if="currentAttribute && lastAttribute" style="width: 20px; margin-left: 10px">
164 <IconFile v-show="!isEqual(currentAttribute.is_official, lastAttribute.is_official)" />
165 </span>
166 </FormItem>
167 <FormItem label="推荐角标" field="mark" :show-colon="true">
168 <Select
169 v-model="formValue.mark"
170 :options="activityMarkOptions"
171 :field-names="{ value: 'identifier', label: 'name' }"
172 :allow-clear="true"
173 placeholder="请选择"
174 />
175 <span v-if="currentAttribute && lastAttribute" style="width: 20px; margin-left: 10px">
176 <IconFile v-show="!isEqual(currentAttribute.mark, lastAttribute.mark)" />
177 </span>
178 </FormItem>
179 <FormItem label="性别要求" field="sex" :show-colon="true">
180 <Select
181 v-model="formValue.sex"
182 :options="activitySexOptions"
183 :field-names="{ value: 'identifier', label: 'name' }"
184 :fallback-option="false"
185 placeholder="请选择"
186 />
187 <span v-if="currentAttribute && lastAttribute" style="width: 20px; margin-left: 10px">
188 <IconFile v-show="!isEqual(currentAttribute.sex, lastAttribute.sex)" />
189 </span>
190 </FormItem>
191 <FormItem label="预计发行日期" field="estimate_release_at" :show-colon="true">
192 <DatePicker
193 v-model="formValue.estimate_release_at"
194 placeholder="请选择"
195 style="width: 100%"
196 :show-now-btn="false"
197 :allow-clear="false"
198 />
199 <span v-if="currentAttribute && lastAttribute" style="width: 20px; margin-left: 10px">
200 <IconFile v-show="!isEqual(currentAttribute.estimate_release_at, lastAttribute.estimate_release_at)" />
201 </span>
202 </FormItem>
203 <FormItem label="外部管理员" :show-colon="true">
204 <OutManageSelect v-model="formValue.out_side_manages" />
205 <span v-if="currentAttribute && lastAttribute" style="width: 20px; margin-left: 10px"> </span>
206 </FormItem>
207 </Form>
208 </template>
209
210 <style lang="less">
211 .arco-checkbox.arco-select-option-checkbox {
212 width: 100%;
213
214 .arco-checkbox-label {
215 width: 100%;
216 }
217 }
218 </style>
1 <script setup lang="ts">
2 import { ref, computed, createVNode } from 'vue';
3 import { FieldRule, FormInstance } from '@arco-design/web-vue/es/form';
4 import { useVModels } from '@vueuse/core';
5 import { Form, FormItem, Link, Textarea } from '@arco-design/web-vue';
6 import InputUpload from '@/components/input-upload/index.vue';
7 import { useSelectionStore } from '@/store';
8 import { first, get, last, set } from 'lodash';
9 import { bytesForHuman, getLyricTimeArr } from '@/utils';
10 import axios from 'axios';
11 import AudioPreview from '@/views/audition/activity-apply/components/audio-preview.vue';
12 import AudioPlayer from '@/components/audio-player/index.vue';
13 import { createModalVNode } from '@/utils/createVNode';
14 import { downloadFile } from '@/http/auth';
15 import { isEqual } from '@arco-design/web-vue/es/_utils/is-equal';
16 import { IconFile } from '@arco-design/web-vue/es/icon';
17
18 const props = defineProps<{ loading?: boolean; modelValue?: any; currentAttribute?: any; lastAttribute?: any }>();
19 const emits = defineEmits(['update:modelValue', 'update:loading']);
20
21 const formRef = ref<FormInstance>();
22 const { loading, modelValue: formValue } = useVModels(props, emits);
23
24 const guideSourceUrl = computed({
25 get: () => get(formValue.value, 'expand.guide_source.url', ''),
26 set: (val) => set(formValue.value, 'expand.guide_source.url', val),
27 });
28
29 const karaokeSourceUrl = computed({
30 get: () => get(formValue.value, 'expand.karaoke_source.url', ''),
31 set: (val) => set(formValue.value, 'expand.karaoke_source.url', val),
32 });
33
34 const trackSourceUrl = computed({
35 get: () => get(formValue.value, 'expand.track_source.url', ''),
36 set: (val) => set(formValue.value, 'expand.track_source.url', val),
37 });
38
39 const trackSourceName = computed(() => get(formValue.value, 'expand.track_source.name', ''));
40 const trackSourceSize = computed((): number => get(formValue.value, 'expand.track_source.size', 0));
41
42 const fullLyricTime = computed(() => getLyricTimeArr(formValue.value.lyric));
43 const clipLyricTime = computed(() => getLyricTimeArr(formValue.value.clip_lyric));
44 const checkClip = computed(
45 () => clipLyricTime.value.length !== 0 && fullLyricTime.value.toString().indexOf(clipLyricTime.value.toString()) !== -1
46 );
47
48 const onUpdateGuide = (file: { name: string; url: string; size: number }) => {
49 set(formValue.value, 'expand.guide_source', { name: file.name, url: file.url, size: file.size });
50 };
51
52 const onUpdateKaraoke = (file: { name: string; url: string; size: number }) => {
53 set(formValue.value, 'expand.karaoke_source', { name: file.name, url: file.url, size: file.size });
54 };
55
56 const onUpdateTrack = (file: { name: string; url: string; size: number }) => {
57 set(formValue.value, 'expand.track_source', { name: file.name, url: file.url, size: file.size });
58 };
59
60 const { activityAudioAccept, activityTrackAccept } = useSelectionStore();
61
62 const formRule = {
63 'expand.guide_source.url': [{ required: true, message: '请上传导唱文件' }],
64 'expand.karaoke_source.url': [{ required: true, message: '请上传伴奏文件' }],
65 'lyric': [
66 { type: 'string', required: true, message: '请填写歌词内容' },
67 {
68 type: 'string',
69 validator: (value: any, cb: (error?: string) => void) => (fullLyricTime.value.length === 0 ? cb('歌词文本格式不正确') : {}),
70 },
71 ],
72 'clip_lyric': [
73 { type: 'string', required: true, message: '请填写推荐歌词内容' },
74 { type: 'string', validator: (value: any, cb: (error?: string) => void) => (checkClip.value ? {} : cb('推荐片段不在歌词中')) },
75 ],
76 } as Record<string, FieldRule[]>;
77
78 const onDownload = (url: string, fileName: string) => downloadFile(url, `${formValue.value.song_name}(${fileName})`);
79
80 const onFormatLyric = (key: string) => {
81 formValue.value[key] = formValue.value[key]?.replace(/(\n[\s\t]*\r*\n)/g, '\n').replace(/^[\n\r\t]*|[\n\r\t]*$/g, '');
82 };
83
84 const guideFile = ref<File | undefined>();
85
86 const getGuideFile = async () => {
87 if (!guideFile.value) {
88 guideFile.value = await axios
89 .get(`${guideSourceUrl.value}`, { responseType: 'blob', timeout: 60000 })
90 .then(({ data }) => Promise.resolve(data));
91 }
92
93 return guideFile.value;
94 };
95
96 const onAudioPreview = async () => {
97 const src = await getGuideFile();
98 createModalVNode(() => createVNode(AudioPreview, { src, lyric: formValue.value.lyric }), {
99 title: '预览歌词-整首',
100 footer: false,
101 closable: true,
102 });
103 };
104
105 const onAudioClipPreview = async () => {
106 const src = await getGuideFile();
107 const startTime = first(clipLyricTime.value) as string;
108 const fullEndIndex = fullLyricTime.value.indexOf(last(clipLyricTime.value) as string);
109 const endTime = fullEndIndex === -1 ? undefined : fullLyricTime.value[fullEndIndex + 1] || undefined;
110
111 createModalVNode(() => createVNode(AudioPreview, { src, lyric: formValue.value.clip_lyric, startTime, endTime }), {
112 title: '预览歌词-片段',
113 footer: false,
114 closable: true,
115 });
116 };
117
118 defineExpose({
119 onValid: (callback?: () => void) => formRef.value?.validate((errors) => !errors && callback?.()),
120 });
121 </script>
122
123 <template>
124 <Form ref="formRef" :model="formValue" :rules="formRule" :auto-label-width="true" size="small">
125 <FormItem label="导唱文件(带伴奏)" field="expand.guide_source.url" :show-colon="true">
126 <InputUpload
127 v-model="guideSourceUrl"
128 :accept="activityAudioAccept"
129 :limit="100"
130 @success="onUpdateGuide"
131 @choose-file="(file:File) => (guideFile = file)"
132 @update:loading="(value:boolean) => (loading = value)"
133 />
134 <span v-if="currentAttribute && lastAttribute" style="width: 20px; margin-left: 10px">
135 <IconFile v-show="!isEqual(currentAttribute.expand?.guide_source?.url, lastAttribute.expand?.guide_source?.url)" />
136 </span>
137 </FormItem>
138 <FormItem v-if="guideSourceUrl">
139 <template #label>
140 <Link icon @click="onDownload(guideSourceUrl, '导唱文件')">下载导唱</Link>
141 </template>
142 <AudioPlayer name="导唱文件" :url="guideSourceUrl" />
143 <span v-if="currentAttribute && lastAttribute" style="width: 20px; margin-left: 10px"> </span>
144 </FormItem>
145 <FormItem label="伴奏文件" field="expand.karaoke_source.url" :show-colon="true">
146 <InputUpload
147 v-model="karaokeSourceUrl"
148 :accept="activityAudioAccept"
149 :limit="100"
150 @success="onUpdateKaraoke"
151 @update:loading="(value:boolean) => (loading = value)"
152 />
153 <span v-if="currentAttribute && lastAttribute" style="width: 20px; margin-left: 10px">
154 <IconFile v-show="!isEqual(currentAttribute.expand?.karaoke_source?.url, lastAttribute.expand?.karaoke_source?.url)" />
155 </span>
156 </FormItem>
157 <FormItem v-if="karaokeSourceUrl">
158 <template #label>
159 <Link icon @click="onDownload(karaokeSourceUrl, '伴奏文件')">下载伴奏</Link>
160 </template>
161 <AudioPlayer name="伴奏文件" :url="karaokeSourceUrl" />
162 <span v-if="currentAttribute && lastAttribute" style="width: 20px; margin-left: 10px"> </span>
163 </FormItem>
164 <FormItem label="分轨文件" :show-colon="true">
165 <InputUpload v-model="trackSourceUrl" :accept="activityTrackAccept" @success="onUpdateTrack" />
166 <span v-if="currentAttribute && lastAttribute" style="width: 20px; margin-left: 10px">
167 <IconFile v-show="!isEqual(currentAttribute.expand?.track_source?.url, lastAttribute.expand?.track_source?.url)" />
168 </span>
169 </FormItem>
170 <FormItem v-if="trackSourceUrl">
171 <template #label>
172 <Link icon @click="onDownload(karaokeSourceUrl, '分轨文件')">下载分轨文件</Link>
173 </template>
174 {{ trackSourceName }} {{ bytesForHuman(trackSourceSize) }}
175 <span v-if="currentAttribute && lastAttribute" style="width: 20px; margin-left: 10px"> </span>
176 </FormItem>
177 <FormItem class="lyric" label="歌词文本" field="lyric" :show-colon="true">
178 <Textarea
179 v-model="formValue.lyric"
180 :auto-size="{ minRows: 6, maxRows: 6 }"
181 placeholder="请粘贴带时间的lrc歌词文本至输入框"
182 @blur="() => onFormatLyric('lyric')"
183 />
184 <span v-if="currentAttribute && lastAttribute" style="width: 20px; margin-left: 10px">
185 <IconFile v-show="!isEqual(currentAttribute.lyric, lastAttribute.lyric)" />
186 </span>
187 <template #extra>
188 <Link :hoverable="false" :disabled="!guideSourceUrl || !formValue.lyric" @click="onAudioPreview()"> 预览歌词</Link>
189 <span v-if="currentAttribute && lastAttribute" style="width: 20px; margin-left: 30px"></span>
190 </template>
191 </FormItem>
192 <FormItem class="lyric" label="片段歌词" field="clip_lyric" :show-colon="true">
193 <Textarea
194 v-model="formValue.clip_lyric"
195 :auto-size="{ minRows: 6, maxRows: 6 }"
196 placeholder="请粘贴带时间的lrc歌词文本至输入框"
197 @blur="() => onFormatLyric('clip_lyric')"
198 />
199 <span v-if="currentAttribute && lastAttribute" style="width: 20px; margin-left: 10px">
200 <IconFile v-show="!isEqual(currentAttribute.clip_lyric, lastAttribute.clip_lyric)" />
201 </span>
202 <template #extra>
203 <Link :hoverable="false" :disabled="!guideSourceUrl || !checkClip" @click="onAudioClipPreview()"> 预览歌词</Link>
204 <span v-if="currentAttribute && lastAttribute" style="width: 20px; margin-left: 30px"> </span>
205 </template>
206 </FormItem>
207 </Form>
208 </template>
209
210 <style scoped lang="less">
211 .lyric {
212 :deep(.arco-form-item-extra) {
213 width: 100%;
214 text-align: right;
215 }
216 }
217 </style>
1 <script setup lang="ts">
2 import { CheckboxGroup, Form, FormItem, RadioGroup, Transfer } from '@arco-design/web-vue';
3 import { computed, ref } from 'vue';
4 import { FieldRule, FormInstance } from '@arco-design/web-vue/es/form';
5 import { get, set } from 'lodash';
6 import { useVModels } from '@vueuse/core';
7 import { AnyObject } from '@/types/global';
8 import { storeToRefs } from 'pinia';
9 import { useSelectionStore } from '@/store';
10
11 const props = withDefaults(
12 defineProps<{
13 loading?: boolean;
14 modelValue?: any;
15 currentAttribute?: AnyObject;
16 lastAttribute?: AnyObject;
17 filterProject?: (value: any) => boolean;
18 hideMode?: boolean;
19 disabledTag?: boolean;
20 }>(),
21 {
22 filterProject: () => true,
23 hideMode: false,
24 disabledTag: true,
25 }
26 );
27
28 const emits = defineEmits(['update:modelValue', 'update:loading']);
29
30 const formRef = ref<FormInstance>();
31 const { modelValue: formValue } = useVModels(props, emits);
32
33 const pushType = computed({
34 get: () => get(formValue.value, 'expand.push_type', ['tag']),
35 set: (val) => set(formValue.value, 'expand.push_type', val),
36 });
37
38 const pushUser = computed({
39 get: () => get(formValue.value, 'expand.push_user', []),
40 set: (val) => set(formValue.value, 'expand.push_user', val),
41 });
42
43 const formRule = {
44 'is_push': [{ required: true, message: '请选择是否推送' }],
45 'expand.push_type': [{ required: formValue.value.is_push === 1, message: '请选择推送方式' }],
46 'expand.push_user': [{ required: pushType.value.includes('user'), message: '请选择推送歌手' }],
47 } as Record<string, FieldRule[]>;
48
49 const isPushOptions = [
50 { label: '无推送', value: 0 },
51 { label: '有推送', value: 1 },
52 ];
53
54 const pushTypeOptions = [
55 { label: '智能推送(歌曲标签的用户)', value: 'tag', disabled: props.disabledTag },
56 { label: '定向推送', value: 'user' },
57 ];
58
59 defineExpose({
60 onValid: (callback?: () => void) => formRef.value?.validate((errors) => !errors && callback?.()),
61 });
62
63 const onChangeMode = (value: number) => {
64 pushType.value = value === 0 ? [] : ['tag'];
65 pushUser.value = [];
66 };
67
68 const { getUserOptions } = storeToRefs(useSelectionStore());
69
70 const userOptions = computed(() =>
71 getUserOptions.value.map((item) => {
72 return { value: item.id, label: item.nick_name };
73 })
74 );
75 </script>
76
77 <template>
78 <Form ref="formRef" :model="formValue" :rules="formRule" :auto-label-width="true" size="small">
79 <FormItem v-if="!hideMode" label="是否推送" :show-colon="true" field="is_push">
80 <RadioGroup v-model="formValue.is_push" :options="isPushOptions" @change="onChangeMode" />
81 </FormItem>
82 <FormItem v-if="formValue.is_push === 1" :show-colon="true" label="推送方式">
83 <CheckboxGroup v-model="pushType" :options="pushTypeOptions" />
84 </FormItem>
85 <FormItem v-if="formValue.is_push === 1 && pushType?.indexOf('user') !== -1" :show-colon="true" label="推送歌手" row-class="mb-0">
86 <Transfer
87 v-model="pushUser"
88 :data="userOptions"
89 size="mini"
90 :title="['用户列表', '推送用户']"
91 :show-search="true"
92 :one-way="true"
93 :source-input-search-props="{ placeholder: '请输入', size: 'mini' }"
94 :target-input-search-props="{ placeholder: '请输入', size: 'mini' }"
95 />
96 <!-- <UserSelect v-model="pushUser" multiple allow-search allow-clear />-->
97 </FormItem>
98 </Form>
99 </template>
100
101 <style scoped lang="less">
102 :deep(.arco-transfer-view) {
103 width: 240px;
104 height: 300px;
105 }
106 </style>
1 <script setup lang="ts">
2 import { computed, createVNode, onMounted, ref } from 'vue';
3
4 import useLoading from '@/hooks/loading';
5 import { AnyObject } from '@/types/global';
6
7 import ModalContent from '@/views/audition/activity-audit/components/modal-content.vue';
8
9 import { useAuthorizedStore, useSelectionStore } from '@/store';
10 import { Message, ModalReturn, TableData } from '@arco-design/web-vue';
11 import { tryOnMounted } from '@vueuse/core';
12 import { createModalVNode } from '@/utils/createVNode';
13 import { useApplyApi } from '@/http/activity';
14 import UserTableColumn from '@/components/filter/user-table-column.vue';
15 import EnumTableColumn from '@/components/filter/enum-table-column.vue';
16 import usePermission from '@/hooks/permission';
17 import SpaceTableColumn from '@/components/filter/space-table-column.vue';
18 import DateTableColumn from '@/components/filter/date-table-column.vue';
19 import ActivityTableColumn from '@/components/filter/activity-table-column.vue';
20
21 const tableRef = ref();
22
23 const { loading, setLoading } = useLoading(false);
24 const filter = ref<AnyObject>({});
25
26 const { get, audit, statusOption } = useApplyApi;
27
28 const dialog = ref<ModalReturn>();
29
30 const queryParams = computed(() => {
31 return {
32 ...filter.value,
33 songType: 1,
34 setColumn: [
35 'id',
36 'song_name',
37 'cover',
38 'lang',
39 'speed',
40 'sub_title',
41 'sex',
42 'user_id',
43 'project_id',
44 'send_url',
45 'send_url',
46 'lyric',
47 'clip_lyric',
48 'is_push',
49 'is_official',
50 'mark',
51 'expand',
52 'weight',
53 'status',
54 'estimate_release_at',
55 'created_at',
56 ],
57 setWith: [
58 'project:id,name',
59 'user:id,real_name,nick_name,role',
60 'tags:id,name',
61 'applyRecords:id,activity_id,current,audit_msg,created_at',
62 ],
63 setSort: ['-updated_at'],
64 };
65 });
66
67 const onQuery = async (params?: AnyObject) => {
68 setLoading(true);
69 return get({ ...queryParams.value, ...params }).finally(() => setLoading(false));
70 };
71
72 const onSearch = () => tableRef.value?.onPageChange(1);
73
74 const onReset = () => {
75 filter.value = { songName: '', userName: '', projectName: '', tagName: '', auditStatus: '', createBetween: [] };
76 onSearch();
77 };
78
79 const onAudit = (record: TableData) => {
80 dialog.value = createModalVNode(
81 () =>
82 createVNode(ModalContent, {
83 row: record,
84 activeKey: 'audit',
85 currentAttribute: record,
86 lastAttribute: record.last_record,
87 submit: (value: AnyObject) =>
88 audit(record.id, value).then(() => {
89 Message.success(`审核歌曲:${record?.song_name}`);
90 tableRef.value?.onFetch();
91 useAuthorizedStore().syncAuditActivity();
92 dialog.value?.close();
93 }),
94 }),
95 { titleAlign: 'center', title: '歌曲上架申请', modalClass: 'retry', footer: false, maskClosable: true }
96 );
97 };
98
99 const onView = (record: TableData) => {
100 dialog.value = createModalVNode(
101 () =>
102 createVNode(ModalContent, {
103 row: record,
104 submitText: '关闭',
105 activeKey: 'record',
106 type: 'view',
107 submit: () => Promise.resolve(dialog.value?.close()),
108 }),
109 { title: '歌曲上架申请', titleAlign: 'center', modalClass: 'retry', footer: false, maskClosable: true, escToClose: true }
110 );
111 };
112
113 tryOnMounted(async () => useSelectionStore().queryAll());
114
115 // eslint-disable-next-line no-return-assign
116 onMounted(() => {
117 onReset();
118 useAuthorizedStore().syncAuditActivity();
119 });
120 </script>
121
122 <template>
123 <page-view has-bread has-card>
124 <filter-search :model="filter" :loading="loading" @search="onSearch" @reset="onReset">
125 <filter-search-item field="song_name" label="歌曲名称">
126 <a-input v-model="filter.songName" :allow-clear="true" placeholder="请输入" />
127 </filter-search-item>
128 <filter-search-item field="projectName" label="厂牌名称">
129 <a-input v-model="filter.projectName" :allow-clear="true" placeholder="请输入" />
130 </filter-search-item>
131 <filter-search-item field="tagName" label="标签名称">
132 <a-input v-model="filter.tagName" :allow-clear="true" placeholder="请输入" />
133 </filter-search-item>
134 <filter-search-item field="userName" label="创建人">
135 <a-input v-model="filter.userName" :allow-clear="true" placeholder="请输入" />
136 </filter-search-item>
137 <filter-search-item field="createBetween" label="创建时间">
138 <a-range-picker v-model="filter.createBetween" :allow-clear="true" />
139 </filter-search-item>
140 <filter-search-item field="audit_status" label="状态">
141 <a-select v-model="filter.auditStatus" :options="statusOption" placeholder="请选择" />
142 </filter-search-item>
143 </filter-search>
144 <filter-table ref="tableRef" :loading="loading" :on-query="onQuery">
145 <activity-table-column :width="460" data-index="id" title="试唱歌曲" />
146 <user-table-column :width="160" data-index="user_id" user="user" title="创建人" show-href />
147 <a-table-column :width="120" data-index="apply_records" title="是否驳回过">
148 <template #cell="{ record }">
149 <span v-if="record.record_count !== 0"></span>
150 <span v-else style="color: rgba(44, 44, 44, 0.5)"></span>
151 </template>
152 </a-table-column>
153 <date-table-column data-index="created_at" title="创建时间" :width="100" split />
154 <enum-table-column data-index="audit_status" title="状态" :option="useApplyApi.statusOption" :width="120" />
155 <space-table-column
156 v-if="usePermission().checkPermission(['audition-activity-audit-audit', 'audition-activity-audit-log'])"
157 :width="120"
158 data-index="operations"
159 title="操作"
160 fixed="right"
161 >
162 <template #default="{ record }">
163 <a-link
164 v-if="record.audit_status === 0"
165 v-permission="['audition-activity-audit-audit']"
166 :hoverable="false"
167 class="link-hover"
168 @click="onAudit(record)"
169 >
170 审核
171 </a-link>
172 <a-link
173 v-if="record.audit_status === 2"
174 v-permission="['audition-activity-audit-log']"
175 :hoverable="false"
176 class="link-hover"
177 @click="onView(record)"
178 >
179 查看驳回信息
180 </a-link>
181 </template>
182 </space-table-column>
183 </filter-table>
184 </page-view>
185 </template>
186
187 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { computed, onMounted, ref } from 'vue';
3 import useLoading from '@/hooks/loading';
4
5 import LinkTableColumn from '@/components/filter/link-table-column.vue';
6 import { useReListApi } from '@/http/activity';
7 import { AnyObject, QueryForParams } from '@/types/global';
8 import UserTableColumn from '@/components/filter/user-table-column.vue';
9
10 const { loading, setLoading } = useLoading(false);
11 const filter = ref({ activityName: '', projectName: '', userName: '', brokerName: '', operatorName: '', createBetween: [] });
12 const tableRef = ref();
13
14 const queryParams = computed((): QueryForParams => {
15 return {
16 ...filter.value,
17 setWith: [
18 'activity:id,song_name',
19 'project:id,name',
20 'user:id,nick_name,identity',
21 'broker:id,nick_name,identity',
22 'operator:id,nick_name,identity',
23 ],
24 sortBy: 'id',
25 sortType: 'desc',
26 };
27 });
28
29 const onQuery = async (params: AnyObject) => {
30 setLoading(true);
31 return useReListApi.get({ ...queryParams.value, ...params }).finally(() => setLoading(false));
32 };
33
34 const onExport = () => useReListApi.getExport('重新上架记录', queryParams.value);
35
36 const onSearch = () => tableRef.value?.onPageChange(1);
37
38 const onReset = () => {
39 filter.value = { activityName: '', projectName: '', userName: '', brokerName: '', operatorName: '', createBetween: [] };
40 onSearch();
41 };
42
43 onMounted(() => onReset());
44 </script>
45
46 <template>
47 <page-view has-bread has-card>
48 <filter-search :loading="loading" :model="filter" @search="onSearch" @reset="onReset">
49 <filter-search-item label="歌曲名">
50 <a-input v-model="filter.activityName" placeholder="请输入" allow-clear />
51 </filter-search-item>
52 <filter-search-item label="歌曲厂牌">
53 <a-input v-model="filter.projectName" placeholder="请输入" allow-clear />
54 </filter-search-item>
55 <filter-search-item label="合作用户">
56 <a-input v-model="filter.userName" placeholder="请输入" allow-clear />
57 </filter-search-item>
58 <filter-search-item label="经纪人">
59 <a-input v-model="filter.brokerName" placeholder="请输入" allow-clear />
60 </filter-search-item>
61 <filter-search-item label="操作人">
62 <a-input v-model="filter.operatorName" placeholder="请输入" allow-clear />
63 </filter-search-item>
64 <filter-search-item label="操作时间">
65 <a-range-picker
66 v-model="filter.createBetween"
67 :time-picker-props="{ defaultValue: ['00:00:00', '23:59:59'] }"
68 :day-start-of-week="1"
69 value-format="YYYY-MM-DD HH:mm:ss"
70 format="YYYY-MM-DD"
71 />
72 </filter-search-item>
73 </filter-search>
74 <filter-table ref="tableRef" :loading="loading" :on-query="onQuery">
75 <template #tool-right>
76 <export-button v-permission="['audition-activity-relist-log-export']" label="导出" :on-download="onExport" />
77 </template>
78 <link-table-column
79 data-index="activity_id"
80 title="歌曲名"
81 :width="200"
82 :formatter="(record:any) => record.activity?.song_name"
83 :to="(record:any) => $router?.push({ name: 'audition-activity-show', params: { id: record.activity_id } })"
84 />
85 <link-table-column
86 data-index="project"
87 title="歌曲厂牌"
88 :width="200"
89 :formatter="(record:any) => record.project?.name"
90 :to="(record:any) => $router?.push({ name: 'audition-project-show', params: { id: record.project_id } })"
91 />
92 <user-table-column data-index="user_id" title="合作用户" user="user" :show-href="true" :width="160" />
93 <user-table-column data-index="broker_id" title="经纪人" user="broker" :show-href="true" dark-value="" :width="160" />
94 <filter-table-column data-index="broker_level" title="经纪人分类" :width="200">
95 <template #default="{ record }: { record: { broker_level: string } }">
96 <span v-if="record.broker_level">{{ record.broker_level }}</span>
97 <span v-else style="color: rgba(44, 44, 44, 0.5)"></span>
98 </template>
99 </filter-table-column>
100 <user-table-column data-index="operator_id" title="操作人" user="operator" :show-href="true" :width="160" />
101 <filter-table-column data-index="created_at" title="操作时间" :width="180" />
102 </filter-table>
103 </page-view>
104 </template>
105
106 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { Activity } from '@/types/activity';
3 import { computed } from 'vue';
4 import { User } from '@/types/user';
5 import UserTag from '@/views/audition/activity-show/components/user-tag.vue';
6 import { unionBy } from 'lodash';
7 import useActivityApi from '@/http/activity';
8
9 const props = defineProps<{ data: Activity }>();
10
11 const links = computed(() => {
12 return unionBy(props.data.links || [], 'id');
13 });
14
15 const inSideLyricist = computed(
16 (): User[] => links.value?.filter((item: User) => props.data.expand?.lyricist?.ids?.indexOf(item.id) !== -1) || []
17 );
18 const outSideLyricist = computed(() => props.data.expand?.lyricist?.supplement || []);
19
20 const inSideComposer = computed(
21 (): User[] => links.value?.filter((item: User) => props.data.expand?.composer?.ids?.indexOf(item.id) !== -1) || []
22 );
23 const outSideComposer = computed(() => props.data.expand?.composer?.supplement || []);
24
25 const inSideArranger = computed(
26 (): User[] => links.value?.filter((item: User) => props.data.expand?.arranger?.ids?.indexOf(item.id) !== -1) || []
27 );
28 const outSideArranger = computed(() => props.data.expand?.arranger?.supplement || []);
29
30 const { statusOption } = useActivityApi;
31 </script>
32
33 <template>
34 <a-card :bordered="false">
35 <a-form :model="data" auto-label-width label-align="left">
36 <a-layout>
37 <a-layout-sider :width="130" style="background: none; box-shadow: none; padding-top: 6px">
38 <a-image show-loader :height="130" :width="130" :src="data.cover" />
39 </a-layout-sider>
40 <a-layout-content style="margin-left: 16px">
41 <a-form-item :hide-label="true">
42 <div class="title">{{ data.song_name }}</div>
43 <a-tag v-for="item in data.tags" :key="item.id" size="small" style="margin-right: 5px">
44 {{ item.name }}
45 </a-tag>
46 <span style="font-size: 10px">
47 {{ statusOption.find((item) => item.value === data.status)?.label }}
48 </span>
49 </a-form-item>
50 <a-form-item
51 :label-col-style="{ flex: 0 }"
52 :wrapper-col-style="{ flex: 'unset', width: 'inherit' }"
53 :show-colon="true"
54 label="关联厂牌"
55 >
56 <div v-if="data.project">{{ data.project.name }}</div>
57 <div v-else></div>
58 </a-form-item>
59 <a-form-item hide-label>
60 <a-form-item :label-col-style="{ flex: 0 }" :show-colon="true" label="作词">
61 <user-tag v-for="item in inSideLyricist" :key="item.id" :user="item" style="margin-right: 5px" />
62 <user-tag v-for="item in outSideLyricist" :key="`lyricist-${item}`" :user="{ nick_name: item }" style="margin-right: 5px" />
63 </a-form-item>
64 <a-form-item :label-col-style="{ flex: 0 }" :show-colon="true" label="作曲">
65 <user-tag v-for="item in inSideComposer" :key="item.id" :user="item" style="margin-right: 5px" />
66 <user-tag v-for="item in outSideComposer" :key="`lyricist-${item}`" :user="{ nick_name: item }" style="margin-right: 5px" />
67 </a-form-item>
68 <a-form-item :label-col-style="{ flex: 0 }" :show-colon="true" label="编曲">
69 <user-tag v-for="item in inSideArranger" :key="item.id" :user="item" style="margin-right: 5px" />
70 <user-tag v-for="item in outSideArranger" :key="`lyricist-${item}`" :user="{ nick_name: item }" style="margin-right: 5px" />
71 </a-form-item>
72 </a-form-item>
73 <a-form-item hide-label>
74 <a-form-item :label-col-style="{ flex: 0 }" :show-colon="true" label="创建信息">
75 <span v-if="data.user" style="margin-right: 8px">{{ data.user.nick_name }}</span>
76 <span style="margin-right: 8px">{{ data.created_at }} </span>
77 </a-form-item>
78 <a-form-item :label-col-style="{ flex: 0 }" :show-colon="true" label="预计发行时间">
79 {{ data.estimate_release_at }}
80 </a-form-item>
81 <a-form-item :label-col-style="{ flex: 0 }"> </a-form-item>
82 </a-form-item>
83 <a-form-item label="推荐语" :show-colon="true" :label-col-style="{ flex: 0 }">
84 <a-typography-paragraph
85 style="margin-bottom: 0; width: 100%; color: var(--color-text-2)"
86 :ellipsis="{ rows: 1, showTooltip: true }"
87 >
88 {{ data.sub_title }}
89 </a-typography-paragraph>
90 </a-form-item>
91 </a-layout-content>
92 </a-layout>
93 </a-form>
94 </a-card>
95 </template>
96
97 <style lang="less" scoped>
98 .arco-form-item {
99 margin-bottom: 0;
100 }
101
102 .arco-form-item-label-col {
103 flex: 0;
104 }
105
106 .title {
107 font-size: 16px;
108 font-weight: bold;
109 margin-right: 8px;
110 }
111
112 .right {
113 margin: 0 20px;
114 min-width: 600px;
115 }
116 </style>
1 <script setup lang="ts">
2 import { onMounted, ref } from 'vue';
3 import useLoading from '@/hooks/loading';
4 import useActivityApi from '@/http/activity';
5
6 import { Input, Select } from '@arco-design/web-vue';
7 import UserTableColumn from '@/components/filter/user-table-column.vue';
8 import EnumTableColumn from '@/components/filter/enum-table-column.vue';
9 import UserIdentityTableColumn from '@/components/filter/user-identity-table-column.vue';
10 import NumberTableColumn from '@/components/filter/number-table-column.vue';
11
12 const props = defineProps<{ activityId: number; queryHook?: () => void }>();
13
14 const { loading, setLoading } = useLoading(false);
15 const filter = ref({ nick_name: '', real_name: '', sex: '', identity: '', scope: '' });
16 const tableRef = ref();
17
18 const { likeUsers } = useActivityApi;
19
20 const sexOptions = [
21 { value: 1, label: '男' },
22 { value: 2, label: '女' },
23 { value: 0, label: '无' },
24 ];
25
26 const roleOptions = [
27 { value: '0', label: '未认证 ' },
28 { value: '1,3', label: '音乐人' },
29 { value: '2,3', label: '经纪人' },
30 ];
31
32 const scopeOptions = [
33 { value: 0, label: '无' },
34 { value: 1, label: '平台管理员' },
35 { value: 2, label: '厂牌管理员' },
36 ];
37
38 const onQuery = async (params: any) => {
39 setLoading(true);
40 props.queryHook?.();
41 return likeUsers(props.activityId, {
42 ...filter.value,
43 identity: filter.value?.identity.split(',').filter(Number) || '',
44 ...params,
45 sortBy: 'last_listen_at',
46 sortType: 'desc',
47 }).finally(() => {
48 setLoading(false);
49 });
50 };
51
52 const onSearch = () => tableRef.value?.onPageChange(1);
53
54 const onReset = () => {
55 filter.value = { nick_name: '', real_name: '', sex: '', identity: '', scope: '' };
56 onSearch();
57 };
58
59 onMounted(() => onReset());
60 </script>
61
62 <template>
63 <FilterSearch :loading="loading" :model="filter" @search="onSearch" @reset="onReset">
64 <FilterSearchItem label="用户艺名">
65 <Input v-model="filter.nick_name" placeholder="请输入" />
66 </FilterSearchItem>
67 <FilterSearchItem label="性别">
68 <Select v-model="filter.sex" :options="sexOptions" placeholder="请选择" allow-clear />
69 </FilterSearchItem>
70 <FilterSearchItem label="身份">
71 <Select v-model="filter.identity" :options="roleOptions" placeholder="请选择" allow-clear />
72 </FilterSearchItem>
73 <FilterSearchItem label="权限">
74 <Select v-model="filter.scope" :options="scopeOptions" placeholder="请选择" allow-clear />
75 </FilterSearchItem>
76 </FilterSearch>
77 <FilterTable ref="tableRef" :loading="loading" :on-query="onQuery">
78 <UserTableColumn title="用户艺名" data-index="id" :width="260" show-href show-avatar />
79 <EnumTableColumn title="性别" data-index="sex" :width="100" :option="sexOptions" :dark-value="0" />
80 <UserIdentityTableColumn title="身份" data-index="identity" :width="100" />
81 <EnumTableColumn title="权限" data-index="scope" :width="120" :option="scopeOptions" :dark-value="0" />
82 <NumberTableColumn title="试听歌曲数量" data-index="listen_count" :width="120" :dark-value="0" />
83 <NumberTableColumn title="收藏歌曲数量" data-index="collection_count" :width="120" :dark-value="0" />
84 <NumberTableColumn title="试唱歌曲数量" data-index="submit_work" :width="120" :dark-value="0" />
85 <FilterTableColumn title="最后试听时间" data-index="last_listen_at" :width="180" />
86 </FilterTable>
87 </template>
88
89 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { ref, onMounted } from 'vue';
3 import FilterSearch from '@/components/filter/search.vue';
4 import FilterSearchItem from '@/components/filter/search-item.vue';
5 import FilterTable from '@/components/filter/table.vue';
6 import { Input, Divider, TableColumn } from '@arco-design/web-vue';
7 import { AnyObject } from '@/types/global';
8 import PhoneTableColumn from '@/components/filter/phone-table-column.vue';
9 import useLoading from '@/hooks/loading';
10 import useUserApi from '@/http/user';
11
12 const props = defineProps<{ activityId: number }>();
13 const filter = ref({ nick_name: '', real_name: '', emailLike: '', phoneLike: '', status: 1 });
14 const tableRef = ref<InstanceType<typeof FilterTable>>();
15
16 const { loading, setLoading } = useLoading(false);
17 const selectedKeys = ref([]);
18
19 const onQuery = (params?: AnyObject) => {
20 setLoading(true);
21
22 return useUserApi
23 .get({
24 exclude_manage_activity: props.activityId,
25 ...filter.value,
26 ...params,
27 sortBy: 'id',
28 sortType: 'desc',
29 })
30 .finally(() => setLoading(false));
31 };
32
33 const onSearch = () => tableRef.value?.onPageChange(1);
34 const onReset = () => {
35 filter.value = { nick_name: '', real_name: '', emailLike: '', phoneLike: '', status: 1 };
36 onSearch();
37 };
38
39 defineExpose({ selectedKeys });
40
41 onMounted(() => onReset());
42 </script>
43
44 <template>
45 <FilterSearch :loading="loading" :model="filter" size="small" @search="onSearch" @reset="onReset">
46 <FilterSearchItem field="nick_name" label="用户艺名">
47 <Input v-model="filter.nick_name" allow-clear placeholder="请输入" />
48 </FilterSearchItem>
49 <FilterSearchItem field="real_name" label="用户真名">
50 <Input v-model="filter.real_name" allow-clear placeholder="请输入" />
51 </FilterSearchItem>
52 <FilterSearchItem field="email" label="用户邮箱">
53 <Input v-model="filter.emailLike" allow-clear placeholder="请输入" />
54 </FilterSearchItem>
55 <FilterSearchItem field="phone" label="手机号码">
56 <Input v-model="filter.phoneLike" allow-clear placeholder="请输入" />
57 </FilterSearchItem>
58 </FilterSearch>
59 <Divider style="margin-top: 0" />
60 <FilterTable
61 ref="tableRef"
62 v-model:selectedKeys="selectedKeys"
63 size="small"
64 :loading="loading"
65 style="height: 360px"
66 :scroll="{ y: 360 }"
67 :row-selection="{ type: 'checkbox' }"
68 :on-query="onQuery"
69 >
70 <TableColumn title="用户艺名" data-index="nick_name" :width="160" :tooltip="true" :ellipsis="true" />
71 <TableColumn title="用户真名" data-index="real_name" :width="140" :tooltip="true" :ellipsis="true" />
72 <TableColumn title="用户邮箱" data-index="email" :width="140" :tooltip="true" :ellipsis="true" />
73 <PhoneTableColumn title="用户手机" data-index="phone" area-index="area_code" :width="140" :tooltip="true" :ellipsis="true" />
74 </FilterTable>
75 </template>
76
77 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import useLoading from '@/hooks/loading';
3 import { createVNode, onMounted, ref } from 'vue';
4 import useActivityApi, { useManagerApi } from '@/http/activity';
5 import { AnyObject } from '@/types/global';
6 import PhoneTableColumn from '@/components/filter/phone-table-column.vue';
7 import EnumTableColumn from '@/components/filter/enum-table-column.vue';
8 import ManagerForm from '@/views/audition/activity-show/components/manager-form.vue';
9
10 import { createFormVNode, createModalVNode, createSelectionFormItemVNode, createSelectVNode } from '@/utils/createVNode';
11 import { Input, Select, Button, Link, Grid, GridItem, Message, TableData } from '@arco-design/web-vue';
12 import FilterTable from '@/components/filter/table.vue';
13 import UserTableColumn from '@/components/filter/user-table-column.vue';
14 import SpaceTableColumn from '@/components/filter/space-table-column.vue';
15 import usePermission from '@/hooks/permission';
16
17 const { loading, setLoading } = useLoading(false);
18 const { manageUsers } = useActivityApi;
19 const { statusOption, sexOption, officialStatusOption, permissionOption, create, update, destroy } = useManagerApi;
20
21 const props = defineProps<{ activityId: number; queryHook?: () => void }>();
22 const tableRef = ref();
23 const filter = ref<AnyObject>({});
24
25 const onQuery = async (params?: AnyObject) => {
26 setLoading(true);
27 props.queryHook?.();
28 return manageUsers(props.activityId, { ...filter.value, ...params, sortBy: 'user_id', sortType: 'desc' }).finally(() =>
29 setLoading(false)
30 );
31 };
32
33 const onSearch = () => tableRef.value?.onPageChange(1);
34 const onReset = () => {
35 filter.value = { nick_name: '', email: '', phone: '', permission: '', status: '' };
36 onSearch();
37 };
38
39 const onCreate = () => {
40 const formValue = ref<string[]>(['view']);
41 const creating = ref<boolean>(false);
42 const formRef = ref();
43
44 const modal = createModalVNode(() => createVNode(ManagerForm, { activityId: props.activityId, ref: formRef }), {
45 title: '新增外部管理用户',
46 titleAlign: 'center',
47 width: '1000px',
48 closable: true,
49 footer: () =>
50 createVNode(Grid, { cols: 6 }, () => [
51 createVNode(GridItem, { span: 2, style: { textAlign: 'left' } }, () =>
52 createSelectVNode(formValue, permissionOption, {
53 'multiple': true,
54 'onUpdate:modelValue': (val?: string[]) => {
55 if (!val?.includes('view')) {
56 val?.unshift('view');
57 }
58 formValue.value = val || [];
59 },
60 })
61 ),
62 createVNode(GridItem, { offset: 3 }, () =>
63 createVNode(
64 Button,
65 {
66 type: 'primary',
67 loading: creating.value,
68 onClick: () => {
69 if (formRef.value.selectedKeys.length === 0) {
70 return Message.warning('请先选择添加用户');
71 }
72 creating.value = true;
73 return create({ activity_id: props.activityId, user_ids: formRef.value.selectedKeys, permission: formValue.value })
74 .then(() => {
75 Message.success('添加成功');
76 tableRef.value?.onFetch();
77 modal.close();
78 })
79 .finally(() => {
80 creating.value = false;
81 });
82 },
83 },
84 () => '确认'
85 )
86 ),
87 ]),
88 });
89 };
90
91 const onUpdate = (record: TableData) => {
92 const formValue = ref<string[]>(record?.permission || ['view']);
93 return createModalVNode(
94 () =>
95 createFormVNode(
96 { layout: 'vertical', model: {} },
97 createSelectionFormItemVNode(
98 formValue,
99 permissionOption,
100 { label: '设置用户权限', rowClass: 'mb-0' },
101 {
102 'multiple': true,
103 'onUpdate:modelValue': (val?: string[]) => {
104 if (!val?.includes('view')) {
105 val?.unshift('view');
106 }
107 formValue.value = val || [];
108 },
109 }
110 )
111 ),
112 {
113 title: '修改',
114 titleAlign: 'center',
115 onBeforeOk: (done: any) =>
116 update(record.id, { permission: formValue.value })
117 .then(() => {
118 Message.success('更新成功');
119 tableRef.value?.onFetch();
120 done(true);
121 })
122 .catch(() => done(false)),
123 }
124 );
125 };
126
127 const onDelete = (record: TableData) => {
128 createModalVNode(`确认取消用户:${record.user?.nick_name} 的外部管理员身份`, {
129 title: '删除操作',
130 onBeforeOk: (done: any) =>
131 destroy(record.id)
132 .then(() => {
133 Message.success('删除成功');
134 tableRef.value?.onFetch();
135 done(true);
136 })
137 .catch(() => done(false)),
138 });
139 };
140
141 const formatPermission = (permission: any[]) =>
142 permission
143 .map((item) => `[${permissionOption.find((option) => option.value === item)?.label}]` || '')
144 .filter((item) => item.length !== 0)
145 .join('');
146
147 onMounted(() => onReset());
148 </script>
149
150 <template>
151 <FilterSearch :model="filter" :loading="loading" @search="onSearch" @reset="onReset">
152 <FilterSearchItem field="nick_name" label="用户艺名">
153 <Input v-model="filter.nick_name" allow-clear placeholder="请输入" />
154 </FilterSearchItem>
155 <FilterSearchItem field="email" label="用户邮箱">
156 <Input v-model="filter.email" allow-clear placeholder="请输入" />
157 </FilterSearchItem>
158 <FilterSearchItem field="phone" label="用户手机">
159 <Input v-model="filter.phone" allow-clear placeholder="请输入" />
160 </FilterSearchItem>
161 <FilterSearchItem field="" label="外部权限">
162 <Select v-model="filter.permission" :options="permissionOption" allow-clear placeholder="请选择" />
163 </FilterSearchItem>
164 <FilterSearchItem field="status" label="状态">
165 <Select v-model="filter.status" :options="statusOption" allow-clear placeholder="请选择" />
166 </FilterSearchItem>
167 </FilterSearch>
168 <FilterTable ref="tableRef" :loading="loading" :on-query="onQuery">
169 <template #tool>
170 <IconButton v-permission="['audition-activity-show-out-manage-create']" icon="plus" label="新增" type="primary" @click="onCreate" />
171 </template>
172 <UserTableColumn title="用户艺名" data-index="user_id" user="user" :width="160" show-avatar />
173 <EnumTableColumn title="性别" data-index="user.sex" :option="sexOption" :dark-value="0" :width="70" />
174 <FilterTableColumn title="用户邮箱" data-index="user.email" :width="170" />
175 <PhoneTableColumn title="手机号码" data-index="user.phone" area-index="user.area_code" :width="160" />
176 <EnumTableColumn title="关注服务号" data-index="user.official_status" :option="officialStatusOption" :dark-value="0" :width="120" />
177 <FilterTableColumn title="外部权限" data-index="permission" :width="200">
178 <template #default="{ record }"> {{ formatPermission(record.permission) }}</template>
179 </FilterTableColumn>
180 <EnumTableColumn title="状态" data-index="user.status" :option="statusOption" :width="80" />
181 <SpaceTableColumn
182 v-if="usePermission().checkPermission(['audition-activity-show-out-manage-edit', 'audition-activity-show-out-manage-delete'])"
183 :width="120"
184 data-index="operations"
185 title="操作"
186 :tooltip="false"
187 :ellipsis="false"
188 >
189 <template #default="{ record }">
190 <Link v-permission="['audition-activity-show-out-manage-edit']" class="link-hover" :hoverable="false" @click="onUpdate(record)">
191 修改
192 </Link>
193 <Link v-permission="['audition-activity-show-out-manage-delete']" class="link-hover" :hoverable="false" @click="onDelete(record)">
194 取消管理
195 </Link>
196 </template>
197 </SpaceTableColumn>
198 </FilterTable>
199 </template>
200
201 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { AnyObject } from '@/types/global';
3 import useLoading from '@/hooks/loading';
4 import useActivityApi from '@/http/activity';
5 import { onMounted, ref } from 'vue';
6
7 import { TableColumn } from '@arco-design/web-vue';
8 import FilterTable from '@/components/filter/table.vue';
9 import UserTableColumn from '@/components/filter/user-table-column.vue';
10 import UserInfo from '@/views/audition/activity-show/components/user-info.vue';
11
12 const props = defineProps<{ activityId: number; queryHook?: () => void }>();
13
14 const { loading, setLoading } = useLoading(false);
15 const tableRef = ref();
16
17 const onQuery = async (params: AnyObject) => {
18 setLoading(true);
19 props.queryHook?.();
20 return useActivityApi
21 .matchUser(props.activityId, {
22 setColumn: ['id', 'submit_at', 'mode', 'demo_url', 'user_id', 'broker_id'],
23 setWith: ['user:id,avatar,nick_name,real_name,identity,email,province,city', 'broker:id,nick_name,identity'],
24 ...params,
25 })
26 .finally(() => setLoading(false));
27 };
28
29 onMounted(() => tableRef.value?.onPageChange());
30 </script>
31
32 <template>
33 <FilterTable ref="tableRef" :loading="loading" :on-query="onQuery">
34 <TableColumn :width="160" data-index="user_id " title="用户艺名">
35 <template #cell="{ record }">
36 <UserInfo v-if="record.user" :row="record.user" />
37 </template>
38 </TableColumn>
39 <UserTableColumn :width="140" data-index="broker_id" user="broker" title="队长" show-href />
40 <TableColumn :width="100" data-index="mode" title="试唱方式">
41 <template #cell="{ record }">{{ record.mode === 1 ? '自主上传' : '在线演唱' }}</template>
42 </TableColumn>
43 <TableColumn :width="180" data-index="submit_at" title="提交时间" />
44 <TableColumn :width="480" data-index="demo_url" title="Demo">
45 <template #cell="{ record }">
46 <audio-player :name="record.song_name" :url="record.demo_url" />
47 </template>
48 </TableColumn>
49 </FilterTable>
50 </template>
51
52 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { Modal, Textarea } from '@arco-design/web-vue';
3 import { computed, h } from 'vue';
4 import { ActivityExpand } from '@/types/activity-apply';
5 import { useAppStore } from '@/store';
6 import { downloadFile } from '@/http/auth';
7
8 type MaterialData = {
9 title: string;
10 type: string;
11 content: string;
12 name?: string;
13 size?: string;
14 };
15
16 const props = defineProps<{ data: { expand: ActivityExpand; lyric: string; song_name: string } }>();
17
18 const appStore = useAppStore();
19 const theme = computed(() => appStore.theme);
20
21 const lyricStyle = computed(() =>
22 theme.value === 'light' ? { border: 'none', backgroundColor: 'white' } : { border: 'none', backgroundColor: '#2a2a2b' }
23 );
24
25 const bytesForHuman = (bytes: number, decimals = 2) => {
26 const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
27
28 let i = 0;
29
30 // eslint-disable-next-line no-plusplus
31 for (i; bytes > 1024; i++) {
32 bytes /= 1024;
33 }
34
35 return `${parseFloat(bytes.toFixed(decimals))} ${units[i]}`;
36 };
37
38 const onDownload = (record: MaterialData) => downloadFile(record.content, `${props.data.song_name}(${record.title})`);
39
40 const onViewLyric = (lyric: string) => {
41 Modal.open({
42 content: () => h(Textarea, { defaultValue: lyric, autoSize: { maxRows: 20 }, style: lyricStyle.value }),
43 footer: false,
44 closable: false,
45 });
46 };
47
48 const materials = computed((): MaterialData[] => {
49 return [
50 {
51 title: '导唱',
52 type: 'guide',
53 content: props.data.expand?.guide_source?.url || '',
54 },
55 {
56 title: '伴唱',
57 type: 'karaoke',
58 content: props.data.expand?.karaoke_source?.url || '',
59 },
60 {
61 title: '分轨文件',
62 type: 'track',
63 content: props.data.expand?.track_source?.url || '',
64 name: props.data.expand?.track_source?.name || '',
65 size: bytesForHuman(props.data.expand?.track_source?.size || 0),
66 },
67 { title: '歌词', type: 'Lyric', content: props.data.lyric },
68 ];
69 });
70 </script>
71
72 <template>
73 <a-table row-key="type" :data="materials" :bordered="false" :table-layout-fixed="true" :pagination="false">
74 <template #columns>
75 <a-table-column title="物料类型" align="center" data-index="title" :width="140" />
76 <a-table-column title="音频播放" data-index="content">
77 <template #cell="{ record }">
78 <template v-if="record.content">
79 <audio-player v-if="['guide', 'karaoke'].indexOf(record.type) !== -1" :name="record.type" :url="record.content" />
80 <div v-if="record.type === 'track'">{{ [record.name, record.size].join(',') }}</div>
81 </template>
82 </template>
83 </a-table-column>
84 <a-table-column title="操作" align="center" :width="200">
85 <template #cell="{ record }">
86 <template v-if="record.content">
87 <a-button v-if="record.type === 'Lyric'" type="primary" size="small" @click="onViewLyric(record.content)">查看</a-button>
88 <a-button v-else type="primary" size="small" @click="onDownload(record)">下载</a-button>
89 </template>
90 </template>
91 </a-table-column>
92 </template>
93 </a-table>
94 </template>
95
96 <style scoped lang="less"></style>
1 <template>
2 <FilterSearch :loading="loading" :model="filter" :inline="true" @search="onSearch" @reset="onReset">
3 <FilterSearchItem label="用户艺名">
4 <Input v-model="filter.searchUser" allow-clear placeholder="请搜索" />
5 </FilterSearchItem>
6 <FilterSearchItem label="经纪人">
7 <Input v-model="filter.searchBusiness" allow-clear placeholder="请搜索" />
8 </FilterSearchItem>
9 <FilterSearchItem label="试唱方式">
10 <Select v-model="filter.mode" :options="modeOption" allow-clear placeholder="请筛选" />
11 </FilterSearchItem>
12 <template #button>
13 <ExportButton v-permission="['audition-activity-show-submit-user-export']" :on-download="onExport" />
14 </template>
15 </FilterSearch>
16 <FilterTable ref="tableRef" :loading="loading" :on-query="onQuery" :hide-expand-button-on-empty="true">
17 <TableColumn :width="160" data-index="user_id " title="用户艺名">
18 <template #cell="{ record }">
19 <UserInfo v-if="record.children && record.user" :row="record.user" />
20 </template>
21 </TableColumn>
22 <UserTableColumn :width="140" data-index="business_id" user="business" title="队长" dark-value="" show-href />
23 <TableColumn :width="100" data-index="mode" title="试唱方式">
24 <template #cell="{ record }">{{ record.mode === 1 ? '自主上传' : '在线演唱' }}</template>
25 </TableColumn>
26 <TableColumn v-if="usePermission().checkPermission('audition-activity-show-submit-user-view')" title="合作模式" :width="100">
27 <template #cell="{ record }">
28 <template v-if="record.price && record.children">
29 <Link class="link-hover" :hoverable="false" @click="onViewPrice(record.price)">查看报价</Link>
30 </template>
31 </template>
32 </TableColumn>
33 <TableColumn :width="100" data-index="version" title="提交版本">
34 <template #cell="{ record }">
35 <Link class="link-hover" :hoverable="false" icon @click="onDownload(record)">{{ `版本V${record.version}` }}</Link>
36 </template>
37 </TableColumn>
38 <TableColumn :width="460" data-index="demo_url" title="Demo">
39 <template #cell="{ record }">
40 <AudioPlayer :name="record.song_name" :url="record.demo_url" />
41 </template>
42 </TableColumn>
43 <SpaceTableColumn :width="130" fixed="right" title="操作">
44 <template #default="{ record }: { record: ActivityWork }">
45 <template v-if="record.children">
46 <Button v-if="record.status === 1" size="mini" type="primary" status="success">已确认合作</Button>
47 <Button v-else-if="record.status === 2" size="mini">试唱不合适</Button>
48 <Button v-else-if="record.status === 0 && [3, 5].indexOf(record.activity_status) !== -1" size="mini">未采纳</Button>
49 <template v-else-if="record.status === 0 && record.activity_status === 1">
50 <Space v-if="usePermission().checkPermission('audition-activity-show-submit-user-audit')">
51 <Button type="primary" size="mini" @click="onPass(record)">合作</Button>
52 <Button type="primary" status="danger" size="mini" @click="onUnPass(record)">不合适</Button>
53 </Space>
54 <Button v-else size="mini">试唱待确认</Button>
55 </template>
56 <Button v-else size="mini">其他</Button>
57 </template>
58 </template>
59 </SpaceTableColumn>
60 </FilterTable>
61 </template>
62
63 <script setup lang="ts">
64 import useLoading from '@/hooks/loading';
65 import { onMounted, ref, createVNode } from 'vue';
66 import useActivityApi, { useWorkApi } from '@/http/activity';
67 import { AnyObject, QueryForParams } from '@/types/global';
68 import { Input, Select, Space, Button, TableColumn, TableData, Link } from '@arco-design/web-vue';
69
70 import FilterSearch from '@/components/filter/search.vue';
71 import FilterSearchItem from '@/components/filter/search-item.vue';
72 import FilterTable from '@/components/filter/table.vue';
73 import UserTableColumn from '@/components/filter/user-table-column.vue';
74 import SpaceTableColumn from '@/components/filter/space-table-column.vue';
75 import UserInfo from '@/views/audition/activity-show/components/user-info.vue';
76 import AudioPlayer from '@/components/audio-player/index.vue';
77
78 import { createFormItemVNode, createFormVNode, createModalVNode } from '@/utils/createVNode';
79 import { downloadFile } from '@/http/auth';
80 import { UserSubmitPrice } from '@/utils/model';
81 import { promiseToBoolean } from '@/utils';
82 import { ActivityWork } from '@/types/activity-work';
83 import usePermission from '@/hooks/permission';
84
85 const props = defineProps<{ activityId: number; exportName?: string; queryHook?: () => void }>();
86 const emits = defineEmits<{ (e: 'matchWork', value: ActivityWork): void }>();
87
88 const { loading, setLoading } = useLoading(false);
89 const filter = ref<QueryForParams>({ searchUser: '', searchBusiness: '', mode: '' });
90 const tableRef = ref();
91
92 const { submitUser, getSubmitUserExport } = useActivityApi;
93
94 const modeOption = [
95 { value: 1, label: '自主上传' },
96 { value: 0, label: '在线演唱' },
97 ];
98
99 const onQuery = async (params: AnyObject) => {
100 setLoading(true);
101
102 props.queryHook?.();
103 return submitUser(props.activityId, {
104 ...filter.value,
105 ...params,
106 sortBy: 'id',
107 sortType: 'desc',
108 }).finally(() => {
109 setLoading(false);
110 });
111 };
112
113 const onSearch = () => tableRef.value?.onPageChange(1);
114
115 const onReset = () => {
116 filter.value = {
117 searchUser: '',
118 searchBusiness: '',
119 mode: '',
120 setColumn: [
121 'id',
122 'user_id',
123 'business_id',
124 'share_id',
125 'price_id',
126 'activity_id',
127 'activity_name',
128 'activity_status',
129 'project_id',
130 'submit_at',
131 'mode',
132 'type',
133 'status',
134 'created_at',
135 'demo_url',
136 'version',
137 ],
138 setWith: [
139 'user:id,avatar,nick_name,real_name,identity,email,province,city,company,rate',
140 'business:id,nick_name,identity',
141 'share:id,nick_name,identity',
142 'price:id,value,is_deduct,is_talk,is_accept_address,address_id',
143 'project:id,is_promote',
144 'children:id,user_id,activity_id,mode,version,demo_url',
145 ],
146 };
147 onSearch();
148 };
149
150 onMounted(() => onReset());
151
152 const onExport = async () => getSubmitUserExport(props.activityId, props.exportName as string, filter.value);
153
154 const onDownload = (row: TableData) => {
155 return downloadFile(row.demo_url, `${row.activity_name}-${row.user.nick_name}-V${row.version}`);
156 };
157
158 const onViewPrice = (price: UserSubmitPrice) => {
159 createModalVNode(
160 () =>
161 createFormVNode({ size: 'small', autoLabelWidth: true }, [
162 createFormItemVNode(
163 { label: '唱酬', showColon: true, rowClass: 'mb-0' },
164 price.value.is_reward ? `${price.value.amounts} 元` : '无'
165 ),
166 createFormItemVNode(
167 { label: '分成', showColon: true, rowClass: 'mb-0' },
168 price.value.is_dividend ? `${price.value.ratio}% | ${price.value.year} | ${price.is_deduct ? '抵扣' : '不抵扣'}` : '无'
169 ),
170 createFormItemVNode({ label: '价格是否可谈', showColon: true, rowClass: 'mb-0' }, price.is_talk ? '【可谈】' : '【不可谈】'),
171 createFormItemVNode(
172 { label: '录音地点', showColon: true, rowClass: 'mb-0' },
173 `${[price.address?.parent?.name, price.address?.name].join('/')}${
174 price.is_accept_address ? '【接受其他录音地点】' : '【不接受其他录音地点】'
175 }`
176 ),
177 ]),
178 { title: '用户报价', hideCancel: true, bodyStyle: { padding: '8px 20px' }, okText: '我知道了' }
179 );
180 };
181
182 const onPass = (row: TableData) => {
183 const itemStyle = { lineHeight: '28px', margin: '4px 0' };
184
185 return createModalVNode(
186 () =>
187 createVNode('ol', {}, [
188 createVNode('li', { style: itemStyle }, '此条Demo的状态将变更为合作状态'),
189 createVNode('li', { style: itemStyle }, `活动《${row.activity_name}》的状态变更为完成状态!`),
190 ]),
191 {
192 title: '确认合作',
193 bodyStyle: { padding: 0 },
194 onBeforeOk: () => promiseToBoolean(useWorkApi.changeStatus(row.id, { status: 1 })),
195 onOk: () => {
196 tableRef.value?.onFetch();
197 emits('matchWork', row as ActivityWork);
198 },
199 }
200 );
201 };
202
203 const onUnPass = (row: TableData) =>
204 createModalVNode(`请确认是否将用户:${row.user.nick_name} 提交的《版本V${row.version}》标记为不合适,并反馈给用户?`, {
205 title: '不适合标记',
206 onBeforeOk: () => promiseToBoolean(useWorkApi.changeStatus(row.id, { status: 2, remark: '' })),
207 onOk: () => tableRef.value?.onFetch(),
208 });
209 </script>
210
211 <style scoped lang="less">
212 .prop {
213 display: flex;
214 align-items: flex-start;
215 }
216
217 .prop-label {
218 font-weight: bold;
219 width: 80px;
220 text-align: right;
221 }
222
223 :deep(.arco-table-cell) {
224 padding: 5px 8px !important;
225
226 :hover {
227 cursor: pointer;
228 }
229
230 & > .arco-table-td-content .arco-btn-size-small {
231 padding: 5px !important;
232 }
233 }
234 </style>
1 <template>
2 <a-popover position="right">
3 <user-link :user="row as BaseUser" :show-tooltip="false" />
4 <template #content>
5 <p class="prop">
6 <span class="label">邮箱:</span>
7 <span>{{ row.email }}</span>
8 </p>
9 <p class="prop">
10 <span class="label">身份:</span>
11 <span>{{ roleLabel }}</span>
12 </p>
13 <p class="prop">
14 <span class="label">常居地:</span>
15 <span>
16 {{ [row.province, row.city].join('-') }}
17 </span>
18 </p>
19 </template>
20 </a-popover>
21 </template>
22
23 <script lang="ts" setup>
24 import { computed, toRefs } from 'vue';
25 import { User } from '@/types/user';
26 import { BaseUser } from '@/utils/model';
27
28 const props = defineProps<{
29 row: Pick<User, 'id' | 'nick_name' | 'real_name' | 'email' | 'province' | 'city' | 'company' | 'rate' | 'identity'>;
30 }>();
31 const { row } = toRefs(props);
32
33 const roleLabel = computed(() => {
34 if (row.value.identity === 1) {
35 return '音乐人';
36 }
37 if ([2, 3].includes(row.value.identity)) {
38 return '经纪人';
39 }
40
41 return '未认证';
42 });
43 </script>
44
45 <style lang="less" scoped>
46 .prop {
47 display: flex;
48 align-items: flex-start;
49
50 .label {
51 font-weight: bold;
52 width: 80px;
53 }
54 }
55 </style>
1 <script setup lang="ts">
2 import { computed, toRef } from 'vue';
3 import { IconRight } from '@arco-design/web-vue/es/icon';
4 import { useRouter } from 'vue-router';
5
6 const props = defineProps<{ user: { id?: number; nick_name: string; identity?: number } }>();
7
8 const user = toRef(props, 'user');
9
10 const cursor = computed(() => (props.user.id ? 'pointer' : 'default'));
11
12 const router = useRouter();
13 const toUserInfo = () => {
14 switch (user?.value?.identity) {
15 case 0:
16 router.push({ name: 'user-register-show', params: { id: user?.value?.id } });
17 break;
18 case 1:
19 router.push({ name: 'user-singer-show', params: { id: user?.value?.id } });
20 break;
21 case 2:
22 case 3:
23 router.push({ name: 'user-business-show', params: { id: user?.value?.id } });
24 break;
25 default:
26 }
27 };
28 </script>
29
30 <template>
31 <a-tag class="link" size="small" @click="toUserInfo">
32 <span>{{ user.nick_name }}</span>
33 <IconRight v-if="user.id" style="margin-left: 8px" />
34 </a-tag>
35 </template>
36
37 <style scoped lang="less">
38 .link:hover {
39 cursor: v-bind(cursor);
40 }
41 </style>
1 <script setup lang="ts">
2 import { onMounted, ref } from 'vue';
3 import useLoading from '@/hooks/loading';
4 import useActivityApi from '@/http/activity';
5
6 import { Input, Select } from '@arco-design/web-vue';
7 import UserTableColumn from '@/components/filter/user-table-column.vue';
8 import EnumTableColumn from '@/components/filter/enum-table-column.vue';
9 import UserIdentityTableColumn from '@/components/filter/user-identity-table-column.vue';
10 import NumberTableColumn from '@/components/filter/number-table-column.vue';
11 import { AnyObject } from '@/types/global';
12
13 const props = defineProps<{ activityId: number; queryHook?: () => void }>();
14
15 const { loading, setLoading } = useLoading(false);
16 const filter = ref<AnyObject>({});
17 const tableRef = ref();
18
19 const { viewUsers } = useActivityApi;
20
21 const sexOptions = [
22 { value: 1, label: '男' },
23 { value: 2, label: '女' },
24 { value: 0, label: '无' },
25 ];
26
27 const roleOptions = [
28 { value: '0', label: '未认证 ' },
29 { value: '1,3', label: '音乐人' },
30 { value: '2,3', label: '经纪人' },
31 ];
32
33 const scopeOptions = [
34 { value: 0, label: '无' },
35 { value: 1, label: '平台管理员' },
36 { value: 2, label: '厂牌管理员' },
37 ];
38
39 const onQuery = async (params: any) => {
40 setLoading(true);
41 props.queryHook?.();
42 return viewUsers(props.activityId, {
43 ...filter.value,
44 identity:
45 filter.value?.identity
46 ?.toString()
47 .split(',')
48 ?.filter((item) => item !== '') || '',
49 ...params,
50 sortBy: 'last_listen_at',
51 sortType: 'desc',
52 }).finally(() => {
53 setLoading(false);
54 });
55 };
56
57 const onSearch = () => tableRef.value?.onPageChange(1);
58
59 const onReset = () => {
60 filter.value = { nick_name: '', sex: '', identity: '', scope: '' };
61 onSearch();
62 };
63
64 onMounted(() => onReset());
65 </script>
66
67 <template>
68 <FilterSearch :loading="loading" :model="filter" @search="onSearch" @reset="onReset">
69 <FilterSearchItem label="用户艺名">
70 <Input v-model="filter.nick_name" placeholder="请输入" />
71 </FilterSearchItem>
72 <FilterSearchItem label="性别">
73 <Select v-model="filter.sex" :options="sexOptions" placeholder="请选择" allow-clear />
74 </FilterSearchItem>
75 <FilterSearchItem label="身份">
76 <Select v-model="filter.identity" :options="roleOptions" placeholder="请选择" allow-clear />
77 </FilterSearchItem>
78 <FilterSearchItem label="权限">
79 <Select v-model="filter.scope" :options="scopeOptions" placeholder="请选择" allow-clear />
80 </FilterSearchItem>
81 </FilterSearch>
82 <FilterTable ref="tableRef" :loading="loading" :on-query="onQuery">
83 <UserTableColumn title="用户艺名" data-index="id" :width="260" show-href show-avatar />
84 <EnumTableColumn title="性别" data-index="sex" :width="100" :option="sexOptions" :dark-value="0" />
85 <UserIdentityTableColumn title="身份" data-index="identity" :width="100" />
86 <EnumTableColumn title="权限" data-index="scope" :width="120" :option="scopeOptions" :dark-value="0" />
87 <NumberTableColumn title="试听歌曲数量" data-index="listen_count" :width="120" :dark-value="0" />
88 <NumberTableColumn title="收藏歌曲数量" data-index="collection_count" :width="120" :dark-value="0" />
89 <NumberTableColumn title="试唱歌曲数量" data-index="submit_work" :width="120" :dark-value="0" />
90 <FilterTableColumn title="最后试听时间" data-index="last_listen_at" :width="180" />
91 </FilterTable>
92 </template>
93
94 <style scoped lang="less"></style>
1 <template>
2 <page-view has-bread>
3 <BasicCard :data="activity" :loading="!activity" />
4
5 <a-card v-if="tabKeys.length" :bordered="false" style="margin-top: 20px">
6 <a-tabs v-model:active-key="tabKey" :animation="true" :justify="true" :header-padding="false" type="rounded" size="small">
7 <a-tab-pane v-if="checkPermission('submit-user')" key="submit-user" :title="`参与试唱(${total.submit_total})`">
8 <submit-table
9 :activity-id="activityKey"
10 :export-name="activity.song_name"
11 :query-hook="syncSubmitCount"
12 @match-work="syncActivity"
13 />
14 </a-tab-pane>
15 <a-tab-pane v-if="checkPermission('match-user')" key="match-user" :title="`合作用户(${total.match_total})`">
16 <match-table :activity-id="activityKey" :query-hook="syncMatchCount" />
17 </a-tab-pane>
18 <a-tab-pane v-if="checkPermission('material')" key="material" title="物料列表">
19 <MaterialTable :data="activity" />
20 </a-tab-pane>
21 <a-tab-pane v-if="checkPermission('view-user')" key="view-user" :title="`试听用户(${total.view_total})`">
22 <view-user-table :activity-id="activityKey" :query-hook="syncViewCount" />
23 </a-tab-pane>
24 <a-tab-pane v-if="checkPermission('like-user')" key="like-user" :title="`收藏用户(${total.like_total})`">
25 <like-user-table :activity-id="activityKey" :query-hook="syncLikeCount" />
26 </a-tab-pane>
27 <a-tab-pane v-if="checkPermission('out-manage')" key="out-manage" :title="`外部管理员(${total.manager_total})`">
28 <manager-table :activity-id="activityKey" :query-hook="syncManagerCount" />
29 </a-tab-pane>
30 <a-tab-pane v-if="checkPermission('operation-log')" key="operation-log" title="操作日志">
31 <operation-table :filter="{ activity_id: activityKey }" :has-search="false" :show-guard="false" />
32 </a-tab-pane>
33 </a-tabs>
34 </a-card>
35 </page-view>
36 </template>
37
38 <script lang="ts" setup>
39 import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router';
40 import { computed, onMounted, ref } from 'vue';
41 import { useRouteQuery } from '@vueuse/router';
42 import useActivityApi from '@/http/activity';
43
44 import BasicCard from '@/views/audition/activity-show/components/basic-card.vue';
45 import MaterialTable from '@/views/audition/activity-show/components/material-table.vue';
46 import SubmitTable from '@/views/audition/activity-show/components/submit-table.vue';
47 import MatchTable from '@/views/audition/activity-show/components/match-table.vue';
48 import OperationTable from '@/views/system/logs/components/query-table.vue';
49 import ViewUserTable from '@/views/audition/activity-show/components/view-user-table.vue';
50 import LikeUserTable from '@/views/audition/activity-show/components/like-user-table.vue';
51 import ManagerTable from '@/views/audition/activity-show/components/manager-table.vue';
52 import { isArray } from '@/utils/is';
53 import usePermission from '@/hooks/permission';
54
55 const routeName = useRoute()?.name as string;
56
57 const checkPermission = (permission: string | string[]): boolean => {
58 return usePermission().checkPermission(
59 isArray(permission) ? permission.map((item) => `${routeName}-${item}`) : `${routeName}-${permission}`
60 );
61 };
62
63 const tabKeys = computed((): string[] =>
64 ['submit-user', 'match-user', 'material', 'view-user', 'like-user', 'out-manage', 'operation-log'].filter((item) =>
65 checkPermission(item)
66 )
67 );
68
69 const tabKey = useRouteQuery('tabKey', tabKeys.value[0] || '');
70
71 const activityKey = Number(useRoute().params?.id);
72
73 const activity = ref<any>({});
74
75 const total = ref({ submit_total: 0, match_total: 0, view_total: 0, like_total: 0, manager_total: 0 });
76
77 const syncSubmitCount = () =>
78 useActivityApi.submitUser(activityKey, { pageSize: 1, limit: 1 }).then(({ meta }) => (total.value.submit_total = meta.total));
79
80 const syncMatchCount = () =>
81 useActivityApi.matchUser(activityKey, { pageSize: 1, limit: 1 }).then(({ meta }) => (total.value.match_total = meta.total));
82
83 const syncViewCount = () =>
84 useActivityApi.viewUsers(activityKey, { pageSize: 1, limit: 1 }).then(({ meta }) => (total.value.view_total = meta.total));
85
86 const syncLikeCount = () =>
87 useActivityApi.likeUsers(activityKey, { pageSize: 1, limit: 1 }).then(({ meta }) => (total.value.like_total = meta.total));
88
89 const syncManagerCount = () =>
90 useActivityApi.manageUsers(activityKey, { pageSize: 1, limit: 1 }).then(({ meta }) => (total.value.manager_total = meta.total));
91
92 const syncActivity = () =>
93 useActivityApi
94 .show(activityKey, {
95 songType: 1,
96 setWith: [
97 'project:id,name,is_promote,is_can_manage',
98 'tags:id,name',
99 'user:id,nick_name,real_name,identity',
100 'links:id,nick_name,identity',
101 ],
102 setColumn: [
103 'id',
104 'song_name',
105 'cover',
106 'sub_title',
107 'lyric',
108 'clip_lyric',
109 'expand',
110 'status',
111 'estimate_release_at',
112 'created_at',
113 'song_type',
114 'project_id',
115 'user_id',
116 ],
117 })
118 .then((data) => {
119 activity.value = data;
120 });
121
122 const router = useRouter();
123 onMounted(() => syncActivity().catch(() => router.replace({ name: 'exception-404' })));
124
125 onBeforeRouteLeave((to, from) => {
126 if (from.meta.from === to.name) {
127 to.meta.reload = false;
128 }
129 });
130 </script>
131
132 <style scoped></style>
1 <template>
2 <page-view has-bread has-card>
3 <filter-search :model="filter" :loading="loading" @search="onSearch" @reset="onReset">
4 <filter-search-item label="歌曲名称" field="song_name">
5 <a-input v-model="filter.songName" allow-clear placeholder="请输入搜索歌曲名称" />
6 </filter-search-item>
7 <filter-search-item v-if="!hideSearchItem?.includes('projectName')" label="厂牌名称" field="projectName">
8 <a-input v-model="filter.projectName" allow-clear placeholder="请输入搜索厂牌名称" />
9 </filter-search-item>
10 <filter-search-item label="标签名称" field="tagName">
11 <a-input v-model="filter.tagName" allow-clear placeholder="请输入搜索标签名称" />
12 </filter-search-item>
13 <!-- <filter-search-item label="推荐权重" field="weight">-->
14 <!-- <a-select v-model="filter.weight" :options="weightOption" allow-clear multiple placeholder="请输入" />-->
15 <!-- </filter-search-item>-->
16 <filter-search-item label="状态" field="status">
17 <a-select v-model="filter.status" :options="statusOption" allow-clear multiple placeholder="请选择搜索状态" />
18 </filter-search-item>
19 <filter-search-item label="创建时间" field="createBetween">
20 <a-range-picker v-model="filter.createBetween" show-time :time-picker-props="{ defaultValue: ['00:00:00', '23:59:59'] }" />
21 </filter-search-item>
22 </filter-search>
23 <filter-table ref="tableRef" :loading="loading" :on-query="onQuery" @row-sort="onSort">
24 <template #tool>
25 <icon-button v-if="checkPermission('create')" icon="plus" label="新增" type="primary" @click="onCreate" />
26 </template>
27 <template #tool-right>
28 <export-button v-if="checkPermission('export')" :on-download="onExport" />
29 </template>
30 <activity-table-column data-index="id" title="试唱歌曲" :width="410" />
31 <number-table-column data-index="view_count" title="试听人数" :dark-value="0" :width="104" has-sort />
32 <number-table-column data-index="like_count" title="收藏人数" :dark-value="0" :width="104" has-sort />
33 <number-table-column data-index="submit_work_count" title="提交人数" :dark-value="0" :width="104" has-sort />
34 <!-- <enum-table-column data-index="weight" :option="weightOption" title="推荐权重" :dark-value="0" :width="100" has-sort />-->
35 <date-table-column data-index="created_at" title="创建时间" :width="100" split has-sort />
36 <date-table-column data-index="audit_at" title="通过时间" :width="100" split has-sort />
37 <date-table-column data-index="match_at" title="确认时间" :width="100" split has-sort />
38 <number-table-column data-index="match_day" title="匹配时间" :width="100" :dark-value="0" suffix="天" has-sort />
39 <enum-table-column data-index="status" title="状态" :option="statusOption" :width="80" has-sort />
40 <space-table-column v-if="hasPermission" :width="80" data-index="operations" fixed="right" title="操作" direction="vertical">
41 <template #default="{ record }">
42 <a-link v-if="checkPermission('show')" class="link-hover" :hoverable="false" @click="onShow(record)"> 查看 </a-link>
43
44 <a-link v-if="record.status === 1 && checkPermission('status')" class="link-hover" :hoverable="false" @click="onDown(record)">
45 下架
46 </a-link>
47 <a-link v-if="record.status === 2 && checkPermission('status')" class="link-hover" :hoverable="false" @click="onUp(record, 'up')">
48 上架
49 </a-link>
50 <a-link
51 v-if="record.status !== 0 && record.status !== 5 && checkPermission('edit')"
52 class="link-hover"
53 :hoverable="false"
54 @click="onUpdate(record)"
55 >
56 编辑
57 </a-link>
58 <a-link
59 v-if="record.status === 3 && checkPermission('status')"
60 class="link-hover"
61 :hoverable="false"
62 @click="onUp(record, 'reUp')"
63 >
64 重新上架
65 </a-link>
66 <a-link v-if="record.status === 3 && checkPermission('status')" class="link-hover" :hoverable="false" @click="onSend(record)">
67 发行
68 </a-link>
69 <a-link v-if="record.status === 5 && checkPermission('edit')" class="link-hover" :hoverable="false" @click="onSend(record)">
70 编辑发行
71 </a-link>
72 <a-link v-if="record.status === 1 && checkPermission('push')" class="link-hover" :hoverable="false" @click="onNotify(record)">
73 推送
74 </a-link>
75 <!-- <delete-button v-if="record.status === 2" :index="rowIndex" :on-delete="() => onDelete(record.id)" :row="record" />-->
76 </template>
77 </space-table-column>
78 </filter-table>
79 </page-view>
80 </template>
81
82 <script lang="ts" setup>
83 import { createVNode, onMounted, ref, computed } from 'vue';
84 import { Message, TableData } from '@arco-design/web-vue';
85 import { tryOnMounted } from '@vueuse/core';
86 import useActivityApi from '@/http/activity';
87 import { AnyObject, AttributeData } from '@/types/global';
88 import useLoading from '@/hooks/loading';
89 import NumberTableColumn from '@/components/filter/number-table-column.vue';
90 import EnumTableColumn from '@/components/filter/enum-table-column.vue';
91 import SpaceTableColumn from '@/components/filter/space-table-column.vue';
92 import DateTableColumn from '@/components/filter/date-table-column.vue';
93 import ActivityTableColumn from '@/components/filter/activity-table-column.vue';
94 import FormContent from '@/views/audition/activity-audit/components/form-content.vue';
95 import NotifyContent from '@/views/audition/activity-audit/components/step4-form-content.vue';
96
97 import { createFormVNode, createInputFormItemVNode, createInputVNode, createModalVNode } from '@/utils/createVNode';
98 import { useSelectionStore } from '@/store';
99 import { promiseToBoolean } from '@/utils';
100 import openNewTab from '@/utils/route-blank';
101 import { useRoute, useRouter } from 'vue-router';
102 import { isArray } from '@/utils/is';
103 import usePermission from '@/hooks/permission';
104
105 const router = useRouter();
106
107 const checkPermission = (permission: string | string[]): boolean => {
108 let routeName = useRoute().name as string;
109 routeName = routeName === 'audition-activity' ? routeName : `${routeName}-activity`;
110 permission = isArray(permission) ? permission.map((item) => `${routeName}-${item}`) : `${routeName}-${permission}`;
111 return usePermission().checkPermission(permission);
112 };
113 const hasPermission = computed(() => checkPermission(['show', 'status', 'edit', 'push']));
114
115 const props = defineProps<{
116 initFilter?: AttributeData;
117 hideSearchItem?: string[];
118 exportName?: string;
119 createProp?: { initValue?: object; hideField?: string[] };
120 queryHook?: () => void;
121 }>();
122
123 const { statusOption, get, getExport, create, update, notify, changeStatus } = useActivityApi;
124
125 const tableRef = ref();
126 const { loading, setLoading } = useLoading(false);
127 const filter = ref<AttributeData>({});
128
129 const onQuery = async (params?: AnyObject) => {
130 setLoading(true);
131 props.queryHook?.();
132 return get({
133 createdForm: [0, 1],
134 ...filter.value,
135 ...params,
136 setColumn: [
137 'id',
138 'song_name',
139 'cover',
140 'lang',
141 'speed',
142 'sub_title',
143 'sex',
144 'user_id',
145 'project_id',
146 'send_url',
147 'match_day',
148 'send_url',
149 'lyric',
150 'clip_lyric',
151 'is_push',
152 'is_official',
153 'mark',
154 'expand',
155 'weight',
156 'status',
157 'estimate_release_at',
158 'audit_at',
159 'created_at',
160 'match_at',
161 'match_day',
162 ],
163 setWith: [
164 'project:id,name',
165 'user:id,real_name,nick_name,identity',
166 'tags:id,name',
167 'outSideManages:id,user_id,activity_id,permission',
168 ],
169 setWithCount: ['viewUsers as view_count', 'collectionUsers as like_count', 'submitUsers as submit_work_count'],
170 }).finally(() => setLoading(false));
171 };
172
173 const onSort = (column: string, type: string) => {
174 filter.value.setSort = type ? [(type === 'desc' ? '-' : '') + column, '-id'] : ['-audit_at', '-id'];
175 tableRef.value?.onFetch();
176 };
177
178 const onSearch = () => tableRef.value?.onPageChange(1);
179
180 const onReset = () => {
181 filter.value = {
182 songType: 1,
183 songName: '',
184 projectName: '',
185 weight: [],
186 tagName: '',
187 status: [],
188 createBetween: [],
189 ...props.initFilter,
190 setSort: ['-audit_at'],
191 };
192 tableRef.value?.resetSort();
193 onSearch();
194 };
195
196 onMounted(() => onReset());
197 tryOnMounted(async () => useSelectionStore().queryAll());
198
199 const onExport = () => getExport(props.exportName || '试唱', filter.value);
200 const onShow = (record: TableData) => openNewTab(router, 'audition-activity-show', { id: record.id });
201
202 const onCreate = () => {
203 const formVal = { is_push: 1, created_form: 1, song_type: 1, ...props.createProp?.initValue };
204 const dialog = createModalVNode(
205 () =>
206 createVNode(FormContent, {
207 initValue: formVal,
208 hideField: props.createProp?.hideField,
209 submit: (value: AttributeData) =>
210 create(value).then((data) => {
211 Message.success(`添加活动:${data.song_name}`);
212 tableRef.value?.onFetch();
213 dialog.close();
214 }),
215 }),
216 {
217 title: '创建活动',
218 titleAlign: 'center',
219 width: 'auto',
220 closable: true,
221 footer: false,
222 }
223 );
224 };
225
226 const onUpdate = (record: TableData) => {
227 const dialog = createModalVNode(
228 () =>
229 createVNode(FormContent, {
230 maxStep: 3,
231 initValue: record,
232 submit: (value: AttributeData) =>
233 update(record.id, Object.assign(value, { song_type: 1 })).then((data) => {
234 Message.success(`编辑歌曲:${data.song_name}`);
235 tableRef.value?.onFetch();
236 dialog.close?.();
237 }),
238 }),
239 {
240 title: '编辑歌曲',
241 titleAlign: 'center',
242 width: 'auto',
243 closable: true,
244 footer: false,
245 }
246 );
247 };
248
249 const onDown = (record: TableData) => {
250 const msg = ref<string>('');
251
252 createModalVNode(
253 () =>
254 createInputVNode(msg, {
255 'placeholder': '请输入下架原因',
256 'maxLength': 20,
257 'show-word-limit': true,
258 }),
259 {
260 title: '变更状态',
261 titleAlign: 'start',
262 okText: '下架',
263 onBeforeOk: () => promiseToBoolean(changeStatus(record.id, { status: 'down', msg: msg.value })),
264 onOk: () => tableRef.value?.onFetch(),
265 }
266 );
267 };
268
269 const onUp = (record: TableData, status: 'up' | 'reUp') => {
270 createModalVNode(`请确认是否上架活动:${record.song_name}`, {
271 title: '变更状态',
272 titleAlign: 'start',
273 okText: '上架',
274 onBeforeOk: () => promiseToBoolean(changeStatus(record.id, { status })),
275 onOk: () => tableRef.value?.onFetch(),
276 });
277 };
278
279 const onSend = (record: TableData) => {
280 const link = ref(record.send_url?.[0]?.url || '');
281
282 createModalVNode(
283 () =>
284 createFormVNode({ model: link }, [
285 createVNode(
286 'div',
287 { style: { fontFamily: 'PingFangSC-Regular, serif', paddingBottom: '16px', color: '#696969' } },
288 '将歌曲发行平台的url链接回填,会展示在App或小程序应用端。将会给您带来更多播放量 ~'
289 ),
290 createInputFormItemVNode(link, { label: 'QQ音乐或酷狗平台链接', rowClass: 'mb-0', required: true }, { size: 'small' }),
291 ]),
292 {
293 title: link.value.length === 0 ? '发行' : '编辑发行',
294 titleAlign: 'start',
295 width: 'auto',
296 onBeforeOk: () => promiseToBoolean(changeStatus(record.id, { status: 'send', link: link.value })),
297 onOk: () => tableRef.value?.onFetch(),
298 }
299 );
300 };
301
302 const onNotify = (record: TableData) => {
303 const { loading: submitLoading, setLoading } = useLoading(false);
304 const formValue = ref({ is_push: 1, expand: { push_type: [], push_user: [] } });
305 const formRef = ref();
306
307 createModalVNode(
308 () =>
309 createVNode(NotifyContent, {
310 ref: formRef,
311 modelValue: formValue.value,
312 loading: submitLoading.value,
313 hideMode: true,
314 disabledTag: false,
315 }),
316 {
317 title: '推送',
318 titleAlign: 'start',
319 width: '680px',
320 onBeforeOk: () => {
321 return formRef.value?.onValid(() => {
322 setLoading(true);
323 notify(record.id, formValue.value.expand).finally(() => setLoading(false));
324 });
325 },
326 }
327 );
328 };
329 </script>
330
331 <style scoped></style>
1 <script setup lang="ts">
2 import { useSelectionStore } from '@/store';
3
4 import { Layout, LayoutSider, LayoutContent, LayoutFooter, Step, Steps, Space, Divider, Link } from '@arco-design/web-vue';
5 import Step1FormContent from '@/views/audition/demo-apply/components/step1-form-content.vue';
6 import Step2FormContent from '@/views/audition/demo-apply/components/step2-form-content.vue';
7 import Step3FormContent from '@/views/audition/demo-apply/components/step3-form-content.vue';
8 import IconButton from '@/components/icon-button/index.vue';
9
10 import { AnyObject } from '@/types/global';
11 import useLoading from '@/hooks/loading';
12 import { computed, markRaw, ref } from 'vue';
13 import { cloneDeep } from 'lodash';
14
15 const props = defineProps<{
16 initValue: AnyObject;
17 filterProject?: (value: unknown) => boolean;
18 onSubmit: (data: AnyObject) => Promise<any>;
19 }>();
20
21 const { loading, setLoading } = useLoading(false);
22 const formRef = ref();
23 const formValue = ref({ ...cloneDeep(props.initValue) });
24
25 const stepItems = [
26 { value: 1, label: '基本信息', template: markRaw(Step1FormContent) },
27 { value: 2, label: '补充信息', template: markRaw(Step2FormContent) },
28 { value: 3, label: '上传文件', template: markRaw(Step3FormContent) },
29 ];
30
31 const currentStep = ref(1);
32 const currentContent = computed(() => stepItems.find((item) => item.value === currentStep.value)?.template);
33 const nextBtnLabel = computed(() => (currentStep.value === 3 ? '提交' : '下一步'));
34
35 const onPrev = (): void => {
36 currentStep.value = Math.max(1, currentStep.value - 1);
37 };
38
39 const onNext = () => {
40 formRef.value.onValid(async () => {
41 if (currentStep.value === stepItems.length) {
42 setLoading(true);
43 props.onSubmit(formValue.value).finally(() => setLoading(false));
44 }
45 currentStep.value = Math.min(stepItems.length, currentStep.value + 1);
46 });
47 };
48 </script>
49
50 <template>
51 <Layout>
52 <Layout has-sider>
53 <LayoutSider :width="120" class="aside">
54 <Steps :current="currentStep" direction="vertical" :small="true">
55 <Step v-for="item in stepItems" :key="item.value">{{ item.label }}</Step>
56 </Steps>
57 </LayoutSider>
58 <LayoutContent class="main">
59 <component :is="currentContent" ref="formRef" v-model="formValue" v-model:loading="loading" :filter-project="filterProject" />
60 </LayoutContent>
61 </Layout>
62 <LayoutFooter>
63 <Divider style="margin-top: 8px; margin-bottom: 20px" />
64 <Link :href="useSelectionStore().lyricTool" :hoverable="false" class="link-hover" icon>歌词制作工具</Link>
65 <Space style="float: right">
66 <IconButton v-show="currentStep !== 1" icon="left" label="上一步" @click="onPrev" />
67 <IconButton icon="right" icon-align="right" :loading="loading" :label="nextBtnLabel" @click="onNext" />
68 </Space>
69 </LayoutFooter>
70 </Layout>
71 </template>
72
73 <style scoped lang="less">
74 .aside {
75 box-shadow: unset !important;
76 border-right: 1px solid var(--color-border);
77 margin-right: 20px;
78 background-color: transparent;
79 }
80
81 .main {
82 min-width: 640px;
83 }
84 </style>
1 <script setup lang="ts">
2 import { Form, FormItem, Input } from '@arco-design/web-vue';
3 import { computed, ref } from 'vue';
4 import AvatarUpload from '@/components/avatar-upload/index.vue';
5 import TagSelect from '@/components/tag-select/index.vue';
6 import { FieldRule, FormInstance } from '@arco-design/web-vue/es/form';
7 import { get, set } from 'lodash';
8 import { useVModels } from '@vueuse/core';
9 import ProjectSelect from '@/components/project-select/index.vue';
10
11 const props = withDefaults(defineProps<{ loading?: boolean; modelValue?: any; filterProject?: (value: any) => boolean }>(), {
12 filterProject: () => true,
13 });
14
15 const emits = defineEmits(['update:modelValue', 'update:loading']);
16
17 const formRef = ref<FormInstance>();
18 const { modelValue: formValue } = useVModels(props, emits);
19
20 const formRule = {
21 'cover': [{ type: 'string', required: true, message: '请上传活动封面' }],
22 'song_name': [{ type: 'string', required: true, message: '请输入歌曲名称' }],
23 'expand.tag_ids': [
24 { type: 'array', required: false, message: '请选择关联标签' },
25 { type: 'array', maxLength: 3, message: '关联标签最多选中3个' },
26 ],
27 'project_id': [{ type: 'number', min: 1, required: true, message: '请选择关联厂牌' }],
28 } as Record<string, FieldRule[]>;
29
30 const tagIds = computed({
31 get: () => get('expand.tag_ids', []),
32 set: (val) => set(formValue.value, 'expand.tag_ids', val),
33 });
34
35 defineExpose({
36 onValid: (callback?: () => void) => formRef.value?.validate((errors) => !errors && callback?.()),
37 });
38 </script>
39
40 <template>
41 <Form ref="formRef" :model="formValue" :rules="formRule" :auto-label-width="true" size="small">
42 <FormItem label="封面图片 " field="cover" :show-colon="true">
43 <AvatarUpload v-model="formValue.cover" :size="100" shape="square" />
44 </FormItem>
45 <FormItem label="歌曲名称" field="song_name" :show-colon="true">
46 <Input v-model="formValue.song_name" :max-length="100" :show-word-limit="true" placeholder="请输入" />
47 </FormItem>
48 <FormItem label="曲风标签" field="expand.tag_ids" :show-colon="true">
49 <TagSelect v-model="tagIds" :multiple="true" :limit="3" placeholder="请选择" limit-error-msg="关联标签最多选中3个" />
50 </FormItem>
51 <FormItem v-if="formValue.created_form !== 2" label="关联厂牌" field="project_id" :show-colon="true">
52 <ProjectSelect v-model="formValue.project_id" :filtrate="filterProject" />
53 </FormItem>
54 </Form>
55 </template>
56
57 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { Form, FormItem, InputTag } from '@arco-design/web-vue';
3 import UserSelect from '@/components/user-select/index.vue';
4
5 import { ref, computed } from 'vue';
6 import { FieldRule, FormInstance } from '@arco-design/web-vue/es/form';
7 import { useVModels } from '@vueuse/core';
8 import { get, set } from 'lodash';
9
10 const props = defineProps<{ loading?: boolean; modelValue?: any }>();
11 const emits = defineEmits(['update:modelValue', 'update:loading']);
12
13 const formRef = ref<FormInstance>();
14 const { modelValue: formValue } = useVModels(props, emits);
15
16 const lyricistIds = computed({
17 get: () => get(formValue.value, 'expand.lyricist.ids', []),
18 set: (val) => set(formValue.value, 'expand.lyricist.ids', val),
19 });
20
21 const lyricistSupplement = computed({
22 get: () => get(formValue.value, 'expand.lyricist.supplement', []),
23 set: (val) => set(formValue.value, 'expand.lyricist.supplement', val),
24 });
25
26 const composerIds = computed({
27 get: () => get(formValue.value, 'expand.composer.ids', []),
28 set: (val) => set(formValue.value, 'expand.composer.ids', val),
29 });
30
31 const composerSupplement = computed({
32 get: () => get(formValue.value, 'expand.composer.supplement', []),
33 set: (val) => set(formValue.value, 'expand.composer.supplement', val),
34 });
35
36 const checkMaxUser = (value: any, cb: (error?: string) => void) => (value && value.length > 2 ? cb('最大选择2人') : false);
37
38 const formRule = {
39 'expand.lyricist.supplement': [{ type: 'array', validator: (value, cb) => checkMaxUser(value, cb) }],
40 'expand.composer.supplement': [{ type: 'array', validator: (value, cb) => checkMaxUser(value, cb) }],
41 'estimate_release_at': [{ required: true, message: '请选择预计发布时间' }],
42 } as Record<string, FieldRule[]>;
43
44 defineExpose({
45 onValid: (callback?: () => void) => formRef.value?.validate((errors) => !errors && callback?.()),
46 });
47 </script>
48
49 <template>
50 <Form ref="formRef" :model="formValue" :rules="formRule" :auto-label-width="true" size="small">
51 <FormItem label="词作者(用户)" field="expand.lyricist.ids" :show-colon="true">
52 <UserSelect v-model="lyricistIds" placeholder="请选择用户" :multiple="true" :allow-search="true" :limit="2" />
53 </FormItem>
54 <FormItem label="词作者(未注册)" field="expand.lyricist.supplement" :show-colon="true">
55 <InputTag v-model="lyricistSupplement" placeholder="输入完成回车键分隔" :max-tag-count="2" :unique-value="true" />
56 <template #extra>
57 <div>注意:上方填1项即可,歌曲信息中【用户】可点击跳转用户主页,【未注册】仅显示;</div>
58 </template>
59 </FormItem>
60 <FormItem label="曲作者(用户)" field="expand.composer.ids" :show-colon="true">
61 <UserSelect v-model="composerIds" placeholder="请选择用户" :multiple="true" :allow-search="true" :limit="2" />
62 </FormItem>
63 <FormItem label="曲作者(未注册)" field="expand.composer.supplement" :show-colon="true">
64 <InputTag v-model="composerSupplement" placeholder="输入完成回车键分隔" :max-tag-count="2" :unique-value="true" />
65 <template #extra>
66 <div>注意:上方填1项即可,歌曲信息中【用户】可点击跳转用户主页,【未注册】仅显示;</div>
67 </template>
68 </FormItem>
69 </Form>
70 </template>
71
72 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { ref, computed } from 'vue';
3 import { FieldRule, FormInstance } from '@arco-design/web-vue/es/form';
4 import { useVModels } from '@vueuse/core';
5 import { Form, FormItem, Link, Textarea, TypographyText } from '@arco-design/web-vue';
6 import InputUpload from '@/components/input-upload/index.vue';
7 import { useSelectionStore } from '@/store';
8 import { get, set } from 'lodash';
9 // import axios from 'axios';
10 // import AudioPreview from '@/views/audition/activity-apply/components/audio-preview.vue';
11 import AudioPlayer from '@/components/audio-player/index.vue';
12 // import { createModalVNode } from '@/utils/createVNode';
13 import { downloadFile } from '@/http/auth';
14
15 const props = defineProps<{ loading?: boolean; modelValue?: any }>();
16 const emits = defineEmits(['update:modelValue', 'update:loading']);
17
18 const formRef = ref<FormInstance>();
19 const { loading, modelValue: formValue } = useVModels(props, emits);
20
21 const guideSourceUrl = computed({
22 get: () => get(formValue.value, 'expand.guide_source.url', ''),
23 set: (val) => set(formValue.value, 'expand.guide_source.url', val),
24 });
25
26 const karaokeSourceUrl = computed({
27 get: () => get(formValue.value, 'expand.karaoke_source.url', ''),
28 set: (val) => set(formValue.value, 'expand.karaoke_source.url', val),
29 });
30
31 const onUpdateGuide = (file: { name: string; url: string; size: number }) => {
32 set(formValue.value, 'expand.guide_source', { name: file.name, url: file.url, size: file.size });
33 };
34
35 const onUpdateKaraoke = (file: { name: string; url: string; size: number }) => {
36 set(formValue.value, 'expand.karaoke_source', { name: file.name, url: file.url, size: file.size });
37 };
38
39 const { activityAudioAccept } = useSelectionStore();
40
41 const formRule = {
42 'expand.guide_source.url': [{ required: true, message: '请上传导唱文件' }],
43 'expand.karaoke_source.url': [{ required: false, message: '请上传伴奏文件' }],
44 'lyric': [{ type: 'string', required: true, message: '请填写歌词内容' }],
45 'clip_lyric': [{ type: 'string', required: true, message: '请填写推荐歌词内容' }],
46 } as Record<string, FieldRule[]>;
47
48 const onDownload = (url: string, fileName: string) => downloadFile(url, `${formValue.value.song_name}(${fileName})`);
49
50 const onFormatLyric = (key: string) => {
51 formValue.value[key] = formValue.value[key].replace(/(\n[\s\t]*\r*\n)/g, '\n').replace(/^[\n\r\t]*|[\n\r\t]*$/g, '');
52 };
53
54 const guideFile = ref<File | undefined>();
55
56 // const getGuideFile = async () => {
57 // if (!guideFile.value) {
58 // guideFile.value = await axios
59 // .get(`${guideSourceUrl.value}`, { responseType: 'blob', timeout: 60000 })
60 // .then(({ data }) => Promise.resolve(data));
61 // }
62
63 // return guideFile.value;
64 // };
65
66 // const onAudioPreview = async () => {
67 // const src = await getGuideFile();
68 // createModalVNode(() => createVNode(AudioPreview, { src, lyric: formValue.value.lyric }), {
69 // title: '预览歌词-整首',
70 // footer: false,
71 // closable: true,
72 // });
73 // };
74
75 defineExpose({
76 onValid: (callback?: () => void) => formRef.value?.validate((errors) => !errors && callback?.()),
77 });
78 </script>
79
80 <template>
81 <Form ref="formRef" :model="formValue" :rules="formRule" :auto-label-width="true" size="small">
82 <FormItem label="音频文件" field="expand.guide_source.url" :show-colon="true">
83 <InputUpload
84 v-model="guideSourceUrl"
85 :accept="activityAudioAccept"
86 :limit="100"
87 @success="onUpdateGuide"
88 @choose-file="(file: any) => (guideFile = file)"
89 @update:loading="(value: boolean) => (loading = value)"
90 />
91 </FormItem>
92 <FormItem v-if="guideSourceUrl">
93 <template #label>
94 <Link icon @click="onDownload(guideSourceUrl, '音频文件')">下载音频</Link>
95 </template>
96 <AudioPlayer name="音频文件" :url="guideSourceUrl" />
97 </FormItem>
98 <FormItem label="伴奏文件" field="expand.karaoke_source.url" :show-colon="true">
99 <InputUpload
100 v-model="karaokeSourceUrl"
101 :accept="activityAudioAccept"
102 :limit="100"
103 @success="onUpdateKaraoke"
104 @update:loading="(value: boolean) => (loading = value)"
105 />
106 </FormItem>
107 <FormItem v-if="karaokeSourceUrl">
108 <template #label>
109 <Link icon @click="onDownload(karaokeSourceUrl, '伴奏文件')">下载伴奏</Link>
110 </template>
111 <AudioPlayer name="伴奏文件" :url="karaokeSourceUrl" />
112 </FormItem>
113 <FormItem class="lyric" label="歌词文本" field="lyric" :show-colon="true">
114 <Textarea
115 v-model="formValue.lyric"
116 :auto-size="{ minRows: 6, maxRows: 6 }"
117 placeholder="请粘贴带时间的lrc歌词文本至输入框"
118 @blur="() => onFormatLyric('lyric')"
119 />
120 <!-- <template #extra>
121 <Link :hoverable="false" :disabled="!guideSourceUrl || !formValue.lyric" @click="onAudioPreview"> 预览歌词</Link>
122 </template> -->
123 </FormItem>
124 <TypographyText type="danger" style="font-size: 13px">
125 注意:demo上架后,存在于您的厂牌私库。需要在App应用内,您的厂牌主页下,进行1对1分享给对方试听
126 </TypographyText>
127 </Form>
128 </template>
129
130 <style scoped lang="less">
131 .lyric {
132 :deep(.arco-form-item-extra) {
133 width: 100%;
134 text-align: right;
135 }
136 }
137 </style>
1 <script setup lang="ts">
2 import useLoading from '@/hooks/loading';
3 import { ref } from 'vue';
4 import { AnyObject } from '@/types/global';
5 import { useApplyApi } from '@/http/activity';
6
7 const { loading } = useLoading(false);
8 const filter = ref<AnyObject>({});
9 const tableRef = ref();
10
11 const onSearch = () => tableRef.value?.onPageChange(1);
12
13 const onReset = () => {
14 filter.value = { songName: '', userName: '', projectName: '', tagName: '', auditStatus: '', createBetween: [] };
15 onSearch();
16 };
17 </script>
18
19 <template>
20 <page-view has-card has-bread>
21 <filter-search :model="filter" :loading="loading" @search="onSearch" @reset="onReset">
22 <filter-search-item field="song_name" label="歌曲名称">
23 <a-input v-model="filter.songName" :allow-clear="true" placeholder="请输入" />
24 </filter-search-item>
25 <filter-search-item field="projectName" label="厂牌名称">
26 <a-input v-model="filter.projectName" :allow-clear="true" placeholder="请输入" />
27 </filter-search-item>
28 <filter-search-item field="tagName" label="标签名称">
29 <a-input v-model="filter.tagName" :allow-clear="true" placeholder="请输入" />
30 </filter-search-item>
31 <filter-search-item field="userName" label="创建人">
32 <a-input v-model="filter.userName" :allow-clear="true" placeholder="请输入" />
33 </filter-search-item>
34 <filter-search-item field="createBetween" label="创建时间">
35 <a-range-picker v-model="filter.createBetween" :allow-clear="true" />
36 </filter-search-item>
37 <filter-search-item field="audit_status" label="状态">
38 <a-select v-model="filter.auditStatus" :options="useApplyApi.statusOption" placeholder="请选择" />
39 </filter-search-item>
40 </filter-search>
41 </page-view>
42 </template>
43
44 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { Activity } from '@/types/activity';
3 import { computed } from 'vue';
4 import { User } from '@/types/user';
5 import UserTag from '@/views/audition/activity-show/components/user-tag.vue';
6 import { unionBy } from 'lodash';
7 import useActivityApi from '@/http/activity';
8
9 const props = defineProps<{ data: Activity; loading?: boolean }>();
10
11 const links = computed(() => {
12 return unionBy(props.data.links || [], 'id');
13 });
14
15 const inSideLyricist = computed(
16 (): User[] => links.value?.filter((item: User) => props.data.expand?.lyricist?.ids?.indexOf(item.id) !== -1) || []
17 );
18 const outSideLyricist = computed(() => props.data.expand?.lyricist?.supplement || []);
19
20 const inSideComposer = computed(
21 (): User[] => links.value?.filter((item: User) => props.data.expand?.composer?.ids?.indexOf(item.id) !== -1) || []
22 );
23 const outSideComposer = computed(() => props.data.expand?.composer?.supplement || []);
24
25 const project = computed(() => props.data.project);
26 </script>
27
28 <template>
29 <a-spin :loading="loading as boolean" style="width: 100%">
30 <a-card :bordered="false">
31 <a-form auto-label-width label-align="left">
32 <a-layout>
33 <a-layout-sider :width="130" style="background: none; box-shadow: none; padding-top: 6px">
34 <a-image show-loader :height="130" :width="130" :src="data.cover" />
35 </a-layout-sider>
36 <a-layout-content style="margin-left: 16px">
37 <a-form-item :hide-label="true">
38 <div class="title">{{ data.song_name }}</div>
39 <a-tag v-for="item in data.tags" :key="item.id" size="small" style="margin-right: 5px">
40 {{ item.name }}
41 </a-tag>
42 <span style="font-size: 10px">
43 {{ useActivityApi.statusOption.find((item) => item.value === data.status)?.label }}
44 </span>
45 </a-form-item>
46 <a-grid :cols="3" :col-gap="12" :row-gap="0">
47 <a-grid-item :span="3">
48 <a-form-item
49 :label-col-style="{ flex: 0 }"
50 :wrapper-col-style="{ flex: 'unset', width: 'inherit' }"
51 :show-colon="true"
52 :label="project ? '关联厂牌' : '关联用户'"
53 >
54 <div v-if="project">{{ data.project.name }}</div>
55 <div v-else> {{ data.user?.nick_name }}</div>
56 </a-form-item>
57 </a-grid-item>
58 <a-grid-item :span="1">
59 <a-form-item :label-col-style="{ flex: 0 }" :show-colon="true" label="作词">
60 <user-tag v-for="item in inSideLyricist" :key="item.id" :user="{ nick_name: item.nick_name }" style="margin-right: 5px" />
61 <user-tag
62 v-for="item in outSideLyricist"
63 :key="`lyricist-${item}`"
64 :user="{ nick_name: item }"
65 style="margin-right: 5px"
66 />
67 </a-form-item>
68 </a-grid-item>
69 <a-grid-item :span="1">
70 <a-form-item :label-col-style="{ flex: 0 }" :show-colon="true" label="作曲">
71 <user-tag v-for="item in inSideComposer" :key="item.id" :user="{ nick_name: item.nick_name }" style="margin-right: 5px" />
72 <user-tag
73 v-for="item in outSideComposer"
74 :key="`lyricist-${item}`"
75 :user="{ nick_name: item }"
76 style="margin-right: 5px"
77 />
78 </a-form-item>
79 </a-grid-item>
80 <a-grid-item :span="3">
81 <a-form-item style="margin-bottom: 0 !important" :label-col-style="{ flex: 0 }" :show-colon="true" label="创建信息">
82 <span v-if="data.user" style="margin-right: 8px">{{ data.user.nick_name }}</span>
83 <span style="margin-right: 8px">{{ data.created_at }} </span>
84 </a-form-item>
85 </a-grid-item>
86 </a-grid>
87 </a-layout-content>
88 </a-layout>
89 </a-form>
90 </a-card>
91 </a-spin>
92 </template>
93
94 <style lang="less" scoped>
95 .arco-form-item {
96 margin-bottom: 10px;
97 }
98
99 .arco-form-item-label-col {
100 flex: 0;
101 }
102
103 .title {
104 font-size: 16px;
105 font-weight: bold;
106 margin-right: 8px;
107 }
108
109 .right {
110 margin: 0 20px;
111 min-width: 600px;
112 }
113 </style>
1 <script setup lang="ts">
2 import { Modal, Textarea } from '@arco-design/web-vue';
3 import { computed, h } from 'vue';
4 import { ActivityExpand } from '@/types/activity-apply';
5 import { useAppStore } from '@/store';
6 import { downloadFile } from '@/http/auth';
7
8 type MaterialData = {
9 title: string;
10 type: string;
11 content: string;
12 name?: string;
13 size?: string;
14 };
15
16 const props = defineProps<{ data: { expand: ActivityExpand; lyric: string; song_name: string }; hideTrack?: boolean }>();
17
18 const appStore = useAppStore();
19 const theme = computed(() => appStore.theme);
20
21 const lyricStyle = computed(() =>
22 theme.value === 'light' ? { border: 'none', backgroundColor: 'white' } : { border: 'none', backgroundColor: '#2a2a2b' }
23 );
24
25 const bytesForHuman = (bytes: number, decimals = 2) => {
26 const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
27
28 let i = 0;
29
30 // eslint-disable-next-line no-plusplus
31 for (i; bytes > 1024; i++) {
32 bytes /= 1024;
33 }
34
35 return `${parseFloat(bytes.toFixed(decimals))} ${units[i]}`;
36 };
37
38 const onDownload = (record: MaterialData) => downloadFile(record.content, `${props.data.song_name}(${record.title})`);
39
40 const onViewLyric = (lyric: string) => {
41 Modal.open({
42 content: () => h(Textarea, { defaultValue: lyric, autoSize: { maxRows: 20 }, style: lyricStyle.value }),
43 footer: false,
44 closable: false,
45 });
46 };
47
48 const materials = computed((): MaterialData[] => {
49 return [
50 {
51 title: '音频',
52 type: 'guide',
53 name: props.data.expand?.guide_source?.name || '',
54 content: props.data.expand?.guide_source?.url || '',
55 size: bytesForHuman(props.data.expand?.guide_source?.size || 0),
56 },
57 {
58 title: '伴奏',
59 type: 'karaoke',
60 name: props.data.expand?.karaoke_source?.name || '',
61 content: props.data.expand?.karaoke_source?.url || '',
62 size: bytesForHuman(props.data.expand?.karaoke_source?.size || 0),
63 },
64 { title: '歌词', type: 'Lyric', content: props.data.lyric },
65 ];
66 });
67 </script>
68
69 <template>
70 <a-table row-key="type" :data="materials" :bordered="false" :table-layout-fixed="true" :pagination="false">
71 <template #columns>
72 <a-table-column title="物料类型" align="center" data-index="title" :width="140" />
73 <a-table-column title="音频播放" data-index="content">
74 <template #cell="{ record }">
75 <template v-if="record.content">
76 <audio-player v-if="['guide', 'karaoke'].indexOf(record.type) !== -1" :name="record.type" :url="record.content" />
77 <div v-if="record.type === 'track'">{{ [record.name, record.size].join(',') }}</div>
78 </template>
79 </template>
80 </a-table-column>
81 <a-table-column title="操作" align="center" :width="200">
82 <template #cell="{ record }">
83 <template v-if="record.content">
84 <a-button v-if="record.type === 'Lyric'" type="primary" size="small" @click="onViewLyric(record.content)">查看</a-button>
85 <a-button v-else type="primary" size="small" @click="onDownload(record)">下载</a-button>
86 </template>
87 </template>
88 </a-table-column>
89 </template>
90 </a-table>
91 </template>
92
93 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { useRoute, useRouter } from 'vue-router';
3 import { computed, onMounted, ref } from 'vue';
4 import useActivityApi from '@/http/activity';
5 import BasicCard from '@/views/audition/demo-show/components/basic-card.vue';
6 import MaterialTable from '@/views/audition/demo-show/components/material-table.vue';
7 import ViewUserTable from '@/views/audition/activity-show/components/view-user-table.vue';
8 import LikeUserTable from '@/views/audition/activity-show/components/like-user-table.vue';
9 import { useRouteQuery } from '@vueuse/router';
10 import usePermission from '@/hooks/permission';
11 import { isArray } from '@/utils/is';
12
13 const routeName = useRoute()?.name as string;
14 const checkPermission = (permission: string | string[]): boolean => {
15 return usePermission().checkPermission(
16 isArray(permission) ? permission.map((item) => `${routeName}-${item}`) : `${routeName}-${permission}`
17 );
18 };
19
20 const tabKeys = computed((): string[] => ['material', 'view-user', 'like-user'].filter((item) => checkPermission(item)));
21
22 const tabKey = useRouteQuery('tabKey', tabKeys.value[0] || '');
23
24 const activityKey = Number(useRoute().params?.id);
25
26 const activity = ref({});
27 const total = ref({ submit_total: 0, match_total: 0, view_total: 0, like_total: 0, manager_total: 0 });
28
29 const syncViewCount = () =>
30 useActivityApi.viewUsers(activityKey, { pageSize: 1, limit: 1 }).then(({ meta }) => (total.value.view_total = meta.total));
31
32 const syncLikeCount = () =>
33 useActivityApi.likeUsers(activityKey, { pageSize: 1, limit: 1 }).then(({ meta }) => (total.value.like_total = meta.total));
34
35 const router = useRouter();
36 onMounted(async () => {
37 await useActivityApi
38 .show(activityKey, {
39 songType: 2,
40 setWith: [
41 'project:id,name,is_promote,is_can_manage',
42 'tags:id,name',
43 'user:id,nick_name,real_name,identity',
44 'links:id,nick_name,identity',
45 ],
46 setColumn: ['id', 'song_name', 'cover', 'lyric', 'expand', 'status', 'created_at', 'song_type', 'project_id', 'user_id'],
47 })
48 .then((data) => {
49 activity.value = data;
50 })
51 .catch(() => router.replace({ name: 'exception-404' }));
52 });
53 </script>
54
55 <template>
56 <page-view has-bread>
57 <basic-card :loading="!activity" :data="activity" />
58
59 <a-card v-if="tabKeys.length" :bordered="false" style="margin-top: 20px">
60 <a-tabs v-model:active-key="tabKey" :animation="true" :justify="true" :header-padding="false" type="rounded" size="small">
61 <a-tab-pane v-if="checkPermission('material')" key="material" title="物料列表">
62 <MaterialTable :data="activity" />
63 </a-tab-pane>
64 <a-tab-pane v-if="checkPermission('view-user')" key="view-user" :title="`试听用户(${total.view_total})`">
65 <view-user-table :activity-id="activityKey" :query-hook="syncViewCount" />
66 </a-tab-pane>
67 <a-tab-pane v-if="checkPermission('like-user')" key="like-user" :title="`收藏用户(${total.like_total})`">
68 <like-user-table :activity-id="activityKey" :query-hook="syncLikeCount" />
69 </a-tab-pane>
70 </a-tabs>
71 </a-card>
72 </page-view>
73 </template>
74
75 <style lang="less" scoped>
76 textarea.arco-textarea::-webkit-scrollbar {
77 display: none;
78 }
79 </style>
1 <script setup lang="ts">
2 import { computed, createVNode, onMounted, ref } from 'vue';
3 import useLoading from '@/hooks/loading';
4 import { AnyObject, AttributeData, QueryForParams } from '@/types/global';
5 import { useRoute, useRouter } from 'vue-router';
6 import { isArray } from '@/utils/is';
7 import usePermission from '@/hooks/permission';
8 import useActivityApi, { useApplyApi } from '@/http/activity';
9 import NumberTableColumn from '@/components/filter/number-table-column.vue';
10 import ActivityTableColumn from '@/components/filter/activity-table-column.vue';
11 import DateTableColumn from '@/components/filter/date-table-column.vue';
12 import EnumTableColumn from '@/components/filter/enum-table-column.vue';
13 import SpaceTableColumn from '@/components/filter/space-table-column.vue';
14 import ActivityForm from '@/views/audition/demo-apply/components/form-content.vue';
15 import { Message, TableData } from '@arco-design/web-vue';
16 import { createInputVNode, createModalVNode } from '@/utils/createVNode';
17 import { promiseToBoolean } from '@/utils';
18 import openNewTab from '@/utils/route-blank';
19 import { useSelectionStore } from '@/store';
20 import { Project } from '@/utils/model';
21 import { tryOnMounted } from '@vueuse/core';
22 import { split } from 'lodash';
23
24 const router = useRouter();
25
26 const checkPermission = (permission: string | string[]): boolean => {
27 let routeName = useRoute().name as string;
28 routeName = routeName === 'audition-demo' ? routeName : `${routeName}-demo`;
29 permission = isArray(permission) ? permission.map((item) => `${routeName}-${item}`) : `${routeName}-${permission}`;
30 return usePermission().checkPermission(permission);
31 };
32 const hasPermission = computed(() => checkPermission(['show', 'status', 'edit']));
33
34 const { statusOption, get, getExport, update, changeStatus } = useActivityApi;
35
36 const platformOption = [
37 { label: '厂牌Demo', value: '0,1' },
38 { label: '个人Demo', value: '2' },
39 ];
40
41 const tableRef = ref();
42 const { loading, setLoading } = useLoading(false);
43 const filter = ref<QueryForParams>({});
44
45 const props = withDefaults(
46 defineProps<{
47 initFilter?: AttributeData;
48 hideSearchItem?: string[];
49 hideColumnItem?: string[];
50 exportName?: string;
51 createProp?: object;
52 queryHook?: () => void;
53 }>(),
54 {
55 hideSearchItem: ['createBetween'] as any,
56 }
57 );
58
59 const onQuery = async (params?: AnyObject) => {
60 setLoading(true);
61 props.queryHook?.();
62 return get({
63 ...filter.value,
64 ...params,
65 createdForm: filter.value.createdForm ? split(filter.value.createdForm as string, ',') : [],
66 setColumn: [
67 'id',
68 'song_name',
69 'cover',
70 'lang',
71 'speed',
72 'sub_title',
73 'sex',
74 'user_id',
75 'project_id',
76 'send_url',
77 'match_day',
78 'send_url',
79 'lyric',
80 'clip_lyric',
81 'is_push',
82 'is_official',
83 'mark',
84 'expand',
85 'weight',
86 'status',
87 'estimate_release_at',
88 'audit_at',
89 'created_at',
90 'created_form',
91 'match_at',
92 'match_day',
93 ],
94 setWith: [
95 'project:id,name',
96 'user:id,real_name,nick_name,identity',
97 'tags:id,name',
98 'outSideManages:id,user_id,activity_id,permission',
99 ],
100 setWithCount: ['viewUsers as view_count', 'collectionUsers as like_count'],
101 }).finally(() => setLoading(false));
102 };
103
104 const onSort = (column: string, type: string) => {
105 filter.value.setSort = type ? [(type === 'desc' ? '-' : '') + column, '-id'] : ['-audit_at', '-id'];
106 tableRef.value?.onFetch();
107 };
108
109 const onSearch = () => tableRef.value?.onPageChange(1);
110
111 const onReset = () => {
112 filter.value = {
113 songName: '',
114 projectName: '',
115 tagName: '',
116 userName: '',
117 createdForm: '',
118 status: [],
119 createBetween: [],
120 ...props.initFilter,
121 songType: 2,
122 setSort: ['-audit_at'],
123 };
124 tableRef.value?.resetSort();
125 onSearch();
126 };
127
128 onMounted(() => onReset());
129 tryOnMounted(async () => useSelectionStore().queryAll());
130
131 const onExport = () =>
132 getExport(props.exportName || 'Demo', {
133 ...filter.value,
134 createdForm: filter.value.createdForm ? split(filter.value.createdForm as string, ',') : [],
135 });
136 const onShow = (record: TableData) => openNewTab(router, 'audition-demo-show', { id: record.id });
137
138 const onDown = (record: TableData) => {
139 const msg = ref<string>('');
140
141 createModalVNode(() => createInputVNode(msg, { 'placeholder': '请输入下架原因', 'maxLength': 20, 'show-word-limit': true }), {
142 title: '变更状态',
143 titleAlign: 'start',
144 okText: '下架',
145 onBeforeOk: () => promiseToBoolean(changeStatus(record.id, { status: 'down', msg: msg.value })),
146 onOk: () => tableRef.value?.onFetch(),
147 });
148 };
149
150 const onUp = (record: TableData, status: 'up' | 'reUp') => {
151 createModalVNode(`请确认是否上架Demo:${record.song_name}`, {
152 title: '变更状态',
153 titleAlign: 'start',
154 okText: '上架',
155 onBeforeOk: () => promiseToBoolean(changeStatus(record.id, { status })),
156 onOk: () => tableRef.value?.onFetch(),
157 });
158 };
159 const onCreate = () => {
160 const dialog = createModalVNode(
161 () =>
162 createVNode(ActivityForm, {
163 initValue: { song_type: 2, cover: useSelectionStore().appleDemoCover, created_form: 1, is_push: 0, expand: { push_type: [] } },
164 filterProject: (value: Project) => value.is_can_demo_apply === 1,
165 onSubmit: (data: AnyObject) =>
166 useApplyApi.create(data).then(() => {
167 Message.success(`申请上架Demo:${data.song_name}`);
168 tableRef.value?.onFetch();
169 dialog.close();
170 }),
171 }),
172 { title: '创建Demo', footer: false, closable: true, width: 'auto' }
173 );
174 };
175
176 const onUpdate = (record: TableData) => {
177 const dialog = createModalVNode(
178 () =>
179 createVNode(ActivityForm, {
180 initValue: record,
181 filterProject: (value: Project) => value.is_can_demo_apply === 1 || value.id === record.project_id,
182 onSubmit: (attribute: AnyObject) =>
183 update(record.id, Object.assign(attribute, { song_type: 2 })).then(() => {
184 Message.success(`编辑Demo:${record.song_name}`);
185 tableRef.value?.onFetch();
186 dialog.close();
187 }),
188 }),
189 { title: '编辑Demo', footer: false, width: 'auto', closable: true }
190 );
191 };
192
193 const onDelete = (row: TableData) =>
194 createModalVNode(`确认要将Demo:${row.song_name} 删除吗?`, {
195 title: '删除操作',
196 onBeforeOk: () => promiseToBoolean(useActivityApi.destroy(row.id)),
197 onOk: () => tableRef.value?.onFetch(),
198 });
199 </script>
200
201 <template>
202 <page-view has-bread has-card>
203 <filter-search :model="filter" :loading="loading" @search="onSearch" @reset="onReset">
204 <filter-search-item label="歌曲名称" field="song_name">
205 <a-input v-model="filter.songName" allow-clear placeholder="请输入" />
206 </filter-search-item>
207 <filter-search-item v-if="!hideSearchItem?.includes('projectName')" label="厂牌名称" field="projectName">
208 <a-input v-model="filter.projectName" allow-clear placeholder="请输入" />
209 </filter-search-item>
210 <filter-search-item v-if="!hideSearchItem?.includes('userName')" label="用户名称" field="userName">
211 <a-input v-model="filter.userName" allow-clear placeholder="请输入" />
212 </filter-search-item>
213 <filter-search-item v-if="!hideSearchItem?.includes('createdForm')" label="关联类别" field="createdForm">
214 <a-select v-model="filter.createdForm" :options="platformOption" allow-clear placeholder="请选择" />
215 </filter-search-item>
216 <filter-search-item label="标签名称" field="tagName">
217 <a-input v-model="filter.tagName" allow-clear placeholder="请输入" />
218 </filter-search-item>
219 <filter-search-item label="状态" field="status">
220 <a-select v-model="filter.status" :options="statusOption" allow-clear multiple placeholder="请选择" />
221 </filter-search-item>
222 <filter-search-item v-if="!hideSearchItem?.includes('createBetween')" label="创建时间" field="createBetween">
223 <a-range-picker v-model="filter.createBetween" show-time :time-picker-props="{ defaultValue: ['00:00:00', '23:59:59'] }" />
224 </filter-search-item>
225 </filter-search>
226 <filter-table ref="tableRef" :loading="loading" :on-query="onQuery" @row-sort="onSort">
227 <template #tool>
228 <icon-button v-if="checkPermission('create')" icon="plus" label="新增" type="primary" @click="onCreate" />
229 </template>
230 <template #tool-right>
231 <export-button v-if="checkPermission('export')" :on-download="onExport" />
232 </template>
233 <activity-table-column data-index="id" title="试唱歌曲" :width="420" hide-sub-title />
234 <number-table-column data-index="view_count" title="试听人数" :dark-value="0" :width="110" has-sort />
235 <number-table-column data-index="like_count" title="收藏人数" :dark-value="0" :width="110" has-sort />
236 <date-table-column data-index="created_at" title="创建时间" :width="100" split has-sort />
237 <enum-table-column data-index="status" title="状态" :option="statusOption" :width="80" has-sort />
238 <space-table-column v-if="hasPermission" :width="80" data-index="operations" fixed="right" title="操作" direction="vertical">
239 <template #default="{ record }">
240 <a-link v-if="checkPermission('show')" class="link-hover" :hoverable="false" @click="onShow(record)"> 查看 </a-link>
241 <a-link
242 v-if="record.status !== 0 && record.status !== 5 && checkPermission('edit')"
243 class="link-hover"
244 :hoverable="false"
245 @click="onUpdate(record)"
246 >
247 编辑
248 </a-link>
249 <a-link v-if="record.status === 1 && checkPermission('status')" class="link-hover" :hoverable="false" @click="onDown(record)">
250 下架
251 </a-link>
252 <a-link v-if="record.status === 2 && checkPermission('status')" class="link-hover" :hoverable="false" @click="onUp(record, 'up')">
253 上架
254 </a-link>
255 <a-link v-if="record.status === 2 && checkPermission('delete')" class="link-hover" :hoverable="false" @click="onDelete(record)">
256 删除
257 </a-link>
258 </template>
259 </space-table-column>
260 </filter-table>
261 </page-view>
262 </template>
263
264 <style scoped lang="less"></style>
1 <template>
2 <router-view />
3 </template>
4
5 <script lang="ts" setup></script>
6
7 <style lang="less" scoped>
8 .container {
9 padding: 0 30px 20px 20px;
10 }
11
12 .operations {
13 display: flex;
14 }
15 </style>
1 <script setup lang="ts">
2 import { Image, List, ListItem, ListItemMeta, TypographyParagraph } from '@arco-design/web-vue';
3 import AudioPlayer from '@/components/audio-player/index.vue';
4 import { onMounted, ref } from 'vue';
5 import usePagination from '@/hooks/pagination';
6 import { UserAudioDynamics } from '@/utils/model';
7 import useLoading from '@/hooks/loading';
8 import useProjectApi from '@/http/project';
9
10 const props = defineProps<{ projectKey: number }>();
11
12 const { loading, setLoading } = useLoading(false);
13 const { pagination, setPage, setTotal } = usePagination({ pageSize: 5, size: 'mini', showPageSize: false });
14 const source = ref<UserAudioDynamics[]>([]);
15
16 const onQuery = () => {
17 setLoading(true);
18 useProjectApi
19 .dynamics(props.projectKey, {
20 type: 'audio',
21 page: pagination.value.current,
22 pageSize: pagination.value.pageSize,
23 sortBy: 'is_top',
24 sortType: 'desc',
25 })
26 .then(({ data, meta }) => {
27 source.value = data as UserAudioDynamics[];
28 setPage(meta.current);
29 setTotal(meta.total);
30 })
31 .finally(() => setLoading(false));
32 };
33
34 onMounted(() => onQuery());
35 </script>
36
37 <template>
38 <List
39 :loading="loading"
40 :data="source"
41 :bordered="false"
42 :scrollbar="true"
43 :max-height="460"
44 :pagination-props="pagination"
45 @page-change="(page:number) => setPage(page) && onQuery()"
46 >
47 <template #item="{ item }">
48 <ListItem :key="item.id" style="padding: 4px 16px">
49 <ListItemMeta>
50 <template #avatar>
51 <Image :src="item.properties.cover.url" :width="60" :height="60" fit="fill" />
52 </template>
53 <template #title>
54 <TypographyParagraph :ellipsis="{ rows: 1, showTooltip: true }" style="margin-bottom: 10px; margin-top: 10px">
55 {{ item.intro }}
56 </TypographyParagraph>
57 </template>
58 <template #description>
59 <AudioPlayer :url="item.properties.url" style="width: 410px; height: 20px" />
60 </template>
61 </ListItemMeta>
62 </ListItem>
63 </template>
64 </List>
65 </template>
66
67 <style scoped lang="less">
68 :deep(.arco-list-pagination) {
69 margin-top: 10px !important;
70 margin-bottom: 10px !important;
71 }
72 </style>
1 <script setup lang="ts">
2 import { createVNode, ref, toRef } from 'vue';
3 import { createModalVNode } from '@/utils/createVNode';
4 import { Project } from '@/utils/model';
5 import ImageDynamics from '@/views/audition/project-show/components/image-dynamics.vue';
6 import AudioDynamics from '@/views/audition/project-show/components/audio-dynamics.vue';
7 import VideoDynamics from '@/views/audition/project-show/components/video-dynamics.vue';
8
9 const props = defineProps<{
10 project: Project & { image_dynamic_count: number; audio_dynamic_count: number; video_dynamic_count: number };
11 }>();
12 const project = toRef(props, 'project');
13 const coverVisible = ref(false);
14
15 const onViewImage = () =>
16 createModalVNode(() => createVNode(ImageDynamics, { projectKey: project.value.id }), {
17 title: '厂牌相册',
18 titleAlign: 'center',
19 closable: true,
20 footer: false,
21 escToClose: true,
22 width: '780px',
23 });
24
25 const onViewAudio = () =>
26 createModalVNode(() => createVNode(AudioDynamics, { projectKey: project.value.id }), {
27 title: '厂牌音频',
28 titleAlign: 'center',
29 closable: true,
30 footer: false,
31 escToClose: true,
32 bodyStyle: { padding: '0 !important' },
33 width: '540px',
34 });
35
36 const onViewVideo = () =>
37 createModalVNode(() => createVNode(VideoDynamics, { projectKey: project.value.id }), {
38 title: '厂牌视频',
39 titleAlign: 'center',
40 closable: true,
41 footer: false,
42 escToClose: true,
43 width: '780px',
44 });
45 </script>
46
47 <template>
48 <a-card :bordered="false">
49 <a-form :model="project" size="small" auto-label-width>
50 <a-row justify="start" align="start">
51 <a-col flex="130px" style="border-radius: 60px; overflow: hidden">
52 <a-image show-loader :height="130" :width="130" :src="project.cover" />
53 </a-col>
54 <a-col style="margin-left: 16px" flex="1">
55 <a-row :gutter="16" justify="space-between">
56 <a-col :span="20">
57 <a-form-item label="厂牌名称" :show-colon="true">{{ project.name }}</a-form-item>
58 </a-col>
59 </a-row>
60 <a-grid :cols="4" :col-gap="12" :row-gap="0">
61 <a-grid-item :span="1">
62 <a-form-item label="创建用户" :show-colon="true">
63 <user-link :user="project.user" />
64 </a-form-item>
65 </a-grid-item>
66 <a-grid-item :span="3">
67 <a-form-item label="主理人" :show-colon="true">
68 <user-link v-if="project.master" :user="project.master" />
69 <span v-else style="color: rgba(44, 44, 44, 0.5)"></span>
70 </a-form-item>
71 </a-grid-item>
72 <a-grid-item :span="1">
73 <a-form-item label="试唱歌曲总数" :show-colon="true">
74 {{ project.activity_count || 0 }}
75 </a-form-item>
76 </a-grid-item>
77 <a-grid-item :span="1">
78 <a-form-item label="试唱进行中" :show-colon="true">
79 {{ project.activity_up_count || 0 }}
80 </a-form-item>
81 </a-grid-item>
82 <a-grid-item :span="1">
83 <a-form-item label="试唱已匹配" :show-colon="true">
84 {{ project.activity_match_count || 0 }}
85 </a-form-item>
86 </a-grid-item>
87 <a-grid-item :span="1">
88 <a-form-item label="歌曲已发行" :show-colon="true">
89 {{ project.activity_send_count || 0 }}
90 </a-form-item>
91 </a-grid-item>
92 <a-grid-item :span="1">
93 <a-form-item label="demo歌曲总数" :show-colon="true">
94 {{ project.demo_count || 0 }}
95 </a-form-item>
96 </a-grid-item>
97 <a-grid-item :span="3">
98 <a-form-item label="demo进行中" :show-colon="true">
99 {{ project.demo_up_count || 0 }}
100 </a-form-item>
101 </a-grid-item>
102 <a-grid-item>
103 <a-form-item label="头部封面" :show-colon="true">
104 <a-link v-if="project.head_cover" :hoverable="false" @click="coverVisible = !coverVisible">查看</a-link>
105 <span v-else style="color: rgba(44, 44, 44, 0.5)"></span>
106 </a-form-item>
107 </a-grid-item>
108 <a-grid-item>
109 <a-form-item label="厂牌相册" :show-colon="true">
110 <a-link v-if="project.image_dynamic_count" :hoverable="false" @click="onViewImage">查看</a-link>
111 <span v-else style="color: rgba(44, 44, 44, 0.5)"></span>
112 </a-form-item>
113 </a-grid-item>
114 <a-grid-item>
115 <a-form-item label="厂牌音频" :show-colon="true">
116 <a-link v-if="project.audio_dynamic_count" :hoverable="false" @click="onViewAudio">查看</a-link>
117 <span v-else style="color: rgba(44, 44, 44, 0.5)"></span>
118 </a-form-item>
119 </a-grid-item>
120 <a-grid-item>
121 <a-form-item label="厂牌视频" :show-colon="true">
122 <a-link v-if="project.video_dynamic_count" :hoverable="false" @click="onViewVideo">查看</a-link>
123 <span v-else style="color: rgba(44, 44, 44, 0.5)"></span>
124 </a-form-item>
125 </a-grid-item>
126 </a-grid>
127 <a-form-item label="厂牌简介" :show-colon="true">
128 <a-typography-paragraph
129 v-if="project.intro"
130 style="margin-bottom: 0"
131 type="secondary"
132 :ellipsis="{ rows: 2, showTooltip: true }"
133 >
134 {{ project.intro }}
135 </a-typography-paragraph>
136 <span v-else style="color: #cccccc"></span>
137 </a-form-item>
138 </a-col>
139 </a-row>
140 </a-form>
141 </a-card>
142
143 <a-image-preview v-model:visible="coverVisible" :closable="false" :src="project.head_cover" :actions-layout="[]" />
144 </template>
145
146 <style scoped lang="less">
147 :deep(.arco-typography) {
148 text-align: left;
149 margin-bottom: 0;
150 width: 100%;
151 }
152
153 :deep(.arco-form-item) {
154 margin-bottom: 4px;
155 }
156
157 :deep(.arco-space-item) {
158 margin-bottom: 0 !important;
159 }
160 </style>
1 <script setup lang="ts">
2 import { Input, Link, Message } from '@arco-design/web-vue';
3 import { onMounted, ref } from 'vue';
4 import FilterSearch from '@/components/filter/search.vue';
5 import FilterSearchItem from '@/components/filter/search-item.vue';
6 import FilterTable from '@/components/filter/table.vue';
7 import FilterTableColumn from '@/components/filter/table-column.vue';
8 import PhoneTableColumn from '@/components/filter/phone-table-column.vue';
9 import EnumTableColumn from '@/components/filter/enum-table-column.vue';
10 import useLoading from '@/hooks/loading';
11 import { AnyObject } from '@/types/global';
12 import useUserApi from '@/http/user';
13 import { useManagerApi } from '@/http/project';
14 import { User } from '@/utils/model';
15
16 const props = defineProps<{ projectKey: number }>();
17
18 // eslint-disable-next-line @typescript-eslint/no-unused-vars
19 const emits = defineEmits<{ (e: 'on-add', value: User): void }>();
20
21 const { loading, setLoading } = useLoading(false);
22 const { get, statusOption } = useUserApi;
23
24 const filter = ref({ nick_name: '', real_name: '', phone_like: '', email_like: '', status: [0, 1], scope: [0, 2] });
25 const tableRef = ref();
26
27 const onQuery = async (params?: AnyObject) => {
28 setLoading(true);
29 return get({ excludeManageProject: props.projectKey, ...filter.value, ...params }).finally(() => setLoading(false));
30 };
31
32 const onSearch = () => tableRef.value?.onPageChange(1);
33
34 const onReset = () => {
35 filter.value = { nick_name: '', real_name: '', phone_like: '', email_like: '', status: [0, 1], scope: [0, 2] };
36 onSearch();
37 };
38 const onCheck = (record: User) => {
39 setLoading(true);
40 useManagerApi.create({ project_id: props.projectKey, user_id: record.id }).then(() => {
41 Message.success(`添加管理员:${record.nick_name}`);
42 tableRef.value.onFetch();
43 });
44 };
45
46 onMounted(() => onReset());
47 </script>
48
49 <template>
50 <FilterSearch :model="filter" :loading="loading" @search="onSearch" @reset="onReset">
51 <FilterSearchItem label="用户艺名">
52 <Input v-model="filter.nick_name" allow-clear placeholder="请输入" />
53 </FilterSearchItem>
54 <FilterSearchItem label="用户真名">
55 <Input v-model="filter.real_name" allow-clear placeholder="请输入" />
56 </FilterSearchItem>
57 <FilterSearchItem label="用户邮箱">
58 <Input v-model="filter.email_like" allow-clear placeholder="请输入" />
59 </FilterSearchItem>
60 <FilterSearchItem label="手机号码">
61 <Input v-model="filter.phone_like" allow-clear placeholder="请输入" />
62 </FilterSearchItem>
63 </FilterSearch>
64 <FilterTable ref="tableRef" :loading="loading" :on-query="onQuery" size="small" style="height: 360px" :scroll="{ y: 360 }">
65 <FilterTableColumn title="用户艺名" data-index="nick_name" :width="200" :ellipsis="true" />
66 <FilterTableColumn title="用户真名" data-index="real_name" :width="160" :ellipsis="true" />
67 <FilterTableColumn title="用户邮箱" data-index="email" :width="160" :ellipsis="true" />
68 <PhoneTableColumn title="手机号码" data-index="phone" area-index="area_code" :width="160" :ellipsis="true" />
69 <EnumTableColumn title="状态" data-index="status" :option="statusOption" :dark-value="0" :width="100" />
70 <FilterTableColumn title="操作" :width="80">
71 <template #default="{ record }: { record: User }">
72 <Link class="link-hover" :hoverable="false" @click="onCheck(record)">添加</Link>
73 </template>
74 </FilterTableColumn>
75 </FilterTable>
76 </template>
77
78 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { ImagePreviewGroup, Grid, GridItem, Image, Pagination } from '@arco-design/web-vue';
3 import { onMounted, ref } from 'vue';
4 import usePagination from '@/hooks/pagination';
5 import { UserImageDynamics } from '@/utils/model';
6 import useProjectApi from '@/http/project';
7
8 const props = defineProps<{ projectKey: number }>();
9
10 const { pagination, setPage, setTotal } = usePagination({ pageSize: 12 });
11 const source = ref<UserImageDynamics[]>([]);
12
13 const onQuery = () =>
14 useProjectApi
15 .dynamics(props.projectKey, {
16 type: 'image',
17 page: pagination.value.current,
18 pageSize: pagination.value.pageSize,
19 sortBy: 'is_top',
20 sortType: 'desc',
21 })
22 .then(({ data, meta }) => {
23 source.value = data as UserImageDynamics[];
24 setPage(meta.current);
25 setTotal(meta.total);
26 });
27
28 onMounted(() => onQuery());
29 </script>
30
31 <template>
32 <ImagePreviewGroup>
33 <Grid :cols="6" :col-gap="12" :row-gap="16">
34 <GridItem v-for="item in source" :key="item.id">
35 <Image :src="item.properties.url" :width="120" :height="120" fit="scale-down" :show-loader="true" />
36 </GridItem>
37 </Grid>
38 </ImagePreviewGroup>
39 <Pagination
40 size="mini"
41 style="justify-content: flex-end; margin-top: 10px"
42 :current="pagination.current"
43 :total="pagination.total"
44 :page-size="pagination.pageSize"
45 :hide-on-single-page="true"
46 @change="(current:number) => setPage(current) && onQuery()"
47 />
48 </template>
49
50 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { h, onMounted, ref } from 'vue';
3 import { Card, Input, Select, Link, Message, TableData } from '@arco-design/web-vue';
4 import useLoading from '@/hooks/loading';
5 import { createModalVNode } from '@/utils/createVNode';
6 import { AnyObject } from '@/types/global';
7 import useProjectApi, { useManagerApi } from '@/http/project';
8 import UserTableColumn from '@/components/filter/user-table-column.vue';
9 import useUserApi from '@/http/user';
10 import EnumTableColumn from '@/components/filter/enum-table-column.vue';
11 import PhoneTableColumn from '@/components/filter/phone-table-column.vue';
12 import FilterSearch from '@/components/filter/search.vue';
13 import FilterSearchItem from '@/components/filter/search-item.vue';
14 import FilterTable from '@/components/filter/table.vue';
15 import FilterTableColumn from '@/components/filter/table-column.vue';
16 import FormContent from '@/views/audition/project-show/components/form-content.vue';
17 import usePermission from '@/hooks/permission';
18
19 const props = defineProps<{ projectKey: number; queryHook?: () => void }>();
20 const emits = defineEmits<{ (e: 'delete', id: number): void }>();
21
22 const { loading, setLoading } = useLoading(false);
23 const { sexOption, statusOption, officialStatusOption } = useUserApi;
24 const { manageUsers } = useProjectApi;
25
26 const filter = ref({ nick_name: '', sex: '', phone_like: '', email_like: '', status: '' });
27 const tableRef = ref();
28
29 const onSearch = () => tableRef.value?.onPageChange(1);
30
31 const onReset = () => {
32 filter.value = { nick_name: '', sex: '', phone_like: '', email_like: '', status: '' };
33 onSearch();
34 };
35
36 const onQuery = async (params: AnyObject) => {
37 setLoading(true);
38 props.queryHook?.();
39 return manageUsers(props.projectKey, { ...filter.value, ...params, sortBy: 'id', sortType: 'asc' }).finally(() => setLoading(false));
40 };
41
42 onMounted(() => onReset());
43
44 const onCreate = () => {
45 const formRef = ref();
46
47 createModalVNode(() => h(FormContent, { ref: formRef, projectKey: props.projectKey }), {
48 title: '新增管理员',
49 titleAlign: 'center',
50 width: '1000px',
51 closable: true,
52 footer: false,
53 onBeforeClose: () => tableRef.value?.onFetch(),
54 });
55 };
56
57 const onDelete = (row: TableData) => {
58 setLoading(true);
59 useManagerApi.destroy(row.pivot_id).then(() => {
60 Message.success(`移除管理员:${row.nick_name}`);
61 tableRef.value.onFetch();
62 emits('delete', row.id);
63 });
64 };
65 </script>
66
67 <template>
68 <Card :bordered="false">
69 <FilterSearch :model="filter" :loading="loading" @search="onSearch" @reset="onReset">
70 <FilterSearchItem label="用户艺名">
71 <Input v-model="filter.nick_name" allow-clear placeholder="请输入" />
72 </FilterSearchItem>
73 <FilterSearchItem label="性别">
74 <Select v-model="filter.sex" allow-clear placeholder="请选择" :options="sexOption" />
75 </FilterSearchItem>
76 <FilterSearchItem label="用户邮箱">
77 <Input v-model="filter.email_like" allow-clear placeholder="请输入" />
78 </FilterSearchItem>
79 <FilterSearchItem label="手机号码">
80 <Input v-model="filter.phone_like" allow-clear placeholder="请输入" />
81 </FilterSearchItem>
82 <FilterSearchItem label="状态">
83 <Select v-model="filter.status" allow-clear placeholder="请选择" :options="statusOption" />
84 </FilterSearchItem>
85 </FilterSearch>
86 <FilterTable ref="tableRef" :loading="loading" :on-query="onQuery">
87 <template #tool>
88 <IconButton v-permission="['audition-project-show-manage-create']" icon="plus" label="新增" type="primary" @click="onCreate" />
89 </template>
90 <UserTableColumn title="用户艺名" data-index="id" show-href show-avatar :width="260" />
91 <EnumTableColumn title="性别" data-index="sex" :option="sexOption" :dark-value="0" :width="100" />
92 <FilterTableColumn title="用户邮箱" data-index="email" :width="200" />
93 <PhoneTableColumn title="手机号码" data-index="phone" area-index="area_code" :width="200" />
94 <EnumTableColumn title="关注服务号" data-index="official_status" :option="officialStatusOption" :dark-value="0" :width="120" />
95 <EnumTableColumn title="状态" data-index="status" :option="statusOption" :dark-value="0" :width="100" />
96 <FilterTableColumn v-if="usePermission().checkPermission('audition-project-show-manage-delete')" title="操作" :width="100">
97 <template #default="{ record }">
98 <Link v-permission="['audition-project-show-manage-delete']" class="link-hover" :hoverable="false" @click="onDelete(record)">
99 删除
100 </Link>
101 </template>
102 </FilterTableColumn>
103 </FilterTable>
104 </Card>
105 </template>
106
107 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { Card, Input, Select } from '@arco-design/web-vue';
3 import { onMounted, ref } from 'vue';
4 import useLoading from '@/hooks/loading';
5 import useUserApi from '@/http/user';
6 import { AnyObject } from '@/types/global';
7 import useProjectApi from '@/http/project';
8 import FilterTable from '@/components/filter/table.vue';
9 import FilterSearch from '@/components/filter/search.vue';
10 import FilterSearchItem from '@/components/filter/search-item.vue';
11 import UserTableColumn from '@/components/filter/user-table-column.vue';
12 import EnumTableColumn from '@/components/filter/enum-table-column.vue';
13 import PhoneTableColumn from '@/components/filter/phone-table-column.vue';
14 import FilterTableColumn from '@/components/filter/table-column.vue';
15
16 const props = defineProps<{ projectKey: number; queryHook?: () => void }>();
17
18 const { loading, setLoading } = useLoading(false);
19 const { sexOption, officialStatusOption } = useUserApi;
20 const { memberUsers } = useProjectApi;
21
22 const filter = ref({ nick_name: '', sex: '', phone_like: '', email_like: '' });
23 const tableRef = ref();
24
25 const onSearch = () => tableRef.value?.onPageChange(1);
26
27 const onReset = () => {
28 filter.value = { nick_name: '', sex: '', phone_like: '', email_like: '' };
29 onSearch();
30 };
31
32 const onQuery = async (params: AnyObject) => {
33 setLoading(true);
34 props.queryHook?.();
35 return memberUsers(props.projectKey, { ...filter.value, ...params, sortBy: 'is_top', sortType: 'desc' }).finally(() =>
36 setLoading(false)
37 );
38 };
39
40 onMounted(() => onReset());
41 </script>
42
43 <template>
44 <Card :bordered="false">
45 <FilterSearch :model="filter" :loading="loading" @search="onSearch" @reset="onReset">
46 <FilterSearchItem label="用户艺名">
47 <Input v-model="filter.nick_name" allow-clear placeholder="请输入" />
48 </FilterSearchItem>
49 <FilterSearchItem label="性别">
50 <Select v-model="filter.sex" allow-clear placeholder="请选择" :options="sexOption" />
51 </FilterSearchItem>
52 <FilterSearchItem label="用户邮箱">
53 <Input v-model="filter.email_like" allow-clear placeholder="请输入" />
54 </FilterSearchItem>
55 <FilterSearchItem label="手机号码">
56 <Input v-model="filter.phone_like" allow-clear placeholder="请输入" />
57 </FilterSearchItem>
58 </FilterSearch>
59 <FilterTable ref="tableRef" :loading="loading" :on-query="onQuery">
60 <UserTableColumn title="用户艺名" data-index="id" nick-index="nick_name" show-href show-avatar :width="260" />
61 <EnumTableColumn title="性别" data-index="sex" :option="sexOption" :dark-value="0" :width="100" />
62 <FilterTableColumn title="用户邮箱" data-index="email" :width="200" />
63 <PhoneTableColumn title="手机号码" data-index="phone" area-index="area_code" :width="200" />
64 <EnumTableColumn title="关注服务号" data-index="official_status" :option="officialStatusOption" :dark-value="0" :width="120" />
65 </FilterTable>
66 </Card>
67 </template>
68
69 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { Link, Message, TableData } from '@arco-design/web-vue';
3 import { computed, onMounted, ref } from 'vue';
4 import FilterTable from '@/components/filter/table.vue';
5 import FilterTableColumn from '@/components/filter/table-column.vue';
6 import EnumTableColumn from '@/components/filter/enum-table-column.vue';
7 import SpaceTableColumn from '@/components/filter/space-table-column.vue';
8
9 import useLoading from '@/hooks/loading';
10 import { AnyObject } from '@/types/global';
11 import useUserApi from '@/http/user';
12 import useActivityApi, { useManagerApi } from '@/http/activity';
13 // eslint-disable-next-line @typescript-eslint/no-unused-vars
14 import { User, UserManageActivity } from '@/utils/model';
15 import { createFormVNode, createModalVNode, createSelectionFormItemVNode } from '@/utils/createVNode';
16 import usePermission from '@/hooks/permission';
17 import { promiseToBoolean } from '@/utils';
18
19 const props = defineProps<{ projectKey: number; user: User }>();
20
21 const { loading, setLoading } = useLoading(false);
22 const { permissionOption } = useManagerApi;
23 const { statusOption } = useActivityApi;
24
25 const tableRef = ref();
26
27 const onQuery = async (params: AnyObject) => {
28 setLoading(true);
29 return useUserApi.manageSongs(props.user.id, { project_id: props.projectKey, ...params }).finally(() => setLoading(false));
30 };
31
32 onMounted(() => tableRef.value?.onPageChange(1));
33
34 const label = computed(() => `${props.user.nick_name} 管理歌曲共:${tableRef.value?.getCount()} `);
35
36 const formatPermission = (permission: any[]) =>
37 permission
38 .map((item) => `[${permissionOption.find((option) => option.value === item)?.label}]` || '')
39 .filter((item) => item.length !== 0)
40 .join('');
41
42 const onUpdate = (record: TableData) => {
43 const formValue = ref<string[]>(record?.permission || ['view']);
44 return createModalVNode(
45 () =>
46 createFormVNode(
47 { layout: 'vertical', model: {} },
48 createSelectionFormItemVNode(
49 formValue,
50 permissionOption,
51 { label: '设置用户权限', rowClass: 'mb-0' },
52 {
53 'multiple': true,
54 'onUpdate:modelValue': (val?: string[]) => {
55 if (!val?.includes('view')) {
56 val?.unshift('view');
57 }
58 formValue.value = val || [];
59 },
60 }
61 )
62 ),
63 {
64 title: '修改',
65 titleAlign: 'center',
66 onBeforeOk: () => promiseToBoolean(useManagerApi.update(record.id, { permission: formValue.value })),
67 onOk: () => {
68 Message.success('更新成功');
69 tableRef.value?.onFetch();
70 },
71 }
72 );
73 };
74
75 const onDelete = (record: TableData) => {
76 createModalVNode(`确认取消用户:${props.user?.nick_name} 的外部管理员身份`, {
77 title: '删除操作',
78 onBeforeOk: () => promiseToBoolean(useManagerApi.destroy(record.id)),
79 onOk: () => {
80 Message.success('删除成功');
81 tableRef.value?.onFetch();
82 },
83 });
84 };
85 </script>
86
87 <template>
88 <div style="margin-bottom: 10px">{{ label }}</div>
89 <FilterTable ref="tableRef" :loading="loading" :on-query="onQuery">
90 <FilterTableColumn title="厂牌名称" data-index="project.name" :width="120" />
91 <FilterTableColumn title="歌曲名称" data-index="activity_name" :width="160" />
92 <FilterTableColumn title="权限" data-index="permission" :width="240">
93 <template #default="{ record }: { record: UserManageActivity }">{{ formatPermission(record.permission) }}</template>
94 </FilterTableColumn>
95 <EnumTableColumn title="状态" data-index="activity_status" :width="100" :option="statusOption" />
96 <SpaceTableColumn
97 v-if="usePermission().checkPermission(['audition-project-show-out-manage-edit', 'audition-project-show-out-manage-delete'])"
98 title="操作"
99 :width="120"
100 >
101 <template #default="{ record }">
102 <Link v-permission="['audition-project-show-out-manage-edit']" class="link-hover" :hoverable="false" @click="onUpdate(record)">
103 修改
104 </Link>
105 <Link v-permission="['audition-project-show-out-manage-delete']" class="link-hover" :hoverable="false" @click="onDelete(record)">
106 取消管理
107 </Link>
108 </template>
109 </SpaceTableColumn>
110 </FilterTable>
111 </template>
112
113 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { Card, Input, Link, Message, Select, TableData } from '@arco-design/web-vue';
3 import { h, onMounted, ref } from 'vue';
4 import useLoading from '@/hooks/loading';
5 import FilterSearchItem from '@/components/filter/search-item.vue';
6 import FilterSearch from '@/components/filter/search.vue';
7 import useUserApi from '@/http/user';
8 import FilterTable from '@/components/filter/table.vue';
9 import { AnyObject } from '@/types/global';
10 import useProjectApi from '@/http/project';
11 import EnumTableColumn from '@/components/filter/enum-table-column.vue';
12 import UserTableColumn from '@/components/filter/user-table-column.vue';
13 import PhoneTableColumn from '@/components/filter/phone-table-column.vue';
14 import FilterTableColumn from '@/components/filter/table-column.vue';
15 import { User } from '@/utils/model';
16 import { createModalVNode } from '@/utils/createVNode';
17 import OutFormContent from '@/views/audition/project-show/components/out-form-content.vue';
18 import usePermission from '@/hooks/permission';
19
20 const props = defineProps<{ projectKey: number; queryHook?: () => void }>();
21
22 const { loading, setLoading } = useLoading(false);
23 const { sexOption, statusOption, officialStatusOption } = useUserApi;
24 const { outManageUsers, destroyOutManageUsers } = useProjectApi;
25
26 const filter = ref({ nick_name: '', sex: '', phone_like: '', email_like: '', status: '' });
27 const tableRef = ref();
28
29 const onSearch = () => tableRef.value?.onPageChange(1);
30
31 const onReset = () => {
32 filter.value = { nick_name: '', sex: '', phone_like: '', email_like: '', status: '' };
33 onSearch();
34 };
35
36 const onQuery = async (params: AnyObject) => {
37 setLoading(true);
38 props.queryHook?.();
39 return outManageUsers(props.projectKey, { ...filter.value, ...params, sortBy: 'id', sortType: 'asc' }).finally(() => setLoading(false));
40 };
41
42 onMounted(() => onReset());
43
44 const onView = (row: User) =>
45 createModalVNode(() => h(OutFormContent, { user: row, projectKey: props.projectKey }), {
46 title: '管理歌曲',
47 titleAlign: 'center',
48 footer: false,
49 closable: true,
50 width: '860px',
51 onClose: () => tableRef.value?.onFetch(),
52 });
53
54 const onDelete = (record: TableData) => {
55 createModalVNode(`取消用户:${record?.nick_name} 在此厂牌下的外部管理员身份`, {
56 title: '删除操作',
57 onBeforeOk: (done: any) =>
58 destroyOutManageUsers(props.projectKey, { user_id: record.id })
59 .then(() => {
60 Message.success('删除成功');
61 tableRef.value?.onFetch();
62 done(true);
63 })
64 .catch(() => done(false)),
65 });
66 };
67 </script>
68
69 <template>
70 <Card :bordered="false">
71 <FilterSearch :model="filter" :loading="loading" @search="onSearch" @reset="onReset">
72 <FilterSearchItem label="用户艺名">
73 <Input v-model="filter.nick_name" allow-clear placeholder="请输入" />
74 </FilterSearchItem>
75 <FilterSearchItem label="性别">
76 <Select v-model="filter.sex" allow-clear placeholder="请选择" :options="sexOption" />
77 </FilterSearchItem>
78 <FilterSearchItem label="用户邮箱">
79 <Input v-model="filter.email_like" allow-clear placeholder="请输入" />
80 </FilterSearchItem>
81 <FilterSearchItem label="手机号码">
82 <Input v-model="filter.phone_like" allow-clear placeholder="请输入" />
83 </FilterSearchItem>
84 <FilterSearchItem label="状态">
85 <Select v-model="filter.status" allow-clear placeholder="请选择" :options="statusOption" />
86 </FilterSearchItem>
87 </FilterSearch>
88 <FilterTable ref="tableRef" :loading="loading" :on-query="onQuery">
89 <UserTableColumn title="用户艺名" data-index="id" show-href show-avatar :width="260" />
90 <EnumTableColumn title="性别" data-index="sex" :option="sexOption" :dark-value="0" :width="80" />
91 <FilterTableColumn title="用户邮箱" data-index="email" :width="180" />
92 <PhoneTableColumn title="手机号码" data-index="phone" area-index="area_code" :width="160" />
93 <FilterTableColumn title="管理歌曲" :width="100">
94 <template #default="{ record }: { record: User & { activities_count: number[] } }">
95 <Link class="link-hover" :hoverable="false" @click="onView(record)">{{ record.activities_count }}</Link>
96 </template>
97 </FilterTableColumn>
98 <EnumTableColumn title="关注服务号" data-index="official_status" :option="officialStatusOption" :dark-value="0" :width="120" />
99 <EnumTableColumn title="状态" data-index="status" :option="statusOption" :dark-value="0" :width="80" />
100 <FilterTableColumn v-if="usePermission().checkPermission('audition-project-show-out-manage-delete')" title="操作" :width="100">
101 <template #default="{ record }">
102 <Link v-permission="['audition-project-show-out-manage-delete']" class="link-hover" :hoverable="false" @click="onDelete(record)">
103 删除
104 </Link>
105 </template>
106 </FilterTableColumn>
107 </FilterTable>
108 </Card>
109 </template>
110
111 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { ImagePreviewGroup, Grid, GridItem, Image, Pagination } from '@arco-design/web-vue';
3 import { h, onMounted, ref } from 'vue';
4 import usePagination from '@/hooks/pagination';
5 import { UserAudioDynamics } from '@/utils/model';
6 import useProjectApi from '@/http/project';
7 import { createModalVNode } from '@/utils/createVNode';
8
9 import vue3videoPlay from 'vue3-video-play';
10 import 'vue3-video-play/dist/style.css';
11
12 const props = defineProps<{ projectKey: number }>();
13
14 const { pagination, setPage, setTotal } = usePagination({ pageSize: 12 });
15 const source = ref<UserAudioDynamics[]>([]);
16
17 const onQuery = () =>
18 useProjectApi
19 .dynamics(props.projectKey, {
20 type: 'video',
21 page: pagination.value.current,
22 pageSize: pagination.value.pageSize,
23 sortBy: 'is_top',
24 sortType: 'desc',
25 })
26 .then(({ data, meta }) => {
27 source.value = data as UserAudioDynamics[];
28 setPage(meta.current);
29 setTotal(meta.total);
30 });
31
32 onMounted(() => onQuery());
33
34 const onClick = (item: any) =>
35 createModalVNode(
36 () => h(vue3videoPlay, { src: item.properties.url, controlBtns: ['volume', 'speedRate', 'pip', 'pageFullScreen', 'fullScreen'] }),
37 {
38 width: 'auto',
39 modalStyle: { padding: '0 !important' },
40 simple: true,
41 footer: false,
42 maskClosable: true,
43 escToClose: true,
44 }
45 );
46 </script>
47
48 <template>
49 <ImagePreviewGroup>
50 <Grid :cols="6" :col-gap="12" :row-gap="16">
51 <GridItem v-for="item in source" :key="item.id">
52 <Image
53 :src="item.properties.cover.url"
54 :width="120"
55 :height="70"
56 fit="scale-down"
57 :show-loader="true"
58 :preview="false"
59 @click="onClick(item)"
60 />
61 </GridItem>
62 </Grid>
63 </ImagePreviewGroup>
64 <Pagination
65 size="mini"
66 style="justify-content: flex-end; margin-top: 10px"
67 :current="pagination.current"
68 :total="pagination.total"
69 :page-size="pagination.pageSize"
70 :hide-on-single-page="true"
71 @change="(current:number) => setPage(current) && onQuery()"
72 />
73 </template>
74
75 <style scoped lang="less"></style>
1 <template>
2 <page-view :loading="loading" has-bread>
3 <basic-card :project="project as any" />
4
5 <a-card v-if="tabKeys.length" :bordered="false" style="margin-top: 16px">
6 <a-tabs v-model:active-key="tabKey" type="rounded" :animation="true" size="small" :justify="true">
7 <a-tab-pane v-if="checkPermission('activity')" key="activity" :title="`歌曲列表(${count.activity})`">
8 <activity-card
9 style="padding: 0"
10 :has-bread="false"
11 :init-filter="{ status: '', project_id: projectKey }"
12 :create-prop="{ initValue: { project_id: projectKey }, hideField: ['project_id'] }"
13 :hide-search-item="['projectName', 'userName']"
14 :export-name="project.name"
15 :query-hook="() => syncActivityCount()"
16 />
17 </a-tab-pane>
18 <a-tab-pane v-if="checkPermission('demo')" key="demo" :title="`Demo列表(${count.demo})`">
19 <demo-card
20 style="padding: 0"
21 :has-bread="false"
22 :init-filter="{ status: '', project_id: projectKey }"
23 :create-prop="{ project_id: projectKey }"
24 :hide-search-item="['projectName', 'createdForm', 'userName']"
25 :export-name="project.name"
26 :query-hook="() => syncDemoCount()"
27 />
28 </a-tab-pane>
29 <a-tab-pane v-if="checkPermission('member')" key="member" :title="`厂牌成员(${count.member})`">
30 <member-table :project-key="projectKey" :query-hook="() => syncMemberCount()" />
31 </a-tab-pane>
32 <a-tab-pane v-if="checkPermission('manage')" key="manage" :title="`厂牌管理(${count.manager})`">
33 <manage-table :project-key="projectKey" :query-hook="() => syncManageCount()" @delete="onDeleteMaster" />
34 </a-tab-pane>
35 <a-tab-pane v-if="checkPermission('out-manage')" key="out-manage" :title="`外部管理(${count.out_manager})`">
36 <out-manage-table :project-key="projectKey" :query-hook="() => syncOutManageCount()" />
37 </a-tab-pane>
38 </a-tabs>
39 </a-card>
40 </page-view>
41 </template>
42
43 <script lang="ts" setup>
44 import { onBeforeRouteLeave, useRoute } from 'vue-router';
45 import { useRouteQuery } from '@vueuse/router';
46 import { computed, onMounted, ref } from 'vue';
47 import ActivityCard from '@/views/audition/activity/index.vue';
48 import DemoCard from '@/views/audition/demo/index.vue';
49 import BasicCard from '@/views/audition/project-show/components/basic-card.vue';
50 import useLoading from '@/hooks/loading';
51
52 import useProjectApi from '@/http/project';
53 import useActivityApi from '@/http/activity';
54 import ManageTable from '@/views/audition/project-show/components/manage-table.vue';
55 import OutManageTable from '@/views/audition/project-show/components/out-manage-table.vue';
56 import MemberTable from '@/views/audition/project-show/components/member-table.vue';
57 import usePermission from '@/hooks/permission';
58 import { isArray } from '@/utils/is';
59
60 const routeName = useRoute()?.name as string;
61 const checkPermission = (permission: string | string[]): boolean => {
62 permission = isArray(permission) ? permission.map((item) => `${routeName}-${item}`) : `${routeName}-${permission}`;
63 return usePermission().checkPermission(permission);
64 };
65
66 const tabKeys = computed((): string[] => {
67 return ['activity', 'demo', 'member', 'manage', 'out-manage'].filter((item) => checkPermission(item));
68 });
69
70 const tabKey = useRouteQuery('tabKey', tabKeys.value[0] || '');
71
72 const projectKey = Number(useRoute().params?.id);
73
74 const project = ref({ name: '', master_id: 0, master: undefined });
75
76 const { loading, setLoading } = useLoading(true);
77
78 const count = ref({ activity: 0, demo: 0, member: 0, manager: 0, out_manager: 0 });
79
80 const onDeleteMaster = (id: number) => {
81 if (project.value.master_id === id) {
82 project.value.master_id = 0;
83 project.value.master = undefined;
84 }
85 };
86
87 const syncActivityCount = () =>
88 useActivityApi
89 .get({ status: '', audit_status: 1, song_type: 1, project_id: projectKey, pageSize: 1 })
90 .then(({ meta }) => (count.value.activity = meta.total));
91
92 const syncDemoCount = () =>
93 useActivityApi
94 .get({ status: '', audit_status: 1, song_type: 2, project_id: projectKey, pageSize: 1 })
95 .then(({ meta }) => (count.value.demo = meta.total));
96
97 const syncManageCount = () =>
98 useProjectApi.manageUsers(projectKey, { pageSize: 1 }).then(({ meta }) => (count.value.manager = meta.total));
99
100 const syncMemberCount = () =>
101 useProjectApi.memberUsers(projectKey, { pageSize: 1 }).then(({ meta }) => (count.value.member = meta.total));
102
103 const syncOutManageCount = () =>
104 useProjectApi.outManageUsers(projectKey, { pageSize: 1 }).then(({ meta }) => (count.value.out_manager = meta.total));
105
106 onMounted(() => {
107 useProjectApi
108 .show(projectKey)
109 .then((data) => (project.value = data as any))
110 .finally(() => setLoading(false));
111 });
112
113 onBeforeRouteLeave((to, from) => {
114 if (from.meta.from === to.name) {
115 to.meta.reload = false;
116 }
117 });
118 </script>
1 <script setup lang="ts">
2 import { Form, FormItem, Input, Select, Textarea } from '@arco-design/web-vue';
3 import { computed, onBeforeMount, ref } from 'vue';
4 import { FieldRule, FormInstance } from '@arco-design/web-vue/es/form';
5 import AvatarUpload from '@/components/avatar-upload/index.vue';
6 import useProjectApi from '@/http/project';
7 import { Project, User } from '@/utils/model';
8 import { AnyObject } from '@/types/global';
9
10 const { publicStatusOption, applyStatusOption, manageStatusOption } = useProjectApi;
11
12 const props = defineProps<{
13 modelValue: AnyObject;
14 hideMaster?: boolean;
15 submit?: (value: AnyObject) => Promise<Project>;
16 }>();
17 const emits = defineEmits<{ (e: 'update:modelValue', value: AnyObject): void }>();
18
19 const formRef = ref<FormInstance>();
20
21 const formRule = {
22 cover: [{ required: true, message: '请上传厂牌图片' }],
23 name: [{ required: true, message: '请输入厂牌名称' }],
24 intro: [{ type: 'string', max: 100, message: '厂牌简介不能超过100字' }],
25 is_public: [{ required: true, message: '请确认是否仅显示自己创建的试唱歌曲' }],
26 is_can_apply: [{ required: true, message: '请确认是否允许提交歌曲上架' }],
27 is_can_manage: [{ required: true, message: '请确认是否允许自行添加外部成员' }],
28 is_can_demo_apply: [{ required: true, message: '请确认是否允许提交Demo上架' }],
29 is_demo_audit: [{ required: true, message: '请确认Demo上架是否需要审核' }],
30 } as Record<string, FieldRule[]>;
31
32 const formValue = computed({
33 get: () => props.modelValue,
34 set: (val) => emits('update:modelValue', val),
35 });
36
37 const onSubmit = async () => {
38 const error = await formRef.value?.validate();
39
40 return error ? Promise.reject(error) : props.submit?.(formValue.value);
41 };
42
43 const masterOption = ref<User[]>([]);
44
45 defineExpose({ onSubmit });
46
47 onBeforeMount(async () => {
48 if (props.modelValue?.id) {
49 await useProjectApi
50 .manageUsers(props.modelValue?.id as number, { fetchType: 'all', setColumn: ['id', 'nick_name'], status: 1 })
51 .then(({ data }) => {
52 masterOption.value = [{ id: 0, nick_name: '无' } as User, ...data];
53 });
54 }
55 });
56 </script>
57
58 <template>
59 <Form ref="formRef" layout="vertical" :model="formValue" :rules="formRule">
60 <FormItem hide-label content-class="justify-center" :wrapper-col-props="{ alignItems: 'center' }">
61 <AvatarUpload v-model="formValue.cover" :size="100" />
62 </FormItem>
63 <FormItem label="厂牌名称" field="name" show-colon>
64 <Input v-model.trim="formValue.name" placeholder="请输入" :max-length="30" show-word-limit />
65 </FormItem>
66 <FormItem hide-label row-class="mb-0">
67 <FormItem v-if="!hideMaster" label="主理人" field="master_id" row-class="formItemRow" show-colon>
68 <Select
69 v-model.number="formValue.master_id"
70 :field-names="{ value: 'id', label: 'nick_name' }"
71 :fallback-option="false"
72 :options="masterOption"
73 placeholder="请选择"
74 />
75 </FormItem>
76 <FormItem label="是否允许提交歌曲上架" field="is_can_apply" row-class="formItemRow" show-colon>
77 <Select v-model.number="formValue.is_can_apply" :options="applyStatusOption" placeholder="请选择" />
78 </FormItem>
79 </FormItem>
80 <FormItem hide-label row-class="mb-0">
81 <FormItem label="仅显示自己创建的试唱歌曲" field="is_public" row-class="formItemRow" show-colon>
82 <Select v-model.number="formValue.is_public" :options="publicStatusOption" placeholder="请选择" />
83 </FormItem>
84 <FormItem label="是否允许自行添加外部成员" field="is_can_manage" row-class="formItemRow" show-colon>
85 <Select v-model.number="formValue.is_can_manage" :options="manageStatusOption" placeholder="请选择" />
86 </FormItem>
87 </FormItem>
88 <FormItem hide-label row-class="mb-0">
89 <FormItem label="是否允许提交Demo上架" field="is_can_demo_apply" row-class="formItemRow" show-colon>
90 <Select v-model.number="formValue.is_can_demo_apply" :options="applyStatusOption" placeholder="请选择" />
91 </FormItem>
92 <!-- <FormItem label="Demo上架是否需要审核" field="is_demo_audit" row-class="formItemRow" show-colon>-->
93 <!-- <Select v-model.number="formValue.is_demo_audit" :options="applyStatusOption" placeholder="请选择" />-->
94 <!-- </FormItem>-->
95 </FormItem>
96 <FormItem label="厂牌简介" field="intro" row-class="mb-0" show-colon>
97 <Textarea v-model.trim="formValue.intro" :auto-size="{ maxRows: 4, minRows: 4 }" :max-length="50" show-word-limit />
98 </FormItem>
99 </Form>
100 </template>
101
102 <style scoped lang="less">
103 .formItemRow:not(:first-child) {
104 margin-left: 8px;
105 }
106 </style>
1 <script setup lang="ts">
2 import { Input, Message, Link, TableData } from '@arco-design/web-vue';
3
4 import { createVNode, onMounted, ref } from 'vue';
5 import { useRouter } from 'vue-router';
6 import { AnyObject } from '@/types/global';
7 import useLoading from '@/hooks/loading';
8 import useProjectApi from '@/http/project';
9 import { createModalVNode } from '@/utils/createVNode';
10 import { Project } from '@/utils/model';
11 import openNewTab from '@/utils/route-blank';
12 import NumberTableColumn from '@/components/filter/number-table-column.vue';
13 import FormContent from '@/views/audition/project/components/form-content.vue';
14 import UserTableColumn from '@/components/filter/user-table-column.vue';
15 import SpaceTableColumn from '@/components/filter/space-table-column.vue';
16 import usePermission from '@/hooks/permission';
17
18 const { get, create, update, destroy, getExport } = useProjectApi;
19 const { checkPermission } = usePermission();
20
21 const router = useRouter();
22
23 const filter = ref<AnyObject>({});
24 const tableRef = ref();
25 const { loading, setLoading } = useLoading(false);
26
27 const onQuery = async (params?: AnyObject) => {
28 setLoading(true);
29 return get({
30 ...filter.value,
31 setWith: ['master:id,nick_name,identity'],
32 setWithCount: ['activity_up', 'activity_match', 'activity_send', 'manage', 'member'],
33 ...params,
34 }).finally(() => setLoading(false));
35 };
36
37 const onSearch = () => tableRef.value?.onPageChange(1);
38
39 const onReset = () => {
40 filter.value = { name: '', masterName: '', status: '', isPromote: '', sortBy: 'id', sortType: 'desc' };
41 tableRef.value?.resetSort();
42 onSearch();
43 };
44
45 const onSort = (column: string, type: string) => {
46 filter.value.sortBy = type ? column : 'id';
47 filter.value.sortType = type || 'desc';
48 tableRef.value?.onFetch();
49 };
50
51 onMounted(() => onReset());
52
53 const onExport = () =>
54 getExport('厂牌', {
55 ...filter.value,
56 setWith: ['master:id,nick_name,identity'],
57 setWithCount: ['activity_up', 'activity_match', 'activity_send', 'manage', 'member'],
58 });
59
60 const onCreate = () => {
61 const formRef = ref();
62 const formValue = ref({ cover: '', name: '', intro: '', is_demo_audit: 0 });
63 createModalVNode(
64 () =>
65 createVNode(FormContent, {
66 'ref': formRef,
67 'hide-master': true,
68 'model-value': formValue.value,
69 'onUpdate:model-value': (val: any) => (formValue.value = val),
70 'submit': (value: Omit<Project, 'id'>) => create(value),
71 }),
72 {
73 title: '新增厂牌',
74 titleAlign: 'center',
75 onBeforeOk: (done: any) =>
76 formRef.value
77 ?.onSubmit()
78 .then(({ name }: Project) => {
79 tableRef.value.onFetch();
80 Message.success(`创建厂牌:${name}`);
81 done(true);
82 })
83 .catch(() => done(false)),
84 }
85 );
86 };
87
88 const onUpdate = (row: TableData) => {
89 const formRef = ref();
90 const formValue = ref({ ...row });
91 createModalVNode(
92 () =>
93 createVNode(FormContent, {
94 'ref': formRef,
95 'model-value': formValue.value,
96 'onUpdate:model-value': (val: any) => (formValue.value = val),
97 'submit': (value: Omit<Project, 'id'>) => update(row.id, value),
98 }),
99 {
100 title: '厂牌编辑',
101 titleAlign: 'center',
102 onBeforeOk: (done: any) =>
103 formRef.value
104 ?.onSubmit()
105 .then(({ name }: Project) => {
106 tableRef.value.onFetch();
107 Message.success(`更新厂牌:${name}`);
108 done(true);
109 })
110 .catch(() => done(false)),
111 }
112 );
113 };
114
115 const onDelete = (row: TableData) => {
116 createModalVNode('请确认是否删除此厂牌', {
117 title: '厂牌删除',
118 onBeforeOk: () =>
119 destroy(row.id)
120 .then(() => true)
121 .catch(() => false),
122 onOk: () => tableRef.value.onFetch(),
123 });
124 };
125
126 // const onShow = (record: TableData) => router.push({ name: 'audition-project-show', params: { id: record.id } });
127 const onShow = (record: TableData) => openNewTab(router, 'audition-project-show', { id: record.id });
128 </script>
129
130 <template>
131 <PageView has-card has-bread>
132 <FilterSearch :model="filter" :loading="loading" :inline="true" @search="onSearch" @reset="onReset">
133 <FilterSearchItem label="厂牌名称" field="name">
134 <Input v-model="filter.name" allow-clear placeholder="请输入" />
135 </FilterSearchItem>
136 <FilterSearchItem label="主理人" field="masterName">
137 <Input v-model="filter.masterName" allow-clear placeholder="请输入" />
138 </FilterSearchItem>
139 </FilterSearch>
140 <FilterTable ref="tableRef" :loading="loading" :on-query="onQuery" @row-sort="onSort">
141 <template #tool>
142 <IconButton v-permission="['audition-project-create']" type="primary" icon="plus" label="新增" @click="onCreate" />
143 </template>
144 <template #tool-right>
145 <ExportButton v-permission="['audition-project-export']" :on-download="onExport" />
146 </template>
147 <UserTableColumn title="厂牌名称" data-index="id" nick-index="name" avatar-index="cover" :width="240" show-avatar />
148 <UserTableColumn title="主理人" data-index="master_id" user="master" :width="160" show-href dark-value="" />
149 <NumberTableColumn title="厂牌管理员人数" data-index="manage_count" :dark-value="0" :width="120" has-sort />
150 <NumberTableColumn title="厂牌成员人数" data-index="member_count" :dark-value="0" :width="120" has-sort />
151 <NumberTableColumn title="上架歌曲数" data-index="activity_up_count" :dark-value="0" :width="110" has-sort />
152 <NumberTableColumn title="已匹配歌曲数" data-index="activity_match_count" :dark-value="0" :width="110" has-sort />
153 <NumberTableColumn title="已发行歌曲数" data-index="activity_send_count" :dark-value="0" :width="110" has-sort />
154 <SpaceTableColumn
155 v-if="checkPermission(['audition-project-show', 'audition-project-edit', 'audition-project-delete'])"
156 title="操作"
157 data-index="operation"
158 :width="120"
159 >
160 <template #default="{ record }">
161 <Link v-permission="['audition-project-show']" class="link-hover" :hoverable="false" @click="onShow(record)">查看</Link>
162 <Link v-permission="['audition-project-edit']" class="link-hover" :hoverable="false" @click="onUpdate(record)">编辑</Link>
163 <Link v-permission="['audition-project-delete']" class="link-hover" :hoverable="false" @click="onDelete(record)">删除</Link>
164 </template>
165 </SpaceTableColumn>
166 </FilterTable>
167 </PageView>
168 </template>
169
170 <style scoped lang="less"></style>
1 <template>
2 <a-card>
3 <div class="content-wrap">
4 <div class="content">
5 <a-statistic :value="todayTotal" :value-from="0" animation show-group-separator title="今日用户收藏" />
6 <div class="desc">
7 <a-typography-text class="label" type="secondary"> 较昨日</a-typography-text>
8 <a-typography-text v-if="diffTotal < 0" type="success">
9 {{ diffTotal }}
10 <icon-arrow-fall />
11 </a-typography-text>
12 <a-typography-text v-else type="danger">
13 {{ diffTotal }}
14 <icon-arrow-rise />
15 </a-typography-text>
16 </div>
17 </div>
18 <div class="chart">
19 <Chart :option="chartOption" style="width: 100%" />
20 </div>
21 </div>
22 </a-card>
23 </template>
24
25 <script lang="ts" setup>
26 import useChartOption from '@/hooks/chart-option';
27 import { IconArrowFall, IconArrowRise } from '@arco-design/web-vue/es/icon';
28 import { computed, onMounted, ref } from 'vue';
29 import { subtract } from 'lodash';
30 import useDashboardApi from '@/http/dashboard';
31 import { ToolTipFormatterParams } from '@/types/echarts';
32
33 const record = ref<{ time: string[]; data: number[] }>({
34 time: [],
35 data: [],
36 });
37
38 const todayTotal = computed(() => {
39 return record.value.data[record.value.data.length - 1] || 0;
40 });
41 const diffTotal = computed(() => {
42 return subtract(record.value.data[record.value.data.length - 1], record.value.data[record.value.data.length - 2]) || 0;
43 });
44
45 const formatTooltip = (params: ToolTipFormatterParams[]) => {
46 const [firstElement] = params;
47 return `
48 <div class="content-panel">
49 <p>
50 <span style="background-color: ${firstElement.color}" class="tooltip-item-icon"></span>
51 <span>${firstElement.name}</span>
52 </p>
53 <span class="tooltip-value">${firstElement.value?.toLocaleString()}</span>
54 </div>`;
55 };
56
57 const { chartOption } = useChartOption(() => {
58 return {
59 grid: { left: 0, right: 0, top: 10, bottom: 5 },
60 xAxis: { type: 'category', show: false, boundaryGap: true, data: record.value.time },
61 yAxis: { show: false, type: 'value', scale: true },
62 tooltip: {
63 show: true,
64 trigger: 'axis',
65 formatter: (params) => formatTooltip(params as ToolTipFormatterParams[]),
66 className: 'echarts-tooltip-diy',
67 },
68 series: { data: record.value.data, type: 'line', showSymbol: false, smooth: true, lineStyle: { width: 3, color: '#165DFF' } },
69 };
70 });
71
72 onMounted(async () => {
73 record.value = await useDashboardApi.activityLike();
74 });
75 </script>
76
77 <style lang="less" scoped>
78 :deep(.arco-card) {
79 border-radius: 4px;
80 }
81
82 :deep(.arco-card-body) {
83 width: 100%;
84 height: 134px;
85 //padding: 0;
86 }
87
88 .content-wrap {
89 width: 100%;
90 //padding: 16px;
91 white-space: nowrap;
92 }
93
94 :deep(.content) {
95 float: left;
96 width: 108px;
97 height: 102px;
98 }
99
100 :deep(.arco-statistic) {
101 .arco-statistic-title {
102 font-size: 14px;
103 font-weight: bold;
104 white-space: nowrap;
105 }
106
107 .arco-statistic-content {
108 margin-top: 10px;
109 }
110 }
111
112 .chart {
113 float: right;
114 width: calc(100% - 108px);
115 height: 100px;
116 vertical-align: bottom;
117 }
118
119 .label {
120 padding-right: 8px;
121 font-size: 12px;
122 }
123 </style>
1 <template>
2 <a-card>
3 <div class="content-wrap">
4 <div class="content">
5 <a-statistic :value="todayTotal" :value-from="0" animation show-group-separator title="今日用户试听" />
6 <div class="desc">
7 <a-typography-text class="label" type="secondary"> 较昨日</a-typography-text>
8 <a-typography-text v-if="diffTotal < 0" type="success">
9 {{ diffTotal }}
10 <icon-arrow-fall />
11 </a-typography-text>
12 <a-typography-text v-else type="danger">
13 {{ diffTotal }}
14 <icon-arrow-rise />
15 </a-typography-text>
16 </div>
17 </div>
18 <div class="chart">
19 <Chart :option="chartOption" style="width: 100%" />
20 </div>
21 </div>
22 </a-card>
23 </template>
24
25 <script lang="ts" setup>
26 import useChartOption from '@/hooks/chart-option';
27 import { IconArrowFall, IconArrowRise } from '@arco-design/web-vue/es/icon';
28 import { computed, onMounted, ref } from 'vue';
29 import { subtract } from 'lodash';
30 import useDashboardApi from '@/http/dashboard';
31 import { ToolTipFormatterParams } from '@/types/echarts';
32
33 const record = ref<{ time: string[]; data: number[] }>({
34 time: [],
35 data: [],
36 });
37
38 const todayTotal = computed(() => {
39 return record.value.data[record.value.data.length - 1] || 0;
40 });
41 const diffTotal = computed(() => {
42 return subtract(record.value.data[record.value.data.length - 1], record.value.data[record.value.data.length - 2]) || 0;
43 });
44
45 const formatTooltip = (params: ToolTipFormatterParams[]) => {
46 const [firstElement] = params;
47 return `
48 <div class="content-panel">
49 <p>
50 <span style="background-color: ${firstElement.color}" class="tooltip-item-icon"></span>
51 <span>${firstElement.name}</span>
52 </p>
53 <span class="tooltip-value">${firstElement.value?.toLocaleString()}</span>
54 </div>`;
55 };
56
57 const { chartOption } = useChartOption(() => {
58 return {
59 grid: { left: 0, right: 0, top: 10, bottom: 5 },
60 xAxis: { type: 'category', show: false, boundaryGap: true, data: record.value.time },
61 yAxis: { show: false, type: 'value', scale: true },
62 tooltip: {
63 show: true,
64 trigger: 'axis',
65 formatter: (params) => formatTooltip(params as ToolTipFormatterParams[]),
66 className: 'echarts-tooltip-diy',
67 },
68 series: { data: record.value.data, type: 'line', showSymbol: false, smooth: true, lineStyle: { width: 3, color: '#165DFF' } },
69 };
70 });
71
72 onMounted(async () => {
73 record.value = await useDashboardApi.activityListen();
74 });
75 </script>
76
77 <style lang="less" scoped>
78 :deep(.arco-card) {
79 border-radius: 4px;
80 }
81
82 :deep(.arco-card-body) {
83 width: 100%;
84 height: 134px;
85 //padding: 0;
86 }
87
88 .content-wrap {
89 width: 100%;
90 //padding: 16px;
91 white-space: nowrap;
92 }
93
94 :deep(.content) {
95 float: left;
96 width: 108px;
97 height: 102px;
98 }
99
100 :deep(.arco-statistic) {
101 .arco-statistic-title {
102 font-size: 14px;
103 font-weight: bold;
104 white-space: nowrap;
105 }
106
107 .arco-statistic-content {
108 margin-top: 10px;
109 }
110 }
111
112 .chart {
113 float: right;
114 width: calc(100% - 108px);
115 height: 100px;
116 vertical-align: bottom;
117 }
118
119 .label {
120 padding-right: 8px;
121 font-size: 12px;
122 }
123 </style>
1 <template>
2 <a-card>
3 <div class="content-wrap">
4 <div class="content">
5 <div data-v-55251d00="" class="arco-statistic">
6 <div class="arco-statistic-title">上架歌曲曲风</div>
7 <div class="arco-statistic-content">
8 <div class="arco-statistic-value">
9 <span class="arco-statistic-value-integer"></span>
10 </div>
11 </div>
12 </div>
13 </div>
14 <div class="chart">
15 <Chart :option="chartOption" style="width: 100%" />
16 </div>
17 </div>
18 </a-card>
19 </template>
20
21 <script lang="ts" setup>
22 import useChartOption from '@/hooks/chart-option';
23 import { onMounted, ref } from 'vue';
24 import Chart from '@/components/chart/index.vue';
25
26 import useDashboardApi from '@/http/dashboard';
27 import { ToolTipFormatterParams } from '@/types/echarts';
28
29 type recordType = { value: number; name: string };
30
31 const record = ref<recordType[]>([]);
32
33 const { chartOption } = useChartOption(() => {
34 return {
35 grid: { left: 0, right: 0, top: 0, bottom: 0 },
36 label: { show: false },
37 tooltip: {
38 show: true,
39 trigger: 'item',
40 formatter(params) {
41 const item = params as ToolTipFormatterParams;
42
43 return `
44 <div class="content-panel">
45 <p>
46 <span style="background-color: ${item.color}" class="tooltip-item-icon"></span>
47 <span>${item.name}</span>
48 </p>
49 <span class="tooltip-value">${item.value?.toLocaleString()}</span>
50 <span class="tooltip-value">${item.percent?.toLocaleString()}%</span>
51 </div>`;
52 },
53 className: 'echarts-tooltip-diy',
54 },
55 series: [{ type: 'pie', radius: ['65%', '90%'], label: { show: false }, data: record.value }],
56 };
57 });
58
59 onMounted(async () => {
60 record.value = await useDashboardApi.activityStyle();
61 });
62 </script>
63
64 <style lang="less" scoped>
65 .echarts-tooltip-diy {
66 background: none !important;
67 border: none !important;
68 }
69
70 :deep(.arco-card) {
71 border-radius: 4px;
72 }
73
74 :deep(.arco-card-body) {
75 width: 100%;
76 height: 134px;
77 }
78
79 .content-wrap {
80 width: 100%;
81 height: 100%;
82 display: flex;
83 white-space: nowrap;
84
85 .content {
86 width: 108px;
87
88 :deep(.arco-statistic) {
89 .arco-statistic-title {
90 font-size: 14px;
91 font-weight: bold;
92 white-space: nowrap;
93 }
94
95 .arco-statistic-content {
96 margin-top: 10px;
97 }
98 }
99 }
100
101 .chart {
102 width: calc(100% - 108px);
103 vertical-align: bottom;
104 }
105 }
106 </style>
1 <template>
2 <a-card>
3 <div class="content-wrap">
4 <div class="content">
5 <a-statistic :value="todayTotal" :value-from="0" animation show-group-separator title="上架的歌曲" />
6 <div class="desc">
7 <a-typography-text class="label" type="secondary"> 较昨日</a-typography-text>
8 <a-typography-text v-if="diffTotal < 0" type="success">
9 {{ diffTotal }}
10 <icon-arrow-fall />
11 </a-typography-text>
12 <a-typography-text v-else type="danger">
13 {{ diffTotal }}
14 <icon-arrow-rise />
15 </a-typography-text>
16 </div>
17 </div>
18 <div class="chart">
19 <Chart :option="chartOption" style="width: 100%" />
20 </div>
21 </div>
22 </a-card>
23 </template>
24
25 <script lang="ts" setup>
26 import useChartOption from '@/hooks/chart-option';
27 import { IconArrowFall, IconArrowRise } from '@arco-design/web-vue/es/icon';
28 import { computed, onMounted, ref } from 'vue';
29 import { subtract } from 'lodash';
30 import useDashboardApi from '@/http/dashboard';
31 import { ToolTipFormatterParams } from '@/types/echarts';
32
33 const record = ref<{ time: string[]; data: number[] }>({
34 time: [],
35 data: [],
36 });
37
38 const todayTotal = computed(() => {
39 return record.value.data[record.value.data.length - 1] || 0;
40 });
41 const diffTotal = computed(() => {
42 return subtract(record.value.data[record.value.data.length - 1], record.value.data[record.value.data.length - 2]) || 0;
43 });
44
45 const formatTooltip = (params: ToolTipFormatterParams[]) => {
46 const [firstElement] = params;
47 return `
48 <div class="content-panel">
49 <p>
50 <span style="background-color: ${firstElement.color}" class="tooltip-item-icon"></span>
51 <span>${firstElement.name}</span>
52 </p>
53 <span class="tooltip-value">${firstElement.value?.toLocaleString()}</span>
54 </div>`;
55 };
56
57 const { chartOption } = useChartOption(() => {
58 return {
59 grid: { left: 0, right: 0, top: 10, bottom: 5 },
60 xAxis: { type: 'category', show: false, boundaryGap: true, data: record.value.time },
61 yAxis: { show: false, type: 'value', scale: true },
62 tooltip: {
63 show: true,
64 trigger: 'axis',
65 formatter: (params) => formatTooltip(params as ToolTipFormatterParams[]),
66 className: 'echarts-tooltip-diy',
67 },
68 series: { data: record.value.data, type: 'line', showSymbol: false, smooth: true, lineStyle: { width: 3, color: '#165DFF' } },
69 };
70 });
71
72 onMounted(async () => {
73 record.value = await useDashboardApi.activityTotal();
74 });
75 </script>
76
77 <style lang="less" scoped>
78 :deep(.arco-card) {
79 border-radius: 4px;
80 }
81
82 :deep(.arco-card-body) {
83 width: 100%;
84 height: 134px;
85 //padding: 0;
86 }
87
88 .content-wrap {
89 width: 100%;
90 //padding: 16px;
91 white-space: nowrap;
92 }
93
94 :deep(.content) {
95 float: left;
96 width: 108px;
97 height: 102px;
98 }
99
100 :deep(.arco-statistic) {
101 .arco-statistic-title {
102 font-size: 14px;
103 font-weight: bold;
104 white-space: nowrap;
105 }
106
107 .arco-statistic-content {
108 margin-top: 10px;
109 }
110 }
111
112 .chart {
113 float: right;
114 width: calc(100% - 108px);
115 height: 100px;
116 vertical-align: bottom;
117 }
118
119 .label {
120 padding-right: 8px;
121 font-size: 12px;
122 }
123 </style>
1 <template>
2 <a-card :body-style="{ padding: '10px 20px 0 20px' }" :header-style="{ paddingBottom: 0 }" class="general-card" title="试听概览">
3 <template #extra>
4 <a-space size="12">
5 <export-button :on-download="onDownload" />
6 <a-range-picker
7 v-model="createBetween"
8 :allow-clear="false"
9 :disabled-date="disabledDate"
10 style="width: 260px"
11 value-format="YYYY-MM-DD"
12 @change="onFetch"
13 @select="onSelect"
14 />
15 </a-space>
16 </template>
17 <Chart :option="chartOption" height="328px" />
18 </a-card>
19 </template>
20
21 <script lang="ts" setup>
22 import Chart from '@/components/chart/index.vue';
23 import useChartOption from '@/hooks/chart-option';
24 import { ToolTipFormatterParams } from '@/types/echarts';
25 import { onMounted, ref } from 'vue';
26 import dayjs from 'dayjs';
27 import { sum } from 'lodash';
28 import useDashboardApi from '@/http/dashboard';
29
30 const createBetween = ref([dayjs().subtract(7, 'day').format('YYYY-MM-DD'), dayjs().format('YYYY-MM-DD')]);
31
32 const disabledDate = (current?: Date) => {
33 const date = createBetween.value;
34
35 if (date && date.length) {
36 return (
37 dayjs(current).isBefore(dayjs(date[0]).subtract(15, 'day')) ||
38 dayjs(current).isAfter(dayjs(date[0]).add(15, 'day')) ||
39 dayjs(current).isAfter(dayjs())
40 );
41 }
42 return dayjs(current).isAfter(dayjs());
43 };
44
45 const onSelect = (value: any[]) => {
46 createBetween.value = value;
47 };
48
49 const tooltipItemsHtmlString = (items: ToolTipFormatterParams[]) => {
50 return items
51 .reverse()
52 .map(
53 (el) => `<div class="content-panel">
54 <p>
55 <span style="background-color: ${el.color}" class="tooltip-item-icon"></span><span>${el.seriesName}</span>
56 </p>
57 <span class="tooltip-value">${el.value?.toLocaleString()}</span>
58 </div>`
59 )
60 .reverse()
61 .join('');
62 };
63
64 const record = ref<{
65 time: string[];
66 listen_user_count: number[];
67 listen_count: number[];
68 like_count: number[];
69 submit_count: number[];
70 }>({
71 time: [],
72 listen_user_count: [],
73 listen_count: [],
74 like_count: [],
75 submit_count: [],
76 });
77
78 const { chartOption } = useChartOption((isDark) => {
79 return {
80 grid: {
81 left: '40',
82 right: '40',
83 top: '40',
84 bottom: '40',
85 },
86 legend: {
87 top: 0,
88 right: 0,
89 bottom: 0,
90 icon: 'rect',
91 textStyle: {
92 color: '#4E5969',
93 },
94 formatter: (name) => {
95 switch (name) {
96 case '试听用户':
97 return `${name}(${sum(record.value.listen_user_count)})`;
98 case '试听次数':
99 return `${name}(${sum(record.value.listen_count)})`;
100 case '收藏次数':
101 return `${name}(${sum(record.value.like_count)})`;
102 case '提交作品':
103 return `${name}(${sum(record.value.submit_count)})`;
104 default:
105 return name;
106 }
107 },
108 },
109 tooltip: {
110 trigger: 'axis',
111 formatter(params) {
112 const [firstElement] = params as ToolTipFormatterParams[];
113 return `<div>
114 <p class="tooltip-title">${firstElement.axisValueLabel}</p>
115 ${tooltipItemsHtmlString(params as ToolTipFormatterParams[])}
116 </div>`;
117 },
118 className: 'echarts-tooltip-diy',
119 },
120 xAxis: [
121 {
122 type: 'category',
123 boundaryGap: true,
124 axisLine: {
125 lineStyle: { color: isDark ? '#3f3f3f' : '#A9AEB8' },
126 },
127 axisTick: {
128 show: true,
129 alignWithLabel: true,
130 lineStyle: { color: '#86909C' },
131 },
132 axisLabel: { color: '#86909C' },
133 data: record.value.time,
134 },
135 ],
136 yAxis: {
137 show: true,
138 type: 'value',
139 axisLabel: { color: '#86909C' },
140 splitLine: {
141 lineStyle: { color: isDark ? '#3F3F3F' : '#E5E6EB' },
142 },
143 },
144 series: [
145 {
146 name: '试听用户',
147 type: 'bar',
148 data: record.value.listen_user_count,
149 color: '#01629f',
150 seriesLayoutBy: 'row',
151 barWidth: 14,
152 },
153 {
154 name: '试听次数',
155 type: 'bar',
156 data: record.value.listen_count,
157 color: '#246EFF',
158 seriesLayoutBy: 'row',
159 barWidth: 14,
160 },
161 {
162 name: '收藏次数',
163 type: 'bar',
164 data: record.value.like_count,
165 color: '#00B2FF',
166 seriesLayoutBy: 'row',
167 barWidth: 14,
168 },
169 {
170 name: '提交作品',
171 type: 'bar',
172 data: record.value.submit_count,
173 color: '#81c0ff',
174 seriesLayoutBy: 'row',
175 barWidth: 14,
176 },
177 ],
178 };
179 });
180
181 const onFetch = async () => {
182 record.value = await useDashboardApi.overview({ createBetween: createBetween.value });
183 };
184
185 const onDownload = async () => useDashboardApi.getOverviewExport('活动数据', { createBetween: createBetween.value });
186
187 onMounted(() => onFetch());
188 </script>
189
190 <style lang="less" scoped>
191 .title {
192 line-height: normal;
193 font-weight: 500;
194 font-size: 16px;
195 color: var(--color-text-1);
196 overflow: hidden;
197 white-space: nowrap;
198 text-overflow: ellipsis;
199 }
200 </style>
1 <template>
2 <a-card class="general-card" title="用户试唱" :header-style="{ paddingBottom: 0 }" :body-style="{ padding: '15px 20px 13px 20px' }">
3 <template #extra>
4 <a-space>
5 <a-range-picker
6 v-model="createBetween"
7 value-format="YYYY-MM-DD"
8 style="width: 260px"
9 :allow-clear="false"
10 :disabled-date="disabledDate"
11 @change="onSearch"
12 />
13 <export-button :on-download="onExport" />
14 </a-space>
15 </template>
16
17 <filter-table ref="tableRef" :loading="loading" :on-query="onQuery" size="mini">
18 <link-table-column
19 title="名称"
20 data-index="activity_name"
21 :link-style="{ fontSize: '12px' }"
22 :width="160"
23 :formatter="(item:TableData) => item.activity_name + ([3,5].includes(item.activity_status)? '( 结束 )' : '')"
24 :to="(item:TableData) => $router.push({ name:'audition-activity-show', params: { id: item.activity_id} })"
25 />
26 <!-- <enum-table-column title="类型" data-index="activity_type" :width="60" :option="useActivityApi.songTypeOption" />-->
27 <link-table-column
28 title="厂牌"
29 data-index="project_id"
30 :link-style="{ fontSize: '12px' }"
31 :width="120"
32 :formatter="(item:TableData) => item.project?.name || ''"
33 :to="(item: TableData) => $router.push({ name: 'audition-project-show', params: { id: item.project_id } })"
34 />
35 <user-table-column
36 title="试唱用户"
37 data-index="user_id"
38 nick-index="user_nick_name"
39 :link-style="{ fontSize: '12px' }"
40 :width="120"
41 show-href
42 />
43 <filter-table-column title="身份" data-index="user_identify" :width="70">
44 <template #default="{ record }">
45 <span v-if="record.user_identity === 1">音乐人</span>
46 <span v-else-if="[2, 3].includes(record.user_identity)">经纪人</span>
47 <span v-else>未认证</span>
48 </template>
49 </filter-table-column>
50 <user-table-column
51 title="经纪人"
52 data-index="business_id"
53 user="business"
54 show-href
55 :width="110"
56 dark-value=""
57 :link-style="{ fontSize: '12px' }"
58 />
59 <filter-table-column title="试唱方式" data-index="mode" :width="80">
60 <template #default="{ record }">
61 <template v-if="record.mode === 1">自主上传</template>
62 <template v-else>{{ record.sing_type === 'Full' ? '唱整首' : '唱片段' }}</template>
63 </template>
64 </filter-table-column>
65 <link-table-column
66 v-if="usePermission().checkPermission('audition-activity-show-submit-user-view')"
67 title="合作模式"
68 data-index="price_id"
69 :link-style="{ fontSize: '12px' }"
70 :width="80"
71 :formatter="(item:TableData) => (item.price ? '查看报价' : '')"
72 :to="(item:TableData) =>onViewPrice(item.price) "
73 />
74 <filter-table-column title="提交时间" data-index="submit_at" :width="100">
75 <template #default="{ record }">
76 {{ dayjs(record.submit_at).format('MM/DD HH:mm') || '' }}
77 </template>
78 </filter-table-column>
79 <filter-table-column title="试唱音频" data-index="demo_url" :width="240">
80 <template #default="{ record }">
81 <audio-player :url="record.demo_url" :name="record.user_nick_name" />
82 </template>
83 </filter-table-column>
84 <a-table-column title="操作" data-index="operation" :width="120">
85 <template #cell="{ record }">
86 <a-button v-if="record.status === 1" size="mini" type="primary" status="success">已确认合作</a-button>
87 <a-button v-else-if="record.status === 2" size="mini">试唱不合适</a-button>
88 <a-button v-else-if="record.status === 0 && [3, 5].indexOf(record.activity_status) !== -1" size="mini">未采纳</a-button>
89 <template v-else-if="record.status === 0 && record.activity_status === 1">
90 <a-space v-if="usePermission().checkPermission('audition-activity-show-submit-user-audit')">
91 <a-button type="primary" size="mini" @click="onPass(record)">合作</a-button>
92 <a-button type="primary" status="danger" size="mini" @click="onNotPass(record)">不合适</a-button>
93 </a-space>
94 <a-button v-else size="mini">试唱待确认</a-button>
95 </template>
96 <a-button v-else size="mini">其他</a-button>
97 </template>
98 </a-table-column>
99 </filter-table>
100 </a-card>
101 </template>
102
103 <script lang="ts" setup>
104 import { computed, createVNode, onMounted, ref } from 'vue';
105 import dayjs from 'dayjs';
106 import useLoading from '@/hooks/loading';
107 import { Modal, TableData } from '@arco-design/web-vue';
108 import { createFormItemVNode, createFormVNode, createModalVNode } from '@/utils/createVNode';
109 import useDashboardApi from '@/http/dashboard';
110 import { AnyObject } from '@/types/global';
111 import FilterTable from '@/components/filter/table.vue';
112 import UserTableColumn from '@/components/filter/user-table-column.vue';
113 import LinkTableColumn from '@/components/filter/link-table-column.vue';
114 import { promiseToBoolean } from '@/utils';
115 import { useWorkApi } from '@/http/activity';
116 import usePermission from '@/hooks/permission';
117
118 const { loading, setLoading } = useLoading(false);
119 const { submitWork, getSubmitWorkExport } = useDashboardApi;
120
121 const createBetween = ref([dayjs().subtract(7, 'day').format('YYYY-MM-DD'), dayjs().format('YYYY-MM-DD')]);
122
123 const timeFilter = computed(() => {
124 return [`${createBetween.value[0]} 00:00:00`, `${createBetween.value[1]} 23:59:59`];
125 });
126
127 const tableRef = ref<typeof FilterTable>();
128
129 const onQuery = async (params: AnyObject) => {
130 setLoading(true);
131 return submitWork({ submitBetween: timeFilter.value, ...params, sortBy: 'submit_at', sortType: 'desc' }).finally(() =>
132 setLoading(false)
133 );
134 };
135
136 const onExport = () => getSubmitWorkExport('用户试唱', { submitBetween: timeFilter.value, sortBy: 'submit_at', sortType: 'desc' });
137
138 const onPass = (row: TableData) => {
139 const itemStyle = { lineHeight: '28px', margin: '4px 0' };
140
141 createModalVNode(
142 () =>
143 createVNode('ol', {}, [
144 createVNode('li', { style: itemStyle }, '此条Demo的状态将变更为合作状态'),
145 createVNode('li', { style: itemStyle }, `活动《${row.activity_name}》的状态变更为完成状态!`),
146 ]),
147 {
148 bodyStyle: { padding: 0 },
149 title: '确认合作',
150 onBeforeOk: () => promiseToBoolean(useWorkApi.changeStatus(row.id, { status: 1 })),
151 onOk: () => tableRef.value?.onFetch(),
152 }
153 );
154 };
155
156 const onNotPass = (row: TableData) => {
157 Modal.open({
158 title: '不合适标记',
159 content: `请确认是否将用户:${row.user_nick_name} 提交的试唱标记为不合适,并反馈给用户?`,
160 closable: false,
161 onBeforeOk: () => promiseToBoolean(useWorkApi.changeStatus(row.id, { status: 2, remark: '' })),
162 onOk: () => tableRef.value?.onFetch(),
163 });
164 };
165
166 const onViewPrice = (price: TableData) => {
167 createModalVNode(
168 () =>
169 createFormVNode({ size: 'small', model: price }, [
170 createFormItemVNode(
171 { label: '唱酬', showColon: true, rowClass: 'mb-0' },
172 price.value.is_reward ? `${price.value.amounts} 元` : '无'
173 ),
174 createFormItemVNode(
175 { label: '分成', showColon: true, rowClass: 'mb-0' },
176 price.value.is_dividend ? `${price.value.ratio}% | ${price.value.year} | ${price.is_deduct ? '抵扣' : '不抵扣'}` : '无'
177 ),
178 createFormItemVNode({ label: '价格是否可谈', showColon: true, rowClass: 'mb-0' }, price.is_talk ? '【可谈】' : '【不可谈】'),
179 createFormItemVNode(
180 { label: '录音地点', showColon: true, rowClass: 'mb-0' },
181 `${[price.address?.parent?.name, price.address?.name].join('/')}${
182 price.is_accept_address ? '【接受其他录音地点】' : '【不接受其他录音地点】'
183 }`
184 ),
185 ]),
186 {
187 title: '用户报价',
188 hideCancel: true,
189 okText: '我知道了',
190 bodyStyle: { padding: '8px 20px' },
191 width: '600px',
192 }
193 );
194 };
195
196 const disabledDate = (current?: Date) => dayjs(current).isAfter(dayjs()) || dayjs(current).isBefore(dayjs().subtract(30, 'day'));
197
198 const onSearch = () => tableRef.value?.onPageChange();
199
200 onMounted(async () => onSearch());
201 </script>
202
203 <style lang="less" scoped></style>
1 s
2 <template>
3 <a-card>
4 <div class="content-wrap">
5 <div class="content">
6 <a-statistic :value="todayTotal" :value-from="0" animation show-group-separator title="用户提交认证" />
7 <div class="desc">
8 <a-typography-text class="label" type="secondary"> 较昨日</a-typography-text>
9 <a-typography-text v-if="diffTotal < 0" type="success">
10 {{ diffTotal }}
11 <icon-arrow-fall />
12 </a-typography-text>
13 <a-typography-text v-else type="danger">
14 {{ diffTotal }}
15 <icon-arrow-rise />
16 </a-typography-text>
17 </div>
18 </div>
19 <div class="chart">
20 <Chart :option="registerChart" />
21 </div>
22 </div>
23 </a-card>
24 </template>
25
26 <script lang="ts" setup>
27 import useChartOption from '@/hooks/chart-option';
28 import { IconArrowFall, IconArrowRise } from '@arco-design/web-vue/es/icon';
29 import { computed, onMounted, ref } from 'vue';
30 import { subtract } from 'lodash';
31 import useDashboardApi from '@/http/dashboard';
32 import { ToolTipFormatterParams } from '@/types/echarts';
33
34 const record = ref<{ time: string[]; data: number[] }>({ time: [], data: [] });
35
36 const todayTotal = computed(() => {
37 return record.value.data[record.value.data.length - 1] || 0;
38 });
39 const diffTotal = computed(() => {
40 return subtract(record.value.data[record.value.data.length - 1], record.value.data[record.value.data.length - 2]) || 0;
41 });
42
43 const formatTooltip = (params: ToolTipFormatterParams[]) => {
44 const [firstElement] = params;
45 return `
46 <div class="content-panel">
47 <p>
48 <span style="background-color: ${firstElement.color}" class="tooltip-item-icon"></span>
49 <span>${firstElement.name}</span>
50 </p>
51 <span class="tooltip-value">${firstElement.value?.toLocaleString()}</span>
52 </div>`;
53 };
54
55 const { chartOption: registerChart } = useChartOption(() => {
56 return {
57 grid: { left: 0, right: 0, top: 10, bottom: 5 },
58 xAxis: { type: 'category', show: false, boundaryGap: true, data: record.value.time },
59 yAxis: { show: false, type: 'value', scale: true },
60 tooltip: {
61 show: true,
62 trigger: 'axis',
63 formatter: (params) => formatTooltip(params as ToolTipFormatterParams[]),
64 className: 'echarts-tooltip-diy',
65 },
66 series: { data: record.value.data, type: 'bar', barWidth: 8, itemStyle: { borderRadius: 2, color: '#165DFF' } },
67 };
68 });
69
70 onMounted(async () => {
71 record.value = await useDashboardApi.userCertify();
72 });
73 </script>
74
75 <style lang="less" scoped>
76 :deep(.arco-card) {
77 border-radius: 4px;
78 }
79
80 :deep(.arco-card-body) {
81 width: 100%;
82 height: 134px;
83 //padding: 0;
84 }
85
86 .content-wrap {
87 width: 100%;
88 //padding: 16px;
89 white-space: nowrap;
90 }
91
92 :deep(.content) {
93 float: left;
94 width: 108px;
95 height: 102px;
96 }
97
98 :deep(.arco-statistic) {
99 .arco-statistic-title {
100 font-size: 14px;
101 font-weight: bold;
102 white-space: nowrap;
103 }
104
105 .arco-statistic-content {
106 margin-top: 10px;
107 }
108 }
109
110 .chart {
111 float: right;
112 width: calc(100% - 108px);
113 //height: 90px;
114 vertical-align: bottom;
115 }
116
117 .label {
118 padding-right: 8px;
119 font-size: 12px;
120 }
121 </style>
1 <template>
2 <a-card>
3 <div class="content-wrap">
4 <div class="content">
5 <div data-v-55251d00="" class="arco-statistic">
6 <div class="arco-statistic-title">音乐人技能分布</div>
7 <div class="arco-statistic-content">
8 <div class="arco-statistic-value">
9 <span class="arco-statistic-value-integer"></span>
10 </div>
11 </div>
12 </div>
13 </div>
14 <div class="chart">
15 <Chart :option="chartOption" style="width: 100%" />
16 </div>
17 </div>
18 </a-card>
19 </template>
20
21 <script lang="ts" setup>
22 import useChartOption from '@/hooks/chart-option';
23 import { onMounted, ref } from 'vue';
24 import Chart from '@/components/chart/index.vue';
25
26 import useDashboardApi from '@/http/dashboard';
27 import { ToolTipFormatterParams } from '@/types/echarts';
28
29 type recordType = { value: number; name: string };
30
31 const record = ref<recordType[]>([]);
32
33 const { chartOption } = useChartOption(() => {
34 return {
35 grid: { left: 0, right: 0, top: 0, bottom: 0 },
36 label: { show: false },
37 tooltip: {
38 show: true,
39 trigger: 'item',
40 formatter(params) {
41 const item = params as ToolTipFormatterParams;
42
43 return `
44 <div class="content-panel">
45 <p>
46 <span style="background-color: ${item.color}" class="tooltip-item-icon"></span>
47 <span>${item.name}</span>
48 </p>
49 <span class="tooltip-value">${item.value?.toLocaleString()}</span>
50 <span class="tooltip-value">${item.percent?.toLocaleString()}%</span>
51 </div>`;
52 },
53 className: 'echarts-tooltip-diy',
54 },
55 series: [{ type: 'pie', radius: ['65%', '90%'], label: { show: false }, data: record.value }],
56 };
57 });
58
59 onMounted(async () => {
60 record.value = await useDashboardApi.userSkill();
61 });
62 </script>
63
64 <style lang="less" scoped>
65 .echarts-tooltip-diy {
66 background: none !important;
67 border: none !important;
68 }
69
70 :deep(.arco-card) {
71 border-radius: 4px;
72 }
73
74 :deep(.arco-card-body) {
75 width: 100%;
76 height: 134px;
77 }
78
79 .content-wrap {
80 width: 100%;
81 height: 100%;
82 display: flex;
83 white-space: nowrap;
84
85 .content {
86 width: 108px;
87
88 :deep(.arco-statistic) {
89 .arco-statistic-title {
90 font-size: 14px;
91 font-weight: bold;
92 white-space: nowrap;
93 }
94
95 .arco-statistic-content {
96 margin-top: 10px;
97 }
98 }
99 }
100
101 .chart {
102 width: calc(100% - 108px);
103 vertical-align: bottom;
104 }
105 }
106 </style>
1 <template>
2 <a-card>
3 <div class="content-wrap">
4 <div class="content">
5 <div data-v-55251d00="" class="arco-statistic">
6 <div class="arco-statistic-title">歌手擅长曲风</div>
7 <div class="arco-statistic-content">
8 <div class="arco-statistic-value">
9 <span class="arco-statistic-value-integer"></span>
10 </div>
11 </div>
12 </div>
13 </div>
14 <div class="chart">
15 <Chart :option="chartOption" style="width: 100%" />
16 </div>
17 </div>
18 </a-card>
19 </template>
20
21 <script lang="ts" setup>
22 import useChartOption from '@/hooks/chart-option';
23 import { onMounted, ref } from 'vue';
24 import Chart from '@/components/chart/index.vue';
25
26 import useDashboardApi from '@/http/dashboard';
27 import { ToolTipFormatterParams } from '@/types/echarts';
28
29 type recordType = { value: number; name: string };
30
31 const record = ref<recordType[]>([]);
32
33 const { chartOption } = useChartOption(() => {
34 return {
35 grid: { left: 0, right: 0, top: 0, bottom: 0 },
36 label: { show: false },
37 tooltip: {
38 show: true,
39 trigger: 'item',
40 formatter(params) {
41 const item = params as ToolTipFormatterParams;
42 return `
43 <div class="content-panel">
44 <p>
45 <span style="background-color: ${item.color}" class="tooltip-item-icon"></span>
46 <span style="margin-right: 10px">${item.name}</span>
47 </p>
48 <span class="tooltip-value">${item.value?.toLocaleString()}</span>
49 <span class="tooltip-value">${item.percent?.toLocaleString()}%</span>
50 </div>`;
51 },
52 className: 'echarts-tooltip-diy',
53 },
54 series: [{ type: 'pie', radius: ['65%', '90%'], label: { show: false }, data: record.value }],
55 };
56 });
57
58 onMounted(async () => {
59 record.value = await useDashboardApi.userStyle();
60 });
61 </script>
62
63 <style lang="less" scoped>
64 .echarts-tooltip-diy {
65 background: none !important;
66 border: none !important;
67 }
68
69 :deep(.arco-card) {
70 border-radius: 4px;
71 }
72
73 :deep(.arco-card-body) {
74 width: 100%;
75 height: 134px;
76 }
77
78 .content-wrap {
79 width: 100%;
80 height: 100%;
81 display: flex;
82 white-space: nowrap;
83
84 .content {
85 width: 108px;
86
87 :deep(.arco-statistic) {
88 .arco-statistic-title {
89 font-size: 14px;
90 font-weight: bold;
91 white-space: nowrap;
92 }
93
94 .arco-statistic-content {
95 margin-top: 10px;
96 }
97 }
98 }
99
100 .chart {
101 width: calc(100% - 108px);
102 vertical-align: bottom;
103 }
104 }
105 </style>
1 <template>
2 <a-card>
3 <div class="content-wrap">
4 <div class="content">
5 <div data-v-55251d00="" class="arco-statistic">
6 <div class="arco-statistic-title">用户概览</div>
7 <div class="arco-statistic-content">
8 <div class="arco-statistic-value">
9 <span class="arco-statistic-value-integer"></span>
10 </div>
11 </div>
12 </div>
13 </div>
14 <div class="chart">
15 <Chart :option="registerChart" style="width: 100%" />
16 </div>
17 </div>
18 </a-card>
19 </template>
20
21 <script lang="ts" setup>
22 import useChartOption from '@/hooks/chart-option';
23 import { onMounted, ref } from 'vue';
24 import useDashboardApi from '@/http/dashboard';
25 import { ToolTipFormatterParams } from '@/types/echarts';
26
27 const record = ref<{ type: string[]; data: number[] }>({ type: [], data: [] });
28
29 const formatTooltip = (params: ToolTipFormatterParams[]) => {
30 const [firstElement] = params;
31 return `<div class="content-panel">
32 <p>
33 <span style="background-color: ${firstElement.color}" class="tooltip-item-icon"></span>
34 <span>${firstElement.name}</span>
35 </p>
36 <span class="tooltip-value">${firstElement.value?.toLocaleString()}</span>
37 </div>`;
38 };
39
40 const { chartOption: registerChart } = useChartOption(() => {
41 return {
42 grid: { left: 0, right: 0, top: 10, bottom: 5 },
43 xAxis: { type: 'category', show: false, boundaryGap: true, data: record.value.type },
44 yAxis: { show: false, type: 'value', scale: true },
45 tooltip: {
46 show: true,
47 trigger: 'axis',
48 formatter: (params) => formatTooltip(params as ToolTipFormatterParams[]),
49 className: 'echarts-tooltip-diy',
50 },
51 series: { data: record.value.data, type: 'bar', barWidth: 16, itemStyle: { borderRadius: 2, color: '#165DFF' } },
52 };
53 });
54
55 onMounted(async () => {
56 record.value = await useDashboardApi.userTotal();
57 });
58 </script>
59
60 <style lang="less" scoped>
61 :deep(.arco-card) {
62 border-radius: 4px;
63 }
64
65 :deep(.arco-card-body) {
66 width: 100%;
67 height: 134px;
68 }
69
70 .content-wrap {
71 width: 100%;
72 height: 100%;
73 display: flex;
74 white-space: nowrap;
75
76 .content {
77 width: 108px;
78
79 :deep(.arco-statistic) {
80 .arco-statistic-title {
81 font-size: 14px;
82 font-weight: bold;
83 white-space: nowrap;
84 }
85
86 .arco-statistic-content {
87 margin-top: 10px;
88 }
89 }
90 }
91
92 .chart {
93 width: calc(100% - 108px);
94 vertical-align: bottom;
95 }
96 }
97 </style>
1 <template>
2 <page-view has-bread>
3 <a-grid class="bot" :col-gap="12" :cols="24" :row-gap="12" style="margin-bottom: 16px">
4 <a-grid-item :span="{ xs: 12, sm: 12, md: 12, lg: 12, xl: 6, xxl: 6 }">
5 <user-certify-card />
6 </a-grid-item>
7 <a-grid-item :span="{ xs: 12, sm: 12, md: 12, lg: 12, xl: 6, xxl: 6 }">
8 <activity-total-card />
9 </a-grid-item>
10 <a-grid-item :span="{ xs: 12, sm: 12, md: 12, lg: 12, xl: 6, xxl: 6 }">
11 <activity-listen-card />
12 </a-grid-item>
13 <a-grid-item :span="{ xs: 12, sm: 12, md: 12, lg: 12, xl: 6, xxl: 6 }">
14 <activity-like-card />
15 </a-grid-item>
16 <a-grid-item :span="{ xs: 12, sm: 12, md: 12, lg: 12, xl: 6, xxl: 6 }">
17 <activity-style-card />
18 </a-grid-item>
19 <a-grid-item :span="{ xs: 12, sm: 12, md: 12, lg: 12, xl: 6, xxl: 6 }">
20 <user-style-card />
21 </a-grid-item>
22 <a-grid-item :span="{ xs: 12, sm: 12, md: 12, lg: 12, xl: 6, xxl: 6 }">
23 <user-skill-card />
24 </a-grid-item>
25 <a-grid-item :span="{ xs: 12, sm: 12, md: 12, lg: 12, xl: 6, xxl: 6 }">
26 <user-total-card />
27 </a-grid-item>
28 </a-grid>
29
30 <overview class="bot" />
31
32 <submit-work-panel class="bot" />
33 </page-view>
34 </template>
35
36 <script lang="ts" setup>
37 import Overview from '@/views/dashboard/components/overview.vue';
38 import SubmitWorkPanel from '@/views/dashboard/components/submit-work-panel.vue';
39 import ActivityLikeCard from '@/views/dashboard/components/activity-like-card.vue';
40 import UserCertifyCard from '@/views/dashboard/components/user-certify-card.vue';
41 import UserTotalCard from '@/views/dashboard/components/user-total-card.vue';
42 import UserSkillCard from '@/views/dashboard/components/user-skill-card.vue';
43 import UserStyleCard from '@/views/dashboard/components/user-style-card.vue';
44 import ActivityStyleCard from '@/views/dashboard/components/activity-style-card.vue';
45 import ActivityListenCard from '@/views/dashboard/components/activity-listen-card.vue';
46 import ActivityTotalCard from '@/views/dashboard/components/activity-total-card.vue';
47 </script>
48
49 <style lang="less" scoped>
50 .container {
51 padding: 0 30px 20px 20px;
52 }
53
54 .bot {
55 margin-bottom: 16px;
56 }
57 </style>
1 <template>
2 <div class="container">
3 <Breadcrumb :items="['exception', 'exception-403']" />
4 <div class="content">
5 <a-result class="result" status="403" subtitle="对不起,您没有访问该资源的权限" />
6 <a-button key="back" type="primary" @click="goBack"> 返回</a-button>
7 </div>
8 </div>
9 </template>
10
11 <script lang="ts" setup>
12 import { useRouter } from 'vue-router';
13
14 const router = useRouter();
15
16 const goBack = () => router.back();
17 </script>
1 <template>
2 <div class="container">
3 <Breadcrumb :items="['exception', 'exception-404']" />
4 <div class="content">
5 <a-result class="result" status="404" subtitle="抱歉,页面不见了~"></a-result>
6 <div class="operation-row">
7 <!-- <a-button key="again" style="margin-right: 16px"> 重试</a-button>-->
8 <a-button key="back" type="primary" @click="goBack"> 返回</a-button>
9 </div>
10 </div>
11 </div>
12 </template>
13
14 <script lang="ts" setup>
15 import { useRouter } from 'vue-router';
16
17 const router = useRouter();
18
19 const goBack = () => router.back();
20 </script>
1 <template>
2 <div class="container">
3 <Breadcrumb :items="['exception', 'exception-500']" />
4 <div class="content">
5 <a-result class="result" status="500" subtitle="抱歉,服务器出了点问题~" />
6 <a-button key="back" type="primary" @click="goBack">返回</a-button>
7 </div>
8 </div>
9 </template>
10
11 <script lang="ts" setup>
12 import { useRouter } from 'vue-router';
13
14 const router = useRouter();
15
16 const goBack = () => router.back();
17 </script>
18
19 <style lang="less" scoped></style>
1 <template>
2 <router-view />
3 </template>
4
5 <script lang="ts">
6 import { defineComponent } from 'vue';
7
8 export default defineComponent({});
9 </script>
10
11 <style lang="less" scoped>
12 .container {
13 // position: relative;
14 // display: flex;
15 // flex-direction: column;
16 // align-items: center;
17 // justify-content: center;
18 // height: 100%;
19 // text-align: center;
20 // background-color: var(--color-bg-1);
21 padding: 0 30px 20px 20px;
22 height: calc(100% - 20px);
23
24 :deep(.content) {
25 position: relative;
26 display: flex;
27 flex-direction: column;
28 align-items: center;
29 justify-content: center;
30 height: 100%;
31 text-align: center;
32 background-color: var(--color-bg-1);
33 border-radius: 4px;
34 }
35 }
36 </style>
1 <template>
2 <a-form ref="fromRef" :model="userInfo" :rules="rules" class="login-form" layout="vertical">
3 <a-form-item field="email" hide-label>
4 <a-input v-model="userInfo.email" placeholder="请输入登陆邮箱">
5 <template #prefix>
6 <icon-user />
7 </template>
8 </a-input>
9 </a-form-item>
10 <a-form-item field="password" hide-label>
11 <a-input-password v-model="userInfo.password" allow-clear placeholder="请输入登陆密码">
12 <template #prefix>
13 <icon-lock />
14 </template>
15 </a-input-password>
16 </a-form-item>
17 </a-form>
18 </template>
19
20 <script lang="ts" setup>
21 import { IconLock, IconUser } from '@arco-design/web-vue/es/icon';
22 import { ref } from 'vue';
23 import { FormInstance } from '@arco-design/web-vue/es/form';
24 import { AttributeData } from '@/types/global';
25
26 const userInfo = ref({
27 email: import.meta.env.VITE_SHOW_LOGIN_ACCOUNT ? '1774507011@qq.com' : '',
28 password: import.meta.env.VITE_SHOW_LOGIN_ACCOUNT ? '112358ys' : '',
29 });
30
31 const rules = {
32 email: [{ required: true, message: '登陆邮箱不能为空' }],
33 password: [{ required: true, message: '密码不能为空' }],
34 };
35
36 const fromRef = ref<FormInstance>();
37 const onSubmit = (callback: (value: AttributeData) => void) => {
38 fromRef.value?.validate((errors) => !errors && callback(userInfo.value));
39 };
40
41 defineExpose({ onSubmit });
42 </script>
43
44 <style lang="less" scoped>
45 .login-form {
46 &-error-msg {
47 height: 32px;
48 color: rgb(var(--red-6));
49 line-height: 32px;
50 }
51
52 &-register-btn {
53 color: var(--color-text-3) !important;
54 }
55 }
56 </style>
1 <template>
2 <a-form v-model="formValue" class="login-form" size="small" auto-label-width>
3 <a-form-item show-colon label="找回方式" style="margin-bottom: 10px">
4 <a-radio-group v-model="apply" :options="applyType" />
5 </a-form-item>
6 <a-form-item v-if="apply === 1" show-colon label="手机号">
7 <a-input v-model="formValue.phone" max-length="14" placeholder="请输入" />
8 </a-form-item>
9 <a-form-item v-else show-colon label="邮箱">
10 <a-input v-model="email" placeholder="请输入" />
11 </a-form-item>
12 <a-form-item show-colon label="验证码">
13 <a-row :gutter="8">
14 <a-col :span="18">
15 <a-input v-model="formValue.code" max-length="6" placeholder="请输入" />
16 </a-col>
17 <a-col :span="6">
18 <a-button html-type="submit" type="primary">获取</a-button>
19 </a-col>
20 </a-row>
21 </a-form-item>
22 <div class="form-actions">
23 <a-link @click="type = 'login'">返回</a-link>
24 <a-button html-type="submit" type="primary">提交</a-button>
25 </div>
26 </a-form>
27 </template>
28
29 <script lang="ts" setup>
30 import { inject, ref } from 'vue';
31
32 const type = inject('type');
33
34 const apply = ref(1);
35 const applyType = ref([
36 { value: 1, label: '手机号' },
37 { value: 2, label: '邮箱' },
38 ]);
39
40 const email = ref('');
41
42 const formValue = ref({
43 phone: '',
44 code: '',
45 });
46 </script>
47
48 <style scoped>
49 .form-actions {
50 display: flex;
51 justify-content: space-between;
52 }
53 </style>
1 <template>
2 <a-form ref="fromRef" :model="userInfo" :rules="rules" class="login-form" layout="vertical">
3 <a-form-item field="phone" hide-label>
4 <a-select
5 v-model="userInfo.area"
6 style="flex: 100px; margin-right: 15px"
7 :options="areaOption"
8 :virtual-list-props="{ height: 200 }"
9 />
10 <a-input v-model="userInfo.phone" placeholder="请输入登陆手机号" :max-length="11">
11 <template #prefix>
12 <icon-phone />
13 </template>
14 </a-input>
15 </a-form-item>
16 <a-form-item show-colon hide-label>
17 <a-input v-model="userInfo.code" hide-button :max-length="6" placeholder="验证码" @input="formatCode">
18 <template #prefix>
19 <icon-message />
20 </template>
21 <template #suffix>
22 <a-link type="text" class="login-form-sms-btn" :disabled="countTime !== 0" @click="onSend()">
23 {{ countTime <= 0 ? '获取验证码' : countTime + 's' }}
24 </a-link>
25 </template>
26 </a-input>
27 </a-form-item>
28 </a-form>
29 </template>
30
31 <script lang="ts" setup>
32 import { IconMessage, IconPhone } from '@arco-design/web-vue/es/icon';
33 import { onMounted, ref } from 'vue';
34 import { useIntervalFn } from '@vueuse/core';
35 import { FormInstance } from '@arco-design/web-vue/es/form';
36 import { AttributeData } from '@/types/global';
37 import { Message } from '@arco-design/web-vue';
38 import useProviderApi from '@/http/provider';
39 import { map, union } from 'lodash';
40
41 const fromRef = ref<FormInstance>();
42 // const countTime = useStorage('smsTime', 0);
43 const countTime = ref(0);
44
45 const userInfo = ref({ area: '+86', phone: '', code: '' });
46
47 const areaOption = ref<string[]>([]);
48
49 onMounted(() => {
50 useProviderApi.area().then(({ data }) => (areaOption.value = union(map(data, (item) => item.identifier))));
51 });
52
53 const rules = {
54 phone: [{ required: true, message: '请输入手机号' }],
55 code: [{ required: true, message: '请输入验证码' }],
56 };
57
58 // eslint-disable-next-line no-return-assign
59 const { pause, resume } = useIntervalFn(() => (countTime.value <= 0 ? pause() : (countTime.value -= 1)), 1000);
60
61 const onSend = async () => {
62 if (countTime.value !== 0 || (await fromRef.value?.validateField('phone'))) {
63 return;
64 }
65
66 useProviderApi.sms('login', userInfo.value.phone, userInfo.value.area).then(() => {
67 Message.success('短信发送成功,请注意查收!');
68 countTime.value = 60;
69 resume();
70 });
71 };
72
73 const onSubmit = (callback: (value: AttributeData) => void) => {
74 fromRef.value?.validate((errors) => !errors && callback(userInfo.value));
75 };
76
77 const formatCode = (value: string) => {
78 userInfo.value.code = value.replace(/\D/g, '').slice(0, 6);
79 };
80
81 defineExpose({ onSubmit });
82 </script>
83
84 <style lang="less" scoped>
85 .login-form {
86 &-error-msg {
87 height: 32px;
88 color: rgb(var(--red-6));
89 line-height: 32px;
90 }
91
92 &-sms-btn {
93 padding-left: -12px;
94 font-size: 12px;
95 }
96 }
97 </style>
1 <template>
2 <div class="container">
3 <div class="banner">
4 <div class="banner-inner"></div>
5 </div>
6 <div class="content">
7 <div class="content-inner">
8 <div class="login-form-wrapper">
9 <div class="login-form-title">海星试唱</div>
10 <div class="login-form-sub-title">运营管理后台</div>
11 <a-card :bordered="true" :hoverable="true" :style="{ width: '360px' }">
12 <a-tabs v-model:active-key="loginType" :justify-="true" :animation="true">
13 <a-tab-pane key="email" title="邮箱登录">
14 <EmailForm ref="emailRef" />
15 </a-tab-pane>
16 <a-tab-pane key="phone" title="手机登录">
17 <PhoneForm ref="phoneRef" />
18 </a-tab-pane>
19 </a-tabs>
20 <div class="login-form-actions">
21 <!-- <a-link>忘记密码</a-link>-->
22 <a-button type="primary" :long="true" :loading="loading" @click="onLogin()"> 登录</a-button>
23 </div>
24 </a-card>
25 </div>
26 </div>
27 </div>
28 </div>
29 </template>
30
31 <script lang="ts" setup>
32 import EmailForm from '@/views/login/components/email-form.vue';
33 import PhoneForm from '@/views/login/components/phone-form.vue';
34
35 import { ref } from 'vue';
36 import { useRouter } from 'vue-router';
37 import { AttributeData } from '@/types/global';
38
39 import { setToken } from '@/utils/auth';
40 import { Message } from '@arco-design/web-vue';
41 import useLoading from '@/hooks/loading';
42 import useProviderApi from '@/http/provider';
43
44 const router = useRouter();
45 const { loading, setLoading } = useLoading(false);
46 const loginType = ref<'email' | 'phone'>('email');
47 const emailRef = ref<InstanceType<typeof EmailForm>>();
48 const phoneRef = ref<InstanceType<typeof PhoneForm>>();
49
50 const onSubmit = (value: AttributeData) => {
51 setLoading(true);
52 useProviderApi
53 .login(loginType.value, value)
54 .then(({ data }) => {
55 const { access_token, refresh_token, nick_name } = data;
56 setToken(access_token, refresh_token);
57 Message.success(`欢迎回来,管理员:${nick_name}`);
58 const { path } = router.currentRoute.value.query;
59 router.replace((path as string) || '/dashboard');
60 })
61 .finally(() => setLoading(false));
62 };
63
64 const onLogin = () => {
65 // eslint-disable-next-line default-case
66 switch (loginType.value) {
67 case 'email':
68 emailRef.value?.onSubmit(onSubmit);
69 break;
70 case 'phone':
71 phoneRef.value?.onSubmit(onSubmit);
72 break;
73 }
74 };
75 </script>
76
77 <style lang="less" scoped>
78 .container {
79 display: flex;
80 height: 100vh;
81
82 .banner {
83 width: 550px;
84 }
85
86 .content {
87 position: relative;
88 display: flex;
89 flex: 1;
90 align-items: center;
91 justify-content: center;
92 padding-bottom: 40px;
93
94 .login-form-wrapper {
95 width: 320px;
96 }
97
98 .login-form-title {
99 color: var(--color-text-1);
100 font-weight: 500;
101 font-size: 30px;
102 line-height: 46px;
103 text-align: center;
104 }
105
106 .login-form-sub-title {
107 color: var(--color-text-3);
108 font-size: 16px;
109 line-height: 24px;
110 text-align: center;
111 margin-bottom: 16px;
112 }
113
114 .login-form-actions {
115 display: flex;
116 justify-content: right;
117 }
118 }
119
120 .footer {
121 position: absolute;
122 right: 0;
123 bottom: 0;
124 width: 100%;
125 }
126 }
127
128 .logo {
129 position: fixed;
130 top: 24px;
131 left: 22px;
132 z-index: 1;
133 display: inline-flex;
134 align-items: center;
135
136 &-text {
137 margin-right: 4px;
138 margin-left: 4px;
139 color: var(--color-fill-1);
140 font-size: 20px;
141 }
142 }
143
144 .banner {
145 display: flex;
146 align-items: center;
147 justify-content: center;
148
149 &-inner {
150 flex: 1;
151 height: 100%;
152 //background-image: url('/src/assets/images/bg.jpg');
153 background-image: url('https://spreadcdn.hikoon.com/default/bg.jpg');
154 background-size: cover;
155 }
156 }
157 </style>
1 <script setup lang="ts">
2 import { ref, onMounted } from 'vue';
3 import FilterTable from '@/components/filter/table.vue';
4 import FilterTableColumn from '@/components/filter/table-column.vue';
5
6 import useLoading from '@/hooks/loading';
7 import { AnyObject, Option } from '@/types/global';
8 import { Form, FormItem, Space, Input, Button, TableColumn } from '@arco-design/web-vue';
9 import useActivityApi from '@/http/activity';
10 import { Activity } from '@/types/activity';
11 import { cloneDeep, find, truncate, union } from 'lodash';
12 import dayjs from 'dayjs';
13 import useConfigApi from '@/http/config';
14
15 const { loading, setLoading } = useLoading(false);
16 const filter = ref({ songName: '', projectName: '' });
17 const tableRef = ref();
18
19 defineProps<{ userKey?: number }>();
20
21 const emits = defineEmits<{ (e: 'check', value: object): void; (e: 'checkRow', value: Activity): void }>();
22 const onQuery = async (params: AnyObject) => {
23 setLoading(true);
24 return useActivityApi
25 .get({ ...filter.value, audit_status: 1, status: [1, 3, 5], ...params, sortBy: 'id', sortType: 'desc' })
26 .finally(() => setLoading(false));
27 };
28
29 const onSearch = () => tableRef.value?.onPageChange(1);
30
31 const onReset = () => {
32 filter.value = { songName: '', projectName: '' };
33 onSearch();
34 };
35
36 const configOption = ref<Option[]>([]);
37 // const { activitySpeedOptions, activityLangOptions } = useSelectionStore();
38
39 onMounted(() => {
40 useConfigApi.getOption({ parentKey: ['activity_lang', 'activity_sex'] }).then((data) => {
41 configOption.value = data;
42 });
43
44 onReset();
45 });
46
47 const onSubmit = (row: Activity) => {
48 emits('checkRow', cloneDeep(row));
49
50 const lyricist: string[] = row.expand?.lyricist?.supplement || [];
51 row.links?.forEach((item) => row.expand?.lyricist?.ids?.includes(item.id) && lyricist.push(item.nick_name));
52 const composer: string[] = row.expand?.composer?.supplement || [];
53 row.links?.forEach((item) => row.expand?.composer?.ids?.includes(item.id) && composer.push(item.nick_name));
54 const arranger: string[] = row.expand?.arranger?.supplement || [];
55 row.links?.forEach((item) => row.expand?.arranger?.ids?.includes(item.id) && arranger.push(item.nick_name));
56
57 const tag: string[] = [];
58 row.tags?.forEach((item) => tag.push(item.name));
59 row.lang?.forEach((item) => tag.push(find(configOption.value, { value: item })?.label || ''));
60 tag.push(find(configOption.value, { value: row.sex })?.label || '');
61
62 emits('check', {
63 link_id: row.id,
64 link_name: row.song_name,
65 title: truncate(row.song_name, { length: 12 }),
66 cover: row.cover,
67 desc1: truncate(`出品:${row.project?.name}`, { length: 18 }),
68 desc2: truncate(`作词:${union(lyricist)?.join(' ')} 作曲:${union(composer)?.join(' ')} 编曲:${union(arranger)?.join(' ')}`, {
69 length: 18,
70 }),
71 desc3: truncate(`${tag?.join(' ')}\n预计发行时间:${dayjs(row.publish_at).format('YYYY年MM月DD日')}`, { length: 36 }),
72 });
73 };
74 </script>
75
76 <template>
77 <Form :model="filter" layout="inline" size="mini">
78 <FormItem label="歌曲名称" show-colon>
79 <Input v-model="filter.songName" allow-clear />
80 </FormItem>
81 <FormItem label="厂牌名称" show-colon>
82 <Input v-model="filter.projectName" allow-clear />
83 </FormItem>
84 <FormItem hide-label>
85 <Space>
86 <Button type="primary" @click="onSearch">搜索</Button>
87 <Button @click="onReset">重置</Button>
88 </Space>
89 </FormItem>
90 </Form>
91 <FilterTable
92 ref="tableRef"
93 size="mini"
94 :loading="loading"
95 :on-query="onQuery"
96 style="height: 300px; margin-top: 10px"
97 :scrollbar="true"
98 :scroll="{ y: 300 }"
99 :simple-page="true"
100 >
101 <FilterTableColumn data-index="song_name" title="歌曲名称" />
102 <FilterTableColumn data-index="project.name" title="厂牌名称" :width="200" />
103 <TableColumn title="操作" :width="80">
104 <template #cell="{ record }">
105 <Button size="mini" type="primary" @click="onSubmit(record)">
106 {{ record.id === userKey ? '更新' : '选择' }}
107 </Button>
108 </template>
109 </TableColumn>
110 </FilterTable>
111 </template>
112
113 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { Layout, LayoutContent, LayoutSider, Form, FormItem, Input, InputNumber, Select } from '@arco-design/web-vue';
3 import ImageUpload from '@/components/image-upload/index.vue';
4 import RoleSelect from '@/views/operation/banner/components/role-select.vue';
5 import { Banner } from '@/utils/model';
6 import useBannerApi from '@/http/bannner';
7 import { computed, ref, watch } from 'vue';
8 import { FormInstance } from '@arco-design/web-vue/es/form';
9 import { pick } from 'lodash';
10 import { AttributeData } from '@/types/global';
11 import { promiseToBoolean } from '@/utils';
12
13 type FormType = Pick<Banner, 'name' | 'scope' | 'weight' | 'permission' | 'type' | 'cover' | 'content_picture'>;
14
15 const props = defineProps<{ init?: AttributeData; http: (params?: object) => Promise<any> }>();
16
17 const { scopeOption, typeOption } = useBannerApi;
18
19 const formVal = ref<FormType>({
20 name: '',
21 scope: 1,
22 weight: 0,
23 permission: [],
24 type: 1,
25 cover: '',
26 content_picture: '',
27 ...pick(props.init, ['name', 'scope', 'weight', 'permission', 'type', 'cover', 'content_picture']),
28 });
29
30 const formRule = computed(() => {
31 return {
32 name: [{ required: true, message: '请输入名称' }],
33 scope: [{ required: true, message: '请选择展示位置' }],
34 weight: [{ required: true, message: '请输入权重' }],
35 permission: [{ required: true, message: '请选择浏览用户' }],
36 type: [{ required: true, message: '请选择类型' }],
37 cover: [{ required: true, message: '请选择封面' }],
38 content_picture: formVal.value.type === 1 ? [] : [{ required: true, message: '请输入链接地址' }],
39 };
40 });
41
42 const formRef = ref<FormInstance>();
43
44 watch(
45 () => formVal.value.type,
46 () => (formVal.value.content_picture = '')
47 );
48
49 const onSubmit = async (): Promise<boolean> => {
50 if (await formRef.value?.validate()) {
51 // eslint-disable-next-line prefer-promise-reject-errors
52 return Promise.reject(false);
53 }
54 return promiseToBoolean(props.http(formVal.value));
55 };
56
57 defineExpose({ onSubmit });
58 </script>
59
60 <template>
61 <Layout>
62 <LayoutContent class="content">
63 <Form ref="formRef" :model="formVal" :rules="formRule" auto-label-width>
64 <FormItem label="封面" field="cover" show-colon>
65 <ImageUpload v-model="formVal.cover" :width="500" :height="154" fit="contain" />
66 </FormItem>
67 <FormItem label="名称" field="name" show-colon>
68 <Input v-model="formVal.name" :max-length="30" show-word-limit placeholder="请输入" />
69 </FormItem>
70 <FormItem label="展示位置" field="scope" show-colon>
71 <Select v-model="formVal.scope" :options="scopeOption" placeholder="请选择" />
72 </FormItem>
73 <FormItem label="权重" field="weight" show-colon>
74 <InputNumber v-model="formVal.weight" :min="0" :max="200" placeholder="请输入" />
75 </FormItem>
76 <FormItem label="浏览用户" field="permission" show-colon>
77 <RoleSelect v-model="formVal.permission" :max-tag-count="3" style="height: 32px; width: 500px" />
78 </FormItem>
79 <FormItem label="类型" field="type" show-colon>
80 <Select v-model="formVal.type" :options="typeOption.filter((item) => [1, 2].includes(item.value))" placeholder="请选择" />
81 </FormItem>
82 <FormItem v-show="formVal.type === 2" label="详情" field="content_picture" show-colon row-class="mb-0">
83 <Input v-model="formVal.content_picture" placeholder="请输入" />
84 </FormItem>
85 </Form>
86 </LayoutContent>
87 <LayoutSider v-show="formVal.type === 1" class="side" :width="260">
88 <ImageUpload v-model="formVal.content_picture" :width="260" height="100%" min-height="414px" fit="fill" />
89 </LayoutSider>
90 </Layout>
91 </template>
92
93 <style scoped lang="less">
94 .side {
95 box-shadow: unset;
96 margin-left: 10px;
97 height: 414px;
98
99 :deep(.arco-upload-list-item) {
100 margin-top: 0 !important;
101
102 .arco-upload-picture-card {
103 height: 414px !important;
104 }
105 }
106 }
107 </style>
1 <script setup lang="ts">
2 import {
3 Col,
4 Form,
5 FormItem,
6 Input,
7 InputNumber,
8 Row,
9 Select,
10 Table,
11 TableColumn,
12 Avatar,
13 Link,
14 Space,
15 Button,
16 TableData,
17 } from '@arco-design/web-vue';
18 import { Banner, BannerLink } from '@/utils/model';
19 import { computed, createVNode, ref } from 'vue';
20 import useBannerApi from '@/http/bannner';
21 import ImageUpload from '@/components/image-upload/index.vue';
22 import { AnyObject } from '@/types/global';
23 import { FormInstance } from '@arco-design/web-vue/es/form';
24 import { createModalVNode } from '@/utils/createVNode';
25 import FormListItemContent from '@/views/operation/banner/components/form-list-item-content.vue';
26 import { cloneDeep, pick } from 'lodash';
27 import { promiseToBoolean } from '@/utils';
28 import RoleSelect from '@/views/operation/banner/components/role-select.vue';
29
30 type FormType = Pick<Banner, 'name' | 'scope' | 'weight' | 'permission' | 'type' | 'cover' | 'content'>;
31
32 const { scopeOption } = useBannerApi;
33
34 const props = defineProps<{ init?: AnyObject; http: (params?: object) => Promise<any> }>();
35
36 const formVal = ref<FormType>({
37 name: '',
38 scope: 1,
39 weight: 0,
40 permission: [],
41 type: 3,
42 cover: '',
43 content: [],
44 ...pick(props.init, ['name', 'scope', 'weight', 'permission ', 'type', 'cover', 'content']),
45 });
46
47 const formRule = computed(() => {
48 return {
49 name: [{ required: true, message: '请输入名称' }],
50 scope: [{ required: true, message: '请选择展示位置' }],
51 weight: [{ required: true, message: '请输入权重' }],
52 permission: [{ required: true, message: '请选择浏览用户' }],
53 type: [{ required: true, message: '请选择类型' }],
54 cover: [{ required: true, message: '请选择封面' }],
55 content: formVal.value.type === 1 ? [] : [{ required: true, message: '请输入链接地址' }],
56 };
57 });
58
59 const handleChange = (data: []) => {
60 formVal.value.content = data;
61 };
62
63 const onCreate = () => {
64 const formRef = ref();
65 createModalVNode(() => createVNode(FormListItemContent, { ref: formRef }), {
66 title: '新增',
67 titleAlign: 'center',
68 onBeforeOk: () => formRef.value?.onSubmit(),
69 onOk: () => formVal.value.content.push(formRef.value.getValue()),
70 });
71 };
72
73 const onUpdate = (row: BannerLink, index: number) => {
74 const formRef = ref();
75 createModalVNode(() => createVNode(FormListItemContent, { init: cloneDeep(row), ref: formRef }), {
76 title: '编辑',
77 titleAlign: 'center',
78 onBeforeOk: () => formRef.value?.onSubmit(),
79 onOk: () => formVal.value.content.splice(index, 1, formRef.value.getValue()),
80 });
81 };
82
83 const onDelete = (index: number) => {
84 formVal.value.content.splice(index, 1);
85 };
86
87 const formRef = ref<FormInstance>();
88
89 const onSubmit = async (): Promise<boolean> => {
90 if (await formRef.value?.validate()) {
91 // eslint-disable-next-line prefer-promise-reject-errors
92 return Promise.reject(false);
93 }
94 return promiseToBoolean(props.http(formVal.value));
95 };
96
97 defineExpose({ onSubmit });
98 </script>
99
100 <template>
101 <Form ref="formRef" style="width: 1100px" :model="formVal" :rules="formRule" auto-label-width label-align="right">
102 <Row :gutter="16">
103 <Col flex="auto">
104 <FormItem label="名称" field="name" show-colon row-class="form-item">
105 <Input v-model="formVal.name" :max-length="30" show-word-limit placeholder="请输入" />
106 </FormItem>
107 <FormItem label="展示位置" field="scope" show-colon row-class="form-item">
108 <Select v-model="formVal.scope" :options="scopeOption" placeholder="请选择" />
109 </FormItem>
110 <FormItem label="权重" field="weight" show-colon row-class="form-item">
111 <InputNumber v-model="formVal.weight" :min="0" :max="200" placeholder="请输入" />
112 </FormItem>
113 <FormItem label="浏览用户" field="permission" show-colon row-class="mb-0">
114 <RoleSelect v-model="formVal.permission" :max-tag-count="4" style="height: 32px" />
115 </FormItem>
116 </Col>
117 <Col flex="400px">
118 <FormItem label="封面" field="cover" row-class="mb-0" hide-label>
119 <ImageUpload v-model="formVal.cover" :width="380" :height="156" fit="fill" class="cover" />
120 </FormItem>
121 </Col>
122 </Row>
123 </Form>
124 <Row style="margin: 16px 0">
125 <Col :span="24">
126 <Space>
127 <Button type="primary" size="small" @click="onCreate">新增</Button>
128 </Space>
129 </Col>
130 </Row>
131 <Table
132 style="width: 1100px"
133 :bordered="true"
134 :data="formVal.content as TableData[]"
135 :show-header="false"
136 :pagination="false"
137 :draggable="{}"
138 @change="handleChange"
139 >
140 <template #columns>
141 <TableColumn data-index="cover" :width="50">
142 <template #cell="{ record }">
143 <Avatar :image-url="record.cover" fit="contain" shape="square" />
144 </template>
145 </TableColumn>
146 <TableColumn data-index="title" :width="200" />
147 <TableColumn data-index="content" :ellipsis="true" :tooltip="true" />
148 <TableColumn :width="100">
149 <template #cell="{ record, rowIndex }">
150 <Space>
151 <Link :hoverable="false" @click="onUpdate(record, rowIndex)">修改</Link>
152 <Link :hoverable="false" @click="onDelete(rowIndex)">删除</Link>
153 </Space>
154 </template>
155 </TableColumn>
156 </template>
157 </Table>
158 </template>
159
160 <style scoped lang="less">
161 .form-item {
162 margin-bottom: 10px !important;
163 }
164
165 :deep(.arco-upload-list-item) {
166 margin-top: 0 !important;
167 }
168 </style>
1 <script setup lang="ts">
2 import { Form, FormItem, Select, Input, Textarea } from '@arco-design/web-vue';
3 import { IconAttachment } from '@arco-design/web-vue/es/icon';
4 import { computed, createVNode, markRaw, ref, watch } from 'vue';
5 import { AnyObject } from '@/types/global';
6 import UserTable from '@/views/operation/banner/components/user-table.vue';
7 import { BannerLink } from '@/utils/model';
8 import { createModalVNode } from '@/utils/createVNode';
9 import ProjectTable from '@/views/operation/banner/components/project-table.vue';
10 import ActivityTable from '@/views/operation/banner/components/activity-table.vue';
11 import { FormInstance } from '@arco-design/web-vue/es/form';
12
13 const props = defineProps<{ init?: AnyObject }>();
14
15 const typeOption = [
16 { value: 1, label: '用户' },
17 { value: 2, label: '歌曲' },
18 { value: 3, label: '厂牌' },
19 ];
20
21 const formRef = ref<FormInstance>();
22
23 const formRule = {
24 link_type: [{ required: true, message: '请选择关联类型' }],
25 link_id: [{ type: 'number', min: 1, required: true, message: '请选择关联对象' }],
26 title: [{ required: true, message: '请输入名称' }],
27 content: [{ required: true, message: '请输入推荐语' }],
28 };
29
30 const formValue = ref<BannerLink>({
31 cover: '',
32 link_type: 1,
33 link_id: 0,
34 link_name: '',
35 title: '',
36 content: '',
37 desc1: '',
38 desc2: '',
39 desc3: '',
40 ...props.init,
41 });
42
43 watch(
44 () => formValue.value.link_type,
45 () => {
46 formValue.value.cover = '';
47 formValue.value.link_id = 0;
48 formValue.value.link_name = '';
49 formValue.value.title = '';
50 formValue.value.content = '';
51 formValue.value.desc1 = '';
52 formValue.value.desc2 = '';
53 formValue.value.desc3 = '';
54 }
55 );
56
57 const tableComponent = computed(() => {
58 switch (formValue.value.link_type) {
59 case 2:
60 return markRaw(ActivityTable);
61 case 3:
62 return markRaw(ProjectTable);
63 default:
64 return markRaw(UserTable);
65 }
66 });
67
68 const onChoose = () => {
69 const modal = ref();
70
71 modal.value = createModalVNode(
72 () =>
73 createVNode(tableComponent.value, {
74 userKey: formValue.value.link_id,
75 onCheck: (value: object) => {
76 Object.assign(formValue.value, value);
77 formRef.value?.clearValidate();
78 modal.value?.close();
79 },
80 }),
81 {
82 title: '关联对象',
83 titleAlign: 'center',
84 footer: false,
85 closable: true,
86 width: '760px',
87 }
88 );
89 };
90
91 const getValue = () => formValue.value;
92
93 const onSubmit = async (): Promise<boolean> => {
94 if (await formRef.value?.validate()) {
95 // eslint-disable-next-line prefer-promise-reject-errors
96 return Promise.reject(false);
97 }
98
99 return Promise.resolve(true);
100 };
101
102 defineExpose({ onSubmit, getValue });
103 </script>
104
105 <template>
106 <Form ref="formRef" :model="formValue" :rules="formRule">
107 <FormItem label="关联类型" field="link_type" show-colon>
108 <Select v-model="formValue.link_type" :options="typeOption" placeholder="请选择" />
109 </FormItem>
110 <FormItem label="关联对象" field="link_id" show-colon>
111 <Input :model-value="formValue.link_name" :readonly="true" placeholder="请选择" @focus="onChoose">
112 <template #suffix>
113 <IconAttachment />
114 </template>
115 </Input>
116 </FormItem>
117 <FormItem label="名称" field="title" show-colon>
118 <Input v-model="formValue.title" :max-length="12" placeholder="请输入" show-word-limit />
119 </FormItem>
120 <FormItem label="描述1" field="desc1" show-colon>
121 <Input v-model="formValue.desc1" :max-length="18" placeholder="请输入" show-word-limit />
122 </FormItem>
123 <FormItem label="描述2" field="desc2" show-colon>
124 <Input v-model="formValue.desc2" :max-length="18" placeholder="请输入" show-word-limit />
125 </FormItem>
126 <FormItem label="描述3" field="desc3" show-colon>
127 <Textarea v-model="formValue.desc3" :max-length="36" placeholder="请输入" :auto-size="{ minRows: 2, maxRows: 2 }" show-word-limit />
128 </FormItem>
129 <FormItem label="推荐语" field="content" show-colon row-class="mb-0">
130 <Textarea v-model="formValue.content" :max-length="36" placeholder="请输入" :auto-size="{ minRows: 2, maxRows: 2 }" show-word-limit />
131 </FormItem>
132 </Form>
133 </template>
134
135 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { Col, Form, FormItem, Input, InputNumber, Row, Select, Layout, LayoutContent, LayoutSider } from '@arco-design/web-vue';
3 import { onBeforeUnmount, ref, shallowRef } from 'vue';
4 import { FieldRule, FormInstance } from '@arco-design/web-vue/es/form';
5 import ImageUpload from '@/components/image-upload/index.vue';
6 import useBannerApi from '@/http/bannner';
7
8 // eslint-disable-next-line @typescript-eslint/no-unused-vars
9 import { Editor, Toolbar } from '@wangeditor/editor-for-vue';
10 import { Boot } from '@wangeditor/editor';
11 import '@/views/operation/notification/module';
12 import { AttributeData } from '@/types/global';
13 import { pick } from 'lodash';
14 import { promiseToBoolean } from '@/utils';
15 import RoleSelect from '@/views/operation/banner/components/role-select.vue';
16
17 const props = defineProps<{ init?: AttributeData; http: (params?: object) => Promise<any> }>();
18 const { scopeOption } = useBannerApi;
19
20 const formRef = ref<FormInstance>();
21 const editorRef = shallowRef();
22
23 Boot.setEditorConfig({ readOnly: false });
24
25 const onCreateEditor = (editor: unknown) => {
26 editorRef.value = editor;
27 };
28
29 onBeforeUnmount(() => {
30 const editor = editorRef.value;
31 if (editor == null) return;
32 editor.destroy();
33 });
34
35 const formVal = ref({
36 name: '',
37 scope: 1,
38 weight: 0,
39 permission: [],
40 type: 4,
41 cover: '',
42 content: '',
43 ...pick(props.init, ['name', 'scope', 'weight', 'permission', 'type', 'cover', 'content']),
44 });
45
46 const formRule = {
47 link_type: [{ required: true, message: '请选择关联类型' }],
48 link_id: [{ type: 'number', min: 1, required: true, message: '请选择关联对象' }],
49 title: [{ required: true, message: '请输入名称' }],
50 content: [{ required: true, message: '请输入推荐语' }],
51 } as Record<string, FieldRule[]>;
52
53 const onSubmit = async (): Promise<boolean> => {
54 if (await formRef.value?.validate()) {
55 // eslint-disable-next-line prefer-promise-reject-errors
56 return Promise.reject(false);
57 }
58 return promiseToBoolean(props.http(formVal.value));
59 };
60
61 defineExpose({ onSubmit });
62 </script>
63
64 <template>
65 <Form ref="formRef" :model="formVal" :rules="formRule" style="width: 1321px" auto-label-width label-align="right">
66 <Row :gutter="16">
67 <Col flex="auto">
68 <FormItem label="名称" field="name" show-colon row-class="form-item">
69 <Input v-model="formVal.name" :max-length="30" show-word-limit placeholder="请输入" />
70 </FormItem>
71 <FormItem label="展示位置" field="scope" show-colon row-class="form-item">
72 <Select v-model="formVal.scope" :options="scopeOption" placeholder="请选择" />
73 </FormItem>
74 <FormItem label="权重" field="weight" show-colon row-class="form-item">
75 <InputNumber v-model="formVal.weight" :min="0" :max="200" placeholder="请输入" />
76 </FormItem>
77 <FormItem label="浏览用户" field="permission" show-colon row-class="mb-0">
78 <RoleSelect v-model="formVal.permission" :max-tag-count="6" style="height: 32px" />
79 </FormItem>
80 </Col>
81 <Col flex="420px">
82 <FormItem label="封面" field="cover" row-class="mb-0" hide-label>
83 <ImageUpload v-model="formVal.cover" :width="380" :height="156" fit="fill" class="cover" />
84 </FormItem>
85 </Col>
86 <Col :span="24" style="margin-top: 16px; margin-bottom: -24px">
87 <Toolbar style="border-top: 1px solid #ccc; border-bottom: 1px solid #ccc" :editor="editorRef" />
88 <Layout>
89 <LayoutContent>
90 <Editor v-model="formVal.content" style="height: 400px" @on-created="onCreateEditor" />
91 </LayoutContent>
92 <LayoutSider class="rich" :width="420">
93 <!-- eslint-disable vue/no-v-html -->
94 <div class="content" v-html="formVal.content" />
95 <!--eslint-enable-->
96 </LayoutSider>
97 </Layout>
98 </Col>
99 </Row>
100 </Form>
101 </template>
102
103 <style scoped lang="less">
104 .form-item {
105 margin-bottom: 10px !important;
106 }
107
108 :deep(.arco-upload-list-item) {
109 margin-top: 0 !important;
110 }
111
112 .rich {
113 box-shadow: unset;
114 border-left: 1px solid #e8e8e8;
115
116 .content {
117 overflow-y: auto;
118 height: 400px;
119 padding: 0 10px;
120
121 :deep(img) {
122 height: auto !important;
123 max-width: 100%;
124 object-fit: contain;
125 }
126
127 :deep(video) {
128 max-width: 100%;
129 height: auto !important;
130 display: block;
131 margin: 0 auto;
132 }
133 }
134 }
135 </style>
1 <script setup lang="ts">
2 import { ref, onMounted } from 'vue';
3 import FilterTable from '@/components/filter/table.vue';
4 import FilterTableColumn from '@/components/filter/table-column.vue';
5 import NumberTableColumn from '@/components/filter/number-table-column.vue';
6
7 import useLoading from '@/hooks/loading';
8 import { AnyObject } from '@/types/global';
9 import { Form, FormItem, Space, Input, Button, TableColumn } from '@arco-design/web-vue';
10 import useProjectApi from '@/http/project';
11 import { Project } from '@/utils/model';
12 import { truncate } from 'lodash';
13
14 const { loading, setLoading } = useLoading(false);
15 const filter = ref({ name: '' });
16 const tableRef = ref();
17
18 defineProps<{ userKey?: number }>();
19
20 const emits = defineEmits<{ (e: 'check', value: object): void }>();
21 const onQuery = async (params: AnyObject) => {
22 setLoading(true);
23 return useProjectApi
24 .get({ ...filter.value, status: 1, setWithCount: ['activity_up'], ...params, sortBy: 'weight', sortType: 'desc' })
25 .finally(() => setLoading(false));
26 };
27
28 const onSearch = () => tableRef.value?.onPageChange(1);
29
30 const onReset = () => {
31 filter.value = { name: '' };
32 onSearch();
33 };
34
35 onMounted(() => onReset());
36
37 const onSubmit = (row: Project) => {
38 emits('check', {
39 link_id: row.id,
40 link_name: row.name,
41 title: truncate(row.name, { length: 12 }),
42 cover: row.cover,
43 desc1: '',
44 desc2: `上架歌曲:${row.activity_up_count}首`,
45 desc3: truncate(row.intro, { length: 36 }),
46 });
47 };
48 </script>
49
50 <template>
51 <Form :model="filter" layout="inline" size="mini">
52 <FormItem label="厂牌名称" show-colon>
53 <Input v-model="filter.name" allow-clear />
54 </FormItem>
55 <FormItem hide-label>
56 <Space>
57 <Button type="primary" @click="onSearch">搜索</Button>
58 <Button @click="onReset">重置</Button>
59 </Space>
60 </FormItem>
61 </Form>
62 <FilterTable
63 ref="tableRef"
64 size="mini"
65 :loading="loading"
66 :on-query="onQuery"
67 style="height: 300px; margin-top: 10px"
68 :scrollbar="true"
69 :scroll="{ y: 300 }"
70 :simple-page="true"
71 >
72 <FilterTableColumn data-index="name" title="厂牌名称" />
73 <NumberTableColumn data-index="activity_up_count" title="上架歌曲数" :width="100" />
74 <TableColumn title="操作" :width="80">
75 <template #cell="{ record }">
76 <Button size="mini" type="primary" @click="onSubmit(record)">
77 {{ record.id === userKey ? '更新' : '选择' }}
78 </Button>
79 </template>
80 </TableColumn>
81 </FilterTable>
82 </template>
83
84 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { computed, onMounted, ref } from 'vue';
3 import useTagApi from '@/http/Tag';
4 import { TreeNodeData, TreeSelect, Checkbox } from '@arco-design/web-vue';
5 import { pull, startsWith, truncate, reject, find } from 'lodash';
6
7 const formValue = defineModel<string[]>({ default: () => [] });
8 const parentKey = { broker: 'captain', manager: 'project' };
9
10 const handleExtCheckTrue = (key: string, parentKey: string) => {
11 if (!formValue.value.includes(key)) {
12 formValue.value.push(key);
13 }
14 pull(formValue.value, parentKey);
15 };
16
17 const handleExtCheckFalse = (key: string, parentKey: string) => {
18 pull(formValue.value, key);
19 if (!formValue.value.includes(parentKey)) {
20 formValue.value.push(parentKey);
21 }
22 };
23
24 const handleExtCheck = (key: string, value: boolean) =>
25 value ? handleExtCheckTrue(key, parentKey[key]) : handleExtCheckFalse(key, parentKey[key]);
26
27 const brokerValue = computed({
28 get: (): boolean => formValue.value.includes('broker'),
29 set: (value: boolean) => handleExtCheck('broker', value),
30 });
31
32 const managerValue = computed({
33 get: (): boolean => formValue.value.includes('manager'),
34 set: (value: boolean) => handleExtCheck('manager', value),
35 });
36
37 const treeValue = computed({
38 get: () => {
39 const values = formValue.value.filter((item) => !['broker', 'manager'].includes(item));
40
41 if (formValue.value.includes('broker') && !formValue.value.includes('captain')) {
42 values.push('captain');
43 }
44
45 if (formValue.value.includes('manager') && !formValue.value.includes('project')) {
46 values.push('project');
47 }
48
49 return values;
50 },
51 set: (value) => {
52 if (!value.includes('project')) {
53 pull(formValue.value, 'manager');
54 }
55
56 if (!value.includes('captain')) {
57 pull(formValue.value, 'broker');
58 }
59 const values = formValue.value?.filter((item) => ['broker', 'manager'].includes(item));
60
61 formValue.value = [...reject(value, (item) => values?.map((item) => parentKey[item]).includes(item)), ...values];
62 },
63 });
64
65 const userAuthTags = ref<TreeNodeData[]>([]);
66
67 const options = computed((): TreeNodeData[] => {
68 return [
69 { key: 'public', title: '未登录' },
70 { key: 'visitor', title: '游客' },
71 { key: 'musician', title: '音乐人', children: userAuthTags.value },
72 { key: 'captain', title: '队长' },
73 { key: 'project', title: '厂牌管理员' },
74 { key: 'system', title: '平台管理员' },
75 ];
76 });
77
78 onMounted(() => {
79 useTagApi.get({ type: 4, fetchType: 'all', setColumn: ['id', 'name'] }).then(({ data }) => {
80 userAuthTags.value = data.map((item) => ({ key: `musician_tag_${item.id}`, title: item.name }));
81 });
82 });
83
84 defineExpose({
85 formatRole: (role: string[]) => {
86 const values = [...options.value, { key: 'broker', title: '经纪人' }, { key: 'manager', title: '厂牌主理人' }];
87
88 return role
89 .map((item) => {
90 if (!startsWith(item, 'musician_tag_')) {
91 return find(values, { key: item })?.title;
92 }
93
94 const tagTitle = find(userAuthTags.value, { key: item })?.title;
95
96 if (tagTitle) {
97 return `音乐人 / ${tagTitle}`;
98 }
99
100 return tagTitle;
101 })
102 .filter((item) => item?.length !== 0)
103 .join('、');
104 },
105 });
106 </script>
107
108 <template>
109 <TreeSelect
110 v-model="treeValue"
111 :multiple="true"
112 :allow-clear="true"
113 :tree-checkable="true"
114 tree-checked-strategy="parent"
115 :fallback-option="false"
116 :data="options"
117 placeholder="请选择"
118 >
119 <template #label="{ data }">
120 <template v-if="data.value === 'captain' && brokerValue">经纪人</template>
121 <template v-else-if="data.value === 'project' && managerValue">厂牌主理人</template>
122 <template v-else-if="startsWith(data.value, 'musician_tag_')">音乐人 / {{ truncate(data.label, { length: 6 }) }}</template>
123 <template v-else>{{ data.label }}</template>
124 </template>
125 <template #tree-slot-extra="row">
126 <Checkbox v-if="row.key === 'captain'" v-model="brokerValue">仅通知经纪人</Checkbox>
127 <Checkbox v-if="row.key === 'project'" v-model="managerValue">仅通知主理人</Checkbox>
128 </template>
129 </TreeSelect>
130 </template>
131
132 <style>
133 .arco-tree-node:hover {
134 color: var(--color-text-1) !important;
135 background-color: var(--color-fill-2) !important;
136 }
137 </style>
1 <script setup lang="ts">
2 import { ref, onMounted } from 'vue';
3 import FilterTable from '@/components/filter/table.vue';
4 import FilterTableColumn from '@/components/filter/table-column.vue';
5 import EnumTableColumn from '@/components/filter/enum-table-column.vue';
6 import useUserApi from '@/http/user';
7 import useLoading from '@/hooks/loading';
8 import { AnyObject } from '@/types/global';
9 import { Form, FormItem, Space, Input, Button, TableColumn } from '@arco-design/web-vue';
10 import { User } from '@/utils/model';
11 import { truncate } from 'lodash';
12
13 const { loading, setLoading } = useLoading(false);
14 const filter = ref({ nick_name: '', real_name: '', status: 1 });
15 const tableRef = ref();
16
17 defineProps<{ userKey?: number }>();
18
19 const emits = defineEmits<{ (e: 'check', value: object): void }>();
20 const onQuery = async (params: AnyObject) => {
21 setLoading(true);
22 return useUserApi
23 .get({
24 ...filter.value,
25 ...params,
26 status: 1,
27 setColumn: ['id', 'nick_name', 'real_name', 'identity', 'avatar'],
28 setWith: ['styleTags:id,name', 'authTags:id,name', 'opus:id,song_name'],
29 sortBy: 'id',
30 sortType: 'desc',
31 })
32 .finally(() => setLoading(false));
33 };
34
35 const onSearch = () => tableRef.value?.onPageChange(1);
36
37 const onReset = () => {
38 filter.value = { nick_name: '', real_name: '', status: 1 };
39 onSearch();
40 };
41
42 const identityOption = [
43 { value: 0, label: '未认证' },
44 { value: 1, label: '音乐人' },
45 { value: 2, label: '经纪人' },
46 { value: 3, label: '音乐人、经纪人' },
47 ];
48
49 onMounted(() => onReset());
50
51 const onSubmit = (row: User) => {
52 emits('check', {
53 link_id: row.id,
54 link_name: row.nick_name,
55 title: truncate(row.nick_name, { length: 12 }),
56 cover: row.avatar || '',
57 desc1: truncate(`其它技能:${row.auth_tags?.map((item) => item.name)?.join(' ')}`, { length: 18 }),
58 desc2: truncate(`擅长风格:${row.style_tags?.map((item) => item.name)?.join(' ')}`, { length: 18 }),
59 desc3: truncate(`代表作:${row.opus?.map((item) => `《${item.song_name}》`)?.join('')}`, { length: 36 }),
60 });
61 };
62 </script>
63
64 <template>
65 <Form :model="filter" layout="inline" size="mini">
66 <FormItem label="用户艺名" show-colon>
67 <Input v-model="filter.nick_name" allow-clear />
68 </FormItem>
69 <FormItem label="用户真名">
70 <Input v-model="filter.real_name" allow-clear />
71 </FormItem>
72 <FormItem hide-label>
73 <Space>
74 <Button type="primary" @click="onSearch">搜索</Button>
75 <Button @click="onReset">重置</Button>
76 </Space>
77 </FormItem>
78 </Form>
79 <FilterTable
80 ref="tableRef"
81 size="mini"
82 :loading="loading"
83 :on-query="onQuery"
84 style="height: 300px; margin-top: 10px"
85 :scrollbar="true"
86 :scroll="{ y: 300 }"
87 :simple-page="true"
88 >
89 <FilterTableColumn data-index="nick_name" title="用户真名" />
90 <FilterTableColumn data-index="real_name" title="用户艺名" :width="200" />
91 <EnumTableColumn data-index="identity" :option="identityOption" title="用户身份" :width="120" />
92 <TableColumn title="操作" :width="80">
93 <template #cell="{ record }">
94 <Button size="mini" type="primary" @click="onSubmit(record)">
95 {{ record.id === userKey ? '更新' : '选择' }}
96 </Button>
97 </template>
98 </TableColumn>
99 </FilterTable>
100 </template>
101
102 <style scoped lang="less"></style>
1 <template>
2 <page-view has-bread has-card>
3 <filter-search :model="filter" :loading="loading" @search="onSearch" @reset="onReset">
4 <filter-search-item label="名称">
5 <a-input v-model="filter.name" placeholder="请输入" allow-clear />
6 </filter-search-item>
7 <filter-search-item label="展示位置">
8 <a-select v-model="filter.scope" :options="scopeOption" placeholder="请选择" allow-clear />
9 </filter-search-item>
10 <filter-search-item label="浏览用户">
11 <RoleSelect ref="roleRef" v-model="filter.permission" :max-tag-count="2" style="height: 32px" />
12 </filter-search-item>
13 <filter-search-item label="状态">
14 <a-select v-model="filter.status" :options="statusOption" placeholder="请选择" allow-clear />
15 </filter-search-item>
16 </filter-search>
17 <filter-table ref="tableRef" :loading="loading" :on-query="onQuery">
18 <template #tool>
19 <icon-button v-permission="['operation-banner-create']" type="primary" icon="plus" label="新增" @click="onCreate" />
20 </template>
21 <a-table-column data-index="cover" title="图片" :width="220">
22 <template #cell="{ record }">
23 <a-image style="margin: 8px" :src="record.cover" :width="180" :height="80" fit="fill" />
24 </template>
25 </a-table-column>
26 <filter-table-column title="名称" data-index="name" :width="200" />
27 <enum-table-column title="展示位置" :option="scopeOption" data-index="scope" :width="120" />
28 <filter-table-column title="权重" data-index="weight" :width="80" />
29 <filter-table-column title="浏览用户" data-index="permission" :width="220">
30 <template #default="{ record }">{{ formatRole(record.permission) }}</template>
31 </filter-table-column>
32 <enum-table-column title="类型" :option="typeOption" data-index="type" :width="80" />
33 <date-table-column data-index="created_at" title="创建时间" :width="100" split />
34 <enum-table-column title="状态" :option="statusOption" data-index="status" :width="80" />
35 <space-table-column
36 v-if="usePermission().checkPermission(['operation-banner-edit', 'operation-banner-delete'])"
37 :width="120"
38 data-index="operations"
39 title="操作"
40 >
41 <template #default="{ record }">
42 <a-link
43 v-permission="['operation-banner-edit']"
44 class="link-hover"
45 :hoverable="false"
46 @click="onChange(record, Number(!record.status))"
47 >
48 {{ formatStatus(Number(!record.status)) }}
49 </a-link>
50 <a-link v-permission="['operation-banner-edit']" :hoverable="false" class="link-hover" @click="onUpdate(record)">编辑</a-link>
51 <a-link v-permission="['operation-banner-delete']" :hoverable="false" class="link-hover" @click="onDelete(record)">删除</a-link>
52 </template>
53 </space-table-column>
54 </filter-table>
55 </page-view>
56 </template>
57
58 <script lang="ts" setup>
59 import { ref, onMounted, createVNode } from 'vue';
60
61 import useLoading from '@/hooks/loading';
62 import useBannerApi from '@/http/bannner';
63 import { AnyObject, AttributeData } from '@/types/global';
64 import { createModalVNode, createSelectionFormItemVNode } from '@/utils/createVNode';
65 import { cloneDeep, find } from 'lodash';
66 import EnumTableColumn from '@/components/filter/enum-table-column.vue';
67 import FormBasicContent from '@/views/operation/banner/components/form-basic-content.vue';
68 import FormListContent from '@/views/operation/banner/components/form-list-content.vue';
69 import FormPageContent from '@/views/operation/banner/components/form-page-content.vue';
70 import SpaceTableColumn from '@/components/filter/space-table-column.vue';
71 import { TableData } from '@arco-design/web-vue';
72 import usePermission from '@/hooks/permission';
73 import DateTableColumn from '@/components/filter/date-table-column.vue';
74 import { promiseToBoolean } from '@/utils';
75 import RoleSelect from '@/views/operation/banner/components/role-select.vue';
76
77 const { scopeOption, typeOption, statusOption, get, changeStatus, create, update, destroy } = useBannerApi;
78
79 const { loading, setLoading } = useLoading(false);
80 const filter = ref({ name: '', scope: '', permission: [], status: '' });
81 const tableRef = ref();
82 const roleRef = ref();
83
84 // const formatPermission = (permisson) => {};
85
86 const formatRole = (role: string[]) => roleRef.value?.formatRole(role);
87 // role
88 // .map((item) => find(roleOption, { value: item })?.label || '')
89 // .filter((item) => item.length !== 0)
90 // .join('、');
91
92 const formatStatus = (status: number) => find(statusOption, { value: status })?.label || '未知';
93
94 const onQuery = async (params?: AnyObject) => {
95 setLoading(true);
96 return get({ ...filter.value, ...params, sortBy: 'weight', sortType: 'desc' }).finally(() => setLoading(false));
97 };
98
99 const onSearch = () => tableRef.value?.onPageChange(1);
100
101 const onReset = () => {
102 filter.value = { name: '', scope: '', permission: [], status: '' };
103 onSearch();
104 };
105
106 onMounted(() => onReset());
107
108 const getTemplateComponent = (type: number) => {
109 if (type === 3) {
110 return FormListContent;
111 }
112 if (type === 4) {
113 return FormPageContent;
114 }
115
116 return FormBasicContent;
117 };
118
119 const onShowModal = (title: string, template: any, templateProp: any) => {
120 const formRef = ref();
121
122 createModalVNode(() => createVNode(template, { ref: formRef, ...templateProp }), {
123 title,
124 titleAlign: 'center',
125 width: 'auto',
126 onBeforeOk: () => formRef.value?.onSubmit(),
127 onOk: () => tableRef.value?.onFetch(),
128 });
129 };
130
131 const onCreate = () => {
132 const type = ref<number>(1);
133 const show = ref(false);
134 const options = [
135 { value: 1, label: '普通或外链' },
136 { value: 3, label: '交互' },
137 { value: 4, label: '自定义H5页面' },
138 ];
139
140 createModalVNode(() => createSelectionFormItemVNode(type, options, { label: '类型', rowClass: 'mb-0' }), {
141 title: '新增',
142 onOk: () => {
143 show.value = true;
144 },
145 onClose: () => {
146 if (show.value) {
147 onShowModal('新增Banner', getTemplateComponent(type.value), { http: (value: AttributeData) => create(value) });
148 }
149 },
150 });
151 };
152
153 const onUpdate = (row: TableData) =>
154 onShowModal('编辑Banner', getTemplateComponent(row.type), {
155 init: cloneDeep(row),
156 http: (value: AttributeData) => update(row.id, value),
157 });
158
159 const onChange = (row: TableData, status: number) =>
160 createModalVNode(`${find(statusOption, { value: status })?.label}轮播图:${row.name}`, {
161 title: '状态变更',
162 onBeforeOk: () => promiseToBoolean(changeStatus(row.id, { status })),
163 onOk: () => {
164 row.status = status;
165 },
166 });
167
168 const onDelete = (row: TableData) =>
169 createModalVNode(`删除轮播图:${row.name}`, {
170 title: '删除操作',
171 onBeforeOk: () => promiseToBoolean(destroy(row.id)),
172 onOk: () => tableRef.value?.onFetch(),
173 });
174 </script>
175
176 <style scoped></style>
1 <script setup lang="ts">
2 import { Form, FormItem, Input, Textarea } from '@arco-design/web-vue';
3 import { computed, ref } from 'vue';
4 import { FormInstance } from '@arco-design/web-vue/es/form';
5
6 const props = defineProps<{ initValue?: object; submit?: (value: unknown) => Promise<unknown> }>();
7
8 const formRef = ref<FormInstance>();
9
10 const formValue = ref({ identifier: '', level_title: '', level_intro: '', match_title: '', match_intro: '', ...props.initValue });
11
12 const formRule = computed(() => {
13 return {
14 identifier: [{ required: true, message: '请输入分类' }],
15 level_intro: [{ validator: (value: any, cb: any) => (formValue.value.level_title && !value ? cb('请输入等级通知内容') : {}) }],
16 match_intro: [{ validator: (value: any, cb: any) => (formValue.value.match_title && !value ? cb('请输入合作通知内容') : {}) }],
17 };
18 });
19
20 const onSubmit = async () => {
21 const error = await formRef.value?.validate();
22 return error ? Promise.reject(error) : props.submit?.(formValue.value);
23 };
24
25 const getIdentifier = () => formValue.value.identifier;
26
27 defineExpose({ onSubmit, getIdentifier });
28 </script>
29
30 <template>
31 <Form ref="formRef" :model="formValue" :rules="formRule as any" auto-label-width>
32 <FormItem label="分类" field="identifier" show-colon>
33 <Input
34 v-model="formValue.identifier"
35 placeholder="请输入"
36 :max-length="32"
37 show-word-limit
38 @input="(value:any) => (formValue.identifier = value.replace(/[^A-Z]/g, ''))"
39 />
40 <template #extra> 仅允许输入A-Z字母组成字符串</template>
41 </FormItem>
42 <FormItem label="等级通知" field="level_title" show-colon>
43 <Input v-model="formValue.level_title" placeholder="请输入" :max-length="64" show-word-limit />
44 </FormItem>
45 <FormItem field="intro">
46 <Textarea v-model="formValue.level_intro" :max-length="500" show-word-limit :auto-size="{ minRows: 4, maxRows: 4 }" />
47 </FormItem>
48 <FormItem label="合作通知" field="match_title" show-colon>
49 <Input v-model="formValue.match_title" placeholder="请输入" :max-length="64" show-word-limit />
50 </FormItem>
51 <FormItem field="match_intro">
52 <Textarea v-model="formValue.match_intro" :max-length="500" show-word-limit :auto-size="{ minRows: 4, maxRows: 4 }" />
53 </FormItem>
54 <div style="color: rgba(44, 44, 44, 0.5); font-size: 14px; line-height: initial; margin-bottom: 6px">
55 注意:如模板中要引用变量,请参照下方规则
56 </div>
57 <div style="color: #ff0000; font-size: 14px; line-height: initial">
58 经纪人:{border};歌手:{singer};开始时间:{start};结束时间:{end};分类:{type};厂牌:{brand};歌曲名{song}
59 </div>
60 </Form>
61 </template>
62
63 <style scoped lang="less"></style>
1 <template>
2 <filter-table ref="tableRef" :loading="loading" :on-query="onQuery" :hide-page="true">
3 <template #tool>
4 <icon-button v-permission="['operation-broker-push-config-create']" type="primary" icon="plus" label="新增" @click="onCreate" />
5 <div style="color: rgba(44, 44, 44, 0.5); margin-right: 10px">
6 名单外的经纪人,如果Ta的歌手达成合作,对应到分类:
7 <span style="color: #1d2129">{{ identifierOptions.find((item) => item.value === defaultIdentifier.content)?.label || '无' }}</span>
8 </div>
9 <a-link
10 v-permission="['operation-broker-push-config-relation']"
11 :hoverable="false"
12 style="text-decoration: underline"
13 @click="onSetting"
14 >设置关联
15 </a-link>
16 </template>
17 <filter-table-column title="分类" data-index="identifier" :width="80" />
18 <filter-table-column title="等级通知(标题)" data-index="level_title" :width="160" />
19 <filter-table-column title="等级通知(内容)" data-index="level_intro" :width="240" />
20 <filter-table-column title="合作通知(标题)" data-index="match_title" :width="160" />
21 <filter-table-column title="合作通知(内容)" data-index="match_intro" :width="240" />
22 <user-table-column title="创建人" data-index="user_id" user="user" show-href :width="140" />
23 <space-table-column
24 v-if="
25 usePermission().checkPermission([
26 'operation-broker-push-config-show',
27 'operation-broker-push-config-edit',
28 'operation-broker-push-config-delete',
29 ])
30 "
31 title="操作"
32 :width="120"
33 >
34 <template #default="{ record }">
35 <a-link v-permission="['operation-broker-push-config-show']" class="link-hover" :hoverable="false" @click="onView(record)">
36 查看
37 </a-link>
38 <a-link v-permission="['operation-broker-push-config-edit']" class="link-hover" :hoverable="false" @click="onUpdate(record)">
39 编辑
40 </a-link>
41 <a-link v-permission="['operation-broker-push-config-delete']" class="link-hover" :hoverable="false" @click="onDelete(record)">
42 删除
43 </a-link>
44 </template>
45 </space-table-column>
46 </filter-table>
47 </template>
48
49 <script setup lang="ts">
50 import { AnyObject } from '@/types/global';
51 import { createFormVNode, createModalVNode, createSelectionFormItemVNode } from '@/utils/createVNode';
52 import { computed, createVNode, h, onMounted, ref } from 'vue';
53
54 import ConfigForm from '@/views/operation/broker/components/config-form.vue';
55 import { usePushConfigApi } from '@/http/broker';
56 import useLoading from '@/hooks/loading';
57 import UserTableColumn from '@/components/filter/user-table-column.vue';
58 import SpaceTableColumn from '@/components/filter/space-table-column.vue';
59 import { TableData } from '@arco-design/web-vue';
60 import { clone } from 'lodash';
61 import useConfigApi from '@/http/config';
62 import { SystemConfig } from '@/types/system-config';
63 import { promiseToBoolean } from '@/utils';
64 import usePermission from '@/hooks/permission';
65
66 const { get, create, update, destroy } = usePushConfigApi;
67
68 const { loading, setLoading } = useLoading(false);
69 const tableRef = ref();
70
71 const defaultIdentifier = ref<Pick<SystemConfig, 'id' | 'content'>>({ id: 0, content: '' });
72
73 const onQuery = async (params: AnyObject) => {
74 setLoading(true);
75 return get({ ...params, fetchType: 'all', setWith: ['user:id,nick_name,identity'], sortBy: 'id', sortType: 'desc' }).finally(() =>
76 setLoading(false)
77 );
78 };
79
80 onMounted(async () => {
81 tableRef.value?.onPageChange();
82 defaultIdentifier.value = (await useConfigApi.getOne({ identifier: 'g65owmrgKSUhxV2afndUg', parent_id: 0 })) || { id: 0, content: '' };
83 });
84
85 const identifierOptions = computed(() => {
86 return [{ value: '', label: '无' }].concat(
87 tableRef.value?.getList()?.map((item: { id: number; identifier: string }) => {
88 return { value: item.identifier, label: item.identifier };
89 })
90 );
91 });
92
93 const onSetting = () => {
94 const formValue = ref(clone(defaultIdentifier.value.content));
95 createModalVNode(
96 () =>
97 createFormVNode({ model: formValue, autoLabelWidth: true }, [
98 createSelectionFormItemVNode(
99 formValue,
100 identifierOptions.value,
101 { label: '关联分类', style: { marginBottom: '8px' } },
102 { fallbackOption: false, placeholder: '请选择' }
103 ),
104 h(
105 'div',
106 { style: { color: 'rgba(44, 44, 44, 0.5)' } },
107 '注意:此处设置名单外的经纪人用户,如果Ta的歌手合作后,经纪人是否收到 [合作通知],如需要收到合作通知,根据哪种分类进行推送'
108 ),
109 ]),
110 {
111 title: '其他关联设置',
112 titleAlign: 'center',
113 onBeforeOk: () => promiseToBoolean(useConfigApi.update(defaultIdentifier.value.id, { content: formValue.value })),
114 onOk: () => (defaultIdentifier.value.content = formValue.value),
115 }
116 );
117 };
118
119 const onView = (record: TableData) => {
120 createModalVNode(() => h(ConfigForm, { initValue: record, disabled: true }), {
121 title: '查看',
122 titleAlign: 'center',
123 width: '640px',
124 hideCancel: true,
125 okText: '我知道了',
126 });
127 };
128
129 const onCreate = () => {
130 const formRef = ref<InstanceType<typeof ConfigForm>>();
131 createModalVNode(() => createVNode(ConfigForm, { ref: formRef, submit: (val: AnyObject) => create(val) }), {
132 title: '新增',
133 titleAlign: 'center',
134 width: '640px',
135 onBeforeOk: () => promiseToBoolean(formRef.value?.onSubmit()),
136 onOk: () => tableRef.value?.onFetch(),
137 });
138 };
139
140 const onUpdate = (record: TableData) => {
141 const formRef = ref<InstanceType<typeof ConfigForm>>();
142 createModalVNode(
143 () => createVNode(ConfigForm, { initValue: record, ref: formRef, submit: (val: AnyObject) => update(record.id, val) }),
144 {
145 title: '编辑',
146 titleAlign: 'center',
147 width: '640px',
148 onBeforeOk: () => promiseToBoolean(formRef.value?.onSubmit()),
149 onOk: () => {
150 if (defaultIdentifier.value?.content === record.identifier) {
151 const content = formRef.value?.getIdentifier();
152 useConfigApi.update(defaultIdentifier.value.id, { content });
153 defaultIdentifier.value.content = content ?? '';
154 }
155 tableRef.value?.onFetch();
156 },
157 }
158 );
159 };
160
161 const onDelete = (record: TableData) =>
162 createModalVNode(`删除推送模版配置:${record.identifier}`, {
163 title: '删除操作',
164 onBeforeOk: () => promiseToBoolean(destroy(record.id)),
165 onOk: () => {
166 if (defaultIdentifier.value?.content === record.identifier) {
167 useConfigApi.update(defaultIdentifier.value.id, { content: '' });
168 defaultIdentifier.value.content = '';
169 }
170 tableRef.value?.onFetch();
171 },
172 });
173 </script>
174
175 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { createVNode, onBeforeMount, onMounted, ref } from 'vue';
3 import { usePushLevelRecordApi } from '@/http/broker';
4 import { AnyObject, Option } from '@/types/global';
5 import useLoading from '@/hooks/loading';
6
7 import useTagApi from '@/http/Tag';
8 import { Input, Select, Link, TableData, Textarea } from '@arco-design/web-vue';
9 import FilterSearch from '@/components/filter/search.vue';
10 import FilterSearchItem from '@/components/filter/search-item.vue';
11 import FilterTable from '@/components/filter/table.vue';
12 import FilterTableColumn from '@/components/filter/table-column.vue';
13 import UserTableColumn from '@/components/filter/user-table-column.vue';
14 import EnumTableColumn from '@/components/filter/enum-table-column.vue';
15 import SpaceTableColumn from '@/components/filter/space-table-column.vue';
16 import { createFormItemVNode, createFormVNode, createModalVNode } from '@/utils/createVNode';
17 import { get } from 'lodash';
18
19 const props = defineProps<{ recordId: number }>();
20
21 const { getChildren } = usePushLevelRecordApi;
22 const { loading, setLoading } = useLoading(false);
23 const filter = ref({ userName: '', authIds: [], status: '', readStatus: '' });
24 const tableRef = ref();
25
26 const statusOption = [
27 { value: 0, label: '待发送' },
28 { value: 1, label: '发送中' },
29 { value: 2, label: '已发送' },
30 { value: 3, label: '已撤销' },
31 { value: -1, label: '发送失败' },
32 ];
33
34 const readStatusOption = [
35 { value: 0, label: '未查看' },
36 { value: 1, label: '已查看' },
37 ];
38
39 const onQuery = async (params: AnyObject) => {
40 setLoading(true);
41 return getChildren(props.recordId, {
42 ...filter.value,
43 ...params,
44 setWith: ['user:id,nick_name,identity', 'user.authTags:id,name'],
45 }).finally(() => setLoading(false));
46 };
47
48 const onSearch = () => tableRef.value?.onPageChange(1);
49
50 const onReset = () => {
51 filter.value = { userName: '', authIds: [], status: '', readStatus: '' };
52 onSearch();
53 };
54
55 onMounted(() => onReset());
56
57 const authTagOptions = ref<Option[]>([]);
58
59 onBeforeMount(() => useTagApi.getOption({ type: 4 }).then((data) => (authTagOptions.value = data)));
60
61 const onView = (row: TableData) => {
62 createModalVNode(
63 () =>
64 createFormVNode({ model: row, autoLabelWidth: true }, [
65 createFormItemVNode({ label: '通知标题' }, createVNode(Input, { modelValue: row.title })),
66 createFormItemVNode(
67 { label: '通知内容', rowClass: 'mb-0' },
68 createVNode(Textarea, { modelValue: row.content, autoSize: { minRows: 6, maxRows: 12 } })
69 ),
70 ]),
71 {
72 title: '查看',
73 titleAlign: 'center',
74 footer: false,
75 escToClose: true,
76 maskClosable: true,
77 }
78 );
79 };
80 </script>
81
82 <template>
83 <FilterSearch :loading="loading" :model="filter" @search="onSearch" @reset="onReset">
84 <FilterSearchItem label="经纪人">
85 <Input v-model="filter.userName" placeholder="请输入" allow-clear />
86 </FilterSearchItem>
87 <FilterSearchItem label="认证能力">
88 <Select
89 v-model="filter.authIds"
90 placeholder="请选择"
91 :options="authTagOptions"
92 multiple
93 allow-clear
94 allow-search
95 :max-tag-count="2"
96 />
97 </FilterSearchItem>
98 <FilterSearchItem label="发送状态">
99 <Select v-model="filter.status" :options="statusOption" placeholder="请选择" allow-clear />
100 </FilterSearchItem>
101 <FilterSearchItem label="用户行为">
102 <Select v-model="filter.readStatus" :options="readStatusOption" placeholder="请选择" allow-clear />
103 </FilterSearchItem>
104 </FilterSearch>
105 <FilterTable ref="tableRef" :loading="loading" :on-query="onQuery">
106 <UserTableColumn title="经纪人" data-index="user_id" user="user" show-href :width="160" />
107 <FilterTableColumn title="认证能力" data-index="user_id" :width="200">
108 <template #default="{ record }">
109 <!-- eslint-disable -->
110 <span v-if="get(record, 'user.auth_tags')">
111 {{
112 get(record, 'user.auth_tags')
113 ?.map((item: any) => item.name)
114 ?.join('|') || ''
115 }}
116 </span>
117 <!--eslint-enable-->
118 <span v-else style="color: rgba(44, 44, 44, 0.5)"></span>
119 </template>
120 </FilterTableColumn>
121 <FilterTableColumn title="通知内容" data-index="content" :width="200" />
122 <FilterTableColumn title="发送时间" data-index="send_at" :width="180" />
123 <EnumTableColumn title="发送状态" data-index="status" :option="statusOption" :width="100" />
124 <FilterTableColumn data-index="read_at" title="用户行为" :width="100">
125 <template #default="{ record }: { record: { read_at?: string } }">{{ record.read_at ? '已查看' : '未查看' }}</template>
126 </FilterTableColumn>
127 <SpaceTableColumn data-index="operation" title="操作" :width="70">
128 <template #default="{ record }">
129 <Link class="link-hover" :hoverable="false" @click="onView(record)">查看</Link>
130 </template>
131 </SpaceTableColumn>
132 </FilterTable>
133 </template>
134
135 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { usePushLevelRecordApi } from '@/http/broker';
3 import { AnyObject, QueryForParams } from '@/types/global';
4 import useLoading from '@/hooks/loading';
5 import { computed, createVNode, h, onMounted, ref } from 'vue';
6 import SpaceTableColumn from '@/components/filter/space-table-column.vue';
7 import NumberTableColumn from '@/components/filter/number-table-column.vue';
8 import EnumTableColumn from '@/components/filter/enum-table-column.vue';
9 import { pick, range } from 'lodash';
10 import { Select, DatePicker, TableData } from '@arco-design/web-vue';
11 import { createFormItemVNode, createFormVNode, createModalVNode } from '@/utils/createVNode';
12 import dayjs, { OpUnitType } from 'dayjs';
13 import { promiseToBoolean } from '@/utils';
14 import PushLevelChildrenTable from '@/views/operation/broker/components/push-level-children-table.vue';
15
16 const { get, update, send, rollback, destroy, statusOption } = usePushLevelRecordApi;
17 const { loading, setLoading } = useLoading(false);
18 const tableRef = ref();
19 const filter = ref({ title: '', userName: '', sendBetween: [], status: '' });
20
21 const queryParams = computed((): QueryForParams => {
22 return { ...filter.value, setWithCount: ['items', 'readItems'], sortBy: 'publish_at', sortType: 'desc' };
23 });
24
25 const onQuery = async (params: AnyObject) => {
26 setLoading(true);
27 return get({ ...queryParams.value, ...params }).finally(() => setLoading(false));
28 };
29
30 const onSearch = () => tableRef.value?.onPageChange(1);
31
32 const onReset = () => {
33 filter.value = { title: '', userName: '', sendBetween: [], status: '' };
34 onSearch();
35 };
36
37 onMounted(() => onReset());
38
39 const isSameTime = (date: dayjs.ConfigType, unit: OpUnitType) => dayjs().isSame(dayjs(date), unit);
40
41 const onView = (row: TableData) => {
42 createModalVNode(() => createVNode(PushLevelChildrenTable, { recordId: row.id }), {
43 title: '详情',
44 titleAlign: 'center',
45 width: '1200px',
46 closable: true,
47 footer: false,
48 });
49 };
50
51 const onDelete = (row: TableData) =>
52 createModalVNode(`删除等级推送:${row.title}`, {
53 title: '删除操作',
54 onBeforeOk: () => promiseToBoolean(destroy(row.id)),
55 onOk: () => tableRef.value?.onFetch(),
56 });
57
58 const onSend = (row: TableData) =>
59 createModalVNode(`确认立即发送通知《${row.title}》`, {
60 title: '更新操作',
61 onBeforeOk: () => promiseToBoolean(send(row.id)),
62 onOk: () => tableRef.value?.onFetch(),
63 });
64
65 const onRollback = (row: TableData) =>
66 createModalVNode(`确认撤回发送通知《${row.title}》`, {
67 title: '撤回操作',
68 onBeforeOk: () => promiseToBoolean(rollback(row.id)),
69 onOk: () => tableRef.value?.onFetch(),
70 });
71
72 const onUpdate = (row: TableData) => {
73 const formValue = ref({ ...pick(row, 'is_alert', 'publish_at') });
74 const alertOption = [
75 { value: 0, label: '否' },
76 { value: 1, label: '是' },
77 ];
78 createModalVNode(
79 () =>
80 createFormVNode({ model: formValue }, [
81 createFormItemVNode(
82 { label: '发送时间', field: 'publish_at', required: true },
83 h(DatePicker, {
84 'style': { width: '100%' },
85 'modelValue': formValue.value.publish_at,
86 'showTime': true,
87 'dayStartOfWeek': 1,
88 'showNowBtn': false,
89 'allowClear': false,
90 'format': 'YYYY-MM-DD HH:mm',
91 'valueFormat': 'YYYY-MM-DD HH:mm:ss',
92 'timePickerProps': { hideDisabledOptions: true },
93 'onUpdate:modelValue': (val: any) => (formValue.value.publish_at = val),
94 'disabledDate': (current?: Date) => dayjs(current).isBefore(dayjs().subtract(1, 'day')),
95 'disabledTime': (current?: Date) => {
96 return {
97 disabledHours: () => range(0, 23).filter((item) => isSameTime(current, 'day') && item < dayjs().hour()),
98 disabledMinutes: () =>
99 range(0, 59).filter(
100 (item) => isSameTime(current, 'day') && isSameTime(current, 'hour') && item < dayjs().add(5, 'minute').minute()
101 ),
102 };
103 },
104 })
105 ),
106 createFormItemVNode(
107 { label: '面板提醒', field: 'is_alert', required: true, rowClass: 'mb-0' },
108 h(Select, {
109 'options': alertOption,
110 'modelValue': formValue.value.is_alert,
111 'onUpdate:modelValue': (val?: unknown) => (formValue.value.is_alert = Number(val)),
112 })
113 ),
114 ]),
115 {
116 title: '编辑',
117 titleAlign: 'center',
118 onBeforeOk: () => promiseToBoolean(update(row.id, formValue.value)),
119 onOk: () => tableRef.value?.onFetch(),
120 }
121 );
122 };
123 </script>
124
125 <template>
126 <filter-search :loading="loading" :model="filter" @search="onSearch" @reset="onReset">
127 <filter-search-item label="名称">
128 <a-input v-model="filter.title" placeholder="请输入" allow-clear />
129 </filter-search-item>
130 <filter-search-item label="创建人">
131 <a-input v-model="filter.userName" placeholder="请输入" allow-clear />
132 </filter-search-item>
133 <filter-search-item label="发送时间">
134 <a-range-picker
135 v-model="filter.sendBetween"
136 :day-start-of-week="1"
137 :time-picker-props="{ defaultValue: ['00:00:00', '23:59:59'] }"
138 value-format="YYYY-MM-DD HH:mm:ss"
139 format="YYYY-MM-DD"
140 />
141 </filter-search-item>
142 <filter-search-item label="发送状态">
143 <a-select v-model="filter.status" :options="statusOption" placeholder="请选择" allow-clear />
144 </filter-search-item>
145 </filter-search>
146 <filter-table ref="tableRef" :loading="loading" :on-query="onQuery">
147 <filter-table-column title="名称" data-index="title" :width="260" />
148 <number-table-column title="推送经纪人(人数)" data-index="items_count" :width="160" />
149 <number-table-column title="查看人数" data-index="read_items_count" :dark-value="0" :width="100" />
150 <filter-table-column title="开始时间" data-index="begin_at" :width="180" />
151 <filter-table-column title="结束时间" data-index="end_at" :width="180" />
152 <filter-table-column title="发送时间" data-index="publish_at" :width="200" />
153 <enum-table-column title="发送状态" data-index="status" :option="statusOption" :width="120" />
154 <space-table-column title="操作" :width="120">
155 <template #default="{ record }: { record: { status: number } }">
156 <a-link class="link-hover" :hoverable="false" @click="onView(record)">查看</a-link>
157 <a-link v-if="record.status === 0" class="link-hover" :hoverable="false" @click="onUpdate(record)">编辑</a-link>
158 <a-link v-if="[-1, 3].includes(record.status)" class="link-hover" :hoverable="false" @click="onSend(record)">
159 {{ record.status === -1 ? '重试' : '发送' }}
160 </a-link>
161 <a-link v-if="record.status === 2" class="link-hover" :hoverable="false" @click="onRollback(record)">撤回</a-link>
162 <a-link v-if="record.status !== 1" class="link-hover" :hoverable="false" @click="onDelete(record)">删除</a-link>
163 </template>
164 </space-table-column>
165 </filter-table>
166 </template>
167
168 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import useLoading from '@/hooks/loading';
3 import { AnyObject, QueryForParams } from '@/types/global';
4 import { usePushMatchRecordApi } from '@/http/broker';
5 import { computed, createVNode, onMounted, ref } from 'vue';
6 import UserTableColumn from '@/components/filter/user-table-column.vue';
7 import LinkTableColumn from '@/components/filter/link-table-column.vue';
8 import EnumTableColumn from '@/components/filter/enum-table-column.vue';
9 import SpaceTableColumn from '@/components/filter/space-table-column.vue';
10 import { Input, TableData, Textarea } from '@arco-design/web-vue';
11 import { createFormItemVNode, createFormVNode, createModalVNode } from '@/utils/createVNode';
12 import { promiseToBoolean } from '@/utils';
13
14 const { loading, setLoading } = useLoading(false);
15
16 const tableRef = ref();
17 const filter = ref({ brokerName: '', userName: '', projectName: '', activityName: '', status: '', read_status: '' });
18
19 const statusOption = [
20 { value: 0, label: '发送中' },
21 { value: 1, label: '已发送' },
22 { value: 2, label: '已撤销' },
23 { value: -1, label: '发送失败' },
24 ];
25
26 const readStatusOption = [
27 { value: 0, label: '未查看' },
28 { value: 1, label: '已查看' },
29 ];
30
31 const queryFilter = computed((): QueryForParams => {
32 return {
33 ...filter.value,
34 setWith: ['activity:id,song_name', 'project:id,name', 'user:id,nick_name,identity', 'broker:id,nick_name,identity'],
35 sortBy: 'id',
36 sortType: 'desc',
37 };
38 });
39
40 const onQuery = async (params: AnyObject) => {
41 setLoading(true);
42 return usePushMatchRecordApi.get({ ...queryFilter.value, ...params }).finally(() => setLoading(false));
43 };
44
45 const onSearch = () => tableRef.value?.onPageChange(1);
46
47 const onReset = () => {
48 filter.value = { brokerName: '', userName: '', projectName: '', activityName: '', status: '', read_status: '' };
49 onSearch();
50 };
51
52 onMounted(() => onReset());
53
54 const onExport = () => usePushMatchRecordApi.getExport('运营经纪人-合作通知日志', queryFilter.value);
55
56 const onView = (row: TableData) =>
57 createModalVNode(
58 () =>
59 createFormVNode({ model: row, autoLabelWidth: true }, [
60 createFormItemVNode({ label: '通知标题' }, createVNode(Input, { modelValue: row.title })),
61 createFormItemVNode(
62 { label: '通知内容', rowClass: 'mb-0' },
63 createVNode(Textarea, { modelValue: row.content, autoSize: { minRows: 6, maxRows: 12 } })
64 ),
65 ]),
66 {
67 title: '查看',
68 titleAlign: 'center',
69 hideCancel: true,
70 okText: '我知道了',
71 escToClose: true,
72 }
73 );
74
75 const onRollback = (row: TableData) =>
76 createModalVNode(`确认撤回发送通知《${row.title}》`, {
77 title: '撤销',
78 onBeforeOk: () => promiseToBoolean(usePushMatchRecordApi.rollback(row.id)),
79 onOk: () => tableRef.value?.onFetch(),
80 });
81
82 const onSend = (row: TableData) =>
83 createModalVNode(`确认立即发送通知《${row.title}》`, {
84 title: '更新操作',
85 onBeforeOk: () => promiseToBoolean(usePushMatchRecordApi.send(row.id)),
86 onOk: () => tableRef.value?.onFetch(),
87 });
88 </script>
89
90 <template>
91 <filter-search :loading="loading" :model="filter" @search="onSearch" @reset="onReset">
92 <filter-search-item label="经纪人">
93 <a-input v-model="filter.brokerName" placeholder="请输入" allow-clear />
94 </filter-search-item>
95 <filter-search-item label="试唱用户">
96 <a-input v-model="filter.userName" placeholder="请输入" allow-clear />
97 </filter-search-item>
98 <filter-search-item label="歌曲厂牌">
99 <a-input v-model="filter.projectName" placeholder="请输入" allow-clear />
100 </filter-search-item>
101 <filter-search-item label="关联歌曲">
102 <a-input v-model="filter.activityName" placeholder="请输入" allow-clear />
103 </filter-search-item>
104 <filter-search-item label="发送状态">
105 <a-select v-model="filter.status" :options="statusOption" placeholder="请选择" allow-clear />
106 </filter-search-item>
107 <filter-search-item label="用户行为">
108 <a-select v-model="filter.read_status" :options="readStatusOption" placeholder="请选择" allow-clear />
109 </filter-search-item>
110 </filter-search>
111 <filter-table ref="tableRef" :loading="loading" :on-query="onQuery">
112 <template #tool>
113 <export-button :on-download="onExport" />
114 </template>
115 <user-table-column data-index="broker_id" title="经纪人" user="broker" show-href :width="120" />
116 <filter-table-column data-index="broker_level" title="经纪人分类" :width="100">
117 <template #default="{ record }: { record: { broker_level: string } }">
118 <span v-if="record.broker_level">{{ record.broker_level }}</span>
119 <span v-else style="color: rgba(44, 44, 44, 0.5)"></span>
120 </template>
121 </filter-table-column>
122 <user-table-column data-index="user_id" title="试唱用户" user="user" show-href :width="120" />
123 <filter-table-column data-index="user_tag" title="试唱用户认证" :width="200" />
124 <filter-table-column data-index="project.name" title="歌曲厂牌" :width="120" />
125 <link-table-column
126 data-index="activity_id"
127 title="关联歌曲"
128 :width="140"
129 :formatter="(record:any) => record.activity?.song_name"
130 :to="(record: any) => $router?.push({ name: 'audition-activity-show', params: { id: record.activity_id } })"
131 />
132 <filter-table-column data-index="content" title="通知内容" :width="240" />
133 <filter-table-column data-index="send_at" title="发送时间" :width="180" />
134 <enum-table-column data-index="status" title="发送状态" :option="statusOption" :width="100" />
135 <filter-table-column data-index="read_at" title="用户行为" :width="100">
136 <template #default="{ record }: { record: { read_at?: string } }">{{ record.read_at ? '已查看' : '未查看' }}</template>
137 </filter-table-column>
138 <space-table-column data-index="operation" title="操作" :width="100">
139 <template #default="{ record }: { record: { status: number } }">
140 <a-link class="link-hover" :hoverable="false" @click="onView(record)">查看</a-link>
141 <a-link v-if="record.status === 1" class="link-hover" :hoverable="false" @click="onRollback(record)">撤销</a-link>
142 <a-link v-if="[-1, 2].includes(record.status)" class="link-hover" :hoverable="false" @click="onSend(record)">
143 {{ record.status === -1 ? '重试' : '发送' }}
144 </a-link>
145 </template>
146 </space-table-column>
147 </filter-table>
148 </template>
149
150 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { fetchEventSource, EventSourceMessage } from '@microsoft/fetch-event-source';
3
4 import {
5 Form,
6 FormItem,
7 Upload,
8 Input,
9 Select,
10 RangePicker,
11 DatePicker,
12 Timeline,
13 TimelineItem,
14 Link,
15 RequestOption,
16 } from '@arco-design/web-vue';
17 import { ref, computed, onMounted, createVNode } from 'vue';
18 import { getToken } from '@/utils/auth';
19 import { createModalVNode } from '@/utils/createVNode';
20 import UserTableItem from '@/views/operation/broker/components/user-table-item.vue';
21 import useConfigApi from '@/http/config';
22 // import { downloadFile } from '@/http/auth';
23 import { first, last, range } from 'lodash';
24 import { isEqual } from '@arco-design/web-vue/es/_utils/is-equal';
25 import dayjs, { OpUnitType } from 'dayjs';
26 import { BrokerUserConfig } from '@/http/broker';
27
28 const pushOption = [
29 { value: 0, label: '不通知' },
30 { value: 1, label: '系统通知' },
31 { value: 2, label: '系统通知+面板提醒' },
32 ];
33
34 const props = defineProps<{
35 initValue?: BrokerUserConfig;
36 disabled?: boolean;
37 disabledUpload?: boolean;
38 disabledType?: boolean;
39 submit?: (value: unknown) => Promise<any>;
40 }>();
41
42 const templateUrl = ref('');
43
44 onMounted(async () => {
45 templateUrl.value = (await useConfigApi.getOne({ identifier: 't8iH6OBLP_WkzP-ILhF48', parent_id: 0 }))?.content || '';
46 });
47
48 const formRef = ref();
49 const formValue = ref<Omit<BrokerUserConfig, 'id' | 'user_id'>>({
50 title: '',
51 begin_at: '',
52 end_at: '',
53 push_type: 0,
54 push_at: '',
55 items: [],
56 ...props.initValue,
57 });
58 const formRule = computed(() => {
59 return {
60 title: [{ required: true, message: '请输入名称' }],
61 begin_at: [{ required: true, message: '请选择生效时间' }],
62 push_type: [{ required: true, message: '请选择推送方式' }],
63 push_at: [{ required: !isEqual(formValue.value.push_type, 0), message: '请选择推送时间' }],
64 };
65 });
66
67 const showTable = ref(!!props.initValue?.items);
68 const timePending = ref<string | false>(false);
69
70 const matchTotal = computed(() => formValue.value?.items?.filter((item) => item?.status === 1)?.length || 0);
71
72 const isFullMatch = computed(() => matchTotal.value !== 0 && formValue.value.items?.length === matchTotal.value);
73
74 const activationTime = computed({
75 get: () => [formValue.value.begin_at, formValue.value.end_at],
76 set: (val) => {
77 formValue.value.begin_at = first(val) || '';
78 formValue.value.end_at = last(val) || '';
79 },
80 });
81
82 const onSubmit = async () => {
83 const error = await formRef.value?.validate();
84 // eslint-disable-next-line no-use-before-define
85 if (timePending.value) {
86 // eslint-disable-next-line prefer-promise-reject-errors
87 return Promise.reject(false);
88 }
89 return error ? Promise.reject(error) : props.submit?.(formValue.value);
90 };
91
92 defineExpose({ onSubmit });
93
94 const isSameTime = (date: dayjs.ConfigType, unit: OpUnitType) => dayjs().isSame(dayjs(date), unit);
95
96 const disabledDate = (current?: Date) => dayjs(current).isBefore(dayjs().subtract(1, 'day'));
97 const disabledTime = (current?: Date) => {
98 return {
99 disabledHours: () => range(0, 23).filter((item) => isSameTime(current, 'day') && item < dayjs().hour()),
100 disabledMinutes: () =>
101 range(0, 59).filter(
102 (item) => isSameTime(current, 'day') && isSameTime(current, 'hour') && item < dayjs().add(5, 'minute').minute()
103 ),
104 };
105 };
106 // const onDownTemplate = () => downloadFile(templateUrl.value, '运营经纪人-经纪人配置');
107
108 const onViewItem = () =>
109 createModalVNode(() => createVNode(UserTableItem, { source: formValue.value.items }), {
110 width: '760px',
111 bodyStyle: { padding: '16px' },
112 footer: false,
113 maskClosable: true,
114 escToClose: true,
115 });
116
117 const finishParse = () => {
118 timePending.value = false;
119 showTable.value = true;
120 };
121
122 const onUpload = async ({ fileItem, onSuccess }: RequestOption) => {
123 const formData = new FormData();
124 formData.append('file', fileItem.file as File);
125 showTable.value = false;
126 timePending.value = '检查表格中的数据...';
127
128 await fetchEventSource(`${import.meta.env.VITE_API_HOST}/admin/system/broker/user-configs/upload`, {
129 method: 'POST',
130 openWhenHidden: true,
131 headers: { Authorization: `Bearer ${getToken()}` },
132 body: formData,
133 onopen() {
134 formValue.value.items = [];
135 onSuccess();
136 return Promise.resolve();
137 },
138 onclose() {
139 throw new Error();
140 },
141 onmessage(ev: EventSourceMessage) {
142 if (ev.event === 'finish') {
143 finishParse();
144 throw new Error('finish');
145 }
146 if (ev.event === 'item') {
147 formValue.value.items?.push({ user_id: ev.id, ...JSON.parse(ev.data) });
148 }
149 },
150 onerror() {
151 finishParse();
152 throw new Error('close');
153 },
154 });
155 };
156 </script>
157
158 <template>
159 <Form
160 ref="formRef"
161 :model="formValue"
162 :rules="formRule"
163 :disabled="disabled as boolean"
164 layout="horizontal"
165 size="small"
166 auto-label-width
167 >
168 <FormItem label="名称" field="title" show-colon>
169 <Input v-model="formValue.title" placeholder="请输入" :max-length="100" show-word-limit />
170 </FormItem>
171 <FormItem label="生效时间" show-colon field="begin_at">
172 <RangePicker
173 v-model="activationTime"
174 style="width: 100%"
175 :time-picker-props="{ defaultValue: ['00:00:00', '23:59:59'] }"
176 :day-start-of-week="1"
177 value-format="YYYY-MM-DD HH:mm:ss"
178 format="YYYY-MM-DD"
179 />
180 </FormItem>
181 <FormItem v-if="!disabledType" label="系统通知" field="push_type" show-colon>
182 <Select v-model="formValue.push_type" :options="pushOption" placeholder="请选择" :disabled="disabledType as boolean" />
183 </FormItem>
184 <FormItem v-if="!disabledType && formValue.push_type" label="推送时间" field="push_at" show-colon>
185 <DatePicker
186 v-model="formValue.push_at"
187 show-time
188 :day-start-of-week="1"
189 :show-now-btn="false"
190 :disabled="disabledType as boolean"
191 :disabled-input="true"
192 :disabled-date="disabledDate"
193 :disabled-time="disabledTime"
194 style="width: 100%"
195 value-format="YYYY-MM-DD HH:mm:ss"
196 format="YYYY-MM-DD HH:mm"
197 />
198 </FormItem>
199 <FormItem row-class="mb-0">
200 <Timeline :pending="timePending">
201 <TimelineItem>
202 下载模版
203 <template #label>
204 <Link :href="templateUrl" :hoverable="false">点击此处下载模板</Link>
205 </template>
206 </TimelineItem>
207 <TimelineItem>
208 在模板中添加要推送的经纪人,并将表格上传
209 <template #label>
210 <Upload accept=".xls,.xlsx" :show-file-list="false" :custom-request="onUpload" :disabled="disabledUpload || !!timePending">
211 <template #upload-button>
212 <Link h :hoverable="false" :disabled="disabledUpload || !!timePending">点击此处上传表格</Link>
213 </template>
214 </Upload>
215 </template>
216 </TimelineItem>
217 <TimelineItem v-if="showTable">
218 <template v-if="isFullMatch">数据检查:全部识别,共 {{ matchTotal }} 名经纪人</template>
219 <template v-else>数据检查:已匹配 {{ matchTotal }} 人,未匹配 {{ formValue.items?.length - matchTotal }} 人!</template>
220 <template #label>
221 <Link :hoverable="false" @click="onViewItem">查看匹配信息</Link>
222 </template>
223 </TimelineItem>
224 </Timeline>
225 </FormItem>
226 </Form>
227 </template>
228
229 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { Input, Select, Table, TableColumn, Form, FormItem, Grid } from '@arco-design/web-vue';
3 import { computed, ref, toRef } from 'vue';
4
5 import EnumTableColumn from '@/components/filter/enum-table-column.vue';
6 import NumberTableColumn from '@/components/filter/number-table-column.vue';
7 import { BrokerUserConfigItem } from '@/http/broker';
8
9 const props = defineProps<{ source: BrokerUserConfigItem[] }>();
10
11 const source = toRef(props, 'source', []);
12
13 const matchOption = [
14 { value: 0, label: '匹配失败' },
15 { value: 1, label: '已匹配' },
16 ];
17
18 const filter = ref({ nick_name: '', identifier: '', status: '' });
19
20 const filterSource = computed((): BrokerUserConfigItem[] => {
21 return source.value
22 ?.filter((item) => filter.value.nick_name?.length === 0 || item.user?.nick_name?.includes(filter.value.nick_name))
23 ?.filter((item) => filter.value.identifier?.length === 0 || item.identifier?.includes(filter.value.identifier))
24 ?.filter((item) => filter.value.status?.length === 0 || item.status?.toString().includes(filter.value.status));
25 });
26 </script>
27
28 <template>
29 <Form :model="filter" size="small" layout="horizontal" auto-label-width style="flex-wrap: unset">
30 <Grid :cols="3" :col-gap="16" :row-gap="16">
31 <FormItem label="经纪人" show-colon>
32 <Input v-model="filter.nick_name" placeholder="请输入" allow-clear />
33 </FormItem>
34 <FormItem label="分类" show-colon>
35 <Input v-model="filter.identifier" placeholder="请输入" allow-clear />
36 </FormItem>
37 <FormItem label="匹配结果" show-colon>
38 <Select v-model="filter.status" :options="matchOption" placeholder="请选择" allow-clear />
39 </FormItem>
40 </Grid>
41 </Form>
42 <Table
43 style="height: 300px"
44 :data="filterSource"
45 size="small"
46 :show-header="true"
47 :bordered="false"
48 :scroll="{ y: 300 }"
49 :virtual-list-props="{ height: 300 }"
50 row-key="user_id"
51 :pagination="false"
52 :table-layout-fixed="true"
53 >
54 <template #columns>
55 <TableColumn title="经纪人ID" data-index="user_id" :width="100" />
56 <TableColumn title="经纪人" data-index="user.nick_name">
57 <template #cell="{ record }">
58 <span v-if="record.user">{{ record.user.nick_name }}</span>
59 <span v-else style="color: rgba(44, 44, 44, 0.5)"></span>
60 </template>
61 </TableColumn>
62 <TableColumn title="分类" data-index="identifier" />
63 <NumberTableColumn title="团队歌手额度" data-index="singer_num" />
64 <EnumTableColumn title="匹配结果" data-index="status" :option="matchOption" :width="120" />
65 </template>
66 </Table>
67 </template>
68
69 <style scoped lang="less"></style>
1 <template>
2 <filter-search :loading="loading" :model="filter" @search="onSearch" @reset="onReset">
3 <filter-search-item label="名称">
4 <a-input v-model="filter.title" placeholder="请输入" />
5 </filter-search-item>
6 <filter-search-item label="创建人">
7 <a-input v-model="filter.userNickName" placeholder="请输入" />
8 </filter-search-item>
9 <filter-search-item label="创建时间">
10 <a-range-picker
11 v-model="filter.createBetween"
12 :time-picker-props="{ defaultValue: ['00:00:00', '23:59:59'] }"
13 :day-start-of-week="1"
14 value-format="YYYY-MM-DD HH:mm:ss"
15 format="YYYY-MM-DD"
16 />
17 </filter-search-item>
18 <filter-search-item label="状态">
19 <a-select v-model="filter.status" :options="statusOption" placeholder="请选择" allow-clear />
20 </filter-search-item>
21 </filter-search>
22 <filter-table ref="tableRef" :loading="loading" :on-query="onQuery">
23 <template #tool>
24 <icon-button v-permission="['operation-broker-user-config-create']" type="primary" icon="plus" label="新增" @click="onCreate" />
25 </template>
26 <filter-table-column title="名称" data-index="title" :width="240" />
27 <filter-table-column title="经纪人(人数)" data-index="match_items_count" :width="140" />
28 <filter-table-column title="开始时间" data-index="begin_at" :width="180" />
29 <filter-table-column title="结束时间" data-index="end_at" :width="180" />
30 <user-table-column title="创建人" data-index="user_id" user="user" show-href :width="140" />
31 <filter-table-column title="创建时间" data-index="created_at" :width="170" />
32 <filter-table-column title="状态" :width="80">
33 <template #default="{ record }: { record: { begin_at: string, end_at: string } }">
34 <template v-if="dayjs(record.begin_at).isBefore(dayjs()) && dayjs(record.end_at).isAfter(dayjs())">应用中</template>
35 <template v-else-if="dayjs(record.end_at).isBefore(dayjs())">已过期</template>
36 <template v-else-if="dayjs(record.begin_at).isAfter(dayjs())">待应用</template>
37 <template v-else>未知状态</template>
38 </template>
39 </filter-table-column>
40 <space-table-column
41 v-if="
42 usePermission().checkPermission([
43 'operation-broker-user-config-show',
44 'operation-broker-user-config-edit',
45 'operation-broker-user-config-delete',
46 ])
47 "
48 title="操作"
49 :width="200"
50 >
51 <template #default="{ record }: { record: { end_at: string } }">
52 <a-link v-permission="['operation-broker-user-config-show']" class="link-hover" :hoverable="false" @click="onShow(record)">
53 查看
54 </a-link>
55 <a-link v-permission="['operation-broker-user-config-edit']" class="link-hover" :hoverable="false" @click="onUpdate(record)">
56 修改
57 </a-link>
58 <a-link
59 v-if="dayjs(record.end_at).isAfter(dayjs())"
60 v-permission="['operation-broker-user-config-edit']"
61 class="link-hover"
62 :hoverable="false"
63 @click="onCreateLevel(record)"
64 >
65 生成等级通知
66 </a-link>
67 <a-link v-permission="['operation-broker-user-config-delete']" class="link-hover" :hoverable="false" @click="onDelete(record)">
68 删除
69 </a-link>
70 </template>
71 </space-table-column>
72 </filter-table>
73 </template>
74
75 <script setup lang="ts">
76 import useLoading from '@/hooks/loading';
77 import { AnyObject, AttributeData } from '@/types/global';
78 import { createVNode, h, onMounted, ref } from 'vue';
79 import { createFormItemVNode, createFormVNode, createModalVNode } from '@/utils/createVNode';
80 import { useUserConfigApi } from '@/http/broker';
81
82 import UserForm from '@/views/operation/broker/components/user-form.vue';
83 import UserTableColumn from '@/components/filter/user-table-column.vue';
84 import SpaceTableColumn from '@/components/filter/space-table-column.vue';
85 import { Alert, DatePicker, Select, TableData } from '@arco-design/web-vue';
86 import { promiseToBoolean } from '@/utils';
87 // eslint-disable-next-line @typescript-eslint/no-unused-vars
88 import dayjs, { OpUnitType } from 'dayjs';
89 import { range } from 'lodash';
90 import usePermission from '@/hooks/permission';
91
92 const { get, show, create, createLevel, update, destroy } = useUserConfigApi;
93
94 const { loading, setLoading } = useLoading(false);
95 const filter = ref({ title: '', userNickName: '', createBetween: [], status: '' });
96 const tableRef = ref();
97
98 const statusOption = [
99 { value: 0, label: '待应用' },
100 { value: 1, label: '应用中' },
101 { value: 2, label: '已过期' },
102 ];
103
104 const onQuery = async (params: AnyObject) => {
105 setLoading(true);
106 return get({
107 ...filter.value,
108 ...params,
109 setWith: ['user:id,nick_name,identity'],
110 setWithCount: ['matchItems'],
111 sortBy: 'begin_at',
112 sortType: 'desc',
113 }).finally(() => setLoading(false));
114 };
115
116 const onSearch = () => tableRef.value?.onPageChange(1);
117
118 const onReset = () => {
119 filter.value = { title: '', userNickName: '', createBetween: [], status: '' };
120 onSearch();
121 };
122
123 onMounted(() => onReset());
124
125 const onCreate = () => {
126 const formRef = ref();
127 createModalVNode(() => createVNode(UserForm, { ref: formRef, submit: (value: AttributeData) => create(value) }), {
128 title: '新增',
129 titleAlign: 'center',
130 width: '540px',
131 bodyStyle: { padding: '24px 20px 0' },
132 onBeforeOk: () => promiseToBoolean(formRef.value?.onSubmit()),
133 onOk: () => tableRef.value?.onFetch(),
134 });
135 };
136
137 const onShow = async (record: TableData) => {
138 const row = await show(record.id);
139
140 createModalVNode(() => h(UserForm, { initValue: row, disabled: true, disabledType: true, disabledUpload: true }), {
141 title: '查看',
142 titleAlign: 'center',
143 width: '540px',
144 bodyStyle: { padding: '24px 20px 0' },
145 });
146 };
147
148 const onUpdate = async (record: TableData) => {
149 const row = await show(record.id);
150 const formRef = ref();
151 createModalVNode(
152 () =>
153 createVNode(UserForm, {
154 ref: formRef,
155 initValue: row,
156 disabledType: true,
157 submit: (value: AttributeData) => update(record.id, value),
158 }),
159 {
160 title: '查看',
161 titleAlign: 'center',
162 width: '540px',
163 bodyStyle: { padding: '24px 20px 0' },
164 onBeforeOk: () => promiseToBoolean(formRef.value?.onSubmit()),
165 onOk: () => tableRef.value?.onFetch(),
166 }
167 );
168 };
169
170 const onDelete = (record: TableData) =>
171 createModalVNode(`删除经纪人配置:${record.title}`, {
172 title: '删除操作',
173 titleAlign: 'center',
174 onBeforeOk: () => promiseToBoolean(destroy(record.id)),
175 onOk: () => tableRef.value?.onFetch(),
176 });
177
178 const isSameTime = (date: dayjs.ConfigType, unit: OpUnitType) => dayjs().isSame(dayjs(date), unit);
179
180 const onCreateLevel = (row: TableData) => {
181 const formValue = ref({ is_alert: 0, publish_at: '' });
182 const alertOption = [
183 { value: 0, label: '否' },
184 { value: 1, label: '是' },
185 ];
186 createModalVNode(
187 () =>
188 createFormVNode({ model: formValue }, [
189 createVNode(
190 Alert,
191 { showIcon: false, banner: true, center: true, style: { marginBottom: '10px' } },
192 '将根据当前配置名单生成待发送等级通知'
193 ),
194 createFormItemVNode(
195 { label: '发送时间', field: 'publish_at', required: true },
196 h(DatePicker, {
197 'style': { width: '100%' },
198 'modelValue': formValue.value.publish_at,
199 'showTime': true,
200 'dayStartOfWeek': 1,
201 'showNowBtn': false,
202 'allowClear': false,
203 'format': 'YYYY-MM-DD HH:mm',
204 'valueFormat': 'YYYY-MM-DD HH:mm:ss',
205 'timePickerProps': { hideDisabledOptions: true },
206 'onUpdate:modelValue': (val: any) => (formValue.value.publish_at = val),
207 'disabledDate': (current?: Date) => dayjs(current).isBefore(dayjs().subtract(1, 'day')),
208 'disabledTime': (current?: Date) => {
209 return {
210 disabledHours: () => range(0, 23).filter((item) => isSameTime(current, 'day') && item < dayjs().hour()),
211 disabledMinutes: () =>
212 range(0, 59).filter(
213 (item) => isSameTime(current, 'day') && isSameTime(current, 'hour') && item < dayjs().add(5, 'minute').minute()
214 ),
215 };
216 },
217 })
218 ),
219 createFormItemVNode(
220 { label: '面板提醒', field: 'is_alert', required: true, rowClass: 'mb-0' },
221 h(Select, {
222 'options': alertOption,
223 'modelValue': formValue.value.is_alert,
224 'onUpdate:modelValue': (val?: unknown) => (formValue.value.is_alert = Number(val)),
225 })
226 ),
227 // createFormItemVNode({ label: '注', rowClass: 'mb-0' }, '将根据当前配置名单生成待发送等级通知'),
228 ]),
229 {
230 title: '生成等级通知',
231 titleAlign: 'center',
232 onBeforeOk: () => promiseToBoolean(createLevel(row.id, formValue.value)),
233 onOk: () => tableRef.value?.onFetch(),
234 }
235 );
236 };
237 </script>
238
239 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { useRouteQuery } from '@vueuse/router';
3 import ConfigTable from '@/views/operation/broker/components/config-table.vue';
4 import UserTable from '@/views/operation/broker/components/user-table.vue';
5 import PushLevelTable from '@/views/operation/broker/components/push-level-table.vue';
6 import PushMatchTable from '@/views/operation/broker/components/push-match-table.vue';
7 import usePermission from '@/hooks/permission';
8 import { computed } from 'vue';
9 import { isArray } from '@/utils/is';
10
11 const checkPermission = (permission: string | string[]): boolean => {
12 permission = isArray(permission) ? permission.map((item) => `operation-broker-${item}`) : `operation-broker-${permission}`;
13 return usePermission().checkPermission(permission);
14 };
15 const hasPermission = computed(() => checkPermission(['user-config', 'push-config', 'level-record', 'confirm-record']));
16
17 const tabKeys = computed(() => {
18 const list = ['user-config', 'push-config', 'level-record', 'confirm-record'];
19 return list.filter((item) => checkPermission(item));
20 });
21
22 const tabKey = useRouteQuery('tabKey', tabKeys.value[0] || '');
23 </script>
24
25 <template>
26 <page-view has-bread has-card>
27 <a-tabs
28 v-if="hasPermission"
29 v-model:active-key="tabKey"
30 type="rounded"
31 size="small"
32 :animation="true"
33 :justify="true"
34 :header-padding="false"
35 :destroy-on-hide="false"
36 :lazy-load="true"
37 >
38 <a-tab-pane v-if="checkPermission('user-config')" key="user-config" title="经纪人配置">
39 <user-table />
40 </a-tab-pane>
41 <a-tab-pane v-if="checkPermission('push-config')" key="push-config" title="推送模版配置">
42 <config-table />
43 </a-tab-pane>
44 <a-tab-pane v-if="checkPermission('level-record')" key="level-record" title="等级通知日志">
45 <push-level-table />
46 </a-tab-pane>
47 <a-tab-pane v-if="checkPermission('confirm-record')" key="confirm-record" title="合作通知日志">
48 <push-match-table />
49 </a-tab-pane>
50 </a-tabs>
51 </page-view>
52 </template>
53
54 <style scoped lang="less"></style>
1 <template>
2 <router-view v-slot="{ Component, route }">
3 <keep-alive :exclude="['notification-create', 'notification-show', 'notification-edit']">
4 <component :is="Component" :key="route.path" />
5 </keep-alive>
6 </router-view>
7 </template>
8
9 <script lang="ts" setup></script>
10
11 <style lang="less" scoped>
12 .container {
13 padding: 0 30px 20px 20px;
14 }
15
16 .operations {
17 display: flex;
18 }
19 </style>
1 <script setup lang="ts">
2 import useLoading from '@/hooks/loading';
3 import { onMounted, ref } from 'vue';
4 import { AnyObject } from '@/types/global';
5 import useActivityApi from '@/http/activity';
6 import { Input, Select, Link, Card } from '@arco-design/web-vue';
7 import NumberTableColumn from '@/components/filter/number-table-column.vue';
8 import EnumTableColumn from '@/components/filter/enum-table-column.vue';
9 import FilterTableColumn from '@/components/filter/table-column.vue';
10 import { Activity } from '@/types/activity';
11
12 const props = defineProps<{ selectKey: number }>();
13 const emits = defineEmits<{ (e: 'onChange', value: { link_id: number; link_name: string }): void }>();
14
15 const { loading, setLoading } = useLoading(false);
16 const { get, statusOption } = useActivityApi;
17
18 const filter = ref({ song_name: '', project_name: '', audit_status: 1, status: 1, setWith: ['project:id,name'] });
19 const tableRef = ref();
20
21 const onQuery = async (params: AnyObject) => {
22 setLoading(true);
23 return get({ ...filter.value, ...params }).finally(() => setLoading(false));
24 };
25
26 const onSearch = () => tableRef.value?.onPageChange(1);
27
28 const onReset = () => {
29 filter.value = { song_name: '', project_name: '', audit_status: 1, status: 1, setWith: ['project:id,name'] };
30 onSearch();
31 };
32
33 onMounted(() => onReset());
34
35 const onClick = (record: Activity) => {
36 emits('onChange', record.id === props.selectKey ? { link_id: 0, link_name: '' } : { link_id: record.id, link_name: record.song_name });
37 };
38 </script>
39
40 <template>
41 <Card :bordered="false">
42 <FilterSearch :loading="loading" :model="filter" :inline="true" @search="onSearch" @reset="onReset">
43 <FilterSearchItem label="歌曲名称">
44 <Input v-model="filter.song_name" allow-clear placeholder="请输入" />
45 </FilterSearchItem>
46 <FilterSearchItem label="厂牌名称">
47 <Input v-model="filter.project_name" allow-clear placeholder="请输入" />
48 </FilterSearchItem>
49 <FilterSearchItem label="状态">
50 <Select v-model="filter.status" :options="statusOption" allow-clear placeholder="请选择" />
51 </FilterSearchItem>
52 </FilterSearch>
53 <FilterTable ref="tableRef" :loading="loading" :on-query="onQuery" :page-size="10">
54 <FilterTableColumn title="歌曲名称" data-index="song_name" :width="200" />
55 <FilterTableColumn title="厂牌名称" data-index="project.name" :width="160" />
56 <NumberTableColumn title="试听人数" data-index="view_count" :dark-value="0" :width="110" />
57 <NumberTableColumn title="收藏人数" data-index="like_count" :dark-value="0" :width="110" />
58 <NumberTableColumn title="提交人数" data-index="submit_work_count" :dark-value="0" :width="110" />
59 <FilterTableColumn title="创建时间" data-index="created_at" :width="100" />
60 <EnumTableColumn title="状态" data-index="status" :option="statusOption" :width="100" />
61 <FilterTableColumn title="操作" :width="100">
62 <template #default="{ record }: { record: Activity }">
63 <Link @click="onClick(record)">{{ record.id !== selectKey ? '选择' : '取消选择' }}</Link>
64 </template>
65 </FilterTableColumn>
66 </FilterTable>
67 </Card>
68 </template>
69
70 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { computed, onBeforeUnmount, ref, shallowRef, watch } from 'vue';
3 import useNotificationApi from '@/http/notification';
4 import ImageUpload from '@/components/image-upload/index.vue';
5 import UserTable from '@/views/operation/notification/components/user-table.vue';
6 import ActivityTable from '@/views/operation/notification/components/activity-table.vue';
7 import ProjectTable from '@/views/operation/notification/components/project-table.vue';
8 import dayjs from 'dayjs';
9 import { range } from 'lodash';
10 import useLoading from '@/hooks/loading';
11 import { onBeforeRouteLeave, useRouter } from 'vue-router';
12
13 // eslint-disable-next-line @typescript-eslint/no-unused-vars
14 import { Editor, Toolbar } from '@wangeditor/editor-for-vue';
15 import { Boot } from '@wangeditor/editor';
16
17 import { Notification } from '@/utils/model';
18
19 import '@/views/operation/notification/module';
20 import PublishUserSelect from '@/views/operation/notification/components/publish-user-select.vue';
21
22 type ModelType = Pick<
23 Notification,
24 | 'title'
25 | 'type'
26 | 'cover'
27 | 'content'
28 | 'rich_content'
29 | 'link_type'
30 | 'link_id'
31 | 'publish_type'
32 | 'publish_at'
33 | 'publish_to'
34 | 'is_alert'
35 > & { publish_count: number; link_name: '' };
36
37 const props = defineProps<{
38 modelValue: ModelType;
39 disabled?: boolean;
40 hideSubmit?: boolean;
41 submit?: (value: unknown) => Promise<unknown>;
42 }>();
43 const emits = defineEmits<{ (e: 'update:modelValue', value: unknown): void }>();
44
45 const { typeOption, publishTypeOption, linkTypeOption, alertOption } = useNotificationApi;
46 const { loading, setLoading } = useLoading(false);
47
48 const formRef = ref();
49 const editorRef = shallowRef();
50 const textareaRef = ref();
51
52 Boot.setEditorConfig({ readOnly: props.disabled });
53
54 const onCreateEditor = (editor: unknown) => {
55 editorRef.value = editor;
56 };
57
58 onBeforeUnmount(() => {
59 const editor = editorRef.value;
60 if (editor == null) return;
61 editor.destroy();
62 });
63
64 const formValue = computed({
65 get: () => props.modelValue,
66 set: (val) => emits('update:modelValue', val),
67 });
68
69 const textareaRow = computed(() => {
70 let row = 3;
71 if (formValue.value.type === 2) {
72 row += 6;
73 }
74 if (formValue.value.publish_type === 2) {
75 row += 2;
76 }
77
78 return { minRows: row, maxRows: row };
79 });
80
81 watch(
82 () => textareaRow.value,
83 () => textareaRef.value?.handleResize()
84 );
85
86 const formRule = computed(() => {
87 return {
88 title: [{ required: true, message: '请输入标题' }],
89 type: [{ required: true, message: '请选择类型' }],
90 content: [{ required: true, message: '请输入内容' }],
91 cover: formValue.value.type === 2 ? [{ required: true, message: '请上传图片' }] : [],
92 link_type: [{ required: true, message: '请选择交互类型' }],
93 link_name: [{ required: ['user', 'activity', 'project'].includes(formValue.value.link_type), message: '请选择交互对象' }],
94 publish_type: [{ required: true, message: '请选择发布设置' }],
95 publish_at: [{ required: formValue.value.publish_type === 2, message: '请选择发布时间' }],
96 publish_to: [{ required: true, message: '请选择通知对象' }],
97 is_alert: [{ required: true, message: '请选择是否弹窗' }],
98 };
99 });
100
101 const onChangeValue = (value: object) => Object.assign(formValue.value, value);
102 const disabledDate = (current?: Date) => dayjs(current).isBefore(dayjs().subtract(1, 'day'));
103 const disabledTime = (current?: Date) => {
104 return {
105 disabledHours: () => range(0, 23).filter((item) => dayjs().isSame(dayjs(current), 'day') && item < dayjs().hour()),
106 disabledMinutes: () => range(0, 59).filter((item) => dayjs().isSame(dayjs(current), 'day') && item < dayjs().minute()),
107 };
108 };
109 const openDate = (visible: boolean) => {
110 if (visible && formValue.value.publish_at?.length === 0) {
111 formValue.value.publish_at = dayjs().format('YYYY-MM-DD hh:mm:ss');
112 }
113 };
114
115 const router = useRouter();
116 const onBack = () => router.replace({ name: 'operation-notification' });
117 const onSubmit = () => {
118 setLoading(true);
119 props.submit?.(formValue.value)?.finally(() => setLoading(false));
120 };
121
122 onBeforeRouteLeave((to, from) => {
123 if (from.meta.from === to.name) {
124 to.meta.reload = false;
125 }
126 });
127 </script>
128
129 <template>
130 <a-form
131 ref="formRef"
132 :disabled="disabled || loading"
133 label-align="right"
134 :model="formValue"
135 :rules="formRule"
136 :auto-label-width="true"
137 :label-col-props="{ span: 6 }"
138 :wrapper-col-props="{ span: 18 }"
139 @submit-success="onSubmit"
140 >
141 <a-row :gutter="16">
142 <a-col :span="24" style="margin-bottom: 8px">
143 <a-alert banner style="margin-bottom: 8px">
144 关联对象: {{ formValue.link_type === 'rich' ? 'H5页面' : formValue.link_name || '无' }}, 通知人数:{{ formValue.publish_count }}
145
146
147 <template #action>
148 <a-button v-if="!hideSubmit" type="primary" :loading="loading" html-type="submit">发布</a-button>
149 <a-button style="margin-left: 8px" @click="onBack">{{ hideSubmit ? '返回' : '取消' }}</a-button>
150 </template>
151 </a-alert>
152 </a-col>
153 <a-col :span="12">
154 <a-form-item label="通知标题" field="title" show-colon>
155 <a-input v-model="formValue.title" placeholder="请输入" :max-length="30" show-word-limit />
156 </a-form-item>
157 <a-form-item label="通知对象" field="publish_to" show-colon>
158 <publish-user-select v-model="formValue.publish_to" v-model:count="formValue.publish_count" :max-tag-count="4" />
159 </a-form-item>
160 <a-form-item label="通知正文" field="content" show-colon>
161 <a-textarea ref="textareaRef" v-model="formValue.content" :auto-size="textareaRow" :max-length="200" show-word-limit />
162 </a-form-item>
163 </a-col>
164 <a-col :span="8">
165 <a-form-item label="类型" field="type" show-colon>
166 <a-select v-model="formValue.type" :options="typeOption" placeholder="请选择" @change="() => onChangeValue({ cover: '' })" />
167 </a-form-item>
168 <a-form-item v-show="formValue.type === 2" label="图片" field="cover" show-colon>
169 <image-upload v-model="formValue.cover" :width="331" :height="118" fit="fill" />
170 </a-form-item>
171 <a-form-item label="弹窗提醒" field="is_alert" show-colon>
172 <a-select v-model="formValue.is_alert" :options="alertOption" placeholder="请选择" />
173 </a-form-item>
174 <a-form-item label="发布设置" field="publish_type" show-colon>
175 <a-select
176 v-model.number="formValue.publish_type"
177 :options="publishTypeOption"
178 placeholder="请选择"
179 @change="(value:number) => (value === 1 ? (formValue.publish_at = '') : void 0)"
180 />
181 </a-form-item>
182 <a-form-item v-show="formValue.publish_type === 2" label="发布时间" field="publish_at" show-colon>
183 <a-date-picker
184 v-model="formValue.publish_at"
185 style="width: 100%"
186 show-time
187 value-format="YYYY-MM-DD HH:mm:ss"
188 format="YYYY-MM-DD HH:mm"
189 :disabled-date="disabledDate"
190 :disabled-time="disabledTime"
191 :day-start-of-week="1"
192 @popup-visible-change="openDate"
193 />
194 </a-form-item>
195 <a-form-item label="点击交互" field="link_type" show-colon>
196 <a-select
197 v-model="formValue.link_type"
198 :options="linkTypeOption"
199 placeholder="请选择"
200 @change="() => onChangeValue({ link_id: 0, link_name: '', rich_content: '' })"
201 />
202 </a-form-item>
203 </a-col>
204 <a-col :span="4" />
205 </a-row>
206 </a-form>
207
208 <template v-if="formValue.link_type !== 'none'">
209 <a-divider style="margin-top: 8px" />
210 <user-table
211 v-if="!disabled && formValue.link_type === 'user'"
212 :body-style="{ padding: 0 }"
213 :select-key="formValue.link_id"
214 @on-change="onChangeValue"
215 />
216 <activity-table
217 v-if="!disabled && formValue.link_type === 'activity'"
218 :body-style="{ padding: 0 }"
219 :select-key="formValue.link_id"
220 @on-change="onChangeValue"
221 />
222 <project-table
223 v-if="!disabled && formValue.link_type === 'project'"
224 :body-style="{ padding: 0 }"
225 :select-key="formValue.link_id"
226 @on-change="onChangeValue"
227 />
228 <a-card v-if="formValue.link_type === 'rich'" :body-style="{ padding: 0 }" style="border: 1px solid #ccc">
229 <Toolbar style="border-bottom: 1px solid #ccc" :editor="editorRef" />
230 <a-layout>
231 <a-layout-content>
232 <Editor v-model="formValue.rich_content" style="min-height: 400px" @on-created="onCreateEditor" />
233 </a-layout-content>
234 <a-layout-sider class="rich" :width="400">
235 <!-- eslint-disable vue/no-v-html -->
236 <div class="content" v-html="formValue.rich_content" />
237 <!--eslint-enable-->
238 </a-layout-sider>
239 </a-layout>
240 </a-card>
241 </template>
242 </template>
243
244 <style scoped lang="less">
245 .rich {
246 box-shadow: unset;
247 margin-left: 10px;
248 border-left: 1px solid #ccc;
249
250 .content {
251 overflow-y: auto;
252 //height: 400px;
253 padding: 0 10px;
254
255 :deep(img) {
256 height: auto !important;
257 max-width: 100%;
258 object-fit: contain;
259 }
260
261 :deep(video) {
262 max-width: 100%;
263 height: auto !important;
264 display: block;
265 margin: 0 auto;
266 }
267 }
268 }
269 </style>
1 <script setup lang="ts">
2 import useLoading from '@/hooks/loading';
3 import useNotificationApi from '@/http/notification';
4 import { onMounted, ref } from 'vue';
5 import UserTableColumn from '@/components/filter/user-table-column.vue';
6 import useUserApi from '@/http/user';
7 import EnumTableColumn from '@/components/filter/enum-table-column.vue';
8 // eslint-disable-next-line @typescript-eslint/no-unused-vars
9 import { User } from '@/utils/model';
10 import { Input, Select } from '@arco-design/web-vue';
11 import FilterSearch from '@/components/filter/search.vue';
12 import FilterSearchItem from '@/components/filter/search-item.vue';
13 import { Option } from '@/types/global';
14 import useTagApi from '@/http/Tag';
15
16 const props = defineProps<{ notifyKey: number }>();
17
18 const { user } = useNotificationApi;
19 const { sexOption, officialStatusOption } = useUserApi;
20
21 const statusOption = [
22 { value: 0, label: '待处理' },
23 { value: 1, label: '处理中' },
24 { value: 2, label: '已发送' },
25 { value: 3, label: '已撤销' },
26 { value: -1, label: '发送失败' },
27 ];
28
29 const readStatusOption = [
30 { value: 0, label: '未查看' },
31 { value: 1, label: '已查看' },
32 ];
33
34 const { loading, setLoading } = useLoading(false);
35
36 const tableRef = ref();
37 const filter = ref({ nick_name: '', authIds: [], status: '', read_status: '', setWith: ['authTags:id,name'] });
38 const authTagOptions = ref<Option[]>([]);
39
40 const onQuery = async (params: object) => {
41 setLoading(true);
42
43 return user(props.notifyKey, { ...filter.value, ...params }).finally(() => setLoading(false));
44 };
45
46 const onSearch = () => tableRef.value?.onPageChange(1);
47
48 const onReset = () => {
49 filter.value = { nick_name: '', authIds: [], status: '', read_status: '', setWith: ['authTags:id,name'] };
50 onSearch();
51 };
52
53 onMounted(() => {
54 tableRef.value?.onPageChange();
55 useTagApi.getOption({ type: 4 }).then((data) => (authTagOptions.value = data));
56 });
57 </script>
58
59 <template>
60 <FilterSearch :loading="loading" :model="filter" :split="3" @search="onSearch" @reset="onReset">
61 <FilterSearchItem label="用户艺名">
62 <Input v-model="filter.nick_name" placeholder="请输入" />
63 </FilterSearchItem>
64 <FilterSearchItem label="认证能力">
65 <Select
66 v-model="filter.authIds"
67 placeholder="请选择"
68 :options="authTagOptions"
69 :max-tag-count="2"
70 multiple
71 allow-clear
72 allow-search
73 />
74 </FilterSearchItem>
75 <FilterSearchItem label="发送状态">
76 <Select v-model="filter.status" placeholder="请选择" :options="statusOption" allow-clear />
77 </FilterSearchItem>
78 <FilterSearchItem label="用户行为">
79 <Select v-model="filter.read_status" placeholder="请选择" :options="readStatusOption" allow-clear />
80 </FilterSearchItem>
81 </FilterSearch>
82 <FilterTable ref="tableRef" :loading="loading" :on-query="onQuery">
83 <UserTableColumn title="用户艺名" data-index="id" :width="260" show-avatar />
84 <EnumTableColumn title="性别" data-index="sex" :option="sexOption" :dark-value="0" :width="80" />
85 <FilterTableColumn title="认证能力" :width="280">
86 <template #default="{ record }: { record: User }">
87 <span v-if="record.auth_tags?.length">{{ record.auth_tags?.map((item) => item.name).join('|') }}</span>
88 <span v-else style="color: rgba(44, 44, 44, 0.5)"></span>
89 </template>
90 </FilterTableColumn>
91 <EnumTableColumn title="关注服务号" data-index="official_status" :option="officialStatusOption" :dark-value="0" :width="160" />
92 <EnumTableColumn title="发送状态" data-index="status" :option="statusOption" :width="100" />
93 <FilterTableColumn title="接收时间" data-index="send_at" :width="160" />
94 <FilterTableColumn title="用户行为" data-index="read_at" :width="120">
95 <template #default="{ record }: { record: { read_at?: string } }">
96 <span v-if="record.read_at">已查看</span>
97 <span v-else style="color: rgba(44, 44, 44, 0.5)">未查看</span>
98 </template>
99 </FilterTableColumn>
100 </FilterTable>
101 </template>
102
103 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import useLoading from '@/hooks/loading';
3 import { onMounted, ref } from 'vue';
4 import { AnyObject } from '@/types/global';
5 import { Card, Input, Link, Select } from '@arco-design/web-vue';
6 import NumberTableColumn from '@/components/filter/number-table-column.vue';
7 import EnumTableColumn from '@/components/filter/enum-table-column.vue';
8 import FilterTableColumn from '@/components/filter/table-column.vue';
9 import useProjectApi from '@/http/project';
10 import { Project } from '@/utils/model';
11
12 const props = defineProps<{ selectKey: number }>();
13 const emits = defineEmits<{ (e: 'onChange', value: { link_id: number; link_name: string }): void }>();
14
15 const { loading, setLoading } = useLoading(false);
16 const { get, promoteStatusOption } = useProjectApi;
17
18 const filter = ref({ name: '', is_promote: '', status: 1, setWith: ['master:id,nick_name'] });
19 const tableRef = ref();
20
21 const onQuery = async (params: AnyObject) => {
22 setLoading(true);
23 return get({ ...filter.value, ...params }).finally(() => setLoading(false));
24 };
25
26 const onSearch = () => tableRef.value?.onPageChange(1);
27
28 const onReset = () => {
29 filter.value = { name: '', is_promote: '', status: 1, setWith: ['master:id,nick_name'] };
30 onSearch();
31 };
32
33 onMounted(() => onReset());
34
35 const onClick = (record: Project) => {
36 emits('onChange', record.id === props.selectKey ? { link_id: 0, link_name: '' } : { link_id: record.id, link_name: record.name });
37 };
38 </script>
39
40 <template>
41 <Card :bordered="false">
42 <FilterSearch :loading="loading" :model="filter" :inline="true" @search="onSearch" @reset="onReset">
43 <FilterSearchItem label="厂牌名称">
44 <Input v-model="filter.name" allow-clear placeholder="请输入" />
45 </FilterSearchItem>
46 <FilterSearchItem label="确认分享人">
47 <Select v-model="filter.is_promote" :options="promoteStatusOption" allow-clear placeholder="请输入" />
48 </FilterSearchItem>
49 </FilterSearch>
50 <FilterTable ref="tableRef" :loading="loading" :on-query="onQuery" :page-size="10">
51 <FilterTableColumn title="厂牌名称" data-index="name" :width="200" />
52 <FilterTableColumn title="简介" data-index="intro" :width="240" />
53 <EnumTableColumn title="确认分享人" data-index="is_promote" :option="promoteStatusOption" :dark-value="0" :width="130" />
54 <NumberTableColumn title="上架歌曲数" data-index="activity_up_count" :dark-value="0" :width="110" />
55 <NumberTableColumn title="已匹配歌曲数" data-index="activity_match_count" :dark-value="0" :width="110" />
56 <NumberTableColumn title="已发行歌曲数" data-index="activity_send_count" :dark-value="0" :width="110" />
57 <FilterTableColumn title="主理人" data-index="master.nick_name" :width="160" />
58 <FilterTableColumn title="操作" :width="100">
59 <template #default="{ record }: { record: Project }">
60 <Link @click="onClick(record)">{{ record.id !== selectKey ? '选择' : '取消选择' }}</Link>
61 </template>
62 </FilterTableColumn>
63 </FilterTable>
64 </Card>
65 </template>
66
67 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { computed, onMounted, ref } from 'vue';
3 import useTagApi from '@/http/Tag';
4 import { TreeNodeData } from '@arco-design/web-vue';
5 import { pull, startsWith, truncate } from 'lodash';
6 import useUserApi from '@/http/user';
7
8 const publishTo = defineModel<string[]>({ default: () => [] });
9 const publishCount = defineModel<number>('count', { default: 1 });
10
11 const cancelSource = new AbortController();
12
13 const queryCount = (role: string[]) => {
14 if (cancelSource.signal.aborted) {
15 cancelSource?.abort?.();
16 }
17 if (role.length > 0) {
18 useUserApi.get({ identityRole: role, status: [0, 1], page: 1, pageSize: 1 }, { signal: cancelSource.signal }).then(({ meta }) => {
19 publishCount.value = meta.total;
20 });
21 } else {
22 publishCount.value = 0;
23 }
24 };
25
26 const parentKey = { broker: 'captain', manager: 'project' };
27
28 const handleExtCheckTrue = (key: string, parentKey: string) => {
29 if (!publishTo.value.includes(key)) {
30 publishTo.value.push(key);
31 if (!publishTo.value.includes(parentKey)) {
32 publishTo.value.push(parentKey);
33 }
34 }
35 };
36
37 const handleExtCheckFalse = (key: string, parentKey: string) => {
38 pull(publishTo.value, key);
39 if (!publishTo.value.includes(parentKey)) {
40 publishTo.value.push(parentKey);
41 }
42 };
43
44 const handleExtCheck = (key: string, value: boolean) => {
45 // eslint-disable-next-line no-unused-expressions
46 value ? handleExtCheckTrue(key, parentKey[key]) : handleExtCheckFalse(key, parentKey[key]);
47 queryCount(publishTo.value);
48 };
49
50 const brokerValue = computed({
51 get: (): boolean => publishTo.value.includes('broker'),
52 set: (value: boolean) => handleExtCheck('broker', value),
53 });
54
55 const managerValue = computed({
56 get: (): boolean => publishTo.value.includes('manager'),
57 set: (value: boolean) => handleExtCheck('manager', value),
58 });
59
60 const treeValue = computed({
61 get: () => publishTo.value.filter((item) => !['broker', 'manager'].includes(item)),
62 set: (value) => {
63 if (!value.includes('project')) {
64 pull(publishTo.value, 'manager');
65 }
66 if (!value.includes('captain')) {
67 pull(publishTo.value, 'broker');
68 }
69 publishTo.value = [...value, ...publishTo.value?.filter((item) => ['broker', 'manager'].includes(item))];
70 },
71 });
72
73 const userAuthTags = ref<TreeNodeData[]>([]);
74
75 const options = computed((): TreeNodeData[] => {
76 return [
77 { key: 'visitor', title: '注册未认证' },
78 { key: 'musician', title: '音乐人', children: userAuthTags.value },
79 { key: 'captain', title: '队长' },
80 { key: 'project', title: '厂牌管理员' },
81 { key: 'system', title: '平台管理员' },
82 ];
83 });
84
85 onMounted(() => {
86 useTagApi.get({ type: 4, fetchType: 'all', setColumn: ['id', 'name'] }).then(({ data }) => {
87 userAuthTags.value = data.map((item) => ({ key: `musician_tag_${item.id}`, title: item.name }));
88 });
89 });
90 </script>
91
92 <template>
93 <a-tree-select
94 v-model="treeValue"
95 :multiple="true"
96 :allow-clear="true"
97 :tree-checkable="true"
98 tree-checked-strategy="parent"
99 :fallback-option="false"
100 :data="options"
101 placeholder="请选择"
102 @change="(value:any) => queryCount([...value,...publishTo?.filter((item) => ['broker', 'manager'].includes(item))])"
103 >
104 <template #label="{ data }">
105 <template v-if="data.value === 'captain' && brokerValue">经纪人</template>
106 <template v-else-if="data.value === 'project' && managerValue">厂牌主理人</template>
107 <template v-else-if="startsWith(data.value, 'musician_tag_')">音乐人 / {{ truncate(data.label, { length: 6 }) }}</template>
108 <template v-else>{{ data.label }}</template>
109 </template>
110 <template #tree-slot-extra="row">
111 <a-checkbox v-if="row.key === 'captain'" v-model="brokerValue">仅通知经纪人</a-checkbox>
112 <a-checkbox v-if="row.key === 'project'" v-model="managerValue">仅通知主理人</a-checkbox>
113 </template>
114 </a-tree-select>
115 </template>
116
117 <style>
118 .arco-tree-node:hover {
119 color: var(--color-text-1) !important;
120 background-color: var(--color-fill-2) !important;
121 }
122 </style>
1 <script setup lang="ts">
2 import FilterSearch from '@/components/filter/search.vue';
3 import FilterSearchItem from '@/components/filter/search-item.vue';
4 import FilterTable from '@/components/filter/table.vue';
5 import FilterTableColumn from '@/components/filter/table-column.vue';
6 import EnumTableColumn from '@/components/filter/enum-table-column.vue';
7 import UserTableColumn from '@/components/filter/user-table-column.vue';
8 import PhoneTableColumn from '@/components/filter/phone-table-column.vue';
9 import { Card, Input, Select, Link } from '@arco-design/web-vue';
10
11 import useUserApi from '@/http/user';
12 import useLoading from '@/hooks/loading';
13 import { AnyObject, Option } from '@/types/global';
14 import { onMounted, ref } from 'vue';
15 import useTagApi from '@/http/Tag';
16 import { User } from '@/utils/model';
17
18 const props = defineProps<{ selectKey: number }>();
19 const emits = defineEmits<{ (e: 'onChange', value: { link_id: number; link_name: string }): void }>();
20
21 const { get } = useUserApi;
22 const { loading, setLoading } = useLoading(false);
23 const { sexOption, officialStatusOption } = useUserApi;
24
25 const filter = ref({ nick_name: '', email_like: '', phone_like: '', authIds: [], status: 1, setWith: ['authTags:id,name'] });
26 const tableRef = ref();
27 const authTagOptions = ref<Option[]>([]);
28
29 const onQuery = async (params: AnyObject) => {
30 setLoading(true);
31 return get({ ...filter.value, ...params }).finally(() => setLoading(false));
32 };
33
34 const onSearch = () => tableRef.value?.onPageChange(1);
35
36 const onReset = () => {
37 filter.value = { nick_name: '', email_like: '', phone_like: '', authIds: [], status: 1, setWith: ['authTags:id,name'] };
38 onSearch();
39 };
40
41 onMounted(() => {
42 onReset();
43 useTagApi.getOption({ type: 4 }).then((data) => (authTagOptions.value = data));
44 });
45
46 const onClick = (record: User) => {
47 emits('onChange', record.id === props.selectKey ? { link_id: 0, link_name: '' } : { link_id: record.id, link_name: record.nick_name });
48 };
49 </script>
50
51 <template>
52 <Card :bordered="false">
53 <FilterSearch :loading="loading" :model="filter" :inline="true" :split="4" @search="onSearch" @reset="onReset">
54 <FilterSearchItem label="用户艺名">
55 <Input v-model="filter.nick_name" placeholder="请输入" />
56 </FilterSearchItem>
57 <FilterSearchItem label="用户邮箱">
58 <Input v-model="filter.email_like" placeholder="请输入" />
59 </FilterSearchItem>
60 <FilterSearchItem label="手机号码">
61 <Input v-model="filter.phone_like" placeholder="请输入" />
62 </FilterSearchItem>
63 <FilterSearchItem label="认证能力">
64 <Select
65 v-model="filter.authIds"
66 placeholder="请选择"
67 :options="authTagOptions"
68 :max-tag-count="2"
69 multiple
70 allow-clear
71 allow-search
72 />
73 </FilterSearchItem>
74 </FilterSearch>
75 <FilterTable ref="tableRef" :loading="loading" :on-query="onQuery" :page-size="10">
76 <UserTableColumn title="用户艺名" data-index="id" :width="260" show-avatar />
77 <EnumTableColumn title="性别" data-index="sex" :option="sexOption" :dark-value="0" :width="100" />
78 <FilterTableColumn title="用户邮箱" data-index="email" :width="260" />
79 <PhoneTableColumn title="手机号码" data-index="phone" area-index="area_code" :width="260" />
80 <FilterTableColumn title="认证能力">
81 <template #default="{ record }: { record: User }">
82 <span v-if="record.auth_tags?.length">{{ record.auth_tags?.map((item) => item.name).join('|') }}</span>
83 <span v-else style="color: rgba(44, 44, 44, 0.5)"></span>
84 </template>
85 </FilterTableColumn>
86 <EnumTableColumn title="关注服务号" data-index="official_status" :option="officialStatusOption" :dark-value="0" :width="160" />
87 <FilterTableColumn title="操作" :width="100">
88 <template #default="{ record }: { record: User }">
89 <Link @click="onClick(record)">{{ record.id !== selectKey ? '选择' : '取消选择' }}</Link>
90 </template>
91 </FilterTableColumn>
92 </FilterTable>
93 </Card>
94 </template>
95
96 <style scoped lang="less"></style>
1 <script setup lang="ts" name="notification-create">
2 import { ref } from 'vue';
3 import FormContent from '@/views/operation/notification/components/form-content.vue';
4 import { Message } from '@arco-design/web-vue';
5 import { onBeforeRouteLeave, useRouter } from 'vue-router';
6 import useNotificationApi from '@/http/notification';
7 import { AttributeData } from '@/types/global';
8
9 const { create } = useNotificationApi;
10
11 const formValue = ref({
12 title: '',
13 type: 1,
14 content: '',
15 cover: '',
16 publish_type: 1,
17 publish_at: '',
18 publish_to: [],
19 publish_count: 0,
20 link_type: 'none',
21 link_id: 0,
22 link_name: '',
23 rich_content: '',
24 is_alert: 0,
25 }) as any;
26
27 const router = useRouter();
28
29 const onSubmit = (value: AttributeData) =>
30 create(formValue.value).then(() => {
31 Message.success(`添加通知:${value.title}`);
32 router.replace({ name: 'operation-notification' });
33 });
34
35 onBeforeRouteLeave((to, from) => {
36 if (from.meta.from === to.name) {
37 to.meta.reload = false;
38 }
39 });
40 </script>
41
42 <template>
43 <page-view has-bread has-card>
44 <form-content v-model="formValue" :submit="onSubmit" />
45 </page-view>
46 </template>
47
48 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import useLoading from '@/hooks/loading';
3 import { computed, onActivated, ref } from 'vue';
4 import useNotificationApi from '@/http/notification';
5 import { AnyObject } from '@/types/global';
6 import EnumTableColumn from '@/components/filter/enum-table-column.vue';
7 import UserTableColumn from '@/components/filter/user-table-column.vue';
8 import { useRoute, useRouter } from 'vue-router';
9 import NumberTableColumn from '@/components/filter/number-table-column.vue';
10 import { createModalVNode } from '@/utils/createVNode';
11 import { Notification } from '@/utils/model';
12 import { promiseToBoolean } from '@/utils';
13 import SpaceTableColumn from '@/components/filter/space-table-column.vue';
14 import usePermission from '@/hooks/permission';
15
16 const { loading, setLoading } = useLoading(false);
17 const { typeOption, statusOption, get, send, cancel, rollback } = useNotificationApi;
18
19 const filter = ref({ title: '', userNickName: '', type: '', status: '', createBetween: [], setWith: ['user:id,nick_name'] });
20 const tableRef = ref();
21 const router = useRouter();
22 const onQuery = async (params: AnyObject) => {
23 setLoading(true);
24 return get({ ...filter.value, ...params, sortBy: 'created_at', sortType: 'desc' }).finally(() => setLoading(false));
25 };
26
27 const onSearch = () => tableRef.value?.onPageChange(1);
28
29 const onReset = () => {
30 filter.value = { title: '', userNickName: '', type: '', status: '', createBetween: [], setWith: ['user:id,nick_name'] };
31 onSearch();
32 };
33
34 const onShow = (record: Notification) => router.push({ name: 'operation-notification-show', params: { id: record.id } });
35
36 const onCreate = () => router.push({ name: 'operation-notification-create' });
37
38 const onUpdate = (record: Notification) => router.push({ name: 'operation-notification-update', params: { id: record.id } });
39
40 const onSend = (record: Notification) =>
41 createModalVNode(`确认立即发送通知《${record.title}》`, {
42 title: '更新操作',
43 onBeforeOk: () => promiseToBoolean(send(record.id)),
44 onOk: () => tableRef.value?.onFetch(),
45 });
46
47 const onCancel = (record: Notification) =>
48 createModalVNode(`确认取消发送通知《${record.title}》`, {
49 title: '取消操作',
50 onBeforeOk: () => promiseToBoolean(cancel(record.id)),
51 onOk: () => tableRef.value?.onFetch(),
52 });
53
54 onActivated(() => (useRoute().meta.reload ? onReset() : tableRef.value?.onFetch()));
55
56 const onRollback = (record: Notification) =>
57 createModalVNode(`确认撤回发送通知《${record.title}》`, {
58 title: '撤回操作',
59 onBeforeOk: () => promiseToBoolean(rollback(record.id)),
60 onOk: () => tableRef.value?.onFetch(),
61 });
62
63 const hasOperationPermission = computed(() => {
64 return usePermission().checkPermission(['operation-notification-show', 'operation-notification-edit', 'operation-notification-send']);
65 });
66 </script>
67
68 <template>
69 <page-view has-bread has-card>
70 <filter-search :loading="loading" :model="filter" @search="onSearch" @reset="onReset">
71 <filter-search-item label="通知标题">
72 <a-input v-model="filter.title" placeholder="请输入" allow-clear />
73 </filter-search-item>
74 <filter-search-item label="创建人">
75 <a-input v-model="filter.userNickName" placeholder="请输入" allow-clear />
76 </filter-search-item>
77 <filter-search-item label="类型">
78 <a-select v-model="filter.type" :options="typeOption" placeholder="请选择" allow-clear />
79 </filter-search-item>
80 <filter-search-item field="createBetween" label="创建时间">
81 <a-range-picker v-model="filter.createBetween" show-time :time-picker-props="{ defaultValue: ['00:00:00', '23:59:59'] }" />
82 </filter-search-item>
83 <filter-search-item label="状态">
84 <a-select v-model="filter.status" :options="statusOption" placeholder="请选择" allow-clear />
85 </filter-search-item>
86 </filter-search>
87 <filter-table ref="tableRef" :loading="loading" :on-query="onQuery">
88 <template #tool>
89 <icon-button v-permission="['operation-notification-create']" type="primary" icon="plus" label="新增" @click="onCreate" />
90 </template>
91 <filter-table-column title="通知标题" data-index="title" :width="260" />
92 <enum-table-column title="类型" data-index="type" :option="typeOption" :width="100" />
93 <number-table-column title="接收人数" data-index="items_count" :width="120" />
94 <number-table-column title="查看人数" data-index="read_count" :width="120" />
95 <user-table-column title="创建人" data-index="user_id" user="user" :width="140" />
96 <filter-table-column title="创建时间" data-index="created_at" :width="160" />
97 <filter-table-column title="发送时间" data-index="publish_at" :width="160" />
98 <enum-table-column title="状态" data-index="status" :option="statusOption" :width="100" />
99 <space-table-column v-if="hasOperationPermission" title="操作" :width="120">
100 <template #default="{ record }: { record: Notification }">
101 <a-link
102 v-if="[-1, 0, 1, 2, 3].includes(record.status)"
103 v-permission="['operation-notification-show']"
104 :hoverable="false"
105 class="link-hover"
106 @click="onShow(record)"
107 >
108 查看
109 </a-link>
110 <a-link
111 v-if="[-2, 3].includes(record.status)"
112 v-permission="['operation-notification-edit']"
113 :hoverable="false"
114 class="link-hover"
115 @click="onUpdate(record)"
116 >
117 修改
118 </a-link>
119 <a-link
120 v-if="record.status === 2"
121 v-permission="['operation-notification-send']"
122 :hoverable="false"
123 class="link-hover"
124 @click="onRollback(record)"
125 >
126 撤回
127 </a-link>
128 <a-link
129 v-if="[-1, 0].includes(record.status)"
130 v-permission="['operation-notification-send']"
131 :hoverable="false"
132 class="link-hover"
133 @click="onSend(record)"
134 >
135 {{ record.status === -1 ? '重试' : '发送' }}
136 </a-link>
137 <a-link
138 v-if="record.status === 0"
139 v-permission="['operation-notification-send']"
140 :hoverable="false"
141 class="link-hover"
142 @click="onCancel(record)"
143 >
144 取消
145 </a-link>
146 </template>
147 </space-table-column>
148 </filter-table>
149 </page-view>
150 </template>
151
152 <style scoped lang="less"></style>
1 import { Boot, DomEditor, IButtonMenu, IDomEditor, SlateElement, SlateText, SlateTransforms } from '@wangeditor/editor';
2 import { createVNode, defineAsyncComponent } from 'vue';
3 import { Node } from 'slate';
4 import { Modal } from '@arco-design/web-vue';
5 import { h, VNode } from 'snabbdom';
6 import { Activity } from '@/types/activity';
7
8 type ActivityCardElement = { type: 'activity-card'; id: number; song_name: string; cover: string; tags: string[]; children: SlateText[] };
9
10 const MENU = {
11 KEY: 'activity-card',
12 TITLE: '活动',
13 ICON: '<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="currentColor" class="arco-icon arco-icon-music" stroke-width="4" stroke-linecap="square" stroke-linejoin="miter" filter="" data-v-249840b0="" style="font-size: 32px;"><path d="M15 37a4 4 0 1 0-8 0 4 4 0 0 0 8 0Zm0 0V18.5M41 37a4 4 0 1 0-8 0 4 4 0 0 0 8 0Zm0 0V16.5m-26 2V9.926a1 1 0 0 1 .923-.997l24-1.846A1 1 0 0 1 41 8.08v8.42m-26 2 26-2"></path></svg>',
14 };
15
16 const DELETE_ICON = 'https://hi-sing-cdn.hikoon.com/image/20230818/tajefbycbk71692325471868yswxs35mag.png';
17
18 function withCard<T extends IDomEditor>(editor: T) {
19 const { isVoid, isInline } = editor;
20
21 const newEditor = editor;
22
23 newEditor.isVoid = (elem) => {
24 const type = DomEditor.getNodeType(elem);
25 if (type === MENU.KEY) {
26 return true;
27 }
28
29 return isVoid(elem);
30 };
31
32 newEditor.isInline = (elem) => {
33 const type = DomEditor.getNodeType(elem);
34 if (type === MENU.KEY) {
35 return true;
36 }
37
38 return isInline(elem);
39 };
40
41 return newEditor;
42 }
43
44 function createCard(editor: IDomEditor, callback: () => void) {
45 const ActivityTable = defineAsyncComponent(() => import('@/views/operation/banner/components/activity-table.vue'));
46 return createVNode(ActivityTable, {
47 onCheckRow: (value: Activity) => {
48 const { id, song_name, cover, tags } = value;
49 editor.insertNode({
50 type: MENU.KEY,
51 id,
52 song_name,
53 cover,
54 tags: tags?.map((item) => item.name) || [],
55 children: [{ text: '' }],
56 } as Node);
57 callback();
58 },
59 });
60 }
61
62 class ActivityCard implements IButtonMenu {
63 title = MENU.TITLE;
64
65 iconSvg = MENU.ICON;
66
67 tag = 'button';
68
69 getValue(): boolean {
70 return false;
71 }
72
73 isActive(): boolean {
74 return false;
75 }
76
77 isDisabled(): boolean {
78 return false;
79 }
80
81 exec(editor: IDomEditor) {
82 const modal = Modal.open({
83 title: MENU.TITLE,
84 content: () => createCard(editor, () => modal?.close()),
85 width: '700px',
86 footer: false,
87 closable: true,
88 escToClose: true,
89 // @ts-ignore
90 bodyStyle: { padding: '16px' },
91 });
92 }
93 }
94
95 const menuConf = { key: MENU.KEY, factory: () => new ActivityCard() };
96
97 const renderElemConf = {
98 type: MENU.KEY,
99 renderElem: (elem: SlateElement, children: VNode[] | null, editor: IDomEditor): VNode => {
100 const { song_name, cover, tags } = elem as ActivityCardElement;
101
102 return h(
103 'span',
104 {
105 props: { contentEditable: false },
106 style: {
107 display: 'flex',
108 alignItems: 'center',
109 justifyContent: 'space-between',
110 background: '#f2f2f2',
111 padding: '8px',
112 border: '1px solid rgb(229,230,235)',
113 boxShadow: '0 2px 5px 0 rgba(0, 0, 0, 0.08)',
114 maxWidth: '400px',
115 },
116 },
117 [
118 h('img', {
119 props: { src: cover, alt: '' },
120 style: { width: '44px', height: '44px', borderRadius: '2pt' },
121 }),
122 h(
123 'span',
124 {
125 style: {
126 flex: '1',
127 display: 'flex',
128 alignItems: 'flex-start',
129 flexDirection: 'column',
130 margin: '0 6px',
131 fontWeight: '500',
132 },
133 },
134 [
135 h(
136 'span',
137 {
138 props: { contentEditable: false },
139 style: {
140 overflow: 'hidden',
141 whiteSpace: 'nowrap',
142 textOverflow: 'ellipsis',
143 lineHeight: '26px',
144 fontWeight: '500',
145 color: '#4E5969',
146 },
147 },
148 song_name
149 ),
150 h(
151 'span',
152 {
153 style: { display: 'flex', justifyContent: 'flex-start', alignItems: 'flex-end', lineHeight: '16px' },
154 },
155 tags?.map((item) =>
156 h(
157 'span',
158 {
159 style: {
160 borderColor: 'transparent',
161 display: 'inline-flex',
162 alignItems: 'center',
163 boxSizing: 'border-box',
164 height: '16px',
165 padding: '0 4px',
166 color: 'rgb(237, 85, 14)',
167 fontWeight: '500',
168 fontSize: '10px',
169 lineHeight: '10px',
170 verticalAlign: 'middle',
171 border: '1px solid rgb(237, 85, 14 ,0.6)',
172 borderRadius: '2px',
173 overflow: 'hidden',
174 whiteSpace: 'nowrap',
175 textOverflow: 'ellipsis',
176 marginRight: '4px',
177 },
178 },
179 item
180 )
181 ) || ''
182 ),
183 ]
184 ),
185 h('img', {
186 props: { src: DELETE_ICON, contentEditable: false },
187 style: { width: '22px', visibility: editor.isDisabled() ? 'hidden' : 'visibility' },
188 on: {
189 click() {
190 // eslint-disable-next-line func-names
191 setTimeout(function () {
192 const node = DomEditor.getSelectedNodeByType(editor, MENU.KEY);
193 if (node) {
194 const path = DomEditor.findPath(editor, node);
195 SlateTransforms.removeNodes(editor, { at: path });
196 }
197 }, 150);
198 },
199 },
200 }),
201 ]
202 );
203 },
204 };
205
206 const parseHtmlConf = {
207 selector: `span[data-w-e-type="${MENU.KEY}"]`,
208 parseElemHtml: (domElem: Element): SlateElement => {
209 const id = domElem.getAttribute('data-id') || 0;
210 const song_name = domElem.getAttribute('data-song-name') || '';
211 const cover = domElem.getAttribute('data-cover') || '';
212 const tags = domElem.getAttribute('data-tags')?.split(',') || [];
213 return { type: MENU.KEY, id, song_name, cover, tags, children: [{ text: '' }] } as SlateElement;
214 },
215 };
216
217 const elemToHtmlConf = {
218 type: MENU.KEY,
219 elemToHtml: (elem: SlateElement) => {
220 const { KEY } = MENU;
221 const { id, song_name, cover, tags } = elem as ActivityCardElement;
222
223 return `<span data-w-e-type="${KEY}" data-w-e-is-void data-w-e-is-inline style="display: flex;align-items:center;justify-content: space-between;background: #f2f2f2;padding: 8px;border:1px solid rgb(229,230,235);box-shadow:0 2px 5px 0 rgba(0, 0, 0, 0.08);margin: 4px;height: 70px;" data-id="${id}" data-song-name="${song_name}" data-cover="${cover}" data-tags="${tags.toString()}">
224 <img src="${cover}" style="width: 44px !important;height: 44px !important;object-fit:fill;border-radius:2pt" alt="" />
225 <span style="flex: 1;margin:0 6px;display: flex;align-items: flex-start;flex-direction: column;">
226 <span style="line-height:26px;text-overflow: ellipsis;white-space: nowrap;overflow: hidden;font-weight: 500;color: #4E5969">${song_name}</span>
227 <span style="line-height:16px;display: flex;justify-content: flex-start;align-items: flex-end;">
228 ${
229 tags
230 ?.map(
231 (item) =>
232 `<span style="display: inline-flex;align-items: center;box-sizing: border-box;height: 16px;padding: 0 4px;color: rgb(237, 85, 14);font-weight: 500;font-size: 10px;line-height: 10px;vertical-align: middle;border: 1px solid rgb(237, 85, 14 ,0.6);border-radius: 2px;overflow: hidden;white-space: nowrap;text-overflow: ellipsis;margin-right: 4px">${item}</span>`
233 )
234 .join('') || ''
235 }
236 </span>
237 </span>
238 <img src="https://hi-sing-cdn.hikoon.com/image/20230822/sjzhgel3pwo1692672120063rjd093t6xd.png" style="width: 22px" alt="" />
239 </span>`;
240 },
241 };
242
243 Boot.registerModule({
244 editorPlugin: withCard,
245 renderElems: [renderElemConf],
246 elemsToHtml: [elemToHtmlConf],
247 parseElemsHtml: [parseHtmlConf],
248 menus: [menuConf],
249 });
1 import '@/views/operation/notification/module/user-card';
2 import '@/views/operation/notification/module/project-card';
3 import '@/views/operation/notification/module/activity-card';
4 import '@wangeditor/editor/dist/css/style.css';
5 import { Boot } from '@wangeditor/editor';
6 import useOss from '@/hooks/oss';
7
8 type InsertImageFnType = (url: string, alt: string, href: string) => void;
9 type InsertVideoFnType = (url: string, poster: string) => void;
10
11 Boot.setToolbarConfig({
12 excludeKeys: ['fullScreen', 'codeBlock', 'undo', 'redo'],
13 insertKeys: { index: 30, keys: ['user-card', 'activity-card', 'project-card'] },
14 });
15
16 Boot.setEditorConfig({
17 MENU_CONF: {
18 uploadImage: {
19 allowedFileTypes: ['image/*'],
20 async customUpload(file: File, insertFn: InsertImageFnType) {
21 return useOss()
22 .upload(file, 'image')
23 .then(({ name, url }) => insertFn(url, name, url));
24 },
25 },
26 uploadVideo: {
27 allowedFileTypes: ['video/*'],
28 maxNumberOfFiles: 1,
29 maxFileSize: 1024 * 1024 * 1024,
30 timeout: 60 * 1000,
31 async customUpload(file: File, insertFn: InsertVideoFnType) {
32 return useOss()
33 .upload(file, 'video')
34 .then(({ url }) => insertFn(url, `${url}?x-oss-process=video/snapshot,t_1000,f_png`));
35 },
36 },
37 },
38 });
1 import { Boot, DomEditor, IButtonMenu, IDomEditor, SlateElement, SlateText, SlateTransforms } from '@wangeditor/editor';
2 import { createVNode, defineAsyncComponent } from 'vue';
3 import { Modal } from '@arco-design/web-vue';
4 import { h, VNode } from 'snabbdom';
5 import { Node } from 'slate';
6
7 type ProjectCardElement = { type: 'project-card'; id: number; name: string; cover: string; children: SlateText[] };
8
9 const MENU = {
10 KEY: 'project-card',
11 TITLE: '厂牌',
12 ICON: '<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="currentColor" class="arco-icon arco-icon-home" stroke-width="4" stroke-linecap="square" stroke-linejoin="miter" filter="" data-v-249840b0="" style="font-size: 32px;"><path d="M7 17 24 7l17 10v24H7V17Z"></path><path d="M20 28h8v13h-8V28Z"></path></svg>',
13 };
14
15 const DELETE_ICON = 'https://hi-sing-cdn.hikoon.com/image/20230818/tajefbycbk71692325471868yswxs35mag.png';
16
17 function withCard<T extends IDomEditor>(editor: T) {
18 const { isVoid, isInline } = editor;
19
20 const newEditor = editor;
21
22 newEditor.isVoid = (elem) => {
23 const type = DomEditor.getNodeType(elem);
24 if (type === MENU.KEY) {
25 return true;
26 }
27
28 return isVoid(elem);
29 };
30
31 newEditor.isInline = (elem) => {
32 const type = DomEditor.getNodeType(elem);
33 if (type === MENU.KEY) {
34 return true;
35 }
36
37 return isInline(elem);
38 };
39
40 return newEditor;
41 }
42
43 function createCard(editor: IDomEditor, callback: () => void) {
44 const ProjectTable = defineAsyncComponent(() => import('@/views/operation/banner/components/project-table.vue'));
45 return createVNode(ProjectTable, {
46 onCheck: (value: any) => {
47 const { link_id, link_name, cover } = value;
48 editor.insertNode({ type: MENU.KEY, id: link_id, name: link_name, cover, children: [{ text: '' }] } as Node);
49 callback();
50 },
51 });
52 }
53
54 class ProjectCard implements IButtonMenu {
55 title = MENU.TITLE;
56
57 iconSvg = MENU.ICON;
58
59 tag = 'button';
60
61 getValue(): boolean {
62 return false;
63 }
64
65 isActive(): boolean {
66 return false;
67 }
68
69 isDisabled(): boolean {
70 return false;
71 }
72
73 exec(editor: IDomEditor) {
74 const modal = Modal.open({
75 title: MENU.TITLE,
76 content: () => createCard(editor, () => modal?.close()),
77 width: '700px',
78 footer: false,
79 closable: true,
80 escToClose: true,
81 // @ts-ignore
82 bodyStyle: { padding: '16px' },
83 });
84 }
85 }
86
87 const menuConf = { key: MENU.KEY, factory: () => new ProjectCard() };
88
89 const renderElemConf = {
90 type: MENU.KEY,
91 renderElem: (elem: SlateElement, children: VNode[] | null, editor: IDomEditor): VNode => {
92 const { name, cover } = elem as ProjectCardElement;
93
94 return h(
95 'span',
96 {
97 props: { contentEditable: false },
98 style: {
99 display: 'flex',
100 alignItems: 'center',
101 justifyContent: 'space-between',
102 background: '#f2f2f2',
103 padding: '8px',
104 border: '1px solid rgb(229,230,235)',
105 boxShadow: '0 2px 5px 0 rgba(0, 0, 0, 0.08)',
106 maxWidth: '400px',
107 },
108 },
109 [
110 h('img', {
111 props: { src: cover, alt: '' },
112 style: { width: '48px', height: '48px', borderRadius: '2pt' },
113 }),
114 h(
115 'span',
116 {
117 props: { contentEditable: false },
118 style: {
119 flex: '1',
120 margin: '0 6px',
121 overflow: 'hidden',
122 whiteSpace: 'nowrap',
123 textOverflow: 'ellipsis',
124 fontWeight: '500',
125 color: '#4E5969',
126 },
127 },
128 name
129 ),
130 h('img', {
131 props: { src: DELETE_ICON, contentEditable: false },
132 style: { width: '22px', visibility: editor.isDisabled() ? 'hidden' : 'visibility' },
133 on: {
134 click() {
135 // eslint-disable-next-line func-names
136 setTimeout(function () {
137 const node = DomEditor.getSelectedNodeByType(editor, MENU.KEY);
138 if (node) {
139 const path = DomEditor.findPath(editor, node);
140 SlateTransforms.removeNodes(editor, { at: path });
141 }
142 }, 150);
143 },
144 },
145 }),
146 ]
147 );
148 },
149 };
150
151 const parseHtmlConf = {
152 selector: `span[data-w-e-type="${MENU.KEY}"]`,
153 parseElemHtml: (domElem: Element): SlateElement => {
154 const id = domElem.getAttribute('data-id') || 0;
155 const name = domElem.getAttribute('data-name') || '';
156 const cover = domElem.getAttribute('data-cover') || '';
157 return { type: MENU.KEY, id, name, cover, children: [{ text: '' }] } as SlateElement;
158 },
159 };
160
161 const elemToHtmlConf = {
162 type: MENU.KEY,
163 elemToHtml: (elem: SlateElement) => {
164 const { id, name, cover } = elem as ProjectCardElement;
165 return `<span style="display: flex;align-items:center;justify-content: space-between;background: #f2f2f2;padding: 8px;border:1px solid rgb(229,230,235);box-shadow:0 2px 5px 0 rgba(0, 0, 0, 0.08);margin: 4px" data-w-e-type="${MENU.KEY}" data-w-e-is-void data-w-e-is-inline data-id="${id}" data-name="${name}" data-cover="${cover}">
166 <img src="${cover}" style="width: 3em !important;height: 3em !important;object-fit:fill;border-radius:2pt" alt="" />
167 <span style="text-overflow: ellipsis;white-space: nowrap;overflow: hidden;flex: 1;margin: 0 6px;font-weight: 500;color: #4E5969">${name}</span>
168 <img src="https://hi-sing-cdn.hikoon.com/image/20230822/sjzhgel3pwo1692672120063rjd093t6xd.png" style="width: 22px" alt="" />
169 </span>`;
170 },
171 };
172
173 Boot.registerModule({
174 editorPlugin: withCard,
175 renderElems: [renderElemConf],
176 elemsToHtml: [elemToHtmlConf],
177 parseElemsHtml: [parseHtmlConf],
178 menus: [menuConf],
179 });
1 import { Boot, DomEditor, IButtonMenu, IDomEditor, SlateElement, SlateText, SlateTransforms } from '@wangeditor/editor';
2
3 import { h, VNode } from 'snabbdom';
4 import { createVNode, defineAsyncComponent } from 'vue';
5 import { Node } from 'slate';
6 import { Modal } from '@arco-design/web-vue';
7
8 const MENU = {
9 KEY: 'user-card',
10 TITLE: '用户',
11 ICON: '<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="currentColor" class="arco-icon arco-icon-user" stroke-width="4" stroke-linecap="butt" stroke-linejoin="miter"><path d="M7 37c0-4.97 4.03-8 9-8h16c4.97 0 9 3.03 9 8v3a1 1 0 0 1-1 1H8a1 1 0 0 1-1-1v-3Z"></path><circle cx="24" cy="15" r="8"></circle></svg>',
12 };
13
14 const DELETE_ICON = 'https://hi-sing-cdn.hikoon.com/image/20230818/tajefbycbk71692325471868yswxs35mag.png';
15
16 type UserCardElement = { type: 'user-card'; id: number; nick_name: string; avatar: string; children: SlateText[] };
17
18 function withCard<T extends IDomEditor>(editor: T) {
19 const { isVoid, isInline } = editor;
20
21 const newEditor = editor;
22
23 newEditor.isVoid = (elem) => {
24 const type = DomEditor.getNodeType(elem);
25 if (type === MENU.KEY) {
26 return true;
27 }
28
29 return isVoid(elem);
30 };
31
32 newEditor.isInline = (elem) => {
33 const type = DomEditor.getNodeType(elem);
34 if (type === MENU.KEY) {
35 return true;
36 }
37
38 return isInline(elem);
39 };
40
41 return newEditor;
42 }
43
44 function createCard(editor: IDomEditor, callback?: () => void) {
45 const UserTable = defineAsyncComponent(() => import('@/views/operation/banner/components/user-table.vue'));
46
47 return createVNode(UserTable, {
48 onCheck: (value: any) => {
49 const { link_id, link_name, cover } = value;
50 editor.insertNode({ type: MENU.KEY, id: link_id, nick_name: link_name, avatar: cover, children: [{ text: '' }] } as Node);
51 callback?.();
52 },
53 });
54 }
55
56 class UserCard implements IButtonMenu {
57 title = MENU.TITLE;
58
59 iconSvg = MENU.ICON;
60
61 tag = 'button';
62
63 getValue(): boolean {
64 return false;
65 }
66
67 isActive(): boolean {
68 return false;
69 }
70
71 isDisabled(): boolean {
72 return false;
73 }
74
75 // 点击菜单时触发的函数
76 exec(editor: IDomEditor) {
77 const modal = Modal.open({
78 title: MENU.TITLE,
79 content: () => createCard(editor, () => modal?.close()),
80 width: '700px',
81 footer: false,
82 closable: true,
83 escToClose: true,
84 // @ts-ignore
85 bodyStyle: { padding: '16px' },
86 });
87 }
88 }
89
90 const menuConf = { key: MENU.KEY, factory: () => new UserCard() };
91
92 const renderElemConf = {
93 type: MENU.KEY,
94 renderElem: (elem: SlateElement, children: VNode[] | null, editor: IDomEditor): VNode => {
95 const { nick_name, avatar } = elem as UserCardElement;
96
97 return h(
98 'span',
99 {
100 props: { contentEditable: false },
101 style: {
102 display: 'flex',
103 alignItems: 'center',
104 justifyContent: 'space-between',
105 background: '#f2f2f2',
106 padding: '8px',
107 border: '1px solid rgb(229,230,235)',
108 boxShadow: '0 2px 5px 0 rgba(0, 0, 0, 0.08)',
109 maxWidth: '400px',
110 },
111 },
112 [
113 h('img', {
114 props: { src: avatar, alt: '' },
115 style: { width: '48px', height: '48px', borderRadius: '50%' },
116 }),
117 h(
118 'span',
119 {
120 props: { contentEditable: false },
121 style: {
122 flex: '1',
123 margin: '0 6px',
124 overflow: 'hidden',
125 whiteSpace: 'nowrap',
126 textOverflow: 'ellipsis',
127 fontWeight: '500',
128 color: '#4E5969',
129 },
130 },
131 nick_name
132 ),
133 h('img', {
134 props: { src: DELETE_ICON, contentEditable: false },
135 style: { width: '22px', visibility: editor.isDisabled() ? 'hidden' : 'visibility' },
136 on: {
137 click() {
138 // eslint-disable-next-line func-names
139 setTimeout(function () {
140 const node = DomEditor.getSelectedNodeByType(editor, MENU.KEY);
141 if (node) {
142 const path = DomEditor.findPath(editor, node);
143 SlateTransforms.removeNodes(editor, { at: path });
144 }
145 }, 150);
146 },
147 },
148 }),
149 ]
150 );
151 },
152 };
153
154 const parseHtmlConf = {
155 selector: `span[data-w-e-type="${MENU.KEY}"]`,
156 parseElemHtml: (domElem: Element): SlateElement => {
157 const id = domElem.getAttribute('data-id') || 0;
158 const nick_name = domElem.getAttribute('data-nick-name') || '';
159 const avatar = domElem.getAttribute('data-avatar') || '';
160 return { type: MENU.KEY, id, nick_name, avatar, children: [{ text: '' }] } as SlateElement;
161 },
162 };
163
164 const elemToHtmlConf = {
165 type: MENU.KEY,
166 elemToHtml: (elem: SlateElement) => {
167 const { id, nick_name, avatar } = elem as UserCardElement;
168 return `<span style="display: flex;align-items:center;justify-content: space-between;background: #f2f2f2;padding: 8px;border:1px solid rgb(229,230,235);box-shadow:0 2px 5px 0 rgba(0, 0, 0, 0.08);margin: 4px" data-w-e-type="${MENU.KEY}" data-w-e-is-void data-w-e-is-inline data-id="${id}" data-nick-name="${nick_name}" data-avatar="${avatar}">
169 <img src="${avatar}" style="width: 3em !important;height: 3em !important;object-fit:fill;border-radius:50%" alt="" />
170 <span style="text-overflow: ellipsis;white-space: nowrap;overflow: hidden;flex: 1;margin: 0 6px;font-weight: 500;color: #4E5969">${nick_name}</span>
171 <img src="https://hi-sing-cdn.hikoon.com/image/20230822/sjzhgel3pwo1692672120063rjd093t6xd.png" style="width: 22px" alt="" />
172 </span>`;
173 },
174 };
175
176 Boot.registerModule({
177 editorPlugin: withCard,
178 renderElems: [renderElemConf],
179 elemsToHtml: [elemToHtmlConf],
180 parseElemsHtml: [parseHtmlConf],
181 menus: [menuConf],
182 });
1 <script setup lang="ts" name="notification-show">
2 import { useRouteQuery } from '@vueuse/router';
3 import FormContent from '@/views/operation/notification/components/form-content.vue';
4 import { onBeforeRouteLeave, useRoute } from 'vue-router';
5 import useNotificationApi from '@/http/notification';
6 import { onMounted, ref } from 'vue';
7 import { pick } from 'lodash';
8 import ItemTable from '@/views/operation/notification/components/item-table.vue';
9
10 const { show } = useNotificationApi;
11 const notifyKey = Number(useRoute().params?.id);
12
13 const tabKey = useRouteQuery('tabKey', '1');
14 const formValue = ref<any>({});
15
16 onBeforeRouteLeave((to, from) => {
17 if (from.meta.from === to.name) {
18 to.meta.reload = false;
19 }
20 });
21
22 onMounted(() => {
23 show(notifyKey).then((res) => {
24 Object.assign(formValue.value, {
25 ...pick(res, [
26 'title',
27 'type',
28 'cover',
29 'content',
30 'rich_content',
31 'publish_type',
32 'publish_to',
33 'link_type',
34 'link_id',
35 'link_name',
36 'is_alert',
37 ]),
38 publish_count: res.items_count || 0,
39 publish_at: res.publish_type === 1 ? '' : res.publish_at,
40 });
41 });
42 });
43 </script>
44
45 <template>
46 <page-view has-bread has-card>
47 <a-tabs v-model:active-key="tabKey" type="rounded" :animation="true" size="small" :justify="true">
48 <a-tab-pane key="1" title="通知表单">
49 <form-content :model-value="formValue" :hide-submit="true" :disabled="true" />
50 </a-tab-pane>
51 <a-tab-pane key="2" title="接收统计">
52 <item-table :notify-key="notifyKey" />
53 </a-tab-pane>
54 </a-tabs>
55 </page-view>
56 </template>
57
58 <style scoped lang="less"></style>
1 <script setup lang="ts" name="notification-update">
2 import FormContent from '@/views/operation/notification/components/form-content.vue';
3 import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router';
4 import { onMounted, ref } from 'vue';
5 import { pick } from 'lodash';
6 import useNotificationApi from '@/http/notification';
7 import { Message } from '@arco-design/web-vue';
8 import { AttributeData } from '@/types/global';
9
10 const router = useRouter();
11
12 const notifyKey = Number(useRoute().params?.id);
13 const { show, update } = useNotificationApi;
14
15 const formValue = ref<any>({});
16
17 const onSubmit = (value: AttributeData) =>
18 update(notifyKey, value).then(() => {
19 Message.success(`更新通知:${value.title}`);
20 router.replace({ name: 'operation-notification' });
21 });
22
23 onBeforeRouteLeave((to, from) => {
24 if (from.meta.from === to.name) {
25 to.meta.reload = false;
26 }
27 });
28
29 onMounted(() => {
30 show(notifyKey).then((res) => {
31 Object.assign(formValue.value, {
32 ...pick(res, [
33 'title',
34 'type',
35 'cover',
36 'content',
37 'rich_content',
38 'publish_type',
39 'publish_to',
40 'link_type',
41 'link_id',
42 'link_name',
43 'is_alert',
44 ]),
45 publish_count: res.items_count || 0,
46 publish_at: res.publish_type === 1 ? '' : res.publish_at,
47 });
48 });
49 });
50 </script>
51
52 <template>
53 <page-view has-bread has-card>
54 <form-content v-model="formValue" :submit="onSubmit" />
55 </page-view>
56 </template>
57
58 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { Form, FormItem, Input, InputGroup, InputNumber, Select, Button, Cascader, Textarea } from '@arco-design/web-vue';
3 import InputUpload from '@/components/input-upload/index.vue';
4 import { useVModel } from '@vueuse/core';
5 import { computed, onMounted, ref } from 'vue';
6 import { FormInstance } from '@arco-design/web-vue/es/form';
7 import { SystemConfig } from '@/types/system-config';
8 import { nanoid } from 'nanoid';
9 import useConfigApi from '@/http/config';
10 import { get, set } from 'lodash';
11 import { arrayToTree } from '@/utils';
12
13 const props = defineProps<{ modelValue: SystemConfig; hideKeyBtn?: boolean; lockField?: string[]; submit?: () => Promise<any> }>();
14 const emit = defineEmits(['update:modelValue']);
15
16 const isLock = (field: string): boolean => props.lockField !== undefined && props.lockField?.indexOf(field) !== -1;
17
18 const formRef = ref<FormInstance>();
19 const formValue = useVModel(props, 'modelValue', emit);
20 const formRule = {
21 'name': [{ required: true, message: '请输入名称' }],
22 'identifier': [{ required: true, message: '请输入唯一标识' }],
23 'status': [{ required: true, message: '请选择状态' }],
24 'expand.type': [{ required: true, message: '请选择类型' }],
25 };
26
27 const treeOption = ref<SystemConfig[]>([]);
28 const treeOptionField = { value: 'id', label: 'name', children: 'children' };
29
30 const onSubmit = async () => {
31 const error = await formRef.value?.validate();
32 return error ? Promise.reject(error) : props.submit?.();
33 };
34
35 defineExpose({ onSubmit });
36
37 onMounted(() => {
38 useConfigApi
39 .get({ setColumn: ['id', 'name', 'parent_id'], fetchType: 'all', parent_id: '', sortBy: 'weight', sortType: 'desc' })
40 .then(({ data }) => set(treeOption, 'value', [{ id: 0, name: '顶级' }, ...arrayToTree(data, 0, { removeEmpty: true })]));
41 });
42
43 const contentType = computed({
44 get: () => get(formValue.value, 'expand.type', 'none'),
45 set: (value) => set(formValue.value, 'expand.type', value),
46 });
47 </script>
48
49 <template>
50 <Form ref="formRef" :model="formValue" :rules="formRule" auto-label-width>
51 <FormItem label="挂载点" field="parent_id" show-colon :disabled="true">
52 <Cascader v-model="formValue.parent_id" :options="treeOption" :field-names="treeOptionField" :check-strictly="true" />
53 </FormItem>
54 <FormItem label="名称" field="name" show-colon>
55 <Input v-model="formValue.name" placeholder="请输入" />
56 </FormItem>
57 <FormItem label="标识" field="identifier" show-colon :disabled="isLock('identifier')">
58 <InputGroup style="width: 100%">
59 <Input v-model="formValue.identifier" placeholder="请输入" />
60 <Button v-if="!hideKeyBtn" @click="() => set(formValue, 'identifier', nanoid())">生成</Button>
61 </InputGroup>
62 </FormItem>
63 <FormItem label="状态" field="status" show-colon>
64 <Select v-model="formValue.status" :options="useConfigApi.statusOptions" placeholder="请选择" />
65 </FormItem>
66 <FormItem label="权重" field="weight" show-colon>
67 <InputNumber v-model="formValue.weight" :min="0" :max="200" :step="1" />
68 </FormItem>
69 <FormItem label="类型" field="expand.type" show-colon>
70 <Cascader v-model="contentType" :options="useConfigApi.contentOptions" @change="() => set(formValue, 'content', '')" />
71 </FormItem>
72 <FormItem
73 v-if="contentType !== 'none'"
74 label="内容"
75 field="content"
76 :show-colon="true"
77 :rules="[{ required: true, message: '缺少内容' }]"
78 >
79 <InputUpload v-if="contentType === 'image'" v-model="formValue.content" accept="image/*" />
80 <InputUpload v-if="contentType === 'audio'" v-model="formValue.content" accept="audio/*" />
81 <InputUpload v-if="contentType === 'video'" v-model="formValue.content" accept="video/*" />
82 <InputUpload v-if="contentType === 'file'" v-model="formValue.content" accept="*" />
83 <Input v-if="contentType === 'input'" v-model="formValue.content" :max-length="64" placeholder="请输入" />
84 <Textarea
85 v-if="contentType === 'textarea'"
86 v-model="formValue.content"
87 placeholder="请输入"
88 :auto-size="{ minRows: 4, maxRows: 4 }"
89 :max-length="800"
90 :show-word-limit="true"
91 />
92 </FormItem>
93 <FormItem label="备注" field="remark" :show-colon="true">
94 <Textarea v-model="formValue.remark" :auto-size="{ minRows: 4, maxRows: 4 }" :max-length="400" :show-word-limit="true" />
95 </FormItem>
96 </Form>
97 </template>
98
99 <style scoped lang="less"></style>
1 <template>
2 <page-view has-card has-bread>
3 <filter-table ref="tableRef" :loading="loading" :on-query="onQuery" :default-expand-all-rows="true" :hide-expand-button-on-empty="true">
4 <template #tool>
5 <IconButton v-permission="['system-config-create']" icon="plus" label="新增" type="primary" @click="onCreateTopVNode()" />
6 </template>
7 <filter-table-column title="名称" data-index="name" :width="240" />
8 <filter-table-column title="标识(KEY)" data-index="identifier" :width="140" />
9 <filter-table-column title="内容" data-index="content" :width="140" />
10 <filter-table-column title="备注" data-index="remark" :width="160" />
11 <enum-table-column title="状态" data-index="status" :option="useConfigApi.statusOptions" :width="64" />
12 <number-table-column title="权重值" data-index="weight" :width="80" />
13 <filter-table-column title="创建时间" data-index="created_at" :width="140" />
14 <space-table-column
15 v-if="usePermission().checkPermission(['system-config-create', 'system-config-edit'])"
16 title="操作"
17 data-index="operation"
18 :width="120"
19 >
20 <template #default="{ record }">
21 <a-link v-permission="['system-config-create']" class="link-hover" :hoverable="false" @click="onCreateChild(record)">新增</a-link>
22 <a-link v-permission="['system-config-edit']" class="link-hover" :hoverable="false" @click="onUpdate(record)">编辑</a-link>
23 <a-link
24 v-permission="['system-config-edit']"
25 class="link-hover"
26 :hoverable="false"
27 @click="onChangeStatus(record, Number(!record.status))"
28 >
29 {{ useConfigApi.statusOptions.find((item) => item.value === Number(!record.status))?.label }}
30 </a-link>
31 </template>
32 </space-table-column>
33 </filter-table>
34 </page-view>
35 </template>
36
37 <script lang="ts" setup>
38 import { createVNode, onMounted, ref } from 'vue';
39 import useLoading from '@/hooks/loading';
40 import useConfigApi from '@/http/config';
41 import { AnyObject } from '@/types/global';
42 import NumberTableColumn from '@/components/filter/number-table-column.vue';
43 import EnumTableColumn from '@/components/filter/enum-table-column.vue';
44 import SpaceTableColumn from '@/components/filter/space-table-column.vue';
45 import FormContent from '@/views/system/config/components/form-content.vue';
46
47 import { createModalVNode } from '@/utils/createVNode';
48 import { Message, TableData } from '@arco-design/web-vue';
49 import useRoleApi from '@/http/role';
50 import { promiseToBoolean } from '@/utils';
51 import usePermission from '@/hooks/permission';
52
53 const { loading, setLoading } = useLoading(false);
54 const tableRef = ref();
55
56 onMounted(async () => {
57 tableRef.value?.onPageChange();
58 });
59
60 const onQuery = async (params: AnyObject) => {
61 setLoading(true);
62 return useConfigApi
63 .get({ setWith: ['children'], parent_id: 0, ...params, sortBy: 'weight', sortType: 'desc' })
64 .finally(() => setLoading(false));
65 };
66
67 const onCreateTopVNode = () => {
68 const formRef = ref();
69 const formValue = ref({ parent_id: 0, weight: 0, expand: { type: 'none' } });
70
71 createModalVNode(
72 () => createVNode(FormContent, { ref: formRef, modelValue: formValue.value, submit: () => useConfigApi.create(formValue.value) }),
73 {
74 hideTitle: true,
75 onBeforeOk: () => promiseToBoolean(formRef.value?.onSubmit()),
76 onOk: () => {
77 Message.success('添加成功');
78 tableRef.value?.onFetch();
79 },
80 }
81 );
82 };
83
84 const onCreateChild = (row: TableData) => {
85 const formRef = ref();
86 const formValue = ref({ parent_id: row.id, weight: 0, expand: { type: 'none' } });
87
88 createModalVNode(
89 () => createVNode(FormContent, { ref: formRef, modelValue: formValue.value, submit: () => useConfigApi.create(formValue.value) }),
90 {
91 hideTitle: true,
92 onBeforeOk: () => promiseToBoolean(formRef.value?.onSubmit()),
93 onOk: () => {
94 Message.success('添加成功');
95 tableRef.value?.onFetch();
96 },
97 }
98 );
99 };
100
101 const onUpdate = (row: TableData) => {
102 const formRef = ref();
103 const formValue = ref({ ...row });
104
105 createModalVNode(
106 () =>
107 createVNode(FormContent, {
108 ref: formRef,
109 modelValue: formValue.value,
110 hideKeyBtn: true,
111 lockField: ['identifier'],
112 submit: () => useConfigApi.update(row.id, formValue.value),
113 }),
114 {
115 hideTitle: true,
116 onBeforeOk: () => promiseToBoolean(formRef.value?.onSubmit()),
117 onOk: () => {
118 Message.success('更新成功');
119 tableRef.value?.onFetch();
120 },
121 }
122 );
123 };
124
125 const onChangeStatus = (row: TableData, status: number) => {
126 createModalVNode(`是否将配置《${row.name}》的状态变更为:${useRoleApi.statusOption.find((item) => item.value === status)?.label}`, {
127 title: '状态变更',
128 titleAlign: 'center',
129 onBeforeOk: () => promiseToBoolean(useConfigApi.changeStatus(row.id, status)),
130 onOk: () => tableRef.value?.onFetch(),
131 });
132 };
133 </script>
134
135 <style lang="less" scoped></style>
1 <template>
2 <div class="container">
3 <Breadcrumb :items="['system', 'system-customer']" />
4 <a-card class="minHeight">
5 <a-space direction="vertical" fill>
6 <div style="color: #4e5969">工作简介:</div>
7 <div style="margin: 8px 0; color: #2c2c2c">{{ config.content }}</div>
8 <icon-button v-permission="['system-customer-edit']" icon="edit" type="primary" label="编辑" @click="onUpdate" />
9 </a-space>
10 </a-card>
11 </div>
12 </template>
13
14 <script lang="ts" setup>
15 import { createVNode, onMounted, ref } from 'vue';
16 import { SystemConfig } from '@/types/system-config';
17 import { Form, FormItem, Input, Modal } from '@arco-design/web-vue';
18 import useConfigApi from '@/http/config';
19 import { set } from 'lodash';
20
21 const { update, getOne } = useConfigApi;
22
23 const config = ref<Pick<SystemConfig, 'id' | 'content'>>({ id: 0, content: '' });
24
25 const onUpdate = () => {
26 const content = ref<string>(config.value?.content || '');
27
28 Modal.open({
29 title: '编辑',
30 content: () =>
31 createVNode(
32 Form,
33 { model: content },
34 {
35 default: () =>
36 createVNode(
37 FormItem,
38 { label: '工作简介' },
39 {
40 default: () =>
41 createVNode(Input, {
42 'maxLength': 30,
43 'modelValue': content.value,
44 'showWordLimit': true,
45 'onUpdate:modelValue': (val: string | undefined) => {
46 content.value = val || '';
47 },
48 }),
49 }
50 ),
51 }
52 ),
53 closable: false,
54 escToClose: false,
55 maskClosable: false,
56 onBeforeOk: (done) => {
57 update(config.value?.id, { ...config.value, content: content.value })
58 .then(() => {
59 set(config.value, 'content', content.value);
60 done(true);
61 })
62 .catch(() => {
63 done(false);
64 });
65 },
66 });
67 };
68
69 onMounted(() => {
70 getOne({ identifier: 'customer_service' }).then((data?: SystemConfig) => {
71 config.value = data || { id: 0, content: '' };
72 });
73 });
74 </script>
75
76 <style lang="less" scoped>
77 .minHeight {
78 min-height: calc(100vh - 200px);
79 }
80 </style>
1 <template>
2 <router-view />
3 </template>
4
5 <script lang="ts" setup></script>
6
7 <style lang="less" scoped>
8 .container {
9 padding: 0 30px 20px 20px;
10 }
11
12 .operations {
13 display: flex;
14 }
15 </style>
1 <template>
2 <filter-search v-show="hasSearch" :loading="loading" :model="filter" @search="onSearch" @reset="onReset">
3 <filter-search-item field="name" label="操作人">
4 <a-input v-model="filter.userNickName" allow-clear placeholder="请输入" />
5 </filter-search-item>
6 <filter-search-item field="guard" label="操作平台">
7 <a-select v-model="filter.guard" :options="guardOption" allow-clear placeholder="请选择" @change="() => (filter.path = '')" />
8 </filter-search-item>
9 <filter-search-item field="name" label="操作模块">
10 <permission-select v-model="filter.path" :guard="filter.guard" allow-clear allow-search />
11 </filter-search-item>
12 <filter-search-item field="createBetween" label="操作时间">
13 <a-range-picker v-model="filter.createBetween" show-time :time-picker-props="{ defaultValue: ['00:00:00', '23:59:59'] }" />
14 </filter-search-item>
15 </filter-search>
16 <filter-table ref="tableRef" :loading="loading" :on-query="onQuery">
17 <user-table-column title="操作人" data-index="user_id" user="user" show-href :width="160" />
18 <filter-table-column title="操作平台" data-index="guard" :width="100">
19 <template #default="{ record }">
20 {{ guardOption.find((item) => item.value === record.guard)?.label || '未知' }}
21 </template>
22 </filter-table-column>
23 <filter-table-column :width="180" data-index="path" title="操作模块">
24 <template #default="{ record }">
25 {{
26 record.path
27 ?.map((item: string) => findPermissionLabel(record.guard, item))
28 .filter((item: string) => item !== '')
29 .join('/')
30 }}
31 </template>
32 </filter-table-column>
33 <filter-table-column :width="100" data-index="action" title="操作类型" />
34 <filter-table-column :width="460" data-index="content" title="操作内容" />
35 <filter-table-column :width="170" data-index="created_at" title="操作时间" />
36 </filter-table>
37 </template>
38
39 <script lang="ts" setup>
40 import { AnyObject, QueryForParams } from '@/types/global';
41 import useLoading from '@/hooks/loading';
42 import { computed, onMounted, ref } from 'vue';
43 import { useAppStore } from '@/store';
44 import { storeToRefs } from 'pinia';
45 import { SystemPermission } from '@/types/system-permission';
46 import UserTableColumn from '@/components/filter/user-table-column.vue';
47 import useOperationLog from '@/http/log';
48
49 type propType = { filter?: AnyObject; hasSearch?: boolean; showGuard?: boolean };
50
51 const props = withDefaults(defineProps<propType>(), { filter: undefined, hasSearch: true, showGuard: true });
52
53 const appStore = useAppStore();
54 const { permissions } = storeToRefs(appStore);
55
56 const { loading, setLoading } = useLoading(false);
57 const filter = ref<QueryForParams>({ userNickName: '', guard: '', path: [], createBetween: [] });
58 const tableRef = ref();
59
60 const guardOption = [
61 { value: 'Admin', label: '运营管理平台' },
62 { value: 'Manage', label: '厂牌管理平台' },
63 ];
64
65 const systemPermission = computed((): SystemPermission[] => {
66 return [
67 { guard: 'Admin', name: 'dashboard', label: '信息概览' } as SystemPermission,
68 { guard: 'Manage', name: 'dashboard', label: '信息概览' } as SystemPermission,
69 ...(permissions?.value ?? []),
70 ];
71 });
72
73 const findPermissionLabel = (guard: 'Admin' | 'Manage', name: string) => {
74 return systemPermission.value?.find((item) => item.guard === guard && item.name === name)?.label || '';
75 };
76
77 const onQuery = async (params: AnyObject) => {
78 setLoading(true);
79
80 return useOperationLog
81 .get({
82 ...filter.value,
83 ...props.filter,
84 setWith: ['user:id,nick_name,real_name,identity'],
85 ...params,
86 sortBy: 'id',
87 sortType: 'desc',
88 })
89 .finally(() => {
90 setLoading(false);
91 });
92 };
93
94 const onSearch = () => {
95 tableRef.value?.onPageChange(1);
96 };
97
98 const onReset = () => {
99 filter.value = { userNickName: '', guard: '', path: [], createBetween: [] };
100 onSearch();
101 };
102
103 onMounted(() => {
104 onReset();
105 });
106 </script>
107
108 <style lang="less" scoped>
109 :deep(.arco-table-cell) {
110 padding: 5px 8px !important;
111
112 & > .arco-table-td-content .arco-btn-size-small {
113 padding: 5px !important;
114 }
115 }
116 </style>
1 <template>
2 <div class="container">
3 <Breadcrumb :items="['system', 'system-log']" />
4 <a-card :body-style="{ padding: '20px' }">
5 <query-table />
6 </a-card>
7 </div>
8 </template>
9
10 <script setup lang="ts">
11 import QueryTable from '@/views/system/logs/components/query-table.vue';
12 </script>
13
14 <style scoped></style>
1 <script setup lang="ts">
2 import usePagination from '@/hooks/pagination';
3 import useMaterialApi from '@/http/material';
4 import { createVNode, onMounted, ref, toRef } from 'vue';
5 import useLoading from '@/hooks/loading';
6 import { AnyObject } from '@/types/global';
7 import {
8 createModalVNode,
9 createFormVNode,
10 createFormItemVNode,
11 createInputNumberFormItemVNode,
12 createInputUploadFormItemVNode,
13 createSelectionFormItemVNode,
14 } from '@/utils/createVNode';
15 import { FileItem, Message, RequestOption, Upload } from '@arco-design/web-vue';
16 import { promiseToBoolean } from '@/utils';
17 import { pick, set } from 'lodash';
18 import useOss from '@/hooks/oss';
19
20 const { loading, setLoading } = useLoading();
21 const { pagination, setPage, setPageSize, setTotal } = usePagination({ pageSize: 30 });
22 const filter = ref<AnyObject>({});
23 const list = ref<AnyObject[]>([]);
24
25 const sortOption = [
26 { label: '创建时间', value: 'id' },
27 { label: '权重值', value: 'weight' },
28 ];
29
30 const onQuery = async (params?: AnyObject) => {
31 setLoading(true);
32 const { current: page, pageSize } = pagination.value;
33 useMaterialApi
34 .get({ ...filter.value, page, pageSize, ...params })
35 .then(({ data, meta }) => {
36 list.value = data as AnyObject[];
37 setPage(meta.current);
38 setPageSize(meta.limit);
39 setTotal(meta.total);
40 })
41 .finally(() => setLoading(false));
42 };
43
44 const onPageChange = (current: number) => {
45 onQuery({ page: current });
46 };
47
48 const onSearch = () => onQuery({ page: 1 });
49
50 const onReset = () => {
51 filter.value = { type: '', sortBy: 'id', sortType: 'desc' };
52 onSearch();
53 };
54
55 const onPageSizeChange = (current: number) => {
56 onQuery({ page: 1, pageSize: current });
57 };
58
59 onMounted(() => onReset());
60
61 const onCreate = () => {
62 const formRef = ref();
63 const formValue = ref<{ type: number | string; fileList: FileItem[] }>({ type: '', fileList: [] });
64 const formRule = {
65 type: [{ required: true, message: '请选择分类' }],
66 fileList: [{ required: true, type: 'array', message: '请选择上传的素材' }],
67 };
68
69 createModalVNode(
70 () =>
71 createFormVNode({ ref: formRef, model: formValue, rules: formRule, autoLabelWidth: true }, [
72 createSelectionFormItemVNode(toRef(formValue.value, 'type'), useMaterialApi.typeOption, { label: '分类', field: 'type' }, {}),
73 createFormItemVNode({ hideLabel: false, label: '素材', field: 'fileList', rowClass: 'mb-0' }, [
74 createVNode(Upload, {
75 'model-value:fileList': formValue.value.fileList,
76 'onUpdate:fileList': (value: FileItem[]) => (formValue.value.fileList = value),
77 'multiple': true,
78 'accept': 'image/*',
79 'listType': 'picture-card',
80 'responseUrlKey': 'url',
81 'imagePreview': true,
82 'imageLoading': 'lazy',
83 'onBeforeUpload': (file: File & { width: number; height: number }) => {
84 const imgObj = new Image();
85 imgObj.src = URL.createObjectURL(file);
86 imgObj.onload = () => {
87 file.width = imgObj.width as number;
88 file.height = imgObj.height as number;
89 };
90 return Promise.resolve(file);
91 },
92 'customRequest': (option: RequestOption) => {
93 const { fileItem, onProgress, onSuccess, onError } = option;
94 if (fileItem.file) {
95 fileItem.status = 'uploading';
96 useOss()
97 .upload(fileItem.file, 'image', onProgress)
98 .then(({ url }) => {
99 fileItem.percent = 100;
100 fileItem.url = url || '';
101 fileItem.status = 'done';
102 onSuccess({ name: fileItem.name, url: fileItem.url });
103 })
104 .catch((e) => {
105 fileItem.status = 'error';
106 onError(e.response);
107 });
108 }
109 },
110 }),
111 ]),
112 ]),
113 {
114 title: '上传素材',
115 width: 540,
116 onOk: () => onSearch(),
117 onBeforeOk: async () => {
118 if (await formRef.value?.validate()) {
119 return false;
120 }
121 if (formValue.value.fileList.filter((item) => item.status === 'uploading')?.length !== 0) {
122 Message.warning({ id: 'create-msg', content: '请待素材上传完成' });
123 return false;
124 }
125
126 const data: { type: number; url: string | undefined; expand: Partial<File> }[] = [];
127 formValue.value.fileList
128 ?.filter((item) => item.status === 'done')
129 ?.forEach((item) => {
130 data.push({
131 type: formValue.value.type as number,
132 url: item.url,
133 expand: pick(item.file, ['size', 'type', 'width', 'height']),
134 });
135 });
136
137 return promiseToBoolean(useMaterialApi.create({ data }));
138 },
139 }
140 );
141 };
142
143 const onUpdate = (row: any) => {
144 const formValue = ref({ ...row });
145 const type = toRef(formValue.value, 'type', '');
146 const weight = toRef(formValue.value, 'weight', 0);
147 const url = toRef(formValue.value, 'url', 0);
148 const expand = toRef(formValue.value, 'expand', undefined);
149
150 createModalVNode(
151 () =>
152 createFormVNode({ model: formValue }, [
153 createSelectionFormItemVNode(type, useMaterialApi.typeOption, { label: '分类' }),
154 createInputNumberFormItemVNode(weight, { label: '权重' }),
155 createInputUploadFormItemVNode(
156 url,
157 { label: '链接', rowClass: 'mb-0' },
158 { accept: 'image/*', onSuccess: async (file: any) => set(expand, 'value', pick(file, ['width', 'height', 'size', 'type'])) }
159 ),
160 ]),
161 {
162 title: '编辑素材信息',
163 onBeforeOk: () => promiseToBoolean(useMaterialApi.update(row.id, formValue.value)),
164 onOk: () => onSearch(),
165 }
166 );
167 };
168
169 const onDelete = (row: any) => {
170 createModalVNode('是否确认删除此素材', {
171 title: '删除操作',
172 onBeforeOk: () => promiseToBoolean(useMaterialApi.destroy(row.id)),
173 onOk: () => onSearch(),
174 });
175 };
176 </script>
177
178 <template>
179 <page-view has-card has-bread>
180 <filter-search :loading="loading" inline :model="filter" @search="onSearch" @reset="onReset">
181 <filter-search-item label="类型">
182 <a-select v-model="filter.type" placeholder="请选择" :options="useMaterialApi.typeOption" allow-clear />
183 </filter-search-item>
184 <filter-search-item label="排序">
185 <a-select v-model="filter.sortBy" placeholder="请选择" :options="sortOption" />
186 </filter-search-item>
187 </filter-search>
188 <a-space v-permission="['system-material-create']" fill>
189 <icon-button style="margin-bottom: 16px" type="primary" icon="plus" label="新增" @click="onCreate" />
190 </a-space>
191
192 <a-image-preview-group :closable="false" :default-scale="0.7" :actions-layout="['fullScreen', 'zoomIn', 'zoomOut']">
193 <a-space size="small" wrap>
194 <template v-for="item in list" :key="item.id">
195 <a-image
196 :src="item.url"
197 :width="269"
198 :height="140"
199 :show-loader="true"
200 fit="cover"
201 :title="`${item.expand.width}*${item.expand.height}`"
202 >
203 <template #extra>
204 <div class="actions">
205 <span v-permission="['system-material-edit']" class="action" @click="onUpdate(item)"><icon-edit /></span>
206 <span v-permission="['system-material-delete']" class="action" @click="onDelete(item)"><icon-delete /></span>
207 </div>
208 </template>
209 </a-image>
210 </template>
211 </a-space>
212 </a-image-preview-group>
213 <div style="display: flex; justify-content: end; margin-top: 10px">
214 <a-pagination
215 :current="pagination.current"
216 :page-size="pagination.pageSize"
217 :total="pagination.total"
218 :page-size-options="pagination.pageSizeOptions as number[]"
219 :show-page-size="pagination.showPageSize as boolean"
220 :show-total="pagination.showTotal as boolean"
221 @change="onPageChange"
222 @page-size-change="onPageSizeChange"
223 />
224 </div>
225 </page-view>
226 </template>
227
228 <style scoped lang="less">
229 .actions {
230 display: flex;
231 align-items: center;
232 }
233
234 .action {
235 padding: 5px 4px;
236 font-size: 14px;
237 margin-left: 12px;
238 border-radius: 2px;
239 line-height: 1;
240 cursor: pointer;
241 }
242
243 .action:first-child {
244 margin-left: 0;
245 }
246 </style>
1 <template>
2 <page-view has-card has-bread>
3 <filter-search :inline="true" :loading="loading" :model="filter" @search="onSearch" @reset="onReset">
4 <!-- <filter-search-item label="类型">-->
5 <!-- <a-select v-model="filter.type" :options="typeOption" placeholder="请选择" allow-clear />-->
6 <!-- </filter-search-item>-->
7 <filter-search-item label="反馈用户">
8 <a-input v-model="filter.name" placeholder="请输入" allow-clear />
9 </filter-search-item>
10 <filter-search-item label="反馈时间">
11 <a-range-picker
12 v-model="filter.createBetween"
13 :allow-clear="true"
14 :show-time="true"
15 :time-picker-props="{ defaultValue: ['00:00:00', '23:59:59'] }"
16 format="YYYY-MM-DD HH:mm"
17 />
18 </filter-search-item>
19 <filter-search-item label="状态">
20 <a-select v-model="filter.status" :options="statusOption" placeholder="请选择" allow-clear />
21 </filter-search-item>
22 </filter-search>
23 <filter-table ref="tableRef" :loading="loading" :on-query="onQuery">
24 <user-table-column data-index="user_id" user="user" title="反馈用户" show-href :width="160" />
25 <enum-table-column data-index="type" title="分类" :option="typeOption" :width="100" />
26 <user-table-column data-index="person_id" user="person" title="关联内容" show-href prefix="@" :width="160" />
27 <filter-table-column data-index="reason" title="反馈内容" :width="300">
28 <template #default="{ record }">
29 <a-typography-text :ellipsis="{ rows: 1, showTooltip: true }">
30 {{ record.reason }}
31 <template v-if="record.desc">{{ record.desc }}</template>
32 </a-typography-text>
33 </template>
34 </filter-table-column>
35 <date-table-column data-index="created_at" title="反馈时间" :split="false" :width="170" />
36 <enum-table-column data-index="status" title="状态" :option="statusOption" :width="100" />
37 <filter-table-column data-index="operation" title="操作" :width="100">
38 <template #default="{ record }">
39 <a-link v-if="record.status === 0" :hoverable="false" class="link-hover" @click="handleUpdate(record)">处理</a-link>
40 </template>
41 </filter-table-column>
42 </filter-table>
43 </page-view>
44 </template>
45
46 <script lang="ts" setup>
47 import { onMounted, ref } from 'vue';
48 import { AnyObject } from '@/types/global';
49 import useLoading from '@/hooks/loading';
50 import useReportApi from '@/http/report';
51 import EnumTableColumn from '@/components/filter/enum-table-column.vue';
52 import UserTableColumn from '@/components/filter/user-table-column.vue';
53 import DateTableColumn from '@/components/filter/date-table-column.vue';
54 import { TableData } from '@arco-design/web-vue';
55 import { createModalVNode } from '@/utils/createVNode';
56 import { promiseToBoolean } from '@/utils';
57
58 const { typeOption, statusOption } = useReportApi;
59 const { loading, setLoading } = useLoading(false);
60
61 const filter = ref<AnyObject>({});
62 const tableRef = ref();
63
64 const onQuery = async (params: AnyObject) => {
65 setLoading(true);
66 return useReportApi
67 .get({ ...filter.value, ...params, setSort: '-id', setWith: ['user:id,nick_name,identity', 'person:id,nick_name,identity'] })
68 .finally(() => setLoading(false));
69 };
70
71 const onSearch = () => tableRef.value?.onPageChange(1);
72
73 const onReset = () => {
74 filter.value = { type: '', name: '', status: '', createBetween: [] };
75 onSearch();
76 };
77
78 onMounted(() => onReset());
79
80 const handleUpdate = (record: TableData) =>
81 createModalVNode('该用户举报/反馈的内容已被处理?', {
82 title: '处理举报/反馈',
83 okText: '是',
84 cancelText: '否',
85 onBeforeOk: () => promiseToBoolean(useReportApi.update(record.id, { status: 1 })),
86 onOk: () => tableRef.value?.onFetch(),
87 });
88 </script>
89
90 <style lang="less" scoped></style>
1 <script setup lang="ts">
2 import { Form, FormItem, Input, Textarea } from '@arco-design/web-vue';
3 import { useVModel } from '@vueuse/core';
4 import { FormInstance } from '@arco-design/web-vue/es/form';
5 import { ref } from 'vue';
6
7 const props = defineProps<{ modelValue: { name: string; intro: string }; submit?: () => Promise<any> }>();
8 const emit = defineEmits(['update:modelValue']);
9
10 const formRef = ref<FormInstance>();
11 const formValue = useVModel(props, 'modelValue', emit);
12 const formRule = {
13 name: [{ required: true, message: '请输入名称' }],
14 intro: [{ max: 500, message: '描述长度超出限制' }],
15 };
16
17 const onSubmit = async () => {
18 const error = await formRef.value?.validate();
19
20 return error ? Promise.reject(error) : props.submit?.();
21 };
22
23 defineExpose({ onSubmit });
24 </script>
25
26 <template>
27 <Form ref="formRef" :model="formValue" :rules="formRule" auto-label-width>
28 <FormItem label="名称" field="name">
29 <Input v-model="formValue.name" placeholder="请输入" :max-length="25" show-word-limit />
30 </FormItem>
31 <FormItem label="描述" field="intro" row-class="mb-0">
32 <Textarea v-model="formValue.intro" placeholder="请输入" :auto-size="{ minRows: 3, maxRows: 6 }" :max-length="500" show-word-limit />
33 </FormItem>
34 </Form>
35 </template>
36
37 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { Collapse, CollapseItem, Tree, TreeNodeData, CheckboxGroup } from '@arco-design/web-vue';
3 import { computed, onMounted, ref, createVNode } from 'vue';
4 import { usePermissionApi } from '@/http/role';
5 import { SystemPermission } from '@/types/system-permission';
6 import { arrayToTree, findTreeParentIds, findTreeChildIds } from '@/utils';
7 import * as Icon from '@arco-design/web-vue/es/icon';
8 import { camelCase, find, set, upperFirst, pull } from 'lodash';
9 import { useVModel } from '@vueuse/core';
10
11 const props = defineProps<{ modelValue: [] }>();
12 const emit = defineEmits(['update:modelValue']);
13 const checkIds = useVModel(props, 'modelValue', emit);
14
15 const activeKey = ref<string[]>([]);
16
17 const permissionData = ref<SystemPermission[]>([]);
18 const btnPermissionData = ref<SystemPermission[]>([]);
19
20 const adminPermissionRef = ref<InstanceType<typeof Tree>>();
21 const appPermissionRef = ref<InstanceType<typeof Tree>>();
22
23 const adminPermissionTree = computed((): TreeNodeData[] => {
24 // return arrayToTree(permissionData.value?.filter((item) => item.guard === 'Admin'));
25 return arrayToTree(permissionData.value?.filter((item) => item.guard === 'Admin' && item.is_show_line));
26 });
27
28 const appPermissionTree = computed(() => {
29 return arrayToTree(permissionData.value?.filter((item) => item.guard === 'App' && item.is_show_line));
30 });
31
32 onMounted(async () => {
33 await usePermissionApi.get({ fetchType: 'all', sortBy: 'weight', sortType: 'desc' }).then(({ data }) => {
34 btnPermissionData.value = data.filter((item) => !item.is_show_line);
35 permissionData.value = data.map((item) => {
36 const key = upperFirst(camelCase(item.icon));
37 // @ts-ignore
38 set(item, 'icon', () => createVNode(Icon[key] || 'span'));
39 return item;
40 });
41 activeKey.value = ['Admin'];
42 });
43 });
44
45 const getNodeCheckOption = (node: SystemPermission) => {
46 return btnPermissionData.value?.filter((btn) => btn.parent_id === node.id)?.map((btn) => ({ value: btn.id, label: btn.label }));
47 };
48
49 const onSelect = (treeRef: any, data: any) => {
50 if (data.node?.children?.length === 0) {
51 treeRef.checkNode(data.node.id, !find(treeRef.getCheckedNodes(), { id: data.node.id }));
52 }
53 // else {
54 // treeRef.expandNode(data.node.id, !find(treeRef.getExpandedNodes(), { id: data.node.id }));
55 // }
56 };
57
58 const onCheck = (checkedKeys: Array<string | number>, data: any) => {
59 if (data.checked) {
60 if (data.node.parent_id === 0 || data.node.type === 'Menu' || data.node.is_show_line) {
61 checkedKeys.push(
62 ...findTreeParentIds(permissionData.value, data.node.parent_id),
63 ...findTreeChildIds(permissionData.value, data.node.id)
64 );
65 } else {
66 checkedKeys.push(...findTreeParentIds(permissionData.value, data.node.parent_id));
67 }
68 } else {
69 pull(checkedKeys, ...findTreeChildIds(permissionData.value, data.node.id));
70 }
71 };
72
73 const onBtnCheck = (value: number[], ev: Event, node: SystemPermission) => {
74 // @ts-ignore
75 if (ev.target && !checkIds.value.includes(ev.target?.value)) {
76 value.push(...findTreeParentIds(permissionData.value, node.id));
77 }
78 };
79 </script>
80
81 <template>
82 <Collapse v-model:active-key="activeKey" :bordered="false" :accordion="true">
83 <CollapseItem key="Admin" header="管理后台">
84 <div style="max-height: 340px; overflow: auto">
85 <Tree
86 ref="adminPermissionRef"
87 v-model:checked-keys="checkIds"
88 size="small"
89 :data="adminPermissionTree"
90 :field-names="{ key: 'id', title: 'label', children: 'children', icon: 'icn' }"
91 :checkable="true"
92 :block-node="true"
93 :check-strictly="true"
94 :only-check-leaf="false"
95 :auto-expand-parent="false"
96 action-on-node-click="expand"
97 @select="(selectedKeys:Array<string | number>, data: any) => onSelect(adminPermissionRef, data)"
98 @check="onCheck"
99 >
100 <template #extra="nodeData">
101 <CheckboxGroup
102 v-model="checkIds"
103 :options="getNodeCheckOption(nodeData)"
104 @change="(value: any[], ev: Event) => onBtnCheck(value as number[], ev, nodeData)"
105 />
106 </template>
107 </Tree>
108 </div>
109 </CollapseItem>
110 <CollapseItem key="App" header="移动端">
111 <div style="max-height: 340px; overflow: auto">
112 <Tree
113 ref="appPermissionRef"
114 v-model:checked-keys="checkIds"
115 size="small"
116 :data="appPermissionTree"
117 :field-names="{ key: 'id', title: 'label', children: 'children', icon: 'icon' }"
118 :checkable="true"
119 :block-node="true"
120 :check-strictly="true"
121 :only-check-leaf="false"
122 :auto-expand-parent="false"
123 action-on-node-click="expand"
124 @select="(selectedKeys:Array<string | number>, data:any) => onSelect(appPermissionRef, data)"
125 @check="onCheck"
126 >
127 <template #extra="nodeData">
128 <CheckboxGroup
129 v-model="checkIds"
130 :options="getNodeCheckOption(nodeData)"
131 @change="(value: any[], ev:Event) => onBtnCheck(value as number[], ev, nodeData)"
132 />
133 </template>
134 </Tree>
135 </div>
136 </CollapseItem>
137 </Collapse>
138 </template>
139
140 <style lang="less" scoped>
141 :deep(.arco-tree-node-selected .arco-tree-node-title) {
142 color: unset;
143 }
144 </style>
1 <script setup lang="ts">
2 import { AnyObject } from '@/types/global';
3 import useRoleApi from '@/http/role';
4
5 import useLoading from '@/hooks/loading';
6 import { createVNode, onMounted, ref } from 'vue';
7 import { pick, set } from 'lodash';
8 import { promiseToBoolean } from '@/utils';
9 import { createModalVNode } from '@/utils/createVNode';
10 import UserTableColumn from '@/components/filter/user-table-column.vue';
11 import NumberTableColumn from '@/components/filter/number-table-column.vue';
12 import SpaceTableColumn from '@/components/filter/space-table-column.vue';
13 import EnumTableColumn from '@/components/filter/enum-table-column.vue';
14 import IconButton from '@/components/icon-button/index.vue';
15 import FormContent from '@/views/system/role/components/form-content.vue';
16 import PermissionContent from '@/views/system/role/components/permission-content.vue';
17 import { SystemPermission } from '@/types/system-permission';
18 import { useAuthorizedStore } from '@/store';
19
20 const { loading, setLoading } = useLoading(false);
21 const tableRef = ref();
22 const filter = ref<AnyObject>({});
23
24 const onQuery = async (params: AnyObject) => {
25 setLoading(true);
26 return useRoleApi
27 .get({
28 guard: 'Admin',
29 setWith: ['creator:id,nick_name', 'permissions:id'],
30 setWithCount: ['users'],
31 ...params,
32 sortBy: 'id',
33 sortType: 'desc',
34 })
35 .finally(() => setLoading(false));
36 };
37
38 const onSearch = () => tableRef.value?.onPageChange(1);
39
40 const onReset = () => {
41 filter.value = { name: '', status: '' };
42 onSearch();
43 };
44
45 onMounted(async () => {
46 onReset();
47 });
48
49 const onCreate = () => {
50 const formRef = ref();
51 const formValue = ref({ name: '', intro: '' });
52 createModalVNode(
53 () =>
54 createVNode(FormContent, {
55 ref: formRef,
56 modelValue: formValue.value,
57 submit: () => useRoleApi.create(formValue.value),
58 }),
59 {
60 title: '新增角色',
61 titleAlign: 'center',
62 onBeforeOk: () => promiseToBoolean(formRef.value?.onSubmit()),
63 onOk: () => tableRef.value?.onFetch(),
64 }
65 );
66 };
67
68 const onUpdate = (row: any) => {
69 const formRef = ref();
70 const formValue = ref(pick(row, ['name', 'intro']));
71
72 createModalVNode(
73 () =>
74 createVNode(FormContent, {
75 ref: formRef,
76 modelValue: formValue.value,
77 submit: () => useRoleApi.update(row.id, formValue.value),
78 }),
79 {
80 title: '编辑角色',
81 titleAlign: 'center',
82 onBeforeOk: () => promiseToBoolean(formRef.value?.onSubmit()),
83 onOk: () => tableRef.value?.onFetch(),
84 }
85 );
86 };
87
88 const onChangeStatus = (row: any, status: number) => {
89 createModalVNode(`是否将角色《${row.name}》的状态变更为:${useRoleApi.statusOption.find((item) => item.value === status)?.label}`, {
90 title: '状态变更',
91 titleAlign: 'center',
92 onBeforeOk: () => promiseToBoolean(useRoleApi.changeStatus(row.id, status)),
93 onOk: () => tableRef.value?.onFetch(),
94 });
95 };
96
97 const onChangePermission = (row: any) => {
98 const formValue = ref<number[]>([]);
99 row.permissions.forEach((item: SystemPermission) => formValue.value.push(item.id));
100
101 createModalVNode(
102 () =>
103 createVNode(PermissionContent, {
104 'modelValue': formValue.value,
105 'onUpdate:modelValue': (val: number[]) => set(formValue, 'value', val),
106 }),
107 {
108 hideTitle: true,
109 bodyStyle: { padding: 0 },
110 width: 720,
111 onBeforeOk: () => promiseToBoolean(useRoleApi.changePermission(row.id, formValue.value)),
112 onOk: () => {
113 useAuthorizedStore().syncInfo();
114 tableRef.value?.onFetch();
115 },
116 }
117 );
118 };
119
120 const onDelete = (row: any) => {
121 createModalVNode(`删除角色:${row.name}`, {
122 title: '删除操作',
123 titleAlign: 'center',
124 onBeforeOk: () => promiseToBoolean(useRoleApi.destroy(row.id)),
125 onOk: () => tableRef.value?.onFetch(),
126 });
127 };
128 </script>
129
130 <template>
131 <page-view has-bread has-card>
132 <filter-search :loading="loading" :model="filter" inline @search="onSearch" @reset="onReset">
133 <filter-search-item label="名称">
134 <a-input v-model="filter.name" placeholder="请输入" allow-clear />
135 </filter-search-item>
136 <filter-search-item label="状态">
137 <a-select v-model="filter.status" :options="useRoleApi.statusOption" placeholder="请选择" allow-clear />
138 </filter-search-item>
139 </filter-search>
140 <filter-table ref="tableRef" :loading="loading" :on-query="onQuery">
141 <template #tool>
142 <icon-button v-permission="['system-role-create']" type="primary" icon="plus" label="新增" @click="onCreate" />
143 </template>
144 <filter-table-column data-index="name" title="名称" :width="160" />
145 <filter-table-column data-index="intro" title="描述" :width="180" />
146 <number-table-column data-index="users_count" title="关联用户数" :width="140" />
147 <user-table-column data-index="user_id" user="creator" title="创建人" show-href :width="140" />
148 <filter-table-column data-index="created_at" title="创建时间" :width="180" />
149 <enum-table-column data-index="status" title="状态" :option="useRoleApi.statusOption" :width="80" />
150 <space-table-column data-index="operator" title="操作" :width="160">
151 <template #default="{ record }">
152 <a-link
153 v-permission="['system-role-edit']"
154 :hoverable="false"
155 class="link-hover"
156 @click="onChangeStatus(record, Number(!record.status))"
157 >
158 {{ useRoleApi.statusOption.find((item) => item.value === Number(!record.status))?.label }}
159 </a-link>
160 <a-link v-permission="['system-role-edit']" :hoverable="false" class="link-hover" @click="onUpdate(record)">编辑</a-link>
161 <a-link v-permission="['system-role-permission']" :hoverable="false" class="link-hover" @click="onChangePermission(record)">
162 权限
163 </a-link>
164 <a-link v-permission="['system-role-delete']" :hoverable="false" class="link-hover" @click="onDelete(record)">删除</a-link>
165 </template>
166 </space-table-column>
167 </filter-table>
168 </page-view>
169 </template>
170
171 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { onMounted, ref } from 'vue';
3 import { Form, FormItem, Input, InputNumber, Select } from '@arco-design/web-vue';
4 import InputUpload from '@/components/input-upload/index.vue';
5 import useTagApi from '@/http/star-tag';
6 import { ColorPicker } from 'vue3-colorpicker';
7 import { set, get, unset } from 'lodash';
8
9 const formValue: any = defineModel();
10 const { frameOption, statusOption, indexFilterOption } = useTagApi;
11 const needFrame = ref<number>(0);
12 const formRule = {
13 name: [{ required: true, message: '请输入名称' }],
14 icon: [{ required: true, message: '请选择图标' }],
15 index_filter: [{ required: true, message: '请选择首页是否可筛选' }],
16 weight: [{ required: true, message: '请输入权重' }],
17 };
18
19 const onFrameChange = (value: number) => {
20 if (value === 1) {
21 set(formValue, 'value.frame', {
22 url: get(formValue, 'value.frame.url', ''),
23 color: get(formValue, 'value.frame.color', '#000000'),
24 });
25 } else {
26 unset(formValue, 'value.frame');
27 }
28 };
29
30 onMounted(() => {
31 needFrame.value = get(formValue, 'value.frame') ? 1 : 2;
32 onFrameChange(needFrame.value);
33 });
34 </script>
35
36 <template>
37 <Form :model="formValue" :rules="formRule" auto-label-width>
38 <FormItem label="名称" field="name" show-colon>
39 <Input v-model="formValue.name" :max-length="20" placeholder="请输入" show-word-limit />
40 </FormItem>
41
42 <FormItem label="图标" field="name" show-colon>
43 <InputUpload v-model="formValue.icon" accept="image/*" />
44 </FormItem>
45 <FormItem label="筛选" field="index_filter" show-colon>
46 <Select v-model="formValue.index_filter" :options="indexFilterOption" placeholder="请选择" />
47 </FormItem>
48 <FormItem label="权重" field="weight" show-colon>
49 <InputNumber v-model="formValue.weight" :min="0" :max="999999" placeholder="请输入" />
50 </FormItem>
51 <FormItem label="状态" field="status" show-colon>
52 <Select v-model="formValue.status" :options="statusOption" placeholder="请选择" />
53 </FormItem>
54 <FormItem label="边框" field="status" show-colon>
55 <Select v-model="needFrame" :options="frameOption" placeholder="请选择" @change="onFrameChange" />
56 </FormItem>
57 <template v-if="needFrame === 1 && formValue">
58 <FormItem label="边框图标" field="formValue.frame" show-colon>
59 <InputUpload v-model="formValue.frame.url" accept="image/*" />
60 </FormItem>
61 <FormItem label="边框颜色" field="frame.color" show-colon>
62 <color-picker v-model:pureColor="formValue.frame.color" disable-alpha format="hex" />
63 </FormItem>
64 </template>
65 </Form>
66 </template>
67
68 <style scoped lang="less"></style>
1 <template>
2 <page-view has-bread has-card>
3 <filter-search :model="filter" :loading="loading" @search="onSearch" @reset="onReset">
4 <filter-search-item field="name" label="名称">
5 <a-input v-model="filter.name" allow-clear placeholder="请输入" />
6 </filter-search-item>
7 <filter-search-item field="status" label="标签状态">
8 <a-select v-model="filter.status" :options="statusOption" allow-clear placeholder="请选择" />
9 </filter-search-item>
10 <filter-search-item field="createBetween" label="创建时间">
11 <a-range-picker
12 v-model="filter.createBetween"
13 :show-time="true"
14 :allow-clear="true"
15 :time-picker-props="{ defaultValue: ['00:00:00', '23:59:59'] }"
16 />
17 </filter-search-item>
18 </filter-search>
19 <filter-table ref="tableRef" :loading="loading" :on-query="onQuery">
20 <template #tool>
21 <icon-button v-permission="['system-star-tag-create']" type="primary" icon="plus" label="新增" @click="onCreate" />
22 </template>
23 <filter-table-column :width="100" data-index="id" title="#" />
24 <filter-table-column :width="260" data-index="name" title="名称" />
25 <star-tag-icon-table-column title="主页图标"></star-tag-icon-table-column>
26 <star-tag-table-column :width="120" title="列表头像样式"></star-tag-table-column>
27 <filter-table-column :width="100" data-index="status" title="首页筛选">
28 <template #default="{ record }">
29 {{ record.index_filter === 1 ? '可筛选' : '不可筛选' }}
30 </template>
31 </filter-table-column>
32 <filter-table-column :width="100" data-index="status" title="权重">
33 <template #default="{ record }">
34 {{ record.weight }}
35 </template>
36 </filter-table-column>
37 <filter-table-column :width="170" data-index="created_at" title="创建时间" />
38 <filter-table-column :width="100" data-index="status" title="状态">
39 <template #default="{ record }">
40 {{ record.status === 1 ? '已开启' : '已禁用' }}
41 </template>
42 </filter-table-column>
43 <space-table-column
44 v-if="usePermission().checkPermission(['system-star-tag-edit', 'system-star-tag-delete'])"
45 :width="100"
46 data-index="operations"
47 title="操作"
48 >
49 <template #default="{ record }">
50 <a-link v-permission="['system-tag-edit']" :hoverable="false" class="link-hover" @click="onUpdate(record)">编辑</a-link>
51 <a-link v-permission="['system-star-tag-delete']" :hoverable="false" class="link-hover" @click="onDelete(record)"> 删除</a-link>
52 </template>
53 </space-table-column>
54 </filter-table>
55 </page-view>
56 </template>
57
58 <script lang="ts" setup>
59 import { onMounted, ref, createVNode } from 'vue';
60 import { Message, TableData } from '@arco-design/web-vue';
61 import useLoading from '@/hooks/loading';
62 import useTagApi from '@/http/star-tag';
63 import { AnyObject } from '@/types/global';
64 import { createModalVNode } from '@/utils/createVNode';
65 import SpaceTableColumn from '@/components/filter/space-table-column.vue';
66 import StarTagTableColumn from '@/components/filter/star-tag-table-column.vue';
67 import StarTagIconTableColumn from '@/components/filter/star-tag-icon-table-column.vue';
68 import FormContent from '@/views/system/star-tag/components/form-content.vue';
69 import usePermission from '@/hooks/permission';
70 import { promiseToBoolean } from '@/utils';
71
72 const tableRef = ref();
73 const filter = ref({ name: '', is_show: '', createBetween: [], status: '' });
74 const { loading, setLoading } = useLoading(false);
75 const { create, update, destroy, statusOption } = useTagApi;
76
77 const onQuery = async (params?: AnyObject) => {
78 setLoading(true);
79 return useTagApi
80 .get({
81 ...filter.value,
82 setWith: ['user:id,nick_name'],
83 sortBy: 'id',
84 sortType: 'desc',
85 ...params,
86 })
87 .finally(() => setLoading(false));
88 };
89
90 const onSearch = () => tableRef.value?.onPageChange(1);
91
92 const onReset = () => {
93 filter.value = { name: '', is_show: '', createBetween: [], status: '' };
94 onSearch();
95 };
96
97 const onCreate = () => {
98 const formValue = ref({ name: '', status: 1, weight: 0, index_filter: 1 });
99 return createModalVNode(() => createVNode(FormContent, { modelValue: formValue.value }), {
100 title: '新增操作',
101 onBeforeOk: () => promiseToBoolean(create(formValue.value)),
102 onOk: () => {
103 tableRef.value?.onFetch();
104 Message.success(`创建标签:${formValue.value.name}`);
105 },
106 });
107 };
108
109 const onUpdate = (row: TableData) => {
110 const formValue = ref({ ...row });
111
112 return createModalVNode(() => createVNode(FormContent, { modelValue: formValue.value }), {
113 title: '编辑操作',
114 onBeforeOk: () => promiseToBoolean(update(row.id, formValue.value)),
115 onOk: () => {
116 tableRef.value?.onFetch();
117 Message.success(`更新标签:${formValue.value.name}`);
118 },
119 });
120 };
121
122 const onDelete = (row: TableData) =>
123 createModalVNode(`确认要将标签:${row.name} 删除吗?`, {
124 title: '删除操作',
125 maskClosable: true,
126 onBeforeOk: () => promiseToBoolean(destroy(row.id)),
127 onOk: () => {
128 tableRef.value?.onFetch();
129 Message.success(`删除标签:${row.name}`);
130 },
131 });
132
133 onMounted(() => onReset());
134 </script>
135
136 <style lang="less" scoped></style>
1 <script setup lang="ts">
2 import { Form, FormItem, Input, Select, InputNumber, CheckboxGroup } from '@arco-design/web-vue';
3 import { computed } from 'vue';
4 import { get, set } from 'lodash';
5 import useTagApi from '@/http/Tag';
6
7 defineProps<{ hideType?: boolean }>();
8
9 const formValue = defineModel();
10
11 const { typeOption, showOption, filterOption, certifyPermissionOption } = useTagApi;
12
13 const formRule = {
14 name: [{ required: true, message: '请输入名称' }],
15 type: [{ required: true, message: '请选择分类' }],
16 is_show: [{ required: true, message: '请选择分类' }],
17 index_filter: [{ required: true, message: '请选择首页筛选' }],
18 };
19
20 const permissionValue = computed({
21 get: () => get(formValue, 'value.expand.permission', []),
22 set: (val) => set(formValue, 'value.expand.permission', val),
23 });
24 </script>
25
26 <template>
27 <Form :model="formValue" :rules="formRule" auto-label-width>
28 <FormItem label="名称" field="name" show-colon>
29 <Input v-model="formValue.name" :max-length="20" placeholder="请输入" show-word-limit />
30 </FormItem>
31 <FormItem v-show="!hideType" label="类型" field="type" show-colon>
32 <Select v-model="formValue.type" :options="typeOption" placeholder="请选择" />
33 </FormItem>
34 <FormItem v-show="formValue.type === 4" label="权限" field="expand.permission" show-colon>
35 <CheckboxGroup v-model="permissionValue" :options="certifyPermissionOption" />
36 </FormItem>
37 <FormItem label="权重" field="weight" show-colon>
38 <InputNumber v-model="formValue.weight" :max="255" :min="0" :step="10" />
39 </FormItem>
40 <FormItem label="前台显示" field="is_show" show-colon>
41 <Select v-model="formValue.is_show" :options="showOption" placeholder="请选择" />
42 </FormItem>
43 <FormItem label="首页筛选" field="index_filter" show-colon row-class="mb-0">
44 <Select v-model="formValue.index_filter" :options="filterOption" placeholder="请选择" />
45 </FormItem>
46 </Form>
47 </template>
48
49 <style scoped lang="less"></style>
1 <template>
2 <page-view has-bread has-card>
3 <filter-search :model="filter" :loading="loading" @search="onSearch" @reset="onReset">
4 <filter-search-item field="name" label="标签名称">
5 <a-input v-model="filter.name" allow-clear placeholder="请输入" />
6 </filter-search-item>
7 <filter-search-item field="type" label="标签类型">
8 <a-select v-model="filter.type" :options="typeOption" allow-clear placeholder="请选择" />
9 </filter-search-item>
10 <filter-search-item field="userName" label="创建者">
11 <a-input v-model="filter.userName" allow-clear placeholder="请输入" />
12 </filter-search-item>
13 <filter-search-item field="createBetween" label="创建时间">
14 <a-range-picker
15 v-model="filter.createBetween"
16 :show-time="true"
17 :allow-clear="true"
18 :time-picker-props="{ defaultValue: ['00:00:00', '23:59:59'] }"
19 />
20 </filter-search-item>
21 <filter-search-item field="is_show" label="前台显示">
22 <a-select v-model="filter.is_show" :options="showOption" allow-clear placeholder="请选择" />
23 </filter-search-item>
24 </filter-search>
25 <filter-table ref="tableRef" :loading="loading" :on-query="onQuery">
26 <template #tool>
27 <icon-button v-permission="['system-tag-create']" type="primary" icon="plus" label="新增" @click="onCreate" />
28 </template>
29 <filter-table-column :width="100" data-index="id" title="#" />
30 <filter-table-column :width="260" data-index="name" title="名称" />
31 <enum-table-column :width="120" data-index="type" title="类型" :option="typeOption" />
32 <filter-table-column :width="140" data-index="user.nick_name" title="创建者" />
33 <filter-table-column :width="80" data-index="weight" title="权重值" />
34 <filter-table-column :width="170" data-index="created_at" title="创建时间" />
35 <enum-table-column :width="110" data-index="is_show" title="前台显示" :option="showOption" :dark-value="0" />
36 <enum-table-column :width="110" data-index="index_filter" title="首页筛选" :option="filterOption" :dark-value="0" />
37 <space-table-column
38 v-if="usePermission().checkPermission(['system-tag-edit', 'system-tag-delete'])"
39 :width="100"
40 data-index="operations"
41 title="操作"
42 >
43 <template #default="{ record }">
44 <a-link v-permission="['system-tag-edit']" :hoverable="false" class="link-hover" @click="onUpdate(record)">编辑</a-link>
45 <a-link v-permission="['system-tag-delete']" :hoverable="false" class="link-hover" @click="onDelete(record)"> 删除</a-link>
46 </template>
47 </space-table-column>
48 </filter-table>
49 </page-view>
50 </template>
51
52 <script lang="ts" setup>
53 import { onMounted, ref, createVNode } from 'vue';
54 import { Message, TableData } from '@arco-design/web-vue';
55 import useLoading from '@/hooks/loading';
56 import useTagApi from '@/http/Tag';
57 import { AnyObject } from '@/types/global';
58 import { createModalVNode } from '@/utils/createVNode';
59 import EnumTableColumn from '@/components/filter/enum-table-column.vue';
60 import SpaceTableColumn from '@/components/filter/space-table-column.vue';
61 import FormContent from '@/views/system/tag/components/form-content.vue';
62 import usePermission from '@/hooks/permission';
63 import { promiseToBoolean } from '@/utils';
64
65 const tableRef = ref();
66 const filter = ref({ name: '', type: '', userName: '', is_show: '', createBetween: [] });
67 const { loading, setLoading } = useLoading(false);
68 const { typeOption, showOption, filterOption, create, update, destroy } = useTagApi;
69
70 const onQuery = async (params?: AnyObject) => {
71 setLoading(true);
72 return useTagApi
73 .get({
74 ...filter.value,
75 setWith: ['user:id,nick_name'],
76 sortBy: 'id',
77 sortType: 'desc',
78 ...params,
79 })
80 .finally(() => setLoading(false));
81 };
82
83 const onSearch = () => tableRef.value?.onPageChange(1);
84
85 const onReset = () => {
86 filter.value = { name: '', type: '', userName: '', is_show: '', createBetween: [] };
87 onSearch();
88 };
89
90 const onCreate = () => {
91 const formValue = ref({ name: '', type: '', weight: 0, is_show: 1, index_filter: 1, expand: { permission: [] } });
92 return createModalVNode(() => createVNode(FormContent, { modelValue: formValue.value }), {
93 title: '新增操作',
94 onBeforeOk: () => promiseToBoolean(create(formValue.value)),
95 onOk: () => {
96 tableRef.value?.onFetch();
97 Message.success(`创建标签:${formValue.value.name}`);
98 },
99 });
100 };
101
102 const onUpdate = (row: TableData) => {
103 const formValue = ref({ ...row });
104
105 return createModalVNode(() => createVNode(FormContent, { modelValue: formValue.value, hideType: true }), {
106 title: '编辑操作',
107 onBeforeOk: () => promiseToBoolean(update(row.id, formValue.value)),
108 onOk: () => {
109 tableRef.value?.onFetch();
110 Message.success(`更新标签:${formValue.value.name}`);
111 },
112 });
113 };
114
115 const onDelete = (row: TableData) =>
116 createModalVNode(`确认要将标签:${row.name} 删除吗?`, {
117 title: '删除操作',
118 maskClosable: true,
119 onBeforeOk: () => promiseToBoolean(destroy(row.id)),
120 onOk: () => {
121 tableRef.value?.onFetch();
122 Message.success(`删除标签:${row.name}`);
123 },
124 });
125
126 onMounted(() => onReset());
127 </script>
128
129 <style lang="less" scoped></style>
1 <script setup lang="ts">
2 import { Form, FormItem, Input, Textarea, Select, InputNumber, RadioGroup } from '@arco-design/web-vue';
3 import InputUpload from '@/components/input-upload/index.vue';
4
5 import useVersionApi from '@/http/version';
6 import { ref } from 'vue';
7
8 const type = ref(1);
9
10 const formValue = defineModel();
11
12 const formRule = {
13 os: [{ required: true, message: '请选择系统' }],
14 app_ver: [{ required: true, message: '请输入版本号' }],
15 app_no: [{ required: true, message: '请输入内部号' }],
16 url: [{ required: true, message: '请输入链接地址' }],
17 is_force: [{ required: true, message: '请选择更新方式' }],
18 };
19
20 const typeOption = [
21 { label: '连接', value: 1 },
22 { label: '上传', value: 2 },
23 ];
24 </script>
25
26 <template>
27 <Form :model="formValue" :rules="formRule" auto-label-width>
28 <FormItem field="os" label="系统" show-colon>
29 <Select v-model="formValue.os" :options="useVersionApi.systemOption" placeholder="请选择" />
30 </FormItem>
31 <FormItem field="app_ver" label="版本号" show-colon>
32 <Input v-model="formValue.app_ver" placeholder="请输入" />
33 </FormItem>
34 <FormItem field="app_no" label="内部号" show-colon>
35 <InputNumber v-model="formValue.app_no" :precision="0" :step="1" :max="99999999" placeholder="请输入" />
36 </FormItem>
37 <FormItem label="方式" show-colon>
38 <RadioGroup v-model="type" :options="typeOption" />
39 </FormItem>
40 <FormItem v-if="type === 1" field="url" label="版本链接" show-colon>
41 <Input v-model="formValue.url" placeholder="请输入" />
42 </FormItem>
43 <FormItem v-else field="url" label="版本文件" show-colon>
44 <InputUpload v-model="formValue.url" placeholder="请选择" />
45 </FormItem>
46 <FormItem field="is_force" label="强制更新" show-colon>
47 <Select v-model="formValue.is_force" :options="useVersionApi.forceOption" placeholder="请选择" />
48 </FormItem>
49 <FormItem field="remark" label="说明" show-colon>
50 <Textarea v-model="formValue.remark" :auto-size="{ minRows: 4, maxRows: 6 }" />
51 </FormItem>
52 </Form>
53 </template>
54
55 <style scoped lang="less"></style>
1 <template>
2 <page-view has-bread has-card>
3 <filter-search :loading="loading" :model="filter" :inline="true" @search="onSearch" @reset="onReset">
4 <filter-search-item label="系统类型">
5 <a-select v-model="filter.os" :options="systemOption" :allow-clear="true" placeholder="请选择" />
6 </filter-search-item>
7 <filter-search-item label="强制更新">
8 <a-select v-model="filter.is_force" :options="forceOption" :allow-clear="true" placeholder="请选择" />
9 </filter-search-item>
10 <filter-search-item label="内部版本">
11 <a-input v-model="filter.appNoLike" :allow-clear="true" placeholder="请输入筛选版本号" />
12 </filter-search-item>
13 </filter-search>
14 <filter-table ref="tableRef" :loading="loading" :on-query="onQuery">
15 <template #tool>
16 <icon-button v-permission="['system-version-create']" type="primary" icon="plus" label="新增" @click="handleCreate" />
17 </template>
18 <enum-table-column data-index="os" title="系统类型" :option="systemOption" align="center" :width="120" />
19 <filter-table-column data-index="app_ver" title="版本号" align="center" :width="120">
20 <template #default="{ record }">
21 <a-link :hoverable="false" class="link-hover" :href="record.url" icon>{{ record.app_ver }}</a-link>
22 </template>
23 </filter-table-column>
24 <filter-table-column data-index="app_no" title="内部号" align="center" :width="120" />
25 <enum-table-column data-index="is_force" title="强制更新" align="center" :option="forceOption" :width="80" />
26 <filter-table-column data-index="remark" title="说明" />
27 <date-table-column data-index="created_at" title="发布时间" :split="false" :width="170" />
28 <space-table-column
29 v-if="usePermission().checkPermission(['system-version-edit', 'system-version-delete'])"
30 :width="100"
31 data-index="operations"
32 title="操作"
33 >
34 <template #default="{ record }">
35 <a-link v-permission="['system-version-edit']" :hoverable="false" class="link-hover" @click="handleUpdate(record)">编辑</a-link>
36 <a-link v-permission="['system-version-delete']" :hoverable="false" class="link-hover" @click="handleDelete(record)">删除</a-link>
37 </template>
38 </space-table-column>
39 </filter-table>
40 </page-view>
41 </template>
42
43 <script lang="ts" setup>
44 import { createVNode, onMounted, ref } from 'vue';
45 import { AnyObject } from '@/types/global';
46 import useVersionApi from '@/http/version';
47 import useLoading from '@/hooks/loading';
48 import EnumTableColumn from '@/components/filter/enum-table-column.vue';
49 import DateTableColumn from '@/components/filter/date-table-column.vue';
50 import SpaceTableColumn from '@/components/filter/space-table-column.vue';
51 import FormContent from '@/views/system/version/components/form-content.vue';
52
53 import usePermission from '@/hooks/permission';
54 import { Message, TableData } from '@arco-design/web-vue';
55 import { createModalVNode } from '@/utils/createVNode';
56 import { promiseToBoolean } from '@/utils';
57
58 const { systemOption, forceOption } = useVersionApi;
59
60 const { loading, setLoading } = useLoading(false);
61 const filter = ref<AnyObject>({});
62 const tableRef = ref();
63
64 const onQuery = async (params: AnyObject) => {
65 setLoading(true);
66 return useVersionApi.get({ ...filter.value, ...params, setSort: '-id' }).finally(() => setLoading(false));
67 };
68
69 const onSearch = () => tableRef.value?.onPageChange(1);
70
71 const onReset = () => {
72 filter.value = { appNoLike: '', is_force: '', os: '' };
73 onSearch();
74 };
75
76 onMounted(() => onReset());
77
78 const handleCreate = () => {
79 const formValue = ref({ app_ver: '' });
80
81 createModalVNode(() => createVNode(FormContent, { modelValue: formValue.value }), {
82 title: '新增操作',
83 onBeforeOk: () => promiseToBoolean(useVersionApi.create(formValue.value)),
84 onOk: () => {
85 tableRef.value?.onFetch();
86 Message.success(`新增版本:${formValue.value.app_ver}`);
87 },
88 });
89 };
90
91 const handleUpdate = (row: TableData) => {
92 const formValue = ref({ ...row });
93
94 createModalVNode(() => createVNode(FormContent, { modelValue: formValue.value }), {
95 title: '编辑操作',
96 onBeforeOk: () => promiseToBoolean(useVersionApi.update(row.id, formValue.value)),
97 onOk: () => {
98 tableRef.value?.onFetch();
99 Message.success(`更新版本:${formValue.value.app_ver}`);
100 },
101 });
102 };
103
104 const handleDelete = (row: TableData) =>
105 createModalVNode(`确认要将版本信息:${row.app_ver} 删除吗?`, {
106 title: '删除操作',
107 maskClosable: true,
108 onBeforeOk: () => promiseToBoolean(useVersionApi.destroy(row.id)),
109 onOk: () => {
110 tableRef.value?.onFetch();
111 Message.success(`删除版本:${row.app_ver}`);
112 },
113 });
114 </script>
115
116 <style lang="less" scoped></style>
1 <script setup lang="ts" name="user-business">
2 import { onActivated, ref } from 'vue';
3 import { AnyObject } from '@/types/global';
4 import useLoading from '@/hooks/loading';
5 import PhoneTableColumn from '@/components/filter/phone-table-column.vue';
6 import EnumTableColumn from '@/components/filter/enum-table-column.vue';
7 import { useBusinessApi } from '@/http/user';
8 import { useRoute, useRouter } from 'vue-router';
9 import { TableData } from '@arco-design/web-vue';
10 import UserTableColumn from '@/components/filter/user-table-column.vue';
11
12 const router = useRouter();
13
14 const tableRef = ref();
15 const filter = ref<AnyObject>({});
16 const { loading, setLoading } = useLoading(false);
17
18 const { sexOption, scopeOption, statusOption, officialStatusOption, get, getExport } = useBusinessApi;
19
20 const onQuery = async (params?: AnyObject) => {
21 setLoading(true);
22 return get({ ...filter.value, ...params }).finally(() => setLoading(false));
23 };
24
25 const onExport = () => getExport('经纪人', filter.value);
26
27 const onSearch = () => tableRef.value?.onPageChange(1);
28
29 const onSort = (column: string, type: string) => {
30 filter.value.sortBy = type ? column : 'id';
31 filter.value.sortType = type || 'desc';
32 tableRef.value?.onFetch();
33 };
34
35 const onReset = () => {
36 filter.value = {
37 nick_name: '',
38 sex: '',
39 email_like: '',
40 phone_like: '',
41 company: '',
42 scope: '',
43 official_status: '',
44 status: '',
45 sortBy: 'id',
46 sortType: 'desc',
47 };
48 tableRef.value?.resetSort();
49 onSearch();
50 };
51
52 const onRowClick = (record: TableData) => router.push({ name: 'user-business-show', params: { id: record.id } });
53
54 onActivated(() => (useRoute().meta.reload ? onReset() : tableRef.value?.onFetch()));
55 </script>
56
57 <template>
58 <page-view has-card has-bread>
59 <filter-search :model="filter" :loading="loading" @search="onSearch" @reset="onReset">
60 <filter-search-item field="nick_name" label="用户艺名">
61 <a-input v-model="filter.nick_name" allow-clear placeholder="请输入" />
62 </filter-search-item>
63 <filter-search-item field="sex" label="性别">
64 <a-select v-model="filter.sex" allow-clear placeholder="请选择" :options="sexOption" />
65 </filter-search-item>
66 <filter-search-item field="email_like" label="用户邮箱">
67 <a-input v-model="filter.email_like" allow-clear placeholder="请输入" />
68 </filter-search-item>
69 <filter-search-item field="phone_like" label="手机号码">
70 <a-input v-model="filter.phone_like" allow-clear placeholder="请输入" />
71 </filter-search-item>
72 <filter-search-item field="company" label="所属公司">
73 <a-input v-model="filter.company" allow-clear placeholder="请输入" />
74 </filter-search-item>
75 <filter-search-item field="scope" label="权限">
76 <a-select v-model="filter.scope" allow-clear placeholder="请选择" :options="scopeOption" />
77 </filter-search-item>
78 <filter-search-item field="official_status" label="关注服务号">
79 <a-select v-model="filter.official_status" allow-clear placeholder="请选择" :options="officialStatusOption" />
80 </filter-search-item>
81 <filter-search-item field="status" label="状态">
82 <a-select v-model="filter.status" allow-clear placeholder="请选择" :options="statusOption" />
83 </filter-search-item>
84 <template #button>
85 <export-button v-permission="['user-business-export']" :on-download="onExport" />
86 </template>
87 </filter-search>
88 <filter-table ref="tableRef" :loading="loading" :on-query="onQuery" hover-type="pointer" @row-click="onRowClick" @row-sort="onSort">
89 <user-table-column title="用户艺名" data-index="id" show-avatar :width="240" />
90 <enum-table-column title="性别" data-index="sex" :option="sexOption" :dark-value="0" :width="70" />
91 <filter-table-column title="用户邮箱" data-index="email" :width="170" />
92 <phone-table-column title="手机号码" data-index="phone" area-index="area_code" :width="160" />
93 <filter-table-column title="认证通过时间" data-index="audit_at" :width="170" has-sort />
94 <filter-table-column title="所属公司" data-index="company" :width="170" />
95 <enum-table-column title="权限" data-index="scope" :option="scopeOption" :dark-value="0" :width="120" />
96 <enum-table-column title="关注服务号" data-index="official_status" :option="officialStatusOption" :dark-value="0" :width="120" />
97 <enum-table-column title="状态" data-index="status" :option="statusOption" :width="80" />
98 </filter-table>
99 </page-view>
100 </template>
101
102 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import {
3 Tabs,
4 TabPane,
5 Row,
6 Col,
7 Avatar,
8 Form,
9 FormItem,
10 Divider,
11 ImagePreviewGroup,
12 Space,
13 Image,
14 RadioGroup,
15 Textarea,
16 Table,
17 TableColumn,
18 } from '@arco-design/web-vue';
19 import AudioPlayer from '@/components/audio-player/index.vue';
20 import { useCertifyApi } from '@/http/user';
21 import { Option } from '@/types/global';
22 import { ref } from 'vue';
23 import { Router } from 'vue-router';
24 import { FormInstance } from '@arco-design/web-vue/es/form';
25 import { UserCertify } from '@/utils/model';
26
27 const { statusOption } = useCertifyApi;
28
29 const props = defineProps<{
30 activeKey: string;
31 router: Router;
32 row: UserCertify;
33 platformOption: Option[];
34 certifyOption: Option[];
35 disabled?: boolean;
36 submit?: (value: { status: number; reason: string }) => Promise<UserCertify>;
37 close?: () => void;
38 }>();
39
40 const auditStatus = [
41 { value: 1, label: '通过' },
42 { value: 2, label: '拒绝' },
43 ];
44
45 const formRef = ref<FormInstance>();
46 const formValue = ref({ status: props.row.status || 0, reason: props.row.reason || '' });
47
48 const onSubmit = async () => {
49 return (await formRef.value?.validate()) ? Promise.reject() : props.submit?.(formValue.value);
50 };
51
52 // const router = useRouter();
53
54 const goUserDetailPage = (userId: any) => {
55 props.close?.();
56 const r = props.router;
57 if (props.row.user?.identity === 1) {
58 return r.push({ name: 'user-singer-show', params: { id: userId } });
59 }
60 if ([2, 3].includes(Number(props.row.user?.identity))) {
61 return r.push({ name: 'user-business-show', params: { id: userId } });
62 }
63 return r.push({ name: 'user-register-show', params: { id: userId } });
64 };
65
66 defineExpose({ onSubmit });
67 </script>
68
69 <template>
70 <Tabs :default-active-key="activeKey || 'audit'" type="rounded" size="small">
71 <TabPane key="audit" title="认证申请">
72 <Row align="center">
73 <Col flex="80px" style="margin-right: 20px; cursor: pointer">
74 <Avatar :size="80" :image-url="row.user?.avatar || ''" @click="goUserDetailPage(row.user?.id)" />
75 </Col>
76 <Col flex="auto">
77 <Form v-if="row.user" :model="row.user" auto-label-width>
78 <FormItem hide-label row-class="mb-0" @click="goUserDetailPage(row.user?.id)">
79 <div style="font-weight: bold; font-size: 18px; cursor: pointer"> {{ row.user.nick_name || '' }}</div>
80 <span style="font-size: 12px; margin-left: 10px; cursor: pointer">{{ row.user.real_name }}</span>
81 </FormItem>
82 <FormItem label="当前身份" show-colon row-class="mb-0">
83 <Space>
84 <template #split>
85 <Divider direction="vertical" />
86 </template>
87 <span v-if="row.user.identity === 0">未认证</span>
88 <span v-if="[1, 3].includes(row.user.identity)">音乐人</span>
89 <span v-if="[2, 3].includes(row.user.identity)">经纪人</span>
90 </Space>
91 </FormItem>
92 <FormItem label="当前能力" show-colon row-class="mb-0">
93 {{ row.user.auth_tags?.map((item) => item.name).join('|') }}
94 </FormItem>
95 </Form>
96 </Col>
97 </Row>
98
99 <Divider orientation="left">注册来源</Divider>
100
101 <Row align="center" style="margin-top: 14px">
102 <Col v-if="row.user?.register_type == 2">
103 <span class="span">注册来源(用户推荐):</span>
104 <span style="color: rgb(18, 92, 254, 1); cursor: pointer" @click="goUserDetailPage(row.user?.inviter.id)">{{
105 row.user?.inviter.nick_name
106 }}</span>
107 </Col>
108 <Col v-else-if="row.user?.register_type == 1">
109 <span class="span">注册来源(自行注册):</span>
110 <span class="span">{{ row.user?.register_remark }}</span>
111 </Col>
112 <Col v-else>
113 <span class="span">注册来源:无</span>
114 </Col>
115 </Row>
116
117 <Divider orientation="left">认证信息</Divider>
118 <Form ref="formRef" :model="formValue" :disabled="disabled || false" auto-label-width>
119 <FormItem label="平台用户名" show-colon>{{ row.nick_name }}</FormItem>
120 <FormItem label="所属平台" show-colon>
121 {{
122 platformOption
123 .filter((item) => row.platform.includes(String(item.value)))
124 ?.map((item) => item.label)
125 ?.join('|')
126 }}
127 </FormItem>
128 <FormItem label="代表作" show-colon>{{ row.works }}</FormItem>
129 <FormItem label="相关截图" show-colon>
130 <ImagePreviewGroup infinite>
131 <Space>
132 <Image v-for="item in row.img" :key="item" :width="60" :height="60" :src="item" />
133 </Space>
134 </ImagePreviewGroup>
135 </FormItem>
136 <FormItem label="认证能力" show-colon>
137 {{
138 certifyOption
139 .filter((item) => row.tags.includes(Number(item.value)))
140 ?.map((item) => item.label)
141 ?.join('|')
142 }}
143 </FormItem>
144 <FormItem label="附带音频" show-colon>
145 <Space direction="vertical" fill style="width: 100%">
146 <Row v-if="row.audio_info?.local" align="center">
147 <Col flex="auto">
148 <AudioPlayer :url="row.audio_info.local" />
149 </Col>
150 <Col flex="100px">
151 <div style="line-height: 30px; height: 36px; margin-left: 10px">本地音频</div>
152 </Col>
153 </Row>
154 <Row v-if="row.audio_info?.online" align="center">
155 <Col flex="auto">
156 <AudioPlayer :url="row.audio_info.online" />
157 </Col>
158 <Col flex="100px">
159 <div style="line-height: 30px; height: 36px; margin-left: 10px">在线演唱</div>
160 </Col>
161 </Row>
162 </Space>
163 </FormItem>
164 <FormItem
165 label="审核结果"
166 field="status"
167 show-colon
168 :rules="[{ type: 'number', required: true, min: 1, message: '请选择审核结果' }]"
169 >
170 <RadioGroup v-model="formValue.status" :options="auditStatus" />
171 </FormItem>
172 <FormItem
173 v-show="formValue.status === 2"
174 field="reason"
175 :hide-asterisk="true"
176 :rules="formValue.status === 2 ? [{ required: true, minLength: 1, message: '请输入拒绝理由' }] : []"
177 >
178 <Textarea v-model="formValue.reason" show-word-limit :max-length="100" :auto-size="{ minRows: 3, maxRows: 3 }" />
179 </FormItem>
180 </Form>
181 </TabPane>
182 <TabPane key="record" :title="`审核记录(${row.records.length})`">
183 <Table :data="row.records" :bordered="false" :table-layout-fixed="true" :pagination="false" :scroll="{ y: 800 }">
184 <template #columns>
185 <TableColumn data-index="id" title="序号" :width="60">
186 <template #cell="{ rowIndex }">
187 {{ row.records.length - rowIndex }}
188 </template>
189 </TableColumn>
190 <TableColumn title="操作人" data-index="operator_id">
191 <template #cell="{ record }">
192 <template v-if="record.operator">{{ `${record.operator.nick_name}(${record.operator.real_name})` }}</template>
193 </template>
194 </TableColumn>
195 <TableColumn title="操作时间" data-index="updated_at" />
196 <TableColumn title="操作结果" data-index="status">
197 <template #cell="{ record }">{{ statusOption.find((item) => item.value === record.status)?.label }}</template>
198 </TableColumn>
199 <TableColumn title="说明" data-index="reason" />
200 </template>
201 </Table>
202 </TabPane>
203 </Tabs>
204 </template>
205
206 <style scoped lang="less">
207 .arco-form-item {
208 margin-bottom: 10px;
209 }
210
211 .span {
212 color: rgb(78, 89, 105, 1);
213 font-size: 14px;
214 }
215 </style>
1 <script setup lang="ts" name="user-certify">
2 import { ref, onActivated, h } from 'vue';
3 import useLoading from '@/hooks/loading';
4 import { useCertifyApi } from '@/http/user';
5 import { AnyObject, Option } from '@/types/global';
6 import { useRoute, useRouter } from 'vue-router';
7 import EnumTableColumn from '@/components/filter/enum-table-column.vue';
8 import useConfigApi from '@/http/config';
9 import useTagApi from '@/http/Tag';
10 import { createModalVNode } from '@/utils/createVNode';
11 import { UserCertify } from '@/utils/model';
12 import { TableData } from '@arco-design/web-vue';
13 import UserTableColumn from '@/components/filter/user-table-column.vue';
14 import ModalContent from './components/modal-content.vue';
15 import { useAuthorizedStore } from '@/store';
16 import { promiseToBoolean } from '@/utils';
17 import usePermission from '@/hooks/permission';
18
19 const { sexOption, statusOption, get, update } = useCertifyApi;
20
21 const tableRef = ref();
22 const filter = ref<AnyObject>({});
23 const { loading, setLoading } = useLoading(false);
24
25 const platformOption = ref<Option[]>([]);
26 const certifyOption = ref<Option[]>([]);
27
28 const router = useRouter();
29
30 const onQuery = async (params?: AnyObject) => {
31 setLoading(true);
32 return get({ ...filter.value, ...params, sortBy: 'id', sortType: 'desc' }).finally(() => setLoading(false));
33 };
34
35 const onSearch = () => tableRef.value?.onPageChange(1);
36
37 const formatPlatform = (platform: string[]) => platformOption.value.filter((item) => platform.includes(String(item.value)));
38
39 const formatTag = (tag: number[]) => certifyOption.value.filter((item) => tag.includes(Number(item.value)));
40
41 const onReset = () => {
42 filter.value = { nick_name: '', sex: '', status: 0, createBetween: [] };
43 onSearch();
44 };
45
46 const onView = (record: UserCertify) =>
47 createModalVNode(
48 () =>
49 h(ModalContent, {
50 activeKey: 'record',
51 row: record,
52 disabled: true,
53 router,
54 platformOption: platformOption.value,
55 certifyOption: certifyOption.value,
56 }),
57 {
58 title: '认证审核',
59 titleAlign: 'center',
60 modalClass: 'retry',
61 escToClose: true,
62 maskClosable: true,
63 hideCancel: true,
64 }
65 );
66
67 const onAudit = (record: UserCertify) => {
68 const auditRef = ref<InstanceType<typeof ModalContent>>();
69 const modal = createModalVNode(
70 () =>
71 h(ModalContent, {
72 activeKey: 'audit',
73 ref: auditRef,
74 row: record,
75 router,
76 platformOption: platformOption.value,
77 certifyOption: certifyOption.value,
78 submit: (data: any) => update(record.id, data),
79 close: () => modal.close(),
80 }),
81 {
82 title: '认证审核',
83 titleAlign: 'center',
84 modalClass: 'retry',
85 onBeforeOk: () => promiseToBoolean(auditRef.value?.onSubmit()),
86 onOk: () => {
87 tableRef.value?.onFetch();
88 useAuthorizedStore().syncAuditUser();
89 },
90 }
91 );
92 };
93
94 const onRowClick = (record: TableData) => {
95 if (record.user?.identity === 1) {
96 return router.push({ name: 'user-singer-show', params: { id: record.user_id } });
97 }
98 if ([2, 3].includes(Number(record.user?.identity))) {
99 return router.push({ name: 'user-business-show', params: { id: record.user_id } });
100 }
101 return router.push({ name: 'user-register-show', params: { id: record.user_id } });
102 };
103
104 onActivated(() => {
105 if (useRoute().meta.reload) {
106 onReset();
107 useConfigApi.getOption({ parentKey: 'music_platform' }).then((data) => (platformOption.value = data));
108 useTagApi.getOption({ type: 4 }).then((data) => (certifyOption.value = data));
109 useAuthorizedStore().syncAuditUser();
110 }
111 });
112 </script>
113
114 <template>
115 <page-view has-card has-bread>
116 <filter-search :model="filter" :loading="loading" @search="onSearch" @reset="onReset">
117 <filter-search-item field="nick_name" label="用户艺名">
118 <a-input v-model="filter.nick_name" allow-clear placeholder="请输入" />
119 </filter-search-item>
120 <filter-search-item field="sex" label="性别">
121 <a-select v-model="filter.sex" allow-clear placeholder="请选择" :options="sexOption" />
122 </filter-search-item>
123 <filter-search-item field="createBetween" label="提交时间">
124 <a-range-picker
125 v-model="filter.createBetween"
126 :time-picker-props="{ defaultValue: ['00:00:00', '23:59:59'] }"
127 :day-start-of-week="1"
128 value-format="YYYY-MM-DD HH:mm:ss"
129 format="YYYY-MM-DD"
130 />
131 </filter-search-item>
132 <filter-search-item field="status" label="状态">
133 <a-select v-model="filter.status" allow-clear placeholder="请选择" :options="statusOption" />
134 </filter-search-item>
135 </filter-search>
136 <filter-table ref="tableRef" :loading="loading" :on-query="onQuery" @row-click="onRowClick">
137 <user-table-column title="用户艺名" data-index="user_id" user="user" show-avatar :width="240" />
138 <enum-table-column title="性别" data-index="user.sex" :option="sexOption" :dark-value="0" :width="70" />
139 <filter-table-column title="平台用户名称" data-index="nick_name" :width="120" />
140 <a-table-column title=" 所属平台" data-index="platform" :ellipsis="true">
141 <template #cell="{ record }">
142 <a-tag v-for="item in formatPlatform(record.platform)" :key="item.value" style="margin-right: 4px">
143 {{ item.label }}
144 </a-tag>
145 </template>
146 </a-table-column>
147 <filter-table-column title="代表作名称/链接" data-index="works" :width="160" />
148 <a-table-column title="认证能力" data-index="tags" :width="200" :ellipsis="true">
149 <template #cell="{ record }">
150 <a-tag v-for="item in formatTag(record.tags)" :key="item.value" style="margin-right: 4px">
151 {{ item.label }}
152 </a-tag>
153 </template>
154 </a-table-column>
155 <filter-table-column title="最后提交审核时间" data-index="created_at" :width="170" />
156 <enum-table-column title="最后审核状态" data-index="status" :option="statusOption" :width="120" />
157 <a-table-column
158 v-if="usePermission().checkPermission(['user-certify-audit', 'user-certify-show'])"
159 title="操作"
160 data-index="operation"
161 :width="100"
162 >
163 <template #cell="{ record }">
164 <a-link
165 v-if="record.status === 0"
166 v-permission="['user-certify-audit']"
167 class="link-hover"
168 :hoverable="false"
169 @click.stop="onAudit(record)"
170 >
171 审核
172 </a-link>
173 <a-link v-else v-permission="['user-certify-show']" class="link-hover" :hoverable="false" @click.stop="onView(record)"
174 >查看详情
175 </a-link>
176 </template>
177 </a-table-column>
178 </filter-table>
179 </page-view>
180 </template>
181
182 <style scoped lang="less"></style>
1 <template>
2 <router-view v-slot="{ Component, route }">
3 <keep-alive :exclude="['user-show']">
4 <component :is="Component" :key="route.path" />
5 </keep-alive>
6 </router-view>
7 </template>
8
9 <script lang="ts" setup>
10 import { onMounted } from 'vue';
11 import { useSelectionStore } from '@/store';
12
13 onMounted(() => useSelectionStore().queryProject());
14 </script>
15
16 <style lang="less" scoped>
17 .container {
18 padding: 0 30px 20px 20px;
19 }
20
21 .operations {
22 display: flex;
23 }
24 </style>
1 <script setup lang="ts">
2 import useUserApi from '@/http/user';
3 import { IconDown } from '@arco-design/web-vue/es/icon';
4 import { createModalVNode } from '@/utils/createVNode';
5 import { createVNode, onMounted, ref, toRef, computed } from 'vue';
6 import PasswordForm from '@/views/user/info/components/password-form.vue';
7 import UpdateForm from '@/views/user/info/components/update-form.vue';
8 import useTagApi from '@/http/Tag';
9 import { Option } from '@/types/global';
10 import { User } from '@/utils/model';
11 import UserImageDynamics from '@/views/user/info/components/user-image-dynamics.vue';
12 import UserAudioDynamics from '@/views/user/info/components/user-audio-dynamics.vue';
13 import UserVideoDynamics from '@/views/user/info/components/user-video-dynamics.vue';
14 import { useRoute, useRouter } from 'vue-router';
15 import { isArray } from '@/utils/is';
16 import usePermission from '@/hooks/permission';
17
18 const props = defineProps<{
19 user: User & { image_dynamic_count?: number; audio_dynamic_count?: number; video_dynamic_count?: number };
20 }>();
21 const user = toRef(props, 'user');
22 const { scopeOption, statusOption, sexOption, update, destroy, changeStatus, changePwd } = useUserApi;
23
24 const coverVisible = ref(false);
25
26 const authTagOptions = ref<Option[]>([]);
27
28 onMounted(() => useTagApi.getOption({ type: 4 }).then((data) => (authTagOptions.value = data)));
29
30 const onViewImage = () =>
31 createModalVNode(() => createVNode(UserImageDynamics, { userKey: user.value.id }), {
32 title: 'Ta的相册',
33 titleAlign: 'center',
34 closable: true,
35 footer: false,
36 escToClose: true,
37 width: '780px',
38 });
39
40 const onViewAudio = () =>
41 createModalVNode(() => createVNode(UserAudioDynamics, { userKey: user.value.id }), {
42 title: 'Ta的音频',
43 titleAlign: 'center',
44 closable: true,
45 footer: false,
46 escToClose: true,
47 bodyStyle: { padding: '0 !important' },
48 width: '540px',
49 });
50
51 const onViewVideo = () =>
52 createModalVNode(() => createVNode(UserVideoDynamics, { userKey: user.value.id }), {
53 title: 'Ta的视频',
54 titleAlign: 'center',
55 closable: true,
56 footer: false,
57 escToClose: true,
58 width: '780px',
59 });
60 const onOperation = (value: string) => {
61 // eslint-disable-next-line default-case
62 switch (value) {
63 case 'STATUS_ON':
64 createModalVNode('是否确认将该用户启用?', {
65 title: '状态变更',
66 onBeforeOk: (done: any) =>
67 changeStatus(user.value?.id || 0, 1)
68 .then(() => {
69 user.value.status = 1;
70 done(true);
71 })
72 .catch(() => done(false)),
73 });
74 break;
75 case 'STATUS_OFF':
76 createModalVNode('是否确认将该用户禁用?', {
77 title: '状态变更',
78 onBeforeOk: (done: any) =>
79 changeStatus(user.value?.id || 0, 0)
80 .then(() => {
81 user.value.status = 0;
82 done(true);
83 })
84 .catch(() => done(false)),
85 });
86 break;
87 case 'CHANGE_PWD':
88 // eslint-disable-next-line no-case-declarations
89 const pwdRef = ref<InstanceType<typeof PasswordForm>>();
90 createModalVNode(() => createVNode(PasswordForm, { ref: pwdRef, onRequest: (data: any) => changePwd(user.value?.id, data) }), {
91 title: '重置密码',
92 onBeforeOk: (done: any) =>
93 pwdRef.value
94 ?.onSubmit()
95 .then(() => done(true))
96 .catch(() => done(false)),
97 });
98 break;
99 case 'UPDATE':
100 // eslint-disable-next-line no-case-declarations
101 const updateRef = ref<InstanceType<typeof UpdateForm>>();
102 createModalVNode(
103 () =>
104 createVNode(UpdateForm, {
105 ref: updateRef,
106 row: user.value,
107 authTagOptions: authTagOptions.value,
108 onRequest: (data: any) => update(user.value?.id, data),
109 }),
110 {
111 title: '编辑信息',
112 modalClass: 'update-modal',
113 onBeforeOk: (done: any) =>
114 updateRef.value
115 ?.onSubmit()
116 .then((attribute: User) => {
117 Object.assign(user.value, attribute);
118 done(true);
119 })
120 .catch(() => done(false)),
121 }
122 );
123 break;
124 case 'DELETE':
125 createModalVNode('是否确认将该用户注销?', {
126 title: '状态变更',
127 onBeforeOk: (done: any) =>
128 destroy(user.value?.id)
129 .then(() => {
130 user.value.status = 2;
131 done(true);
132 })
133 .catch(() => done(false)),
134 });
135 break;
136 }
137 };
138
139 const routeName = useRoute().name as string;
140 const router = useRouter();
141 const checkPermission = (permission: string | string[]): boolean => {
142 permission = isArray(permission) ? permission.map((item) => `${routeName}-${item}`) : `${routeName}-${permission}`;
143 return usePermission().checkPermission(permission);
144 };
145 const hasPermission = computed(() => checkPermission(['update', 'status', 'password', 'delete']));
146
147 const goUserDetailPage = (user: any) => {
148 if (user.identity === 1) {
149 return router.push({ name: 'user-singer-show', params: { id: user.id } });
150 }
151 if ([2, 3].includes(Number(user.identity))) {
152 return router.push({ name: 'user-business-show', params: { id: user.id } });
153 }
154 return router.push({ name: 'user-register-show', params: { id: user.id } });
155 };
156 </script>
157
158 <template>
159 <a-card :bordered="false">
160 <a-form style="margin-top: 16px" :model="user" size="small" auto-label-width>
161 <a-row justify="start" align="start">
162 <a-col flex="120px">
163 <a-avatar :size="120" :image-url="user.avatar" />
164 </a-col>
165 <a-col flex="1">
166 <a-row :gutter="16">
167 <a-col :span="8">
168 <a-form-item label="用户艺名" :show-colon="true">
169 <a-typography-paragraph type="secondary" :ellipsis="{ rows: 1, showTooltip: true }">
170 {{ `${user.nick_name}(${user.real_name})` }}
171 </a-typography-paragraph>
172 </a-form-item>
173 </a-col>
174 <a-col :span="8">
175 <a-form-item label="关注服务号" :show-colon="true">
176 <span v-if="user.official_status === 1">已关注</span>
177 <span v-else style="color: rgba(44, 44, 44, 0.5)">未关注</span>
178 </a-form-item>
179 </a-col>
180 <a-col :span="6">
181 <a-form-item label="状态" :show-colon="true">
182 {{ statusOption.find((item) => item.value === user.status)?.label }}
183 </a-form-item>
184 </a-col>
185 <a-col :span="2">
186 <a-dropdown v-if="hasPermission && [0, 1].includes(user.status)" :popup-max-height="false" @select="onOperation">
187 <a-button>
188 操作
189 <icon-down style="margin-left: 4px" />
190 </a-button>
191 <template #content>
192 <a-doption v-if="checkPermission('status') && user.status === 0" value="STATUS_ON">启用账号</a-doption>
193 <a-doption v-if="checkPermission('status') && user.status === 1" value="STATUS_OFF">禁用账号</a-doption>
194 <a-doption v-if="checkPermission('update')" value="UPDATE">编辑信息</a-doption>
195 <a-doption v-if="checkPermission('password')" value="CHANGE_PWD">重置密码</a-doption>
196 <a-doption v-if="checkPermission('delete')" value="DELETE">注销账号</a-doption>
197 </template>
198 </a-dropdown>
199 </a-col>
200 <a-col :span="8">
201 <a-form-item label="性别" :show-colon="true">
202 <span v-if="user.sex === 0" style="color: rgba(44, 44, 44, 0.5)"></span>
203 <span v-else>{{ sexOption.find((item) => item.value === user.sex)?.label }}</span>
204 </a-form-item>
205 </a-col>
206 <a-col :span="8">
207 <a-form-item label="邮箱" :show-colon="true">
208 <span v-if="user.email">{{ user.email }}</span>
209 <span v-else style="color: rgba(44, 44, 44, 0.5)"></span>
210 </a-form-item>
211 </a-col>
212 <a-col :span="8">
213 <a-form-item label="手机号" :show-colon="true">
214 <span v-if="user.phone"> {{ `(+${user.area_code}) ${user.phone}` }}</span>
215 <span v-else style="color: rgba(44, 44, 44, 0.5)"></span>
216 </a-form-item>
217 </a-col>
218 <a-col :span="8">
219 <a-form-item label="经纪人" :show-colon="true">
220 <user-link v-if="user?.business" :user="user.business" />
221 <span v-else style="color: rgba(44, 44, 44, 0.5)"></span>
222 </a-form-item>
223 </a-col>
224 <a-col :span="8">
225 <a-form-item label="用户认证" :show-colon="true">
226 <span v-if="user.auth_tags?.length === 0">
227 <span style="color: rgba(44, 44, 44, 0.5)"></span>
228 <a-divider v-if="user.auth_tags && [2, 3].includes(user.identity)" direction="vertical" />
229 <span v-if="[2, 3].includes(user.identity)"> 经纪人</span>
230 </span>
231 <a-typography-paragraph v-else type="secondary" :ellipsis="{ rows: 1, showTooltip: true }">
232 <span v-if="user.auth_tags?.length !== 0">音乐人{{ user.auth_tags?.map((item) => `【${item.name}】`).join('') }} </span>
233 <a-divider v-if="user.auth_tags && [2, 3].includes(user.identity)" direction="vertical" />
234 <span v-if="[2, 3].includes(user.identity)"> 经纪人</span>
235 </a-typography-paragraph>
236 </a-form-item>
237 </a-col>
238 <a-col :span="8">
239 <a-form-item label="公司" :show-colon="true">
240 <a-typography-paragraph v-if="user.company" type="secondary" :ellipsis="{ rows: 1, showTooltip: true }">
241 {{ user.company }}
242 </a-typography-paragraph>
243 <span v-else style="color: rgba(44, 44, 44, 0.5)"></span>
244 </a-form-item>
245 </a-col>
246 <a-col :span="8">
247 <a-form-item label="常居地" :show-colon="true">
248 <span v-if="!user.province && !user.city" style="color: rgba(44, 44, 44, 0.5)"></span>
249 <span v-else>{{ [user.province, user.city].filter((item) => item).join('-') }}</span>
250 </a-form-item>
251 </a-col>
252 <a-col :span="8">
253 <a-form-item label="擅长曲风" :show-colon="true">
254 <span v-if="user.style_tags?.length === 0" style="color: rgba(44, 44, 44, 0.5)"></span>
255 <span v-else>{{ user.style_tags?.map((item) => item.name).join('、') }}</span>
256 </a-form-item>
257 </a-col>
258 <a-col :span="8">
259 <a-form-item label="权限" :show-colon="true">
260 <span v-if="user.scope === 0" style="color: rgba(44, 44, 44, 0.5)"></span>
261 <span v-else>{{ scopeOption.find((item) => item.value === user.scope)?.label }}</span>
262 </a-form-item>
263 </a-col>
264 <a-col :span="8">
265 <a-form-item label="Ta的相册" show-colon>
266 <a-link v-if="user.image_dynamic_count" :hoverable="false" @click="onViewImage">查看</a-link>
267 <span v-else style="color: rgba(44, 44, 44, 0.5)"></span>
268 </a-form-item>
269 </a-col>
270 <a-col :span="8">
271 <a-form-item label="Ta的音频" show-colon>
272 <a-link v-if="user.audio_dynamic_count" :hoverable="false" @click="onViewAudio">查看</a-link>
273 <span v-else style="color: rgba(44, 44, 44, 0.5)"></span>
274 </a-form-item>
275 </a-col>
276 <a-col :span="8">
277 <a-form-item label="Ta的视频" :show-colon="true">
278 <a-link v-if="user.video_dynamic_count" :hoverable="false" @click="onViewVideo">查看</a-link>
279 <span v-else style="color: rgba(44, 44, 44, 0.5)"></span>
280 </a-form-item>
281 </a-col>
282 <a-col :span="8">
283 <a-form-item label="头部封面" show-colon>
284 <a-link :hoverable="false" @click="coverVisible = !coverVisible">查看</a-link>
285 </a-form-item>
286 </a-col>
287 <a-col :span="12">
288 <a-form-item v-if="user.register_type == 2" label="来源渠道(用户推荐)" :show-colon="true">
289 <a-link :hoverable="false" @click="goUserDetailPage(user.inviter)">{{ user.inviter?.nick_name }}</a-link>
290 </a-form-item>
291 <a-form-item v-else-if="user.register_type == 1" label="来源渠道(自行注册)" :show-colon="true">
292 <span>{{ user.register_remark }}</span>
293 </a-form-item>
294 <a-form-item v-else label="来源渠道" :show-colon="true">
295 <span style="color: rgba(44, 44, 44, 0.5)"></span>
296 </a-form-item>
297 </a-col>
298 <a-col :span="24">
299 <a-form-item label="简介" :show-colon="true">
300 <span v-if="user.intro">{{ user.intro }}</span>
301 <span v-else style="color: rgba(44, 44, 44, 0.5)"></span>
302 </a-form-item>
303 </a-col>
304 </a-row>
305 </a-col>
306 </a-row>
307 </a-form>
308
309 <a-image-preview
310 v-model:visible="coverVisible"
311 :closable="false"
312 :src="`${user.cover}?x-oss-process=image/format,webp`"
313 :actions-layout="[]"
314 />
315 </a-card>
316 </template>
317
318 <style scoped lang="less">
319 :deep(.arco-typography) {
320 text-align: left;
321 margin-bottom: 0;
322 width: 100%;
323 }
324
325 :deep(.arco-form-item) {
326 margin-bottom: 8px;
327 }
328 </style>
1 <script setup lang="ts">
2 import { onMounted, ref } from 'vue';
3 import useLoading from '@/hooks/loading';
4 import { AnyObject } from '@/types/global';
5
6 import UserTableColumn from '@/components/filter/user-table-column.vue';
7 import EnumTableColumn from '@/components/filter/enum-table-column.vue';
8 import PhoneTableColumn from '@/components/filter/phone-table-column.vue';
9 import { TableData } from '@arco-design/web-vue';
10 import { useRoute, useRouter } from 'vue-router';
11 import useUserApi, { useMemberApi } from '@/http/user';
12 import { createModalVNode } from '@/utils/createVNode';
13 import { isArray } from '@/utils/is';
14 import usePermission from '@/hooks/permission';
15
16 const router = useRouter();
17 const props = defineProps<{ userKey: number }>();
18
19 const tableRef = ref();
20 const filter = ref<AnyObject>({});
21 const { loading, setLoading } = useLoading(false);
22 const total = ref(0);
23
24 const { officialStatusOption, statusOption, scopeOption, sexOption } = useUserApi;
25
26 const { get, destroy } = useMemberApi;
27
28 const onQuery = async (params?: AnyObject) => {
29 setLoading(true);
30
31 get(props.userKey, { pageSize: 1 }).then(({ meta }) => (total.value = meta.total));
32
33 return get(props.userKey, { ...filter.value, ...params, sortBy: 'id', sortType: 'desc' }).finally(() => setLoading(false));
34 };
35
36 const onRowClick = (record: TableData) => {
37 const identity = record.member?.identity || 0;
38
39 if (identity) {
40 router.push({ name: 'user-register-show', params: { id: record.member_id } });
41 }
42 if ([2, 3].includes(identity)) {
43 router.push({ name: 'user-business-show', params: { id: record.member_id } });
44 }
45
46 router.push({ name: 'user-register-show', params: { id: record.member_id } });
47 };
48
49 const onSearch = () => tableRef.value?.onPageChange(1);
50
51 const onReset = () => {
52 filter.value = {
53 member_nick_name: '',
54 member_email_like: '',
55 member_phone_like: '',
56 member_official_status: '',
57 member_status: '',
58 };
59 onSearch();
60 };
61
62 const onDelete = (record: TableData) =>
63 createModalVNode(`确认移除此用户?`, {
64 title: '删除操作',
65 onBeforeOk: (done: any) =>
66 destroy(record.member_id)
67 .then(() => {
68 tableRef.value?.onFetch();
69 done(true);
70 })
71 .catch(() => done(false)),
72 });
73
74 onMounted(() => onReset());
75
76 defineExpose({ total });
77
78 const routeName = useRoute().name as string;
79 const checkPermission = (permission: string | string[]): boolean => {
80 permission = isArray(permission) ? permission.map((item) => `${routeName}-${item}`) : `${routeName}-${permission}`;
81 return usePermission().checkPermission(permission);
82 };
83 </script>
84
85 <template>
86 <a-card :bordered="false">
87 <filter-search :model="filter" :loading="loading" @search="onSearch" @reset="onReset">
88 <filter-search-item field="member_nick_name" label="用户艺名">
89 <a-input v-model="filter.member_nick_name" allow-clear placeholder="请输入" />
90 </filter-search-item>
91 <filter-search-item field="member_email_like" label="用户邮箱">
92 <a-input v-model="filter.member_email_like" allow-clear placeholder="请输入" />
93 </filter-search-item>
94 <filter-search-item field="member_phone_like" label="手机号码">
95 <a-input v-model="filter.member_phone_like" allow-clear placeholder="请输入" />
96 </filter-search-item>
97 <filter-search-item field="member_official_status" label="关注服务号">
98 <a-select v-model="filter.member_official_status" :options="officialStatusOption" placeholder="请输入" allow-clear />
99 </filter-search-item>
100 <filter-search-item field="member_status" label="状态">
101 <a-select v-model="filter.member_status" :options="statusOption" placeholder="请输入" allow-clear />
102 </filter-search-item>
103 </filter-search>
104
105 <filter-table ref="tableRef" :loading="loading" :on-query="onQuery" hover-type="pointer" @row-click="onRowClick">
106 <user-table-column title="用户艺名" data-index="member_id" user="member" :width="260" show-avatar />
107 <enum-table-column title="性别" data-index="member.sex" :option="sexOption" :dark-value="0" :width="70" />
108 <filter-table-column title="用户邮箱" data-index="member.email" :width="170" />
109 <phone-table-column title="手机号码" data-index="member.phone" area-index="member.area_code" :width="160" />
110 <enum-table-column title="权限" data-index="member.scope" :option="scopeOption" :dark-value="0" :width="120" />
111 <enum-table-column
112 title="关注服务号"
113 data-index="member.official_status"
114 :option="officialStatusOption"
115 :dark-value="0"
116 :width="120"
117 />
118 <enum-table-column title="状态" data-index="member.status" :option="statusOption" :width="80" />
119 <a-table-column v-if="checkPermission('member-delete')" title="操作" data-index="operation" :width="100">
120 <template #cell="{ record }">
121 <a-link v-if="checkPermission('member-delete')" @click.stop="onDelete(record)">删除</a-link>
122 </template>
123 </a-table-column>
124 </filter-table>
125 </a-card>
126 </template>
127
128 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { AnyObject } from '@/types/global';
3 import useLoading from '@/hooks/loading';
4 import useUserApi from '@/http/user';
5 import { onMounted, ref } from 'vue';
6 import NumberTableColumn from '@/components/filter/number-table-column.vue';
7 import useActivityApi from '@/http/activity';
8 import { useRouter } from 'vue-router';
9 import { TableData } from '@arco-design/web-vue';
10 import UserTableColumn from '@/components/filter/user-table-column.vue';
11 import ActivityTableColumn from '@/components/filter/activity-table-column.vue';
12 import DateTableColumn from '@/components/filter/date-table-column.vue';
13 import EnumTableColumn from '@/components/filter/enum-table-column.vue';
14
15 const router = useRouter();
16 const props = defineProps<{ userKey: number }>();
17
18 const tableRef = ref();
19 const { loading, setLoading } = useLoading(false);
20 const filter = ref({ song_name: '', project_name: '', tag_name: '', status: '' });
21
22 const total = ref(0);
23
24 const onQuery = async (params?: AnyObject) => {
25 setLoading(true);
26 useUserApi.likeSongs(props.userKey, { pageSize: 1, setColumn: ['id'] }).then(({ meta }) => (total.value = meta.total));
27
28 return useUserApi.likeSongs(props.userKey, { ...filter.value, ...params, setSort: ['-like_at'] }).finally(() => setLoading(false));
29 };
30
31 const onSearch = () => tableRef.value?.onPageChange(1);
32
33 const onRowClick = (record: TableData) => router.push({ name: 'audition-activity-show', params: { id: record.id } });
34
35 const onReset = () => {
36 filter.value = { song_name: '', project_name: '', tag_name: '', status: '' };
37 onSearch();
38 };
39
40 onMounted(() => onReset());
41
42 defineExpose({ total });
43 </script>
44
45 <template>
46 <a-card :bordered="false">
47 <filter-search :model="filter" :loading="loading" @search="onSearch" @reset="onReset">
48 <filter-search-item label="歌曲名称" filed="song_name">
49 <a-input v-model="filter.song_name" allow-clear placeholder="请输入" />
50 </filter-search-item>
51 <filter-search-item label="厂牌名称" filed="project_name">
52 <a-input v-model="filter.project_name" allow-clear placeholder="请输入" />
53 </filter-search-item>
54 <filter-search-item label="标签名称" filed="tag_name">
55 <a-input v-model="filter.tag_name" allow-clear placeholder="请输入" />
56 </filter-search-item>
57 <filter-search-item label="状态" filed="status">
58 <a-select v-model="filter.status" :options="useActivityApi.statusOption" allow-clear placeholder="请选择" />
59 </filter-search-item>
60 </filter-search>
61 <filter-table ref="tableRef" :loading="loading" :on-query="onQuery" hover-type="pointer" @row-click="onRowClick">
62 <activity-table-column data-index="id" title="试唱歌曲" />
63 <number-table-column :width="120" data-index="view_count" title="试听人数" />
64 <number-table-column :width="120" data-index="like_count" title="收藏人数" />
65 <number-table-column :width="120" data-index="submit_work_count" title="提交人数" />
66 <user-table-column :width="140" data-index="user_id" user="user" title="创建人" />
67 <date-table-column :width="110" data-index="like_at" title="收藏时间" />
68 <enum-table-column :width="110" data-index="status" title="状态" :option="useActivityApi.statusOption" />
69 </filter-table>
70 </a-card>
71 </template>
72
73 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { AnyObject } from '@/types/global';
3 import useLoading from '@/hooks/loading';
4 import { onMounted, ref } from 'vue';
5 import useUserApi from '@/http/user';
6 import useActivityApi from '@/http/activity';
7 import NumberTableColumn from '@/components/filter/number-table-column.vue';
8 import UserTableColumn from '@/components/filter/user-table-column.vue';
9 import { useRouter } from 'vue-router';
10 import { TableData } from '@arco-design/web-vue';
11 import EnumTableColumn from '@/components/filter/enum-table-column.vue';
12 import DateTableColumn from '@/components/filter/date-table-column.vue';
13 import ActivityTableColumn from '@/components/filter/activity-table-column.vue';
14
15 const router = useRouter();
16 const props = defineProps<{ userKey: number }>();
17
18 const tableRef = ref();
19 const { loading, setLoading } = useLoading(false);
20 const filter = ref({ song_name: '', project_name: '', tag_name: '', status: '' });
21
22 const total = ref(0);
23
24 const onQuery = async (params?: AnyObject) => {
25 setLoading(true);
26 useUserApi.listenSongs(props.userKey, { pageSize: 1 }).then(({ meta }) => (total.value = meta.total));
27
28 return useUserApi
29 .listenSongs(props.userKey, { ...filter.value, ...params, sortBy: 'listen_at', sortType: 'desc' })
30 .finally(() => setLoading(false));
31 };
32
33 const onSearch = () => tableRef.value?.onPageChange(1);
34
35 const onRowClick = (record: TableData) => router.push({ name: 'audition-activity-show', params: { id: record.id } });
36
37 const onReset = () => {
38 filter.value = { song_name: '', project_name: '', tag_name: '', status: '' };
39 onSearch();
40 };
41
42 onMounted(() => onReset());
43
44 defineExpose({ total });
45 </script>
46
47 <template>
48 <a-card :bordered="false">
49 <filter-search :model="filter" :loading="loading" @search="onSearch" @reset="onReset">
50 <filter-search-item label="歌曲名称" filed="song_name">
51 <a-input v-model="filter.song_name" allow-clear placeholder="请输入" />
52 </filter-search-item>
53 <filter-search-item label="厂牌名称" filed="project_name">
54 <a-input v-model="filter.project_name" allow-clear placeholder="请输入" />
55 </filter-search-item>
56 <filter-search-item label="标签名称" filed="tag_name">
57 <a-input v-model="filter.tag_name" allow-clear placeholder="请输入" />
58 </filter-search-item>
59 <filter-search-item label="状态" filed="status">
60 <a-select v-model="filter.status" :options="useActivityApi.statusOption" allow-clear placeholder="请选择" />
61 </filter-search-item>
62 </filter-search>
63 <filter-table ref="tableRef" :loading="loading" :on-query="onQuery" hover-type="pointer" @row-click="onRowClick">
64 <activity-table-column data-index="id" title="试唱歌曲" />
65 <number-table-column :width="120" data-index="view_count" title="试听人数" />
66 <number-table-column :width="120" data-index="like_count" title="收藏人数" />
67 <number-table-column :width="120" data-index="submit_work_count" title="提交人数" />
68 <user-table-column :width="140" data-index="user_id" user="user" title="创建人" />
69 <date-table-column :width="110" data-index="listen_at" title="试听时间" />
70 <enum-table-column :width="110" data-index="status" title="状态" :option="useActivityApi.statusOption" />
71 </filter-table>
72 </a-card>
73 </template>
74
75 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { computed, onMounted, ref } from 'vue';
3 import PageView from '@/components/page-view/index.vue';
4 import FilterSearch from '@/components/filter/search.vue';
5 import FilterSearchItem from '@/components/filter/search-item.vue';
6 import FilterTable from '@/components/filter/table.vue';
7 import { Input, Select, TableColumn, Link, Message, TableData } from '@arco-design/web-vue';
8 import { AnyObject } from '@/types/global';
9 import useLoading from '@/hooks/loading';
10 import useActivityApi, { useManagerApi } from '@/http/activity';
11 import useUserApi from '@/http/user';
12 import EnumTableColumn from '@/components/filter/enum-table-column.vue';
13 import NumberTableColumn from '@/components/filter/number-table-column.vue';
14 import SpaceTableColumn from '@/components/filter/space-table-column.vue';
15
16 import { createFormVNode, createModalVNode, createSelectionFormItemVNode } from '@/utils/createVNode';
17 import { useRoute } from 'vue-router';
18 import { isArray } from '@/utils/is';
19 import usePermission from '@/hooks/permission';
20
21 const { loading, setLoading } = useLoading(false);
22 const filter = ref({ projectName: '', songName: '', permission: '', activityStatus: '' });
23 const tableRef = ref();
24 const total = ref(0);
25
26 const props = defineProps<{ userKey: number }>();
27 const { statusOption } = useActivityApi;
28 const { permissionOption, update, destroy } = useManagerApi;
29
30 const formatPermission = (permission: any[]) =>
31 permission
32 .map((item) => `[${permissionOption.find((option) => option.value === item)?.label}]` || '')
33 .filter((item) => item.length !== 0)
34 .join('');
35
36 const onQuery = async (params?: AnyObject) => {
37 setLoading(true);
38 useUserApi
39 .manageSongs(props.userKey, { sortBy: 'activity_publish_at', sortType: 'desc' })
40 .then(({ meta }) => (total.value = meta.total));
41
42 return useUserApi
43 .manageSongs(props.userKey, { ...filter.value, ...params, sortBy: 'activity_publish_at', sortType: 'desc' })
44 .finally(() => setLoading(false));
45 };
46
47 const onSearch = () => tableRef.value?.onPageChange(1);
48
49 const onReset = () => {
50 filter.value = { projectName: '', songName: '', permission: '', activityStatus: '' };
51 onSearch();
52 };
53
54 onMounted(() => onReset());
55
56 defineExpose({ total });
57
58 const onUpdate = (record: TableData) => {
59 const formValue = ref<string[]>(record?.permission || ['view']);
60 return createModalVNode(
61 () =>
62 createFormVNode(
63 { layout: 'vertical', model: {} },
64 createSelectionFormItemVNode(
65 formValue,
66 permissionOption,
67 { label: '设置用户权限', rowClass: 'mb-0' },
68 {
69 'multiple': true,
70 'onUpdate:modelValue': (val?: string[]) => {
71 if (!val?.includes('view')) {
72 val?.unshift('view');
73 }
74 formValue.value = val || [];
75 },
76 }
77 )
78 ),
79 {
80 title: '修改',
81 titleAlign: 'center',
82 onBeforeOk: (done: any) =>
83 update(record.id, { permission: formValue.value })
84 .then(() => {
85 Message.success('更新成功');
86 tableRef.value?.onFetch();
87 done(true);
88 })
89 .catch(() => done(false)),
90 }
91 );
92 };
93
94 const onDelete = (record: TableData) => {
95 createModalVNode(`确认移除此用户在歌曲《${record.activity_name}》的外部管理员身份?`, {
96 title: '删除操作',
97 onBeforeOk: (done: any) =>
98 destroy(record.id)
99 .then(() => {
100 Message.success('删除成功');
101 tableRef.value?.onFetch();
102 done(true);
103 })
104 .catch(() => done(false)),
105 });
106 };
107
108 const routeName = useRoute().name as string;
109 const checkPermission = (permission: string | string[]): boolean => {
110 permission = isArray(permission) ? permission.map((item) => `${routeName}-${item}`) : `${routeName}-${permission}`;
111 return usePermission().checkPermission(permission);
112 };
113 const hasPermission = computed(() => checkPermission(['manage-activity-edit', 'manage-activity-delete']));
114 </script>
115
116 <template>
117 <PageView>
118 <FilterSearch :loading="loading" :model="filter" @search="onSearch" @reset="onReset">
119 <FilterSearchItem label="厂牌名称" filed="projectName">
120 <Input v-model="filter.projectName" allow-clear placeholder="请输入" />
121 </FilterSearchItem>
122 <FilterSearchItem label="歌曲名称" filed="projectName">
123 <Input v-model="filter.songName" allow-clear placeholder="请输入" />
124 </FilterSearchItem>
125 <FilterSearchItem label="状态" filed="projectName">
126 <Select v-model="filter.activityStatus" :options="statusOption" allow-clear placeholder="请选择" />
127 </FilterSearchItem>
128 <FilterSearchItem label="外部权限" filed="projectName">
129 <Select v-model="filter.permission" :options="permissionOption" allow-clear placeholder="请选择" />
130 </FilterSearchItem>
131 </FilterSearch>
132 <FilterTable ref="tableRef" :loading="loading" :on-query="onQuery">
133 <TableColumn title="厂牌名称" data-index="project.name" :width="160" :ellipsis="true" :tooltip="true" />
134 <TableColumn title="歌曲名称" data-index="activity_name" :width="160" :ellipsis="true" :tooltip="true" />
135 <NumberTableColumn title="试听人数" data-index="view_count" :width="100" :dark-value="0" />
136 <NumberTableColumn title="收藏人数" data-index="like_count" :width="100" :dark-value="0" />
137 <NumberTableColumn title="提交人数" data-index="submit_work_count" :width="100" :dark-value="0" />
138 <TableColumn title="上架时间" data-index="activity_publish_at" :width="170" />
139 <TableColumn title="外部权限" data-index="permission" :width="200" :ellipsis="true" :tooltip="true">
140 <template #cell="{ record }"> {{ formatPermission(record.permission) }}</template>
141 </TableColumn>
142 <EnumTableColumn title="状态" data-index="activity_status" :option="statusOption" :width="120" />
143 <SpaceTableColumn v-if="hasPermission" :width="120" data-index="operations" title="操作" :tooltip="false" :ellipsis="false">
144 <template #default="{ record }">
145 <Link v-if="checkPermission('manage-activity-edit')" :hoverable="false" @click="onUpdate(record)">修改</Link>
146 <Link v-if="checkPermission('manage-activity-delete')" :hoverable="false" @click="onDelete(record)">取消管理</Link>
147 </template>
148 </SpaceTableColumn>
149 </FilterTable>
150 </PageView>
151 </template>
152
153 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { onMounted, ref } from 'vue';
3 import useLoading from '@/hooks/loading';
4 import { AnyObject } from '@/types/global';
5 import useProjectApi from '@/http/project';
6 import NumberTableColumn from '@/components/filter/number-table-column.vue';
7 import EnumTableColumn from '@/components/filter/enum-table-column.vue';
8 import UserTableColumn from '@/components/filter/user-table-column.vue';
9
10 const props = defineProps<{ userKey: number }>();
11
12 const tableRef = ref();
13 const { loading, setLoading } = useLoading(false);
14 const filter = ref({ name: '', is_promote: '', tag_name: '', status: '' });
15
16 const { statusOption } = useProjectApi;
17
18 const total = ref(0);
19
20 const onQuery = async (params?: AnyObject) => {
21 setLoading(true);
22 useProjectApi.get({ manager: props.userKey, pageSize: 1 }).then(({ meta }) => (total.value = meta.total));
23
24 return useProjectApi
25 .get({
26 manager: props.userKey,
27 ...filter.value,
28 ...params,
29 setWithCount: ['activities', 'activity_up', 'activity_match', 'activity_down', 'activity_send', 'manage', 'member'],
30 sortBy: 'id',
31 sortType: 'desc',
32 })
33 .finally(() => setLoading(false));
34 };
35
36 const onSearch = () => tableRef.value?.onPageChange(1);
37
38 const onReset = () => {
39 filter.value = { name: '', is_promote: '', tag_name: '', status: '' };
40 onSearch();
41 };
42
43 onMounted(() => onReset());
44
45 defineExpose({ total });
46 </script>
47
48 <template>
49 <a-card :bordered="false">
50 <filter-search :model="filter" :loading="loading" inline @search="onSearch" @reset="onReset">
51 <filter-search-item label="厂牌名称" filed="name">
52 <a-input v-model="filter.name" allow-clear placeholder="请输入" />
53 </filter-search-item>
54 <filter-search-item label="状态">
55 <a-select v-model="filter.status" :options="statusOption" allow-clear placeholder="请选择" />
56 </filter-search-item>
57 </filter-search>
58 <filter-table ref="tableRef" :loading="loading" :on-query="onQuery">
59 <user-table-column data-index="id" nick-index="name" avatar-index="cover" title="厂牌名称" show-avatar :width="200" />
60 <filter-table-column data-index="intro" title="简介" :width="200" />
61 <number-table-column data-index="activities_count" title="试唱歌曲总数" :width="100" :dark-value="0" />
62 <number-table-column data-index="activity_up_count" title="试唱进行中" :width="100" :dark-value="0" />
63 <number-table-column data-index="activity_match_count" title="试唱已匹配" :width="100" :dark-value="0" />
64 <number-table-column data-index="activity_down_count" title="试唱已下架" :width="100" :dark-value="0" />
65 <number-table-column data-index="activity_send_count" title="歌曲已发行" :width="100" :dark-value="0" />
66 <number-table-column data-index="manage_count" title="管理员人数" :width="100" :dark-value="0" />
67 <enum-table-column data-index="status" title="状态" :width="70" :option="statusOption" :dark-value="0" />
68 </filter-table>
69 </a-card>
70 </template>
71
72 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { ref } from 'vue';
3 import { Form, FormItem, InputPassword } from '@arco-design/web-vue';
4 import { FormInstance } from '@arco-design/web-vue/es/form';
5
6 type formValueProp = {
7 password: string;
8 password_confirmation: string;
9 };
10
11 const props = defineProps<{ onRequest: (data: formValueProp) => Promise<void> }>();
12
13 const formRef = ref<FormInstance>();
14 const formValue = ref({ password: '', password_confirmation: '' });
15 const rules = {
16 password: [
17 { required: true, message: '请输入新密码' },
18 { minLength: 6, message: '新密码不能低于6位' },
19 { maxLength: 20, message: '新的密码不能超过20位' },
20 ],
21 password_confirmation: [
22 { required: true, message: '请输入确认密码' },
23 { minLength: 6, message: '新密码不能低于6位' },
24 { maxLength: 20, message: '新的密码不能超过20位' },
25 {
26 validator: (value: string | undefined, cb: (error?: string) => void) => {
27 return new Promise((resolve) => {
28 if (value !== formValue.value.password) {
29 cb('确认密码与新密码不一致');
30 }
31 resolve(true);
32 });
33 },
34 },
35 ],
36 };
37
38 const onSubmit = async (): Promise<void> => {
39 const error = await formRef.value?.validate();
40
41 if (error) {
42 return Promise.reject(error);
43 }
44
45 return props.onRequest(formValue.value);
46 };
47
48 defineExpose({ onSubmit });
49 </script>
50
51 <template>
52 <Form ref="formRef" :model="formValue" :rules="rules" auto-label-width>
53 <FormItem field="password" label="新密码">
54 <InputPassword v-model="formValue.password" :max-length="20" />
55 </FormItem>
56 <FormItem field="password_confirmation" label="确认密码" row-class="mb-0">
57 <InputPassword v-model="formValue.password_confirmation" :max-length="20" />
58 </FormItem>
59 </Form>
60 </template>
61
62 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { onMounted, ref } from 'vue';
3 import useLoading from '@/hooks/loading';
4 import useActivityApi from '@/http/activity';
5 import { AnyObject } from '@/types/global';
6 import UserTableColumn from '@/components/filter/user-table-column.vue';
7 import NumberTableColumn from '@/components/filter/number-table-column.vue';
8 import { TableData } from '@arco-design/web-vue';
9 import { useRouter } from 'vue-router';
10 import ActivityTableColumn from '@/components/filter/activity-table-column.vue';
11 import DateTableColumn from '@/components/filter/date-table-column.vue';
12 import EnumTableColumn from '@/components/filter/enum-table-column.vue';
13
14 const router = useRouter();
15 const props = defineProps<{ userKey: number }>();
16
17 const tableRef = ref();
18 const { loading, setLoading } = useLoading(false);
19 const filter = ref({ song_name: '', project_name: '', tag_name: '', status: '', createBetween: [] });
20
21 const total = ref(0);
22
23 const { statusOption } = useActivityApi;
24
25 const onQuery = async (params?: AnyObject) => {
26 setLoading(true);
27 useActivityApi.get({ user_id: props.userKey, pageSize: 1 }).then(({ meta }) => (total.value = meta.total));
28
29 return useActivityApi
30 .get({
31 user_id: props.userKey,
32 ...filter.value,
33 ...params,
34 setColumn: ['id', 'cover', 'song_name', 'user_id', 'project_id', 'sub_title', 'status', 'created_at'],
35 setWith: ['project:id,name', 'user:id,real_name,nick_name,identity', 'tags:id,name', 'outSideManages:id,user_id,activity_id'],
36 setWithCount: ['viewUsers as view_count', 'collectionUsers as like_count', 'submitUsers as submit_work_count'],
37 setSort: ['-id'],
38 })
39 .finally(() => setLoading(false));
40 };
41
42 const onSearch = () => tableRef.value?.onPageChange(1);
43
44 const onRowClick = (record: TableData) => router.push({ name: 'audition-activity-show', params: { id: record.id } });
45
46 const onReset = () => {
47 filter.value = { song_name: '', project_name: '', tag_name: '', status: '', createBetween: [] };
48 onSearch();
49 };
50
51 onMounted(() => onReset());
52
53 defineExpose({ total });
54 </script>
55
56 <template>
57 <a-card :bordered="false">
58 <filter-search :model="filter" :loading="loading" @search="onSearch" @reset="onReset">
59 <filter-search-item label="歌曲名称" filed="song_name">
60 <a-input v-model="filter.song_name" allow-clear placeholder="请输入" />
61 </filter-search-item>
62 <filter-search-item label="厂牌名称" filed="project_name">
63 <a-input v-model="filter.project_name" allow-clear placeholder="请输入" />
64 </filter-search-item>
65 <filter-search-item label="标签名称" filed="tag_name">
66 <a-input v-model="filter.tag_name" allow-clear placeholder="请输入" />
67 </filter-search-item>
68 <filter-search-item label="状态" filed="status">
69 <a-select v-model="filter.status" :options="statusOption" allow-clear placeholder="请选择" />
70 </filter-search-item>
71 </filter-search>
72 <filter-table ref="tableRef" :loading="loading" :on-query="onQuery" hover-type="pointer" @row-click="onRowClick">
73 <activity-table-column data-index="id" title="试唱歌曲" />
74 <number-table-column :width="120" data-index="view_count" title="试听人数" />
75 <number-table-column :width="120" data-index="like_count" title="收藏人数" />
76 <number-table-column :width="120" data-index="submit_work_count" title="提交人数" />
77 <user-table-column :width="140" data-index="user_id" user="user" title="创建人" />
78 <date-table-column :width="110" data-index="created_at" title="创建时间" />
79 <enum-table-column :width="110" data-index="status" title="状态" :option="statusOption" />
80 </filter-table>
81 </a-card>
82 </template>
83
84 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { onMounted, ref } from 'vue';
3 import useLoading from '@/hooks/loading';
4 import useActivityApi from '@/http/activity';
5 import { AnyObject } from '@/types/global';
6 import useUserApi from '@/http/user';
7 import { TableData } from '@arco-design/web-vue';
8 import { useRouter } from 'vue-router';
9 import DateTableColumn from '@/components/filter/date-table-column.vue';
10 import EnumTableColumn from '@/components/filter/enum-table-column.vue';
11 import ActivityTableColumn from '@/components/filter/activity-table-column.vue';
12
13 const router = useRouter();
14 const props = defineProps<{ userKey: number }>();
15
16 const tableRef = ref();
17 const { loading, setLoading } = useLoading(false);
18 const filter = ref({ activity_name: '', project_name: '', tag_name: '', use_sing_type: '', use_sing_status: '', activity_status: '' });
19
20 const total = ref(0);
21
22 const { statusOption } = useActivityApi;
23 const { workSingTypeOption, workSingStatusOption } = useActivityApi;
24
25 const onQuery = async (params?: AnyObject) => {
26 setLoading(true);
27 useUserApi.submitSongs(props.userKey, { pageSize: 1 }).then(({ meta }) => (total.value = meta.total));
28
29 return useUserApi
30 .submitSongs(props.userKey, { ...filter.value, ...params, sortBy: 'id', sortType: 'desc' })
31 .finally(() => setLoading(false));
32 };
33
34 const onSearch = () => tableRef.value?.onPageChange(1);
35
36 const onReset = () => {
37 filter.value = { activity_name: '', project_name: '', tag_name: '', use_sing_type: '', use_sing_status: '', activity_status: '' };
38 onSearch();
39 };
40
41 const onRowClick = (record: TableData) => router.push({ name: 'audition-activity-show', params: { id: record.activity_id } });
42
43 onMounted(() => onReset());
44
45 defineExpose({ total });
46 </script>
47
48 <template>
49 <a-card :bordered="false">
50 <filter-search :model="filter" :loading="loading" @search="onSearch" @reset="onReset">
51 <filter-search-item label="歌曲名称" filed="activity_name">
52 <a-input v-model="filter.activity_name" allow-clear placeholder="请输入" />
53 </filter-search-item>
54 <filter-search-item label="厂牌名称" filed="project_name">
55 <a-input v-model="filter.project_name" allow-clear placeholder="请输入" />
56 </filter-search-item>
57 <filter-search-item label="标签名称" filed="tag_name">
58 <a-input v-model="filter.tag_name" allow-clear placeholder="请输入" />
59 </filter-search-item>
60 <filter-search-item label="试唱方式" filed="status">
61 <a-select v-model="filter.use_sing_type" :options="workSingTypeOption" allow-clear placeholder="请选择" />
62 </filter-search-item>
63 <filter-search-item label="试唱结果" filed="status">
64 <a-select v-model="filter.use_sing_status" :options="workSingStatusOption" allow-clear placeholder="请选择" />
65 </filter-search-item>
66 <filter-search-item label="试唱状态" filed="status">
67 <a-select v-model="filter.activity_status" :options="statusOption" allow-clear placeholder="请选择" />
68 </filter-search-item>
69 </filter-search>
70 <filter-table ref="tableRef" :loading="loading" :on-query="onQuery" hover-type="pointer" @row-click="onRowClick">
71 <activity-table-column
72 data-index="id"
73 title="试唱歌曲"
74 name-index="activity_name"
75 sub-index="activity_title"
76 cover-index="activity_cover"
77 />
78 <filter-table-column :width="100" data-index="mode" title="试唱方式">
79 <template #default="{ record }">
80 <template v-if="record.mode === 1">自主上传</template>
81 <template v-else>{{ record.sing_type === 'Full' ? '唱整首' : '唱片段' }}</template>
82 </template>
83 </filter-table-column>
84 <date-table-column :width="110" data-index="submit_at" title="提交时间" />
85 <filter-table-column :width="120" data-index="status" title="试唱结果">
86 <template #default="{ record }">
87 <span v-if="record.status === 1">已确认</span>
88 <span v-else-if="record.status === 2">不合适</span>
89 <span v-else-if="record.status === 0 && [3, 5].indexOf(record.activity_status) !== -1">未采纳</span>
90 <span v-else-if="record.status === 0 && record.activity_status === 1">待采纳</span>
91 <span v-else>其他</span>
92 </template>
93 </filter-table-column>
94 <filter-table-column :width="320" data-index="demo_url" title="试唱音频">
95 <template #default="{ record }">
96 <audio-player :url="record.demo_url" />
97 </template>
98 </filter-table-column>
99 <enum-table-column :width="110" data-index="status" title="试唱状态" :option="useActivityApi.statusOption" />
100 </filter-table>
101 </a-card>
102 </template>
103
104 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { Form, FormItem, Input, InputNumber, Select, Alert, SelectOptionData } from '@arco-design/web-vue';
3 import ProjectSection from '@/components/project-select/index.vue';
4 import { computed, onMounted, ref, watch } from 'vue';
5 import useUserApi from '@/http/user';
6 import useTagApi from '@/http/star-tag';
7 import { FormInstance } from '@arco-design/web-vue/es/form';
8 import { User } from '@/utils/model';
9 import useRoleApi from '@/http/role';
10 import { SystemRole } from '@/types/system-role';
11 import useConfigApi from '@/http/config';
12 import { Option } from '@/types/global';
13
14 const { scopeOption } = useUserApi;
15
16 type FormValueProp = {
17 nick_name: string;
18 real_name: string;
19 email: string;
20 lang: string;
21 area_code: string;
22 phone: string;
23 scope: number;
24 demo_type: number;
25 user_tag_id: number;
26 authIds?: number[];
27 roleIds?: number[];
28 projectIds?: number[];
29 business_singer_limit: number;
30 };
31
32 const props = defineProps<{ row: User; authTagOptions: SelectOptionData[]; onRequest: (data: FormValueProp) => Promise<User> }>();
33
34 const formRef = ref<FormInstance>();
35
36 const formValue = ref<FormValueProp>({
37 nick_name: props.row.nick_name,
38 real_name: props.row.real_name,
39 email: props.row.email,
40 area_code: props.row.area_code || '86',
41 phone: props.row.phone,
42 scope: props.row.scope,
43 demo_type: props.row.demo_type,
44 user_tag_id: props.row.user_tag_id,
45 lang: props.row.lang,
46 business_singer_limit: props.row.business_singer_limit || 0,
47 authIds: props.row.auth_tags?.map((item) => item.id) || [],
48 roleIds: props.row.roles?.map((item) => item.id) || [],
49 projectIds: props.row.projects?.map((item) => item.id) || [],
50 });
51
52 const formRules = computed(() => {
53 return {
54 nick_name: [{ required: true, message: '请输入用户艺名' }],
55 real_name: [{ required: true, message: '请输入用户真名' }],
56 phone: [
57 { required: true, message: '请输入用户手机号码' },
58 { validator: (value: any, cb: (error?: string) => void) => !formValue.value.area_code && cb('请输入用户手机国际区号') },
59 ],
60 email: [{ required: true, message: '请输入用户邮箱' }],
61 scope: [{ required: true, message: '请选择权限' }],
62 demo_type: [{ required: true, message: '请选择个人Demo权限' }],
63 user_tag_id: [{ required: true, message: '请选择标签' }],
64 business_singer_limit: [{ required: true, message: '请输入团队歌手数' }],
65 projectIds: [{ required: formValue.value.scope === 2, message: '请选择管理厂牌' }],
66 roleIds: [{ required: formValue.value.scope === 1, message: '请选择角色' }],
67 };
68 });
69
70 const roleOption = ref<SystemRole[]>([]);
71 const langOption = ref<Option[]>([]);
72 const demoScopeOption = ref<Option[]>([
73 { label: '跟随认证标签权限', value: 1, disabled: false },
74 { label: '关闭', value: 0, disabled: false },
75 ]);
76
77 const userTagOption = ref<Option[]>([]);
78
79 const allAuthDemoTags = ref<string[]>([]);
80
81 const activityRecommendOption = ref<Option[]>([]);
82
83 const onScopeChange = () => {
84 formValue.value.roleIds = [];
85 formValue.value.projectIds = [];
86 };
87
88 const onSubmit = async (): Promise<User> => {
89 if (await formRef.value?.validate()) {
90 return Promise.reject();
91 }
92 return props.onRequest(formValue.value);
93 };
94
95 const setDemoScopeOptionStatus = (setType?: boolean) => {
96 let has = false;
97 if (formValue.value) {
98 const { authIds } = formValue.value;
99 if (authIds) {
100 authIds.forEach((item) => {
101 allAuthDemoTags.value.forEach((allItem) => {
102 if (item === Number(allItem)) {
103 has = true;
104 }
105 });
106 });
107 }
108 }
109 if (has) {
110 demoScopeOption.value[1].disabled = false;
111 } else {
112 if (![2, 3].includes(props.row.identity)) {
113 demoScopeOption.value[1].disabled = true;
114 }
115
116 if (setType) {
117 formValue.value.demo_type = 1;
118 }
119 }
120 };
121
122 const onAuthTagOptionsChange = () => {
123 setDemoScopeOptionStatus(true);
124 };
125
126 watch(allAuthDemoTags, () => {
127 setDemoScopeOptionStatus();
128 });
129
130 watch(
131 formValue.value,
132 () => {
133 setDemoScopeOptionStatus();
134 },
135 {
136 immediate: true,
137 }
138 );
139
140 defineExpose({ onSubmit });
141
142 onMounted(() => {
143 useRoleApi.get({ guard: 'Admin', fetchType: 'all' }).then(({ data }) => {
144 roleOption.value = data;
145 });
146
147 useConfigApi
148 .getOption({ parentKey: '01HMFTN51ZEVCGSSKRF6C9EC3Y', setColumn: ['identifier', 'name'], setSort: ['-weight'] })
149 .then((data) => {
150 activityRecommendOption.value = data;
151 });
152
153 useConfigApi.getOption({ parentKey: 'activity_lang', setColumn: ['identifier', 'name'], setSort: ['-weight'] }).then((data) => {
154 langOption.value = data;
155 });
156
157 useConfigApi.getOne({ identifier: 'CtGly-jsLjAu5yJvLrN7L' }).then((data: any) => {
158 allAuthDemoTags.value = data.content.split(',');
159 });
160
161 useTagApi
162 .get({
163 sortBy: 'id',
164 sortType: 'desc',
165 page: 1,
166 pageSize: 9999,
167 })
168 .then((res) => {
169 const arr = [
170 {
171 label: '无',
172 value: 0,
173 disabled: false,
174 },
175 ];
176 const { data } = res;
177
178 data.forEach((item) => {
179 arr.push({
180 label: item.name,
181 value: item.id,
182 disabled: item.status !== 1,
183 });
184 });
185 userTagOption.value = arr;
186 });
187 });
188 </script>
189
190 <template>
191 <Form ref="formRef" :model="formValue" :rules="formRules" auto-label-width>
192 <FormItem label="用户艺名" field="nick_name" show-colon>
193 <Input v-model="formValue.nick_name" :max-length="99" show-word-limit />
194 </FormItem>
195 <FormItem label="用户真名" field="real_name" show-colon>
196 <Input v-model="formValue.real_name" :max-length="99" show-word-limit />
197 </FormItem>
198 <FormItem label="邮箱" field="email" show-colon>
199 <Input v-model="formValue.email" :max-length="99" show-word-limit />
200 </FormItem>
201 <FormItem label="手机号" field="phone" show-colon>
202 <Input v-model="formValue.area_code" style="width: 20%" :max-length="6" @blur="() => formRef?.validateField('phone')" />
203 <div style="width: 5%; text-align: center">-</div>
204 <Input v-model="formValue.phone" :max-length="16" show-word-limit @blur="() => formRef?.validateField('phone')" />
205 </FormItem>
206 <FormItem label="语种" field="lang" show-colon>
207 <Select v-model="formValue.lang" :options="langOption" :fallback-option="false" placeholder="请选择" />
208 </FormItem>
209 <FormItem label="音乐认证" field="authIds" show-colon>
210 <Select
211 v-model="formValue.authIds"
212 placeholder="请选择"
213 :options="authTagOptions"
214 multiple
215 allow-clear
216 allow-search
217 @change="onAuthTagOptionsChange"
218 />
219 </FormItem>
220 <FormItem label="团队歌手" field="business_singer_limit" show-colon>
221 <InputNumber v-model="formValue.business_singer_limit" :min="2" :max="1000" />
222 </FormItem>
223 <FormItem label="权限" field="scope" show-colon>
224 <Select v-model="formValue.scope" placeholder="请选择" :options="scopeOption" :fallback-option="false" @change="onScopeChange" />
225 </FormItem>
226 <FormItem label="个人Demo权限" field="demo_type" show-colon>
227 <!-- <span v-if="[2, 3].includes(user.identity)"> 经纪人</span> -->
228 <Select v-model="formValue.demo_type" placeholder="请选择" :options="demoScopeOption" :fallback-option="false" />
229 </FormItem>
230 <FormItem label="明星标签" field="user_tag_id" show-colon>
231 <Select v-model="formValue.user_tag_id" placeholder="请选择" :options="userTagOption" :fallback-option="false" />
232 </FormItem>
233 <FormItem v-show="formValue.scope === 2" label="管理厂牌" field="projectIds" show-colon>
234 <ProjectSection v-model="formValue.projectIds" :multiple="true" :max-tag-count="5" placeholder="请选择" allow-search />
235 </FormItem>
236 <FormItem v-show="formValue.scope === 1" field="roleIds" label="角色" show-colon row-class="mb-0">
237 <Select
238 v-model="formValue.roleIds"
239 placeholder="请选择"
240 :field-names="{ value: 'id', label: 'name' }"
241 :options="roleOption"
242 :multiple="true"
243 :fallback-option="false"
244 :allow-search="true"
245 />
246 </FormItem>
247 <Alert v-show="formValue.scope !== 0 && formValue.scope !== row.scope" type="warning">
248 权限变更将初始化用户登录密码,初始值为该用户手机号
249 </Alert>
250 </Form>
251 </template>
252
253 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { Image, List, ListItem, ListItemMeta, TypographyParagraph } from '@arco-design/web-vue';
3 import AudioPlayer from '@/components/audio-player/index.vue';
4 import { onMounted, ref } from 'vue';
5 import usePagination from '@/hooks/pagination';
6 import { UserAudioDynamics } from '@/utils/model';
7 import useLoading from '@/hooks/loading';
8 import useUserApi from '@/http/user';
9
10 const props = defineProps<{ userKey: number }>();
11
12 const { loading, setLoading } = useLoading(false);
13 const { pagination, setPage, setTotal } = usePagination({ pageSize: 5, size: 'mini', showPageSize: false });
14 const source = ref<UserAudioDynamics[]>([]);
15
16 const onQuery = () => {
17 setLoading(true);
18 useUserApi
19 .dynamics(props.userKey, {
20 type: 'audio',
21 page: pagination.value.current,
22 pageSize: pagination.value.pageSize,
23 sortBy: 'is_top',
24 sortType: 'desc',
25 })
26 .then(({ data, meta }) => {
27 source.value = data as UserAudioDynamics[];
28 setPage(meta.current);
29 setTotal(meta.total);
30 })
31 .finally(() => setLoading(false));
32 };
33
34 onMounted(() => onQuery());
35 </script>
36
37 <template>
38 <List
39 :loading="loading"
40 :data="source"
41 :bordered="false"
42 :scrollbar="true"
43 :max-height="460"
44 :pagination-props="pagination"
45 @page-change="(page:number) => setPage(page) && onQuery()"
46 >
47 <template #item="{ item }">
48 <ListItem :key="item.id" style="padding: 4px 16px">
49 <ListItemMeta>
50 <template #avatar>
51 <Image :src="item.properties.cover.url" :width="60" :height="60" fit="fill" />
52 </template>
53 <template #title>
54 <TypographyParagraph :ellipsis="{ rows: 1, showTooltip: true }" style="margin-bottom: 10px; margin-top: 10px">
55 {{ item.intro }}
56 </TypographyParagraph>
57 </template>
58 <template #description>
59 <AudioPlayer :url="item.properties.url" style="width: 410px; height: 20px" />
60 </template>
61 </ListItemMeta>
62 </ListItem>
63 </template>
64 </List>
65 </template>
66
67 <style scoped lang="less">
68 :deep(.arco-list-pagination) {
69 margin-top: 10px !important;
70 margin-bottom: 10px !important;
71 }
72 </style>
1 <script setup lang="ts">
2 import { ImagePreviewGroup, Grid, GridItem, Image, Pagination } from '@arco-design/web-vue';
3 import usePagination from '@/hooks/pagination';
4 import { onMounted, ref } from 'vue';
5 import useUserApi from '@/http/user';
6 import { UserImageDynamics } from '@/utils/model';
7
8 const props = defineProps<{ userKey: number }>();
9
10 const { pagination, setPage, setTotal } = usePagination({ pageSize: 12 });
11 const source = ref<UserImageDynamics[]>([]);
12
13 const onQuery = () =>
14 useUserApi
15 .dynamics(props.userKey, {
16 type: 'image',
17 page: pagination.value.current,
18 pageSize: pagination.value.pageSize,
19 sortBy: 'is_top',
20 sortType: 'desc',
21 })
22 .then(({ data, meta }) => {
23 source.value = data as UserImageDynamics[];
24 setPage(meta.current);
25 setTotal(meta.total);
26 });
27
28 onMounted(() => onQuery());
29 </script>
30
31 <template>
32 <ImagePreviewGroup>
33 <Grid :cols="6" :col-gap="12" :row-gap="16">
34 <GridItem v-for="item in source" :key="item.id">
35 <Image :src="item.properties.url" :width="120" :height="120" fit="scale-down" :show-loader="true" />
36 </GridItem>
37 </Grid>
38 </ImagePreviewGroup>
39 <Pagination
40 size="mini"
41 style="justify-content: flex-end; margin-top: 10px"
42 :current="pagination.current"
43 :total="pagination.total"
44 :page-size="pagination.pageSize"
45 :hide-on-single-page="true"
46 @change="(current:number) => setPage(current) && onQuery()"
47 />
48 </template>
49
50 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { ImagePreviewGroup, Grid, GridItem, Image, Pagination } from '@arco-design/web-vue';
3 import { h, onMounted, ref } from 'vue';
4 import usePagination from '@/hooks/pagination';
5 import { UserAudioDynamics } from '@/utils/model';
6 import { createModalVNode } from '@/utils/createVNode';
7
8 import vue3videoPlay from 'vue3-video-play';
9 import 'vue3-video-play/dist/style.css';
10 import useUserApi from '@/http/user';
11
12 const props = defineProps<{ userKey: number }>();
13
14 const { pagination, setPage, setTotal } = usePagination({ pageSize: 12 });
15 const source = ref<UserAudioDynamics[]>([]);
16
17 const onQuery = () =>
18 useUserApi
19 .dynamics(props.userKey, {
20 type: 'video',
21 page: pagination.value.current,
22 pageSize: pagination.value.pageSize,
23 sortBy: 'is_top',
24 sortType: 'desc',
25 })
26 .then(({ data, meta }) => {
27 source.value = data as UserAudioDynamics[];
28 setPage(meta.current);
29 setTotal(meta.total);
30 });
31
32 onMounted(() => onQuery());
33
34 const onClick = (item: any) =>
35 createModalVNode(
36 () => h(vue3videoPlay, { src: item.properties.url, controlBtns: ['volume', 'speedRate', 'pip', 'pageFullScreen', 'fullScreen'] }),
37 {
38 width: 'auto',
39 modalStyle: { padding: '0 !important' },
40 simple: true,
41 footer: false,
42 maskClosable: true,
43 escToClose: true,
44 }
45 );
46 </script>
47
48 <template>
49 <ImagePreviewGroup>
50 <Grid :cols="6" :col-gap="12" :row-gap="16">
51 <GridItem v-for="item in source" :key="item.id">
52 <Image
53 :src="item.properties.cover.url"
54 :width="120"
55 :height="70"
56 fit="scale-down"
57 :show-loader="true"
58 :preview="false"
59 @click="onClick(item)"
60 />
61 </GridItem>
62 </Grid>
63 </ImagePreviewGroup>
64 <Pagination
65 size="mini"
66 style="justify-content: flex-end; margin-top: 10px"
67 :current="pagination.current"
68 :total="pagination.total"
69 :page-size="pagination.pageSize"
70 :hide-on-single-page="true"
71 @change="(current:number) => setPage(current) && onQuery()"
72 />
73 </template>
74
75 <style scoped lang="less"></style>
1 <script setup lang="ts" name="user-show">
2 import { computed, onBeforeMount, ref, watch } from 'vue';
3 import useUserApi from '@/http/user';
4 import { onBeforeRouteLeave, useRoute } from 'vue-router';
5 import BasicCard from '@/views/user/info/components/basic-card.vue';
6 import ListenActivityTable from '@/views/user/info/components/listen-activity-table.vue';
7 import LikeActivityTable from '@/views/user/info/components/like-activity-table.vue';
8 import PublishActivityTable from '@/views/user/info/components/publish-activity-table.vue';
9 import SubmitActivityTable from '@/views/user/info/components/submit-activity-table.vue';
10 import ManageProjectTable from '@/views/user/info/components/manage-project-table.vue';
11 import ManageActivityTable from '@/views/user/info/components/manage-activity-table.vue';
12 import GroupMemberTable from '@/views/user/info/components/group-member-table.vue';
13 import DemoActivityTable from '@/views/audition/demo/index.vue';
14
15 import useLoading from '@/hooks/loading';
16 import { User } from '@/utils/model';
17 import { useRouteQuery } from '@vueuse/router';
18 import usePermission from '@/hooks/permission';
19 import { isArray } from '@/utils/is';
20 import useActivityApi from '@/http/activity';
21
22 const userId = Number(useRoute().params?.id);
23 const user = ref({ identity: 0, scope: 0 });
24 const routeName = useRoute()?.name as string;
25
26 const checkPermission = (permission: string | string[]): boolean => {
27 permission = isArray(permission) ? permission.map((item) => `${routeName}-${item}`) : `${routeName}-${permission}`;
28 return usePermission().checkPermission(permission);
29 };
30
31 const { loading, setLoading } = useLoading(true);
32
33 const tabKeys = computed((): string[] => {
34 const keys = ['listen-activity', 'like-activity', 'submit-activity', 'manage-activity', 'member', 'demo'];
35
36 if ([1, 2].includes(user.value.scope)) {
37 keys.push('publish-activity');
38 }
39 if ([2].includes(user.value.scope)) {
40 keys.push('project');
41 }
42 return keys.filter((item) => checkPermission(item));
43 });
44
45 const tabKey = useRouteQuery('tabKey', tabKeys.value[0] || '');
46
47 const listenRef = ref();
48 const likeRef = ref();
49 const submitRef = ref();
50 const activityRef = ref();
51 const groupMemberRef = ref();
52 const publishRef = ref();
53 const projectRef = ref();
54
55 const count = ref({ demo: 0 });
56
57 watch(
58 () => tabKeys.value,
59 () => (tabKeys.value.includes(tabKey.value) ? undefined : (tabKey.value = tabKeys.value[0] || ''))
60 );
61
62 onBeforeMount(() => {
63 useUserApi
64 .show(userId)
65 .then((data) => (user.value = data))
66 .finally(() => setLoading(false));
67 });
68
69 onBeforeRouteLeave((to, from) => {
70 if (from.meta.from === to.name) {
71 to.meta.reload = false;
72 }
73 });
74
75 const syncDemoCount = () =>
76 useActivityApi.get({ audit_status: 1, song_type: 2, user_id: userId, pageSize: 1 }).then(({ meta }) => {
77 count.value.demo = meta.total;
78 });
79 </script>
80
81 <template>
82 <page-view :loading="loading" has-bread>
83 <basic-card :user="user as User" />
84
85 <a-card v-if="tabKeys.length" :bordered="false" style="margin-top: 16px">
86 <a-tabs v-model:active-key="tabKey" type="rounded" :animation="true" size="small" :justify="true" :header-padding="false">
87 <a-tab-pane v-if="checkPermission('listen-activity')" key="listen-activity" :title="`试听歌曲(${listenRef?.total})`">
88 <listen-activity-table ref="listenRef" :user-key="userId" />
89 </a-tab-pane>
90 <a-tab-pane v-if="checkPermission('like-activity')" key="like-activity" :title="`收藏歌曲(${likeRef?.total})`">
91 <like-activity-table ref="likeRef" :user-key="userId" />
92 </a-tab-pane>
93 <a-tab-pane v-if="checkPermission('submit-activity')" key="submit-activity" :title="`试唱歌曲(${submitRef?.total})`">
94 <submit-activity-table ref="submitRef" :user-key="userId" />
95 </a-tab-pane>
96 <a-tab-pane v-if="checkPermission('manage-activity')" key="manage-activity" :title="`管理歌曲(${activityRef?.total})`">
97 <manage-activity-table ref="activityRef" :user-key="userId" />
98 </a-tab-pane>
99 <a-tab-pane v-if="checkPermission('member')" key="member" :title="`Ta的成员(${groupMemberRef?.total})`">
100 <group-member-table ref="groupMemberRef" :user-key="userId" />
101 </a-tab-pane>
102 <a-tab-pane
103 v-if="checkPermission('publish-activity') && tabKeys.includes('publish-activity')"
104 key="publish-activity"
105 :title="`发布歌曲(${publishRef?.total})`"
106 >
107 <publish-activity-table ref="publishRef" :user-key="userId" />
108 </a-tab-pane>
109 <a-tab-pane
110 v-if="checkPermission('project') && tabKeys.includes('project')"
111 key="project"
112 :title="`Ta的厂牌(${projectRef?.total})`"
113 >
114 <manage-project-table ref="projectRef" :user-key="userId" />
115 </a-tab-pane>
116 <a-tab-pane v-if="checkPermission('demo') && tabKeys.includes('demo')" key="demo" :title="`Ta的Demo(${count.demo})`">
117 <demo-activity-table
118 style="padding: 0"
119 :has-bread="false"
120 :init-filter="{ user_id: userId }"
121 :hide-search-item="['projectName', 'createdForm', 'userName', 'auditBetween']"
122 :query-hook="() => syncDemoCount()"
123 />
124 </a-tab-pane>
125 </a-tabs>
126 </a-card>
127 </page-view>
128 </template>
129
130 <style scoped lang="less"></style>
1 <script setup lang="ts">
2 import { onActivated, ref } from 'vue';
3 import useLoading from '@/hooks/loading';
4 import { useRegisterApi } from '@/http/user';
5 import { AnyObject } from '@/types/global';
6
7 import EnumTableColumn from '@/components/filter/enum-table-column.vue';
8 import PhoneTableColumn from '@/components/filter/phone-table-column.vue';
9 import { TableData } from '@arco-design/web-vue';
10 import { useRoute, useRouter } from 'vue-router';
11 import UserTableColumn from '@/components/filter/user-table-column.vue';
12
13 const router = useRouter();
14
15 const tableRef = ref();
16 const filter = ref<AnyObject>({});
17 const { loading, setLoading } = useLoading(false);
18
19 const { officialStatusOption, statusOption, scopeOption, sexOption, get, getExport } = useRegisterApi;
20 const onQuery = async (params?: AnyObject) => {
21 setLoading(true);
22 return get({ ...filter.value, ...params, sortBy: 'id', sortType: 'desc' }).finally(() => setLoading(false));
23 };
24
25 const onExport = () => getExport('注册未认证', { ...filter.value, sortBy: 'id', sortType: 'desc' });
26
27 const onRowClick = (record: TableData) => router.push({ name: 'user-register-show', params: { id: record.id } });
28
29 const onSearch = () => tableRef.value?.onPageChange(1);
30
31 const onReset = () => {
32 filter.value = { nick_name: '', email_like: '', phone_like: '', official_status: '', status: '' };
33 onSearch();
34 };
35
36 onActivated(() => (useRoute().meta.reload ? onReset() : tableRef.value?.onFetch()));
37 </script>
38
39 <template>
40 <page-view has-card has-bread>
41 <filter-search :model="filter" :loading="loading" @search="onSearch" @reset="onReset">
42 <filter-search-item field="nick_name" label="用户艺名">
43 <a-input v-model="filter.nick_name" allow-clear placeholder="请输入" />
44 </filter-search-item>
45 <filter-search-item field="sex" label="性别">
46 <a-select v-model="filter.sex" allow-clear placeholder="请选择" :options="sexOption" />
47 </filter-search-item>
48 <filter-search-item field="email_like" label="用户邮箱">
49 <a-input v-model="filter.email_like" allow-clear placeholder="请输入" />
50 </filter-search-item>
51 <filter-search-item field="phone_like" label="手机号码">
52 <a-input v-model="filter.phone_like" allow-clear placeholder="请输入" />
53 </filter-search-item>
54 <filter-search-item field="name" label="关注服务号">
55 <a-select v-model="filter.official_status" :options="officialStatusOption" placeholder="请选择" allow-clear />
56 </filter-search-item>
57 <filter-search-item field="name" label="状态">
58 <a-select v-model="filter.status" :options="statusOption" placeholder="请选择" allow-clear />
59 </filter-search-item>
60 <filter-search-item field="scope" label="权限">
61 <a-select v-model="filter.scope" allow-clear placeholder="请选择" :options="scopeOption" />
62 </filter-search-item>
63 <template #button>
64 <export-button v-permission="['user-register-export']" :on-download="onExport" />
65 </template>
66 </filter-search>
67 <filter-table ref="tableRef" :loading="loading" :on-query="onQuery" hover-type="pointer" @row-click="onRowClick">
68 <user-table-column title="用户艺名" data-index="id" show-avatar :width="240" />
69 <enum-table-column title="性别" data-index="sex" :option="sexOption" :dark-value="0" :width="80" />
70 <filter-table-column title="用户邮箱" data-index="email" :width="200" />
71 <phone-table-column title="手机号码" data-index="phone" area-index="area_code" :width="160" />
72 <filter-table-column title="注册时间" data-index="created_at" :width="170" />
73 <enum-table-column title="权限" data-index="scope" :option="scopeOption" :dark-value="0" :width="120" />
74 <enum-table-column title="关注服务号" data-index="official_status" :option="officialStatusOption" :dark-value="0" :width="120" />
75 <enum-table-column title="状态" data-index="status" :option="statusOption" :width="80" />
76 </filter-table>
77 </page-view>
78 </template>
79
80 <style scoped lang="less"></style>
1 <script setup lang="ts" name="user-singer">
2 import { onActivated, ref } from 'vue';
3 import { AnyObject } from '@/types/global';
4 import useLoading from '@/hooks/loading';
5 import PhoneTableColumn from '@/components/filter/phone-table-column.vue';
6 import EnumTableColumn from '@/components/filter/enum-table-column.vue';
7 import { TableData } from '@arco-design/web-vue';
8 import { useRoute, useRouter } from 'vue-router';
9 import { useSingerApi } from '@/http/user';
10 import UserTableColumn from '@/components/filter/user-table-column.vue';
11
12 const tableRef = ref();
13 const filter = ref<AnyObject>({});
14 const { loading, setLoading } = useLoading(false);
15
16 const { sexOption, scopeOption, statusOption, officialStatusOption, get, getExport } = useSingerApi;
17
18 const onQuery = async (params?: AnyObject) => {
19 setLoading(true);
20 return get({ ...filter.value, ...params }).finally(() => setLoading(false));
21 };
22 const onExport = () => getExport('音乐人', filter.value);
23
24 const onSearch = () => tableRef.value?.onPageChange(1);
25
26 const onSort = (column: string, type: string) => {
27 if (type) {
28 filter.value.sortBy = column;
29 filter.value.sortType = type;
30 } else {
31 filter.value.sortBy = 'id';
32 filter.value.sortType = 'desc';
33 }
34 tableRef.value?.onFetch();
35 };
36
37 const onReset = () => {
38 filter.value = {
39 nick_name: '',
40 sex: '',
41 email_like: '',
42 phone_like: '',
43 businessName: '',
44 scope: '',
45 official_status: '',
46 status: '',
47 sortBy: 'id',
48 sortType: 'desc',
49 };
50 tableRef.value?.resetSort();
51 onSearch();
52 };
53
54 const router = useRouter();
55
56 const onRowClick = (record: TableData) => router.push({ name: 'user-singer-show', params: { id: record.id } });
57
58 onActivated(() => (useRoute()?.meta?.reload ? onReset() : tableRef.value?.onFetch()));
59 </script>
60
61 <template>
62 <page-view has-card has-bread>
63 <filter-search :model="filter" :loading="loading" @search="onSearch" @reset="onReset">
64 <filter-search-item field="nick_name" label="用户艺名">
65 <a-input v-model="filter.nick_name" allow-clear placeholder="请输入" />
66 </filter-search-item>
67 <filter-search-item field="sex" label="性别">
68 <a-select v-model="filter.sex" allow-clear placeholder="请选择" :options="sexOption" />
69 </filter-search-item>
70 <filter-search-item field="email_like" label="用户邮箱">
71 <a-input v-model="filter.email_like" allow-clear placeholder="请输入" />
72 </filter-search-item>
73 <filter-search-item field="phone_like" label="手机号码">
74 <a-input v-model="filter.phone_like" allow-clear placeholder="请输入" />
75 </filter-search-item>
76 <filter-search-item field="businessName" label="队长">
77 <a-input v-model="filter.businessName" allow-clear placeholder="请输入" />
78 </filter-search-item>
79 <filter-search-item field="scope" label="权限">
80 <a-select v-model="filter.scope" allow-clear placeholder="请选择" :options="scopeOption" />
81 </filter-search-item>
82 <filter-search-item field="official_status" label="关注服务号">
83 <a-select v-model="filter.official_status" allow-clear placeholder="请选择" :options="officialStatusOption" />
84 </filter-search-item>
85 <filter-search-item field="status" label="状态">
86 <a-select v-model="filter.status" allow-clear placeholder="请选择" :options="statusOption" />
87 </filter-search-item>
88 <template #button>
89 <export-button v-permission="['user-singer-export']" :on-download="onExport" />
90 </template>
91 </filter-search>
92 <filter-table ref="tableRef" hover-type="pointer" :loading="loading" :on-query="onQuery" @row-click="onRowClick" @row-sort="onSort">
93 <user-table-column title="用户艺名" data-index="id" show-avatar :width="240" />
94 <enum-table-column title="性别" data-index="sex" :option="sexOption" :dark-value="0" :width="70" />
95 <filter-table-column title="用户邮箱" data-index="email" :width="170" />
96 <phone-table-column title="手机号码" data-index="phone" area-index="area_code" :width="160" />
97 <filter-table-column title="认证通过时间" data-index="audit_at" :width="170" has-sort />
98 <a-table-column :width="160" title="队长" data-index="business_id">
99 <template #cell="{ record }">
100 <user-link v-if="record.business" :user="record.business" />
101 <span v-else style="color: rgba(44, 44, 44, 0.5)"></span>
102 </template>
103 </a-table-column>
104 <enum-table-column title="权限" data-index="scope" :option="scopeOption" :dark-value="0" :width="120" />
105 <enum-table-column title="关注服务号" data-index="official_status" :option="officialStatusOption" :dark-value="0" :width="120" />
106 <enum-table-column title="状态" data-index="status" :option="statusOption" :width="80" />
107 </filter-table>
108 </page-view>
109 </template>
110
111 <style scoped lang="less"></style>
1 {
2 "compilerOptions": {
3 "target": "ES2020",
4 "module": "ES2020",
5 "moduleResolution": "node",
6 "strict": true,
7 "jsx": "preserve",
8 "sourceMap": true,
9 "resolveJsonModule": true,
10 "esModuleInterop": true,
11 "noImplicitAny": false,
12 "baseUrl": ".",
13 "paths": {
14 "@/*": [
15 "src/*"
16 ]
17 },
18 "lib": [
19 "es2020",
20 "dom"
21 ],
22 "skipLibCheck": true,
23 "terserOptions": {
24 "compress": {
25 "drop_console": true,
26 "drop_debugger": true
27 }
28 }
29 },
30 "include": [
31 "src/**/*",
32 "src/**/*.vue"
33 ],
34 "exclude": [
35 "node_modules",
36 "next/core-web-vitals"
37 ]
38 }