Merge pull request 'feature/v1.3_营销资产中台' (#1) from feature/v1.3_营销资产中台 into main

Reviewed-on: ai-team/lingji-work-fe#1
This commit is contained in:
2025-07-09 01:55:08 +00:00
193 changed files with 21786 additions and 261 deletions

View File

@ -1,3 +1,7 @@
/*
* @Author: RenXiaoDong
* @Date: 2025-06-24 16:29:10
*/
/* eslint-env node */ /* eslint-env node */
// eslint-disable-next-line @typescript-eslint/no-require-imports // eslint-disable-next-line @typescript-eslint/no-require-imports
require('@rushstack/eslint-patch/modern-module-resolution'); require('@rushstack/eslint-patch/modern-module-resolution');
@ -29,6 +33,8 @@ module.exports = {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'prettier/prettier': ['error', { endOfLine: 'auto' }], 'prettier/prettier': ['error', { endOfLine: 'auto' }],
'vue/no-duplicate-attributes': 'off',
'vue/v-on-event-hyphenation': 'off',
}, },
globals: { globals: {
defineOptions: 'readonly', defineOptions: 'readonly',

1
.gitignore vendored
View File

@ -15,6 +15,7 @@ coverage
*.local *.local
*.js.map *.js.map
*.js *.js
CURSOR_RULES.md
/cypress/videos/ /cypress/videos/
/cypress/screenshots/ /cypress/screenshots/

View File

@ -1,8 +1,8 @@
/* /*
* @Author: 田鑫 * @Author: 田鑫
* @Date: 2023-03-05 18:14:16 * @Date: 2023-03-05 18:14:16
* @LastEditors: 田鑫 * @LastEditors: Please set LastEditors
* @LastEditTime: 2023-03-05 19:23:48 * @LastEditTime: 2025-06-25 10:54:24
* @Description: * @Description:
*/ */
import Unocss from 'unocss/vite'; import Unocss from 'unocss/vite';

View File

@ -1,3 +1,7 @@
/*
* @Author: RenXiaoDong
* @Date: 2025-06-24 16:29:10
*/
/** /**
* 自动引入API * 自动引入API
* */ * */
@ -17,7 +21,7 @@ export function configAutoImport() {
'@vueuse/core', '@vueuse/core',
{ {
dayjs: [['default', 'dayjs']], dayjs: [['default', 'dayjs']],
'lodash-es': ['cloneDeep', 'omit', 'pick'], 'lodash-es': ['cloneDeep', 'omit', 'pick', 'union', 'isNumber'],
'@/hooks': ['useModal'], '@/hooks': ['useModal'],
}, },
], ],

7232
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -6,9 +6,8 @@
"dev": "vite", "dev": "vite",
"build": "run-p build-only", "build": "run-p build-only",
"build:test": "vite build --mode test && tar -czvf dist-test.tar.gz dist", "build:test": "vite build --mode test && tar -czvf dist-test.tar.gz dist",
"build:production": "vite build --mode production && tar -czvf dist.tar.gz dist", "build:prod": "vite build --mode production && tar -czvf dist.tar.gz dist",
"build-only": "vite build -- mode development", "build-only": "vite build -- mode development",
"test": "vite build --mode test",
"type-check": "vue-tsc --noEmit", "type-check": "vue-tsc --noEmit",
"prepare": "husky install" "prepare": "husky install"
}, },
@ -20,16 +19,20 @@
"axios": "^1.3.0", "axios": "^1.3.0",
"dayjs": "^1.11.7", "dayjs": "^1.11.7",
"echarts": "^5.6.0", "echarts": "^5.6.0",
"html2canvas": "^1.4.1",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"mitt": "^3.0.0", "mitt": "^3.0.0",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
"pinia": "^2.0.29", "pinia": "^2.0.29",
"sass": "^1.89.2", "sass": "^1.89.2",
"swiper": "^11.2.8", "swiper": "^11.2.8",
"update": "^0.7.4",
"vue": "^3.2.45", "vue": "^3.2.45",
"vue-cropper": "^1.1.4", "vue-cropper": "^1.1.4",
"vue-draggable-plus": "^0.6.0",
"vue-echarts": "^7.0.3", "vue-echarts": "^7.0.3",
"vue-router": "^4.1.6" "vue-router": "^4.1.6",
"vuedraggable": "^4.1.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.20.12", "@babel/core": "^7.20.12",

284
pnpm-lock.yaml generated
View File

@ -41,18 +41,30 @@ importers:
pinia: pinia:
specifier: ^2.0.29 specifier: ^2.0.29
version: 2.0.29(typescript@4.9.5)(vue@3.2.45) version: 2.0.29(typescript@4.9.5)(vue@3.2.45)
sass:
specifier: ^1.89.2
version: 1.89.2
swiper: swiper:
specifier: ^11.2.8 specifier: ^11.2.8
version: 11.2.8 version: 11.2.8
vue: vue:
specifier: ^3.2.45 specifier: ^3.2.45
version: 3.2.45 version: 3.2.45
vue-cropper:
specifier: ^1.1.4
version: 1.1.4
vue-draggable-plus:
specifier: ^0.6.0
version: 0.6.0(@types/sortablejs@1.15.8)
vue-echarts: vue-echarts:
specifier: ^7.0.3 specifier: ^7.0.3
version: 7.0.3(@vue/runtime-core@3.2.45)(echarts@5.6.0)(vue@3.2.45) version: 7.0.3(@vue/runtime-core@3.2.45)(echarts@5.6.0)(vue@3.2.45)
vue-router: vue-router:
specifier: ^4.1.6 specifier: ^4.1.6
version: 4.1.6(vue@3.2.45) version: 4.1.6(vue@3.2.45)
vuedraggable:
specifier: ^4.1.0
version: 4.1.0(vue@3.2.45)
devDependencies: devDependencies:
'@babel/core': '@babel/core':
specifier: ^7.20.12 specifier: ^7.20.12
@ -80,10 +92,10 @@ importers:
version: 0.49.2 version: 0.49.2
'@vitejs/plugin-vue': '@vitejs/plugin-vue':
specifier: ^4.0.0 specifier: ^4.0.0
version: 4.0.0(vite@4.0.4(@types/node@18.11.18)(less@4.1.3))(vue@3.2.45) version: 4.0.0(vite@4.0.4(@types/node@18.11.18)(less@4.1.3)(sass@1.89.2))(vue@3.2.45)
'@vitejs/plugin-vue-jsx': '@vitejs/plugin-vue-jsx':
specifier: ^3.0.0 specifier: ^3.0.0
version: 3.0.0(vite@4.0.4(@types/node@18.11.18)(less@4.1.3))(vue@3.2.45) version: 3.0.0(vite@4.0.4(@types/node@18.11.18)(less@4.1.3)(sass@1.89.2))(vue@3.2.45)
'@vue/eslint-config-prettier': '@vue/eslint-config-prettier':
specifier: ^7.0.0 specifier: ^7.0.0
version: 7.0.0(eslint@8.33.0)(prettier@2.8.3) version: 7.0.0(eslint@8.33.0)(prettier@2.8.3)
@ -134,7 +146,7 @@ importers:
version: 4.9.5 version: 4.9.5
unocss: unocss:
specifier: ^0.49.2 specifier: ^0.49.2
version: 0.49.2(rollup@3.12.0)(vite@4.0.4(@types/node@18.11.18)(less@4.1.3)) version: 0.49.2(rollup@3.12.0)(vite@4.0.4(@types/node@18.11.18)(less@4.1.3)(sass@1.89.2))
unplugin-auto-import: unplugin-auto-import:
specifier: ^0.13.0 specifier: ^0.13.0
version: 0.13.0(@vueuse/core@9.12.0(vue@3.2.45))(rollup@3.12.0) version: 0.13.0(@vueuse/core@9.12.0(vue@3.2.45))(rollup@3.12.0)
@ -149,13 +161,13 @@ importers:
version: 1.4.5(rollup@3.12.0)(vue@3.2.45) version: 1.4.5(rollup@3.12.0)(vue@3.2.45)
vite: vite:
specifier: ^4.0.4 specifier: ^4.0.4
version: 4.0.4(@types/node@18.11.18)(less@4.1.3) version: 4.0.4(@types/node@18.11.18)(less@4.1.3)(sass@1.89.2)
vite-plugin-compression: vite-plugin-compression:
specifier: ^0.5.1 specifier: ^0.5.1
version: 0.5.1(vite@4.0.4(@types/node@18.11.18)(less@4.1.3)) version: 0.5.1(vite@4.0.4(@types/node@18.11.18)(less@4.1.3)(sass@1.89.2))
vite-plugin-progress: vite-plugin-progress:
specifier: ^0.0.6 specifier: ^0.0.6
version: 0.0.6(vite@4.0.4(@types/node@18.11.18)(less@4.1.3)) version: 0.0.6(vite@4.0.4(@types/node@18.11.18)(less@4.1.3)(sass@1.89.2))
vue-eslint-parser: vue-eslint-parser:
specifier: ^9.1.0 specifier: ^9.1.0
version: 9.1.0(eslint@8.33.0) version: 9.1.0(eslint@8.33.0)
@ -550,6 +562,88 @@ packages:
resolution: {integrity: sha512-/KuoCDVGrLD9W7vwuYhu4HbdT/BpbrhA4Pm9dGn7Jah40kHDGqUnJxugvMjt+4suq53rLQyTA0LRDWfFxfxAOQ==} resolution: {integrity: sha512-/KuoCDVGrLD9W7vwuYhu4HbdT/BpbrhA4Pm9dGn7Jah40kHDGqUnJxugvMjt+4suq53rLQyTA0LRDWfFxfxAOQ==}
engines: {node: ^14.16.0 || ^16.10.0 || ^17.0.0 || ^18.0.0 || ^19.0.0} engines: {node: ^14.16.0 || ^16.10.0 || ^17.0.0 || ^18.0.0 || ^19.0.0}
'@parcel/watcher-android-arm64@2.5.1':
resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [android]
'@parcel/watcher-darwin-arm64@2.5.1':
resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [darwin]
'@parcel/watcher-darwin-x64@2.5.1':
resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [darwin]
'@parcel/watcher-freebsd-x64@2.5.1':
resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [freebsd]
'@parcel/watcher-linux-arm-glibc@2.5.1':
resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==}
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
'@parcel/watcher-linux-arm-musl@2.5.1':
resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==}
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
'@parcel/watcher-linux-arm64-glibc@2.5.1':
resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
'@parcel/watcher-linux-arm64-musl@2.5.1':
resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
'@parcel/watcher-linux-x64-glibc@2.5.1':
resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
'@parcel/watcher-linux-x64-musl@2.5.1':
resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
'@parcel/watcher-win32-arm64@2.5.1':
resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [win32]
'@parcel/watcher-win32-ia32@2.5.1':
resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==}
engines: {node: '>= 10.0.0'}
cpu: [ia32]
os: [win32]
'@parcel/watcher-win32-x64@2.5.1':
resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [win32]
'@parcel/watcher@2.5.1':
resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==}
engines: {node: '>= 10.0.0'}
'@polka/url@1.0.0-next.21': '@polka/url@1.0.0-next.21':
resolution: {integrity: sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==} resolution: {integrity: sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==}
@ -595,6 +689,9 @@ packages:
'@types/semver@7.3.13': '@types/semver@7.3.13':
resolution: {integrity: sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==} resolution: {integrity: sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==}
'@types/sortablejs@1.15.8':
resolution: {integrity: sha512-b79830lW+RZfwaztgs1aVPgbasJ8e7AXtZYHTELNXZPsERt4ymJdjV4OccDbHQAvHrCcFpbF78jkm0R6h/pZVg==}
'@types/web-bluetooth@0.0.16': '@types/web-bluetooth@0.0.16':
resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==} resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==}
@ -1032,6 +1129,10 @@ packages:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'} engines: {node: '>= 8.10.0'}
chokidar@4.0.3:
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
engines: {node: '>= 14.16.0'}
chownr@2.0.0: chownr@2.0.0:
resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -1204,6 +1305,11 @@ packages:
resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
detect-libc@1.0.3:
resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==}
engines: {node: '>=0.10'}
hasBin: true
digest-header@1.1.0: digest-header@1.1.0:
resolution: {integrity: sha512-glXVh42vz40yZb9Cq2oMOt70FIoWiv+vxNvdKdU8CwjLad25qHM3trLxhl9bVjdr6WaslIXhWpn0NO8T/67Qjg==} resolution: {integrity: sha512-glXVh42vz40yZb9Cq2oMOt70FIoWiv+vxNvdKdU8CwjLad25qHM3trLxhl9bVjdr6WaslIXhWpn0NO8T/67Qjg==}
engines: {node: '>= 8.0.0'} engines: {node: '>= 8.0.0'}
@ -1684,6 +1790,9 @@ packages:
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
hasBin: true hasBin: true
immutable@5.1.3:
resolution: {integrity: sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==}
import-fresh@3.3.0: import-fresh@3.3.0:
resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
engines: {node: '>=6'} engines: {node: '>=6'}
@ -2124,6 +2233,9 @@ packages:
nice-try@1.0.5: nice-try@1.0.5:
resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==}
node-addon-api@7.1.1:
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
node-fetch-native@1.0.1: node-fetch-native@1.0.1:
resolution: {integrity: sha512-VzW+TAk2wE4X9maiKMlT+GsPU4OMmR1U9CrHSmd3DFLn2IcZ9VJ6M6BBugGfYUnPCLSYxXdZy17M0BEJyhUTwg==} resolution: {integrity: sha512-VzW+TAk2wE4X9maiKMlT+GsPU4OMmR1U9CrHSmd3DFLn2IcZ9VJ6M6BBugGfYUnPCLSYxXdZy17M0BEJyhUTwg==}
@ -2397,6 +2509,10 @@ packages:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'} engines: {node: '>=8.10.0'}
readdirp@4.1.2:
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
engines: {node: '>= 14.18.0'}
regexp.prototype.flags@1.4.3: regexp.prototype.flags@1.4.3:
resolution: {integrity: sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==} resolution: {integrity: sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -2465,6 +2581,11 @@ packages:
safer-buffer@2.1.2: safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
sass@1.89.2:
resolution: {integrity: sha512-xCmtksBKd/jdJ9Bt9p7nPKiuqrlBMBuuGkQlkhZjjQk3Ty48lv93k5Dq6OPkKt4XwxDJ7tvlfrTa1MPA9bf+QA==}
engines: {node: '>=14.0.0'}
hasBin: true
sax@1.2.4: sax@1.2.4:
resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==} resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==}
@ -2550,6 +2671,9 @@ packages:
resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
sortablejs@1.14.0:
resolution: {integrity: sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==}
source-map-js@1.0.2: source-map-js@1.0.2:
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -2895,6 +3019,9 @@ packages:
terser: terser:
optional: true optional: true
vue-cropper@1.1.4:
resolution: {integrity: sha512-5m98vBsCEI9rbS4JxELxXidtAui3qNyTHLHg67Qbn7g8cg+E6LcnC+hh3SM/p94x6mFh6KRxT1ttnta+wCYqWA==}
vue-demi@0.13.11: vue-demi@0.13.11:
resolution: {integrity: sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==} resolution: {integrity: sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==}
engines: {node: '>=12'} engines: {node: '>=12'}
@ -2906,6 +3033,15 @@ packages:
'@vue/composition-api': '@vue/composition-api':
optional: true optional: true
vue-draggable-plus@0.6.0:
resolution: {integrity: sha512-G5TSfHrt9tX9EjdG49InoFJbt2NYk0h3kgjgKxkFWr3ulIUays0oFObr5KZ8qzD4+QnhtALiRwIqY6qul4egqw==}
peerDependencies:
'@types/sortablejs': ^1.15.0
'@vue/composition-api': '*'
peerDependenciesMeta:
'@vue/composition-api':
optional: true
vue-echarts@7.0.3: vue-echarts@7.0.3:
resolution: {integrity: sha512-/jSxNwOsw5+dYAUcwSfkLwKPuzTQ0Cepz1LxCOpj2QcHrrmUa/Ql0eQqMmc1rTPQVrh2JQ29n2dhq75ZcHvRDw==} resolution: {integrity: sha512-/jSxNwOsw5+dYAUcwSfkLwKPuzTQ0Cepz1LxCOpj2QcHrrmUa/Ql0eQqMmc1rTPQVrh2JQ29n2dhq75ZcHvRDw==}
peerDependencies: peerDependencies:
@ -2939,6 +3075,11 @@ packages:
vue@3.2.45: vue@3.2.45:
resolution: {integrity: sha512-9Nx/Mg2b2xWlXykmCwiTUCWHbWIj53bnkizBxKai1g61f2Xit700A1ljowpTIM11e3uipOeiPcSqnmBg6gyiaA==} resolution: {integrity: sha512-9Nx/Mg2b2xWlXykmCwiTUCWHbWIj53bnkizBxKai1g61f2Xit700A1ljowpTIM11e3uipOeiPcSqnmBg6gyiaA==}
vuedraggable@4.1.0:
resolution: {integrity: sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==}
peerDependencies:
vue: ^3.0.1
webpack-sources@3.2.3: webpack-sources@3.2.3:
resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==}
engines: {node: '>=10.13.0'} engines: {node: '>=10.13.0'}
@ -3463,6 +3604,67 @@ snapshots:
- rollup - rollup
- supports-color - supports-color
'@parcel/watcher-android-arm64@2.5.1':
optional: true
'@parcel/watcher-darwin-arm64@2.5.1':
optional: true
'@parcel/watcher-darwin-x64@2.5.1':
optional: true
'@parcel/watcher-freebsd-x64@2.5.1':
optional: true
'@parcel/watcher-linux-arm-glibc@2.5.1':
optional: true
'@parcel/watcher-linux-arm-musl@2.5.1':
optional: true
'@parcel/watcher-linux-arm64-glibc@2.5.1':
optional: true
'@parcel/watcher-linux-arm64-musl@2.5.1':
optional: true
'@parcel/watcher-linux-x64-glibc@2.5.1':
optional: true
'@parcel/watcher-linux-x64-musl@2.5.1':
optional: true
'@parcel/watcher-win32-arm64@2.5.1':
optional: true
'@parcel/watcher-win32-ia32@2.5.1':
optional: true
'@parcel/watcher-win32-x64@2.5.1':
optional: true
'@parcel/watcher@2.5.1':
dependencies:
detect-libc: 1.0.3
is-glob: 4.0.3
micromatch: 4.0.5
node-addon-api: 7.1.1
optionalDependencies:
'@parcel/watcher-android-arm64': 2.5.1
'@parcel/watcher-darwin-arm64': 2.5.1
'@parcel/watcher-darwin-x64': 2.5.1
'@parcel/watcher-freebsd-x64': 2.5.1
'@parcel/watcher-linux-arm-glibc': 2.5.1
'@parcel/watcher-linux-arm-musl': 2.5.1
'@parcel/watcher-linux-arm64-glibc': 2.5.1
'@parcel/watcher-linux-arm64-musl': 2.5.1
'@parcel/watcher-linux-x64-glibc': 2.5.1
'@parcel/watcher-linux-x64-musl': 2.5.1
'@parcel/watcher-win32-arm64': 2.5.1
'@parcel/watcher-win32-ia32': 2.5.1
'@parcel/watcher-win32-x64': 2.5.1
optional: true
'@polka/url@1.0.0-next.21': {} '@polka/url@1.0.0-next.21': {}
'@rollup/pluginutils@5.0.2(rollup@3.12.0)': '@rollup/pluginutils@5.0.2(rollup@3.12.0)':
@ -3497,6 +3699,8 @@ snapshots:
'@types/semver@7.3.13': {} '@types/semver@7.3.13': {}
'@types/sortablejs@1.15.8': {}
'@types/web-bluetooth@0.0.16': {} '@types/web-bluetooth@0.0.16': {}
'@typescript-eslint/eslint-plugin@5.50.0(@typescript-eslint/parser@5.50.0(eslint@8.33.0)(typescript@4.9.5))(eslint@8.33.0)(typescript@4.9.5)': '@typescript-eslint/eslint-plugin@5.50.0(@typescript-eslint/parser@5.50.0(eslint@8.33.0)(typescript@4.9.5))(eslint@8.33.0)(typescript@4.9.5)':
@ -3583,11 +3787,11 @@ snapshots:
'@typescript-eslint/types': 5.50.0 '@typescript-eslint/types': 5.50.0
eslint-visitor-keys: 3.3.0 eslint-visitor-keys: 3.3.0
'@unocss/astro@0.49.2(rollup@3.12.0)(vite@4.0.4(@types/node@18.11.18)(less@4.1.3))': '@unocss/astro@0.49.2(rollup@3.12.0)(vite@4.0.4(@types/node@18.11.18)(less@4.1.3)(sass@1.89.2))':
dependencies: dependencies:
'@unocss/core': 0.49.2 '@unocss/core': 0.49.2
'@unocss/reset': 0.49.2 '@unocss/reset': 0.49.2
'@unocss/vite': 0.49.2(rollup@3.12.0)(vite@4.0.4(@types/node@18.11.18)(less@4.1.3)) '@unocss/vite': 0.49.2(rollup@3.12.0)(vite@4.0.4(@types/node@18.11.18)(less@4.1.3)(sass@1.89.2))
transitivePeerDependencies: transitivePeerDependencies:
- rollup - rollup
- vite - vite
@ -3687,7 +3891,7 @@ snapshots:
dependencies: dependencies:
'@unocss/core': 0.49.2 '@unocss/core': 0.49.2
'@unocss/vite@0.49.2(rollup@3.12.0)(vite@4.0.4(@types/node@18.11.18)(less@4.1.3))': '@unocss/vite@0.49.2(rollup@3.12.0)(vite@4.0.4(@types/node@18.11.18)(less@4.1.3)(sass@1.89.2))':
dependencies: dependencies:
'@ampproject/remapping': 2.2.0 '@ampproject/remapping': 2.2.0
'@rollup/pluginutils': 5.0.2(rollup@3.12.0) '@rollup/pluginutils': 5.0.2(rollup@3.12.0)
@ -3699,23 +3903,23 @@ snapshots:
chokidar: 3.5.3 chokidar: 3.5.3
fast-glob: 3.2.12 fast-glob: 3.2.12
magic-string: 0.27.0 magic-string: 0.27.0
vite: 4.0.4(@types/node@18.11.18)(less@4.1.3) vite: 4.0.4(@types/node@18.11.18)(less@4.1.3)(sass@1.89.2)
transitivePeerDependencies: transitivePeerDependencies:
- rollup - rollup
'@vitejs/plugin-vue-jsx@3.0.0(vite@4.0.4(@types/node@18.11.18)(less@4.1.3))(vue@3.2.45)': '@vitejs/plugin-vue-jsx@3.0.0(vite@4.0.4(@types/node@18.11.18)(less@4.1.3)(sass@1.89.2))(vue@3.2.45)':
dependencies: dependencies:
'@babel/core': 7.20.12 '@babel/core': 7.20.12
'@babel/plugin-transform-typescript': 7.20.13(@babel/core@7.20.12) '@babel/plugin-transform-typescript': 7.20.13(@babel/core@7.20.12)
'@vue/babel-plugin-jsx': 1.1.1(@babel/core@7.20.12) '@vue/babel-plugin-jsx': 1.1.1(@babel/core@7.20.12)
vite: 4.0.4(@types/node@18.11.18)(less@4.1.3) vite: 4.0.4(@types/node@18.11.18)(less@4.1.3)(sass@1.89.2)
vue: 3.2.45 vue: 3.2.45
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@vitejs/plugin-vue@4.0.0(vite@4.0.4(@types/node@18.11.18)(less@4.1.3))(vue@3.2.45)': '@vitejs/plugin-vue@4.0.0(vite@4.0.4(@types/node@18.11.18)(less@4.1.3)(sass@1.89.2))(vue@3.2.45)':
dependencies: dependencies:
vite: 4.0.4(@types/node@18.11.18)(less@4.1.3) vite: 4.0.4(@types/node@18.11.18)(less@4.1.3)(sass@1.89.2)
vue: 3.2.45 vue: 3.2.45
'@volar/language-core@1.0.24': '@volar/language-core@1.0.24':
@ -4137,6 +4341,10 @@ snapshots:
optionalDependencies: optionalDependencies:
fsevents: 2.3.2 fsevents: 2.3.2
chokidar@4.0.3:
dependencies:
readdirp: 4.1.2
chownr@2.0.0: {} chownr@2.0.0: {}
clean-stack@2.2.0: {} clean-stack@2.2.0: {}
@ -4282,6 +4490,9 @@ snapshots:
destroy@1.2.0: {} destroy@1.2.0: {}
detect-libc@1.0.3:
optional: true
digest-header@1.1.0: {} digest-header@1.1.0: {}
dir-glob@3.0.1: dir-glob@3.0.1:
@ -4829,6 +5040,8 @@ snapshots:
image-size@0.5.5: image-size@0.5.5:
optional: true optional: true
immutable@5.1.3: {}
import-fresh@3.3.0: import-fresh@3.3.0:
dependencies: dependencies:
parent-module: 1.0.1 parent-module: 1.0.1
@ -5251,6 +5464,9 @@ snapshots:
nice-try@1.0.5: {} nice-try@1.0.5: {}
node-addon-api@7.1.1:
optional: true
node-fetch-native@1.0.1: {} node-fetch-native@1.0.1: {}
node-releases@2.0.9: {} node-releases@2.0.9: {}
@ -5518,6 +5734,8 @@ snapshots:
dependencies: dependencies:
picomatch: 2.3.1 picomatch: 2.3.1
readdirp@4.1.2: {}
regexp.prototype.flags@1.4.3: regexp.prototype.flags@1.4.3:
dependencies: dependencies:
call-bind: 1.0.2 call-bind: 1.0.2
@ -5582,6 +5800,14 @@ snapshots:
safer-buffer@2.1.2: {} safer-buffer@2.1.2: {}
sass@1.89.2:
dependencies:
chokidar: 4.0.3
immutable: 5.1.3
source-map-js: 1.2.0
optionalDependencies:
'@parcel/watcher': 2.5.1
sax@1.2.4: {} sax@1.2.4: {}
scroll-into-view-if-needed@2.2.31: scroll-into-view-if-needed@2.2.31:
@ -5671,6 +5897,8 @@ snapshots:
ansi-styles: 6.2.1 ansi-styles: 6.2.1
is-fullwidth-code-point: 4.0.0 is-fullwidth-code-point: 4.0.0
sortablejs@1.14.0: {}
source-map-js@1.0.2: {} source-map-js@1.0.2: {}
source-map-js@1.2.0: {} source-map-js@1.2.0: {}
@ -5891,9 +6119,9 @@ snapshots:
universalify@2.0.1: {} universalify@2.0.1: {}
unocss@0.49.2(rollup@3.12.0)(vite@4.0.4(@types/node@18.11.18)(less@4.1.3)): unocss@0.49.2(rollup@3.12.0)(vite@4.0.4(@types/node@18.11.18)(less@4.1.3)(sass@1.89.2)):
dependencies: dependencies:
'@unocss/astro': 0.49.2(rollup@3.12.0)(vite@4.0.4(@types/node@18.11.18)(less@4.1.3)) '@unocss/astro': 0.49.2(rollup@3.12.0)(vite@4.0.4(@types/node@18.11.18)(less@4.1.3)(sass@1.89.2))
'@unocss/cli': 0.49.2(rollup@3.12.0) '@unocss/cli': 0.49.2(rollup@3.12.0)
'@unocss/core': 0.49.2 '@unocss/core': 0.49.2
'@unocss/preset-attributify': 0.49.2 '@unocss/preset-attributify': 0.49.2
@ -5909,7 +6137,7 @@ snapshots:
'@unocss/transformer-compile-class': 0.49.2 '@unocss/transformer-compile-class': 0.49.2
'@unocss/transformer-directives': 0.49.2 '@unocss/transformer-directives': 0.49.2
'@unocss/transformer-variant-group': 0.49.2 '@unocss/transformer-variant-group': 0.49.2
'@unocss/vite': 0.49.2(rollup@3.12.0)(vite@4.0.4(@types/node@18.11.18)(less@4.1.3)) '@unocss/vite': 0.49.2(rollup@3.12.0)(vite@4.0.4(@types/node@18.11.18)(less@4.1.3)(sass@1.89.2))
transitivePeerDependencies: transitivePeerDependencies:
- rollup - rollup
- supports-color - supports-color
@ -6039,23 +6267,23 @@ snapshots:
spdx-correct: 3.1.1 spdx-correct: 3.1.1
spdx-expression-parse: 3.0.1 spdx-expression-parse: 3.0.1
vite-plugin-compression@0.5.1(vite@4.0.4(@types/node@18.11.18)(less@4.1.3)): vite-plugin-compression@0.5.1(vite@4.0.4(@types/node@18.11.18)(less@4.1.3)(sass@1.89.2)):
dependencies: dependencies:
chalk: 4.1.2 chalk: 4.1.2
debug: 4.3.4 debug: 4.3.4
fs-extra: 10.1.0 fs-extra: 10.1.0
vite: 4.0.4(@types/node@18.11.18)(less@4.1.3) vite: 4.0.4(@types/node@18.11.18)(less@4.1.3)(sass@1.89.2)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
vite-plugin-progress@0.0.6(vite@4.0.4(@types/node@18.11.18)(less@4.1.3)): vite-plugin-progress@0.0.6(vite@4.0.4(@types/node@18.11.18)(less@4.1.3)(sass@1.89.2)):
dependencies: dependencies:
picocolors: 1.0.0 picocolors: 1.0.0
progress: 2.0.3 progress: 2.0.3
rd: 2.0.1 rd: 2.0.1
vite: 4.0.4(@types/node@18.11.18)(less@4.1.3) vite: 4.0.4(@types/node@18.11.18)(less@4.1.3)(sass@1.89.2)
vite@4.0.4(@types/node@18.11.18)(less@4.1.3): vite@4.0.4(@types/node@18.11.18)(less@4.1.3)(sass@1.89.2):
dependencies: dependencies:
esbuild: 0.16.17 esbuild: 0.16.17
postcss: 8.4.21 postcss: 8.4.21
@ -6065,11 +6293,18 @@ snapshots:
'@types/node': 18.11.18 '@types/node': 18.11.18
fsevents: 2.3.2 fsevents: 2.3.2
less: 4.1.3 less: 4.1.3
sass: 1.89.2
vue-cropper@1.1.4: {}
vue-demi@0.13.11(vue@3.2.45): vue-demi@0.13.11(vue@3.2.45):
dependencies: dependencies:
vue: 3.2.45 vue: 3.2.45
vue-draggable-plus@0.6.0(@types/sortablejs@1.15.8):
dependencies:
'@types/sortablejs': 1.15.8
vue-echarts@7.0.3(@vue/runtime-core@3.2.45)(echarts@5.6.0)(vue@3.2.45): vue-echarts@7.0.3(@vue/runtime-core@3.2.45)(echarts@5.6.0)(vue@3.2.45):
dependencies: dependencies:
echarts: 5.6.0 echarts: 5.6.0
@ -6117,6 +6352,11 @@ snapshots:
'@vue/server-renderer': 3.2.45(vue@3.2.45) '@vue/server-renderer': 3.2.45(vue@3.2.45)
'@vue/shared': 3.2.45 '@vue/shared': 3.2.45
vuedraggable@4.1.0(vue@3.2.45):
dependencies:
sortablejs: 1.14.0
vue: 3.2.45
webpack-sources@3.2.3: {} webpack-sources@3.2.3: {}
webpack-virtual-modules@0.5.0: {} webpack-virtual-modules@0.5.0: {}

