提交 12371c27 authored 作者: 王鹏飞's avatar 王鹏飞

Merge branch 'pro' into 202412

......@@ -2,3 +2,8 @@ VITE_LOGIN_URL=http://172.16.3.203:1001/auth/login/index
VITE_LAB_URL=http://172.16.3.203:1012/bi/?proc=0&action=index
VITE_SURVEYKING_URL=http://172.16.3.203:1011
VITE_STATIC_URL=https://saas-lab-api
# 直播语音识别
VITE_ACCESS_KEY_ID=LTAI5t7YUVzDVSFLYvnuWGuq
VITE_ACCESS_KEY_SECRET=GBsohg5hSUP99dzIuRuCQilUXTSiYe
VITE_APP_KEY=W7Yqc8L49MEnnLsE
......@@ -302,6 +302,11 @@
"onWatcherCleanup": true,
"useId": true,
"useModel": true,
"useTemplateRef": true
"useTemplateRef": true,
"createRef": true,
"onElementRemoval": true,
"useCountdown": true,
"usePreferredReducedTransparency": true,
"useSSRWidth": true
}
}
......@@ -28,6 +28,7 @@ declare global {
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
const createRef: typeof import('@vueuse/core')['createRef']
const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate']
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise']
......@@ -62,6 +63,7 @@ declare global {
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onClickOutside: typeof import('@vueuse/core')['onClickOutside']
const onDeactivated: typeof import('vue')['onDeactivated']
const onElementRemoval: typeof import('@vueuse/core')['onElementRemoval']
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke']
const onLongPress: typeof import('@vueuse/core')['onLongPress']
......@@ -144,6 +146,7 @@ declare global {
const useCloned: typeof import('@vueuse/core')['useCloned']
const useColorMode: typeof import('@vueuse/core')['useColorMode']
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
const useCountdown: typeof import('@vueuse/core')['useCountdown']
const useCounter: typeof import('@vueuse/core')['useCounter']
const useCssModule: typeof import('vue')['useCssModule']
const useCssVar: typeof import('@vueuse/core')['useCssVar']
......@@ -224,12 +227,14 @@ declare global {
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
const usePreferredReducedTransparency: typeof import('@vueuse/core')['usePreferredReducedTransparency']
const usePrevious: typeof import('@vueuse/core')['usePrevious']
const useRafFn: typeof import('@vueuse/core')['useRafFn']
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
const useRoute: typeof import('vue-router')['useRoute']
const useRouter: typeof import('vue-router')['useRouter']
const useSSRWidth: typeof import('@vueuse/core')['useSSRWidth']
const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
......
......@@ -15,14 +15,16 @@
"@tinymce/tinymce-vue": "^5.0.1",
"@vue-flow/controls": "^1.1.2",
"@vue-flow/core": "^1.39.0",
"@vueuse/components": "^11.2.0",
"@vueuse/core": "^11.2.0",
"axios": "^1.6.8",
"@vueuse/components": "^13.3.0",
"@vueuse/core": "^13.3.0",
"axios": "^1.9.0",
"blueimp-md5": "^2.19.0",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.10",
"echarts": "^5.5.0",
"echarts-wordcloud": "^2.1.0",
"element-plus": "^2.8.7",
"eventsource-parser": "^3.0.2",
"file-saver": "^2.0.5",
"html-to-image": "^1.11.11",
"jspdf": "^2.5.1",
......@@ -30,14 +32,16 @@
"nanoid": "^5.0.7",
"pinia": "^2.2.6",
"scroll-into-view-if-needed": "^3.1.0",
"vue": "^3.5.12",
"vue": "^3.5.16",
"vue-echarts": "^6.6.9",
"vue-router": "^4.4.5",
"vue-markdown-render": "^2.2.1",
"vue-router": "^4.5.1",
"xss": "^1.0.15"
},
"devDependencies": {
"@tsconfig/node20": "^20.1.4",
"@types/blueimp-md5": "^2.18.2",
"@types/crypto-js": "^4.2.2",
"@types/file-saver": "^2.0.7",
"@types/node": "^20.17.6",
"@vitejs/plugin-vue": "^5.1.4",
......@@ -332,18 +336,18 @@
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
"integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
"integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
......@@ -462,12 +466,12 @@
}
},
"node_modules/@babel/parser": {
"version": "7.26.2",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz",
"integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==",
"version": "7.27.3",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.3.tgz",
"integrity": "sha512-xyYxRj6+tLNDTWi0KCBcZ9V7yg3/lwL9DWh9Uwh/RIVlIfFidggcgxKX3GCXwCiswwcGRawBKbEg2LG/Y8eJhw==",
"license": "MIT",
"dependencies": {
"@babel/types": "^7.26.0"
"@babel/types": "^7.27.3"
},
"bin": {
"parser": "bin/babel-parser.js"
......@@ -549,13 +553,13 @@
}
},
"node_modules/@babel/types": {
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz",
"integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==",
"version": "7.27.3",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.3.tgz",
"integrity": "sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw==",
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.25.9",
"@babel/helper-validator-identifier": "^7.25.9"
"@babel/helper-string-parser": "^7.27.1",
"@babel/helper-validator-identifier": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
......@@ -2282,6 +2286,13 @@
"integrity": "sha512-dJ9yRry9Olt5GAWlgCtE5dK9d/Dfhn/V7hna86eEO2Pn76+E8Y0S0n61iEUEGhWXXgtKtHxtZLVNwL8X+vLHzg==",
"dev": true
},
"node_modules/@types/crypto-js": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz",
"integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/estree": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
......@@ -2771,53 +2782,53 @@
}
},
"node_modules/@vue/compiler-core": {
"version": "3.5.12",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.12.tgz",
"integrity": "sha512-ISyBTRMmMYagUxhcpyEH0hpXRd/KqDU4ymofPgl2XAkY9ZhQ+h0ovEZJIiPop13UmR/54oA2cgMDjgroRelaEw==",
"version": "3.5.16",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.16.tgz",
"integrity": "sha512-AOQS2eaQOaaZQoL1u+2rCJIKDruNXVBZSiUD3chnUrsoX5ZTQMaCvXlWNIfxBJuU15r1o7+mpo5223KVtIhAgQ==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.25.3",
"@vue/shared": "3.5.12",
"@babel/parser": "^7.27.2",
"@vue/shared": "3.5.16",
"entities": "^4.5.0",
"estree-walker": "^2.0.2",
"source-map-js": "^1.2.0"
"source-map-js": "^1.2.1"
}
},
"node_modules/@vue/compiler-dom": {
"version": "3.5.12",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.12.tgz",
"integrity": "sha512-9G6PbJ03uwxLHKQ3P42cMTi85lDRvGLB2rSGOiQqtXELat6uI4n8cNz9yjfVHRPIu+MsK6TE418Giruvgptckg==",
"version": "3.5.16",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.16.tgz",
"integrity": "sha512-SSJIhBr/teipXiXjmWOVWLnxjNGo65Oj/8wTEQz0nqwQeP75jWZ0n4sF24Zxoht1cuJoWopwj0J0exYwCJ0dCQ==",
"license": "MIT",
"dependencies": {
"@vue/compiler-core": "3.5.12",
"@vue/shared": "3.5.12"
"@vue/compiler-core": "3.5.16",
"@vue/shared": "3.5.16"
}
},
"node_modules/@vue/compiler-sfc": {
"version": "3.5.12",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.12.tgz",
"integrity": "sha512-2k973OGo2JuAa5+ZlekuQJtitI5CgLMOwgl94BzMCsKZCX/xiqzJYzapl4opFogKHqwJk34vfsaKpfEhd1k5nw==",
"version": "3.5.16",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.16.tgz",
"integrity": "sha512-rQR6VSFNpiinDy/DVUE0vHoIDUF++6p910cgcZoaAUm3POxgNOOdS/xgoll3rNdKYTYPnnbARDCZOyZ+QSe6Pw==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.25.3",
"@vue/compiler-core": "3.5.12",
"@vue/compiler-dom": "3.5.12",
"@vue/compiler-ssr": "3.5.12",
"@vue/shared": "3.5.12",
"@babel/parser": "^7.27.2",
"@vue/compiler-core": "3.5.16",
"@vue/compiler-dom": "3.5.16",
"@vue/compiler-ssr": "3.5.16",
"@vue/shared": "3.5.16",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.11",
"postcss": "^8.4.47",
"source-map-js": "^1.2.0"
"magic-string": "^0.30.17",
"postcss": "^8.5.3",
"source-map-js": "^1.2.1"
}
},
"node_modules/@vue/compiler-ssr": {
"version": "3.5.12",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.12.tgz",
"integrity": "sha512-eLwc7v6bfGBSM7wZOGPmRavSWzNFF6+PdRhE+VFJhNCgHiF8AM7ccoqcv5kBXA2eWUfigD7byekvf/JsOfKvPA==",
"version": "3.5.16",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.16.tgz",
"integrity": "sha512-d2V7kfxbdsjrDSGlJE7my1ZzCXViEcqN6w14DOsDrUCHEA6vbnVCpRFfrc4ryCP/lCKzX2eS1YtnLE/BuC9f/A==",
"license": "MIT",
"dependencies": {
"@vue/compiler-dom": "3.5.12",
"@vue/shared": "3.5.12"
"@vue/compiler-dom": "3.5.16",
"@vue/shared": "3.5.16"
}
},
"node_modules/@vue/compiler-vue2": {
......@@ -2915,53 +2926,53 @@
}
},
"node_modules/@vue/reactivity": {
"version": "3.5.12",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.12.tgz",
"integrity": "sha512-UzaN3Da7xnJXdz4Okb/BGbAaomRHc3RdoWqTzlvd9+WBR5m3J39J1fGcHes7U3za0ruYn/iYy/a1euhMEHvTAg==",
"version": "3.5.16",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.16.tgz",
"integrity": "sha512-FG5Q5ee/kxhIm1p2bykPpPwqiUBV3kFySsHEQha5BJvjXdZTUfmya7wP7zC39dFuZAcf/PD5S4Lni55vGLMhvA==",
"license": "MIT",
"dependencies": {
"@vue/shared": "3.5.12"
"@vue/shared": "3.5.16"
}
},
"node_modules/@vue/runtime-core": {
"version": "3.5.12",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.12.tgz",
"integrity": "sha512-hrMUYV6tpocr3TL3Ad8DqxOdpDe4zuQY4HPY3X/VRh+L2myQO8MFXPAMarIOSGNu0bFAjh1yBkMPXZBqCk62Uw==",
"version": "3.5.16",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.16.tgz",
"integrity": "sha512-bw5Ykq6+JFHYxrQa7Tjr+VSzw7Dj4ldR/udyBZbq73fCdJmyy5MPIFR9IX/M5Qs+TtTjuyUTCnmK3lWWwpAcFQ==",
"license": "MIT",
"dependencies": {
"@vue/reactivity": "3.5.12",
"@vue/shared": "3.5.12"
"@vue/reactivity": "3.5.16",
"@vue/shared": "3.5.16"
}
},
"node_modules/@vue/runtime-dom": {
"version": "3.5.12",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.12.tgz",
"integrity": "sha512-q8VFxR9A2MRfBr6/55Q3umyoN7ya836FzRXajPB6/Vvuv0zOPL+qltd9rIMzG/DbRLAIlREmnLsplEF/kotXKA==",
"version": "3.5.16",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.16.tgz",
"integrity": "sha512-T1qqYJsG2xMGhImRUV9y/RseB9d0eCYZQ4CWca9ztCuiPj/XWNNN+lkNBuzVbia5z4/cgxdL28NoQCvC0Xcfww==",
"license": "MIT",
"dependencies": {
"@vue/reactivity": "3.5.12",
"@vue/runtime-core": "3.5.12",
"@vue/shared": "3.5.12",
"@vue/reactivity": "3.5.16",
"@vue/runtime-core": "3.5.16",
"@vue/shared": "3.5.16",
"csstype": "^3.1.3"
}
},
"node_modules/@vue/server-renderer": {
"version": "3.5.12",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.12.tgz",
"integrity": "sha512-I3QoeDDeEPZm8yR28JtY+rk880Oqmj43hreIBVTicisFTx/Dl7JpG72g/X7YF8hnQD3IFhkky5i2bPonwrTVPg==",
"version": "3.5.16",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.16.tgz",
"integrity": "sha512-BrX0qLiv/WugguGsnQUJiYOE0Fe5mZTwi6b7X/ybGB0vfrPH9z0gD/Y6WOR1sGCgX4gc25L1RYS5eYQKDMoNIg==",
"license": "MIT",
"dependencies": {
"@vue/compiler-ssr": "3.5.12",
"@vue/shared": "3.5.12"
"@vue/compiler-ssr": "3.5.16",
"@vue/shared": "3.5.16"
},
"peerDependencies": {
"vue": "3.5.12"
"vue": "3.5.16"
}
},
"node_modules/@vue/shared": {
"version": "3.5.12",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.12.tgz",
"integrity": "sha512-L2RPSAwUFbgZH20etwrXyVyCBu9OxRSi8T/38QsvnkJyvq2LufW2lDCOzm7t/U9C1mkhJGWYfCuFBCmIuNivrg==",
"version": "3.5.16",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.16.tgz",
"integrity": "sha512-c/0fWy3Jw6Z8L9FmTyYfkpM5zklnqqa9+a6dz3DvONRKW2NEbh46BP0FHuLFSWi2TnQEtp91Z6zOWNrU6QiyPg==",
"license": "MIT"
},
"node_modules/@vue/tsconfig": {
......@@ -2971,128 +2982,60 @@
"dev": true
},
"node_modules/@vueuse/components": {
"version": "11.2.0",
"resolved": "https://registry.npmjs.org/@vueuse/components/-/components-11.2.0.tgz",
"integrity": "sha512-L9uDsTcaMvz3x1tX2RepdmvDJGIHBiSeYVXNFfHceiM3mmPY6vfRlS/XqZTpip7FdXxu0s/zSmtZCffZGTNRXQ==",
"version": "13.3.0",
"resolved": "https://registry.npmjs.org/@vueuse/components/-/components-13.3.0.tgz",
"integrity": "sha512-ZnJiVknPtlWyeE4qwIXkDOlHM3W4bgMCxgeXj1Dec/aF/+8N+yAj+7rRdRUWUnqr8uKRin368RjG1FPKsF2erA==",
"license": "MIT",
"dependencies": {
"@vueuse/core": "11.2.0",
"@vueuse/shared": "11.2.0",
"vue-demi": ">=0.14.10"
}
},
"node_modules/@vueuse/components/node_modules/vue-demi": {
"version": "0.14.10",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
"hasInstallScript": true,
"license": "MIT",
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
"@vueuse/core": "13.3.0",
"@vueuse/shared": "13.3.0"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
"vue": "^3.5.0"
}
},
"node_modules/@vueuse/core": {
"version": "11.2.0",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-11.2.0.tgz",
"integrity": "sha512-JIUwRcOqOWzcdu1dGlfW04kaJhW3EXnnjJJfLTtddJanymTL7lF1C0+dVVZ/siLfc73mWn+cGP1PE1PKPruRSA==",
"version": "13.3.0",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-13.3.0.tgz",
"integrity": "sha512-uYRz5oEfebHCoRhK4moXFM3NSCd5vu2XMLOq/Riz5FdqZMy2RvBtazdtL3gEcmDyqkztDe9ZP/zymObMIbiYSg==",
"license": "MIT",
"dependencies": {
"@types/web-bluetooth": "^0.0.20",
"@vueuse/metadata": "11.2.0",
"@vueuse/shared": "11.2.0",
"vue-demi": ">=0.14.10"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/core/node_modules/vue-demi": {
"version": "0.14.10",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
"hasInstallScript": true,
"license": "MIT",
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
"@types/web-bluetooth": "^0.0.21",
"@vueuse/metadata": "13.3.0",
"@vueuse/shared": "13.3.0"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
"vue": "^3.5.0"
}
},
"node_modules/@vueuse/core/node_modules/@types/web-bluetooth": {
"version": "0.0.21",
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
"integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==",
"license": "MIT"
},
"node_modules/@vueuse/metadata": {
"version": "11.2.0",
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-11.2.0.tgz",
"integrity": "sha512-L0ZmtRmNx+ZW95DmrgD6vn484gSpVeRbgpWevFKXwqqQxW9hnSi2Ppuh2BzMjnbv4aJRiIw8tQatXT9uOB23dQ==",
"version": "13.3.0",
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-13.3.0.tgz",
"integrity": "sha512-42IzJIOYCKIb0Yjv1JfaKpx8JlCiTmtCWrPxt7Ja6Wzoq0h79+YVXmBV03N966KEmDEESTbp5R/qO3AB5BDnGw==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/shared": {
"version": "11.2.0",
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-11.2.0.tgz",
"integrity": "sha512-VxFjie0EanOudYSgMErxXfq6fo8vhr5ICI+BuE3I9FnX7ePllEsVrRQ7O6Q1TLgApeLuPKcHQxAXpP+KnlrJsg==",
"license": "MIT",
"dependencies": {
"vue-demi": ">=0.14.10"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/shared/node_modules/vue-demi": {
"version": "0.14.10",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
"hasInstallScript": true,
"version": "13.3.0",
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-13.3.0.tgz",
"integrity": "sha512-L1QKsF0Eg9tiZSFXTgodYnu0Rsa2P0En2LuLrIs/jgrkyiDuJSsPZK+tx+wU0mMsYHUYEjNsuE41uqqkuR8VhA==",
"license": "MIT",
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
"vue": "^3.5.0"
}
},
"node_modules/@webassemblyjs/ast": {
......@@ -3499,7 +3442,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true,
"license": "Python-2.0"
},
"node_modules/arr-diff": {
......@@ -3638,9 +3580,9 @@
}
},
"node_modules/axios": {
"version": "1.7.7",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
"integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
"integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
......@@ -4063,6 +4005,7 @@
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
"integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
"peer": true,
"dependencies": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
......@@ -4077,6 +4020,35 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/call-bound": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"get-intrinsic": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
......@@ -4498,6 +4470,12 @@
"node": "*"
}
},
"node_modules/crypto-js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
"license": "MIT"
},
"node_modules/css-line-break": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
......@@ -4704,6 +4682,7 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"peer": true,
"dependencies": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
......@@ -4878,6 +4857,20 @@
"node": ">=12"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/duplexify": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz",
......@@ -5118,12 +5111,10 @@
}
},
"node_modules/es-define-property": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
"dependencies": {
"get-intrinsic": "^1.2.4"
},
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
......@@ -5136,6 +5127,18 @@
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/esbuild": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
......@@ -5451,6 +5454,15 @@
"node": ">=0.8.x"
}
},
"node_modules/eventsource-parser": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.2.tgz",
"integrity": "sha512-6RxOBZ/cYgd8usLwsEl+EC09Au/9BcmCKYF2/xbml6DNczf7nv0MQb+7BA2F+li6//I+28VNlQR37XfQtcAJuA==",
"license": "MIT",
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/evp_bytestokey": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz",
......@@ -5909,15 +5921,21 @@
}
},
"node_modules/get-intrinsic": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"has-proto": "^1.0.1",
"has-symbols": "^1.0.3",
"hasown": "^2.0.0"
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
......@@ -5926,6 +5944,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/get-ready": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/get-ready/-/get-ready-1.0.0.tgz",
......@@ -6004,11 +6035,12 @@
}
},
"node_modules/gopd": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
"dependencies": {
"get-intrinsic": "^1.1.3"
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
......@@ -6040,6 +6072,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"peer": true,
"dependencies": {
"es-define-property": "^1.0.0"
},
......@@ -6047,21 +6080,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
"integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
......@@ -6668,6 +6691,15 @@
"node": ">= 0.8.0"
}
},
"node_modules/linkify-it": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz",
"integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==",
"license": "MIT",
"dependencies": {
"uc.micro": "^1.0.1"
}
},
"node_modules/loader-runner": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz",
......@@ -6776,9 +6808,9 @@
}
},
"node_modules/magic-string": {
"version": "0.30.12",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz",
"integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==",
"version": "0.30.17",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0"
......@@ -6838,6 +6870,43 @@
"node": ">=0.10.0"
}
},
"node_modules/markdown-it": {
"version": "13.0.2",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.2.tgz",
"integrity": "sha512-FtwnEuuK+2yVU7goGn/MJ0WBZMM9ZPgU9spqlFs7/A/pDIUNSOQZhUgOqYCficIuR2QaFnrt8LHqBWsbTAoI5w==",
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1",
"entities": "~3.0.1",
"linkify-it": "^4.0.1",
"mdurl": "^1.0.1",
"uc.micro": "^1.0.5"
},
"bin": {
"markdown-it": "bin/markdown-it.js"
}
},
"node_modules/markdown-it/node_modules/entities": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz",
"integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/md5.js": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
......@@ -6849,6 +6918,12 @@
"safe-buffer": "^5.1.2"
}
},
"node_modules/mdurl": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
"integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==",
"license": "MIT"
},
"node_modules/memoize-one": {
"version": "6.0.0",
"resolved": "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz",
......@@ -7419,9 +7494,13 @@
}
},
"node_modules/object-inspect": {
"version": "1.12.3",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
"integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
......@@ -7921,9 +8000,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.47",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
"integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
"integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
"funding": [
{
"type": "opencollective",
......@@ -7940,8 +8019,8 @@
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.1.0",
"nanoid": "^3.3.8",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
......@@ -7974,15 +8053,16 @@
}
},
"node_modules/postcss/node_modules/nanoid": {
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
......@@ -8086,11 +8166,12 @@
}
},
"node_modules/qs": {
"version": "6.11.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz",
"integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==",
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.0.4"
"side-channel": "^1.1.0"
},
"engines": {
"node": ">=0.6"
......@@ -8502,6 +8583,7 @@
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
"peer": true,
"dependencies": {
"define-data-property": "^1.1.4",
"es-errors": "^1.3.0",
......@@ -8572,13 +8654,72 @@
}
},
"node_modules/side-channel": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
"license": "MIT",
"dependencies": {
"call-bind": "^1.0.0",
"get-intrinsic": "^1.0.2",
"object-inspect": "^1.9.0"
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3",
"side-channel-list": "^1.0.0",
"side-channel-map": "^1.0.1",
"side-channel-weakmap": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-list": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-map": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-weakmap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3",
"side-channel-map": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
......@@ -9357,6 +9498,12 @@
}
}
},
"node_modules/uc.micro": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==",
"license": "MIT"
},
"node_modules/ufo": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz",
......@@ -10166,16 +10313,16 @@
"license": "MIT"
},
"node_modules/vue": {
"version": "3.5.12",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.12.tgz",
"integrity": "sha512-CLVZtXtn2ItBIi/zHZ0Sg1Xkb7+PU32bJJ8Bmy7ts3jxXTcbfsEfBivFYYWz1Hur+lalqGAh65Coin0r+HRUfg==",
"version": "3.5.16",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.16.tgz",
"integrity": "sha512-rjOV2ecxMd5SiAmof2xzh2WxntRcigkX/He4YFJ6WdRvVUrbt6DxC1Iujh10XLl8xCDRDtGKMeO3D+pRQ1PP9w==",
"license": "MIT",
"dependencies": {
"@vue/compiler-dom": "3.5.12",
"@vue/compiler-sfc": "3.5.12",
"@vue/runtime-dom": "3.5.12",
"@vue/server-renderer": "3.5.12",
"@vue/shared": "3.5.12"
"@vue/compiler-dom": "3.5.16",
"@vue/compiler-sfc": "3.5.16",
"@vue/runtime-dom": "3.5.16",
"@vue/server-renderer": "3.5.16",
"@vue/shared": "3.5.16"
},
"peerDependencies": {
"typescript": "*"
......@@ -10295,10 +10442,22 @@
"url": "https://opencollective.com/eslint"
}
},
"node_modules/vue-markdown-render": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/vue-markdown-render/-/vue-markdown-render-2.2.1.tgz",
"integrity": "sha512-XkYnC0PMdbs6Vy6j/gZXSvCuOS0787Se5COwXlepRqiqPiunyCIeTPQAO2XnB4Yl04EOHXwLx5y6IuszMWSgyQ==",
"license": "MIT",
"dependencies": {
"markdown-it": "^13.0.2"
},
"peerDependencies": {
"vue": "^3.3.4"
}
},
"node_modules/vue-router": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.4.5.tgz",
"integrity": "sha512-4fKZygS8cH1yCyuabAXGUAsyi1b2/o/OKgu/RUb+znIYOxPRxdkytJEx+0wGcpBE1pX6vUgh5jwWOKRGvuA/7Q==",
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz",
"integrity": "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==",
"license": "MIT",
"dependencies": {
"@vue/devtools-api": "^6.6.4"
......
......@@ -22,14 +22,16 @@
"@tinymce/tinymce-vue": "^5.0.1",
"@vue-flow/controls": "^1.1.2",
"@vue-flow/core": "^1.39.0",
"@vueuse/components": "^11.2.0",
"@vueuse/core": "^11.2.0",
"axios": "^1.6.8",
"@vueuse/components": "^13.3.0",
"@vueuse/core": "^13.3.0",
"axios": "^1.9.0",
"blueimp-md5": "^2.19.0",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.10",
"echarts": "^5.5.0",
"echarts-wordcloud": "^2.1.0",
"element-plus": "^2.8.7",
"eventsource-parser": "^3.0.2",
"file-saver": "^2.0.5",
"html-to-image": "^1.11.11",
"jspdf": "^2.5.1",
......@@ -37,14 +39,16 @@
"nanoid": "^5.0.7",
"pinia": "^2.2.6",
"scroll-into-view-if-needed": "^3.1.0",
"vue": "^3.5.12",
"vue": "^3.5.16",
"vue-echarts": "^6.6.9",
"vue-router": "^4.4.5",
"vue-markdown-render": "^2.2.1",
"vue-router": "^4.5.1",
"xss": "^1.0.15"
},
"devDependencies": {
"@tsconfig/node20": "^20.1.4",
"@types/blueimp-md5": "^2.18.2",
"@types/crypto-js": "^4.2.2",
"@types/file-saver": "^2.0.7",
"@types/node": "^20.17.6",
"@vitejs/plugin-vue": "^5.1.4",
......
class PCMProcessor extends AudioWorkletProcessor {
process(inputs) {
const input = inputs[0]
if (input.length > 0) {
const inputData = input[0] // 取第一个通道
const inputData16 = new Int16Array(inputData.length)
for (let i = 0; i < inputData.length; i++) {
// PCM 16-bit 转换
inputData16[i] = Math.max(-1, Math.min(1, inputData[i])) * 0x7fff
}
// 通过 port 向主线程发送处理后的数据
this.port.postMessage(inputData16.buffer, [inputData16.buffer])
}
return true // 保持处理器活跃
}
}
registerProcessor('pcm-processor', PCMProcessor)
import httpRequest from '@/utils/axios'
// 获取天工AI的使用详情
export function getAIUsage(params: { marketing_material_id: string }) {
return httpRequest.get('/api/lab/v1/experiment/marketing-ai/ai-usage-detail', { params })
}
// 天工AI-聊天
export function postAIChat(data: {
marketing_material_id: string
context: string
type: number
chart_id: string | null
}) {
return httpRequest.post('/api/lab/v1/experiment/marketing-ai/sky-agents-chat', data)
}
// 天工3.0文字生成图片
export function postGenerateImage(data: {
marketing_material_id: string
context: string
type: number
chart_id: string | null
}) {
return httpRequest.post('/api/lab/v1/experiment/marketing-ai/sky-agent3-generate-image', data)
}
......@@ -180,3 +180,8 @@ export function getProductList(params?: {
export function getAuth() {
return httpRequest.get('/api/lab/v1/experiment/auth/all')
}
// 获取实验旅程中的群组列表
export function getGroupList() {
return httpRequest.get('/api/lab/v1/experiment/itinerary/groups')
}
import httpRequest from '@/utils/axios'
// 获取实验试题列表
export function getExperimentQuestionList() {
return httpRequest.get('/api/lab/v1/student/experiment-question/list')
}
// 学生创建群组页面-获取老师维护的群组
export function getTeacherGroups(params?: { type?: string }) {
return httpRequest.get('/api/lab/v1/experiment/group/teacher-groups', { params })
}
// 学生创建标签页面-获取老师维护的标签
export function getTeacherTags() {
return httpRequest.get('/api/lab/v1/experiment/tag/teacher-tags')
}
// 学生创建营销物料页面-获取老师维护的营销物料
export function getTeacherMaterials(params?: { type?: string }) {
return httpRequest.get('/api/lab/v1/experiment/marketing-ai/teacher-materials', { params })
}
......@@ -96,3 +96,26 @@ textarea:focus {
margin: -8px -15px;
padding: 8px 15px;
}
.markdown-body {
line-height: 1.6;
}
.markdown-body h1 {
font-size: 24px;
}
.markdown-body h2 {
font-size: 20px;
}
.markdown-body h3 {
font-size: 18px;
}
.markdown-body table {
width: 100%;
border-collapse: collapse;
border-spacing: 0;
}
.markdown-body table th,
.markdown-body table td {
padding: 8px 16px;
border: 1px solid #e6e6e6;
}
......@@ -3,7 +3,7 @@ withDefaults(defineProps<{ multiColor?: boolean; name: string; color?: string; w
multiColor: false,
color: '#000000',
w: '30',
h: '30'
h: '30',
})
/*
-type: icon
......@@ -33,14 +33,52 @@ withDefaults(defineProps<{ multiColor?: boolean; name: string; color?: string; w
:height="h"
viewBox="0 0 48 48">
<g>
<path d="M13 22H29C33.4183 22 37 25.5817 37 30V44" stroke="#fff" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"></path>
<circle cx="13" cy="8.94365" r="5" transform="rotate(-90 13 8.94365)" fill="none" stroke="#fff" stroke-width="4"></circle>
<path d="M13 14V43" stroke="#fff" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" fill="none"></path>
<path d="M18 39L13 44L8 39" stroke="#fff" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" fill="none"></path>
<path d="M42 39L37 44L32 39" stroke="#fff" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" fill="none"></path>
<path
d="M13 22H29C33.4183 22 37 25.5817 37 30V44"
stroke="#fff"
stroke-width="4"
stroke-linecap="round"
stroke-linejoin="round"></path>
<circle
cx="13"
cy="8.94365"
r="5"
transform="rotate(-90 13 8.94365)"
fill="none"
stroke="#fff"
stroke-width="4"></circle>
<path
d="M13 14V43"
stroke="#fff"
stroke-width="4"
stroke-linecap="round"
stroke-linejoin="round"
fill="none"></path>
<path
d="M18 39L13 44L8 39"
stroke="#fff"
stroke-width="4"
stroke-linecap="round"
stroke-linejoin="round"
fill="none"></path>
<path
d="M42 39L37 44L32 39"
stroke="#fff"
stroke-width="4"
stroke-linecap="round"
stroke-linejoin="round"
fill="none"></path>
</g>
</svg>
<svg v-if="name == '999'" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" version="1.1" class="icon" viewBox="0 0 512 512" :width="w" :height="h">
<svg
v-if="name == '999'"
xmlns="http://www.w3.org/2000/svg"
xml:space="preserve"
version="1.1"
class="icon"
viewBox="0 0 512 512"
:width="w"
:height="h">
<g>
<g>
<path
......@@ -69,7 +107,10 @@ withDefaults(defineProps<{ multiColor?: boolean; name: string; color?: string; w
:height="h"
viewBox="0 0 624.000000 703.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,703.000000) scale(0.100000,-0.100000)" :fill="multiColor ? '#aa1941' : color" stroke="none">
<g
transform="translate(0.000000,703.000000) scale(0.100000,-0.100000)"
:fill="multiColor ? '#aa1941' : color"
stroke="none">
<path
d="M3055 7007 c-1229 -586 -3025 -1454 -3037 -1469 -17 -19 -18 -130
-18 -2028 0 -1898 1 -2009 17 -2028 22 -25 3072 -1482 3103 -1482 31 0 3081
......@@ -97,8 +138,18 @@ withDefaults(defineProps<{ multiColor?: boolean; name: string; color?: string; w
</g>
</svg>
<svg v-if="name == '100'" version="1.0" xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 86 86" :width="w" :height="h">
<g transform="translate(0.000000,86.000000) scale(0.100000,-0.100000)" :fill="multiColor ? '#19AA20' : color" stroke="none">
<svg
v-if="name == '100'"
version="1.0"
xmlns="http://www.w3.org/2000/svg"
class="icon"
viewBox="0 0 86 86"
:width="w"
:height="h">
<g
transform="translate(0.000000,86.000000) scale(0.100000,-0.100000)"
:fill="multiColor ? '#19AA20' : color"
stroke="none">
<path
d="M351 849 c-173 -34 -311 -177 -342 -357 -31 -172 73 -367 235 -443 128 -60 244 -60 372 0 162 76 266 271 235 443 -32 184 -169 323 -350 358 -61 11 -88 11 -150 -1z m256 -253 c31 -28 38 -41 38 -73 0 -50 -27 -91 -72 -110 -31 -12 -38 -12 -50 -1 -18 19 -9 39 28 60 39 23 38 65 -3 88 -25 13 -31 13 -55 0 -27 -15 -28 -18 -33 -131 -6 -131 -18 -161 -77 -186 -50 -20 -99 -9 -140 32 -27 27 -33 41 -33 75 0 34 6 48 34 76 37 37 82 46 93 17 4 -11 -5 -25 -30 -46 -41 -32 -46 -51 -20 -79 19 -22 62 -23 89 -4 16 12 20 31 24 133 3 65 9 124 14 131 17 27 72 52 114 52 34 0 49 -6 79 -34z" />
</g>
......@@ -483,7 +534,14 @@ withDefaults(defineProps<{ multiColor?: boolean; name: string; color?: string; w
</defs>
<g clip-path="url(#master_svg0_394_02199)">
<g>
<rect x="0" y="0" width="24" height="24" rx="0" :fill="multiColor ? '#FFFFFF' : color" fill-opacity="0.009999999776482582" />
<rect
x="0"
y="0"
width="24"
height="24"
rx="0"
:fill="multiColor ? '#FFFFFF' : color"
fill-opacity="0.009999999776482582" />
</g>
<g>
<path
......@@ -529,7 +587,14 @@ withDefaults(defineProps<{ multiColor?: boolean; name: string; color?: string; w
</defs>
<g clip-path="url(#master_svg0_394_02220)">
<g>
<rect x="0" y="0" width="24" height="24" rx="0" :fill="multiColor ? '#FFFFFF' : color" fill-opacity="0.009999999776482582" />
<rect
x="0"
y="0"
width="24"
height="24"
rx="0"
:fill="multiColor ? '#FFFFFF' : color"
fill-opacity="0.009999999776482582" />
</g>
<g>
<path
......@@ -538,7 +603,10 @@ withDefaults(defineProps<{ multiColor?: boolean; name: string; color?: string; w
fill-opacity="1" />
</g>
<g>
<path d="M7,13.36L7,17L10.6586,17L21,6.65405L17.3475,3L7,13.36Z" :fill="multiColor ? '#FFFFFF' : color" fill-opacity="1" />
<path
d="M7,13.36L7,17L10.6586,17L21,6.65405L17.3475,3L7,13.36Z"
:fill="multiColor ? '#FFFFFF' : color"
fill-opacity="1" />
<path
d="M17.7013,3.353342L7.353765,13.7133L7,13.36L7.5,13.36L7.5,17L7,17L7,16.5L10.6586,16.5L10.6586,17L10.30497,16.6465L20.6464,6.3005700000000004L21,6.65405L20.6464,7.00753L16.99392,3.353476L17.3475,3L17.7013,3.353342ZM16.99378,2.646658Q16.99393,2.646514,16.99407,2.646369Q17.0289,2.611555,17.0699,2.584204Q17.110799999999998,2.556854,17.1563,2.538018Q17.2018,2.519183,17.2501,2.509586Q17.2984,2.4999890000000002,17.3477,2.5Q17.396900000000002,2.5000109999999998,17.4452,2.509629Q17.493499999999997,2.519247,17.539,2.538102Q17.5845,2.556958,17.6254,2.584326Q17.6664,2.611694,17.7012,2.646524L21.3536,6.3005700000000004Q21.3884,6.33539,21.4158,6.376329999999999Q21.4431,6.41727,21.462,6.462759999999999Q21.480800000000002,6.50824,21.4904,6.55653Q21.5,6.60482,21.5,6.65405Q21.5,6.7032799999999995,21.4904,6.75157Q21.480800000000002,6.79985,21.462,6.84534Q21.4431,6.890829999999999,21.4158,6.93177Q21.3884,6.97271,21.3536,7.00753L11.012229999999999,17.3535Q10.9419,17.4238,10.84999,17.4619Q10.75808,17.5,10.6586,17.5L7,17.5Q6.9507543,17.5,6.9024549,17.4904Q6.854155,17.480800000000002,6.808658,17.4619Q6.763161,17.4431,6.722215,17.4157Q6.681269,17.3884,6.646447,17.3536Q6.611625,17.3187,6.584265,17.2778Q6.556906,17.236800000000002,6.53806,17.1913Q6.519215,17.1458,6.509607,17.0975Q6.5,17.0492,6.5,17L6.5,13.36Q6.5,13.2606,6.538003,13.1687Q6.576006,13.0769,6.646235,13.0066L16.99378,2.646658Z"
:fill="multiColor ? '#FFFFFF' : color"
......@@ -598,7 +666,14 @@ withDefaults(defineProps<{ multiColor?: boolean; name: string; color?: string; w
</defs>
<g clip-path="url(#master_svg0_394_02210)">
<g>
<rect x="0" y="0" width="24" height="24" rx="0" :fill="multiColor ? '#FFFFFF' : color" fill-opacity="0.009999999776482582" />
<rect
x="0"
y="0"
width="24"
height="24"
rx="0"
:fill="multiColor ? '#FFFFFF' : color"
fill-opacity="0.009999999776482582" />
</g>
<g>
<path
......@@ -621,7 +696,11 @@ withDefaults(defineProps<{ multiColor?: boolean; name: string; color?: string; w
fill-opacity="1" />
</g>
<g>
<path d="M16,9.5L22,9.5L16,9.5Z" fill-rule="evenodd" :fill="multiColor ? '#FFFFFF' : color" fill-opacity="1" />
<path
d="M16,9.5L22,9.5L16,9.5Z"
fill-rule="evenodd"
:fill="multiColor ? '#FFFFFF' : color"
fill-opacity="1" />
</g>
<g>
<path
......@@ -642,7 +721,14 @@ withDefaults(defineProps<{ multiColor?: boolean; name: string; color?: string; w
viewBox="0 0 24 24">
<g>
<g>
<rect x="0" y="0" width="24" height="24" rx="0" :fill="multiColor ? '#FFFFFF' : color" fill-opacity="0.009999999776482582" />
<rect
x="0"
y="0"
width="24"
height="24"
rx="0"
:fill="multiColor ? '#FFFFFF' : color"
fill-opacity="0.009999999776482582" />
</g>
<g>
<g>
......@@ -706,10 +792,20 @@ withDefaults(defineProps<{ multiColor?: boolean; name: string; color?: string; w
</defs>
<g clip-path="url(#master_svg0_394_02239)">
<g>
<rect x="0" y="0" width="24" height="24" rx="0" :fill="multiColor ? '#FFFFFF' : color" fill-opacity="0.009999999776482582" />
<rect
x="0"
y="0"
width="24"
height="24"
rx="0"
:fill="multiColor ? '#FFFFFF' : color"
fill-opacity="0.009999999776482582" />
</g>
<g>
<path d="M12,2C8.134,2,5,5.134,5,9L5,19L19,19L19,9C19,5.134,15.866,2,12,2Z" :fill="multiColor ? '#FFFFFF' : color" fill-opacity="1" />
<path
d="M12,2C8.134,2,5,5.134,5,9L5,19L19,19L19,9C19,5.134,15.866,2,12,2Z"
:fill="multiColor ? '#FFFFFF' : color"
fill-opacity="1" />
</g>
<g>
<path
......@@ -743,7 +839,14 @@ withDefaults(defineProps<{ multiColor?: boolean; name: string; color?: string; w
</defs>
<g clip-path="url(#master_svg0_394_02264)">
<g>
<rect x="0" y="0" width="24" height="24" rx="0" :fill="multiColor ? '#FFFFFF' : color" fill-opacity="0.009999999776482582" />
<rect
x="0"
y="0"
width="24"
height="24"
rx="0"
:fill="multiColor ? '#FFFFFF' : color"
fill-opacity="0.009999999776482582" />
</g>
<g>
<path
......@@ -784,7 +887,14 @@ withDefaults(defineProps<{ multiColor?: boolean; name: string; color?: string; w
</defs>
<g clip-path="url(#master_svg0_394_02615)">
<g>
<rect x="0" y="0" width="24" height="24" rx="0" :fill="multiColor ? '#FFFFFF' : color" fill-opacity="0.009999999776482582" />
<rect
x="0"
y="0"
width="24"
height="24"
rx="0"
:fill="multiColor ? '#FFFFFF' : color"
fill-opacity="0.009999999776482582" />
</g>
<g>
<path
......@@ -819,7 +929,14 @@ withDefaults(defineProps<{ multiColor?: boolean; name: string; color?: string; w
</defs>
<g clip-path="url(#master_svg0_394_02625)">
<g>
<rect x="0" y="0" width="24" height="24" rx="0" :fill="multiColor ? '#FFFFFF' : color" fill-opacity="0.009999999776482582" />
<rect
x="0"
y="0"
width="24"
height="24"
rx="0"
:fill="multiColor ? '#FFFFFF' : color"
fill-opacity="0.009999999776482582" />
</g>
<g>
<path
......@@ -910,7 +1027,14 @@ withDefaults(defineProps<{ multiColor?: boolean; name: string; color?: string; w
</defs>
<g clip-path="url(#master_svg0_394_02644)">
<g>
<rect x="0" y="0" width="24" height="24" rx="0" :fill="multiColor ? '#FFFFFF' : color" fill-opacity="0.009999999776482582" />
<rect
x="0"
y="0"
width="24"
height="24"
rx="0"
:fill="multiColor ? '#FFFFFF' : color"
fill-opacity="0.009999999776482582" />
</g>
<g>
<path
......@@ -970,7 +1094,14 @@ withDefaults(defineProps<{ multiColor?: boolean; name: string; color?: string; w
fill-opacity="1" />
</g>
</svg>
<svg v-if="name == 'mall'" version="1.1" viewBox="0 0 200 200" style="enable-background: new 0 0 200 200" xml:space="preserve" :width="w" :height="h">
<svg
v-if="name == 'mall'"
version="1.1"
viewBox="0 0 200 200"
style="enable-background: new 0 0 200 200"
xml:space="preserve"
:width="w"
:height="h">
<path
:fill="multiColor ? '#af1c40' : color"
class="st0"
......@@ -980,6 +1111,18 @@ withDefaults(defineProps<{ multiColor?: boolean; name: string; color?: string; w
c2,2,3.1,4.6,3.1,7.5c0,3.3-1.5,6.4-4.2,8.4C65,85.6,81.1,98.4,100,98.4c18.9,0,35-12.8,38.7-30.6c-2.7-2-4.2-5.1-4.2-8.4
c0-2.8,1.1-5.5,3.1-7.5c2-2,4.6-3.1,7.5-3.1s5.5,1.1,7.5,3.1c2,2,3.1,4.6,3.1,7.5C155.6,63.1,153.8,66.3,150.8,68.3L150.8,68.3z" />
</svg>
<svg
v-if="name == 'wechatVideo'"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
:width="w"
:height="h">
<path
d="M956.928 176.028444c-17.080889-29.141333-43.292444-45.155556-73.870222-45.155555h-0.853334c-103.139556 1.009778-207.672889 164.451556-367.587555 433.536-0.881778 1.479111-1.749333 2.944-2.616889 4.408889-0.881778-1.479111-1.749333-2.944-2.616889-4.408889C349.468444 295.338667 244.935111 131.896889 141.795556 130.887111h-0.853334c-30.577778 0-56.789333 16.014222-73.870222 45.155556-44.501333 75.918222-24.974222 234.097778-9.372444 321.635555 24.064 135.025778 67.456 268.515556 113.223111 348.387556 33.991111 59.306667 68.394667 88.135111 105.173333 88.135111 71.921778 0 144.967111-89.898667 235.904-238.108445 90.936889 148.209778 163.982222 238.108444 235.904 238.108445 36.792889 0 71.196444-28.828444 105.173333-88.135111 45.767111-79.872 89.144889-213.361778 113.223111-348.387556 15.601778-87.537778 35.128889-245.717333-9.372444-321.649778zM362.652444 798.762667c-51.214222 63.217778-77.596444 70.044444-86.556444 70.044444-4.352 0-22.599111-10.154667-48.455111-55.267555-86.115556-150.286222-158.222222-512.227556-104.163556-604.444445 7.580444-12.942222 13.980444-12.871111 17.692445-12.828444 16.839111 0.170667 56.504889 15.857778 132.878222 119.082666 57.230222 77.354667 119.196444 181.632 179.128889 282.481778 6.940444 11.676444 13.866667 23.338667 20.778667 34.929778-32.554667 54.314667-72.746667 118.385778-111.303112 166.001778z m433.706667 14.776889c-25.841778 45.112889-44.088889 55.267556-48.455111 55.267555-8.96 0-35.328-6.826667-86.556444-70.044444-38.570667-47.616-78.748444-111.687111-111.303112-166.016 6.897778-11.591111 13.838222-23.253333 20.778667-34.929778 59.932444-100.849778 121.898667-205.127111 179.128889-282.481778 76.373333-103.224889 116.039111-118.912 132.878222-119.082667 3.697778-0.042667 10.097778-0.113778 17.692445 12.828445 54.058667 92.231111-18.048 454.172444-104.163556 604.458667z"
:fill="multiColor ? '#FB9D3B' : color"
p-id="1466"></path>
</svg>
</div>
</template>
......
......@@ -27,6 +27,7 @@ const component = computed(() => {
MAWeibo: markRaw(defineAsyncComponent(() => import('./components/marketingAction/weibo/Index.vue'))),
MADingTalk: markRaw(defineAsyncComponent(() => import('./components/marketingAction/dingtalk/Index.vue'))),
MAAB: markRaw(defineAsyncComponent(() => import('./components/marketingAction/ab/Index.vue'))),
MAWechatVideo: markRaw(defineAsyncComponent(() => import('./components/marketingAction/wechatVideo/Index.vue'))),
MAXiaohongshu: markRaw(defineAsyncComponent(() => import('./components/marketingAction/xiaohongshu/Index.vue'))),
CBAttributeJudgment: markRaw(defineAsyncComponent(() => import('./components/conditionalBranch/attributeJudgment/Index.vue'))),
CBGroupJudgment: markRaw(defineAsyncComponent(() => import('./components/conditionalBranch/groupJudgment/Index.vue'))),
......
......@@ -27,7 +27,7 @@ const list = ref([
type_name: '触发条件',
icon: '13',
component_type: 1,
component_name: 'TCRealTimeTrigger'
component_name: 'TCRealTimeTrigger',
},
{
name: '加入群组',
......@@ -35,7 +35,7 @@ const list = ref([
type_name: '触发条件',
icon: '14',
component_type: 2,
component_name: 'TCJoinGroup'
component_name: 'TCJoinGroup',
},
{
name: '变更属性',
......@@ -43,7 +43,7 @@ const list = ref([
type_name: '触发条件',
icon: '15',
component_type: 3,
component_name: 'TCChangeProps'
component_name: 'TCChangeProps',
},
{
name: '公众号',
......@@ -52,7 +52,7 @@ const list = ref([
icon: '1',
component_type: 4,
component_name: 'TCOffiaccount',
connection_type: 1
connection_type: 1,
},
{
name: '抖音',
......@@ -61,7 +61,7 @@ const list = ref([
icon: '6',
component_type: 5,
component_name: 'TCDouyin',
connection_type: 6
connection_type: 6,
},
{
name: '小红书',
......@@ -70,7 +70,7 @@ const list = ref([
icon: '8',
component_type: 6,
component_name: 'TCXiaohongshu',
connection_type: 8
connection_type: 8,
},
{
name: '微博',
......@@ -79,7 +79,7 @@ const list = ref([
icon: '7',
component_type: 7,
component_name: 'TCWeibo',
connection_type: 7
connection_type: 7,
},
{
name: '自定义',
......@@ -88,7 +88,7 @@ const list = ref([
icon: '12',
component_type: 10,
component_name: 'TCCustom',
connection_type: 12
connection_type: 12,
},
{
name: '小鹅通',
......@@ -97,7 +97,7 @@ const list = ref([
icon: '3',
component_type: 8,
component_name: 'TCXiaoetong',
connection_type: 3
connection_type: 3,
},
{
name: '问卷星',
......@@ -106,9 +106,9 @@ const list = ref([
icon: '4',
component_type: 9,
component_name: 'TCWenjuanxing',
connection_type: 4
}
]
connection_type: 4,
},
],
},
{
name: '营销动作',
......@@ -121,7 +121,7 @@ const list = ref([
icon: '16',
component_type: 99,
component_name: 'MAEndTrip',
color: '#AA1941'
color: '#AA1941',
},
{
name: '加入群组',
......@@ -129,7 +129,7 @@ const list = ref([
type_name: '营销动作',
icon: '14',
component_type: 1,
component_name: 'MAJoinGroup'
component_name: 'MAJoinGroup',
},
{
name: '移除群组',
......@@ -137,7 +137,7 @@ const list = ref([
type_name: '营销动作',
icon: '17',
component_type: 2,
component_name: 'MALeaveGroup'
component_name: 'MALeaveGroup',
},
{
name: '变更属性',
......@@ -145,7 +145,7 @@ const list = ref([
type_name: '营销动作',
icon: '15',
component_type: 3,
component_name: 'MAChangeProps'
component_name: 'MAChangeProps',
},
{
name: '延时处理',
......@@ -153,7 +153,7 @@ const list = ref([
type_name: '营销动作',
icon: '18',
component_type: 4,
component_name: 'MADelayProcess'
component_name: 'MADelayProcess',
},
{
name: '内部通知',
......@@ -161,7 +161,7 @@ const list = ref([
type_name: '营销动作',
icon: '19',
component_type: 5,
component_name: 'MAInternalNotice'
component_name: 'MAInternalNotice',
},
{
name: '短信',
......@@ -170,7 +170,7 @@ const list = ref([
icon: '10',
component_type: 11,
component_name: 'MASMS',
connection_type: 10
connection_type: 10,
},
{
name: '邮件',
......@@ -179,7 +179,7 @@ const list = ref([
icon: '9',
component_type: 10,
component_name: 'MAEmail',
connection_type: 9
connection_type: 9,
},
{
name: '公众号',
......@@ -188,7 +188,7 @@ const list = ref([
icon: '1',
component_type: 6,
component_name: 'MAOffiaccount',
connection_type: 1
connection_type: 1,
},
{
name: '抖音',
......@@ -197,9 +197,17 @@ const list = ref([
icon: '6',
component_type: 7,
component_name: 'MADouyin',
connection_type: 6
connection_type: 6,
},
{
name: '小红书',
type: 2,
type_name: '营销动作',
icon: '8',
component_type: 13,
component_name: 'MAXiaohongshu',
connection_type: 8,
},
{ name: '小红书', type: 2, type_name: '营销动作', icon: '8', component_type: 13, component_name: 'MAXiaohongshu', connection_type: 8 },
{
name: '微博',
type: 2,
......@@ -207,7 +215,7 @@ const list = ref([
icon: '7',
component_type: 8,
component_name: 'MAWeibo',
connection_type: 7
connection_type: 7,
},
{
name: '钉钉',
......@@ -216,7 +224,7 @@ const list = ref([
icon: '2',
component_type: 9,
component_name: 'MADingTalk',
connection_type: 2
connection_type: 2,
},
{
name: 'A/B分配',
......@@ -224,9 +232,17 @@ const list = ref([
type_name: '营销动作',
icon: '101',
component_type: 12,
component_name: 'MAAB'
}
]
component_name: 'MAAB',
},
{
name: '视频号',
type: 2,
type_name: '营销动作',
icon: 'wechatVideo',
component_type: 16,
component_name: 'MAWechatVideo',
},
],
},
{
name: '条件分支',
......@@ -238,7 +254,7 @@ const list = ref([
type_name: '条件分支',
icon: '20',
component_type: 1,
component_name: 'CBAttributeJudgment'
component_name: 'CBAttributeJudgment',
},
{
name: '标签判断',
......@@ -246,7 +262,7 @@ const list = ref([
type_name: '条件分支',
icon: '21',
component_type: 2,
component_name: 'CBLabelJudgment'
component_name: 'CBLabelJudgment',
},
{
name: '群组判断',
......@@ -254,7 +270,7 @@ const list = ref([
type_name: '条件分支',
icon: '22',
component_type: 3,
component_name: 'CBGroupJudgment'
component_name: 'CBGroupJudgment',
},
{
name: '事件判断',
......@@ -262,7 +278,7 @@ const list = ref([
type_name: '条件分支',
icon: '23',
component_type: 5,
component_name: 'CBEventJudgment'
component_name: 'CBEventJudgment',
},
{
name: '时间判断',
......@@ -270,7 +286,7 @@ const list = ref([
type_name: '条件分支',
icon: '24',
component_type: 4,
component_name: 'CBTimeJudgment'
component_name: 'CBTimeJudgment',
},
{
name: '公众号',
......@@ -279,7 +295,7 @@ const list = ref([
icon: '1',
component_type: 6,
component_name: 'CBOffiaccount',
connection_type: 1
connection_type: 1,
},
{
name: '小红书',
......@@ -288,7 +304,7 @@ const list = ref([
icon: '8',
component_type: 7,
component_name: 'CBXiaohongshu',
connection_type: 8
connection_type: 8,
},
{
name: '抖音',
......@@ -297,17 +313,19 @@ const list = ref([
icon: '6',
component_type: 8,
component_name: 'CBDouyin',
connection_type: 6
}
]
}
connection_type: 6,
},
],
},
])
const currentList = computed(() => {
return list.value.map(item => {
return list.value.map((item) => {
return {
...item,
children: item.children.filter(item => (item.connection_type ? connections.value.find(connection => connection.type === item.connection_type) : true))
children: item.children.filter((item) =>
item.connection_type ? connections.value.find((connection) => connection.type === item.connection_type) : true
),
}
})
})
......@@ -326,9 +344,18 @@ const onDragStart = (event: DragEvent, data: any) => {
<dt :style="`background: ${parent.background?.color}`">{{ parent.name }}</dt>
<dd>
<ul>
<li v-for="(item, index) in parent.children" :key="index" :draggable="true" @dragstart="event => onDragStart(event, item)">
<li
v-for="(item, index) in parent.children"
:key="index"
:draggable="true"
@dragstart="(event) => onDragStart(event, item)">
<div class="icon-box">
<Icon class="circle" :color="item.color || parent.background?.color" :name="parent.background?.icon || ''" w="60" h="60"></Icon>
<Icon
class="circle"
:color="item.color || parent.background?.color"
:name="parent.background?.icon || ''"
w="60"
h="60"></Icon>
<Icon class="icon" color="#fff" :name="item.icon" w="24" h="24"></Icon>
</div>
<p>{{ item.name }}</p>
......
<script setup lang="ts">
import ConfigTemplate from '../../ConfigTemplate.vue'
import { useConnection, useMaterial } from '../../../composables/useAllData'
const MaterialPreview = defineAsyncComponent(() => import('@/components/MaterialPreview.vue'))
const props = defineProps<{ node: any }>()
const role = inject('role') as string
const form = reactive({
operate: '',
connection_id: '',
message: '',
material_id: ''
})
watchEffect(() => {
Object.assign(form, props.node.data[role])
})
const operateList = ref([{ label: '推送视频号视频', value: '0' }])
const { materialList } = useMaterial('4')
const { connectionList } = useConnection(10)
const material = computed(() => materialList.value.find(item => item.id === form.material_id))
watch(material, () => {
if (material.value) {
form.message = material.value.content
}
})
</script>
<template>
<ConfigTemplate :model="form" :node="node" :stepNum="2">
<template #default="{ step }: { step: number }">
<template v-if="step === 0">
<el-form-item>
<el-radio-group v-model="form.operate" v-if="step === 0">
<el-radio v-for="item in operateList" :key="item.value" :value="item.value">
{{ item.label }}
</el-radio>
</el-radio-group>
</el-form-item>
</template>
<template v-else-if="step === 1">
<el-form-item>
<el-select placeholder="请选择" style="width: 100%" v-model="form.connection_id">
<el-option v-for="item in connectionList" :key="item.id" :value="item.id" :label="item.name"></el-option>
</el-select>
</el-form-item>
</template>
<template v-else-if="step === 2">
<el-form-item>
<el-select placeholder="请选择资料内容" v-model="form.material_id" style="width: 100%">
<el-option v-for="item in materialList" :key="item.id" :value="item.id" :label="item.name"></el-option>
</el-select>
<MaterialPreview :data="material" v-if="material"></MaterialPreview>
</el-form-item>
<!-- <el-form-item prop="message">
<el-input v-model="form.message" :rows="3" type="textarea" placeholder="请输入短信内容"></el-input>
</el-form-item> -->
</template>
</template>
</ConfigTemplate>
</template>
<script setup lang="ts">
import ConfigViewTemplate from '../../ConfigViewTemplate.vue'
import { useConnection } from '../../../composables/useAllData'
const role = inject('role') as string
defineProps<{ node: any }>()
const { connectionList } = useConnection()
const operateList = [{ label: '推送视频号视频', value: '0' }]
const getConnectionLabel = function (id: string) {
return connectionList.value.find(item => item.id === id)?.name || ''
}
</script>
<template>
<ConfigViewTemplate :node="node">
<el-form-item :label="role === 'student' ? '我的答案' : '学生答案'">
{{ operateList.find(item => item.value === node.data.student?.operate)?.label || '' }}
{{ getConnectionLabel(node.data.student?.connection_id) }}
{{ node.data.student?.message }}
</el-form-item>
<el-form-item label="正确答案">
{{ operateList.find(item => item.value === node.data.teacher?.operate)?.label || '' }}
{{ getConnectionLabel(node.data.teacher?.connection_id) }}
{{ node.data.teacher?.message }}
</el-form-item>
</ConfigViewTemplate>
</template>
<!-- 公众号 -->
<script setup lang="ts">
import NodeTemplate from '../../NodeTemplate.vue'
import Icon from '@/components/ConnectionIcon.vue'
const Config = defineAsyncComponent(() => import('./Config.vue'))
const ConfigView = defineAsyncComponent(() => import('./ConfigView.vue'))
const Rule = defineAsyncComponent(() => import('./Rule.vue'))
const props = defineProps<{ node: any }>()
const action = inject('action') as string
const role = inject('role') as string
const templateType = inject('templateType') as string
// 是否置灰
const isGray = computed(() => {
return templateType === '2' && role === 'student' && action === 'edit' && !props.node.data[role]
})
// 设置
const settingVisible = ref(false)
</script>
<template>
<NodeTemplate :node="node" :connectionType="2" @setting="settingVisible = true">
<div class="node-item">
<Icon class="circle" name="square" :color="isGray ? '#9a9a9a' : '#19AAA5'" w="60" h="60"></Icon>
<Icon class="icon" name="wechatVideo" color="#fff" w="24" h="24"></Icon>
</div>
</NodeTemplate>
<!-- 配置 -->
<Config v-model="settingVisible" :node="node" v-if="settingVisible && action === 'edit'" />
<!-- 查看配置 -->
<ConfigView v-model="settingVisible" :node="node" v-if="settingVisible && action === 'view'" />
<!-- 数据生成规则 -->
<Rule v-model="settingVisible" :node="node" v-if="settingVisible && action === 'rule'" />
</template>
<script setup lang="ts">
import RuleTemplate from '../../RuleTemplate.vue'
defineProps<{ node: any }>()
</script>
<template>
<RuleTemplate :node="node" step>
<template #header-answer>答案</template>
</RuleTemplate>
</template>
<script lang="ts">
export default { name: 'AppMain' }
<script setup lang="ts">
const isIframe = computed(() => {
return window.self !== window.top
})
const padding = computed(() => {
return isIframe.value ? '0 10px' : '10px'
})
</script>
<template>
......@@ -11,7 +17,7 @@ export default { name: 'AppMain' }
<style>
.app-main {
flex: 1;
padding: 10px;
padding: v-bind(padding);
overflow: hidden;
box-sizing: border-box;
}
......
<script setup lang="ts">
import type { GroupRule } from '@/types'
import { PriceTag, Plus, CloseBold } from '@element-plus/icons-vue'
import { useGroup } from '@/composables/useAllData'
const rule = defineModel<GroupRule>({ default: { current_logic_operate: 'and', items: [] } })
const { buttonText = '添加条件' } = defineProps<{
buttonText?: string
}>()
const { groupList } = useGroup()
// 获取逻辑运算符名称
function getLogicalName(value: 'and' | 'or') {
return value === 'or' ? '或' : '且'
}
// 切换逻辑运算符
function toggleOperate(rule: GroupRule) {
rule.current_logic_operate = rule.current_logic_operate === 'or' ? 'and' : 'or'
}
// 添加条件
function handleAdd(items: any[]) {
items.push({ tag_id: '' })
}
// 删除
function handleRemove(items: any[], index: number) {
items.splice(index, 1)
}
</script>
<template>
<el-card shadow="never">
<template #header>
<slot name="header">
<el-button circle color="#567722" :icon="PriceTag"></el-button>
群组满足以下条件
</slot>
</template>
<div class="rule" v-if="rule.items.length">
<div class="rule-operator">
<span @click="toggleOperate(rule)">{{ getLogicalName(rule.current_logic_operate) }}</span>
</div>
<div class="rule-list">
<el-row justify="space-between" class="rule-item" v-for="(item, index) in rule.items" :key="index">
<div>
群组 等于
<el-form-item>
<el-select v-model="item.tag_id" style="width: 300px">
<el-option
v-for="option in groupList"
:key="option.id"
:label="option.name"
:value="option.id"></el-option>
</el-select>
</el-form-item>
</div>
<el-button text :icon="CloseBold" @click="handleRemove(rule.items, index)"></el-button>
</el-row>
</div>
</div>
<el-button text :icon="Plus" @click="handleAdd(rule.items)">{{ buttonText }}</el-button>
</el-card>
</template>
<style src="@/assets/styles/rule.scss"></style>
......@@ -7,6 +7,10 @@ import { useRfmRes } from '@/composables/useRFMData'
// const tagRule = ref(inject('tagRule') as TagRule)
const tagRule = defineModel<TagRule>({ default: { current_logic_operate: 'and', items: [] } })
const { buttonText = '添加条件' } = defineProps<{
buttonText?: string
}>()
const { tagList } = useTag()
const { rfmResList } = useRfmRes()
......@@ -31,11 +35,11 @@ function handleRemove(items: any[], index: number) {
}
function showRfm(id: string) {
return tagList.value.find(item => item.id === id)?.label == '4'
return tagList.value.find((item) => item.id === id)?.label == '4'
}
function handleRfmChange(rfmKey: string, item: any) {
const found = rfmResList.value.find(item => item.frm_key === rfmKey)
const found = rfmResList.value.find((item) => item.frm_key === rfmKey)
item.rfm_value = found?.frm_value
}
// const a = [
......@@ -53,8 +57,10 @@ function handleRfmChange(rfmKey: string, item: any) {
<template>
<el-card shadow="never">
<template #header>
<slot name="header">
<el-button circle color="#567722" :icon="PriceTag"></el-button>
标签满足以下条件
</slot>
</template>
<div class="rule" v-if="tagRule.items.length">
<div class="rule-operator">
......@@ -66,11 +72,23 @@ function handleRfmChange(rfmKey: string, item: any) {
标签 等于
<el-form-item>
<el-select v-model="item.tag_id" style="width: 300px">
<el-option v-for="option in tagList" :key="option.id" :label="option.name" :value="option.id"></el-option>
<el-option
v-for="option in tagList"
:key="option.id"
:label="option.name"
:value="option.id"></el-option>
</el-select>
<template v-if="showRfm(item.tag_id)">
<el-select v-model="item.rfm_key" @change="value => handleRfmChange(value, item)" style="width: 400px; margin: 0 10px">
<el-option v-for="item in rfmResList" :key="item.frm_key" :label="item.frm_value" :value="item.frm_key" style="height: auto">
<el-select
v-model="item.rfm_key"
@change="(value) => handleRfmChange(value, item)"
style="width: 400px; margin: 0 10px">
<el-option
v-for="item in rfmResList"
:key="item.frm_key"
:label="item.frm_value"
:value="item.frm_key"
style="height: auto">
<div style="line-height: 24px; padding: 5px 0">
<p>
<span style="float: left">{{ item.frm_value }}</span>
......@@ -78,7 +96,9 @@ function handleRfmChange(rfmKey: string, item: any) {
{{ item.frm_extend_info.customer_marketing_strategy }}
</span>
</p>
<p style="clear: both; color: var(--el-text-color-secondary); font-size: 13px">{{ item.frm_extend_info.label_desc }}</p>
<p style="clear: both; color: var(--el-text-color-secondary); font-size: 13px">
{{ item.frm_extend_info.label_desc }}
</p>
</div>
</el-option>
</el-select>
......@@ -90,7 +110,10 @@ function handleRfmChange(rfmKey: string, item: any) {
<el-table-column prop="frm_extend_info.m" label="M值" width="52" />
<el-table-column prop="frm_value" label="标签值" width="110" />
<el-table-column prop="frm_extend_info.label_desc" label="标签说明" />
<el-table-column prop="frm_extend_info.customer_marketing_strategy" label="客户营销策略" width="110" />
<el-table-column
prop="frm_extend_info.customer_marketing_strategy"
label="客户营销策略"
width="110" />
</el-table>
<template #reference>
<el-icon><QuestionFilled /></el-icon>
......@@ -103,7 +126,7 @@ function handleRfmChange(rfmKey: string, item: any) {
</el-row>
</div>
</div>
<el-button text :icon="Plus" @click="handleAdd(tagRule.items)">添加条件</el-button>
<el-button text :icon="Plus" @click="handleAdd(tagRule.items)">{{ buttonText }}</el-button>
</el-card>
</template>
......
......@@ -2,6 +2,7 @@
import { QuestionFilled } from '@element-plus/icons-vue'
import { useUserAttr, useMetaEvent, useUserAttrRange } from '@/composables/useRFMData'
import { searchMetaMemberAttrs } from '@/api/base'
import { cloneDeep } from 'lodash-es'
defineProps<{ label: string }>()
const form = defineModel<any>()
......@@ -9,12 +10,12 @@ const form = defineModel<any>()
const ruleList = [
{ label: '属性值平均法', value: '101', basis: ['1'] },
{ label: '属性值分类法', value: '102', basis: ['1'] },
{ label: '事件发生次数平均法', value: '201', basis: ['2'] }
{ label: '事件发生次数平均法', value: '201', basis: ['2'] },
]
const defaultLevel = [
{ level: '高', value: '' },
{ level: '低', value: '' }
{ level: '低', value: '' },
]
const defaultScore = [
......@@ -22,7 +23,7 @@ const defaultScore = [
{ score: 2, min_value: '', max_value: '' },
{ score: 3, min_value: '', max_value: '' },
{ score: 4, min_value: '', max_value: '' },
{ score: 5, min_value: '', max_value: '' }
{ score: 5, min_value: '', max_value: '' },
]
onMounted(() => {
......@@ -33,8 +34,8 @@ onMounted(() => {
event_id: '-1',
attr_id: '',
attr_type: '',
config: [...defaultScore],
extend_config: { default_score_config: { switch: false, score: undefined } }
config: cloneDeep(defaultScore),
extend_config: { default_score_config: { switch: false, score: undefined } },
},
form.value
)
......@@ -45,7 +46,7 @@ const { metaEventList, fetchMetaEventList } = useMetaEvent()
const { userAttrRange, fetchUserAttrRange } = useUserAttrRange()
const currentRuleList = computed(() => {
return ruleList.filter(item => item.basis.includes(form.value.basis))
return ruleList.filter((item) => item.basis.includes(form.value.basis))
})
const currentMetaEventList = computed(() => {
......@@ -58,21 +59,31 @@ function handleBasisChange(value: any) {
} else {
form.value.rule = '201'
}
form.value.attr_id = ''
// 清空数据
Object.assign(
form.value,
cloneDeep({
event_id: '-1',
attr_id: '',
attr_type: '',
config: defaultScore,
extend_config: { default_score_config: { switch: false, score: undefined } },
})
)
handleRuleChange(form.value.rule)
}
function handleRuleChange(value: any) {
if (value === '102') {
form.value.config = [...defaultLevel]
form.value.config = cloneDeep(defaultLevel)
} else {
form.value.config = [...defaultScore]
form.value.config = cloneDeep(defaultScore)
}
form.value.attr_id = ''
}
function handleAttrChange(value: any) {
form.value.attr_type = userAttrList.value.find(item => item.id === value)?.type
form.value.attr_type = userAttrList.value.find((item) => item.id === value)?.type
}
const options = ref<{ label: string; value: string }[]>([])
......@@ -81,7 +92,7 @@ async function remoteMethod(search: string = '') {
options.value = []
if (form.value.attr_id) {
loading.value = true
await searchMetaMemberAttrs({ search, member_meta_id: form.value.attr_id, per_page: 100 }).then(res => {
await searchMetaMemberAttrs({ search, member_meta_id: form.value.attr_id, per_page: 100 }).then((res) => {
options.value = res.data.list.map((item: any) => {
return { label: item.attr_value, value: item.attr_value }
})
......@@ -96,7 +107,7 @@ function querySearch(queryString: string, cb: any) {
watch(
() => form.value.attr_id,
attrId => {
(attrId) => {
if (form.value.rule === '102') {
remoteMethod()
} else {
......@@ -120,7 +131,7 @@ const a = [
{ id: '002', label: '1500' },
{ id: '003', label: '3000' },
{ id: '004', label: '2200' },
{ id: '005', label: '1800' }
{ id: '005', label: '1800' },
]
const defaultOptions = Array.from({ length: 5 }).map((_, index) => ({ value: index + 1, label: index + 1 }))
......@@ -147,11 +158,23 @@ const defaultOptions = Array.from({ length: 5 }).map((_, index) => ({ value: ind
</el-radio-group>
<p style="margin-left: 10px">计算规则:</p>
<el-select v-model="form.rule" style="width: 170px" @change="handleRuleChange">
<el-option v-for="item in currentRuleList" :key="item.value" :label="item.label" :value="item.value"></el-option>
<el-option
v-for="item in currentRuleList"
:key="item.value"
:label="item.label"
:value="item.value"></el-option>
</el-select>
<div class="rfm-tips">
<el-popover popper-class="rfm-popover" placement="right" title="属性值平均法" :width="400" trigger="hover" v-if="form.rule == '101'">
<p>用于计算选中属性的平均值,通过对选定的字段中的所有记录进行数值相加,然后除以记录的数量来计算的。主要针对“数字”和“整数”两种字段类型。</p>
<el-popover
popper-class="rfm-popover"
placement="right"
title="属性值平均法"
:width="400"
trigger="hover"
v-if="form.rule == '101'">
<p>
用于计算选中属性的平均值,通过对选定的字段中的所有记录进行数值相加,然后除以记录的数量来计算的。主要针对“数字”和“整数”两种字段类型。
</p>
<p>举例:</p>
<el-table :data="a" border>
<el-table-column prop="id" label="用户ID" />
......@@ -162,13 +185,25 @@ const defaultOptions = Array.from({ length: 5 }).map((_, index) => ({ value: ind
<el-icon><QuestionFilled /></el-icon>
</template>
</el-popover>
<el-popover popper-class="rfm-popover" placement="right" title="属性值分类法" :width="400" trigger="hover" v-if="form.rule == '102'">
<el-popover
popper-class="rfm-popover"
placement="right"
title="属性值分类法"
:width="400"
trigger="hover"
v-if="form.rule == '102'">
<p>将数据的属性值按照一定的规则或特性进行分类,本系统中分了“高”和“低”两类。主要针对“字符串”的字段类型。</p>
<template #reference>
<el-icon><QuestionFilled /></el-icon>
</template>
</el-popover>
<el-popover popper-class="rfm-popover" placement="right" title="事件发生次数平均法" :width="400" trigger="hover" v-if="form.rule == '201'">
<el-popover
popper-class="rfm-popover"
placement="right"
title="事件发生次数平均法"
:width="400"
trigger="hover"
v-if="form.rule == '201'">
<p>分析事件发生频率的方法,即通过计算用户事件发生的平均次数。</p>
<template #reference>
<el-icon><QuestionFilled /></el-icon>
......@@ -177,19 +212,37 @@ const defaultOptions = Array.from({ length: 5 }).map((_, index) => ({ value: ind
</div>
<el-select v-model="form.event_id" placeholder="选择事件" style="width: 160px" v-if="form.basis === '2'">
<el-option v-for="item in currentMetaEventList" :key="item.event_id" :label="item.event_name" :value="item.event_id"></el-option>
<el-option
v-for="item in currentMetaEventList"
:key="item.event_id"
:label="item.event_name"
:value="item.event_id"></el-option>
</el-select>
<el-select v-model="form.attr_id" placeholder="选择属性" style="width: 160px" @change="handleAttrChange" v-else>
<el-option v-for="item in userAttrList" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
<div style="flex: 1; display: flex; justify-content: space-between" v-if="form.basis == 1 && form.rule != '102' && form.attr_id">
<p>最小值:{{ userAttrRange.min }}<br />最大值:{{ userAttrRange.max }}<br />"0"值数量:{{ userAttrRange.zero_count }}</p>
<p>平均值:{{ userAttrRange.avg }}<br />中位数:{{ userAttrRange.median }}<br />中位数(不含0):{{ userAttrRange.no_zero_median }}</p>
<div
style="flex: 1; display: flex; justify-content: space-between"
v-if="form.basis == 1 && form.rule != '102' && form.attr_id">
<p>
最小值:{{ userAttrRange.min }}<br />最大值:{{ userAttrRange.max }}<br />"0"值数量:{{
userAttrRange.zero_count
}}
</p>
<p>
平均值:{{ userAttrRange.avg }}<br />中位数:{{ userAttrRange.median }}<br />中位数(不含0):{{
userAttrRange.no_zero_median
}}
</p>
</div>
</div>
<div class="rfm-header-extra" v-if="form.rule === '101' && form.extend_config">
<p>未匹配数据默认赋值</p>
<el-select-v2 v-model="form.extend_config.default_score_config.score" :options="defaultOptions" style="width: 100px; margin: 0 10px" clearable />
<el-select-v2
v-model="form.extend_config.default_score_config.score"
:options="defaultOptions"
style="width: 100px; margin: 0 10px"
clearable />
<el-switch v-model="form.extend_config.default_score_config.switch"></el-switch>
</div>
<div class="rfm-body">
......
import md5 from 'blueimp-md5'
import { useStorage } from '@vueuse/core'
import { fetchEventSource } from '@fortaine/fetch-event-source'
import axios from 'axios'
import { getAIUsage, postGenerateImage } from '@/api/ai'
import { ElMessage } from 'element-plus'
export function useAI(config) {
// AI 配置列表
const options = [
{ label: '文心一言', value: 'yiyan' },
{ label: 'DeepSeek', value: 'deepseek' },
{ label: '通义千问', value: 'qwen' },
{ label: '天工', value: 'tiangong' },
]
const ai = useStorage('ai', 'tiangong')
const messages = ref([])
const isLoading = ref(false)
async function post(data) {
isLoading.value = true
try {
switch (ai.value) {
case 'yiyan':
await yiyan(data)
break
case 'deepseek':
await siliconflow(data)
break
case 'qwen':
await qwen(data)
break
case 'tiangong':
await tiangong(data)
break
default:
throw new Error('未找到对应的 AI 配置')
}
} catch (err) {
console.error('AI 请求失败:', err)
} finally {
isLoading.value = false
}
}
// 文心一言
async function yiyan(data) {
// 获取token
const getAccessToken = async () => {
const AK = 'wY7bvMpkWeZbDVq9w3EDvpjU'
const SK = 'XJwpiJWxs5HXkOtbo6tQrvYPZFJAWdAy'
const resp = await axios.post(
'/api/qianfan/oauth/2.0/token?grant_type=client_credentials&client_id=' + AK + '&client_secret=' + SK
)
return resp.data.access_token
}
const resp = await axios.post(
'/api/qianfan/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/eb-instant?access_token=' + (await getAccessToken()),
{
messages: [{ role: 'user', content: data.content }],
}
)
messages.value.push({ role: 'assistant', content: resp.data.result.replaceAll('\n', '<br/>') })
}
// DeepSeek
// async function deepseek(data) {
// const apiKey = 'sk-f1a6f0a7013241de8393cb2cb108e777'
// const resp = await axios.post(
// '/api/deepseek/chat/completions',
// {
// model: 'deepseek-chat',
// messages: [{ role: 'user', content: data.content }],
// },
// {
// headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
// }
// )
// if (resp.data) {
// const [choice = {}] = resp.data.choices
// messages.value.push({ role: 'assistant', content: choice.message.content.replaceAll('\n', '<br/>') })
// }
// }
// 硅基流动
async function siliconflow(data) {
const apiKey = 'sk-bivnwauskdbvpspvmdorrgkrpwlyfxbfcezqsfsevowzubdj'
const resp = await axios.post(
'/api/siliconflow/v1/chat/completions',
{
model: 'deepseek-ai/DeepSeek-V3',
messages: [{ role: 'user', content: data.content }],
},
{
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
}
)
if (resp.data) {
const [choice = {}] = resp.data.choices
messages.value.push({ role: 'assistant', content: choice.message.content.replaceAll('\n', '<br/>') })
}
}
// 通义千问
async function qwen(data) {
const apiKey = 'sk-afd0fcdb53bf4058b2068b8548820150'
const resp = await axios.post(
'/api/qwen/compatible-mode/v1/chat/completions',
{
model: 'qwen-max',
messages: [{ role: 'user', content: data.content }],
},
{
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
}
)
if (resp.data) {
const [choice = {}] = resp.data.choices
messages.value.push({ role: 'assistant', content: choice.message.content.replaceAll('\n', '<br/>') })
}
}
// 天工
async function tiangong(data) {
const appKey = 'a8701b73637562d33a53c668a90ee3be'
const appSecret = 'e191593f486bb88a39c634f46926762dddc97b9082e192af'
const timestamp = Math.floor(Date.now() / 1000)
const sign = md5(`${appKey}${appSecret}${timestamp}`)
return await fetchEventSource('/api/tiangong/sky-saas-writing/api/v1/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json', app_key: appKey, sign, timestamp, stream: 'true' },
body: JSON.stringify({
chat_history: [{ role: 'user', content: data.content }],
stream_resp_type: 'update',
}),
async onopen(response) {
console.log(response)
if (response.ok) {
return response
} else {
throw response
}
},
onmessage(res) {
console.log(res.data)
const message = JSON.parse(res.data)
if (message.type !== 1) return
const messageId = message.conversation_id
const messageIndex = messages.value.findIndex((message) => message.id === messageId)
const content = message?.arguments?.[0]?.messages?.[0]?.text || ''
if (messageIndex === -1) {
messages.value.push({ id: messageId, role: 'assistant', content })
} else {
messages.value[messageIndex].content = content
}
isLoading.value = false
},
onerror(err) {
isLoading.value = false
throw err
},
})
}
const usages = ref({
chart_count: 0,
ai_creation_count: 0,
ai_polish_count: 0,
ai_expand_count: 0,
ai_refresh_count: 0,
chart_max_count: 20,
ai_creation_max_count: 5,
ai_polish_max_count: 5,
ai_expand_max_count: 5,
ai_refresh_max_count: 5,
})
async function fetchUsages() {
const res = await getAIUsage(config)
usages.value = res.data.detail
}
async function generateText(data) {
isLoading.value = true
const docAction = {
2: 'write',
3: 'rewrite',
4: 'expand',
5: 'rewrite',
7: 'abbreviate',
8: 'summary',
}
const params = {
content: data.content,
doc_action: docAction[data.type],
full_text: !!(data.type === 2),
}
await fetchEventSource('/api/lab/v1/experiment/marketing-ai/sky-agent3-chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...config,
...data,
api_type: parseInt(data.type) === 1 ? 1 : 2,
context: data.content,
params: params,
}),
async onopen(response) {
if (response.ok) {
return
} else {
throw response
}
},
onmessage(res) {
const message = JSON.parse(res.data)
// 聊天返回内容
if (data.type === '1') {
if (message.code === 0) {
ElMessage.error(message.message)
return
}
const conversationId = message.conversation_id
const messageIndex = messages.value.findIndex((session) => session.conversationId === conversationId)
const content = message?.arguments?.reduce((a, b) => {
a = b?.messages[0]?.text || ''
return a
}, '')
if (messageIndex === -1) {
messages.value.push({ conversationId, role: 'assistant', content, input: data.context })
} else {
if (content) {
messages.value[messageIndex].content = content
}
}
} else {
// 按钮功能返回内容
const requestId = message.request_id
const messageIndex = messages.value.findIndex((session) => session.conversationId === requestId)
if (messageIndex === -1) {
messages.value.push({
conversationId: requestId,
role: 'assistant',
content: message.data?.text || '',
input: data.context,
})
} else {
messages.value[messageIndex].content = message.data?.text
}
}
isLoading.value = false
},
onclose() {
fetchUsages()
isLoading.value = false
},
onerror(err) {
console.log(err)
isLoading.value = false
throw err
},
})
}
// 生成图片
async function generateImage(data) {
isLoading.value = true
try {
const res = await postGenerateImage({ ...config, ...data })
if (res.data.detail.image_url) {
messages.value.push({ type: 'image', role: 'assistant', ...res.data.detail })
} else {
ElMessage.error(res.data.detail.failure_reason)
}
fetchUsages()
} catch (error) {
console.log(error)
}
isLoading.value = false
}
return { ai, options, post, messages, isLoading, usages, fetchUsages, generateText, generateImage }
}
import { getMetaUserAttrList, getMetaEventList, getTagList, getConnectionList, getUserList } from '@/api/base'
import {
getMetaUserAttrList,
getMetaEventList,
getTagList,
getConnectionList,
getUserList,
getGroupList,
} from '@/api/base'
import { useMapStore } from '@/stores/map'
// 用户属性类型
......@@ -36,6 +43,12 @@ export interface ConnectionType {
config_attributes: any
}
// 群组类型
export interface GroupType {
id: string
name: string
}
// 所有用户属性
const userAttrList = ref<AttrType[]>([])
const userAttrLoading = ref(false)
......@@ -100,8 +113,9 @@ export function useConnection() {
const connectionType = useMapStore().getMapValuesByKey('experiment_connection_type')
await getConnectionList().then((res: any) => {
connectionList.value = res.data.items.map((item: any) => {
const connection = connectionType.find(type => type.value == item.type)
const attrs = typeof item.config_attributes === 'string' ? JSON.parse(item.config_attributes) : item.config_attributes
const connection = connectionType.find((type) => type.value == item.type)
const attrs =
typeof item.config_attributes === 'string' ? JSON.parse(item.config_attributes) : item.config_attributes
const name = Array.isArray(attrs) ? attrs.find((item: any) => item.prop === 'name')?.value : attrs.name
return { ...item, config_attributes: attrs, name, type_name: connection?.label || item.type }
})
......@@ -142,3 +156,21 @@ export function useUser() {
})
return { fetchUserList, userList, userValue }
}
// 所有群组
const groupList = ref<GroupType[]>([])
export function useGroup() {
function fetchGroupList() {
getGroupList().then((res: any) => {
groupList.value = res.data.items
})
}
onMounted(() => {
if (!groupList.value?.length) fetchGroupList()
})
function getGroup(groupId: string) {
return groupList.value.find((item) => item.id === groupId)
}
return { fetchGroupList, groupList, getGroup }
}
......@@ -4,9 +4,10 @@ export function useChat() {
const messages = ref([])
const isLoading = ref(false)
async function post(message, isReplace = true) {
function post(message, isReplace = true) {
isLoading.value = true
await fetchEventSource('/api/lab/v1/experiment/qwen/chat', {
return new Promise((resolve, reject) => {
fetchEventSource('/api/lab/v1/experiment/qwen/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model: 'qwen-long', messages: [message] }),
......@@ -14,6 +15,8 @@ export function useChat() {
if (response.ok) {
return response
} else {
isLoading.value = false
reject(response)
throw response
}
},
......@@ -21,6 +24,8 @@ export function useChat() {
console.log(res.data)
if (res.data === '[DONE]') {
isLoading.value = false
resolve(messages.value.at(-1))
return
}
try {
const message = JSON.parse(res.data)
......@@ -42,13 +47,16 @@ export function useChat() {
} catch (error) {
console.log(error)
isLoading.value = false
reject(error)
}
},
onerror(err) {
isLoading.value = false
reject(err)
throw err
},
})
})
}
return { messages, post, isLoading }
......
import type { Ref } from 'vue'
import { getExperimentQuestionList, getTeacherGroups, getTeacherTags, getTeacherMaterials } from '@/api/question'
import { useUserStore } from '@/stores/user'
type QuesitonItem = {
experiment_id: string
id: string
type: string
}
type OptionItem = {
id: string
name: string
}
type MaterialItem = {
id: string
name: string
type: string
content: ''
}
export function useQuestion(questionType: string | Ref<string>, type?: string) {
questionType = shallowRef(questionType)
const useStore = useUserStore()
// 试题列表
const questionList = ref<QuesitonItem[]>([])
const fetchQuestionList = async () => {
const res = await getExperimentQuestionList()
questionList.value = res.data.items
}
const hasQuestion = computed(() => {
return !!questionList.value.find((item) => item.type === questionType.value)
})
onMounted(async () => {
if (useStore.role?.id === 1) {
await fetchQuestionList()
}
if (hasQuestion.value) {
if (questionType.value == '202') await fetchTeacherTagList()
if (questionType.value == '301' || questionType.value == '302') await fetchTeacherGroupList()
if (['401', '402', '403', '404', '405', '406', '407', '408'].includes(questionType.value)) {
await fetchTeacherMaterialList()
}
}
})
const teacherGroupList = ref<OptionItem[]>([])
const fetchTeacherGroupList = async () => {
const res = await getTeacherGroups({ type })
teacherGroupList.value = res.data.items
}
const teacherTagList = ref<OptionItem[]>([])
const fetchTeacherTagList = async () => {
const res = await getTeacherTags()
teacherTagList.value = res.data.items
}
const teacherMaterialAllList = ref<MaterialItem[]>([])
const teacherMaterialList = computed(() => {
const questionTypes: any = {
'401': 1,
'402': 2,
'403': 3,
'404': 4,
'405': 5,
'406': 6,
'407': 7,
'408': 8,
}
return teacherMaterialAllList.value.filter((item) => item.type == questionTypes[questionType.value])
})
const fetchTeacherMaterialList = async () => {
const res = await getTeacherMaterials()
teacherMaterialAllList.value = res.data.items
}
return {
hasQuestion,
questionList,
fetchQuestionList,
teacherGroupList,
fetchTeacherGroupList,
teacherTagList,
fetchTeacherTagList,
teacherMaterialList,
fetchTeacherMaterialList,
}
}
......@@ -34,3 +34,8 @@ export function getMemberMetaAttrs() {
export function getUserTags(params: { sso_id: string; limit: number }) {
return httpRequest.get('/api/lab/v1/experiment/analyse/user-tags', { params })
}
// AI分析与总结
export function getAISummary() {
return httpRequest.get('/api/lab/v1/experiment/member/ai-all-person', { adapter: 'fetch', responseType: 'stream' })
}
<script setup>
import { getAISummary } from '../api'
import { aiStreamParse } from '@/utils/parse'
import VueMarkdown from 'vue-markdown-render'
const content = ref('')
const isLoading = ref(false)
async function fetchAI() {
isLoading.value = true
const stream = await getAISummary()
aiStreamParse(stream, (json, messageContent) => {
content.value += messageContent
})
isLoading.value = false
}
onMounted(() => {
fetchAI()
})
</script>
<template>
<el-dialog title="AI用户整体画像分析与建议">
<div v-loading="isLoading">
<VueMarkdown :source="content" class="markdown-body" />
</div>
<template #footer>
<el-row justify="center">
<el-button round @click="$emit('update:modelValue', false)">关闭</el-button>
</el-row>
</template>
</el-dialog>
</template>
......@@ -8,6 +8,9 @@ import { useMapStore } from '@/stores/map'
import { getNameByValue } from '@/utils/dictionary'
import * as api from '../api'
const AISummaryDialog = defineAsyncComponent(() => import('../components/AISummaryDialog.vue'))
const aiDialogVisible = ref(false)
const connectionTypeList = useMapStore().getMapValuesByKey('experiment_connection_type')
const statusList = useMapStore().getMapValuesByKey('system_status')
......@@ -57,7 +60,7 @@ const genderOption = computed(() => {
return {
grid: { left: '60', right: '60' },
tooltip: {
trigger: 'item'
trigger: 'item',
},
yAxis: {
data: ['男性', '女性'],
......@@ -67,15 +70,20 @@ const genderOption = computed(() => {
axisLabel: {
formatter: function (value, index) {
const total = parseInt(man.total) + parseInt(woman.total)
return value + '\n' + (index === 0 ? ((man.total / total) * 100).toFixed(1) : ((woman.total / total) * 100).toFixed(1)) + '%'
}
}
return (
value +
'\n' +
(index === 0 ? ((man.total / total) * 100).toFixed(1) : ((woman.total / total) * 100).toFixed(1)) +
'%'
)
},
},
},
xAxis: {
splitLine: { show: false },
axisLabel: { show: false },
axisTick: { show: false },
axisLine: { show: false }
axisLine: { show: false },
},
series: [
{
......@@ -86,10 +94,10 @@ const genderOption = computed(() => {
symbolMargin: 10,
data: [
{ value: man.total, symbol: manIcon, itemStyle: { color: '#767aca' } },
{ value: woman.total, symbol: womanIcon, itemStyle: { color: '#d26080' } }
]
}
]
{ value: woman.total, symbol: womanIcon, itemStyle: { color: '#d26080' } },
],
},
],
}
})
......@@ -101,7 +109,7 @@ async function fetchConnections() {
loading2.value = true
try {
const res = await api.getMemberConnections({ sso_id: userValue.value })
connection.value = res.data.items.map(item => {
connection.value = res.data.items.map((item) => {
return { ...item, group_name: getNameByValue(item.group_name, connectionTypeList) }
})
} finally {
......@@ -113,24 +121,24 @@ const connectionOption = computed(() => {
return {
grid: { left: '5%', top: '10%', right: '5%', bottom: '5%', containLabel: true },
tooltip: {
trigger: 'axis'
trigger: 'axis',
},
xAxis: {
type: 'category',
axisLabel: { interval: 0 },
data: connection.value.map(item => item.group_name)
data: connection.value.map((item) => item.group_name),
},
yAxis: {
type: 'value'
type: 'value',
},
series: [
{
name: '数据',
type: 'bar',
label: { show: true, position: 'top' },
data: connection.value.map(item => item.total)
}
]
data: connection.value.map((item) => item.total),
},
],
}
})
......@@ -142,7 +150,7 @@ async function fetchStatus() {
loading3.value = true
try {
const res = await api.getMemberStatus({ sso_id: userValue.value })
status.value = res.data.items.map(item => {
status.value = res.data.items.map((item) => {
return { name: getNameByValue(item.group_name, statusList), value: item.total }
})
} finally {
......@@ -155,7 +163,7 @@ const statusOption = computed(() => {
grid: { left: '5%', top: '10%', right: '5%', bottom: '5%', containLabel: true },
tooltip: {
trigger: 'item',
formatter: '{b}: {c}<br />{d}%'
formatter: '{b}: {c}<br />{d}%',
},
series: [
{
......@@ -163,9 +171,9 @@ const statusOption = computed(() => {
label: { formatter: '{b}\n{d}%' },
itemStyle: { borderRadius: 6 },
radius: [0, '70%'],
data: status.value
}
]
data: status.value,
},
],
}
})
</script>
......@@ -173,15 +181,28 @@ const statusOption = computed(() => {
<template>
<AppCard title="用户分析">
<el-form inline label-suffix=":">
<div style="display: flex; justify-content: space-between">
<div>
<el-form-item label="实验名称">{{ info?.name }}</el-form-item>
<el-form-item label="请选择学生/老师">
<el-select v-model="userValue" filterable>
<el-option v-for="item in userList" :label="item.name" :value="item.sso_id" :key="item.sso_id"></el-option>
<el-option
v-for="item in userList"
:label="item.name"
:value="item.sso_id"
:key="item.sso_id"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" :icon="DataLine" :loading="loading" @click="handleStart">分析</el-button>
</el-form-item>
</div>
<div>
<el-form-item>
<el-button type="primary" @click="aiDialogVisible = true">AI分析与总结</el-button>
</el-form-item>
</div>
</div>
<el-divider style="margin: 10px 0" />
<el-form-item label="用户总数">
<b class="total">{{ userTotal }}</b>
......@@ -199,6 +220,7 @@ const statusOption = computed(() => {
<UserChart :ssoId="userValue" />
</div>
</AppCard>
<AISummaryDialog v-model="aiDialogVisible" v-if="aiDialogVisible"></AISummaryDialog>
</template>
<style lang="scss" scoped>
......
......@@ -5,6 +5,7 @@ import type { DetailsProp, PlatformItem, ConfigAttribute } from '../types'
const StepOne = defineAsyncComponent(() => import('../components/StepOne.vue'))
const StepTwo = defineAsyncComponent(() => import('../components/StepTwo.vue'))
const WechatVideo = defineAsyncComponent(() => import('../components/WechatVideo.vue'))
const props = defineProps<{ data?: DetailsProp }>()
......@@ -18,7 +19,7 @@ const platformList: PlatformItem[] = [
type: '1',
type_name: '公众号',
config_attributes: [
{ label: '连接名称', prop: 'name', value: '' }
{ label: '连接名称', prop: 'name', value: '' },
// { label: '公众号类型', prop: 'type', value: '' },
// { label: '授权方昵称', prop: 'nikeName', value: '' }
],
......@@ -30,7 +31,7 @@ const platformList: PlatformItem[] = [
return false
}
return true
}
},
},
{
type: '2',
......@@ -39,8 +40,8 @@ const platformList: PlatformItem[] = [
{ label: '连接名称', prop: 'name', value: '钉钉' },
{ label: 'AgentId', prop: 'agentId', value: '8441459810' },
{ label: 'AppKey', prop: 'appKey', value: 'dingigucs3beqlotpf24' },
{ label: 'AppSecret', prop: 'appSecret', value: '6dNRvuOzvX_xq5N9tFdjepdf3FeooN25yUZK6ammDbPUVq9sfdXD-sKUg' }
]
{ label: 'AppSecret', prop: 'appSecret', value: '6dNRvuOzvX_xq5N9tFdjepdf3FeooN25yUZK6ammDbPUVq9sfdXD-sKUg' },
],
},
{
type: '3',
......@@ -49,8 +50,8 @@ const platformList: PlatformItem[] = [
{ label: '连接名称', prop: 'name', value: '小鹅通' },
{ label: 'app_id', prop: 'app_id', value: 'appc4bolgenF58' },
{ label: 'client_id', prop: 'client_id', value: '_5e7f809dd6317_qSMuUoAi?type=2SDK' },
{ label: 'secret_key', prop: 'secret_key', value: 'xiao_5ac1dd24803ae_GtfAOxiS1pdf3FeooN2huhu92WRE52S-SkOh' }
]
{ label: 'secret_key', prop: 'secret_key', value: 'xiao_5ac1dd24803ae_GtfAOxiS1pdf3FeooN2huhu92WRE52S-SkOh' },
],
},
{
type: '4',
......@@ -58,8 +59,8 @@ const platformList: PlatformItem[] = [
config_attributes: [
{ label: '连接名称', prop: 'name', value: '问卷星' },
{ label: 'AppKey', prop: 'appKey', value: '82286f9c5114dc2bda214cd9567dodc' },
{ label: 'AppSecret', prop: 'appSecret', value: 'pages/wjxqList/wjxqList?activityId= P251FBP' }
]
{ label: 'AppSecret', prop: 'appSecret', value: 'pages/wjxqList/wjxqList?activityId= P251FBP' },
],
},
{ type: '5', type_name: '今日头条', config_attributes: [{ label: '连接名称', prop: 'name', value: '今日头条' }] },
{
......@@ -73,11 +74,11 @@ const platformList: PlatformItem[] = [
label: '网站应用简介',
prop: 'dyInput3',
value:
'不仅是下载抖音应用程序的官方渠道,也是一个展示抖音最新动态、功能更新和推广活动的平台。用户可以通过官网了解抖音的特色功能、查看热门视频、参与互动活动,以及获取帮助和教程等。官网还为创作者和企业提供了一个展示空间,让他们了解如何利用抖音平台进行内容创作、品牌推广和电子商务等。'
'不仅是下载抖音应用程序的官方渠道,也是一个展示抖音最新动态、功能更新和推广活动的平台。用户可以通过官网了解抖音的特色功能、查看热门视频、参与互动活动,以及获取帮助和教程等。官网还为创作者和企业提供了一个展示空间,让他们了解如何利用抖音平台进行内容创作、品牌推广和电子商务等。',
},
{ label: '应用官网', prop: 'dyInput4', value: 'https://www.douyin.com' },
{ label: '联系人姓名', prop: 'dyInput5', value: '清控紫荆(北京)教育股份有限公司' }
]
{ label: '联系人姓名', prop: 'dyInput5', value: '清控紫荆(北京)教育股份有限公司' },
],
},
{
type: '7',
......@@ -85,8 +86,8 @@ const platformList: PlatformItem[] = [
config_attributes: [
{ label: '连接名称', prop: 'name', value: '微博' },
{ label: 'AppKey', prop: 'appKey', value: '1206405345' },
{ label: 'AppSecret', prop: 'appSecret', value: '6a6095e113cd28fde6e14c7b7145c5c5' }
]
{ label: 'AppSecret', prop: 'appSecret', value: '6a6095e113cd28fde6e14c7b7145c5c5' },
],
},
{
type: '8',
......@@ -94,8 +95,8 @@ const platformList: PlatformItem[] = [
config_attributes: [
{ label: '连接名称', prop: 'name', value: '小红书' },
{ label: 'AppKey', prop: 'appKey', value: '6c1dd8dd64d074d56124c751f6bc240b' },
{ label: 'AppSecret', prop: 'appSecret', value: '' }
]
{ label: 'AppSecret', prop: 'appSecret', value: '' },
],
},
{
type: '9',
......@@ -105,8 +106,8 @@ const platformList: PlatformItem[] = [
{ label: 'client_id', prop: 'client_id', value: 'swanzhong' },
{ label: 'client_secret', prop: 'client_secret', value: '563a8c6a89d2368194c1c7889c508b34' },
{ label: 'token URL', prop: 'token', value: 'openapi/user/get' },
{ label: 'API URL', prop: 'apiUrl', value: 'openapi/user/check' }
]
{ label: 'API URL', prop: 'apiUrl', value: 'openapi/user/check' },
],
},
{
type: '10',
......@@ -116,8 +117,8 @@ const platformList: PlatformItem[] = [
{ label: 'client_id', prop: 'client_id', value: 'FbFgN2of-mlc' },
{ label: 'SdkAppId', prop: 'sdkAppId', value: 'CV3X1%2FJG7mdNZm03l9puvwPAktmfw1aj8XvBb6sm696MqoW57' },
{ label: 'token URL', prop: 'token', value: 'https://oauth-login.cloud.ali.com/oauth2/v3/token' },
{ label: 'API URL', prop: 'apiUrl', value: 'oauth2v3wPAktm' }
]
{ label: 'API URL', prop: 'apiUrl', value: 'oauth2v3wPAktm' },
],
},
{ type_name: '内部消息', type: '11', config_attributes: [{ label: '连接名称', prop: 'name', value: '内部消息' }] },
{
......@@ -126,14 +127,14 @@ const platformList: PlatformItem[] = [
config_attributes: [
{ label: '连接名称', prop: 'name', value: '自定义' },
{ label: 'APP类型', prop: 'appType', value: '自定义' },
{ label: 'AppId', prop: 'appId', value: 'Custom App ID' }
]
{ label: 'AppId', prop: 'appId', value: 'Custom App ID' },
],
},
{
type: '13',
type_name: '紫荆表单',
icon: '99',
config_attributes: [{ label: '连接名称', prop: 'name', value: '紫荆表单' }]
config_attributes: [{ label: '连接名称', prop: 'name', value: '紫荆表单' }],
},
{
type: '14',
......@@ -148,14 +149,32 @@ const platformList: PlatformItem[] = [
return false
}
return true
}
},
},
{
icon: 'mall',
type: '15',
type_name: '紫荆商城',
config_attributes: [{ label: '连接名称', prop: 'name', value: '紫荆商城' }]
config_attributes: [{ label: '连接名称', prop: 'name', value: '紫荆商城' }],
},
{
icon: 'wechatVideo',
type: '16',
type_name: '视频号',
config_attributes: [{ label: '连接名称', prop: 'name', value: '视频号' }],
async onBeforePrev(stepActive) {
if (stepActive == 2) {
return wechatVideoRef.value?.handlePrev()
}
return true
},
async onBeforeNext(stepActive) {
if (stepActive == 2) {
return wechatVideoRef.value?.handleNext()
}
return true
},
},
]
// 请求参数
......@@ -172,7 +191,7 @@ watchEffect(() => {
Object.assign(params, { type, config_attributes: attributes })
})
const selectedItem = computed(() => platformList.find(item => item.type === params.type))
const selectedItem = computed(() => platformList.find((item) => item.type === params.type))
const defaultStepActive = props.data?.id ? 2 : 1
const stepActive = ref(defaultStepActive)
......@@ -183,13 +202,17 @@ function handleChange(data: PlatformItem) {
}
// 上一步
function handlePrev() {
async function handlePrev() {
if (selectedItem.value?.onBeforePrev) {
const next = await selectedItem.value?.onBeforePrev(stepActive.value, selectedItem.value)
if (!next) return
}
stepActive.value--
}
// 下一步
async function handleNext() {
const isEmpty = params.config_attributes.find(item => item.value === '')
const isEmpty = params.config_attributes.find((item) => item.value === '')
if (isEmpty && stepActive.value === 2) {
ElMessage('请填写完整')
return
......@@ -223,6 +246,8 @@ async function handleSave() {
emit('update')
emit('update:modelValue', false)
}
const wechatVideoRef = ref<any>(null)
</script>
<template>
......@@ -239,7 +264,13 @@ async function handleSave() {
</el-tab-pane>
<!-- 第二步 -->
<el-tab-pane disabled lazy label="配置连接信息" :name="2">
<StepTwo :platform="selectedItem" v-model="params.config_attributes" v-if="selectedItem"></StepTwo>
<template v-if="selectedItem">
<WechatVideo
ref="wechatVideoRef"
v-model="params.config_attributes"
v-if="params.type === '16'"></WechatVideo>
<StepTwo :platform="selectedItem" v-model="params.config_attributes" v-else></StepTwo>
</template>
</el-tab-pane>
<el-tab-pane disabled lazy label="测试连接" :name="3">
<el-button type="primary" @click="handleTest">测试连接</el-button>
......
......@@ -50,6 +50,7 @@ const edit = function () {
const iconMap: Record<string, string> = {
'13': '99',
'14': '100',
'16': 'wechatVideo'
}
const generateUserData = function () {
......
<script setup>
defineProps(['data'])
const step = ref(1)
const stepLength = 4
function handlePrev() {
if (step.value === 1) return true
step.value--
}
function handleNext() {
if (step.value === stepLength) return true
step.value++
}
defineExpose({ step, handlePrev, handleNext })
</script>
<template>
<div class="wechat-video">
<div class="wechat-video-step" v-show="step === 1">
<h6 class="wechat-video-step__title">第1步:请点击下图示例的“视频号”</h6>
<img src="/wechat_video/1.png" />
</div>
<div class="wechat-video-step" v-show="step === 2">
<h6 class="wechat-video-step__title">第2步:请点击下图示例的</h6>
<img src="/wechat_video/2.png" />
</div>
<div class="wechat-video-step" v-show="step === 3">
<h6 class="wechat-video-step__title">第3步:请点击下图示例的“发表视频”</h6>
<img src="/wechat_video/3.png" />
</div>
<div class="wechat-video-step" v-show="step === 4">
<h6 class="wechat-video-step__title">第4步:上传头像,维护视频号名称,勾选相关规范和隐私说明,点击“创建”</h6>
<img src="/wechat_video/4.png" />
</div>
</div>
</template>
<style lang="scss">
.wechat-video {
.wechat-video-step {
margin-bottom: 20px;
img {
display: block;
width: 375px;
margin: 0 auto;
}
&__title {
margin-bottom: 20px;
}
}
}
</style>
......@@ -22,6 +22,7 @@ export interface PlatformItem {
type_name: string
icon?: string
config_attributes?: ConfigAttribute[]
onBeforePrev?: (index: number, data: PlatformItem) => Promise<boolean> | boolean
onBeforeNext?: (index: number, data: PlatformItem) => Promise<boolean> | boolean
}
......@@ -60,7 +61,7 @@ export interface OtherFields {
rule: any
}
export interface StudentFollow{
export interface StudentFollow {
follow_flag: string
logs: any[] | undefined
connect_id: string
......
......@@ -27,7 +27,7 @@ const attributes = computed<ConfigAttribute[]>(() => {
})
function getAttributeValueByProp(prop: string) {
return attributes.value.find(item => item.prop === prop)?.value || ''
return attributes.value.find((item) => item.prop === prop)?.value || ''
}
const platformList = [
......@@ -43,10 +43,10 @@ const platformList = [
async onClick() {
await asyncOfficialAccountInfo({
connection_id: connectId.value,
appid: getAttributeValueByProp('appid')
appid: getAttributeValueByProp('appid'),
})
ElMessage.success('重新获取公众号信息成功')
}
},
},
{
title: '重新获取公众号粉丝',
......@@ -58,25 +58,25 @@ const platformList = [
)
await asyncOfficialAccountUsers({
connection_id: connectId.value,
appid: getAttributeValueByProp('appid')
appid: getAttributeValueByProp('appid'),
})
ElMessage.success(`已经开始同步公众号“${nikeName}”的粉丝,完成时间取决于您公众号的粉丝数量,请耐心等待。`)
}
}
},
},
// { title: '获取公众号统计数据', msg: '' },
// { title: '获取图文群发评论数据', msg: '' }
]
],
},
{
title: '2.向公众号同步',
children: [{ title: '批量为用户打标签', msg: '' }]
children: [{ title: '批量为用户打标签', msg: '' }],
},
{
title: '3.其他设置',
children: [
{ title: '客服会话设置', msg: '' },
{ title: '删除GDPR数据', msg: '' }
]
{ title: '删除GDPR数据', msg: '' },
],
},
{
title: '4.客户旅程能力:触发条件',
......@@ -93,8 +93,8 @@ const platformList = [
{ title: '用户领取卡券', msg: '' },
{ title: '用户转赠卡券', msg: '' },
{ title: '用户核销卡券', msg: '' },
{ title: '用户删除卡券', msg: '' }
]
{ title: '用户删除卡券', msg: '' },
],
},
{
title: '5.客户旅程能力:执行动作',
......@@ -109,10 +109,10 @@ const platformList = [
{ title: '发送图片', msg: '' },
{ title: '发送微信语音', msg: '' },
{ title: '发送微信视频', msg: '' },
{ title: '发送卡券', msg: '' }
]
}
]
{ title: '发送卡券', msg: '' },
],
},
],
},
{
type: 2,
......@@ -120,25 +120,25 @@ const platformList = [
data: [
{
title: '1.从钉钉同步',
children: [{ title: '获取用户信息', msg: '' }]
children: [{ title: '获取用户信息', msg: '' }],
},
{
title: '2.向钉钉同步',
children: [{ title: '导入用户信息', msg: '' }]
children: [{ title: '导入用户信息', msg: '' }],
},
{
title: '3.其他设置',
children: [{ title: '访问钉钉官方网站', msg: '' }]
children: [{ title: '访问钉钉官方网站', msg: '' }],
},
{
title: '4.客户旅程能力:触发条件',
children: []
children: [],
},
{
title: '5.客户旅程能力:执行动作',
children: [{ title: '发送消息', msg: '' }]
}
]
children: [{ title: '发送消息', msg: '' }],
},
],
},
{
type: 3,
......@@ -149,12 +149,12 @@ const platformList = [
children: [
{ title: '消息推送', msg: '' },
{ title: '同步历史用户', msg: '' },
{ title: '同步直播学员签到事件', msg: '' }
]
{ title: '同步直播学员签到事件', msg: '' },
],
},
{
title: '2.向小鹅通同步',
children: []
children: [],
},
{
title: '3.其他设置',
......@@ -162,18 +162,18 @@ const platformList = [
{ title: '访问小鹅通官方网站', msg: '' },
{ title: '配置用户属性字段映射', msg: '' },
{ title: '配置事件属性字段映射', msg: '' },
{ title: '配置信息采集表单字段映射', msg: '' }
]
{ title: '配置信息采集表单字段映射', msg: '' },
],
},
{
title: '4.客户旅程能力:触发条件',
children: [{ title: '新用户注册', msg: '' }]
children: [{ title: '新用户注册', msg: '' }],
},
{
title: '5.客户旅程能力:执行动作',
children: []
}
]
children: [],
},
],
},
{
type: 4,
......@@ -181,28 +181,28 @@ const platformList = [
data: [
{
title: '1.从问卷星同步',
children: [{ title: '同步填写者信息', msg: '' }]
children: [{ title: '同步填写者信息', msg: '' }],
},
{
title: '2.向问卷星同步',
children: []
children: [],
},
{
title: '3.其他设置',
children: [
{ title: '访问问卷星官方网站', msg: '' },
{ title: '管理表单字段映射', msg: '' }
]
{ title: '管理表单字段映射', msg: '' },
],
},
{
title: '4.客户旅程能力:触发条件',
children: [{ title: '提交表单', msg: '' }]
children: [{ title: '提交表单', msg: '' }],
},
{
title: '5.客户旅程能力:执行动作',
children: []
}
]
children: [],
},
],
},
{
type: 5,
......@@ -212,32 +212,32 @@ const platformList = [
title: '1.从今日头条同步',
children: [
{ title: '同步头条推广基础数据', msg: '' },
{ title: '同步头条推广展点消数据', msg: '' }
]
{ title: '同步头条推广展点消数据', msg: '' },
],
},
{
title: '2.向今日头条同步',
children: [
{ title: '飞鱼线索表格导入', msg: '' },
{ title: '设置返点系数', msg: '' }
]
{ title: '设置返点系数', msg: '' },
],
},
{
title: '3.其他设置',
children: [
{ title: '访问今日头条投放管理平台', msg: '' },
{ title: '广告效果分析', msg: '' }
]
{ title: '广告效果分析', msg: '' },
],
},
{
title: '4.客户旅程能力:触发条件',
children: []
children: [],
},
{
title: '5.客户旅程能力:执行动作',
children: []
}
]
children: [],
},
],
},
{
type: 6,
......@@ -251,16 +251,16 @@ const platformList = [
{ title: '获取用户点赞数', msg: '' },
{ title: '获取用户分享数', msg: '' },
{ title: '获取用户主页访问数', msg: '' },
{ title: '获取用户信息', msg: '' }
]
{ title: '获取用户信息', msg: '' },
],
},
{
title: '2.向抖音同步',
children: []
children: [],
},
{
title: '3.其他设置',
children: []
children: [],
},
{
title: '4.客户旅程能力:触发条件',
......@@ -268,18 +268,18 @@ const platformList = [
{ title: '用户发送文本私信', msg: '' },
{ title: '用户发送表情私信', msg: '' },
{ title: '用户发送卡片私信', msg: '' },
{ title: '在主页Tab提交预约', msg: '' }
]
{ title: '在主页Tab提交预约', msg: '' },
],
},
{
title: '5.客户旅程能力:执行动作',
children: [
{ title: '向用户发送文本私信', msg: '' },
{ title: '向用户发送图片私信', msg: '' },
{ title: '向用户发送视频私信', msg: '' }
]
}
]
{ title: '向用户发送视频私信', msg: '' },
],
},
],
},
{
type: 7,
......@@ -290,32 +290,32 @@ const platformList = [
children: [
{ title: '获取用户信息', msg: '' },
{ title: '获取用户私信数', msg: '' },
{ title: '获取用户关注数', msg: '' }
]
{ title: '获取用户关注数', msg: '' },
],
},
{
title: '2.向微博同步',
children: []
children: [],
},
{
title: '3.其他设置',
children: []
children: [],
},
{
title: '4.客户旅程能力:触发条件',
children: [
{ title: '关注微博', msg: '' },
{ title: '发送私信', msg: '' }
]
{ title: '发送私信', msg: '' },
],
},
{
title: '5.客户旅程能力:执行动作',
children: [
{ title: '发送私信', msg: '' },
{ title: '发送图片', msg: '' }
]
}
]
{ title: '发送图片', msg: '' },
],
},
],
},
{
type: 8,
......@@ -331,16 +331,16 @@ const platformList = [
{ title: '获取笔记收藏数', msg: '' },
{ title: '获取笔记评论数', msg: '' },
{ title: '获取笔记点赞数', msg: '' },
{ title: '获取笔记转发数', msg: '' }
]
{ title: '获取笔记转发数', msg: '' },
],
},
{
title: '2.向小红书同步',
children: []
children: [],
},
{
title: '3.其他设置',
children: []
children: [],
},
{
title: '4.客户旅程能力:触发条件',
......@@ -350,14 +350,14 @@ const platformList = [
{ title: '主页被收藏', msg: '' },
{ title: '笔记被点赞', msg: '' },
{ title: '笔记被收藏', msg: '' },
{ title: '笔记被转发', msg: '' }
]
{ title: '笔记被转发', msg: '' },
],
},
{
title: '5.客户旅程能力:执行动作',
children: []
}
]
children: [],
},
],
},
{
type: 9,
......@@ -365,25 +365,25 @@ const platformList = [
data: [
{
title: '1.从邮件同步',
children: []
children: [],
},
{
title: '2.向邮件同步',
children: []
children: [],
},
{
title: '3.其他设置',
children: []
children: [],
},
{
title: '4.客户旅程能力:触发条件',
children: []
children: [],
},
{
title: '5.客户旅程能力:执行动作',
children: [{ title: '发送邮件', msg: '' }]
}
]
children: [{ title: '发送邮件', msg: '' }],
},
],
},
{
type: 10,
......@@ -391,28 +391,28 @@ const platformList = [
data: [
{
title: '1.从短信同步',
children: []
children: [],
},
{
title: '2.向短信同步',
children: []
children: [],
},
{
title: '3.其他设置',
children: [
{ title: '管理签名', msg: '' },
{ title: '管理模板', msg: '' }
]
{ title: '管理模板', msg: '' },
],
},
{
title: '4.客户旅程能力:触发条件',
children: []
children: [],
},
{
title: '5.客户旅程能力:执行动作',
children: [{ title: '发送短信', msg: '' }]
}
]
children: [{ title: '发送短信', msg: '' }],
},
],
},
{
type: 11,
......@@ -420,25 +420,25 @@ const platformList = [
data: [
{
title: '1.从内部消息同步',
children: []
children: [],
},
{
title: '2.向内部消息同步',
children: []
children: [],
},
{
title: '3.其他设置',
children: []
children: [],
},
{
title: '4.客户旅程能力:触发条件',
children: []
children: [],
},
{
title: '5.客户旅程能力:执行动作',
children: [{ title: '发送内部消息', msg: '' }]
}
]
children: [{ title: '发送内部消息', msg: '' }],
},
],
},
{
type: 12,
......@@ -451,20 +451,20 @@ const platformList = [
{ title: '安卓移动应用接入', msg: '' },
{ title: 'JS SDK接入', msg: '' },
{ title: '同步链接用户', msg: '' },
{ title: '同步链接事件', msg: '' }
]
{ title: '同步链接事件', msg: '' },
],
},
{
title: '2.向自定义同步',
children: []
children: [],
},
{
title: '3.其他设置',
children: [
{ title: '自定义图元', msg: '' },
{ title: '事件元数据', msg: '' },
{ title: '删除用户', msg: '' }
]
{ title: '删除用户', msg: '' },
],
},
{
title: '4.客户旅程能力:触发条件',
......@@ -476,15 +476,15 @@ const platformList = [
{ title: '展示APP页面', msg: '' },
{ title: '点击APP元素', msg: '' },
{ title: '退出APP页面', msg: '' },
{ title: '离开网页', msg: '' }
]
{ title: '离开网页', msg: '' },
],
},
{
title: '5.客户旅程能力:执行动作',
children: []
}
]
children: [],
},
],
},
{
type: 13,
......@@ -498,22 +498,22 @@ const platformList = [
onClick() {
surveyKingDialogVisible.value = true
// window.open('https://surveyking.ezijing.com/')
}
},
},
{
title: '管理表单自动映射',
onClick() {
router.push({ path: 'view/surveyking', query: route.query })
}
}
]
}
]
},
},
],
},
],
},
{
type: 14,
type_name: '小程序',
data: []
data: [],
},
{
type: 15,
......@@ -528,16 +528,39 @@ const platformList = [
window.open(
`https://mall-h5-web.ezijing.com?id=${route.query.id}&experiment_id=${route.query.experiment_id}`
)
}
}
]
}
]
}
},
},
],
},
],
},
{
type: 16,
type_name: '视频号',
data: [
{
title: '1.从视频号同步',
children: [],
},
{
title: '2.向视频号同步',
children: [],
},
{
title: '3.客户旅程能力:触发条件',
children: [],
},
{
title: '5.客户旅程能力:执行动作',
children: [{ title: '推送视频号视频', msg: '' }],
},
],
},
]
const platformDataList = computed(() => {
return platformList.find(item => item.type == detail.value?.type)?.data || []
return platformList.find((item) => item.type == detail.value?.type)?.data || []
})
function handleClick(item: any) {
......@@ -546,7 +569,8 @@ function handleClick(item: any) {
const iconMap: Record<string, string> = {
'13': '99',
'14': '100'
'14': '100',
'16': 'wechatVideo',
}
const surveyKingDialogVisible = ref<boolean>(false)
</script>
......@@ -559,8 +583,7 @@ const surveyKingDialogVisible = ref<boolean>(false)
:multiColor="true"
:name="detail.type === '15' ? 'mall' : iconMap[detail.type] || detail.type"
w="50"
h="50"
/>
h="50" />
</div>
<div class="view-info_content">
<p>连接名称:{{ getAttributeValueByProp('name') || detail.type_name }}</p>
......@@ -589,8 +612,7 @@ const surveyKingDialogVisible = ref<boolean>(false)
v-model="surveyKingDialogVisible"
:account="getAttributeValueByProp('account')"
:password="getAttributeValueByProp('password')"
v-if="detail?.type === '13'"
></SurveyKingDialog>
v-if="detail?.type === '13'"></SurveyKingDialog>
</template>
<style lang="scss">
......
......@@ -10,7 +10,7 @@ import {
updateDynamicGroup,
getGroupInfo,
createRFMGroup,
updateRFMGroup
updateRFMGroup,
} from '../api'
import UserRule from '@/components/rule/UserRule.vue'
import EventRule from '@/components/rule/EventRule.vue'
......@@ -18,6 +18,7 @@ import LabelRule from '@/components/rule/LabelRule.vue'
import UserActionRule from '@/components/rule/UserActionRule.vue'
import RFMRule from '@/components/rule/RFMRule.vue'
import { pick, merge } from 'lodash-es'
import { useQuestion } from '@/composables/useQuestion'
interface Props {
data: Partial<Group>
......@@ -47,7 +48,8 @@ const form: any = reactive({
event_attr_rule: { current_logic_operate: 'and', items: [] },
tag_rule: { current_logic_operate: 'and', items: [] },
user_action_rule: { current_logic_operate: 'and', items: [] },
rules: { R: {}, F: {}, M: {} }
rules: { R: {}, F: {}, M: {} },
teacher_id: '',
})
function genRuleData(data: any) {
......@@ -56,7 +58,7 @@ function genRuleData(data: any) {
}
function fetchInfo() {
if (!props.data.id) return
getGroupInfo({ id: props.data.id }).then(res => {
getGroupInfo({ id: props.data.id }).then((res) => {
const { detail } = res.data
const user_attr_rule = genRuleData(detail.user_attr_rule)
const event_attr_rule = genRuleData(detail.event_attr_rule)
......@@ -82,7 +84,7 @@ function fetchInfo() {
event_attr_rule: { ...event_attr_rule, items: eventRuleItems },
user_action_rule,
tag_rule: { ...tag_rule, items: tagRuleItems },
update_rule: { type: 1, info: 1 }
update_rule: { type: 1, info: 1 },
})
})
}
......@@ -90,9 +92,14 @@ function fetchInfo() {
watchEffect(() => fetchInfo())
const rules = ref<FormRules>({
name: [{ required: true, message: '请输入群组名称' }]
name: [{ required: true, message: '请输入群组名称' }],
teacher_id: [{ required: true, message: '请选择老师标签' }],
})
const questionType = props.data.type === '1' ? '301' : '302'
const { hasQuestion, teacherGroupList } = useQuestion(questionType, props.data.type)
// 提交
function handleSubmit() {
formRef.value?.validate().then(() => (isUpdate.value ? handleUpdate() : handleCreate()))
......@@ -122,7 +129,7 @@ async function handleCreate() {
user_attr_rule: JSON.stringify([form.user_attr_rule]),
event_attr_rule: JSON.stringify([form.event_attr_rule]),
tag_rule: JSON.stringify([{ ...form.tag_rule, ...tagRule }]),
user_action_rule: JSON.stringify(form.user_action_rule)
user_action_rule: JSON.stringify(form.user_action_rule),
},
[
'name',
......@@ -132,7 +139,8 @@ async function handleCreate() {
'user_attr_rule',
'event_attr_rule',
'tag_rule',
'user_action_rule'
'user_action_rule',
'teacher_id',
]
)
await createDynamicGroup(params)
......@@ -142,9 +150,9 @@ async function handleCreate() {
{
...form,
update_rule: JSON.stringify(form.update_rule),
rules: JSON.stringify(form.rules)
rules: JSON.stringify(form.rules),
},
['name', 'status', 'update_status', 'update_rule', 'rules']
['name', 'status', 'update_status', 'update_rule', 'rules', 'teacher_id']
)
await createRFMGroup(params)
}
......@@ -156,7 +164,7 @@ async function handleCreate() {
async function handleUpdate() {
if (props.data.type === '1') {
// 静态群组
const params = pick(form, ['id', 'name', 'status'])
const params = pick(form, ['id', 'name', 'status', 'teacher_id'])
await updateStaticGroup(params)
} else if (props.data.type === '2') {
// 动态群组
......@@ -186,7 +194,7 @@ async function handleUpdate() {
user_attr_rule: JSON.stringify([{ ...form.user_attr_rule, items: attrRuleItems }]),
event_attr_rule: JSON.stringify([{ ...form.event_attr_rule, items: eventRuleItems }]),
tag_rule: JSON.stringify([{ ...form.tag_rule, ...tagRule }]),
user_action_rule: JSON.stringify(form.user_action_rule)
user_action_rule: JSON.stringify(form.user_action_rule),
},
[
'id',
......@@ -197,7 +205,8 @@ async function handleUpdate() {
'user_attr_rule',
'event_attr_rule',
'tag_rule',
'user_action_rule'
'user_action_rule',
'teacher_id',
]
)
await updateDynamicGroup(params)
......@@ -209,7 +218,8 @@ async function handleUpdate() {
'status',
'update_status',
'update_rule',
'rules'
'rules',
'teacher_id',
])
await updateRFMGroup(params)
}
......@@ -221,7 +231,7 @@ async function handleUpdate() {
<template>
<el-dialog :title="title" :close-on-click-modal="false" width="980px" @closed="$emit('update:modelValue', false)">
<el-form ref="formRef" :model="form" :rules="rules" label-suffix=":" label-width="100px">
<el-form ref="formRef" :model="form" :rules="rules" label-suffix=":" label-width="110px">
<el-form-item label="群组名称" prop="name">
<el-input v-model="form.name" placeholder="请输入" />
</el-form-item>
......@@ -269,6 +279,11 @@ async function handleUpdate() {
</div>
</el-form-item>
</template>
<el-form-item label="匹配老师标签" prop="teacher_id" v-if="hasQuestion">
<el-select v-model="form.teacher_id" style="width: 100%">
<el-option v-for="item in teacherGroupList" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-switch v-model="form.status" active-text="生效" active-value="1" inactive-text="失效" inactive-value="0" />
</el-form-item>
......
import httpRequest from '@/utils/axios'
import type { LabelTypeListRequest, LabelTypeCreateRequest, LabelTypeUpdateRequest, LabelListRequest, LabelCreateRequest, LabelUpdateRequest } from './types'
import type {
LabelTypeListRequest,
LabelTypeCreateRequest,
LabelTypeUpdateRequest,
LabelListRequest,
LabelCreateRequest,
LabelUpdateRequest,
} from './types'
// 获取标签类型列表
export function getLabelTypeList(params?: LabelTypeListRequest) {
......@@ -70,3 +77,8 @@ export function updateLabelRule(data: { id: string; rules: string }) {
export function getLabelMembers(params: { tag_id: string }) {
return httpRequest.get('/api/lab/v1/experiment/tag/bda-statistics-users', { params })
}
// AI分析与总结
export function getAISummary() {
return httpRequest.get('/api/lab/v1/experiment/member/ai-tag', { responseType: 'stream', adapter: 'fetch' })
}
<script setup>
import { getAISummary } from '../api'
import { aiStreamParse } from '@/utils/parse'
import VueMarkdown from 'vue-markdown-render'
const content = ref('')
const isLoading = ref(false)
async function fetchAI() {
isLoading.value = true
const stream = await getAISummary()
aiStreamParse(stream, (json, messageContent) => {
content.value += messageContent
})
isLoading.value = false
}
onMounted(() => {
fetchAI()
})
</script>
<template>
<el-dialog title="AI标签建议">
<div v-loading="isLoading">
<VueMarkdown :source="content" class="markdown-body" />
</div>
<template #footer>
<el-row justify="center">
<el-button round @click="$emit('update:modelValue', false)">关闭</el-button>
</el-row>
</template>
</el-dialog>
</template>
......@@ -6,6 +6,7 @@ import { updateStatusRuleList, dateUnitList, weekList, labelList } from '@/utils
import { createLabel, updateLabel } from '../api'
import { useLabelType } from '../composables/useLabelType'
import { pick } from 'lodash-es'
import { useQuestion } from '@/composables/useQuestion'
const props = defineProps<{
data?: Label
......@@ -30,7 +31,8 @@ const form = reactive({
update_rule: { type: 1, info: 1 },
status: '1',
label: '',
weight: 0
weight: 0,
teacher_id: '',
})
watchEffect(() => {
if (props.data) {
......@@ -48,9 +50,12 @@ const rules = ref<FormRules>({
name: [{ required: true, message: '请输入标签名称' }],
label: [{ required: true, message: '请选择标签类型' }],
type_id: [{ required: true, message: '请选择标签目录' }],
update_status: [{ required: true, message: '请选择更新评率' }]
update_status: [{ required: true, message: '请选择更新评率' }],
teacher_id: [{ required: true, message: '请选择老师标签' }],
})
const { hasQuestion, teacherTagList } = useQuestion('202')
// 提交
function handleSubmit() {
formRef?.validate().then(() => (isUpdate ? handleUpdate() : handleCreate()))
......@@ -64,7 +69,8 @@ function handleCreate() {
'update_rule',
'status',
'label',
'weight'
'weight',
'teacher_id',
])
createLabel(params).then(() => {
ElMessage({ message: '创建成功', type: 'success' })
......@@ -82,7 +88,8 @@ function handleUpdate() {
'update_rule',
'status',
'label',
'weight'
'weight',
'teacher_id',
])
updateLabel(params).then(() => {
ElMessage({ message: '修改成功', type: 'success' })
......@@ -94,7 +101,7 @@ function handleUpdate() {
<template>
<el-dialog :title="title" :close-on-click-modal="false" width="600px" @closed="$emit('update:modelValue', false)">
<el-form ref="formRef" :model="form" :rules="rules" label-suffix=":" label-width="100px">
<el-form ref="formRef" :model="form" :rules="rules" label-suffix=":" label-width="110px">
<el-form-item label="标签名称" prop="name">
<el-input v-model="form.name" placeholder="请输入" />
</el-form-item>
......@@ -103,6 +110,11 @@ function handleUpdate() {
<el-option v-for="item in labelList" :key="item.value" :label="item.label" :value="item.value"></el-option>
</el-select>
</el-form-item>
<el-form-item label="匹配老师标签" prop="teacher_id" v-if="hasQuestion">
<el-select v-model="form.teacher_id" style="width: 100%">
<el-option v-for="item in teacherTagList" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="标签目录" prop="type_id">
<el-select v-model="form.type_id" style="width: 100%">
<el-option v-for="item in typeList" :key="item.id" :label="item.name" :value="item.id"></el-option>
......
......@@ -31,13 +31,13 @@ const statusList = useMapStore().getMapValuesByKey('system_status')
const formRef = $ref<FormInstance>()
const form = reactive({
const form: any = reactive({
id: '',
rules: undefined
rules: undefined,
})
function fetchInfo() {
getLabelRule({ id: props.data.id }).then(res => {
getLabelRule({ id: props.data.id }).then((res) => {
const { detail } = res.data
let rules = detail.rules
......@@ -47,9 +47,9 @@ function fetchInfo() {
event_attr_rule: {
current_logic_operate: 'and',
happen_info: { is_happened: true, event_id: '-1', event_name: '所有事件', attr_list: [] },
trigger_info: { operate: '', operate_name: '', value: '' }
trigger_info: { operate: '', operate_name: '', value: '' },
},
tag_rule: { event_id: '', event_name: '', attr_id: '', attr_name: '', type: 1, value: undefined }
tag_rule: { event_id: '', event_name: '', attr_id: '', attr_name: '', type: 1, value: undefined },
}
}
if (detail.label == '3') {
......@@ -57,9 +57,9 @@ function fetchInfo() {
event_attr_rule: {
current_logic_operate: 'and',
happen_info: { is_happened: true, event_id: '-1', event_name: '所有事件', attr_list: [] },
trigger_info: { operate: '', operate_name: '', value: '' }
trigger_info: { operate: '', operate_name: '', value: '' },
},
tag_rule: { way: '' }
tag_rule: { way: '' },
}
}
if (detail.label === '4') {
......@@ -75,7 +75,7 @@ function fetchInfo() {
rules = {
user_attr_rule: { current_logic_operate: 'and', items: [] },
event_attr_rule: { current_logic_operate: 'and', items: [] },
user_action_rule: { current_logic_operate: 'and', items: [] }
user_action_rule: { current_logic_operate: 'and', items: [] },
}
}
}
......@@ -100,9 +100,15 @@ function handleUpdate() {
// item.value = Array.isArray(item.value) ? item.value.join(',') : item.value
// return item
// })
let rules = form.rules
if (props.data.label == '1' && rules?.length > 0) {
rules = rules.map((item: any, index: number) => {
return { ...item, level: index }
})
}
const params = {
id: form.id,
rules: JSON.stringify(form.rules)
rules: JSON.stringify(rules),
// user_attr_rule: JSON.stringify([{ ...form.user_attr_rule, items: attrRuleItems }]),
// event_attr_rule: JSON.stringify([{ ...form.event_attr_rule, items: eventRuleItems }])
}
......
......@@ -29,6 +29,7 @@ export interface Label {
updated_operator: Operator
label: string
weight: string
teacher_id?: string
}
// 标签更新规则
export interface LabelUpdateRule {
......@@ -38,7 +39,10 @@ export interface LabelUpdateRule {
export type LabelListRequest = Pick<Label, 'id' | 'name'> & { experiment_id?: string }
export type LabelUpdateRequest = Pick<Label, 'id' | 'name' | 'type_id' | 'update_status' | 'update_rule' | 'status' | 'label'> & {
export type LabelUpdateRequest = Pick<
Label,
'id' | 'name' | 'type_id' | 'update_status' | 'update_rule' | 'status' | 'label' | 'teacher_id'
> & {
experiment_id?: string
}
......
......@@ -16,6 +16,8 @@ const userStore = useUserStore()
const LabelFormDialog = defineAsyncComponent(() => import('../components/LabelFormDialog.vue'))
const LabelViewDialog = defineAsyncComponent(() => import('../components/LabelViewDialog.vue'))
const LabelRuleDialog = defineAsyncComponent(() => import('../components/LabelRuleDialog.vue'))
const AISummaryDialog = defineAsyncComponent(() => import('../components/AISummaryDialog.vue'))
const aiDialogVisible = ref(false)
const statusList = useMapStore().getMapValuesByKey('system_status')
const { typeList } = useLabelType()
......@@ -36,7 +38,7 @@ const listOptions = computed(() => {
params.updated_operator = listParams.updated_operator
}
return params
}
},
},
filters: [
{ type: 'input', prop: 'name', placeholder: '请输入标签名称' },
......@@ -46,16 +48,16 @@ const listOptions = computed(() => {
placeholder: '请选择标签目录',
options: typeList.value,
labelKey: 'name',
valueKey: 'id'
valueKey: 'id',
},
{
type: 'select',
prop: 'label',
placeholder: '请选择标签类型',
options: labelList
options: labelList,
},
{ type: 'select', prop: 'status', placeholder: '请选择标签状态', options: statusList },
{ type: 'input', prop: 'updated_operator', placeholder: '更新人', slots: 'filter-user' }
{ type: 'input', prop: 'updated_operator', placeholder: '更新人', slots: 'filter-user' },
],
columns: [
{ type: 'selection' },
......@@ -67,7 +69,7 @@ const listOptions = computed(() => {
prop: 'label',
computed({ row }: { row: Label }) {
return getNameByValue(row.label, labelList) || row.label
}
},
},
{ label: '标签目录', prop: 'tag_type.name' },
{ label: '标签权重', prop: 'weight' },
......@@ -76,7 +78,7 @@ const listOptions = computed(() => {
prop: 'update_status',
computed({ row }: { row: Label }) {
return getNameByValue(row.update_status, updateStatusRuleList)
}
},
},
{
label: '状态',
......@@ -84,18 +86,18 @@ const listOptions = computed(() => {
computed({ row }: { row: Label }) {
const color = row.status === '1' ? 'var(--main-success-color)' : 'var(--main-color)'
return `<span style="color: ${color}">${getNameByValue(row.status, statusList)}</span>`
}
},
},
{
label: '更新人',
prop: 'updated_operator.real_name',
computed({ row }: any) {
return row.updated_operator?.real_name || row.updated_operator?.nickname
}
},
},
{ label: '更新时间', prop: 'updated_time' },
{ label: '操作', slots: 'table-x', width: 320 }
]
{ label: '操作', slots: 'table-x', width: 320 },
],
}
})
// 刷新
......@@ -151,11 +153,11 @@ function handleSelectionChange(selection: Label[]) {
}
const handleRemoves = async function () {
const ids = multipleSelection.map(item => item.id)
const ids = multipleSelection.map((item) => item.id)
await ElMessageBox.confirm('确定要删除选中的标签数据吗?', '提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning'
type: 'warning',
})
await deleteLabels({ ids: JSON.stringify(ids) })
appList?.refetch(true)
......@@ -169,10 +171,23 @@ const handleRemoves = async function () {
<div class="label-left"><LabelType :active-id="listParams.type_id" @select="handleSelect"></LabelType></div>
<AppList v-bind="listOptions" ref="appList" class="label-right" @selection-change="handleSelectionChange">
<template #header-buttons>
<el-button type="primary" :icon="Plus" @click="handleAdd" v-if="!userStore.status.tag_status">新建</el-button>
<el-button type="primary" plain :icon="Delete" :disabled="!multipleSelection.length" @click="handleRemoves" v-permission="'experiment_tag_delete'"
<div style="display: flex; justify-content: space-between">
<div>
<el-button type="primary" :icon="Plus" @click="handleAdd" v-if="!userStore.status.tag_status"
>新建</el-button
>
<el-button
type="primary"
plain
:icon="Delete"
:disabled="!multipleSelection.length"
@click="handleRemoves"
v-permission="'experiment_tag_delete'"
>删除</el-button
>
</div>
<el-button type="primary" @click="aiDialogVisible = true">AI建议</el-button>
</div>
</template>
<template #filter-user>
<SelectUser v-model="listParams.updated_operator" placeholder="更新人" @change="handleRefresh"></SelectUser>
......@@ -181,18 +196,27 @@ const handleRemoves = async function () {
<template #table-x="{ row }">
<el-button type="primary" plain @click="handleRule(row)">规则</el-button>
<el-button type="primary" plain @click="handleView(row)">查看</el-button>
<el-button type="primary" plain @click="handleUpdate(row)" v-permission="'experiment_tag_update'">编辑</el-button>
<el-button type="primary" plain @click="handleRemove(row)" v-permission="'experiment_tag_delete'">删除</el-button>
<el-button type="primary" plain @click="handleUpdate(row)" v-permission="'experiment_tag_update'"
>编辑</el-button
>
<el-button type="primary" plain @click="handleRemove(row)" v-permission="'experiment_tag_delete'"
>删除</el-button
>
</template>
</AppList>
</div>
</AppCard>
<!-- 新建/修改标签 -->
<LabelFormDialog v-model="formVisible" :data="currentRow" @update="handleRefresh" v-if="formVisible"></LabelFormDialog>
<LabelFormDialog
v-model="formVisible"
:data="currentRow"
@update="handleRefresh"
v-if="formVisible"></LabelFormDialog>
<!-- 查看标签 -->
<LabelViewDialog v-model="viewVisible" :data="currentRow" v-if="viewVisible && currentRow"></LabelViewDialog>
<!-- 规则 -->
<LabelRuleDialog v-model="ruleVisible" :data="currentRow" v-if="ruleVisible && currentRow"></LabelRuleDialog>
<AISummaryDialog v-model="aiDialogVisible" v-if="aiDialogVisible"></AISummaryDialog>
</template>
<style lang="scss">
......
import httpRequest from '@/utils/axios'
// 获取订单列表
export function getOrderList(params?: { live_commodity_type_id?: string; live_commodity_title?: string }) {
return httpRequest.get('/api/lab/v1/experiment/live-practice-order/list', { params })
}
import type { RouteRecordRaw } from 'vue-router'
import Layout from '@/components/layout/Index.vue'
const routes: RouteRecordRaw[] = [
{
path: '/live/order',
component: Layout,
children: [{ path: '', component: () => import('./views/Index.vue') }]
}
]
export { routes }
<script setup>
import LiveProductCategory from '@/components/LiveProductCategory.vue'
import { getOrderList } from '../api'
const appList = ref(null)
// 刷新
const handleRefresh = () => {
appList.value?.refetch()
}
const listParams = reactive({ name: '', live_commodity_type_id: '', live_commodity_title: '' })
// 列表配置
const listOptions = computed(() => {
return {
remote: {
httpRequest: getOrderList,
params: listParams,
beforeRequest(params, isReset) {
if (isReset) listParams.live_commodity_type_id = ''
params.live_commodity_type_id = listParams.live_commodity_type_id
return params
},
},
filters: [
{ label: '直播主题品类', prop: 'live_commodity_type_id', slots: 'filter-category' },
{ label: '直播主题名称', prop: 'live_commodity_title', type: 'input' },
],
columns: [
{ label: '序号', type: 'index', width: 60 },
{ label: '主播', prop: 'updated_operator_name' },
{
label: '商品信息',
prop: 'commodity_info',
computed({ row }) {
try {
const info = JSON.parse(row.commodity_info)
const [picture] = JSON.parse(info.picture_addreses) || []
return `<div style="display: flex; align-items: center; gap: 10px;"><img src="${picture.url}" width="100" />${info.title}</div>`
} catch (e) {
console.log(e)
}
return '--'
},
},
{
label: '单价/数量',
prop: 'num',
computed({ row }) {
return `¥${row.price}<br/>x${row.num}`
},
},
{ label: '订单编号', prop: 'id' },
{
label: '订单状态',
prop: 'status',
computed() {
return '待发货'
},
},
{ label: '下单时间', prop: 'updated_time' },
],
}
})
</script>
<template>
<AppCard title="订单管理">
<AppList v-bind="listOptions" ref="appList">
<template #filter-category>
<LiveProductCategory v-model="listParams.live_commodity_type_id" @change="handleRefresh"></LiveProductCategory>
</template>
</AppList>
</AppCard>
</template>
......@@ -3,19 +3,11 @@ import { useChat } from '@/composables/useChat'
import { getAttrList } from '../api'
const form = inject('form')
const { isLoading, post, messages } = useChat()
watch(
messages,
() => {
const lastMessage = messages.value[messages.value.length - 1]
if (lastMessage) {
form.shopping_guide_short_title = lastMessage.content.replace('推荐导购短标题:', '')
}
},
{ deep: true }
)
const { post } = useChat()
const descLoading = ref(false)
function handleAIGenerate() {
descLoading.value = true
post({
role: 'user',
content: `请根据以下内容,给出1个用于抖音电商使用的推荐的“导购短标题”内容:${form.title}
......@@ -25,6 +17,30 @@ function handleAIGenerate() {
3. 标题要具有吸引力,能够激发消费者的购买欲望。
4. 标题要简洁明了,不要过于冗长或复杂。
5. 输出结果以“推荐导购短标题:”开始`,
}).then((res) => {
form.shopping_guide_short_title = res.content.replace('推荐导购短标题:', '')
descLoading.value = false
})
}
const titleLoading = ref(false)
function handleAIGenerateTitle() {
titleLoading.value = true
post({
role: 'user',
content: `
请根据以下内容,给出1个用于抖音电商使用的推荐的“商品标题”内容:${form.title}
要求:
1. 标题要简洁明了,能够吸引消费者的注意力。
2. 标题要突出商品的特点和优势,如设计、材质、功能等。
3. 标题要具有吸引力,能够激发消费者的购买欲望。
4. 标题要简洁明了,不要过于冗长或复杂。
5. 字数控制在8-30个字(16-60字符)以内
6. 输出结果以“推荐商品标题:”开始
`,
}).then((res) => {
form.title = res.content.replace('推荐商品标题:', '')
titleLoading.value = false
})
}
......@@ -76,7 +92,13 @@ const unimportanceTotal = computed(() => {
<div>
<el-form-item label="商品标题" prop="title" :rules="[{ required: true, message: '请输入', trigger: 'blur' }]">
<div class="form-tips">标题不规范会有可能引起商品下架,影响您的正常销售,请点击学习商品发布规范认真填写</div>
<el-input placeholder="至少输入8个字(16个字符)以上,30个字(60个字符)以下" size="large" v-model="form.title" />
<div style="display: flex; width: 100%; align-items: center; gap: 20px">
<el-input
placeholder="至少输入8个字(16个字符)以上,30个字(60个字符)以下"
size="large"
v-model="form.title" />
<el-button type="primary" plain @click="handleAIGenerateTitle" :loading="titleLoading">一键智能优化</el-button>
</div>
</el-form-item>
<el-form-item label="导购短标题" prop="shopping_guide_short_title">
<div class="form-tips">短标题可用于商品搜索、首页推荐、物流单等场景,请提炼商品关键信息,客观准确填写</div>
......@@ -86,7 +108,7 @@ const unimportanceTotal = computed(() => {
size="large"
v-model="form.shopping_guide_short_title"
style="flex: 1" />
<el-button type="primary" plain @click="handleAIGenerate" :loading="isLoading">一键智能推荐</el-button>
<el-button type="primary" plain @click="handleAIGenerate" :loading="descLoading">一键智能推荐</el-button>
</div>
</el-form-item>
<el-form-item
......
......@@ -61,3 +61,8 @@ export function getRecordList(params: { live_practice_id: string }) {
export function getRecord(params: { id: string }) {
return httpRequest.get('/api/lab/v1/experiment/live-practice/live-practice-record', { params })
}
// 推送字幕
export function pushSubtitle(params: { subtitle: string; selling_point: string; marketing_campaign: string }) {
return httpRequest.get('/api/lab/v1/experiment/live-practice/push-subtitle', { params })
}
<script setup>
import Live from './Live.vue'
import LivePlayback from './LivePlayback.vue'
import { getTest, getRecord, pushSubtitle } from '../api'
import { useCountdown } from '@/composables/useCountdown'
const props = defineProps({
id: { type: String },
recordId: { type: String },
isView: { type: Boolean, default: false },
})
const { timeLeft, formattedTime, stop, reset } = useCountdown({
// 倒计时结束
onEnd: () => {
live.value?.stop()
},
})
const live = ref(null)
const detail = ref(null)
provide('detail', detail)
const duration = computed(() => {
return parseInt(detail.value?.duration) * 60 || 0
})
const isLocalUpload = computed(() => {
return detail.value?.upload_way == 2
})
async function fetchInfo() {
const res = await getTest({ id: props.id })
detail.value = res.data.detail
timeLeft.value = duration.value
}
watchEffect(() => {
!props.isView && fetchInfo()
})
// 商品卖点
const hotList = computed(() => {
return detail.value?.live_speech.selling_point.split(/;|;/)
})
// 营销活动
const actList = computed(() => {
return detail.value?.live_speech.marketing_campaign.split(/;|;/)
})
// 直播记录数据
const record = ref(null)
provide('record', record)
// 直播数据
const stats = computed(() => {
const result = { totalViewers: 0, peakViewers: 0, totalGifts: 0, totalLikes: 0, totalGiftViewers: 0 }
return Object.assign(result, record.value?.live_info.stats)
})
const timelines = ref([])
const fetchRecord = async () => {
const res = await getRecord({ id: props.recordId })
const resDetail = res.data.detail
record.value = { ...resDetail, live_info: JSON.parse(resDetail.live_info) }
detail.value = resDetail.live_practice_info
timeLeft.value = duration.value
timelines.value = JSON.parse(resDetail.subtitle)?.Result?.Sentences || []
}
onMounted(() => {
props.isView && fetchRecord()
})
// 毫秒转分钟 00:00,需要补0
const formatTime = (time) => {
const minutes = Math.floor(time / 1000 / 60)
const seconds = Math.floor((time / 1000) % 60)
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
}
const orderCount = ref(0)
const onSentenceEnd = async (result, data) => {
console.log('onSentenceEnd', result, data)
const res = await pushSubtitle({
subtitle: result,
selling_point: detail.value.live_speech.selling_point,
marketing_campaign: detail.value.live_speech.marketing_campaign,
})
orderCount.value += res.data.count
}
const currentStats = ref({})
const onStatsChange = (stats) => {
currentStats.value = stats
}
</script>
<template>
<div class="live-row">
<div class="live-col">
<template v-if="isView">
<LivePlayback :record="record" v-if="record"></LivePlayback>
</template>
<template v-else>
<Live
ref="live"
:orderCount="orderCount"
:isLocalUpload="isLocalUpload"
:onStart="() => reset(duration)"
:onStop="stop"
:onSentenceEnd="onSentenceEnd"
:onStatsChange="onStatsChange"
v-if="detail" />
</template>
</div>
<div class="live-col" style="flex: 1" v-if="isView">
<el-timeline style="max-width: 600px">
<el-timeline-item
placement="top"
v-for="(item, index) in timelines"
:key="index"
:timestamp="formatTime(item.BeginTime)">
{{ item.Text }}
</el-timeline-item>
</el-timeline>
</div>
<div class="live-col" style="flex: 1">
<h2 class="h2-title">直播话术</h2>
<div class="live-talk-content" v-html="detail?.live_speech.content"></div>
</div>
<div class="live-col" style="width: 350px" v-if="!isView">
<div class="live-col-box" v-if="isView">
<h2 class="h2-title">直播数据</h2>
<div class="live-data">
<dl>
<dt>观众总人数:</dt>
<dd>{{ stats.totalViewers }}</dd>
</dl>
<dl>
<dt>最高峰人数:</dt>
<dd>{{ stats.peakViewers }}</dd>
</dl>
<dl>
<dt>点赞数:</dt>
<dd>{{ stats.totalLikes }}</dd>
</dl>
<dl>
<dt>刷礼物人数:</dt>
<dd>{{ stats.totalGiftViewers }}</dd>
</dl>
<dl>
<dt>刷礼物总数:</dt>
<dd>{{ stats.totalGifts }}</dd>
</dl>
</div>
</div>
<div class="live-col-box" v-else>
<h2 class="h2-title">倒计时</h2>
<h3 class="live-time">{{ formattedTime }}</h3>
<div class="live-data2">
<dl>
<dt>观众人数</dt>
<dd>{{ currentStats.viewers }}</dd>
</dl>
<dl>
<dt>订单量</dt>
<dd>{{ orderCount }}</dd>
</dl>
</div>
</div>
<div class="live-col-box">
<h2 class="h2-title">主题卖点</h2>
<ul class="live-tag live-tag__hot">
<li v-for="item in hotList" :key="item">{{ item }}</li>
</ul>
</div>
<div class="live-col-box">
<h2 class="h2-title">营销活动</h2>
<ul class="live-tag live-tag__act">
<li v-for="item in actList" :key="item">{{ item }}</li>
</ul>
</div>
</div>
</div>
</template>
<style lang="scss">
.live-row {
height: 100%;
display: flex;
gap: 20px;
}
.live-col {
padding: 20px;
border-radius: 10px;
border: 1px solid #eee;
max-height: 100%;
overflow-y: auto;
.h2-title {
margin-top: 0;
}
}
.live-tag {
margin: 20px 0;
background-color: #eee;
border-radius: 10px;
padding: 20px;
gap: 10px;
li {
line-height: 30px;
background-color: #fff;
text-align: center;
border: 1px solid rgba(105, 113, 140, 0.12);
}
}
.live-data {
padding: 0 0 20px 10px;
dl {
margin: 10px 0;
display: flex;
}
dt {
font-weight: bold;
}
}
.live-data2 {
display: flex;
gap: 10px;
text-align: center;
dl {
flex: 1;
text-align: center;
margin-bottom: 40px;
}
dt {
font-size: 20px;
font-weight: bold;
}
dd {
font-size: 42px;
text-align: center;
color: var(--main-color);
}
}
.live-time {
height: 140px;
font-size: 72px;
text-align: center;
color: var(--main-color);
}
.live-tag__hot {
display: flex;
flex-wrap: wrap;
li {
flex: 0 0 calc(50% - 10px);
}
}
.live-tag__act {
li {
margin: 10px 0;
}
}
.live-talk-content {
max-height: 660px;
overflow-y: auto;
line-height: 24px;
}
</style>
......@@ -11,14 +11,25 @@ import { saveAs } from 'file-saver'
import md5 from 'blueimp-md5'
import dayjs from 'dayjs'
import { useLiveChat } from '../composables/useLiveChat'
import { useSpeechTranscriber } from '../composables/useSpeechTranscriber'
const props = defineProps({
orderCount: { type: Number, default: 0 },
isLocalUpload: { type: Boolean, default: false },
onStart: { type: Function, default: () => {} },
onStop: { type: Function, default: () => {} },
onSentenceEnd: { type: Function, default: () => {} },
onStatsChange: { type: Function, default: () => {} },
})
const { messages, viewers, stats, currentTime, start: startChat } = useLiveChat()
watch(
() => [stats, viewers],
() => {
props.onStatsChange && props.onStatsChange({ ...stats, viewers: viewers.value.length })
},
{ immediate: true, deep: true }
)
const detail = inject('detail')
......@@ -50,6 +61,9 @@ const { send } = useSocket({
}
},
})
const { start: startSpeechTranscriber, stop: stopSpeechTranscriber } = useSpeechTranscriber({
onSentenceEnd: props.onSentenceEnd,
})
const {
stream,
......@@ -61,8 +75,12 @@ const {
currentTime: currentLiveTime,
} = useLive({
onStart: () => {
console.log('开始直播')
props.onStart && props.onStart()
startChat()
if (!props.isLocalUpload) {
startSpeechTranscriber(stream.value)
}
},
onRecord: async (blob) => {
if (props.isLocalUpload) return
......@@ -75,6 +93,7 @@ const {
send(jsonData)
},
onStop: (blob) => {
console.log('结束直播')
if (enabled.value) return
props.onStop && props.onStop(blob)
handleUpdateRecord({ live_video_size: blob.size.toString() })
......@@ -83,6 +102,7 @@ const {
saveAs(blob, `${fileName.value}.mp4`)
ElMessageBox.alert('请将保存的本地视频文件上传服务端,否则会影响您的实操成绩评价!', '温馨提示')
}
stopSpeechTranscriber()
},
})
......@@ -110,6 +130,7 @@ const handleUpdateRecord = async (params) => {
live_duration: duration.value.toString(),
live_video_addres: fileUrl.value,
live_info: JSON.stringify({ messages: messages.value, stats }),
order_count: props.orderCount,
}
const requestParams = { ...defaultParams, ...params }
if (!requestParams.live_practice_id) return
......
......@@ -46,7 +46,21 @@ const listOptions = {
return row.live_video_addres ? '已上传' : '未上传'
},
},
{ label: '操作', slots: 'table-x', width: 150 },
{
label: '状态',
prop: 'ai_status',
computed({ row }) {
return row.ai_status || '--'
},
},
{
label: '评级',
prop: 'ai_level',
computed({ row }) {
return row.ai_level || '--'
},
},
{ label: '操作', slots: 'table-x', width: 120 },
],
}
......
......@@ -72,7 +72,7 @@ export function useLive({ enabledUserMedia = true, onStart, onRecord, onStop }:
// Update currentTime every 100ms while recording
timeUpdateInterval = setInterval(() => {
currentTime.value = Math.floor((Date.now() - startTime.value) / 1000)
}, 100)
}, 1000 * 5)
}
// 录像停止时处理
......
......@@ -102,7 +102,7 @@ export function useLiveChat(options: UseLiveChatOptions = {}) {
// 离开直播
case 'leave':
viewers.value = viewers.value.filter((v) => v.id !== message.user.id)
stats.totalViewers--
// stats.totalViewers--
break
// 点赞
case 'like':
......
import axios from 'axios'
import Base64 from 'crypto-js/enc-base64'
import HmacSHA1 from 'crypto-js/hmac-sha1'
import { useWebSocket } from '@vueuse/core'
// https://help.aliyun.com/zh/isi/developer-reference/websocket
export function useSpeechTranscriber({ onSentenceEnd }: { onSentenceEnd?: (result: string, data: any) => void }) {
let audioContext: AudioContext
let audioInput: MediaStreamAudioSourceNode
const token = ref('')
const isTranscriptionStarted = ref(false)
const wsUrl = computed(() => {
return `wss://nls-gateway-cn-shanghai.aliyuncs.com/ws/v1?token=${token.value}`
})
const header = {
appkey: import.meta.env.VITE_APP_KEY,
namespace: 'SpeechTranscriber',
task_id: crypto.randomUUID().replace(/-/g, ''),
message_id: crypto.randomUUID().replace(/-/g, ''),
}
const { send, status, open } = useWebSocket(wsUrl, {
autoConnect: false,
immediate: false,
onConnected: () => {
console.log('语音识别连接成功')
const startTranscriptionMessage = {
header: { ...header, name: 'StartTranscription' },
payload: {
format: 'pcm',
sample_rate: 16000,
enable_intermediate_result: true,
enable_punctuation_prediction: true,
enable_inverse_text_normalization: true,
},
}
send(JSON.stringify(startTranscriptionMessage))
},
onMessage: (ws, event) => {
try {
const data = JSON.parse(event.data)
if (data.header.name === 'TranscriptionStarted') {
isTranscriptionStarted.value = true
}
if (data.header.name === 'SentenceEnd') {
// 一句话结束
console.log(data.payload.result)
onSentenceEnd?.(data.payload.result, data)
}
} catch (e) {
console.log('error', e, event.data)
}
},
onError: (ws, event) => {
console.log('语音识别连接失败', event)
},
onDisconnected: (ws, event) => {
console.log('语音识别连接断开', event)
},
})
const start = async (stream: MediaStream) => {
console.log('开始录音', stream)
token.value = await getToken()
open()
try {
// 创建 AudioContext 并设定采样率
audioContext = new AudioContext({ sampleRate: 16000 })
audioInput = audioContext.createMediaStreamSource(stream)
// 加载 AudioWorkletProcessor 模块
await audioContext.audioWorklet.addModule('/processor.js')
// 创建自定义处理器节点
const audioWorkletNode = new AudioWorkletNode(audioContext, 'pcm-processor')
// 监听自定义消息
audioWorkletNode.port.onmessage = (event) => {
const inputData16 = event.data as ArrayBuffer
if (status.value === 'OPEN' && isTranscriptionStarted.value) {
send(inputData16) // 发送到后端或处理函数
}
}
// 连接音频流到处理器
audioInput.connect(audioWorkletNode)
// 如果你不需要播放,可以不连接 destination
// audioWorkletNode.connect(audioContext.destination);
} catch (e) {
console.error('录音失败: ' + e)
}
}
const stop = () => {
if (audioInput) {
audioInput.disconnect()
}
if (audioContext) {
audioContext.close()
}
const stopTranscriptionMessage = { header: { ...header, name: 'StopTranscription' } }
send(JSON.stringify(stopTranscriptionMessage))
}
return { start, stop }
}
async function getToken() {
const parameters: Record<string, string> = {
AccessKeyId: import.meta.env.VITE_ACCESS_KEY_ID,
Action: 'CreateToken',
Format: 'JSON',
RegionId: 'cn-shanghai',
SignatureMethod: 'HMAC-SHA1',
SignatureNonce: crypto.randomUUID(),
SignatureVersion: '1.0',
Timestamp: new Date().toISOString().replace(/\.\d{3}/, ''),
Version: '2019-02-28',
}
const specialUrlEncode = (text: string) => {
return encodeURIComponent(text).replace(/\+/g, '%20').replace(/\*/g, '%2A').replace(/%7E/g, '~')
}
const encodeParameters = (params: Record<string, string>) => {
return Object.keys(params)
.sort()
.map((key) => `${specialUrlEncode(key)}=${specialUrlEncode(params[key])}`)
.join('&')
}
const canonicalizedQueryString = encodeParameters(parameters)
const stringToSign = 'GET' + '&' + specialUrlEncode('/') + '&' + specialUrlEncode(canonicalizedQueryString)
const signature = Base64.stringify(HmacSHA1(stringToSign, `${import.meta.env.VITE_ACCESS_KEY_SECRET}&`))
const encodedSignature = specialUrlEncode(signature)
const url = `https://nls-meta.cn-shanghai.aliyuncs.com/?Signature=${encodedSignature}&${canonicalizedQueryString}`
const response = await axios.get(url)
return response.data.Token.Id
}
......@@ -12,7 +12,7 @@ const routes: RouteRecordRaw[] = [
children: [
{ path: '', component: () => import('./views/Index.vue') },
{ path: 'demo', component: () => import('./views/Demo.vue') },
{ path: 'view', component: () => import('./views/Demo.vue'), props: { isView: true } },
{ path: 'view', component: () => import('./views/View.vue') },
],
},
{
......
<script setup>
import Live from '../components/Live.vue'
import LivePlayback from '../components/LivePlayback.vue'
import { getTest, getRecord } from '../api'
import { useCountdown } from '@/composables/useCountdown'
import Demo from '../components/Demo.vue'
const props = defineProps({
isView: { type: Boolean, default: false },
})
const route = useRoute()
const { timeLeft, formattedTime, stop, reset } = useCountdown({
// 倒计时结束
onEnd: () => {
live.value?.stop()
},
})
const live = ref(null)
const detail = ref(null)
provide('detail', detail)
const duration = computed(() => {
return parseInt(detail.value?.duration) * 60 || 0
})
const isLocalUpload = computed(() => {
return detail.value?.upload_way == 2
})
async function fetchInfo() {
const res = await getTest({ id: route.query.id })
detail.value = res.data.detail
timeLeft.value = duration.value
}
watchEffect(() => {
!props.isView && fetchInfo()
})
// 商品卖点
const hotList = computed(() => {
return detail.value?.live_speech.selling_point.split(/;|;/)
})
// 营销活动
const actList = computed(() => {
return detail.value?.live_speech.marketing_campaign.split(/;|;/)
})
// 直播记录数据
const record = ref(null)
provide('record', record)
// 直播数据
const stats = computed(() => {
const result = { totalViewers: 0, peakViewers: 0, totalGifts: 0, totalLikes: 0, totalGiftViewers: 0 }
return Object.assign(result, record.value?.live_info.stats)
})
const fetchRecord = async () => {
const res = await getRecord({ id: route.query.record_id })
const resDetail = res.data.detail
record.value = { ...resDetail, live_info: JSON.parse(resDetail.live_info) }
detail.value = resDetail.live_practice_info
timeLeft.value = duration.value
}
onMounted(() => {
props.isView && fetchRecord()
})
const id = route.query.id
</script>
<template>
<AppCard :title="isView ? '查看历史直播详情' : '直播'" full>
<div class="live-row">
<div class="live-col">
<template v-if="isView">
<LivePlayback :record="record" v-if="record"></LivePlayback>
</template>
<template v-else>
<Live
ref="live"
:isLocalUpload="isLocalUpload"
:onStart="() => reset(duration)"
:onStop="stop"
v-if="detail" />
</template>
</div>
<div class="live-col" style="flex: 1">
<h2 class="h2-title">直播话术</h2>
<div class="live-talk-content" v-html="detail?.live_speech.content"></div>
</div>
<div class="live-col" style="width: 350px">
<div class="live-col-box" v-if="isView">
<h2 class="h2-title">直播数据</h2>
<div class="live-data">
<dl>
<dt>观众总人数:</dt>
<dd>{{ stats.totalViewers }}</dd>
</dl>
<dl>
<dt>最高峰人数:</dt>
<dd>{{ stats.peakViewers }}</dd>
</dl>
<dl>
<dt>点赞数:</dt>
<dd>{{ stats.totalLikes }}</dd>
</dl>
<dl>
<dt>刷礼物人数:</dt>
<dd>{{ stats.totalGiftViewers }}</dd>
</dl>
<dl>
<dt>刷礼物总数:</dt>
<dd>{{ stats.totalGifts }}</dd>
</dl>
</div>
</div>
<div class="live-col-box" v-else>
<h2 class="h2-title">倒计时</h2>
<h3 class="live-time">{{ formattedTime }}</h3>
</div>
<div class="live-col-box">
<h2 class="h2-title">主题卖点</h2>
<ul class="live-tag live-tag__hot">
<li v-for="item in hotList" :key="item">{{ item }}</li>
</ul>
</div>
<div class="live-col-box">
<h2 class="h2-title">营销活动</h2>
<ul class="live-tag live-tag__act">
<li v-for="item in actList" :key="item">{{ item }}</li>
</ul>
</div>
</div>
</div>
<AppCard title="直播" full>
<Demo :id="id" />
</AppCard>
</template>
<style lang="scss">
.live-row {
height: 100%;
display: flex;
gap: 20px;
}
.live-col {
padding: 20px;
border-radius: 10px;
border: 1px solid #eee;
.h2-title {
margin-top: 0;
}
}
.live-tag {
margin: 20px 0;
background-color: #eee;
border-radius: 10px;
padding: 20px;
gap: 10px;
li {
line-height: 30px;
background-color: #fff;
text-align: center;
border: 1px solid rgba(105, 113, 140, 0.12);
}
}
.live-data {
padding: 0 0 20px 10px;
dl {
margin: 10px 0;
display: flex;
}
dt {
font-weight: bold;
}
}
.live-time {
height: 140px;
font-size: 72px;
text-align: center;
color: var(--main-color);
}
.live-tag__hot {
display: flex;
flex-wrap: wrap;
li {
flex: 0 0 calc(50% - 10px);
}
}
.live-tag__act {
li {
margin: 10px 0;
}
}
.live-talk-content {
max-height: 660px;
overflow-y: auto;
line-height: 24px;
}
</style>
<script setup>
import VueMarkdown from 'vue-markdown-render'
import Demo from '../components/Demo.vue'
import { getRecord } from '../api'
const route = useRoute()
const id = route.query.id
const recordId = route.query.record_id
const dialogVisible = ref(false)
const detail = ref(null)
const fetchRecord = async () => {
const res = await getRecord({ id: recordId })
const resDetail = res.data.detail
detail.value = { ...resDetail, live_info: JSON.parse(resDetail.live_info) }
}
onMounted(() => {
fetchRecord()
})
const tipsList = {
A: [
{ title: '卖点讲解', content: '强化产品独特性与情感场景化表达。' },
{ title: '违规情况', content: '关注平台新规并规避敏感词。' },
{ title: '讲解时长', content: '精准分配时间,促销环节压缩冗余。' },
{ title: '其他建议', content: '模拟实战并复盘转化数据。' },
],
B: [
{ title: '卖点讲解', content: '场景化设计减少参数罗列。' },
{ title: '违规情况', content: '熟记合规词库并规避竞品对比。' },
{ title: '讲解时长', content: '控制节奏,促销环节提速。' },
{ title: '其他建议', content: '学习高感染力话术并准备互动模板。' },
],
C: [
{ title: '卖点讲解', content: '结构化表达并突出核心卖点。' },
{ title: '违规情况', content: '系统学习规则并使用AI审核。' },
{ title: '讲解时长', content: '分段练习并制定时间分配表。' },
{ title: '其他建议', content: '参加基础培训并拆解优秀案例。' },
],
D: [
{ title: '卖点讲解', content: '模板化练习并记忆关键词。' },
{ title: '违规情况', content: '精读合规手册并替换敏感词。' },
{ title: '讲解时长', content: '分段训练并使用计时器。' },
{ title: '其他建议', content: '录制回放并建立反馈机制。' },
],
}
const tips = computed(() => {
return tipsList[detail.value?.ai_level || 'A']
})
function formatDuration(seconds) {
const minutes = Math.floor(seconds / 60)
const remainingSeconds = (seconds % 60).toFixed(2)
return minutes > 0 ? `${minutes}m${remainingSeconds}s` : `${remainingSeconds}s`
}
</script>
<template>
<div class="live-view" v-if="detail">
<AppCard>
<div class="live-info">
<div class="live-info-header">
<h2>我的练习表现</h2>
<p>评分等级</p>
<h3>{{ detail.ai_level || '--' }}</h3>
<p>主播完成质量良好,需要关注细节勤加练习</p>
</div>
<div class="live-info-item">
<div>
<p>主播姓名</p>
<p>{{ detail.created_operator.real_name || detail.created_operator.nickname }}</p>
</div>
<div>
<p>直播时长</p>
<p>{{ formatDuration(detail.live_duration) }}</p>
</div>
<div>
<p>开始时间</p>
<p>{{ detail.live_start_time }}</p>
</div>
<div>
<p>结束时间</p>
<p>{{ detail.live_end_time }}</p>
</div>
<div>
<p>视频状态</p>
<p>已上传</p>
</div>
<div>
<p>观众总人数</p>
<p>{{ detail.live_info.stats.totalViewers }}</p>
</div>
<div>
<p>最高峰人数</p>
<p>{{ detail.live_info.stats.peakViewers }}</p>
</div>
<div>
<p>点赞数</p>
<p>{{ detail.live_info.stats.totalLikes }}</p>
</div>
<div>
<p>刷礼物人数</p>
<p>{{ detail.live_info.stats.totalGiftViewers }}</p>
</div>
<div>
<p>刷礼物总数</p>
<p>{{ detail.live_info.stats.totalGifts }}</p>
</div>
<div>
<p>下单量</p>
<p>{{ detail.orders_count }}</p>
</div>
<div>
<p>操作</p>
<el-button type="primary" link @click="dialogVisible = true">查看</el-button>
</div>
</div>
</div>
</AppCard>
<AppCard>
<h2 class="live-title">练习优化建议</h2>
<div v-for="(item, index) in tips" :key="index">
<p class="live-tips">
<b>{{ item.title }}</b> {{ item.content }}
</p>
</div>
</AppCard>
<AppCard>
<h2 class="live-title">违规情况</h2>
<div class="live-markdown">
<VueMarkdown :source="detail.illegal_word" v-if="detail.illegal_word" />
</div>
</AppCard>
<AppCard>
<h2 class="live-title">卖点讲解</h2>
<el-steps direction="vertical" :active="1">
<el-step
v-for="(item, index) in detail.words"
:key="index"
:title="item.name"
:status="item.time ? 'success' : 'error'">
<template #description> {{ item.time }} {{ item.subtitle }} </template>
</el-step>
</el-steps>
</AppCard>
<AppCard>
<h2 class="live-title">产生订单</h2>
<el-steps direction="vertical" :active="1">
<el-step
v-for="(item, index) in detail.words"
:key="index"
:title="item.name"
:status="item.time ? 'success' : 'error'">
<template #description> {{ item.time }} {{ item.subtitle }} </template>
</el-step>
</el-steps>
</AppCard>
<el-dialog v-model="dialogVisible" title="直播回放" width="1200px">
<div style="height: 740px">
<Demo :id="id" :recordId="recordId" :isView="true" />
</div>
</el-dialog>
</div>
</template>
<style lang="scss">
.live-view {
.live-info {
display: flex;
gap: 12px;
p {
font-size: 14px;
color: #525252;
margin: 10px 0;
}
.live-info-header {
display: flex;
flex-direction: column;
border-right: 1px solid #e5e5e5;
padding-right: 12px;
h2 {
font-size: 24px;
font-weight: bold;
}
h3 {
font-size: 40px;
font-weight: bold;
}
}
.live-info-item {
flex: 1;
display: flex;
flex-wrap: wrap;
gap: 12px;
div {
width: 180px;
}
}
}
.live-title {
font-size: 20px;
font-weight: bold;
margin-bottom: 20px;
}
.live-tips {
margin-top: 10px;
font-size: 14px;
color: #000;
b {
font-size: 14px;
font-weight: bold;
margin-right: 20px;
}
}
.el-step__description {
margin-bottom: 20px;
}
.live-markdown {
line-height: 1.8;
li {
list-style: disc;
margin-left: 20px;
}
p {
margin: 10px 0;
}
strong,
b {
font-weight: bold;
}
}
}
</style>
<script setup>
import { useClipboard } from '@vueuse/core'
import { useChat } from '../composables/useChat'
import { useAI } from '@/composables/useAI'
import { useMapStore } from '@/stores/map'
import { useUserStore } from '@/stores/user'
import { useConnection } from '../composables/useConnection'
......@@ -11,7 +11,7 @@ import {
materialUsageList,
materialUsersList,
materialPictureStyleList,
textPurposeList
textPurposeList,
} from '@/utils/dictionary'
import IconComputer from './IconComputer.vue'
import IconUser from './IconUser.vue'
......@@ -31,10 +31,10 @@ const welcomeMessage = computed(() => {
const data = form.value
const way = getNameByValue(data.way, materialMethodList)
const type = getNameByValue(data.type, materialType)
const industry = industryList.value.find(item => item.id == data.industry_id)?.name
const industry = industryList.value.find((item) => item.id == data.industry_id)?.name
const personnel = getNameByValue(data.personnel_type, materialUsersList)
const scenario = getNameByValue(data.scenario_type, materialUsageList)
const connection = connectionList.value.find(item => item.id == data.channel)?.type_name
const connection = connectionList.value.find((item) => item.id == data.channel)?.type_name
const extendInfo = data.extend_info || {}
const pictureStyle = getNameByValue(extendInfo.picture_style, materialPictureStyleList)
const textPurpose = getNameByValue(extendInfo.text_use, textPurposeList)
......@@ -49,13 +49,14 @@ const welcomeMessage = computed(() => {
const content = ref('')
const route = useRoute()
const { usages, messages, post, isLoading } = useChat({
const { ai, options, usages, messages, post, isLoading, fetchUsages, generateText, generateImage } = useAI({
experiment_id: route.query.experiment_id,
marketing_material_id: form.value.id,
fileType: form.value.type
})
onMounted(() => {
fetchUsages()
messages.value.push({ role: 'system', content: welcomeMessage.value })
// if (form.value.content) {
// messages.value.push({ role: 'bot', content: form.value.content })
......@@ -77,7 +78,11 @@ watch(welcomeMessage, () => {
async function postMessage() {
if (!content.value) return
messages.value.push({ role: 'user', content: content.value })
if (form.value.type == 2) {
generateImage({ content: content.value })
} else {
post({ content: content.value, type: '1' })
}
content.value = ''
}
......@@ -92,7 +97,7 @@ async function handleSendType(type, content) {
content = xss(content, {
whiteList: {}, // 白名单为空,表示过滤所有标签
stripIgnoreTag: true, // 过滤所有非白名单标签的HTML
stripIgnoreTagBody: ['script'] // script标签较特殊,需要过滤标签中间的内容
stripIgnoreTagBody: ['script'], // script标签较特殊,需要过滤标签中间的内容
})
switch (type) {
case 2:
......@@ -111,7 +116,11 @@ async function handleSendType(type, content) {
content = `我是${userName},请帮我总结以下内容:${content.replace('请帮我创作一个', '')}`
break
}
post({ type, content })
if (type == 99) {
generateImage({ type, content })
} else {
generateText({ type, content })
}
}
const chatRef = ref()
......@@ -128,7 +137,7 @@ function handleCopy(content) {
const html = xss(content, {
whiteList: {}, // 白名单为空,表示过滤所有标签
stripIgnoreTag: true, // 过滤所有非白名单标签的HTML
stripIgnoreTagBody: ['script'] // script标签较特殊,需要过滤标签中间的内容
stripIgnoreTagBody: ['script'], // script标签较特殊,需要过滤标签中间的内容
})
copy(html)
}
......@@ -167,7 +176,7 @@ async function handleSave(message) {
size="small"
type="primary"
@click="handleSendType(5, item.input || item.content)"
v-if="item.role == 'bot'"
v-if="item.role == 'assistant'"
>刷新({{ usages.ai_refresh_count }}/{{ usages.ai_refresh_max_count }})</el-button
>
<el-button size="small" type="primary" @click="handleSendType(2, item.content)"
......@@ -194,7 +203,7 @@ async function handleSave(message) {
>生成({{ usages.ai_generate_image_count }}/{{ usages.ai_generate_image_max_count }})</el-button
>
</template>
<template v-if="item.role == 'bot'">
<template v-if="item.role == 'assistant'">
<el-button size="small" type="primary" @click="handleSendType(99, item.content)"
>重画({{ usages.ai_generate_image_count }}/{{ usages.ai_generate_image_max_count }})</el-button
>
......@@ -212,6 +221,11 @@ async function handleSave(message) {
</div>
</div>
<div class="chat-footer">
<el-select-v2
:options="options"
v-model="ai"
style="width: 100%; margin-bottom: 10px"
v-if="form.type == 1"></el-select-v2>
<el-input
type="textarea"
:autosize="{ minRows: 1, maxRows: 12 }"
......
......@@ -5,7 +5,7 @@ import { uploadFileByUrl } from '@/utils/upload'
const ChuangKitDesign = defineAsyncComponent(() => import('@/components/ChuangKitDesign.vue'))
// const route = useRoute()
defineProps({ kindId: { type: Number, default: 447 } })
const userStore = useUserStore()
......@@ -19,7 +19,7 @@ const data = reactive({
async function fetchList() {
const res = await getChuanKitDesignList({
user_flag: userStore.user.id,
kind_id: 447,
// kind_id: props.kindId,
page_no: 1,
page_size: 1000,
time_order: 1,
......@@ -59,7 +59,7 @@ function onClose() {
<img :src="item.thumbUrl" />
</li>
</ul>
<ChuangKitDesign v-model="model" @close="onClose" v-if="designVisible"></ChuangKitDesign>
<ChuangKitDesign v-model="model" :kindId="kindId" @close="onClose" v-if="designVisible"></ChuangKitDesign>
</div>
</template>
......
<script setup>
import { useMapStore } from '@/stores/map'
import { materialMethodList } from '@/utils/dictionary'
import { useQuestion } from '@/composables/useQuestion'
const materialType = useMapStore().getMapValuesByKey('experiment_marketing_material_type')
......@@ -14,7 +15,8 @@ const formRef = ref()
const rules = ref({
type: [{ required: true, message: '请选择营销内容类型' }],
name: [{ required: true, message: '请输入内容名称' }]
name: [{ required: true, message: '请输入内容名称' }],
teacher_id:[{ required: true, message: '请选择老师标签' }],
})
async function handleValidate() {
......@@ -36,6 +38,21 @@ function wayDisabled(item, type) {
}
return false
}
const questionType = computed(() => {
const questionTypes = {
1: '401',
2: '402',
3: '403',
4: '404',
5: '405',
6: '406',
7: '407',
8: '408',
}
return questionTypes[form.value.type]
})
const { hasQuestion, teacherMaterialList } = useQuestion(questionType, form.value.type)
</script>
<template>
......@@ -62,6 +79,11 @@ function wayDisabled(item, type) {
<el-form-item label="内容名称" prop="name">
<el-input v-model="form.name" placeholder="请输入内容名称" />
</el-form-item>
<el-form-item label="匹配老师标签" prop="teacher_id" v-if="hasQuestion">
<el-select v-model="form.teacher_id" style="width: 100%">
<el-option v-for="item in teacherMaterialList" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="创作方式" prop="way">
<el-radio-group v-model="form.way">
<el-radio
......
......@@ -17,7 +17,7 @@ const form = defineModel()
const formRef = ref()
const rules = ref({
content: [{ required: true, message: '请输入' }]
content: [{ required: true, message: '请输入' }],
})
const typeName = computed(() => {
......@@ -55,12 +55,12 @@ async function handleSubmit() {
<el-form-item prop="content">
<template v-if="form.type == 1">
<!-- 文本 -->
<el-input type="textarea" rows="14" v-model="form.content"></el-input>
<el-input type="textarea" :rows="14" v-model="form.content"></el-input>
</template>
<template v-if="['2', '6', '7', '8'].includes(form.type)">
<!-- 图片|二维码|小程序|卡券 -->
<template v-if="form.way == 3">
<ImageDesign v-model="form.content"></ImageDesign>
<ImageDesign v-model="form.content" :kindId="parseInt(form.extend_info?.scenario_type)"></ImageDesign>
</template>
<template v-else>
<AppUpload v-model="form.content" accept="image/*"></AppUpload>
......
......@@ -6,9 +6,10 @@ import {
getNameByValue,
materialMethodList,
materialUsageList,
materialDesignUsageList,
materialUsersList,
materialPictureStyleList,
textPurposeList
textPurposeList,
} from '@/utils/dictionary'
defineProps(['action'])
......@@ -32,7 +33,8 @@ const rules = ref({
'extend_info.person_des': [{ required: true, message: '请输入人物描述' }],
'extend_info.scene_des': [{ required: true, message: '请输入场景描述' }],
'extend_info.important_info_desc': [{ required: true, message: '请输入突出重点信息描述' }],
'extend_info.text_use': [{ required: true, message: '请选择文本用途' }]
'extend_info.text_use': [{ required: true, message: '请选择文本用途' }],
'extend_info.scenario_type': [{ required: true, message: '请选择使用场景' }],
})
async function handleValidate() {
......@@ -64,8 +66,7 @@ async function handleNext() {
:model="form"
:rules="rules"
ref="formRef"
:disabled="action === 'view'"
>
:disabled="action === 'view'">
<template v-if="form.type == 2 && form.way == 1">
<!-- 图片AI -->
<el-form-item label="图片风格" prop="extend_info.picture_style">
......@@ -85,6 +86,20 @@ async function handleNext() {
<el-input type="textarea" :rows="3" v-model="form.extend_info.important_info_desc" />
</el-form-item>
</template>
<template v-else-if="form.way == 3">
<el-form-item label="所属行业" prop="industry_id">
<el-select v-model="form.industry_id">
<el-option v-for="item in industryList" :key="item.id" :label="item.name" :value="item.id + ''"></el-option>
</el-select>
</el-form-item>
<el-form-item label="使用场景" prop="extend_info.scenario_type">
<el-radio-group v-model="form.extend_info.scenario_type">
<el-radio v-for="item in materialDesignUsageList" :key="item.id" :value="item.value">{{
item.label
}}</el-radio>
</el-radio-group>
</el-form-item>
</template>
<template v-else>
<el-form-item label="文本用途" prop="extend_info.text_use">
<el-radio-group v-model="form.extend_info.text_use">
......@@ -133,8 +148,7 @@ async function handleNext() {
type="textarea"
:rows="4"
v-model="form.key_points"
placeholder="请输入内容的核心内容或者关键点,多个请使用英文“,”号进行隔离。"
/>
placeholder="请输入内容的核心内容或者关键点,多个请使用英文“,”号进行隔离。" />
</el-form-item>
</template>
</el-form>
......
......@@ -24,7 +24,8 @@ const form: any = reactive({
channel: '',
key_points: '',
content: '',
extend_info: {}
extend_info: {},
teacher_id: '',
})
const detail = ref()
......@@ -50,7 +51,7 @@ function handleNext() {
// 下一步提交
async function handleNextAndSubmit() {
if (!form.id && form.way == 1) {
const params = pickBy(form, item => item !== '' && item != '0')
const params = pickBy(form, (item) => item !== '' && item != '0')
const res = await createMaterial(params)
form.id = res.data.id
}
......@@ -62,7 +63,7 @@ async function handleSubmit() {
}
// 创建
async function handleCreate() {
const params = pickBy(form, item => item !== '' && item != '0')
const params = pickBy(form, (item) => item !== '' && item != '0')
await createMaterial(params)
ElMessage.success('创建成功')
router.replace('/material')
......@@ -70,7 +71,7 @@ async function handleCreate() {
// 修改
async function handleUpdate() {
const params = pickBy(form, item => item !== '' && item != '0')
const params = pickBy(form, (item) => item !== '' && item != '0')
await updateMaterial(params)
ElMessage.success('修改成功')
router.replace('/material')
......@@ -83,10 +84,15 @@ async function handleUpdate() {
<el-tab-pane lazy label="第1步" :name="1" disabled>
<StepOne v-model="form" :action="action" style="max-width: 1000px; margin: 0 auto" @next="handleNext"></StepOne>
</el-tab-pane>
<el-tab-pane lazy label="第2步" :name="2" disabled v-if="form.way !== '3'">
<StepTwo v-model="form" :action="action" style="max-width: 1000px; margin: 0 auto" @prev="handlePrev" @next="handleNextAndSubmit"></StepTwo>
<el-tab-pane lazy label="第2步" :name="2" disabled>
<StepTwo
v-model="form"
:action="action"
style="max-width: 1000px; margin: 0 auto"
@prev="handlePrev"
@next="handleNextAndSubmit"></StepTwo>
</el-tab-pane>
<el-tab-pane lazy :label="form.way === '3' ? '第二步' : '第3步'" :name="form.way === '3' ? 2 : 3" disabled>
<el-tab-pane lazy label="第3步" :name="3" disabled>
<StepThree v-model="form" :action="action" @prev="handlePrev" @submit="handleSubmit"></StepThree>
</el-tab-pane>
</el-tabs>
......
......@@ -33,16 +33,18 @@ const form = reactive(
type: '',
format: '',
status: '1',
type_name: ''
type_name: '',
category: '',
}
)
const rules = ref<FormRules>({
name: [{ required: true, message: '请输入' }],
english_name: [{ required: true, message: '请输入' }],
category: [{ required: true, message: '请选择' }],
type: [{ required: true, message: '请选择' }],
format: [{ required: true, message: '请输入' }],
status: [{ required: true, message: '请选择' }]
status: [{ required: true, message: '请选择' }],
})
// 提交
......@@ -71,6 +73,7 @@ function handleUpdate() {
// 属性字段类型
const experimentAttributeOptions = $ref(store.getMapValuesByKey('experiment_attribute_type'))
const membersCategory = $ref(store.getMapValuesByKey('experiment_meta_members_category'))
// 属性字段类型选择后->格式的placeholder改变
const formatPlaceholder = computed(() => {
......@@ -132,6 +135,9 @@ const popoverText = function (type: any) {
<el-form-item label="属性名称" prop="name">
<el-input v-model="form.name" placeholder="请输入" />
</el-form-item>
<el-form-item label="属性类型" prop="category">
<el-select-v2 v-model="form.category" style="width: 100%" placeholder="请选择" :options="membersCategory" />
</el-form-item>
<el-form-item label="属性字段类型" prop="type">
<el-select
:disabled="isUpdate"
......
<script setup lang="ts">
import type { UserProp } from '../types'
import { useMapStore } from '@/stores/map'
import { getNameByValue } from '@/utils/dictionary'
interface Props {
data: UserProp
}
defineProps<Props>()
const props = defineProps<Props>()
const store = useMapStore()
const category = computed(() => {
return getNameByValue(props.data.category, store.getMapValuesByKey('experiment_meta_members_category'))
})
</script>
<template>
......@@ -19,6 +27,9 @@ defineProps<Props>()
<el-form-item label="属性名称">
{{ data.name }}
</el-form-item>
<el-form-item label="属性类型">
{{ category }}
</el-form-item>
<el-form-item label="属性字段类型">
{{ data.type_name }}
</el-form-item>
......
......@@ -9,4 +9,5 @@ export interface UserProp {
status_name: string
updated_operator_name: string
updated_time: string
category: string
}
......@@ -5,6 +5,7 @@ import { ElMessageBox, ElMessage } from 'element-plus'
import AppList from '@/components/base/AppList.vue'
import { getMemberMeta, deleteMemberMeta } from '../api'
import { useMapStore } from '@/stores/map'
import { getNameByValue } from '@/utils/dictionary'
const store = useMapStore()
......@@ -17,7 +18,7 @@ const listOptions = computed(() => {
return {
remote: {
httpRequest: getMemberMeta,
params: { name: '', status: '', type: '' }
params: { name: '', status: '', type: '' },
},
filters: [
{ type: 'input', prop: 'name', placeholder: '请输入用户属性名称' },
......@@ -25,7 +26,7 @@ const listOptions = computed(() => {
type: 'select',
prop: 'type',
placeholder: '请选择属性字段类型',
options: store.getMapValuesByKey('experiment_attribute_type')
options: store.getMapValuesByKey('experiment_attribute_type'),
},
{
type: 'select',
......@@ -33,14 +34,21 @@ const listOptions = computed(() => {
placeholder: '请选择生效状态',
options: [
{ label: '有效', value: '1' },
{ label: '失效', value: '0' }
]
}
{ label: '失效', value: '0' },
],
},
],
columns: [
{ label: '序号', type: 'index', width: 60 },
{ label: '属性ID', prop: 'id' },
{ label: '属性名称', prop: 'name' },
{
label: '属性类型',
prop: 'category',
computed({ row }: any) {
return getNameByValue(row.category, store.getMapValuesByKey('experiment_meta_members_category'))
},
},
{ label: '属性英文名称', prop: 'english_name' },
{ label: '属性字段类型', prop: 'type_name' },
{ label: '属性字段格式', prop: 'format' },
......@@ -51,12 +59,12 @@ const listOptions = computed(() => {
return row.row.status === '0'
? `<span style="color: rgb(170, 2, 49)">${row.row.status_name}</span>`
: `<span style="color: #00ac27">${row.row.status_name}</span>`
}
},
},
{ label: '更新人', prop: 'updated_operator_name' },
{ label: '更新时间', prop: 'updated_time' },
{ label: '操作', slots: 'table-x', width: 240 }
]
{ label: '操作', slots: 'table-x', width: 240 },
],
}
})
......
import httpRequest from '@/utils/axios'
import type { StrategyListRequest, StrategyCreateRequest, StrategyUpdateRequest } from './types'
// 获取策略列表
export function getStrategyList(params?: StrategyListRequest) {
return httpRequest.get('/api/lab/v1/experiment/operational-strategies/list', { params })
}
// 创建策略
export function createStrategy(data: StrategyCreateRequest) {
return httpRequest.post('/api/lab/v1/experiment/operational-strategies/create', data)
}
// 更新策略
export function updateStrategy(data: StrategyUpdateRequest) {
return httpRequest.post('/api/lab/v1/experiment/operational-strategies/update', data)
}
// 删除策略
export function deleteStrategy(data: { id: string }) {
return httpRequest.post('/api/lab/v1/experiment/operational-strategies/delete', data)
}
// 获取策略详情
export function getStrategyInfo(params: { id: string }) {
return httpRequest.get('/api/lab/v1/experiment/operational-strategies/view', { params })
}
<script setup lang="ts">
import type { Strategy } from '../types'
import type { FormInstance, FormRules } from 'element-plus'
import { ElMessage } from 'element-plus'
import { updateStatusRuleList, dateUnitList, weekList } from '@/utils/dictionary'
import { createStrategy, updateStrategy, getStrategyInfo } from '../api'
import { pick } from 'lodash-es'
import { useMapStore } from '@/stores/map'
import LabelRule from '@/components/rule/LabelRule.vue'
import GroupRule from '@/components/rule/GroupRule.vue'
import { useConnection } from '@/composables/useAllData'
import { useChat } from '@/composables/useChat'
import { getNameByValue } from '@/utils/dictionary'
const props = defineProps<{
data?: Strategy
}>()
const emit = defineEmits<{
(e: 'update'): void
(e: 'update:modelValue', visible: boolean): void
}>()
const typeList = useMapStore().getMapValuesByKey('strategy_type')
const industryList = useMapStore().getMapValuesByKey('strategy_industry')
const isUpdate = $computed(() => !!props.data?.id)
const title = $computed(() => (isUpdate ? '修改运营策略' : '新建运营策略'))
const { connectionList } = useConnection()
const formRef = $ref<FormInstance>()
const form: any = reactive({
id: '',
name: '',
type: '',
industry: '',
update_status: '2',
update_rule: { type: 1, info: 1 },
status: '1',
target: '',
scene: '',
tags_id: { current_logic_operate: 'and', items: [] },
groups_id: { current_logic_operate: 'and', items: [] },
time: [],
connections_id: '',
})
function fetchInfo() {
if (!props.data?.id) return
getStrategyInfo({ id: props.data.id }).then(({ data }) => {
const tagRule = data.tags_id ? JSON.parse(data.tags_id) : { current_logic_operate: 'and', items: [] }
const groupRule = data.groups_id ? JSON.parse(data.groups_id) : { current_logic_operate: 'and', items: [] }
Object.assign(form, data, {
update_rule: { type: 1, info: 1 },
tags_id: tagRule,
groups_id: groupRule,
time: data.time ? data.time.split(',') : [],
})
})
}
watchEffect(() => fetchInfo())
const rules = ref<FormRules>({
name: [{ required: true, message: '请输入策略名称' }],
type: [{ required: true, message: '请选择策略类型' }],
industry: [{ required: true, message: '请选择行业' }],
update_status: [{ required: true, message: '请选择更新评率' }],
})
// 提交
function handleSubmit() {
formRef?.validate().then(() => (isUpdate ? handleUpdate() : handleCreate()))
}
// 新建
function handleCreate() {
const params = pick({ ...form, update_rule: JSON.stringify(form.update_rule) }, [
'name',
'type',
'industry',
'update_status',
'update_rule',
'status',
])
createStrategy(params).then(() => {
ElMessage({ message: '创建成功', type: 'success' })
emit('update')
emit('update:modelValue', false)
})
}
// 修改
function handleUpdate() {
const params = pick(
{
...form,
time: form.time.join(','),
update_rule: JSON.stringify(form.update_rule),
tags_id: JSON.stringify(form.tags_id),
groups_id: JSON.stringify(form.groups_id),
},
[
'id',
'name',
'type',
'industry',
'update_status',
'update_rule',
'status',
'target',
'scene',
'tags_id',
'groups_id',
'time',
'connections_id',
]
)
updateStrategy(params).then(() => {
ElMessage({ message: '修改成功', type: 'success' })
emit('update')
emit('update:modelValue', false)
})
}
const { post, messages, isLoading } = useChat()
watch(
messages,
() => {
const lastMessage: any = messages.value[messages.value.length - 1]
if (lastMessage && aiActive.value === 1) {
form.target = lastMessage.content
}
if (lastMessage && aiActive.value === 2) {
form.scene = lastMessage.content
}
},
{ deep: true }
)
const aiActive = ref(1)
function handleAIGenerate(index: number) {
aiActive.value = index
let content = ''
// 商品卖点
if (index === 1) {
content = `我的运营策略行业是${getNameByValue(form.industry, industryList)},策略类型为:${getNameByValue(
form.type,
typeList
)},请给出相应的策略目标`
}
if (index === 2) {
content = `我的运营策略行业是${getNameByValue(form.industry, industryList)},策略类型为:${getNameByValue(
form.type,
typeList
)},策略目标为:${form.target},请给出适合的场景信息`
}
post({ role: 'user', content }, index === 3)
}
</script>
<template>
<el-dialog :title="title" :close-on-click-modal="false" width="980px" @closed="$emit('update:modelValue', false)">
<el-form ref="formRef" :model="form" :rules="rules" label-suffix=":" label-width="94px">
<el-form-item label="策略名称" prop="name">
<el-input v-model="form.name" placeholder="请输入" />
</el-form-item>
<el-form-item label="行业" prop="industry">
<el-select v-model="form.industry" style="width: 100%" :disabled="isUpdate">
<el-option v-for="item in industryList" :key="item.value" :label="item.label" :value="item.value"></el-option>
</el-select>
</el-form-item>
<el-form-item label="策略类型" prop="type">
<el-select v-model="form.type" style="width: 100%" :disabled="isUpdate">
<el-option v-for="item in typeList" :key="item.value" :label="item.label" :value="item.value"></el-option>
</el-select>
</el-form-item>
<el-form-item label="更新频率" prop="update_status">
<el-radio-group v-model="form.update_status">
<el-radio
v-for="item in updateStatusRuleList"
:key="item.value"
:value="item.value"
:disabled="item.value === '1'">
{{ item.label }}
</el-radio>
</el-radio-group>
<div class="update-rule-wrap" v-if="form.update_status === '1'">
<span></span>
<el-select v-model="form.update_rule.type" placeholder=" " style="width: 60px">
<el-option
v-for="item in dateUnitList"
:key="item.value"
:label="item.label"
:value="item.value"></el-option>
</el-select>
<template v-if="form.update_rule.type === 1">
<span>的凌晨更新</span>
</template>
<template v-if="form.update_rule.type === 2">
<span></span>
<el-select v-model="form.update_rule.info" placeholder=" " style="width: 80px">
<el-option v-for="item in weekList" :key="item.value" :label="item.label" :value="item.value"></el-option>
</el-select>
<span>的凌晨更新</span>
</template>
<template v-if="form.update_rule.type === 3">
<span></span>
<el-select v-model="form.update_rule.info" placeholder=" " style="width: 60px">
<el-option v-for="item in 6" :key="item" :label="item" :value="item"></el-option>
</el-select>
<span>天的凌晨更新</span>
</template>
</div>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-switch v-model="form.status" active-text="生效" active-value="1" inactive-text="失效" inactive-value="0" />
</el-form-item>
<template v-if="isUpdate">
<el-tag style="margin: 20px 0">目标与场景</el-tag>
<el-card shadow="never">
<template #header>输入策略目标</template>
<el-form-item prop="target" label-width="auto">
<el-input
type="textarea"
v-model="form.target"
:rows="6"
placeholder="例如:提升新用户注册率20%,提升月活跃用户数15%,提升付费转化率5%等" />
</el-form-item>
<el-row justify="end">
<el-button type="primary" @click="handleAIGenerate(1)" :loading="isLoading && aiActive === 1"
>AI辅助梳理</el-button
>
</el-row>
</el-card>
<el-card shadow="never" style="margin-top: 20px">
<template #header>输入策略场景</template>
<el-form-item prop="scene" label-width="auto">
<el-input
type="textarea"
v-model="form.scene"
:rows="6"
placeholder="例如:新用户首次访问APP第N日,用户连续N天未登录,用户将商品加入购物车但是3小时未支付。" />
</el-form-item>
<el-row justify="end">
<el-button type="primary" @click="handleAIGenerate(2)" :loading="isLoading && aiActive === 2"
>AI辅助梳理</el-button
>
</el-row>
</el-card>
<el-tag style="margin: 20px 0">目标人群</el-tag>
<LabelRule v-model="form.tags_id" buttonText="添加标签">
<template #header>请选择标签</template>
</LabelRule>
<GroupRule v-model="form.groups_id" buttonText="添加群组" style="margin-top: 20px">
<template #header>请选择群组</template>
</GroupRule>
<el-tag style="margin: 20px 0">运营计划</el-tag>
<el-form-item label="运营时间" prop="time">
<el-date-picker v-model="form.time" type="datetimerange" value-format="YYYY-MM-DD HH:mm:ss" />
</el-form-item>
<el-form-item label="运营渠道" prop="connections_id">
<el-select v-model="form.connections_id">
<el-option v-for="item in connectionList" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</el-form-item>
</template>
</el-form>
<template #footer>
<el-row justify="center">
<el-button plain auto-insert-space @click="$emit('update:modelValue', false)">关闭</el-button>
<el-button type="primary" auto-insert-space @click="handleSubmit">保存</el-button>
</el-row>
</template>
</el-dialog>
</template>
<style lang="scss">
.update-rule-wrap {
width: 100%;
.el-select {
margin: 0 10px;
}
}
</style>
<script setup lang="ts">
import type { Strategy } from '../types'
import type { FormInstance } from 'element-plus'
import { updateStatusRuleList, dateUnitList, weekList } from '@/utils/dictionary'
import { getStrategyInfo } from '../api'
import { useMapStore } from '@/stores/map'
import LabelRule from '@/components/rule/LabelRule.vue'
import GroupRule from '@/components/rule/GroupRule.vue'
import { useConnection } from '@/composables/useAllData'
const props = defineProps<{
data?: Strategy
}>()
defineEmits<{
(e: 'update'): void
(e: 'update:modelValue', visible: boolean): void
}>()
const typeList = useMapStore().getMapValuesByKey('strategy_type')
const industryList = useMapStore().getMapValuesByKey('strategy_industry')
const isUpdate = $computed(() => !!props.data?.id)
const title = $computed(() => (isUpdate ? '修改运营策略' : '新建运营策略'))
const { connectionList } = useConnection()
const formRef = $ref<FormInstance>()
const form: any = reactive({
id: '',
name: '',
type: '',
industry: '',
update_status: '2',
update_rule: { type: 1, info: 1 },
status: '1',
target: '',
scene: '',
tags_id: { current_logic_operate: 'and', items: [] },
groups_id: { current_logic_operate: 'and', items: [] },
time: [],
connections_id: '',
})
function fetchInfo() {
if (!props.data?.id) return
getStrategyInfo({ id: props.data.id }).then(({ data }) => {
const tagRule = data.tags_id ? JSON.parse(data.tags_id) : { current_logic_operate: 'and', items: [] }
const groupRule = data.groups_id ? JSON.parse(data.groups_id) : { current_logic_operate: 'and', items: [] }
Object.assign(form, data, {
update_rule: { type: 1, info: 1 },
tags_id: tagRule,
groups_id: groupRule,
time: data.time ? data.time.split(',') : [],
})
})
}
watchEffect(() => fetchInfo())
</script>
<template>
<el-dialog :title="title" :close-on-click-modal="false" width="980px" @closed="$emit('update:modelValue', false)">
<el-form ref="formRef" :model="form" label-suffix=":" label-width="94px" disabled>
<el-form-item label="策略名称" prop="name">
<el-input v-model="form.name" placeholder="请输入" />
</el-form-item>
<el-form-item label="行业" prop="industry">
<el-select v-model="form.industry" style="width: 100%" :disabled="isUpdate">
<el-option v-for="item in industryList" :key="item.value" :label="item.label" :value="item.value"></el-option>
</el-select>
</el-form-item>
<el-form-item label="策略类型" prop="type">
<el-select v-model="form.type" style="width: 100%" :disabled="isUpdate">
<el-option v-for="item in typeList" :key="item.value" :label="item.label" :value="item.value"></el-option>
</el-select>
</el-form-item>
<el-form-item label="更新频率" prop="update_status">
<el-radio-group v-model="form.update_status">
<el-radio
v-for="item in updateStatusRuleList"
:key="item.value"
:value="item.value"
:disabled="item.value === '1'">
{{ item.label }}
</el-radio>
</el-radio-group>
<div class="update-rule-wrap" v-if="form.update_status === '1'">
<span></span>
<el-select v-model="form.update_rule.type" placeholder=" " style="width: 60px">
<el-option
v-for="item in dateUnitList"
:key="item.value"
:label="item.label"
:value="item.value"></el-option>
</el-select>
<template v-if="form.update_rule.type === 1">
<span>的凌晨更新</span>
</template>
<template v-if="form.update_rule.type === 2">
<span></span>
<el-select v-model="form.update_rule.info" placeholder=" " style="width: 80px">
<el-option v-for="item in weekList" :key="item.value" :label="item.label" :value="item.value"></el-option>
</el-select>
<span>的凌晨更新</span>
</template>
<template v-if="form.update_rule.type === 3">
<span></span>
<el-select v-model="form.update_rule.info" placeholder=" " style="width: 60px">
<el-option v-for="item in 6" :key="item" :label="item" :value="item"></el-option>
</el-select>
<span>天的凌晨更新</span>
</template>
</div>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-switch v-model="form.status" active-text="生效" active-value="1" inactive-text="失效" inactive-value="0" />
</el-form-item>
<template v-if="isUpdate">
<el-tag style="margin: 20px 0">目标与场景</el-tag>
<el-card shadow="never">
<template #header>输入策略目标</template>
<el-form-item prop="target" label-width="auto">
<el-input
type="textarea"
v-model="form.target"
:rows="6"
placeholder="例如:提升新用户注册率20%,提升月活跃用户数15%,提升付费转化率5%等" />
</el-form-item>
</el-card>
<el-card shadow="never" style="margin-top: 20px">
<template #header>输入策略场景</template>
<el-form-item prop="scene" label-width="auto">
<el-input
type="textarea"
v-model="form.scene"
:rows="6"
placeholder="例如:新用户首次访问APP第N日,用户连续N天未登录,用户将商品加入购物车但是3小时未支付。" />
</el-form-item>
</el-card>
<el-tag style="margin: 20px 0">目标人群</el-tag>
<LabelRule v-model="form.tags_id" buttonText="添加标签">
<template #header>请选择标签</template>
</LabelRule>
<GroupRule v-model="form.groups_id" buttonText="添加群组" style="margin-top: 20px">
<template #header>请选择群组</template>
</GroupRule>
<el-tag style="margin: 20px 0">运营计划</el-tag>
<el-form-item label="运营时间" prop="time">
<el-date-picker v-model="form.time" type="datetimerange" value-format="YYYY-MM-DD HH:mm:ss" />
</el-form-item>
<el-form-item label="运营渠道" prop="connections_id">
<el-select v-model="form.connections_id">
<el-option v-for="item in connectionList" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</el-form-item>
</template>
</el-form>
<template #footer>
<el-row justify="center">
<el-button plain auto-insert-space @click="$emit('update:modelValue', false)">关闭</el-button>
</el-row>
</template>
</el-dialog>
</template>
<style lang="scss">
.update-rule-wrap {
width: 100%;
.el-select {
margin: 0 10px;
}
}
</style>
import type { RouteRecordRaw } from 'vue-router'
import Layout from '@/components/layout/Index.vue'
const routes: RouteRecordRaw[] = [
{
path: '/strategy',
component: Layout,
children: [{ path: '', component: () => import('./views/Index.vue') }]
}
]
export { routes }
import type { Operator } from '@/types'
// 策略
export interface Strategy {
id: string
name: string
type: string
industry: string
status: string
update_status: string // '1' | '2'
update_rule: string
created_time: string
created_operator: Operator
updated_time: string
updated_operator: Operator
}
// 策略更新规则
export interface StrategyUpdateRule {
type: 1 | 2 | 3
info: number
}
export type StrategyListRequest = Pick<Strategy, 'id' | 'name'> & { experiment_id?: string }
export type StrategyUpdateRequest = Pick<
Strategy,
'id' | 'name' | 'type' | 'industry' | 'status' | 'update_status' | 'update_rule'
> & {
experiment_id?: string
}
export type StrategyCreateRequest = Omit<StrategyUpdateRequest, 'id'>
<script setup lang="ts">
import type { Strategy } from '../types'
import { Plus, Delete } from '@element-plus/icons-vue'
import AppList from '@/components/base/AppList.vue'
import { ElMessageBox, ElMessage } from 'element-plus'
import { getStrategyList, deleteStrategy } from '../api'
import { useMapStore } from '@/stores/map'
import { getNameByValue, updateStatusRuleList } from '@/utils/dictionary'
import SelectUser from '@/components/SelectUser.vue'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const FormDialog = defineAsyncComponent(() => import('../components/FormDialog.vue'))
const ViewDialog = defineAsyncComponent(() => import('../components/ViewDialog.vue'))
const typeList = useMapStore().getMapValuesByKey('strategy_type')
const industryList = useMapStore().getMapValuesByKey('strategy_industry')
const statusList = useMapStore().getMapValuesByKey('system_status')
const appList = $ref<InstanceType<typeof AppList> | null>(null)
// 列表配置
const listParams = reactive({ name: '', type_id: '', status: '', updated_operator: '' })
const listOptions = computed(() => {
return {
remote: {
httpRequest: getStrategyList,
params: listParams,
beforeRequest(params: any, isReset: boolean) {
if (isReset) {
listParams.updated_operator = ''
listParams.type_id = ''
} else {
params.updated_operator = listParams.updated_operator
}
return params
},
},
filters: [
{ type: 'input', prop: 'name', placeholder: '请输入策略名称' },
{
type: 'select',
prop: 'industry',
placeholder: '请选择策略行业',
options: industryList,
},
{
type: 'select',
prop: 'type',
placeholder: '请选择策略类型',
options: typeList,
},
{ type: 'select', prop: 'status', placeholder: '请选择策略状态', options: statusList },
{ type: 'input', prop: 'updated_operator', placeholder: '更新人', slots: 'filter-user' },
],
columns: [
{ type: 'selection' },
{ label: '序号', type: 'index', width: 60 },
{ label: '策略ID', prop: 'id' },
{ label: '策略名称', prop: 'name' },
{
label: '策略类型',
prop: 'type',
computed({ row }: { row: Strategy }) {
return getNameByValue(row.type, typeList)
},
},
{
label: '策略行业',
prop: 'industry',
computed({ row }: { row: Strategy }) {
return getNameByValue(row.industry, industryList)
},
},
{
label: '更新频率',
prop: 'update_status',
computed({ row }: { row: Strategy }) {
return getNameByValue(row.update_status, updateStatusRuleList)
},
},
{
label: '状态',
prop: 'status',
computed({ row }: { row: Strategy }) {
const color = row.status === '1' ? 'var(--main-success-color)' : 'var(--main-color)'
return `<span style="color: ${color}">${getNameByValue(row.status, statusList)}</span>`
},
},
{
label: '更新人',
prop: 'updated_operator_name',
},
{ label: '更新时间', prop: 'updated_time' },
{ label: '操作', slots: 'table-x', width: 320 },
],
}
})
// 刷新
function handleRefresh() {
appList?.refetch()
}
let formVisible = $ref(false)
let currentRow = $ref<Strategy>()
// 新建
function handleAdd() {
currentRow = undefined
formVisible = true
}
// 修改
function handleUpdate(row: Strategy) {
currentRow = row
formVisible = true
}
// 删除
function handleRemove(row: Strategy) {
ElMessageBox.confirm('确定要删除该策略吗?', '提示').then(() => {
deleteStrategy({ id: row.id }).then(() => {
ElMessage({ message: '删除成功', type: 'success' })
handleRefresh()
})
})
}
// 查看
let viewVisible = $ref(false)
function handleView(row: Strategy) {
currentRow = row
viewVisible = true
}
let multipleSelection = $ref<Strategy[]>([])
function handleSelectionChange(selection: Strategy[]) {
multipleSelection = selection
}
const handleRemoves = async function () {
const ids = multipleSelection.map((item) => item.id)
await ElMessageBox.confirm('确定要删除选中的策略数据吗?', '提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
})
await deleteStrategy({ id: ids.join(',') })
appList?.refetch(true)
ElMessage({ message: '删除成功', type: 'success' })
}
</script>
<template>
<AppCard>
<AppList v-bind="listOptions" ref="appList" class="label-right" @selection-change="handleSelectionChange">
<template #header-buttons>
<div style="display: flex; justify-content: space-between">
<div>
<el-button type="primary" :icon="Plus" @click="handleAdd" v-if="!userStore.status.tag_status"
>新建</el-button
>
<el-button
type="primary"
plain
:icon="Delete"
:disabled="!multipleSelection.length"
@click="handleRemoves"
v-permission="'experiment_tag_delete'"
>删除</el-button
>
</div>
</div>
</template>
<template #filter-user>
<SelectUser v-model="listParams.updated_operator" placeholder="更新人" @change="handleRefresh"></SelectUser>
</template>
<template #table-x="{ row }">
<el-button type="primary" plain @click="handleView(row)">查看</el-button>
<el-button type="primary" plain @click="handleUpdate(row)" v-permission="'experiment_tag_update'"
>编辑</el-button
>
<el-button type="primary" plain @click="handleRemove(row)" v-permission="'experiment_tag_delete'"
>删除</el-button
>
</template>
</AppList>
</AppCard>
<!-- 新建/修改策略 -->
<FormDialog v-model="formVisible" :data="currentRow" @update="handleRefresh" v-if="formVisible"></FormDialog>
<!-- 查看策略 -->
<ViewDialog v-model="viewVisible" :data="currentRow" v-if="viewVisible && currentRow"></ViewDialog>
</template>
import httpRequest from '@/utils/axios'
// 人员列表
export function getMemberList(params: { name?: string; id?: string; mobile?: string; status?: string; page?: number; page_size?: number }) {
export function getMemberList(params: {
name?: string
id?: string
mobile?: string
status?: string
page?: number
page_size?: number
}) {
return httpRequest.get('/api/lab/v1/experiment/member/list', { params })
}
......@@ -16,7 +23,14 @@ export function getMemberFieldsList() {
}
// 新建用户
export function createMember(data: { name: string; status: string; experiment_connection_id: string; gender: string; mobile: string; fields: string }) {
export function createMember(data: {
name: string
status: string
experiment_connection_id: string
gender: string
mobile: string
fields: string
}) {
return httpRequest.post('/api/lab/v1/experiment/member/create', data)
}
......@@ -79,7 +93,13 @@ export function importEvent(data: { event_id: string; url: string; name: string;
// headers: { 'Content-Type': 'multipart/form-data' }
// })
// }
export function importMember(data: { groups_id?: string; connection_id: string; url: string; name: string; size: string }) {
export function importMember(data: {
groups_id?: string
connection_id: string
url: string
name: string
size: string
}) {
return httpRequest.post('/api/lab/v1/experiment/member/member-upload', data)
}
......@@ -96,3 +116,21 @@ export function getProgress(params: { page?: number; 'per-page'?: number }) {
export function getConnectionList() {
return httpRequest.get('/api/lab/v1/experiment/member/has-data-connections')
}
// 同步用户和事件数据
export function syncMember() {
return httpRequest.get('/api/lab/v1/experiment/member/sync-member')
}
// 学生用户列表-清空数据(lab系统)
export function clearMember() {
return httpRequest.get('/api/lab/v1/experiment/member/clear')
}
// AI分析与总结
export function getAISummary(params: { member_id: string }) {
return httpRequest.get('/api/lab/v1/experiment/member/ai-one-person', {
params,
responseType: 'stream',
adapter: 'fetch',
})
}
<script setup>
import { getAISummary } from '../api'
import { aiStreamParse } from '@/utils/parse'
import VueMarkdown from 'vue-markdown-render'
const props = defineProps(['id'])
const content = ref('')
const isLoading = ref(false)
async function fetchAI() {
isLoading.value = true
const stream = await getAISummary({ member_id: props.id })
aiStreamParse(stream, (json, messageContent) => {
content.value += messageContent
})
isLoading.value = false
}
onMounted(() => {
fetchAI()
})
</script>
<template>
<el-dialog title="AI用户画像分析与建议">
<div v-loading="isLoading">
<VueMarkdown :source="content" class="markdown-body" />
</div>
<template #footer>
<el-row justify="center">
<el-button round @click="$emit('update:modelValue', false)">关闭</el-button>
</el-row>
</template>
</el-dialog>
</template>
......@@ -7,6 +7,8 @@ import Icon from '@/components/ConnectionIcon.vue'
const ViewEvent = defineAsyncComponent(() => import('@/components/ViewEvent.vue'))
const ViewLabel = defineAsyncComponent(() => import('@/components/ViewLabel.vue'))
const ViewGroup = defineAsyncComponent(() => import('@/components/ViewGroup.vue'))
const AISummaryDialog = defineAsyncComponent(() => import('../components/AISummaryDialog.vue'))
const aiDialogVisible = ref(false)
const route = useRoute()
......@@ -20,7 +22,7 @@ let data = $ref<ImageProp>()
let fieldsList = $ref<MemberFieldsProp[]>([])
onMounted(() => {
// 画像
getMemberImage({ id: userId.value, 'per-page': 100 }).then(res => {
getMemberImage({ id: userId.value, 'per-page': 100 }).then((res) => {
data = res.data
getFields(res.data)
})
......@@ -28,7 +30,7 @@ onMounted(() => {
})
const getFields = function (data: { fields: any }) {
getMemberFieldsList().then(res => {
getMemberFieldsList().then((res) => {
fieldsList = res.data.map((item: MemberFieldsProp) => {
if (data.fields[item.id]) {
item.value = data.fields[item.id]
......@@ -90,7 +92,7 @@ async function fetchEvent(isReset = false) {
id: userId.value,
connection_id: currentConnection.value,
page: event.page,
'per-page': 20
'per-page': 20,
})
Object.assign(event, { page: event.page + 1, total: data.events.total, list: [...event.list, ...data.events.list] })
}
......@@ -128,7 +130,9 @@ watch(currentConnection, () => {
</el-form>
<el-form label-suffix=":" label-width="110px">
<el-form-item label="最近活跃时间">{{ data.updated_time }} </el-form-item>
<el-form-item label="最近活跃时间" style="opacity: 0">{{ data.updated_time }} </el-form-item>
<el-form-item label-width="0">
<el-button type="primary" @click="aiDialogVisible = true">AI分析与总结</el-button>
</el-form-item>
</el-form>
</div>
</div>
......@@ -256,6 +260,8 @@ watch(currentConnection, () => {
v-model="viewGroupVisible"
:data="currentViewGroup"
v-if="viewGroupVisible && currentViewGroup"></ViewGroup>
<AISummaryDialog v-model="aiDialogVisible" :id="userId" v-if="aiDialogVisible"></AISummaryDialog>
</template>
<style lang="scss">
.info-box {
......
<script setup lang="ts">
import { Plus, Download, Upload, Delete } from '@element-plus/icons-vue'
import AppList from '@/components/base/AppList.vue'
import { getMemberList, deleteMember, getMemberConnectionsList } from '../api'
import { getMemberList, deleteMember, getMemberConnectionsList, syncMember, clearMember } from '../api'
import { ElMessage, ElMessageBox, ElLoading } from 'element-plus'
import type { MemberProp, ConnectionsProp } from '../types'
import { useUserStore } from '@/stores/user'
......@@ -33,7 +33,7 @@ const listOptions = computed(() => {
return {
remote: {
httpRequest: getMemberList,
params: { name: '', id: '', mobile: '', status: '', experiment_connection_id: '' }
params: { name: '', id: '', mobile: '', status: '', experiment_connection_id: '' },
},
filters: [
{ type: 'input', prop: 'name', placeholder: '请输入用户姓名' },
......@@ -45,8 +45,8 @@ const listOptions = computed(() => {
placeholder: '请选择生效状态',
options: [
{ label: '有效', value: '1' },
{ label: '失效', value: '0' }
]
{ label: '失效', value: '0' },
],
},
{
type: 'select',
......@@ -54,8 +54,8 @@ const listOptions = computed(() => {
placeholder: '请选择来源链接',
options: connectionOptions,
labelKey: 'type_name',
valueKey: 'id'
}
valueKey: 'id',
},
],
columns: [
{ type: 'selection' },
......@@ -72,12 +72,12 @@ const listOptions = computed(() => {
return row.row.status === '0'
? `<span style="color: rgb(170, 2, 49)">${row.row.status_name}</span>`
: `<span style="color: #00ac27">${row.row.status_name}</span>`
}
},
},
{ label: '更新人', prop: 'updated_operator_name' },
{ label: '更新时间', prop: 'updated_time' },
{ label: '操作', slots: 'table-x', width: 380 }
]
{ label: '操作', slots: 'table-x', width: 380 },
],
}
})
......@@ -109,7 +109,7 @@ const handleRemove = function (row: { id: string; have_event: boolean }) {
ElMessageBox.confirm('此操作将会删除用户及所有关联事件数据', '提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning'
type: 'warning',
}).then(() => {
deleteMembers({ ids: row.id })
})
......@@ -123,7 +123,7 @@ const handleRemoves = function (isAll?: boolean) {
ElMessageBox.confirm('确定要删除全部用户数据吗?', '提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning'
type: 'warning',
}).then(() => {
deleteMembers({ type: 'all' })
})
......@@ -131,7 +131,7 @@ const handleRemoves = function (isAll?: boolean) {
ElMessageBox.confirm('确定要删除选中的用户数据吗?', '提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning'
type: 'warning',
}).then(() => {
deleteMembers({ ids })
})
......@@ -143,7 +143,7 @@ const deleteMembers = function (params: any) {
loading = ElLoading.service({
lock: true,
text: 'Loading',
background: 'rgba(0, 0, 0, 0.7)'
background: 'rgba(0, 0, 0, 0.7)',
})
deleteMember(params).then(() => {
loading.close()
......@@ -177,7 +177,7 @@ const handleAdd = function () {
const goPage = function (row: MemberProp) {
router.push({
path: '/user/eventDetails',
query: { user_id: row.id }
query: { user_id: row.id },
})
}
......@@ -185,7 +185,7 @@ const goPage = function (row: MemberProp) {
const handleImage = function (row: MemberProp) {
router.push({
path: '/user/image',
query: { user_id: row.id }
query: { user_id: row.id },
})
}
......@@ -205,6 +205,23 @@ const downloadMember = function (isAll?: boolean) {
}
const generateEventVisible = ref(false)
// 同步用户和事件数据
const handleSync = async () => {
await syncMember()
ElMessage({ message: '同步成功', type: 'success' })
handleRefresh()
}
const handleClear = async () => {
await ElMessageBox.confirm('确定要清空用户数据吗?', '提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
})
await clearMember()
ElMessage({ message: '清空成功', type: 'success' })
handleRefresh()
}
</script>
<template>
......@@ -213,20 +230,29 @@ const generateEventVisible = ref(false)
<template #header-buttons>
<el-row justify="space-between">
<el-space>
<el-button v-if="!userStore.status.status" type="primary" :icon="Plus" @click="handleAdd" v-permission="'v1-experiment-member-create'"
<el-button
v-if="!userStore.status.status"
type="primary"
:icon="Plus"
@click="handleAdd"
v-permission="'v1-experiment-member-create'"
>新建</el-button
>
<el-button v-if="userStore.status.status" type="primary" @click="handleSync">同步用户和事件数据</el-button>
<el-dropdown v-permission="'v1-experiment-member-download'">
<el-button type="primary" :icon="Download">导出</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="downloadMember(true)">全部用户数据</el-dropdown-item>
<el-dropdown-item :disabled="!multipleSelection.length" @click="downloadMember(false)">勾选用户数据</el-dropdown-item>
<el-dropdown-item :disabled="!multipleSelection.length" @click="downloadMember(false)"
>勾选用户数据</el-dropdown-item
>
</el-dropdown-menu>
</template>
</el-dropdown>
<!-- v-permission="['v1-experiment-member-member-upload', 'v1-experiment-member-event-upload']" -->
<el-dropdown>
<el-dropdown v-if="!userStore.status.status">
<el-button type="primary" :icon="Upload">导入</el-button>
<template #dropdown>
<el-dropdown-menu>
......@@ -235,7 +261,13 @@ const generateEventVisible = ref(false)
</el-dropdown-menu>
</template>
</el-dropdown>
<el-button type="primary" @click="progressVisible = true" v-permission="'v1-experiment-member-tasks'">数据导入进度</el-button>
<el-button
type="primary"
v-if="!userStore.status.status"
@click="progressVisible = true"
v-permission="'v1-experiment-member-tasks'"
>数据导入进度</el-button
>
<!-- <el-button type="danger" plain :icon="Delete" :disabled="!multipleSelection.length" @click="handleRemoves()" v-permission="'v1-experiment-member-delete'">删除</el-button> -->
<!-- v-permission="'v1-experiment-member-delete'" -->
<el-dropdown>
......@@ -243,10 +275,13 @@ const generateEventVisible = ref(false)
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="handleRemoves(true)">删除全部用户</el-dropdown-item>
<el-dropdown-item :disabled="!multipleSelection.length" @click="handleRemoves(false)">删除勾选用户</el-dropdown-item>
<el-dropdown-item :disabled="!multipleSelection.length" @click="handleRemoves(false)"
>删除勾选用户</el-dropdown-item
>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-button type="danger" @click="handleClear" v-if="userStore.role?.id == 1">清空数据</el-button>
</el-space>
<el-space>
<router-link to="/analyze/user"><el-button type="primary">用户分析</el-button></router-link>
......@@ -257,8 +292,12 @@ const generateEventVisible = ref(false)
<template #table-x="{ row }">
<el-button type="primary" plain @click="handleImage(row)">画像</el-button>
<el-button type="primary" plain @click="handleView(row)">查看</el-button>
<el-button type="primary" plain @click="handleEdit(row)" v-permission="'v1-experiment-member-update'">编辑</el-button>
<el-button type="primary" plain @click="handleRemove(row)" v-permission="'v1-experiment-member-delete'">删除</el-button>
<el-button type="primary" plain @click="handleEdit(row)" v-permission="'v1-experiment-member-update'"
>编辑</el-button
>
<el-button type="primary" plain @click="handleRemove(row)" v-permission="'v1-experiment-member-delete'"
>删除</el-button
>
<el-button type="primary" plain @click="goPage(row)">事件</el-button>
</template>
</AppList>
......
......@@ -253,6 +253,13 @@ const adminMenus: IMenuItem[] = [
icon: markRaw(IconGroup),
// tag: 'experiment_groups',
},
{
id: 71,
name: '运营策略管理',
path: '/strategy',
icon: markRaw(IconLiveTalk),
// tag: 'experiment_groups',
},
],
},
{
......@@ -335,11 +342,24 @@ const adminMenus: IMenuItem[] = [
path: '/live',
icon: markRaw(IconLive),
children: [
{ id: 21, name: '商品品类管理', path: '/live/product/category', icon: markRaw(IconLiveProductCategory) },
{ id: 22, name: '商品属性管理', path: '/live/product/attr', icon: markRaw(IconLiveProductAttr) },
{ id: 23, name: '商品管理', path: '/live/product/management', icon: markRaw(IconLiveProductManagement) },
{
id: 21,
name: '商品品类管理',
path: '/live/product/category',
icon: markRaw(IconLiveProductCategory),
role: [5, 6],
},
{ id: 22, name: '商品属性管理', path: '/live/product/attr', icon: markRaw(IconLiveProductAttr), role: [5, 6] },
{
id: 23,
name: '商品管理',
path: '/live/product/management',
icon: markRaw(IconLiveProductManagement),
role: [5, 6],
},
{ id: 24, name: '直播话术管理', path: '/live/talk', icon: markRaw(IconLiveTalk) },
{ id: 25, name: '直播', path: '/live/test', icon: markRaw(IconLiveTest) },
{ id: 201, name: '订单管理', path: '/live/order', icon: markRaw(IconCard) },
],
},
{
......@@ -365,13 +385,14 @@ export const useMenuStore = defineStore({
getters: {
menus: (state) => {
const userStore = useUserStore()
const userRole = userStore.role?.id
const userRole = userStore.role?.id || 1
const userPermissions = userStore.menus || []
// 递归过滤菜单及其子菜单
const filterMenus = (menus: IMenuItem[]): IMenuItem[] => {
return menus
.filter((menu) => userPermissions.some((perm) => perm.id === menu.id))
.filter((menu) => menu.role ? menu.role?.includes(userRole) : true)
.map((menu) => {
const filteredMenu: IMenuItem = {
...menu,
......
......@@ -8,6 +8,7 @@ export interface IMenuItem {
studentPath?: string
icon?: Component
children?: IMenuItem[]
role?: number[]
}
// 用户信息
......@@ -143,3 +144,9 @@ export interface MaterialProp {
status: string
isView: boolean
}
// 群组规则
export interface GroupRule {
current_logic_operate: 'and' | 'or'
items: any[]
}
......@@ -126,6 +126,29 @@ export const materialUsageList = [
{ label: '系统通知 ', value: '4' },
]
// 在线设计使用场景
export const materialDesignUsageList = [
{ label: '手机海报 ', value: '447' },
{ label: '公众号首图 ', value: '216' },
{ label: '小红书配图', value: '502' },
{ label: '每日一签', value: '209' },
{ label: '全屏海报', value: '581' },
{ label: '邀请函', value: '170' },
{ label: 'PPT16:9', value: '1' },
{ label: '简历', value: '38' },
{ label: '长图海报', value: '35' },
{ label: '公众号次图', value: '40' },
{ label: '主图图标', value: '503' },
{ label: '商品主图', value: '29' },
{ label: '名片', value: '12' },
{ label: '长页H5', value: '166' },
{ label: '直播贴片', value: '511' },
{ label: '视频海报', value: '498' },
{ label: '竖版直播背景', value: '572' },
{ label: '横版海报', value: '20' },
{ label: '竖版插画', value: '233' },
]
// 使用人员
export const materialUsersList = [
{ label: '销售人员 ', value: '1' },
......
import { createParser, type EventSourceMessage } from 'eventsource-parser'
export async function parse(stream: ReadableStream<Uint8Array>, onEvent: (event: EventSourceMessage) => void) {
const parser = createParser({ onEvent })
const reader = stream.getReader()
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader.read()
if (done) break
parser.feed(decoder.decode(value, { stream: true }))
}
}
export async function aiStreamParse(
stream: ReadableStream<Uint8Array>,
onMessage: (json: any, content: string) => void
) {
parse(stream, (event) => {
if (event.data === '[DONE]') return
const json = JSON.parse(event.data)
onMessage(json, json.choices[0].delta.content)
})
}
......@@ -30,10 +30,30 @@ export default defineConfig(() => ({
// cert: fs.readFileSync(path.join(__dirname, './https/ezijing.com.pem'))
// },
proxy: {
// '/api/resource': {
// target: 'http://com-resource-admin-test.ezijing.com/',
'/api/tiangong': {
target: 'https://api.singularity-ai.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/tiangong/, ''),
},
'/api/deepseek': {
target: 'https://api.deepseek.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/deepseek/, ''),
},
'/api/qwen': {
target: 'https://dashscope.aliyuncs.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/qwen/, ''),
},
'/api/qianfan': {
target: 'https://aip.baidubce.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/qianfan/, ''),
},
// '/api/lab': {
// target: 'http://local-com-resource-api.frontend.ezijing.com',
// changeOrigin: true,
// rewrite: path => path.replace(/^\/api\/resource/, '')
// rewrite: (path) => path.replace(/^\/api\/lab/, ''),
// },
'/api': 'https://saas-dml.ezijing.com',
},
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论