15
src/api/all/common.ts Normal file
View File

@ -0,0 +1,15 @@
/*
* @Author: RenXiaoDong
* @Date: 2025-06-30 14:25:22
*/
import Http from '@/api';
// 获取用户自定义列
export const getCustomColumns = (params = {}) => {
return Http.get('/v1/custom-columns', params);
};
// 保存用户自定义列
export const updateCustomColumns = (params = {}) => {
return Http.put('/v1/custom-columns', params);
};

View File

@ -0,0 +1,26 @@
import Http from '@/api';
// 获取物料列表
export const getMaterialsList = (params = {}) => {
// 发送GET请求获取行业树
return Http.get('v1/materials', params);
};
//删除物料
export const deleteMaterials = (id: number) => {
return Http.delete('v1/materials/' + id);
};
//添加物料
export const addMaterials = (data: any) => {
return Http.post('v1/materials', data);
};
//修改物料
export const updateMaterials = (id: number, data: any) => {
return Http.put('v1/materials/' + id, data);
};
//获取详情
export const getMaterialsDetail = (id: number) => {
return Http.get('v1/materials/' + id);
};

View File

@ -0,0 +1,330 @@
/*
* @Author: RenXiaoDong
* @Date: 2025-06-25 17:34:56
*/
import Http from '@/api';
// 媒体账号标签-列表
export const fetchAccountTags = (params = {}) => {
return Http.get('/v1/media-account-tags/list', params);
};
// 媒体账号分组-列表
export const fetchAccountGroups = (params = {}) => {
return Http.get('/v1/media-account-groups/list', params);
};
// 媒体运营人员分组-列表
export const fetchAccountOperators = (params = {}) => {
return Http.get('/v1/media-account-operators/list', params);
};
// 媒体账号-分页
export const getMediaAccounts = (params = {}) => {
return Http.get('/v1/media-accounts', params);
};
// 媒体账号-健康情况
export const getMediaAccountsHealth = (params = {}) => {
return Http.get('/v1/media-accounts/health', params);
};
// 投放账号-健康情况
export const getPlacementAccountsHealth = (params = {}) => {
return Http.get('/v1/placement-accounts/health', params);
};
// 媒体账号-添加
export const postMediaAccounts = (params = {}) => {
return Http.post('/v1/media-accounts', params);
};
// 媒体账号-详情
export const getMediaAccountsDetail = (id: string) => {
return Http.get(`/v1/media-accounts/${id}`);
};
// 媒体账号-修改
export const putMediaAccounts = (params = {}) => {
const { id, ...rest } = params as { id: string; [key: string]: any };
return Http.put(`/v1/media-accounts/${id}`, rest);
};
// 媒体账号-删除
export const deleteMediaAccount = (id: string) => {
return Http.delete(`/v1/media-accounts/${id}`);
};
// 媒体账号-获取模板地址
export const getTemplateUrl = (params = {}) => {
return Http.get('/v1/media-accounts/template', params);
};
// 媒体账号分组-分页
export const getAccountGroup = (data: any) => {
return Http.get('/v1/media-account-groups', data);
};
// 媒体账号分组-列表
export const getAccountGroupList = (params = {}) => {
return Http.get('/v1/media-account-groups/list', params);
};
// 媒体账号分组 -添加
export const postAccountGroups = (params = {}) => {
return Http.post('/v1/media-account-groups', params);
};
// 媒体账号分组-编辑
export const putGroup = (params = {}) => {
const { id, ...rest } = params as { id: string; [key: string]: any };
return Http.put(`/v1/media-account-groups/${id}`, rest);
};
// 媒体账号分组-删除
export const deleteGroup = (id: string) => {
return Http.delete(`/v1/media-account-groups/${id}`);
};
// 媒体账号标签-列表
export const getTagsList = (params = {}) => {
return Http.get('/v1/media-account-tags/list', params);
};
// 媒体账号标签-添加
export const postAccountTags = (params = {}) => {
return Http.post('/v1/media-account-tags', params);
};
// 媒体账号标签-修改
export const putTag = (params = {}) => {
const { id, ...rest } = params as { id: string; [key: string]: any };
return Http.put(`/v1/media-account-tags/${id}`, rest);
};
// 媒体账号标签-删除
export const deleteTag = (id: string) => {
return Http.delete(`/v1/media-account-tags/${id}`);
};
// 媒体账号-批量删除
export const batchDeleteMediaAccounts = (params = {}) => {
return Http.delete(`/v1/media-accounts/batch`, { data: params });
};
// 媒体账号-批量标签
export const batchPutTag = (params = {}) => {
return Http.put(`/v1/media-accounts/batch-tag`, params);
};
// 媒体账号-批量分组
export const batchPutGroup = (params = {}) => {
return Http.put(`/v1/media-accounts/batch-group`, params);
};
// 媒体账号-暂停爬取
export const pausePatchAccount = (id: string) => {
return Http.patch(`/v1/media-accounts/${id}/pause`);
};
// 媒体账号-开始爬取
export const startPatchAccount = (id: string) => {
return Http.patch(`/v1/media-accounts/${id}/start`);
};
// 媒体账号-获取授权图片
export const getAuthorizedImage = (id: string) => {
return Http.get(`/v1/media-accounts/${id}/authorize/image`);
};
// 账号看板-数据总览
export const getAccountBoardOverview = (params = {}) => {
return Http.get('/v1/media-account-boards/overview', params);
};
// 账号看板-分页
export const getAccountBoardList = (params = {}) => {
return Http.get('/v1/media-account-boards', params);
};
// 账号看板-导出
export const postAccountBoardExport = (params = {}) => {
return Http.post('/v1/media-account-boards/export', params);
};
// 账号看板-详情
export const getAccountBoardDetail = (id: string) => {
return Http.get(`/v1/media-account-boards/${id}`);
};
// 投放账号-分页
export const getPlacementAccounts = (params = {}) => {
return Http.get('/v1/placement-accounts', params);
};
// 投放账号-暂停爬取
export const pausePatchPlacementAccount = (id: string) => {
return Http.patch(`/v1/placement-accounts/${id}/pause`);
};
// 投放账号-删除
export const deletePlacementAccount = (id: string) => {
return Http.delete(`/v1/placement-accounts/${id}`);
};
// 投放账号-批量删除
export const batchDeletePlacementAccounts = (params = {}) => {
return Http.delete(`/v1/placement-accounts/batch`, { data: params });
};
// 投放账号-批量添加
export const batchPlacementAccounts = (params = {}, config = {}) => {
return Http.post('/v1/placement-accounts/batch', params, config);
};
// 投放账号-修改
export const putPlacementAccounts = (params = {}) => {
const { id, ...rest } = params as { id: string; [key: string]: any };
return Http.put(`/v1/placement-accounts/${id}`, rest);
};
// 投放账号-添加
export const postPlacementAccounts = (params = {}) => {
return Http.post('/v1/placement-accounts', params);
};
// 投放账号-详情
export const getPlacementAccountsDetail = (id: string) => {
return Http.get(`/v1/placement-accounts/${id}`);
};
// 投放账号-授权
export const putPlacementAccountsAuthorized = (params = {}) => {
const { id, ...rest } = params as { id: string; [key: string]: any };
return Http.put(`/v1/placement-accounts/${id}/authorize`, rest);
};
// 投放账号-获取模板地址
export const getPlacementAccountsTemplateUrl = (params = {}) => {
return Http.get('/v1/placement-accounts/template', params);
};
// 投放账号-查询授权状态
export const getPlacementAccountsAuthorizedStatus = (id: string) => {
return Http.get(`/v1/placement-accounts/${id}/status`);
};
// 投放账户运营人员分组-列表
export const getPlacementAccountOperators = (params = {}) => {
return Http.get('/v1/placement-account-operators/list', params);
};
// 投放账号项目分组-分页
export const getPlacementAccountProjectGroups = (params = {}) => {
return Http.get('/v1/placement-account-project-groups', params);
};
// 投放账号项目分组-列表
export const getPlacementAccountProjectGroupsList = (params = {}) => {
return Http.get('/v1/placement-account-project-groups/list', params);
};
// 投放账号项目分组-添加
export const postPlacementAccountProjectGroups = (params = {}) => {
return Http.post('/v1/placement-account-project-groups', params);
};
// 投放账号项目分组-修改
export const putPlacementAccountProjectGroups = (params = {}) => {
const { id, ...rest } = params as { id: string; [key: string]: any };
return Http.put(`/v1/media-account-project-groups/${id}`, rest);
};
// 投放账号项目分组-删除
export const deletePlacementAccountProjectGroups = (id: string) => {
return Http.delete(`/v1/placement-account-project-groups/${id}`);
};
// 账号笔记-分页
export const getMediaAccountBoardWorks = (id: string, params = {}) => {
return Http.get(`/v1/media-account-boards/${id}/works`, params);
};
// 投放账号数据-分页
export const getPlacementAccountData = (params = {}) => {
return Http.get('/v1/placement-account-boards', params);
};
// 投放账号数据-导出
export const postPlacementAccountDataExport = (params = {}) => {
return Http.post('/v1/placement-account-boards/export', params);
};
// 投放账户项目数据-分页
export const getPlacementAccountDataList = (params = {}) => {
return Http.get('/v1/placement-account-projects', params);
};
// 投放账户项目数据-导出
export const postPlacementAccountDataListExport = (params = {}) => {
return Http.post('/v1/placement-account-projects/export', params);
};
// 媒体账号-批量添加
export const batchMediaAccounts = (params = {}, config = {}) => {
return Http.post('/v1/media-accounts/batch', params, config);
};
// 媒体账号-查询授权状态
export const getMediaAccountsAuthorizedStatus = (id: string) => {
return Http.get(`/v1/media-accounts/${id}/status`);
};
// 投放账号-趋势
export const getPlacementAccountsTrend = (params = {}) => {
return Http.get(`/v1/placement-accounts/trend`, params);
};
// 投放账号计划数据-趋势
export const getPlacementAccountProjectsTrend = (params = {}) => {
return Http.get(`/v1/placement-account-projects/trend`, params);
};
// 投放指南查询
export const getPlacementGuide = (params: {}) => {
return Http.get(`/v1/placement-account-projects/getGuideList`, params);
};
//查询投放指南历史
export const getPlacementGuideHistory = (params: {}) => {
return Http.get(`/v1/placement-account-projects/getGuideListHistory`, params);
};
// 前端定时轮询获取ai检测结果
export const getAiResult = (params: {}) => {
return Http.get(`/v1/placement-account-projects/getAiResult`, params);
};
export const savePlacementGuide = (params: {}) => {
return Http.post(`/v1/placement-account-projects/saveGuideResult`, params);
};
export const getPlacementGuideDetail = (id: string) => {
return Http.get(`/v1/placement-account-projects/historylog/${id}`);
};
//删除记录
export const deleteHistorylog = (id: string) => {
return Http.delete(`/v1/placement-account-projects/historylog/${id}`);
};
// 投放账号-列表
export const getPlacementAccountsList = (params = {}) => {
return Http.get('/v1/placement-accounts/list', params);
};
// 投放账号-同步数据
export const postPlacementAccountsSync = (id: string) => {
return Http.post(`/v1/placement-accounts/${id}/sync-data`);
};

View File

@ -2,27 +2,26 @@
* @Author: 田鑫 * @Author: 田鑫
* @Date: 2023-02-17 11:58:44 * @Date: 2023-02-17 11:58:44
* @LastEditors: Please set LastEditors * @LastEditors: Please set LastEditors
* @LastEditTime: 2025-06-23 05:51:32 * @LastEditTime: 2025-07-05 17:59:59
* @Description: * @Description:
*/ */
import axios from 'axios'; import axios from 'axios';
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
import { handleUserLogout } from '@/utils/user'; import { handleUserLogout, goUserLogin } from '@/utils/user';
import { useEnterpriseStore } from '@/stores/modules/enterprise';
import pinia from '@/stores';
const contentType = 'application/json'; const contentType = 'application/json';
const requestTimeout = 30000; const requestTimeout = 30000;
const HttpStatusCode = { enum HttpStatusCode {
OK: 200, Success = 200,
BadRequest: 400, // 请求参数错误 BadRequest = 400, // 请求参数错误
Unauthorized: 401, // token 无效或过期 Unauthorized = 401, // token 无效或过期
NotFound: 404, NotFound = 404,
InternalServerError: 500, InternalServerError = 500,
}; }
import { useEnterpriseStore } from '@/stores/modules/enterprise';
import pinia from '@/stores';
//* 导出Request类可以用来自定义传递配置来创建实例 //* 导出Request类可以用来自定义传递配置来创建实例
export class Request { export class Request {
@ -63,16 +62,30 @@ export class Request {
(res: AxiosResponse) => { (res: AxiosResponse) => {
const { data } = res; const { data } = res;
switch (data.code) { switch (data.code) {
case HttpStatusCode.OK: case HttpStatusCode.Success:
return data; return data;
default: default:
return Promise.reject(data); return Promise.reject(data);
} }
}, },
(err: any) => { (err: any) => {
const message = err.response?.data?.message ?? err.message; const { response } = err;
AMessage.error(message); const status = response?.status;
// 这里用来处理http常见错误进行全局提示 let errMessage = response?.data?.message ?? err.message;
switch (status) {
case HttpStatusCode.InternalServerError:
errMessage = '系统繁忙,请稍后再试或联系管理员。';
break;
case HttpStatusCode.NotFound:
errMessage = '接口不存在';
break;
case HttpStatusCode.Unauthorized:
handleUserLogout();
break;
}
AMessage.error(errMessage);
return Promise.reject(err.response); return Promise.reject(err.response);
}, },
); );

View File

@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.39993 6.99966C1.39993 3.90701 3.90701 1.39993 6.99966 1.39993C10.0923 1.39993 12.5994 3.90701 12.5994 6.99966C12.5994 10.0923 10.0923 12.5994 6.99966 12.5994C3.90701 12.5994 1.39993 10.0923 1.39993 6.99966ZM6.99966 0C3.13385 0 0 3.13385 0 6.99966C0 10.8655 3.13385 13.9993 6.99966 13.9993C10.8655 13.9993 13.9993 10.8655 13.9993 6.99966C13.9993 3.13385 10.8655 0 6.99966 0ZM6.29969 6.92455L8.76161 9.57585L9.78747 8.62326L7.69962 6.37481V3.14985H6.29969V6.92455Z" fill="#3C4043"/>
</svg>

After

Width:  |  Height:  |  Size: 636 B

3
src/assets/icon_plus.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="13" height="13" viewBox="0 0 13 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.81579 5.81579V0H7.18422V5.81579H13V7.18422H7.18422V13H5.81579V7.18422H0V5.81579H5.81579Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 260 B

View File

@ -0,0 +1,3 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6 1.33333C3.42267 1.33333 1.33333 3.42267 1.33333 6C1.33333 8.57733 3.42267 10.6667 6 10.6667C8.17357 10.6667 10.0016 9.18007 10.5197 7.16716L11.8109 7.49951C11.1449 10.0872 8.79667 12 6 12C2.68629 12 0 9.31371 0 6C0 2.68629 2.68629 0 6 0C7.88554 0 9.56708 0.86964 10.6667 2.22866V0.666667H12V4.66667H8V3.33333H9.83038C8.98668 2.12378 7.58512 1.33333 6 1.33333Z" fill="#3C4043"/>
</svg>

After

Width:  |  Height:  |  Size: 533 B

View File

@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.96077 2.89207C8.0088 0.940101 4.84404 0.940101 2.89207 2.89207C0.940101 4.84404 0.940101 8.0088 2.89207 9.96077C4.84404 11.9127 8.0088 11.9127 9.96077 9.96077C11.9127 8.0088 11.9127 4.84404 9.96077 2.89207ZM1.88225 1.88225C4.39193 -0.627418 8.46091 -0.627418 10.9706 1.88225C13.3096 4.22126 13.4686 7.91472 11.4478 10.4379L14 12.9902L12.9902 14L10.4379 11.4478C7.91472 13.4686 4.22126 13.3096 1.88225 10.9706C-0.627418 8.46091 -0.627418 4.39193 1.88225 1.88225Z" fill="#737478"/>
</svg>

After

Width:  |  Height:  |  Size: 635 B

View File

@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.96077 2.89207C8.0088 0.940101 4.84404 0.940101 2.89207 2.89207C0.940101 4.84404 0.940101 8.0088 2.89207 9.96077C4.84404 11.9127 8.0088 11.9127 9.96077 9.96077C11.9127 8.0088 11.9127 4.84404 9.96077 2.89207ZM1.88225 1.88225C4.39193 -0.627418 8.46091 -0.627418 10.9706 1.88225C13.3096 4.22126 13.4686 7.91472 11.4478 10.4379L14 12.9902L12.9902 14L10.4379 11.4478C7.91472 13.4686 4.22126 13.3096 1.88225 10.9706C-0.627418 8.46091 -0.627418 4.39193 1.88225 1.88225Z" fill="#6D4CFE"/>
</svg>

After

Width:  |  Height:  |  Size: 635 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 903 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 483 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 641 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 513 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 760 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 632 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 639 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 490 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 791 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M11.4165 8.97559C12.8259 8.97562 13.969 10.118 13.9693 11.5273L13.9556 11.7891C13.9073 12.2634 13.7275 12.6982 13.4556 13.0596L14.0542 13.6582C14.2495 13.8535 14.2495 14.17 14.0542 14.3652C13.8589 14.5603 13.5424 14.5604 13.3472 14.3652L12.7085 13.7256C12.3292 13.949 11.8886 14.0801 11.4165 14.0801L11.1558 14.0664C9.86908 13.9356 8.86497 12.8485 8.86477 11.5273C8.86501 10.1182 10.0073 8.97583 11.4165 8.97559ZM8.74074 3.80469C8.74092 4.35682 9.18857 4.80469 9.74074 4.80469L11.8862 4.80469V7.85937C11.7712 7.84857 11.6545 7.84278 11.5366 7.84277C9.49559 7.84277 7.84035 9.49802 7.84035 11.5391C7.84042 12.5835 8.27458 13.5262 8.97121 14.1982H2.79934C2.24716 14.1981 1.79934 13.7504 1.79934 13.1982L1.79934 2.48828C1.79934 1.93608 2.24716 1.48841 2.79934 1.48828L8.74074 1.48828V3.80469ZM11.4165 9.97559C10.5596 9.97583 9.86501 10.6704 9.86477 11.5273C9.86498 12.3843 10.5596 13.0798 11.4165 13.0801C12.2736 13.08 12.969 12.3844 12.9693 11.5273C12.969 10.6703 12.2736 9.97562 11.4165 9.97559ZM3.48879 6.35156C3.37847 6.35156 3.28882 6.44051 3.28859 6.55078V7.0498C3.28868 7.16018 3.37839 7.25 3.48879 7.25L7.14211 7.25C7.25238 7.24984 7.34221 7.16009 7.3423 7.0498V6.55078C7.34208 6.44061 7.25229 6.35172 7.14211 6.35156L3.48879 6.35156ZM3.48879 4.04395C3.37837 4.04395 3.28866 4.13374 3.28859 4.24414V4.74316C3.28884 4.85341 3.37848 4.94238 3.48879 4.94238H7.14211C7.25228 4.94222 7.34206 4.85332 7.3423 4.74316V4.24414C7.34223 4.13384 7.25239 4.0441 7.14211 4.04395H3.48879ZM11.978 3.91895L10.0474 3.91895C9.77124 3.91895 9.54738 3.69509 9.54738 3.41895V1.48828L11.978 3.91895Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M10.0492 9.03058C12.2731 9.03059 14.0867 10.8117 14.1273 13.0352C14.1439 13.9477 13.4092 14.6964 12.4966 14.6966L3.50042 14.6966C2.59261 14.6965 1.86034 13.9541 1.87254 13.0463C1.90255 10.8197 3.71631 9.03058 5.94319 9.03058H10.0492ZM7.87154 1.30322C9.93387 1.30328 11.6061 2.88101 11.6064 4.8269C11.6064 6.773 9.93402 8.35146 7.87154 8.35151C5.809 8.35151 4.1367 6.77303 4.1367 4.8269C4.13695 2.88097 5.80916 1.30322 7.87154 1.30322Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 571 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M13.434 6.4018C13.7387 6.4018 13.9863 6.64936 13.9863 6.95409L13.9863 13.4136C13.9863 13.9214 13.5744 14.333 13.0667 14.3332L2.94324 14.3332C2.4355 14.333 2.02367 13.9214 2.02367 13.4136L2.02367 6.95409C2.02367 6.64938 2.27125 6.40182 2.57595 6.4018L13.434 6.4018ZM3.6482 9.82418C3.49585 9.8242 3.3725 9.94752 3.3725 10.0999L3.3725 12.6521C3.37274 12.8042 3.496 12.9277 3.6482 12.9278H5.6948C5.84701 12.9278 5.97025 12.8042 5.97049 12.6521L5.97049 10.0999C5.97049 9.94751 5.84716 9.82418 5.6948 9.82418H3.6482ZM7.55192 5.66362L2.38198 5.66362C2.1291 5.66334 1.95132 5.41309 2.03444 5.1742L3.04113 2.28345C3.16976 1.9142 3.51856 1.66682 3.90952 1.6665L7.55192 1.6665L7.55192 5.66362ZM12.0537 1.6665C12.4421 1.66658 12.7886 1.91054 12.9194 2.27626L13.9549 5.1715C14.0405 5.41104 13.8626 5.66357 13.6082 5.66362L8.44276 5.66362V1.6665L12.0537 1.6665Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 984 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M2.97785 10.8532C3.06071 10.71 3.24367 10.6612 3.38703 10.7438L6.48859 12.5348C6.63203 12.6176 6.68168 12.8005 6.59894 12.944L5.44757 14.9371C5.34178 15.1204 5.08511 15.1398 4.95343 14.9743L3.88214 13.6256C3.83419 13.5653 3.76511 13.5259 3.68878 13.5153L2.04816 13.2897C1.83724 13.2606 1.72302 13.0269 1.82941 12.8424L2.97785 10.8532ZM12.6126 10.7438C12.7561 10.661 12.9399 10.7097 13.0228 10.8532L14.1712 12.8424C14.2775 13.0269 14.1625 13.2607 13.9515 13.2897L12.3118 13.5153C12.2353 13.5258 12.1655 13.5651 12.1175 13.6256L11.0472 14.9743C10.9155 15.14 10.6579 15.1205 10.5521 14.9371L9.40167 12.944C9.31889 12.8006 9.36778 12.6177 9.51105 12.5348L12.6126 10.7438ZM7.55304 0.954723C7.82985 0.795119 8.17083 0.794962 8.44757 0.954723L12.7542 3.44105C13.0311 3.60091 13.2015 3.89674 13.2015 4.21644L13.2015 9.1891C13.2015 9.50881 13.0311 9.80462 12.7542 9.96449L8.44757 12.4508C8.17085 12.6105 7.82982 12.6104 7.55304 12.4508L3.2464 9.96449C2.96951 9.80462 2.79816 9.50883 2.79816 9.1891L2.79816 4.21644C2.79817 3.89673 2.96952 3.60091 3.2464 3.44105L7.55304 0.954723ZM10.8304 4.61488C10.5246 4.42717 10.1247 4.52313 9.93683 4.82875L8.0921 7.8307L6.04914 4.80433L5.96417 4.70375C5.74823 4.49246 5.406 4.45398 5.14582 4.62953C4.88559 4.80537 4.79393 5.1373 4.90949 5.41664L4.97101 5.53285L7.58039 9.39418C7.70405 9.57726 7.91224 9.6851 8.13312 9.68031C8.35384 9.67538 8.55762 9.5589 8.67316 9.37074L11.0443 5.50843C11.2316 5.20262 11.136 4.80263 10.8304 4.61488Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,3 @@
<svg width="10" height="1" viewBox="0 0 10 1" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.5 1H0V0H9.5V1Z" fill="#737478"/>
</svg>

After

Width:  |  Height:  |  Size: 186 B

View File

@ -87,7 +87,30 @@ export default defineComponent({
// 跳过没有 name 的菜单项,防止 key 报错 // 跳过没有 name 的菜单项,防止 key 报错
if (!element?.name) return; if (!element?.name) return;
const icon = element?.meta?.icon ? () => h(element?.meta?.icon as object) : null; // const icon element?.meta?.icon
const icon = element?.meta?.icon
? (() => {
if (typeof element.meta.icon === 'string') {
return h(
'svg',
{
style: {
width: '16px',
height: '16px',
},
},
[
h('use', {
'xlink:href': element.meta.icon,
}),
],
);
} else {
// 如果是对象,按原来的方式渲染
return h(element.meta.icon as object);
}
})()
: null;
if (element.children && element.children.length > 0) { if (element.children && element.children.length > 0) {
nodes.push( nodes.push(
<a-sub-menu <a-sub-menu
@ -146,7 +169,7 @@ export default defineComponent({
} }
.arco-menu-title { .arco-menu-title {
color: var(--Text-2, #3c4043); color: var(--Text-2, #3c4043);
font-family: 'PuHuiTi-Regular'; font-family: 'PuHuiTi-Medium';
font-size: 16px; font-size: 16px;
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;

View File

@ -9,6 +9,7 @@ import { MENU_GROUP_IDS } from '@/router/constants';
import router from '@/router'; import router from '@/router';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import ExitAccountModal from '@/components/_base/exit-account-modal/index.vue'; import ExitAccountModal from '@/components/_base/exit-account-modal/index.vue';
import { appRoutes } from '@/router/routes';
interface MenuItem { interface MenuItem {
name: string; name: string;
@ -56,21 +57,31 @@ const handleSelect = (index: any) => {
} }
}; };
const flattenRoutes = (routes: any, parentPath = ''): any[] => {
let result: any[] = [];
for (const route of routes) {
const fullPath = route.path.startsWith('/') ? route.path : parentPath.replace(/\/$/, '') + '/' + route.path;
if (route.children && route.children.length) {
result = result.concat(flattenRoutes(route.children, fullPath));
} else {
result.push({ ...route, fullPath });
}
}
return result;
};
const handleDopdownClick = (index: any, ind: any) => { const handleDopdownClick = (index: any, ind: any) => {
const { children } = lists.value[index]; const { children } = lists.value[index];
const indPath = children[ind] as any; const indPath = children[ind];
if (indPath.name === '行业热门话题洞察') {
router.push('/dataEngine/hotTranslation'); const allChildren = flattenRoutes(appRoutes);
} else if (indPath.name === '行业词云') {
router.push('/dataEngine/hotCloud'); const target = allChildren.find((item) => item.meta && item.meta.menuId === indPath.id);
} else if (indPath.name === '行业关键词动向') {
router.push('/dataEngine/keyWord'); if (target) {
} else if (indPath.name === '用户痛点观察') { router.push(target.fullPath);
router.push('/dataEngine/userPainPoints'); } else {
} else if (indPath.name === '重点品牌动向') { console.warn('未找到对应的菜单路由', indPath.id);
router.push('/dataEngine/keyBrandMovement');
} else if (indPath.name === '用户画像') {
router.push('/dataEngine/userPersona');
} }
}; };
</script> </script>

Binary file not shown.

After

Width:  |  Height:  |  Size: 677 B

View File

@ -0,0 +1,181 @@
<!--
* @Author: RenXiaoDong
* @Date: 2025-06-30 10:54:49
-->
<template>
<a-modal
v-model:visible="visible"
title="自定义列"
width="960px"
unmountOnClose
titleAlign="start"
class="custom-table-column-modal"
@close="close"
>
<div class="modal-body">
<!-- 左侧分组 -->
<div class="left">
<div v-for="group in dataSource" :key="group.label" class="group-item">
<div class="title-row">
<span class="text">{{ group.label }}</span>
</div>
<div class="fields">
<a-checkbox
v-for="option in group.columns"
:key="option.value"
:model-value="isCheck(option)"
:value="option.value"
:disabled="option.is_require === ENUM_STATUS.NO"
@change="(checked) => onCheckChange(checked, option)"
>
{{ option.label }}
</a-checkbox>
</div>
</div>
</div>
<!-- 右侧已选 -->
<div class="right">
<span class="checked-title mb-16px">
已添加<span class="count">{{ checkColumns.length }}</span>
</span>
<div class="checked-list">
<div v-for="(groupName, index) in requiredGroupNames" :key="index" class="checked-item !cursor-default">
<div class="flex items-center">
<img :src="icon1" alt="icon" class="mr-8px" width="16" height="16" />
<span>{{ groupName }}</span>
</div>
</div>
<VueDraggable v-model="checkColumns">
<div
v-for="item in checkColumns"
:key="item"
class="checked-item justify-between"
:class="isRequiredColumn(item) ? '!display-none' : ''"
>
<div class="flex items-center">
<icon-menu size="16" class="mr-8px" />
<span>{{ getCheckColumnLabel(item) }}</span>
</div>
<icon-close size="16" class="color-#737478 cursor-pointer" @click="removeCheckedField(item)" />
</div>
</VueDraggable>
</div>
</div>
</div>
<template #footer>
<div style="text-align: right">
<a-button class="mr-8px cancel-btn" size="medium" @click="close">取消</a-button>
<a-button type="primary" size="medium" @click="onSubmit">确定</a-button>
</div>
</template>
</a-modal>
</template>
<script setup>
import { ref, defineExpose } from 'vue';
import { VueDraggable } from 'vue-draggable-plus';
import { getCustomColumns, updateCustomColumns } from '@/api/all/common';
import icon1 from './img/icon-lock.png';
const props = defineProps({
type: {
type: String,
default: '',
required: true,
},
});
const emit = defineEmits(['success']);
const ENUM_STATUS = {
YES: 0,
NO: 1,
};
const visible = ref(false);
const dataSource = ref([]);
const checkColumns = ref([]); // 选中字段
const allColumns = ref([]); // 所有字段
const requiredGroupNames = ref([]); // 必选分组名称
const open = () => {
initData();
visible.value = true;
};
const close = () => {
visible.value = false;
dataSource.value = [];
checkColumns.value = [];
allColumns.value = [];
requiredGroupNames.value = [];
};
const initData = async () => {
const { code, data } = await getCustomColumns({ type: props.type });
if (code === 200) {
const { selected_columns, groups } = data;
dataSource.value = groups;
setDefaultCheckColumns(groups, selected_columns);
allColumns.value = groups.flatMap((group) => group.columns);
}
};
const isCheck = (option) => {
return checkColumns.value.includes(option.value);
};
const getCheckColumnLabel = (value) => {
const column = allColumns.value.find((column) => column.value === value);
return column?.label;
};
const isRequiredColumn = (value) => {
const column = allColumns.value.find((column) => column.value === value);
return column?.is_require === 1;
};
const removeCheckedField = (value) => {
checkColumns.value = checkColumns.value.filter((column) => column !== value);
};
// 勾选/取消
const onCheckChange = (checked, option) => {
if (checked) {
checkColumns.value.push(option.value);
} else {
checkColumns.value = checkColumns.value.filter((item) => item !== option.value);
}
};
// 提交
const onSubmit = async () => {
const { code } = await updateCustomColumns({ type: props.type, selected_columns: checkColumns.value });
if (code === 200) {
emit('success', checkColumns.value);
close();
}
};
const setDefaultCheckColumns = (groups, selected_columns) => {
const requiredGroups = groups.filter((group) => group.is_require === 1);
const requiredValues = requiredGroups
.flatMap((group) => (group.columns || []).filter((col) => col.is_require === 1))
.map((col) => col.value);
const merged = union(requiredValues, selected_columns);
checkColumns.value = merged;
requiredGroupNames.value = requiredGroups.map((group) => group.label);
};
// 暴露方法
defineExpose({ open });
</script>
<style lang="scss">
@import './style.scss';
</style>

View File

@ -0,0 +1,99 @@
.custom-table-column-modal {
.arco-modal-body {
.modal-body {
height: 504px;
border-radius: 8px;
border: 1px solid var(--BG-300, #e6e6e8);
display: flex;
flex-direction: row;
.left {
flex: 1;
padding: 20px;
border-right: 1px solid #eee;
overflow-y: auto;
.group-item {
.title-row {
border-radius: 4px;
background: var(--BG-100, #f7f8fa);
display: flex;
height: 44px;
padding: 0px 12px;
align-items: center;
margin-bottom: 16px;
.text {
color: var(--Text-1, #211f24);
font-family: 'PuHuiTi-Medium';
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 24px; /* 150% */
}
}
.fields {
width: 100%;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
&:not(:last-child) {
margin-bottom: 24px;
}
}
}
.right {
width: 280px;
padding: 16px 12px;
display: flex;
flex-direction: column;
.checked-title {
color: var(--Text-1, #211f24);
font-family: 'PuHuiTi-Medium';
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 24px;
.count {
font-weight: 500;
}
}
.checked-list {
flex: 1;
overflow-y: auto;
.checked-item {
cursor: move;
border-radius: 4px;
background: var(--BG-100, #f7f8fa);
display: flex;
height: 32px;
padding: 0px 12px;
align-items: center;
.text {
color: var(--Text-1, #211f24);
font-family: 'PuHuiTi-Medium';
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px; /* 157.143% */
}
&:not(:last-child) {
margin-bottom: 12px;
}
}
.draggable-list {
min-height: 40px;
}
}
}
}
}
.arco-modal-footer {
.cancel-btn {
border-radius: 4px;
border: 1px solid var(--BG-500, #b1b2b5);
background: var(--BG-white, #fff);
&:hover {
border: 1px solid var(--BG-500, #b1b2b5);
}
}
}
}

View File

@ -0,0 +1,141 @@
<template>
<a-upload
:custom-request="customRequest"
list-type="picture-card"
action="/"
:limit="limit"
:fileList="fileList"
image-preview
@change="onChange"
@success="handleSuccess"
@error="handleError"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { Message } from '@arco-design/web-vue';
import { fetchImageUploadFile } from '@/api/all';
import axios from 'axios';
const fileList = ref([]);
const props = defineProps({
modelValue: {
type: [Array, String],
default: '',
},
limit: {
type: Number,
default: 0, // 0 表示不限制
},
});
const emit = defineEmits(['update:modelValue']);
const handleSuccess = (fileItem) => {
let response = fileItem.response;
response = JSON.parse(response);
if (response && response.data.file_url) {
if (props.limit === 1) {
emit('update:modelValue', response.data.file_url);
} else {
emit('update:modelValue', [...props.modelValue, response.data.file_url]);
}
}
};
watch(
() => props.modelValue,
async (value) => {
console.log(value, 'value');
if (value) {
fileList.value =
props.limit == 1
? [
{
name: '',
url: props.modelValue as string,
},
]
: (props.modelValue as string[]).map((item) => {
return {
name: '',
url: item,
};
});
} else {
fileList.value = [];
}
},
{ once: true },
);
let previousFileListLength = 0;
//删除图片
const onChange = (fileList) => {
if (fileList.length < previousFileListLength) {
// 在这里执行你需要的操作
if (props.limit === 1) {
if (fileList.length === 0) {
emit('update:modelValue', '');
}
} else {
if (fileList.length === 0) {
emit('update:modelValue', []);
} else {
let image_data = fileList.map((item) => item.url);
emit('update:modelValue', image_data);
}
}
}
// 更新上一次的文件列表长度
previousFileListLength = fileList.length;
};
const beforeUpload = (file, files) => {
if (props.limit > 0 && files.length >= props.limit) {
Message.warning(`最多只能上传 ${props.limit} 张图片`);
return false; // 阻止上传
}
return true;
};
const handleError = (error) => {
Message.error('上传失败');
console.error(error);
};
const customRequest = async (option) => {
const { onProgress, onError, onSuccess, fileItem, name } = option;
try {
// 1. 获取预签名上传URL
const response = await fetchImageUploadFile({ suffix: getFileExtension(fileItem.file.name) });
const preSignedUrl = response?.data?.upload_url;
if (!preSignedUrl) {
throw new Error('未能获取有效的预签名上传地址');
}
console.log('preSignedUrl', preSignedUrl);
// 2. 使用预签名URL上传文件
const blob = new Blob([fileItem.file], { type: fileItem.file.type });
await axios.put(preSignedUrl, blob, {
headers: { 'Content-Type': fileItem.file.type },
});
onSuccess(JSON.stringify(response));
} catch (error) {
onError(error);
}
};
function getFileExtension(filename: string): string {
const match = filename.match(/\.([^.]+)$/);
return match ? match[1].toLowerCase() : '';
}
</script>
<style scoped>
/* 添加一些样式 */
</style>

View File

@ -11,8 +11,6 @@ const appStore = useAppStore();
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
console.log({ appStore });
useResponsive(true); useResponsive(true);
const navbarHeight = `72px`; const navbarHeight = `72px`;
const navbar = computed(() => appStore.navbar); const navbar = computed(() => appStore.navbar);
@ -35,8 +33,6 @@ const paddingStyle = computed(() => {
return { ...paddingLeft, ...paddingTop }; return { ...paddingLeft, ...paddingTop };
}); });
console.log('showSidebar', showSidebar);
onMounted(() => { onMounted(() => {
checkHasInviteCode(); checkHasInviteCode();
}); });
@ -96,7 +92,7 @@ provide('toggleDrawerMenu', () => {
</a-drawer> </a-drawer>
<a-layout class="layout-content" :style="paddingStyle"> <a-layout class="layout-content" :style="paddingStyle">
<base-tab-bar v-if="appStore.tabBar" /> <base-tab-bar v-if="appStore.tabBar" />
<a-layout-content class="px-5"> <a-layout-content class="px-5 py-5">
<base-breadcrumb /> <base-breadcrumb />
<layout-page /> <layout-page />
</a-layout-content> </a-layout-content>

View File

@ -1,6 +1,6 @@
/* /*
* @Author: RenXiaoDong * @Author: RenXiaoDong
* @Date: 2025-06-30 16:03:42 * @Date: 2025-06-24 16:29:10
*/ */
import App from './App.vue'; import App from './App.vue';
import router from './router'; import router from './router';
@ -8,12 +8,12 @@ import store from './stores';
import NoData from '@/components/no-data'; import NoData from '@/components/no-data';
import '@/api/index'; import '@/api/index';
import '@arco-design/web-vue/dist/arco.css'; // Arco 默认样式
import './styles'; import './styles';
import './core'; import './core';
import 'uno.css'; import 'uno.css';
import './mock'; import './mock';
import '@/styles/vars.css'; // 优先加载 // import '@/styles/vars.css'; // 优先加载
import '@arco-design/web-vue/dist/arco.css'; // Arco 默认样式
const app = createApp(App); const app = createApp(App);
app.use(store); app.use(store);

View File

@ -1,3 +1,7 @@
/*
* @Author: RenXiaoDong
* @Date: 2025-06-24 16:50:35
*/
export const WHITE_LIST = [ export const WHITE_LIST = [
{ name: 'notFound', children: [] }, { name: 'notFound', children: [] },
{ name: 'login', children: [] }, { name: 'login', children: [] },
@ -18,7 +22,8 @@ export const DEFAULT_ROUTE = {
}; };
export const MENU_GROUP_IDS = { export const MENU_GROUP_IDS = {
DATA_ENGINE_ID: 1, DATA_ENGINE_ID: 1, // 全域数据分析
MANAGEMENT_ID: -1, MANAGEMENT_ID: -1, // 管理中心
WORK_BENCH_ID: -99, PROPERTY_ID: 10, // 资产营销平台
WORK_BENCH_ID: -99, // 工作台
}; };

View File

@ -48,14 +48,9 @@ const router = createRouter({
requiresAuth: false, requiresAuth: false,
}, },
}, },
{ ...appRoutes,
path: '/', REDIRECT_MAIN,
name: '', NOT_FOUND_ROUTE,
children: [...appRoutes, REDIRECT_MAIN, NOT_FOUND_ROUTE],
meta: {
requiresAuth: true,
},
},
], ],
scrollBehavior() { scrollBehavior() {
return { top: 0 }; return { top: 0 };

View File

@ -6,80 +6,88 @@ import { IconBookmark } from '@arco-design/web-vue/es/icon';
import type { AppRouteRecordRaw } from '../types'; import type { AppRouteRecordRaw } from '../types';
import { MENU_GROUP_IDS } from '@/router/constants'; import { MENU_GROUP_IDS } from '@/router/constants';
const COMPONENTS: AppRouteRecordRaw = { const COMPONENTS: AppRouteRecordRaw[] = [
path: 'dataEngine', {
name: 'dataEngine', path: '/dataEngine',
redirect: 'dataEngine/hotTranslation', name: 'DataEngine',
meta: { redirect: 'dataEngine/hotTranslation',
locale: '全域数据引擎', meta: {
icon: IconBookmark, locale: '全域数据引擎',
requiresAuth: true, icon: IconBookmark,
roles: ['*'], requiresAuth: true,
requiresSidebar: true, roles: ['*'],
id: MENU_GROUP_IDS.DATA_ENGINE_ID, requiresSidebar: true,
id: MENU_GROUP_IDS.DATA_ENGINE_ID,
},
children: [
{
path: 'hotTranslation',
name: 'DataEngineHotTranslation',
meta: {
locale: '行业热门话题洞察',
requiresAuth: true,
roles: ['*'],
menuId: 2,
},
component: () => import('@/views/components/dataEngine/hotTranslation.vue'),
},
{
path: 'hotCloud',
name: 'DataEngineHotCloud',
meta: {
locale: '行业词云',
requiresAuth: true,
roles: ['*'],
menuId: 3,
},
component: () => import('@/views/components/dataEngine/hotCloud.vue'),
},
{
path: 'keyWord',
name: 'DataEngineKeyWord',
meta: {
locale: '行业关键词动向',
requiresAuth: true,
roles: ['*'],
menuId: 4,
},
component: () => import('@/views/components/dataEngine/keyWord.vue'),
},
{
path: 'userPainPoints',
name: 'DataEngineUserPainPoints',
meta: {
locale: '用户痛点观察',
requiresAuth: true,
roles: ['*'],
menuId: 5,
},
component: () => import('@/views/components/dataEngine/userPainPoints.vue'),
},
{
path: 'keyBrandMovement',
name: 'DataEngineKeyBrandMovement',
meta: {
locale: '重点品牌动向',
requiresAuth: true,
roles: ['*'],
menuId: 6,
},
component: () => import('@/views/components/dataEngine/keyBrandMovement.vue'),
},
{
path: 'userPersona',
name: 'DataEngineUserPersona',
meta: {
locale: '用户画像',
requiresAuth: true,
roles: ['*'],
menuId: 7,
},
component: () => import('@/views/components/dataEngine/userPersona.vue'),
},
],
}, },
children: [ ];
{
path: 'hotTranslation',
name: '行业热门话题洞察',
meta: {
locale: '行业热门话题洞察',
requiresAuth: true,
roles: ['*'],
},
component: () => import('@/views/components/dataEngine/hotTranslation.vue'),
},
{
path: 'hotCloud',
name: '行业词云',
meta: {
locale: '行业词云',
requiresAuth: true,
roles: ['*'],
},
component: () => import('@/views/components/dataEngine/hotCloud.vue'),
},
{
path: 'keyWord',
name: '行业关键词动向',
meta: {
locale: '行业关键词动向',
requiresAuth: true,
roles: ['*'],
},
component: () => import('@/views/components/dataEngine/keyWord.vue'),
},
{
path: 'userPainPoints',
name: '用户痛点观察',
meta: {
locale: '用户痛点观察',
requiresAuth: true,
roles: ['*'],
},
component: () => import('@/views/components/dataEngine/userPainPoints.vue'),
},
{
path: 'keyBrandMovement',
name: '重点品牌动向',
meta: {
locale: '重点品牌动向',
requiresAuth: true,
roles: ['*'],
},
component: () => import('@/views/components/dataEngine/keyBrandMovement.vue'),
},
{
path: 'userPersona',
name: '用户画像',
meta: {
locale: '用户画像',
requiresAuth: true,
roles: ['*'],
},
component: () => import('@/views/components/dataEngine/userPersona.vue'),
},
],
};
export default COMPONENTS; export default COMPONENTS;

View File

@ -6,50 +6,52 @@ import { IconBookmark } from '@arco-design/web-vue/es/icon';
import type { AppRouteRecordRaw } from '../types'; import type { AppRouteRecordRaw } from '../types';
import { MENU_GROUP_IDS } from '@/router/constants'; import { MENU_GROUP_IDS } from '@/router/constants';
const COMPONENTS: AppRouteRecordRaw = { const COMPONENTS: AppRouteRecordRaw[] = [
path: 'management', {
name: 'management', path: '/management',
redirect: 'management/person', name: 'Management',
meta: { redirect: 'management/person',
locale: '管理中心', meta: {
icon: IconBookmark, locale: '管理中心',
requiresAuth: true, icon: IconBookmark,
roles: ['*'], requiresAuth: true,
requiresSidebar: true, roles: ['*'],
id: MENU_GROUP_IDS.MANAGEMENT_ID, requiresSidebar: true,
id: MENU_GROUP_IDS.MANAGEMENT_ID,
},
children: [
{
path: 'person',
name: 'ManagementPerson',
component: () => import('@/views/components/management/person'),
meta: {
locale: '个人信息',
requiresAuth: true,
roles: ['*'],
},
},
{
path: 'enterprise',
name: 'ManagementEnterprise',
component: () => import('@/views/components/management/enterprise'),
meta: {
locale: '企业信息',
requiresAuth: true,
roles: ['*'],
},
},
{
path: 'account',
name: 'ManagementAccount',
component: () => import('@/views/components/management/account'),
meta: {
locale: '账号管理',
requiresAuth: true,
roles: ['*'],
},
},
],
}, },
children: [ ];
{
path: 'person',
name: '个人信息',
component: () => import('@/views/components/management/person'),
meta: {
locale: '个人信息',
requiresAuth: true,
roles: ['*'],
},
},
{
path: 'enterprise',
name: '企业信息',
component: () => import('@/views/components/management/enterprise'),
meta: {
locale: '企业信息',
requiresAuth: true,
roles: ['*'],
},
},
{
path: 'account',
name: '账号管理',
component: () => import('@/views/components/management/account'),
meta: {
locale: '账号管理',
requiresAuth: true,
roles: ['*'],
},
},
],
};
export default COMPONENTS; export default COMPONENTS;

View File

@ -0,0 +1,194 @@
/**
* 资产营销平台
*/
import type { AppRouteRecordRaw } from '../types';
import { MENU_GROUP_IDS } from '@/router/constants';
import IconRepository from '@/assets/svg/icon-repository.svg';
import IconMediaAccount from '@/assets/svg/icon-mediaAccount.svg';
import IconPutAccount from '@/assets/svg/icon-putAccount.svg';
import IconIntelligentSolution from '@/assets/svg/icon-intelligentSolution.svg';
const COMPONENTS: AppRouteRecordRaw[] = [
{
path: '/repository',
name: 'Repository',
redirect: 'repository/brandMaterials',
meta: {
locale: '品牌资产管理',
icon: IconRepository,
requiresAuth: true,
roles: ['*'],
requiresSidebar: true,
id: MENU_GROUP_IDS.PROPERTY_ID,
},
children: [
{
path: 'brandMaterials',
name: 'RepositoryBrandMaterials',
meta: {
locale: '品牌信息',
requiresAuth: true,
roles: ['*'],
menuId: 11,
},
component: () => import('@/views/property-marketing/brands/brand-materials/index.vue'),
},
],
},
{
path: '/media-account',
name: 'MediaAccount',
redirect: 'media-account/accountManagement',
meta: {
locale: '账号资源中心',
icon: IconMediaAccount,
requiresAuth: true,
roles: ['*'],
requiresSidebar: true,
id: MENU_GROUP_IDS.PROPERTY_ID,
},
children: [
{
path: 'manage',
name: 'MediaAccountAccountManagement',
meta: {
locale: '账号管理',
requiresAuth: true,
roles: ['*'],
menuId: 12,
},
component: () => import('@/views/property-marketing/media-account/account-manage'),
},
{
path: 'dashboard',
name: 'MediaAccountAccountDashboard',
meta: {
locale: '账号数据看板',
requiresAuth: true,
roles: ['*'],
},
component: () => import('@/views/property-marketing/media-account/account-dashboard'),
},
{
path: 'detail/:id',
name: 'MediaAccountAccountDetails',
meta: {
locale: '账号详情',
requiresAuth: true,
roles: ['*'],
hideInMenu: true,
activeMenu: 'MediaAccountAccountDashboard',
},
component: () => import('@/views/property-marketing/media-account/account-detail'),
},
],
},
{
path: '/put-account',
name: 'PutAccount',
redirect: 'put-account/accountManagement',
meta: {
locale: '投放资源中心',
icon: IconPutAccount,
requiresAuth: true,
roles: ['*'],
requiresSidebar: true,
id: MENU_GROUP_IDS.PROPERTY_ID,
},
children: [
{
path: 'manage',
name: 'PutAccountAccountManagement',
meta: {
locale: '账户管理',
requiresAuth: true,
roles: ['*'],
menuId: 13,
},
component: () => import('@/views/property-marketing/put-account/account-manage'),
},
{
path: 'data',
name: 'PutAccountAccountData',
meta: {
locale: '账户数据',
requiresAuth: true,
roles: ['*'],
},
component: () => import('@/views/property-marketing/put-account/account-data'),
},
{
path: 'account-dashboard',
name: 'PutAccountAccountDashboard',
meta: {
locale: '投放表现分析',
requiresAuth: true,
roles: ['*'],
},
component: () => import('@/views/property-marketing/put-account/account-dashboard'),
},
{
path: 'investmentGuidelines',
name: 'PutAccountInvestmentGuidelines',
meta: {
locale: '投放指南',
requiresAuth: true,
roles: ['*'],
},
component: () => import('@/views/property-marketing/put-account/investment-guidelines'),
},
{
path: 'detail/:id',
name: 'guideDetail',
meta: {
locale: '投放指南详情',
requiresAuth: true,
hideInMenu: true,
roles: ['*'],
activeMenu: 'PutAccountInvestmentGuidelines',
},
component: () => import('@/views/property-marketing/put-account/investment-guidelines/detail'),
},
],
},
{
path: '/intelligent-solution',
name: 'IntelligentSolution',
redirect: 'intelligent-solution/businessAnalysisReport',
meta: {
locale: '智能方案管理',
icon: IconIntelligentSolution,
requiresAuth: true,
roles: ['*'],
requiresSidebar: true,
id: MENU_GROUP_IDS.PROPERTY_ID,
},
children: [
{
path: 'businessAnalysisReport',
name: 'IntelligentSolutionBusinessAnalysisReport',
meta: {
locale: '业务洞察报告',
requiresAuth: true,
roles: ['*'],
menuId: 14,
},
component: () => import('@/views/property-marketing/intelligent-solution/businessAnalysisReport'),
},
{
path: 'competitiveProductAnalysisReport',
name: 'IntelligentSolutionCompetitiveProductAnalysisReport',
meta: {
locale: '竟品对比报告',
requiresAuth: true,
roles: ['*'],
},
component: () => import('@/views/property-marketing/intelligent-solution/competitiveProductAnalysisReport'),
},
],
},
];
export default COMPONENTS;

View File

@ -4,7 +4,7 @@ declare module 'vue-router' {
interface RouteMeta { interface RouteMeta {
roles?: string[]; // Controls roles that have access to the page roles?: string[]; // Controls roles that have access to the page
requiresAuth?: boolean; // Whether login is required to access the current page (every route must declare) requiresAuth?: boolean; // Whether login is required to access the current page (every route must declare)
icon?: RouteComponent; // The icon show in the side menu icon?: RouteComponent | string; // The icon show in the side menu
locale?: string; // The locale name show in side menu and breadcrumb locale?: string; // The locale name show in side menu and breadcrumb
needNavigate?: boolean; // if set true, the breadcrumb will support navigate needNavigate?: boolean; // if set true, the breadcrumb will support navigate
hideInMenu?: boolean; // If true, it is not displayed in the side menu hideInMenu?: boolean; // If true, it is not displayed in the side menu

View File

@ -0,0 +1,31 @@
.arco-checkbox {
.arco-checkbox-icon {
width: 16px;
height: 16px;
border-color: #d7d7d9;
}
&.arco-checkbox-disabled {
.arco-checkbox-icon {
background-color: #f2f3f5;
border-color: #d7d7d9;
}
&.arco-checkbox-checked {
.arco-checkbox-icon {
background-color: #A794FE !important;
}
}
&.arco-checkbox-indeterminate {
.arco-checkbox-icon {
border: none;
background-color: #A794FE !important;
}
}
}
&.arco-checkbox-checked,
&.arco-checkbox-indeterminate {
.arco-checkbox-icon {
background-color: #6D4CFE !important;
}
}
}

View File

@ -1 +1,6 @@
@import './table.scss'; @import './input.scss';
@import './table.scss';
@import './checkbox.scss';
@import './pagination.scss';
@import './tabs.scss';
@import './modal.scss';

View File

@ -0,0 +1,5 @@
.arco-input-wrapper {
border-radius: 4px;
border-color: #d7d7d9;
background-color: #fff;
}

View File

@ -0,0 +1,23 @@
.arco-modal {
.arco-modal-header {
border-bottom: 1px solid var(--Border-1, #d7d7d9);
height: 56px;
padding: 0 20px;
.arco-modal-title {
justify-content: flex-start;
}
}
.arco-modal-body {
padding: 24px 20px;
}
.arco-modal-footer {
display: flex;
height: 64px;
padding: 0px 20px;
justify-content: flex-end;
align-items: center;
border-top: 1px solid var(--Border-1, #d7d7d9);
}
}

View File

@ -0,0 +1,41 @@
.arco-pagination {
.arco-pagination-list {
.arco-pagination-item {
border-radius: 4px;
border: 1px solid var(--BG-300, #e6e6e8);
&-ellipsis {
border: none;
}
&-active {
background-color: transparent;
border: 1px solid var(--Brand-Brand-6, #6d4cfe);
}
}
}
.arco-pagination-options {
.arco-select {
background-color: transparent;
border-radius: 4px;
border: 1px solid var(--BG-300, #e6e6e8);
}
}
.arco-pagination-jumper {
&-input {
border-radius: 4px;
border: 1px solid var(--BG-300, #e6e6e8);
}
&-prepend {
color: var(--Text-2, #3c4043);
text-align: right;
font-family: 'PuHuiTi-Medium';
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px;
}
.arco-input-wrapper {
background-color: transparent;
}
}
}

View File

@ -0,0 +1,29 @@
.arco-tabs {
.arco-tabs-nav-tab {
&-list {
.arco-tabs-tab {
.arco-tabs-tab-title {
color: var(--Text-2, #3c4043);
font-family: 'PuHuiTi-Medium';
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 24px;
}
&-active {
.arco-tabs-tab-title {
color: #6d4cfe;
}
}
&:hover {
.arco-tabs-tab-title::before {
display: none;
}
}
}
.arco-tabs-nav-ink {
background-color: #6d4cfe;
}
}
}
}

View File

@ -17,6 +17,10 @@
* { * {
box-sizing: border-box; box-sizing: border-box;
} }
p {
margin: 0;
padding: 0;
}
.flex { .flex {
display: flex; display: flex;

107
src/utils/tools.ts Normal file
View File

@ -0,0 +1,107 @@
/*
* @Author: RenXiaoDong
* @Date: 2025-06-27 17:36:31
*/
import dayjs from 'dayjs';
export function toFixed(num: number | string, n: number): number {
return parseFloat(parseFloat(num.toString()).toFixed(n));
}
export function isNotData(n: number): boolean {
if (n === undefined) {
return true;
}
return n === -2147483648;
}
export function splitNumber(num: number): string | number {
if (!num) {
return num;
}
const parts = num.toString().split('.');
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
return parts.join('.');
}
export function formatNumberShow(...args: any[]): string | number {
const [_args] = args;
const { value, len = 2, split = true, showExactValue = false } = typeof _args === 'object' ? _args : { value: _args };
const getNumber = (value: number) => {
return split ? splitNumber(value) : value;
};
if (isNotData(value)) {
return '-';
}
if (value < 0) {
return `-${formatNumberShow({
value: -value,
len,
split,
showExactValue,
})}`;
}
if (showExactValue) {
return getNumber(toFixed(value, len));
}
if (value < 10000) {
return getNumber(toFixed(value, len));
} else if (value < 100000000) {
const _n = Math.round((value / 10000) * 100) / 100;
return split ? `${splitNumber(_n)}w` : `${toFixed(_n, len)}w`;
} else {
const _n = Math.round((value / 100000000) * 100) / 100;
return split ? `${splitNumber(_n)}亿` : `${toFixed(_n, len)}亿`;
}
}
export function formatTableField(fieldItem: any, rowValue: any, showExactValue = false) {
// 获取嵌套属性值的函数
const getNestedValue = (obj: any, path: string) => {
if (!obj || !path) return undefined;
// 如果路径包含点号,说明是链式取值
if (path.includes('.')) {
return path.split('.').reduce((current, key) => {
return current && current[key] !== undefined ? current[key] : undefined;
}, obj);
}
// 普通属性取值
return obj[path];
};
const _getValue = (value: any) => {
if (!isNumber(value)) return value || '-';
return formatNumberShow({ value, showExactValue });
};
// 使用链式取值获取数据
const rawValue = getNestedValue(rowValue, fieldItem.dataIndex);
const value = _getValue(rawValue ?? '-');
return `${fieldItem.prefix || ''}${value}${fieldItem.suffix || ''}`;
}
export function exactFormatTime(val: number, curYearFmt = 'MM-DD HH:mm', otherYearFmt = 'YYYY-MM-DD HH:mm') {
if (!val) return '-';
const year = dayjs(val * 1000).year();
const currYear = dayjs().year();
const diff = year - currYear;
const fmt = diff === 0 ? curYearFmt : otherYearFmt;
return dayjs(val * 1000).format(fmt);
}
// 导出文件
export function downloadByUrl(url: string, filename?: string) {
const a = document.createElement('a');
a.href = url;
if (filename) {
a.download = filename;
}
a.style.display = 'none';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}

View File

@ -5,7 +5,7 @@
<!-- 关键词热度榜 --> <!-- 关键词热度榜 -->
<a-space <a-space
direction="vertical" direction="vertical"
class="bg-#fff rounded-8px border-1px border-#D7D7D9 border-solid w-100% py-0 px-20px mb-24px" class="bg-#fff rounded-8px border-1px border-#D7D7D9 border-solid w-100% py-0 px-20px mb-20px"
> >
<div class="title-row"> <div class="title-row">
<span class="title mr-4px">关键词热度榜</span> <span class="title mr-4px">关键词热度榜</span>

View File

@ -3,8 +3,8 @@
<!-- 头部 --> <!-- 头部 -->
<a-space <a-space
direction="vertical" direction="vertical"
class="bg-#fff rounded-8px border-1px border-#D7D7D9 border-solid" class="bg-#fff rounded-8px border-1px border-#D7D7D9 border-solid mb-20px"
style="background-color: #fff; width: 100%; padding: 24px; margin: 24px 0; color: #737478; font-size: 14px" style="background-color: #fff; width: 100%; padding: 24px; color: #737478; font-size: 14px"
> >
<a-space align="start" style="width: 100%; align-items: flex-start" class="mb-12px"> <a-space align="start" style="width: 100%; align-items: flex-start" class="mb-12px">
<span style="flex-shrink: 0; line-height: 28px" class="mr-32px">行业大类</span> <span style="flex-shrink: 0; line-height: 28px" class="mr-32px">行业大类</span>

View File

@ -22,7 +22,7 @@
<a-input <a-input
v-model="loginForm.mobile" v-model="loginForm.mobile"
placeholder="输入手机号" placeholder="输入手机号"
class="form-input border border-solid border-#d7d7d9 rounded-8px w-100% h-48px text-14 rounded-4px color-#333 bg-#fff" class="form-input border border-solid border-#d7d7d9 x w-100% h-48px text-14 rounded-4px color-#333 bg-#fff"
clearable clearable
allow-clear allow-clear
@blur="validateField('mobile')" @blur="validateField('mobile')"
@ -31,7 +31,7 @@
</a-form-item> </a-form-item>
<a-form-item field="captcha" hide-label> <a-form-item field="captcha" hide-label>
<div <div
class="form-input border border-solid border-#d7d7d9 rounded-8px w-100% h-48px text-14 rounded-4px color-#333 bg-#fff flex justify-between items-center rounded-8px" class="form-input border border-solid border-#d7d7d9 w-100% h-48px text-14 rounded-4px color-#333 bg-#fff flex justify-between items-center"
> >
<a-input <a-input
v-model="loginForm.captcha" v-model="loginForm.captcha"
@ -239,16 +239,10 @@ const getCode = async () => {
// 先重置验证状态 // 先重置验证状态
formRef.value.clearValidate('mobile'); formRef.value.clearValidate('mobile');
// 验证手机号字段 const result = await formRef.value.validateField('mobile');
try { // 只有当验证通过时才会显示滑块验证
const result = await formRef.value.validateField('mobile'); if (result === true || result === undefined) {
// 只有当验证通过时才会显示滑块验证 isVerificationVisible.value = true;
if (result === true || result === undefined) {
isVerificationVisible.value = true;
}
} catch (error) {
// 验证失败,错误信息会自动显示
console.log('手机号验证失败:', error);
} }
}; };
@ -261,7 +255,6 @@ const handleVerificationSubmit = async () => {
await fetchLoginCaptCha({ mobile: loginForm.mobile }); await fetchLoginCaptCha({ mobile: loginForm.mobile });
AMessage.success('验证码发送成功'); AMessage.success('验证码发送成功');
} catch (error) { } catch (error) {
AMessage.error('验证码发送失败');
// 重置倒计时 // 重置倒计时
countdown.value = 0; countdown.value = 0;
clearInterval(timer.value); clearInterval(timer.value);

View File

@ -1,4 +1,19 @@
.login-wrap { .login-wrap {
.arco-input-wrapper,
.arco-select-view-single,
.arco-textarea-wrapper,
.arco-picker,
.arco-select-view-multiple {
border-radius: 4px;
border-color: #d7d7d9 !important;
background-color: #fff !important;
&:focus-within,
&.arco-input-focus {
background-color: var(--color-bg-2);
// border-color: rgb(var(--primary-6));
box-shadow: 0 0 0 0 var(--color-primary-light-2);
}
}
.login-bg { .login-bg {
position: fixed; position: fixed;
left: 0; left: 0;
@ -38,8 +53,6 @@
} }
} }
.account-bind-container { .account-bind-container {
width: 100%; width: 100%;
max-width: 400px; max-width: 400px;

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="container"> <div class="container">
<div class="flex arco-row-justify-space-between flex-center"> <div class="flex arco-row-justify-space-between">
<img class="avatar" :src="props.product.image" :alt="props.product.name" /> <img class="avatar" :src="props.product.image" :alt="props.product.name" />
<a-tag v-if="props.product.status === Status.Enable" class="status status-enable">已开通</a-tag> <a-tag v-if="props.product.status === Status.Enable" class="status status-enable">已开通</a-tag>
<a-tag v-if="props.product.status === Status.Disable" class="status status-disable">未开通</a-tag> <a-tag v-if="props.product.status === Status.Disable" class="status status-disable">未开通</a-tag>
@ -21,7 +21,7 @@
{{ props.product.desc }} {{ props.product.desc }}
</p> </p>
</div> </div>
<div class="footer flex arco-row-justify-start flex-center"> <div class="footer flex arco-row-justify-start">
<a-button <a-button
v-if="props.product.status === Status.Enable || props.product.status === Status.ON_TRIAL" v-if="props.product.status === Status.Enable || props.product.status === Status.ON_TRIAL"
class="primary-button" class="primary-button"

View File

@ -0,0 +1 @@
## 资产营销平台

View File

@ -0,0 +1,266 @@
<template>
<div class="brand-wrap">
<div class="filter-wrap bg-#fff rounded-8px border-1px border-#D7D7D9 border-solid">
<div class="top flex h-64px px-24px py-10px justify-between items-center">
<p class="text-18px font-400 lh-26px color-#211F24 title">品牌物料</p>
<div class="flex items-center">
<a-button class="add-btn" type="primary" @click="handleAdd">+ 添加品牌</a-button>
</div>
</div>
<div class="container px-24px pt-12px pb-24px">
<div class="filter-row flex mb-20px">
<div class="filter-row-item flex items-center">
<span class="label">品牌名称</span>
<a-space size="medium">
<a-input v-model="query.name" class="w-240px" placeholder="请搜索..." size="medium" allow-clear>
<template #prefix>
<icon-search />
</template>
</a-input>
</a-space>
</div>
<div class="filter-row flex">
<a-button class="w-84px search-btn mr-12px" size="medium" @click="handleSearch">
<template #icon>
<icon-search />
</template>
<template #default>搜索</template>
</a-button>
<a-button class="w-84px reset-btn" size="medium" @click="handleReset">
<template #icon>
<icon-refresh />
</template>
<template #default>重置</template>
</a-button>
</div>
</div>
<a-modal
v-model:visible="modalVisible"
:mask-closable="false"
:esc-to-close="false"
width="510px"
@cancel="handleModalCancel"
>
<template #title>
<span class="modal-title">{{ form.id > 0 ? '编辑品牌' : '添加品牌' }}</span>
</template>
<a-form :model="form" :rules="formRule" ref="formRef" layout="horizontal" auto-label-width>
<a-form-item field="name" label="品牌名称">
<a-input v-model="form.name" class="h-36px" placeholder="请输入..." />
</a-form-item>
<a-form-item field="logo" class="form-item-logo" label="标准版Logo">
<a-space>
<ImageUpload v-model="form.logo" :limit="1"></ImageUpload>
</a-space>
<a-space>
<span class="form-tip">品牌常规展示使用支持PNGJPG格式</span>
</a-space>
</a-form-item>
<a-form-item field="otherLogos" class="form-item-logo" label="其他Logo">
<ImageUpload v-model="form.other_logos" :limit="3"></ImageUpload>
</a-form-item>
<a-form-item field="slogan" label="Slogan">
<a-textarea v-model="form.slogan" placeholder="请输入..." :max-length="50" show-word-limit />
</a-form-item>
</a-form>
<template #footer>
<a-button @click="handleModalCancel">取消</a-button>
<a-button type="primary" @click="handleModalOk">{{ btn_str }}</a-button>
</template>
</a-modal>
</div>
</div>
<div
class="table-wrap bg-#fff rounded-8px border-1px border-#D7D7D9 border-solid px-24px py-24px flex-1 flex flex-col"
>
<a-table :data="tableData" ref="tableRef" :pagination="false">
<template #columns>
<a-table-column title="品牌名称" data-index="name" />
<a-table-column title="品牌logo" data-index="logo">
<template #cell="{ record }">
<img :src="record.logo" style="width: 50px; height: 50px" />
</template>
</a-table-column>
<a-table-column title="Slogan" data-index="slogan" />
<a-table-column width="150" min-widht="150" title="操作" data-index="optional">
<template #cell="{ record }">
<a-space size="medium">
<a-space>
<a-popconfirm
content="确定删除吗?"
type="warning"
ok-text="确认删除"
cancel-text="取消"
@ok="deleteBrand(record.id)"
>
<icon-delete></icon-delete>
</a-popconfirm>
</a-space>
<a-space>
<a-button class="edit-btn" type="outline" @click="handleEdit(record.id)">编辑</a-button>
</a-space>
</a-space>
</template>
</a-table-column>
</template>
</a-table>
<div class="pagination-box">
<a-pagination
:total="pageInfo.total"
size="mini"
show-total
show-jumper
show-page-size
:current="pageInfo.page"
:page-size="pageInfo.pageSize"
@change="onPageChange"
@page-size-change="onPageSizeChange"
/>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, reactive, onMounted } from 'vue';
import { Message } from '@arco-design/web-vue';
import { IconDelete } from '@arco-design/web-vue/es/icon';
import {
addMaterials,
deleteMaterials,
getMaterialsList,
getMaterialsDetail,
updateMaterials,
} from '@/api/all/enterpriseKnowledge';
import ImageUpload from '@/components/upload/ImageUpload.vue';
const tableRef = ref(null);
const tableData = ref([]);
const pageInfo = reactive({
page: 1,
pageSize: 20,
total: 0,
});
const query = reactive({
page: ref(1),
name: ref(''),
page_size: ref('10'),
});
const modalVisible = ref(false);
const formRef = ref();
const formRule = {
name: [{ required: true, message: '请输入品牌名称', trigger: ['blur', 'change'] }],
logo: [{ required: true, message: '请上传品牌logo', trigger: ['blur', 'change'] }],
};
const form = reactive({
name: '',
logo: '',
id: 0,
other_logos: [], // [{ url: '' }]
slogan: '',
});
const btn_str = ref('确认添加');
onMounted(() => {
handleSearch();
});
const handleSearch = () => {
query.page = pageInfo.page;
query.page_size = pageInfo.pageSize;
getMaterialsList(query).then((response) => {
tableData.value = response.data.data;
pageInfo.total = response.data.total;
console.log(tableData.value, 'tableData.value');
});
};
const reload = () => {
pageInfo.page = 1;
handleSearch();
};
const handleReset = () => {
pageInfo.page = 1;
pageInfo.pageSize = 20;
pageInfo.total = 0;
query.name = '';
reload();
};
const onPageChange = (current) => {
pageInfo.page = current;
handleSearch();
};
const onPageSizeChange = (pageSize) => {
pageInfo.pageSize = pageSize;
reload();
};
const deleteBrand = (id) => {
console.log(id, 'id');
deleteMaterials(id).then(() => {
Message.success('删除成功');
handleSearch();
});
};
function handleAdd() {
btn_str.value = '确认添加';
form.id = 0;
form.logo = '';
form.name = '';
form.slogan = '';
form.other_logos = [];
modalVisible.value = true;
}
function handleModalCancel() {
modalVisible.value = false;
}
function handleModalOk() {
formRef.value
.validate()
.then((valid) => {
if (!valid) {
if (form.id) {
updateMaterials(form.id, form).then(() => {
Message.success('修改成功');
handleSearch();
});
} else {
addMaterials(form).then((response) => {
Message.success('新增成功');
handleSearch();
});
}
modalVisible.value = false;
}
})
.catch((error) => {
console.error('验证失败:', error);
Message.error('请检查表单填写是否正确');
});
}
function handleEdit(id) {
modalVisible.value = true;
btn_str.value = '确认';
getMaterialsDetail(id).then((response) => {
Object.assign(form, response.data);
});
}
</script>
<style scoped lang="scss">
@import './style.scss';
</style>

View File

@ -0,0 +1,82 @@
.brand-wrap {
height: 100%;
display: flex;
flex-direction: column;
:deep(.search-btn) {
border-radius: 4px;
border: 1px solid var(--Brand-Brand-6, #6d4cfe);
color: #6d4cfe;
}
:deep(.edit-btn) {
border: 1px solid var(--Brand-Brand-6, #6d4cfe);
color: #6d4cfe;
}
:deep(.reset-btn) {
border-radius: 4px;
border: 1px solid var(--BG-500, #b1b2b5);
background: var(--BG-white, #fff);
}
.table-wrap {
width: 100%;
.pagination-box {
display: flex;
width: 100%;
padding: 16px 24px;
justify-content: flex-end;
align-items: center;
}
margin-top: 20px;
}
.container {
.filter-row {
.filter-row-item {
&:not(:last-child) {
margin-right: 24px;
}
.label {
margin-right: 8px;
color: #211f24;
font-family: 'PuHuiTi-Regular';
font-size: 14px;
font-style: normal;
font-weight: 400;
flex-shrink: 0;
line-height: 22px; /* 157.143% */
}
}
}
}
}
:deep(.arco-input-wrapper),
:deep(.arco-select-view-single),
:deep(.arco-textarea-wrapper),
:deep(.arco-select-view-multiple) {
border-radius: 4px;
border-color: #d7d7d9;
background-color: #fff;
&:focus-within,
&.arco-input-focus {
background-color: var(--color-bg-2);
border-color: rgb(var(--primary-6));
box-shadow: 0 0 0 0 var(--color-primary-light-2);
}
}
.form-tip {
font-size: 12px;
color: #999;
margin-top: 4px;
}

View File

@ -0,0 +1,44 @@
.arco-input-wrapper,
.arco-select-view-single,
.arco-textarea-wrapper,
.arco-picker,
.arco-select-view-multiple {
border-radius: 4px;
border-color: #d7d7d9;
background-color: #fff;
&:focus-within,
&.arco-input-focus {
background-color: var(--color-bg-2);
border-color: rgb(var(--primary-6));
box-shadow: 0 0 0 0 var(--color-primary-light-2);
}
}
.arco-modal {
.cancel-btn {
border-radius: 4px;
border: 1px solid var(--BG-500, #b1b2b5);
background: var(--BG-white, #fff);
&:hover {
border: 1px solid var(--BG-500, #b1b2b5);
}
}
.arco-modal-body {
.arco-form-item {
margin-bottom: 16px;
&:last-child {
margin-bottom: 0;
}
.arco-form-item-label {
color: var(--Text-1, #211f24);
font-family: 'PuHuiTi-Regular';
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px; /* 157.143% */
}
}
}
}

View File

@ -0,0 +1,212 @@
<template>
<div class="business-wrap">
<div class="filter-wrap bg-#fff rounded-8px border-1px border-#D7D7D9 border-solid">
<div class="top flex h-64px px-24px py-10px justify-between items-center">
<p class="text-18px font-400 lh-26px color-#211F24 title">业务洞察报告</p>
</div>
<div class="container px-24px pt-12px pb-24px">
<div class="filter-row flex mb-20px">
<div class="filter-row-item flex items-center">
<span class="label">服务/产品</span>
<a-space size="medium">
<a-input v-model="query.name" class="w-240px" placeholder="请搜索..." size="medium" allow-clear>
<template #prefix>
<icon-search />
</template>
</a-input>
</a-space>
</div>
<div class="filter-row-item flex items-center">
<span class="label">时间筛选</span>
<a-space class="w-240px">
<a-range-picker size="medium" allow-clear format="YYYY-MM-DD HH:mm" class="w-100%" />
</a-space>
</div>
<div class="filter-row flex">
<a-button class="w-84px search-btn mr-12px" size="medium">
<template #icon>
<icon-search />
</template>
<template #default>搜索</template>
</a-button>
<a-button class="w-84px reset-btn" size="medium">
<template #icon>
<icon-refresh />
</template>
<template #default>重置</template>
</a-button>
</div>
</div>
</div>
</div>
<div
class="table-wrap bg-#fff rounded-8px border-1px border-#D7D7D9 border-solid px-24px py-24px flex-1 flex flex-col"
>
<a-table :columns="columns" :data="tableData" @change="handleChange" :pagination="false">
</a-table>
<div class="pagination-box">
<a-pagination
:total="pageInfo.total"
size="mini"
show-total
show-jumper
show-page-size
:current="pageInfo.page"
:page-size="pageInfo.pageSize"
@change="onPageChange"
@page-size-change="onPageSizeChange"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue';
const pageInfo = reactive({
page: 1,
pageSize: 20,
total: 0,
});
const query = reactive({
name: ref(''),
});
const listResult = reactive({
data: [],
total: 0,
});
const onPageSizeChange = () => {};
const tableData = ref([]);
const handleChange = () => {};
const onPageChange = () => {};
const columns = [
{
title: '服务/产品',
dataIndex: 'service_name',
slotName: 'rank',
width: 60,
minWidth: 60,
},
{
title: '生成日期',
dataIndex: 'create_time',
width: 120,
minWidth: 120,
},
{
titleSlotName: 'hotTitle',
width: 180,
minWidth: 180,
title: '目标客群',
dataIndex: 'customer',
slotName: 'hot',
},
{
title: '价格区间',
dataIndex: 'price',
width: 120,
minWidth: 120,
},
{
titleSlotName: 'lasterUpdateTime',
title: '最后更新日期',
dataIndex: 'volumeRate',
width: 180,
minWidth: 180,
slotName: 'lasterUpdateTime',
},
{
title: '操作人',
dataIndex: 'operator',
width: 120,
minWidth: 120,
},
{
title: '操作',
dataIndex: 'operator',
width: 120,
minWidth: 120,
},
];
</script>
<style scoped lang="scss">
.business-wrap {
height: 100%;
display: flex;
flex-direction: column;
:deep(.search-btn) {
border-radius: 4px;
border: 1px solid var(--Brand-Brand-6, #6d4cfe);
color: #6d4cfe;
}
:deep(.edit-btn) {
border: 1px solid var(--Brand-Brand-6, #6d4cfe);
color: #6d4cfe;
}
:deep(.reset-btn) {
border-radius: 4px;
border: 1px solid var(--BG-500, #b1b2b5);
background: var(--BG-white, #fff);
}
:deep(.arco-input-wrapper),
:deep(.arco-select-view-single),
:deep(.arco-textarea-wrapper),
:deep(.arco-picker),
:deep(.arco-select-view-multiple) {
border-radius: 4px;
border-color: #d7d7d9;
background-color: #fff;
&:focus-within,
&.arco-input-focus {
background-color: var(--color-bg-2);
border-color: rgb(var(--primary-6));
box-shadow: 0 0 0 0 var(--color-primary-light-2);
}
}
.table-wrap {
width: 100%;
.pagination-box {
display: flex;
width: 100%;
padding: 16px 24px;
justify-content: flex-end;
align-items: center;
}
margin-top: 20px;
}
.container {
.filter-row {
.filter-row-item {
&:not(:last-child) {
margin-right: 24px;
}
.label {
margin-right: 8px;
color: #211f24;
font-family: 'PuHuiTi-Medium';
font-size: 14px;
font-style: normal;
font-weight: 400;
flex-shrink: 0;
line-height: 22px; /* 157.143% */
}
}
}
}
}
</style>

View File

@ -0,0 +1,204 @@
<template>
<div class="competitive-wrap">
<div class="filter-wrap bg-#fff rounded-8px border-1px border-#D7D7D9 border-solid">
<div class="top flex h-64px px-24px py-10px justify-between items-center">
<p class="text-18px font-400 lh-26px color-#211F24 title">竞品对比报告</p>
</div>
<div class="container px-24px pt-12px pb-24px">
<div class="filter-row flex mb-20px">
<div class="filter-row-item flex items-center">
<span class="label">服务/产品</span>
<a-space size="medium">
<a-input v-model="query.name" class="w-240px" placeholder="请搜索..." size="medium" allow-clear>
<template #prefix>
<icon-search />
</template>
</a-input>
</a-space>
</div>
<div class="filter-row-item flex items-center">
<span class="label">时间筛选</span>
<a-space class="w-240px">
<a-range-picker size="medium" allow-clear format="YYYY-MM-DD HH:mm" class="w-100%" />
</a-space>
</div>
<div class="filter-row flex">
<a-button class="w-84px search-btn mr-12px" size="medium">
<template #icon>
<icon-search />
</template>
<template #default>搜索</template>
</a-button>
<a-button class="w-84px reset-btn" size="medium">
<template #icon>
<icon-refresh />
</template>
<template #default>重置</template>
</a-button>
</div>
</div>
</div>
</div>
<div
class="table-wrap bg-#fff rounded-8px border-1px border-#D7D7D9 border-solid px-24px py-24px flex-1 flex flex-col"
>
<a-table :columns="columns" :data="tableData" @change="handleChange" :pagination="false">
</a-table>
<div class="pagination-box">
<a-pagination
:total="pageInfo.total"
size="mini"
show-total
show-jumper
show-page-size
:current="pageInfo.page"
:page-size="pageInfo.pageSize"
@change="onPageChange"
@page-size-change="onPageSizeChange"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue';
const pageInfo = reactive({
page: 1,
pageSize: 20,
total: 0,
});
const query = reactive({
name: ref(''),
});
const handleChange = () => {};
const onPageChange = () => {};
const onPageSizeChange = () => {};
const tableData = ref([]);
const columns = [
{
title: '服务/产品',
dataIndex: 'service_name',
slotName: 'rank',
width: 60,
minWidth: 60,
},
{
title: '生成日期',
dataIndex: 'create_time',
width: 120,
minWidth: 120,
},
{
titleSlotName: 'customer',
width: 180,
minWidth: 180,
title: '竞争对手',
dataIndex: 'customer',
slotName: 'hot',
},
{
titleSlotName: 'lasterUpdateTime',
title: '最后更新日期',
dataIndex: 'volumeRate',
width: 180,
minWidth: 180,
slotName: 'lasterUpdateTime',
},
{
title: '操作人',
dataIndex: 'operator',
width: 120,
minWidth: 120,
},
{
title: '操作',
dataIndex: 'operator',
width: 120,
minWidth: 120,
},
];
</script>
<style scoped lang="scss">
.competitive-wrap {
height: 100%;
display: flex;
flex-direction: column;
:deep(.search-btn) {
border-radius: 4px;
border: 1px solid var(--Brand-Brand-6, #6d4cfe);
color: #6d4cfe;
}
:deep(.edit-btn) {
border: 1px solid var(--Brand-Brand-6, #6d4cfe);
color: #6d4cfe;
}
:deep(.reset-btn) {
border-radius: 4px;
border: 1px solid var(--BG-500, #b1b2b5);
background: var(--BG-white, #fff);
}
:deep(.arco-input-wrapper),
:deep(.arco-select-view-single),
:deep(.arco-textarea-wrapper),
:deep(.arco-picker),
:deep(.arco-select-view-multiple) {
border-radius: 4px;
border-color: #d7d7d9;
background-color: #fff;
&:focus-within,
&.arco-input-focus {
background-color: var(--color-bg-2);
border-color: rgb(var(--primary-6));
box-shadow: 0 0 0 0 var(--color-primary-light-2);
}
}
.table-wrap {
width: 100%;
.pagination-box {
display: flex;
width: 100%;
padding: 16px 24px;
justify-content: flex-end;
align-items: center;
}
margin-top: 20px;
}
.container {
.filter-row {
.filter-row-item {
&:not(:last-child) {
margin-right: 24px;
}
.label {
margin-right: 8px;
color: #211f24;
font-family: 'PuHuiTi-Medium';
font-size: 14px;
font-style: normal;
font-weight: 400;
flex-shrink: 0;
line-height: 22px; /* 157.143% */
}
}
}
}
}
</style>

View File

@ -0,0 +1,297 @@
<!--
* @Author: RenXiaoDong
* @Date: 2025-06-27 18:08:04
-->
<template>
<div class="action-row mb-12px flex justify-between">
<div>
<a-checkbox
v-if="dataSource.length > 0"
:model-value="checkedAll"
:indeterminate="indeterminate"
class="!pl-13px"
@change="handleSelectAll"
>全选</a-checkbox
>
</div>
<div class="flex items-center">
<a-button class="w-110px search-btn mr-12px" size="medium" @click="handleExport">
<template #icon> <icon-download /> </template>
<template #default>导出数据</template>
</a-button>
<a-button class="w-110px search-btn" size="medium" @click="openCustomColumn">
<template #icon>
<img :src="icon1" width="14" height="14" />
</template>
<template #default>自定义列</template>
</a-button>
</div>
</div>
<a-table
ref="tableRef"
:data="dataSource"
row-key="id"
column-resizable
:row-selection="{
type: 'checkbox',
showCheckedAll: true,
width: 48,
}"
:selected-keys="selectedItems"
:pagination="false"
:scroll="{ x: '100%' }"
class="account-table w-100%"
bordered
@sorter-change="handleSorterChange"
@select="handleSelect"
@select-all="handleSelectAll"
>
<template #empty>
<NoData />
</template>
<template #columns>
<a-table-column
v-for="column in tableColumns"
:key="column.dataIndex"
:data-index="column.dataIndex"
:fixed="column.fixed"
:width="column.width"
:min-width="column.minWidth"
:sortable="column.sortable"
:align="column.align"
ellipsis
tooltip
>
<template #title>
<div class="flex items-center">
<img v-if="column.dataIndex === 'ai_evaluate'" width="16" height="16" :src="icon5" class="mr-4px" />
<span class="cts mr-4px">{{ column.title }}</span>
<a-tooltip v-if="column.tooltip" :content="column.tooltip" position="top">
<icon-question-circle class="tooltip-icon color-#737478" size="16" />
</a-tooltip>
</div>
</template>
<template v-if="column.dataIndex === 'platform'" #cell="{ record }">
{{ record.platform === 0 ? '抖音' : record.platform === 1 ? '小红书' : '-' }}
</template>
<template v-else-if="column.dataIndex === 'status'" #cell="{ record }">
<div class="status-tag" :class="`status-tag-${record.status}`">
<span class="cts status-tag-text">{{
STATUS_LIST.find((item) => item.value === record.status)?.label
}}</span>
</div>
</template>
<template v-else-if="column.dataIndex === 'ai_evaluate'" #cell="{ record }">
<div class="ai-evaluation-row flex">
<template v-if="record.ai_evaluate">
<img
width="16"
height="16"
:src="record.ai_evaluate?.status === 0 ? icon2 : record.ai_evaluate?.status === 1 ? icon3 : icon4"
class="mr-8px icon"
/>
<div>
<p class="cts">{{ `${record.ai_evaluate?.level} | ${record.ai_evaluate?.advise}` }}</p>
<p class="cts text-12px lh-20px !color-#939499">
{{
`观看: ${record[`${getPropPrefix(dateType)}view_rate`]}% 点赞: ${
record[`${getPropPrefix(dateType)}like_rate`]
}%`
}}
</p>
</div>
</template>
<template v-else>
<p class="cts">-</p>
</template>
</div>
</template>
<template v-else-if="column.dataIndex === 'like_collect_number'" #cell="{ record }">
{{
formatNumberShow({
value: `${record[`${getPropPrefix(dateType)}like_number`] ?? 0} + ${
record[`${getPropPrefix(dateType)}collect_number`] ?? 0
}`,
showExactValue: true,
})
}}
</template>
<template v-else-if="column.dataIndex === 'operation'" #cell="{ record }">
<a-button type="outline" size="small" class="search-btn" @click="handleDetail(record)">详情</a-button>
</template>
<template v-else-if="column.isRateField" #cell="{ record }">
<div class="flex items-center rate-row justify-end" :class="record[column.dataIndex] > 0 ? 'up' : 'down'">
<icon-arrow-up v-if="record[column.dataIndex] > 0" size="16" />
<icon-arrow-down v-else size="16" />
{{ formatTableField(column, record) }}
</div>
</template>
<template v-else-if="column.dataIndex === 'newest_work_title'" #cell="{ record }">
<p class="cts cursor-pointer hover:!color-#6D4CFE">{{ record.newest_work_title }}</p>
<p class="cts text-12px lh-20px !color-#939499">
{{ exactFormatTime(record.newest_work_published_at) }}
</p>
</template>
<template v-else-if="column.dataIndex === 'second_new_work_title'" #cell="{ record }">
<p class="cts cursor-pointer hover:!color-#6D4CFE">{{ record.second_new_work_title }}</p>
<p class="cts text-12px lh-20px !color-#939499">
{{ exactFormatTime(record.second_new_work_published_at) }}
</p>
</template>
<template v-else #cell="{ record }">
{{ formatTableField(column, record, true) }}
</template>
</a-table-column>
<a-table-column data-index="operation" fixed="right" width="100" title="操作">
<template #cell="{ record }">
<a-button type="outline" size="small" class="search-btn" @click="handleDetail(record)">详情</a-button>
</template>
</a-table-column>
</template>
</a-table>
<CustomTableColumnModal
ref="customTableColumnModalRef"
:type="CUSTOM_COLUMN_TYPE"
:dateType="dateType"
@success="onCustomColumnSuccess"
/>
</template>
<script setup>
import { ref, computed } from 'vue';
import { useRouter } from 'vue-router';
import { getCustomColumns } from '@/api/all/common';
import { STATUS_LIST } from '@/views/property-marketing/media-account/components/status-select/constants';
import { formatTableField, formatNumberShow, exactFormatTime } from '@/utils/tools';
import { getDefaultColumns, getPropPrefix } from '@/views/property-marketing/media-account/account-dashboard/constants';
import CustomTableColumnModal from '../custom-column-modal';
import icon1 from '@/assets/img/media-account/icon-custom.png';
import icon2 from '@/assets/img/media-account/icon-warn.png';
import icon3 from '@/assets/img/media-account/icon-warn-1.png';
import icon4 from '@/assets/img/media-account/icon-success.png';
import icon5 from '@/assets/img/media-account/icon5.png';
const props = defineProps({
dataSource: {
type: Array,
default: () => [],
},
query: {
type: Object,
default: () => {},
},
});
const emit = defineEmits(['selectionChange', 'sorterChange', 'export']);
const CUSTOM_COLUMN_TYPE = 'media_account';
const router = useRouter();
const selectedItems = ref([]);
const tableRef = ref(null);
const customTableColumnModalRef = ref(null);
const selectedColumns = ref([]);
const checkedAll = computed(
() => selectedItems.value.length > 0 && selectedItems.value.length === props.dataSource.length,
);
const indeterminate = computed(
() => selectedItems.value.length > 0 && selectedItems.value.length < props.dataSource.length,
);
const dateType = computed(() => (props.query.type === 7 ? 'week' : 'month'));
const tableColumns = computed(() => {
const _result = [];
const _columns = getDefaultColumns(dateType.value);
selectedColumns.value.forEach((item) => {
const _column = _columns.find((_item) => _item.prop === item);
if (_column) {
_result.push(_column);
}
});
return _result;
});
const handleSelectAll = (checked) => {
if (checked) {
selectedItems.value = props.dataSource.map((item) => item.id);
} else {
selectedItems.value = [];
}
emit('selectionChange', checked ? selectedItems.value : []);
};
const handleDetail = (record) => {
router.push(`/media-account/detail/${record.id}?type=${dateType.value}`);
};
// 处理排序变化
const handleSorterChange = (column, order) => {
console.log(column, order);
emit('sorterChange', column, order === 'ascend' ? 'asc' : 'desc');
};
const handleSelect = (selectedRowKeys, selectedRows) => {
selectedItems.value = selectedRowKeys;
emit('selectionChange', selectedRows);
};
const handleExport = () => {
emit('export');
};
const resetTable = () => {
selectedItems.value = [];
};
const openCustomColumn = () => {
customTableColumnModalRef.value.open();
};
const onCustomColumnSuccess = (columns) => {
selectedColumns.value = columns;
};
const getSelectedColumns = () => {
getCustomColumns({ type: CUSTOM_COLUMN_TYPE }).then((res) => {
const { code, data } = res;
if (code === 200) {
const { selected_columns, groups } = data;
setDefaultCheckColumns(groups, selected_columns);
}
});
};
const setDefaultCheckColumns = (groups, selected_columns) => {
const requiredGroups = groups.filter((group) => group.is_require === 1);
const requiredValues = requiredGroups
.flatMap((group) => (group.columns || []).filter((col) => col.is_require === 1))
.map((col) => col.value);
const merged = union(requiredValues, selected_columns);
selectedColumns.value = merged;
};
defineExpose({
resetTable,
});
onMounted(() => {
getSelectedColumns();
});
</script>
<style scoped lang="scss">
@import './style.scss';
</style>

View File

@ -0,0 +1,70 @@
.action-row {
:deep(.arco-btn) {
.arco-btn-icon {
line-height: 14px;
}
}
}
.account-table {
.cts {
color: var(--Text-1, #211f24);
font-family: 'PuHuiTi-Medium';
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px;
}
.status-tag {
width: fit-content;
display: flex;
height: 28px;
padding: 0px 8px;
align-items: center;
border-radius: 2px;
background: #f2f3f5;
.status-tag-text {
color: var(--BG-700, #737478);
}
&-1 {
background: #ebf7f2;
.status-tag-text {
color: #25c883;
}
}
&-2,
&-4,
&-5,
&-6,
&-7 {
background: #ffe7e4;
.status-tag-text {
color: #f64b31;
}
}
&-3 {
background: #fff7e5;
color: #ffae00;
.status-tag-text {
color: #ffae00;
}
}
}
.ai-evaluation-row {
.icon {
position: relative;
top: 1px;
}
}
.rate-row {
&.up {
color: var(--Functional-Red-6, #f64b31);
}
&.down {
color: var(--Functional-Green-6, #25c883);
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 677 B

View File

@ -0,0 +1,223 @@
<!--
* @Author: RenXiaoDong
* @Date: 2025-06-30 10:54:49
-->
<template>
<a-modal
v-model:visible="visible"
title="自定义列"
width="960px"
unmountOnClose
titleAlign="start"
class="custom-table-column-modal-98"
@close="close"
>
<div class="modal-body">
<!-- 左侧分组 -->
<div class="left">
<div v-for="group in dataSource" :key="group.label" class="group-item">
<div class="title-row">
<span class="text">{{ group.label }}</span>
</div>
<div class="fields">
<a-checkbox
v-for="option in group.columns"
:key="option.value"
:model-value="isCheck(option)"
:value="option.value"
:disabled="option.is_require === ENUM_STATUS.NO"
@change="(checked) => onCheckChange(checked, option)"
>
{{ localFields.find((item) => item.prop === option.value)?.title }}
</a-checkbox>
</div>
</div>
</div>
<!-- 右侧已选 -->
<div class="right">
<span class="checked-title mb-16px">
已添加<span class="count">{{ checkColumns.length }}</span>
</span>
<div class="checked-list">
<div v-for="(groupName, index) in requiredGroupNames" :key="index" class="checked-item !cursor-default">
<div class="flex items-center">
<img :src="icon1" alt="icon" class="mr-8px" width="16" height="16" />
<span>{{ groupName }}</span>
</div>
</div>
<VueDraggable v-model="checkColumns">
<div
v-for="item in checkColumns"
:key="item"
class="checked-item justify-between"
:class="isRequiredColumn(item) ? '!display-none' : ''"
>
<div class="flex items-center">
<icon-menu size="16" class="mr-8px" />
<span>{{ getCheckColumnLabel(item) }}</span>
</div>
<icon-close size="16" class="color-#737478 cursor-pointer" @click="removeCheckedField(item)" />
</div>
</VueDraggable>
</div>
</div>
</div>
<template #footer>
<div style="text-align: right">
<a-button class="mr-8px cancel-btn" size="medium" @click="close">取消</a-button>
<a-button type="primary" size="medium" @click="onSubmit">确定</a-button>
</div>
</template>
</a-modal>
</template>
<script setup>
import { ref, defineExpose } from 'vue';
import { VueDraggable } from 'vue-draggable-plus';
import { getCustomColumns, updateCustomColumns } from '@/api/all/common';
import { getPropPrefix, getDefaultColumns } from '@/views/property-marketing/media-account/account-dashboard/constants';
import icon1 from './img/icon-lock.png';
const props = defineProps({
type: {
type: String,
default: '',
required: true,
},
dateType: {
type: String,
default: 'week',
},
});
const emit = defineEmits(['success']);
const ENUM_STATUS = {
YES: 0,
NO: 1,
};
const visible = ref(false);
const dataSource = ref([]);
const checkColumns = ref([]); // 选中字段
const allColumns = ref([]); // 所有字段
const requiredGroupNames = ref([]); // 必选分组名称
const localFields = ref([]);
const open = () => {
initData();
localFields.value = getDefaultColumns(props.dateType);
visible.value = true;
};
const close = () => {
visible.value = false;
dataSource.value = [];
checkColumns.value = [];
allColumns.value = [];
localFields.value = [];
requiredGroupNames.value = [];
};
const initData = async () => {
const { code, data } = await getCustomColumns({ type: props.type });
if (code === 200) {
const { selected_columns, groups } = data;
dataSource.value = groups;
formatDataSource();
setDefaultCheckColumns(groups, selected_columns);
allColumns.value = groups.flatMap((group) => group.columns);
}
};
// 过滤近7天/近30天字段
const formatDataSource = () => {
const prefix = getPropPrefix(props.dateType);
dataSource.value.forEach((group) => {
group.columns = group.columns.filter((item) => {
if (item.value.startsWith('seven_') || item.value.startsWith('thirty_')) {
return item.value.startsWith(prefix);
}
return true;
});
});
};
const isCheck = (option) => {
return checkColumns.value.includes(option.value);
};
const getCheckColumnLabel = (value) => {
const column = localFields.value.find((column) => column.prop === value);
return column?.title;
};
const isRequiredColumn = (value) => {
const column = allColumns.value.find((column) => column.value === value);
return column?.is_require === 1;
};
const removeCheckedField = (value) => {
checkColumns.value = checkColumns.value.filter((column) => column !== value);
};
// 勾选/取消
const onCheckChange = (checked, option) => {
if (checked) {
checkColumns.value.push(option.value);
} else {
checkColumns.value = checkColumns.value.filter((item) => item !== option.value);
}
};
// 提交
const onSubmit = async () => {
const result = checkColumns.value
.map((item) => {
if (item.startsWith('thirty_')) {
return [item, item.replace('thirty_', 'seven_')];
} else if (item.startsWith('seven_')) {
return [item, item.replace('seven_', 'thirty_')];
} else {
return item;
}
})
.flat();
const { code } = await updateCustomColumns({ type: props.type, selected_columns: result });
if (code === 200) {
emit('success', result);
close();
}
};
const setDefaultCheckColumns = (groups, selected_columns) => {
const prefix = getPropPrefix(props.dateType);
const requiredGroups = groups.filter((group) => group.is_require === 1);
const requiredValues = requiredGroups
.flatMap((group) => (group.columns || []).filter((col) => col.is_require === 1))
.map((col) => col.value);
const filteredSelected = selected_columns.filter(
(col) => (!col.startsWith('seven_') && !col.startsWith('thirty_')) || col.startsWith(prefix),
);
const merged = union(requiredValues, filteredSelected);
checkColumns.value = merged;
requiredGroupNames.value = requiredGroups.map((group) => group.label);
};
// 暴露方法
defineExpose({ open });
</script>
<style lang="scss">
@import './style.scss';
</style>

View File

@ -0,0 +1,99 @@
.custom-table-column-modal-98 {
.arco-modal-body {
.modal-body {
height: 504px;
border-radius: 8px;
border: 1px solid var(--BG-300, #e6e6e8);
display: flex;
flex-direction: row;
.left {
flex: 1;
padding: 20px;
border-right: 1px solid #eee;
overflow-y: auto;
.group-item {
.title-row {
border-radius: 4px;
background: var(--BG-100, #f7f8fa);
display: flex;
height: 44px;
padding: 0px 12px;
align-items: center;
margin-bottom: 16px;
.text {
color: var(--Text-1, #211f24);
font-family: 'PuHuiTi-Medium';
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 24px; /* 150% */
}
}
.fields {
width: 100%;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
&:not(:last-child) {
margin-bottom: 24px;
}
}
}
.right {
width: 280px;
padding: 16px 12px;
display: flex;
flex-direction: column;
.checked-title {
color: var(--Text-1, #211f24);
font-family: 'PuHuiTi-Medium';
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 24px;
.count {
font-weight: 500;
}
}
.checked-list {
flex: 1;
overflow-y: auto;
.checked-item {
cursor: move;
border-radius: 4px;
background: var(--BG-100, #f7f8fa);
display: flex;
height: 32px;
padding: 0px 12px;
align-items: center;
.text {
color: var(--Text-1, #211f24);
font-family: 'PuHuiTi-Medium';
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px; /* 157.143% */
}
&:not(:last-child) {
margin-bottom: 12px;
}
}
.draggable-list {
min-height: 40px;
}
}
}
}
}
.arco-modal-footer {
.cancel-btn {
border-radius: 4px;
border: 1px solid var(--BG-500, #b1b2b5);
background: var(--BG-white, #fff);
&:hover {
border: 1px solid var(--BG-500, #b1b2b5);
}
}
}
}

View File

@ -0,0 +1,117 @@
<!-- eslint-disable vue/no-mutating-props -->
<!--
* @Author: RenXiaoDong
* @Date: 2025-06-25 14:02:40
-->
<template>
<div class="container">
<div class="filter-row flex mb-20px">
<div class="filter-row-item flex items-center">
<span class="label">账号名称</span>
<a-space size="medium" class="w-240px">
<a-input v-model="query.name" placeholder="请搜索..." size="medium" allow-clear @change="handleSearch">
<template #prefix>
<icon-search />
</template>
</a-input>
</a-space>
</div>
<div class="filter-row-item flex items-center">
<span class="label">分组</span>
<a-space class="w-200px">
<GroupSelect v-model="query.group_ids" multiple :options="groups" @change="handleSearch" />
</a-space>
</div>
<div class="filter-row-item flex items-center">
<span class="label">状态</span>
<a-space class="w-180px">
<StatusSelect v-model="query.status" @change="handleSearch" />
</a-space>
</div>
<div class="filter-row-item flex items-center">
<span class="label">运营人员</span>
<a-space class="w-160px">
<OperatorSelect v-model="query.operator_id" :options="operators" @change="handleSearch" />
</a-space>
</div>
</div>
<div class="filter-row flex">
<div class="filter-row-item flex items-center">
<span class="label">时间筛选</span>
<a-space class="w-240px">
<a-select v-model="query.type" size="medium" placeholder="全部" class="w-120px" @change="handleSearch">
<template #arrow-icon> <icon-calendar size="16" /> </template>
<a-option :value="7" label="近7天">近7天</a-option>
<!-- <a-option :value="14" label="近14天">近14天</a-option> -->
<a-option :value="30" label="近30天">近30天</a-option>
</a-select>
</a-space>
</div>
<a-button class="w-84px search-btn mr-12px" size="medium" @click="handleSearch">
<template #icon>
<icon-search />
</template>
<template #default>搜索</template>
</a-button>
<a-button class="w-84px reset-btn" size="medium" @click="handleReset">
<template #icon>
<icon-refresh />
</template>
<template #default>重置</template>
</a-button>
</div>
</div>
</template>
<script setup>
import { reactive, defineEmits, defineProps } from 'vue';
import { fetchAccountGroups, fetchAccountOperators } from '@/api/all/propertyMarketing';
import GroupSelect from '@/views/property-marketing/media-account/components/group-select';
import OperatorSelect from '@/views/property-marketing/media-account/components/operator-select';
import StatusSelect from '@/views/property-marketing/media-account/components/status-select';
const props = defineProps({
query: {
type: Object,
required: true,
},
});
const emits = defineEmits('onSearch', 'onReset', 'update:query');
const groups = ref([]);
const operators = ref([]);
const handleSearch = () => {
emits('update:query', props.query);
nextTick(() => {
emits('onSearch');
});
};
const handleReset = () => {
emits('onReset');
};
const getGroups = async () => {
const { code, data } = await fetchAccountGroups();
if (code === 200) {
groups.value = data;
}
};
const getOperators = async () => {
const { code, data } = await fetchAccountOperators();
if (code === 200) {
operators.value = data;
}
};
onMounted(() => {
getGroups();
getOperators();
});
</script>
<style scoped lang="scss">
@import './style.scss';
</style>

View File

@ -0,0 +1,36 @@
.container {
:deep(.arco-input-wrapper),
:deep(.arco-select-view-single),
:deep(.arco-select-view-multiple) {
border-radius: 4px;
border-color: #d7d7d9;
background-color: #fff;
&:focus-within,
&.arco-input-focus {
background-color: var(--color-bg-2);
border-color: rgb(var(--primary-6));
box-shadow: 0 0 0 0 var(--color-primary-light-2);
}
}
.filter-row {
.filter-row-item {
&:not(:last-child) {
margin-right: 24px;
}
.label {
margin-right: 8px;
color: #211f24;
font-family: 'PuHuiTi-Regular';
font-size: 14px;
font-style: normal;
font-weight: 400;
flex-shrink: 0;
line-height: 22px; /* 157.143% */
}
:deep(.arco-space-item) {
width: 100%;
}
}
}
}

View File

@ -0,0 +1,276 @@
/*
* @Author: RenXiaoDong
* @Date: 2025-07-04 15:50:37
*/
import { CUSTOM_FIELDS } from '@/views/property-marketing/media-account/common_constants';
import icon1 from '@/assets/img/media-account/icon1.png';
import icon2 from '@/assets/img/media-account/icon2.png';
import icon3 from '@/assets/img/media-account/icon3.png';
import icon4 from '@/assets/img/media-account/icon4.png';
export const CARD_FIELDS = [
{
label: '账号总数',
prop: 'total_number',
icon: icon1,
},
{
label: '总粉丝数',
prop: 'total_fans_number',
icon: icon2,
tooltip: '所有账号当前累计的粉丝数量,反映账号的基础用户体量。',
},
{
label: '总赞藏数',
prop: 'total_like_number',
icon: icon3,
tooltip: '所有账号所有内容获得的点赞数与收藏数总和,用于衡量历史内容的整体吸引力与认可度。',
},
{
label: '近7日观看数',
prop: 'seven_view_number',
icon: icon4,
tooltip: '最近7天内所有账号所有作品的总观看次数用于衡量内容在短期内的整体传播效果。',
},
];
export const INITIAL_QUERY = {
name: '',
status: '',
operator_id: '',
group_ids: [],
type: 7,
column: '',
order: '',
};
export const getPropPrefix = (type: string) => {
return type === 'week' ? 'seven_' : 'thirty_';
};
export function getPeriodColumns(type = 'week') {
const prefix = getPropPrefix(type);
const labelPrefix = type === 'week' ? '近7天' : '近30天';
return CUSTOM_FIELDS.map((item) => ({
...item,
dataIndex: `${prefix}${item.dataIndex}`,
title: `${labelPrefix}${item.title}`,
prop: `${prefix}${item.prop}`,
tooltip: `${labelPrefix}${item.tooltip}`,
}));
}
export const getDefaultColumns = (type = 'week') => {
return [
{
title: '账号名称',
dataIndex: 'name',
prop: 'name',
width: 180,
fixed: 'left',
},
{
title: '账号分组',
dataIndex: 'group.name',
prop: 'group',
width: 180,
fixed: 'left',
},
{
title: '状态',
dataIndex: 'status',
prop: 'status',
width: 180,
fixed: 'left',
},
{
title: '运营人员',
dataIndex: 'operator.name',
prop: 'operator',
width: 180,
},
{
title: 'AI评价',
dataIndex: 'ai_evaluate',
prop: 'ai_evaluate',
width: 260,
},
{
title: '粉丝量',
dataIndex: 'fans_number',
prop: 'fans_number',
width: 180,
tooltip: '账号的当前粉丝总数。',
align: 'right',
sortable: {
sortDirections: ['ascend', 'descend'],
},
},
{
title: '总赞藏数',
dataIndex: 'like_collect_number',
prop: 'like_number',
width: 180,
tooltip: '账号所有内容获得的点赞数与收藏数总和,用于衡量历史内容的整体吸引力与认可度。',
align: 'right',
sortable: {
sortDirections: ['ascend', 'descend'],
},
},
...getPeriodColumns(type),
{
title: '最新内容标题/日期',
dataIndex: 'newest_work_title',
prop: 'newest_work_title',
width: 240,
// tooltip: '最新发布内容的标题和发布日期',
},
{
title: '最新作品观看数',
dataIndex: 'newest_work_view_number',
prop: 'newest_work_view_number',
width: 180,
tooltip: '最新一篇发布内容的总观看次数,反映当前内容热度初步表现。',
align: 'right',
sortable: {
sortDirections: ['ascend', 'descend'],
},
},
{
title: '最新作品点赞数',
dataIndex: 'newest_work_like_number',
prop: 'newest_work_like_number',
width: 180,
tooltip: '最新发布内容的点赞数',
align: 'right',
sortable: {
sortDirections: ['ascend', 'descend'],
},
},
{
title: '最新作品收藏数',
dataIndex: 'newest_work_collect_number',
prop: 'newest_work_collect_number',
width: 180,
tooltip: '最新发布内容的收藏数',
align: 'right',
sortable: {
sortDirections: ['ascend', 'descend'],
},
},
{
title: '最新作品评论数',
dataIndex: 'newest_work_comment_number',
prop: 'newest_work_comment_number',
width: 180,
tooltip: '最新发布内容的评论数',
align: 'right',
sortable: {
sortDirections: ['ascend', 'descend'],
},
},
{
title: '最新作品分享数',
dataIndex: 'newest_work_share_number',
prop: 'newest_work_share_number',
width: 180,
tooltip: '最新发布内容的分享数',
align: 'right',
sortable: {
sortDirections: ['ascend', 'descend'],
},
},
// {
// title: '最新作品日增长',
// dataIndex: 'newest_work_view_grow_number',
// prop: 'newest_work_view_grow_number',
// width: 180,
// tooltip: '最新作品的日均新增观看量,衡量内容是否具备持续热度。',
// align: 'right',
// sortable: {
// sortDirections: ['ascend', 'descend'],
// },
// },
{
title: '次新内容标题/日期',
dataIndex: 'second_new_work_title',
prop: 'second_new_work_title',
width: 240,
},
{
title: '次新作品观看数',
dataIndex: 'second_new_work_view_number',
prop: 'second_new_work_view_number',
width: 180,
tooltip: '次新作品内容的累计观看量,便于对比不同作品表现差异。',
align: 'right',
sortable: {
sortDirections: ['ascend', 'descend'],
},
},
{
title: '次新作品点赞数',
dataIndex: 'second_new_work_like_number',
prop: 'second_new_work_like_number',
width: 180,
tooltip: '次新作品内容的点赞数',
align: 'right',
sortable: {
sortDirections: ['ascend', 'descend'],
},
},
{
title: '次新作品收藏数',
dataIndex: 'second_new_work_collect_number',
prop: 'second_new_work_collect_number',
width: 180,
tooltip: '次新作品内容的收藏数',
align: 'right',
sortable: {
sortDirections: ['ascend', 'descend'],
},
},
{
title: '次新作品评论数',
dataIndex: 'second_new_work_comment_number',
prop: 'second_new_work_comment_number',
width: 180,
tooltip: '次新作品内容的评论数',
align: 'right',
sortable: {
sortDirections: ['ascend', 'descend'],
},
},
{
title: '次新作品分享数',
dataIndex: 'second_new_work_share_number',
prop: 'second_new_work_share_number',
width: 180,
tooltip: '次新作品内容的分享数',
align: 'right',
sortable: {
sortDirections: ['ascend', 'descend'],
},
},
// {
// title: '次新作品日增长',
// dataIndex: 'second_new_work_view_grow_number',
// prop: 'second_new_work_view_grow_number',
// width: 180,
// tooltip: '次新作品每日新增观看数,用于判断内容是否处于生命周期增长阶段。',
// align: 'right',
// sortable: {
// sortDirections: ['ascend', 'descend'],
// },
// },
// {
// title: '操作',
// dataIndex: 'operation',
// width: 100,
// fixed: 'right',
// },
];
};

View File

@ -0,0 +1,161 @@
<!--
* @Author: RenXiaoDong
* @Date: 2025-06-25 10:02:20
-->
<template>
<div class="account-dashboard-wrap">
<div class="filter-wrap bg-#fff rounded-8px border-1px border-#D7D7D9 border-solid px-24px pb-20px mb-16px">
<div class="top flex h-64px py-10px justify-between items-center">
<div class="flex items-center">
<p class="text-18px font-400 lh-26px color-#211F24 mr-4px title">数据总览</p>
<a-tooltip content="展示所筛选的账号的信息汇总">
<icon-question-circle size="16" class="color-#737478" />
</a-tooltip>
</div>
</div>
<div class="overview-row flex">
<div v-for="item in CARD_FIELDS" :key="item.prop" class="overview-item flex-1">
<div class="flex items-center mb-8px">
<img :src="item.icon" width="20" height="20" class="mr-8px" />
<p class="label color-#211F24">{{ item.label }}</p>
<a-tooltip v-if="item.tooltip" :content="item.tooltip">
<img :src="icon1" width="14" height="14" class="ml-4px" />
</a-tooltip>
</div>
<span class="value color-#211F24 ml-32px">{{ formatNumberShow(overviewData[item.prop]) }}</span>
</div>
</div>
</div>
<div class="filter-wrap bg-#fff rounded-8px border-1px border-#D7D7D9 border-solid px-24px py-24px mb-16px">
<FilterBlock v-model:query="query" @onSearch="handleSearch" @onReset="handleReset" />
</div>
<div
class="table-wrap bg-#fff rounded-8px border-1px border-#D7D7D9 border-solid px-24px py-24px flex-1 flex flex-col"
>
<AccountTable
ref="accountTableRef"
:dataSource="dataSource"
:query="query"
@selectionChange="handleSelectionChange"
@export="handleExport"
@sorterChange="handleSorterChange"
/>
<div v-if="pageInfo.total > 0" class="pagination-box">
<a-pagination
:total="pageInfo.total"
size="mini"
show-total
show-jumper
show-page-size
:current="pageInfo.page"
:page-size="pageInfo.page_size"
@change="onPageChange"
@page-size-change="onPageSizeChange"
/>
</div>
</div>
</div>
</template>
<script setup>
import FilterBlock from './components/filter-block';
import AccountTable from './components/account-table';
import { getAccountBoardOverview, getAccountBoardList, postAccountBoardExport } from '@/api/all/propertyMarketing';
import { formatNumberShow } from '@/utils/tools';
import { INITIAL_QUERY, CARD_FIELDS } from './constants';
import { downloadByUrl } from '@/utils/tools';
import icon1 from '@/assets/img/icon-question.png';
const query = ref(cloneDeep(INITIAL_QUERY));
const dataSource = ref([]);
const overviewData = ref({});
const selectedRowKeys = ref([]);
const accountTableRef = ref(null);
const pageInfo = ref({
page: 1,
page_size: 20,
total: 0,
});
const getOverviewData = async () => {
const { code, data } = await getAccountBoardOverview();
if (code === 200) {
overviewData.value = data;
}
};
const getData = async () => {
const { page, page_size } = pageInfo.value;
const { code, data } = await getAccountBoardList({
...query.value,
page,
page_size,
});
if (code === 200) {
dataSource.value = data?.data ?? [];
pageInfo.value.total = data.total;
}
};
const onPageChange = (current) => {
pageInfo.value.page = current;
getData();
};
const onPageSizeChange = (pageSize) => {
pageInfo.value.page_size = pageSize;
reload();
};
const handleSearch = () => {
reload();
};
const reload = () => {
pageInfo.value.page = 1;
getData();
};
const handleReset = () => {
selectedRowKeys.value = [];
pageInfo.value.page = 1;
pageInfo.value.page_size = 20;
pageInfo.value.total = 0;
query.value = cloneDeep(INITIAL_QUERY);
accountTableRef.value?.resetTable();
reload();
};
const handleSelectionChange = (selectedRows) => {
selectedRowKeys.value = selectedRows;
};
const handleExport = () => {
postAccountBoardExport({
...query.value,
}).then((res) => {
const { code, data } = res;
if (code === 200) {
downloadByUrl(data.download_url);
}
});
};
const handleSorterChange = (column, order) => {
query.value.column = column;
query.value.order = order;
reload();
};
onMounted(() => {
getOverviewData();
getData();
});
</script>
<style scoped lang="scss">
@import './style.scss';
</style>

View File

@ -0,0 +1,68 @@
.account-dashboard-wrap {
height: 100%;
display: flex;
flex-direction: column;
:deep(.search-btn) {
border-radius: 4px;
border: 1px solid var(--Brand-Brand-6, #6d4cfe);
color: #6d4cfe;
}
:deep(.reset-btn) {
border-radius: 4px;
border: 1px solid var(--BG-500, #b1b2b5);
background: var(--BG-white, #fff);
}
.filter-wrap {
border-radius: 8px;
border: 1px solid #e6e6e8;
.top {
.title {
font-family: 'PuHuiTi-Medium';
font-style: normal;
}
:deep(.arco-btn) {
.arco-btn-icon {
line-height: 14px;
}
}
}
.overview-row {
.overview-item {
height: 88px;
border-radius: 8px;
background: var(--BG-100, #f7f8fa);
padding: 16px;
&:not(:last-child) {
margin-right: 12px;
}
.label {
color: var(--Text-3, #737478);
font-family: 'PuHuiTi-Medium';
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px;
}
.value {
color: var(--Text-1, #211f24);
font-family: 'HarmonyOS Sans SC';
font-size: 18px;
font-style: normal;
font-weight: 500;
line-height: 26px;
}
}
}
}
.table-wrap {
width: 100%;
.pagination-box {
display: flex;
width: 100%;
padding: 16px 24px;
justify-content: flex-end;
align-items: center;
}
}
}

View File

@ -0,0 +1,143 @@
<!--
* @Author: RenXiaoDong
* @Date: 2025-06-28 12:58:25
-->
<template>
<div class="account-info-wrap bg-#fff rounded-8px border-1px border-#D7D7D9 border-solid px-24px mb-16px">
<div class="title-row">
<span class="cts !text-18px !lh-26px title">账号信息</span>
</div>
<div class="account-info-box">
<div
v-for="(row, rowIdx) in getAccountInfoFields(dateType, showMore)"
:key="rowIdx"
class="grid grid-cols-4 mb-24px"
>
<div
v-for="(field, colIdx) in row"
:key="colIdx"
:class="field.dataIndex === 'ai_evaluation' ? 'col-span-2' : ''"
>
<template v-if="field.dataIndex === 'ai_evaluation'">
<div class="flex items-center mb-4px">
<img width="16" height="16" :src="icon1" class="mr-4px" />
<span class="cts !color-#737478 mr-4px">AI评价</span>
</div>
<div class="flex items-center">
<template v-if="detailData.ai_evaluate">
<img
width="16"
height="16"
:src="
detailData.ai_evaluate?.status === 0 ? icon2 : detailData.ai_evaluate?.status === 1 ? icon3 : icon4
"
class="mr-4px"
/>
<div class="flex items-center">
<p class="cts">{{ `${detailData.ai_evaluate?.level} | ${detailData.ai_evaluate?.advise}` }}</p>
<p class="cts text-12px lh-20px">
{{
`观看: ${detailData[`${getPropPrefix(dateType)}view_rate`] ?? '-'}% 点赞: ${
detailData[`${getPropPrefix(dateType)}like_rate`] ?? '-'
}%`
}}
</p>
</div>
</template>
</div>
</template>
<template v-else>
<div class="flex items-center mb-4px">
<p class="cts !color-#737478 !mr-4px">{{ field.title }}</p>
<a-tooltip v-if="field.tooltip" :content="field.tooltip" position="top">
<icon-question-circle class="tooltip-icon color-#737478" size="16" />
</a-tooltip>
</div>
<p class="cts">
<template v-if="field.type === 'status'">
<div class="status-tag" :class="`status-tag-${detailData.status}`">
<span class="cts status-tag-text">{{
STATUS_LIST.find((item) => item.value === detailData.status)?.label
}}</span>
</div>
</template>
<template v-else-if="field.dataIndex === 'like_collect_number'">
{{
formatNumberShow({
value: `${detailData[`${getPropPrefix(dateType)}like_number`] ?? 0} + ${
detailData[`${getPropPrefix(dateType)}collect_number`] ?? 0
}`,
showExactValue: true,
})
}}
</template>
<!-- 环比字段特殊渲染 -->
<template v-else-if="field.isRateField">
<div
class="flex items-center"
:class="detailData[field.dataIndex] > 0 ? 'color-#F64B31' : 'color-#25C883'"
>
<icon-arrow-up v-if="detailData[field.dataIndex] > 0" size="16" />
<icon-arrow-down v-else size="16" />
{{ `${detailData[field.dataIndex]}%` }}
</div>
</template>
<template v-else>
{{ formatTableField(field, detailData, true) }}
</template>
</p>
</template>
</div>
</div>
</div>
<div
class="more-btn-row flex items-center absolute right-24px bottom-32px cursor-pointer"
@click="showMore = !showMore"
>
<span class="cts mr-8px !color-#6D4CFE"> {{ showMore ? '收起' : '更多' }} </span>
<icon-down
size="13"
class="!color-#6D4CFE transform transition-transform duration-300 ease-in-out"
:class="showMore ? 'rotate-180' : ''"
/>
</div>
</div>
</template>
<script setup>
import { useRoute } from 'vue-router';
import { formatTableField, formatNumberShow } from '@/utils/tools';
import { getAccountInfoFields } from '../../constants';
import { STATUS_LIST } from '@/views/property-marketing/media-account/components/status-select/constants';
import { getPropPrefix } from '@/views/property-marketing/media-account/account-dashboard/constants';
import { getAccountBoardDetail } from '@/api/all/propertyMarketing';
import icon1 from '@/assets/img/media-account/icon5.png';
import icon2 from '@/assets/img/media-account/icon-warn.png';
import icon3 from '@/assets/img/media-account/icon-warn-1.png';
import icon4 from '@/assets/img/media-account/icon-success.png';
const route = useRoute();
const id = route.params.id;
const dateType = route.query.type;
const detailData = ref({});
const showMore = ref(false);
const getDetail = async () => {
const { code, data } = await getAccountBoardDetail(id);
if (code === 200) {
detailData.value = data;
}
};
onMounted(() => {
getDetail();
});
</script>
<style lang="scss" scoped>
@import './style.scss';
</style>

View File

@ -0,0 +1,41 @@
.account-info-wrap {
position: relative;
.status-tag {
width: fit-content;
display: flex;
height: 20px;
padding: 0px 8px;
align-items: center;
border-radius: 2px;
background: #f2f3f5;
.status-tag-text {
color: var(--BG-700, #737478);
}
&-1 {
background: #ebf7f2;
.status-tag-text {
color: #25c883;
}
}
&-2,
&-4,
&-5,
&-6,
&-7 {
background: #ffe7e4;
.status-tag-text {
color: #f64b31;
}
}
&-3 {
background: #fff7e5;
color: #ffae00;
.status-tag-text {
color: #ffae00;
}
}
}
}

View File

@ -0,0 +1,81 @@
/*
* @Author: RenXiaoDong
* @Date: 2025-06-28 13:30:58
*/
export const INITIAL_QUERY = {
title: '',
published_at: [],
};
export const INITIAL_PAGE_INFO = {
page: 1,
page_size: 20,
total: 0,
};
export const TABLE_COLUMNS = [
{
title: '笔记标题',
dataIndex: 'title',
width: 240,
fixed: 'left',
},
{
title: '发布日期',
dataIndex: 'published_at',
width: 160,
fixed: 'left',
},
{
title: '曝光量',
dataIndex: 'exposure_number',
width: 180,
tooltip: '内容被展示给用户的总次数,不代表用户实际观看。',
align: 'right',
},
{
title: '观看量',
dataIndex: 'view_number',
width: 180,
tooltip: '用户点击内容并实际观看的次数,是内容实际触达的重要指标。',
align: 'right',
},
{
title: '点赞量',
dataIndex: 'like_number',
width: 180,
tooltip: '单篇笔记获得的点赞总数,反映用户喜好程度。',
align: 'right',
},
{
title: '收藏量',
dataIndex: 'collect_number',
width: 180,
tooltip: '用户将内容保存到收藏夹的次数,代表内容被认可为“值得保留”。',
align: 'right',
},
{
title: '评论数',
dataIndex: 'comment_number',
width: 180,
tooltip: '内容下方用户留言的总数,体现用户参与度与讨论热度。',
align: 'right',
},
{
title: '分享量',
dataIndex: 'share_number',
width: 180,
tooltip: '内容被转发或分享至其他平台或私信的次数,代表外扩传播意愿。',
align: 'right',
},
{
title: '封面点击率',
dataIndex: 'cover_click_rate',
width: 180,
tooltip: '内容在被曝光后,用户点击进入的比例,反映封面与标题吸引力。',
align: 'right',
suffix: '%',
},
];

View File

@ -0,0 +1,183 @@
<!--
* @Author: RenXiaoDong
* @Date: 2025-06-28 12:58:09
-->
<template>
<div class="note-table-wrap bg-#fff rounded-8px border-1px border-#D7D7D9 border-solid px-24px flex-1 flex flex-col">
<div class="title-row">
<div class="flex items-center">
<span class="cts !text-18px !lh-26px mr-4px title">笔记详情</span>
<a-tooltip content="展示笔记层级的详细数据,如曝光、互动等,是内容精细分析入口。">
<icon-question-circle size="16" class="color-#737478" />
</a-tooltip>
</div>
</div>
<div class="filter-row flex my-16px">
<div class="filter-row-item flex items-center">
<span class="label">笔记标题</span>
<a-space size="medium" class="w-240px">
<a-input v-model="query.title" placeholder="请搜索..." size="medium" allow-clear @change="handleSearch">
<template #prefix>
<icon-search />
</template>
</a-input>
</a-space>
</div>
<div class="filter-row-item flex items-center">
<span class="label">发布日期</span>
<a-space size="medium" class="w-260px">
<a-range-picker
v-model="published_at"
size="medium"
allow-clear
format="YYYY-MM-DD"
class="w-100%"
@change="onDateChange"
/>
</a-space>
</div>
<a-button class="w-84px search-btn mr-12px" size="medium" @click="handleSearch">
<template #icon>
<icon-search />
</template>
<template #default>搜索</template>
</a-button>
<a-button class="w-84px reset-btn" size="medium" @click="handleReset">
<template #icon>
<icon-refresh />
</template>
<template #default>重置</template>
</a-button>
</div>
<a-table
:data="dataSource"
row-key="id"
:pagination="false"
:scroll="{ x: '100%' }"
class="w-100%"
bordered
column-resizable
>
<template #empty>
<NoData />
</template>
<template #columns>
<a-table-column
v-for="column in TABLE_COLUMNS"
:key="column.dataIndex"
:data-index="column.dataIndex"
:fixed="column.fixed"
:width="column.width"
:min-width="column.minWidth"
:sortable="column.sortable"
:align="column.align"
ellipsis
tooltip
>
<template #title>
<div class="flex items-center">
<span class="cts mr-4px">{{ column.title }}</span>
<a-tooltip v-if="column.tooltip" :content="column.tooltip" position="top">
<icon-question-circle class="tooltip-icon color-#737478" size="16" />
</a-tooltip>
</div>
</template>
<template #cell="{ record }">
<template v-if="column.dataIndex === 'published_at'">
{{ exactFormatTime(record.published_at) }}
</template>
<template v-else>
{{ formatTableField(column, record, true) }}
</template>
</template>
</a-table-column>
</template>
</a-table>
<div v-if="pageInfo.total > 0" class="pagination-box">
<a-pagination
:total="pageInfo.total"
size="mini"
show-total
show-jumper
show-page-size
:current="pageInfo.page"
:page-size="pageInfo.page_size"
@change="onPageChange"
@page-size-change="onPageSizeChange"
/>
</div>
</div>
</template>
<script setup>
import { TABLE_COLUMNS, INITIAL_QUERY, INITIAL_PAGE_INFO } from './constants';
import { useRoute } from 'vue-router';
import { formatTableField, exactFormatTime } from '@/utils/tools';
import { getMediaAccountBoardWorks } from '@/api/all/propertyMarketing';
const route = useRoute();
const id = route.params.id;
const dataSource = ref([]);
const published_at = ref([]);
const pageInfo = ref(cloneDeep(INITIAL_PAGE_INFO));
const query = ref(cloneDeep(INITIAL_QUERY));
const handleSearch = () => {
reload();
};
const handleReset = () => {
pageInfo.value = cloneDeep(INITIAL_PAGE_INFO);
query.value = cloneDeep(INITIAL_QUERY);
published_at.value = [];
reload();
};
const onDateChange = (value) => {
const FORMAT_DATE = 'YYYY-MM-DD HH:mm:ss';
query.value.published_at = [
dayjs(value[0]).startOf('day').format(FORMAT_DATE),
dayjs(value[1]).endOf('day').format(FORMAT_DATE),
];
handleSearch();
};
const onPageChange = (current) => {
pageInfo.value.page = current;
getData();
};
const onPageSizeChange = (pageSize) => {
pageInfo.value.page_size = pageSize;
reload();
};
const reload = () => {
pageInfo.value.page = 1;
getData();
};
const getData = async () => {
const { page, page_size } = pageInfo.value;
const { code, data } = await getMediaAccountBoardWorks(id, {
...query.value,
page,
page_size,
});
if (code === 200) {
dataSource.value = data?.data || [];
pageInfo.value.total = data.total;
}
};
onMounted(() => {
getData();
});
</script>
<style lang="scss" scoped>
@import './style.scss';
</style>

View File

@ -0,0 +1,44 @@
.note-table-wrap {
:deep(.arco-input-wrapper),
:deep(.arco-select-view-single),
:deep(.arco-select-view-multiple),
:deep(.arco-picker) {
border-radius: 4px;
border-color: #d7d7d9;
background-color: #fff;
&:focus-within,
&.arco-input-focus {
background-color: var(--color-bg-2);
border-color: rgb(var(--primary-6));
box-shadow: 0 0 0 0 var(--color-primary-light-2);
}
}
.filter-row {
.filter-row-item {
&:not(:last-child) {
margin-right: 24px;
}
.label {
margin-right: 8px;
color: #211f24;
font-family: 'PuHuiTi-Regular';
font-size: 14px;
font-style: normal;
font-weight: 400;
flex-shrink: 0;
line-height: 22px; /* 157.143% */
}
:deep(.arco-space-item) {
width: 100%;
}
}
}
.pagination-box {
display: flex;
width: 100%;
padding: 16px 24px;
justify-content: flex-end;
align-items: center;
}
}

View File

@ -0,0 +1,53 @@
/*
* @Author: RenXiaoDong
* @Date: 2025-06-28 12:55:44
*/
import { CUSTOM_FIELDS, getPropPrefix } from '@/views/property-marketing/media-account/common_constants';
// 不足4个。就补两个null进去
export function groupArrayBySize<T extends { dataIndex: string; prop: string; title: string; tooltip: string }>(
fields: T[],
groupSize = 4,
dateType: string,
): T[][] {
const result: T[][] = [];
for (let i = 0; i < fields.length; i += groupSize) {
result.push(fields.slice(i, i + groupSize));
}
const labelPrefix = dateType === 'week' ? '近7天' : '近30天';
return result.map((item) => {
return item.map((item) => {
return {
...item,
dataIndex: `${getPropPrefix(dateType)}${item.dataIndex}`,
prop: `${getPropPrefix(dateType)}${item.prop}`,
title: `${labelPrefix}${item.title}`,
tooltip: `${labelPrefix}${item.tooltip}`,
};
});
});
}
export const getAccountInfoFields = (dateType: string, showMore: boolean) => {
const baseFields = [
[
{ title: '账号名称', dataIndex: 'name' },
{ title: '项目分组', dataIndex: 'group.name' },
{ title: '状态', dataIndex: 'status', type: 'status' },
{ title: '运营人员', dataIndex: 'operator_name' },
],
[
{ title: 'AI评价', dataIndex: 'ai_evaluation' },
{ title: '粉丝量', dataIndex: 'fans_number', tooltip: '账号的当前粉丝总数。' },
{
title: '总赞藏数',
dataIndex: 'like_collect_number',
tooltip: '账号所有内容获得的点赞数与收藏数总和,用于衡量历史内容的整体吸引力与认可度。',
},
],
];
const customFields = groupArrayBySize(CUSTOM_FIELDS, 4, dateType);
return showMore ? [...baseFields, ...customFields] : [...baseFields];
};

View File

@ -0,0 +1,30 @@
<!--
* @Author: RenXiaoDong
* @Date: 2025-06-28 11:21:10
-->
<template>
<div class="account-detail-wrap">
<div class="flex items-center mb-16px cursor-pointer" @click="handleBack">
<icon-left size="16" />
<spa class="cts title ml-8px">账号详情</spa>
</div>
<AccountInfo />
<NoteTable />
</div>
</template>
<script setup>
import AccountInfo from './components/account-info';
import NoteTable from './components/note-table';
import { useRouter } from 'vue-router';
const router = useRouter();
const handleBack = () => {
router.push('/media-account/dashboard');
};
</script>
<style scoped lang="scss">
@import './style.scss';
</style>

View File

@ -0,0 +1,43 @@
.account-detail-wrap {
height: 100%;
display: flex;
flex-direction: column;
:deep(.cts) {
color: var(--Text-1, #211f24);
font-family: 'PuHuiTi-Regular';
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px;
&.title {
font-family: 'PuHuiTi-Medium';
}
}
:deep(.title-row) {
display: flex;
height: 64px;
padding: 10px 0;
align-items: center;
}
:deep(.search-btn) {
border-radius: 4px;
border: 1px solid var(--Brand-Brand-6, #6d4cfe);
color: #6d4cfe;
}
:deep(.reset-btn) {
border-radius: 4px;
border: 1px solid var(--BG-500, #b1b2b5);
background: var(--BG-white, #fff);
}
.table-wrap {
width: 100%;
.pagination-box {
display: flex;
width: 100%;
padding: 16px 24px;
justify-content: flex-end;
align-items: center;
}
}
}

Some files were not shown because too many files have changed in this diff Show More