提交 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 ...@@ -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_LAB_URL=http://172.16.3.203:1012/bi/?proc=0&action=index
VITE_SURVEYKING_URL=http://172.16.3.203:1011 VITE_SURVEYKING_URL=http://172.16.3.203:1011
VITE_STATIC_URL=https://saas-lab-api 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 @@ ...@@ -302,6 +302,11 @@
"onWatcherCleanup": true, "onWatcherCleanup": true,
"useId": true, "useId": true,
"useModel": true, "useModel": true,
"useTemplateRef": true "useTemplateRef": true,
"createRef": true,
"onElementRemoval": true,
"useCountdown": true,
"usePreferredReducedTransparency": true,
"useSSRWidth": true
} }
} }
...@@ -28,6 +28,7 @@ declare global { ...@@ -28,6 +28,7 @@ declare global {
const createGlobalState: typeof import('@vueuse/core')['createGlobalState'] const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
const createInjectionState: typeof import('@vueuse/core')['createInjectionState'] const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn'] const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
const createRef: typeof import('@vueuse/core')['createRef']
const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate'] const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate']
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable'] const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise'] const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise']
...@@ -62,6 +63,7 @@ declare global { ...@@ -62,6 +63,7 @@ declare global {
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onClickOutside: typeof import('@vueuse/core')['onClickOutside'] const onClickOutside: typeof import('@vueuse/core')['onClickOutside']
const onDeactivated: typeof import('vue')['onDeactivated'] const onDeactivated: typeof import('vue')['onDeactivated']
const onElementRemoval: typeof import('@vueuse/core')['onElementRemoval']
const onErrorCaptured: typeof import('vue')['onErrorCaptured'] const onErrorCaptured: typeof import('vue')['onErrorCaptured']
const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke'] const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke']
const onLongPress: typeof import('@vueuse/core')['onLongPress'] const onLongPress: typeof import('@vueuse/core')['onLongPress']
...@@ -144,6 +146,7 @@ declare global { ...@@ -144,6 +146,7 @@ declare global {
const useCloned: typeof import('@vueuse/core')['useCloned'] const useCloned: typeof import('@vueuse/core')['useCloned']
const useColorMode: typeof import('@vueuse/core')['useColorMode'] const useColorMode: typeof import('@vueuse/core')['useColorMode']
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog'] const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
const useCountdown: typeof import('@vueuse/core')['useCountdown']
const useCounter: typeof import('@vueuse/core')['useCounter'] const useCounter: typeof import('@vueuse/core')['useCounter']
const useCssModule: typeof import('vue')['useCssModule'] const useCssModule: typeof import('vue')['useCssModule']
const useCssVar: typeof import('@vueuse/core')['useCssVar'] const useCssVar: typeof import('@vueuse/core')['useCssVar']
...@@ -224,12 +227,14 @@ declare global { ...@@ -224,12 +227,14 @@ declare global {
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark'] const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages'] const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion'] const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
const usePreferredReducedTransparency: typeof import('@vueuse/core')['usePreferredReducedTransparency']
const usePrevious: typeof import('@vueuse/core')['usePrevious'] const usePrevious: typeof import('@vueuse/core')['usePrevious']
const useRafFn: typeof import('@vueuse/core')['useRafFn'] const useRafFn: typeof import('@vueuse/core')['useRafFn']
const useRefHistory: typeof import('@vueuse/core')['useRefHistory'] const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver'] const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
const useRoute: typeof import('vue-router')['useRoute'] const useRoute: typeof import('vue-router')['useRoute']
const useRouter: typeof import('vue-router')['useRouter'] const useRouter: typeof import('vue-router')['useRouter']
const useSSRWidth: typeof import('@vueuse/core')['useSSRWidth']
const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation'] const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea'] const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
const useScriptTag: typeof import('@vueuse/core')['useScriptTag'] const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
......
差异被折叠。
...@@ -22,14 +22,16 @@ ...@@ -22,14 +22,16 @@
"@tinymce/tinymce-vue": "^5.0.1", "@tinymce/tinymce-vue": "^5.0.1",
"@vue-flow/controls": "^1.1.2", "@vue-flow/controls": "^1.1.2",
"@vue-flow/core": "^1.39.0", "@vue-flow/core": "^1.39.0",
"@vueuse/components": "^11.2.0", "@vueuse/components": "^13.3.0",
"@vueuse/core": "^11.2.0", "@vueuse/core": "^13.3.0",
"axios": "^1.6.8", "axios": "^1.9.0",
"blueimp-md5": "^2.19.0", "blueimp-md5": "^2.19.0",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"echarts": "^5.5.0", "echarts": "^5.5.0",
"echarts-wordcloud": "^2.1.0", "echarts-wordcloud": "^2.1.0",
"element-plus": "^2.8.7", "element-plus": "^2.8.7",
"eventsource-parser": "^3.0.2",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"html-to-image": "^1.11.11", "html-to-image": "^1.11.11",
"jspdf": "^2.5.1", "jspdf": "^2.5.1",
...@@ -37,14 +39,16 @@ ...@@ -37,14 +39,16 @@
"nanoid": "^5.0.7", "nanoid": "^5.0.7",
"pinia": "^2.2.6", "pinia": "^2.2.6",
"scroll-into-view-if-needed": "^3.1.0", "scroll-into-view-if-needed": "^3.1.0",
"vue": "^3.5.12", "vue": "^3.5.16",
"vue-echarts": "^6.6.9", "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" "xss": "^1.0.15"
}, },
"devDependencies": { "devDependencies": {
"@tsconfig/node20": "^20.1.4", "@tsconfig/node20": "^20.1.4",
"@types/blueimp-md5": "^2.18.2", "@types/blueimp-md5": "^2.18.2",
"@types/crypto-js": "^4.2.2",
"@types/file-saver": "^2.0.7", "@types/file-saver": "^2.0.7",
"@types/node": "^20.17.6", "@types/node": "^20.17.6",
"@vitejs/plugin-vue": "^5.1.4", "@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?: { ...@@ -180,3 +180,8 @@ export function getProductList(params?: {
export function getAuth() { export function getAuth() {
return httpRequest.get('/api/lab/v1/experiment/auth/all') 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 { ...@@ -96,3 +96,26 @@ textarea:focus {
margin: -8px -15px; margin: -8px -15px;
padding: 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;
}
...@@ -27,6 +27,7 @@ const component = computed(() => { ...@@ -27,6 +27,7 @@ const component = computed(() => {
MAWeibo: markRaw(defineAsyncComponent(() => import('./components/marketingAction/weibo/Index.vue'))), MAWeibo: markRaw(defineAsyncComponent(() => import('./components/marketingAction/weibo/Index.vue'))),
MADingTalk: markRaw(defineAsyncComponent(() => import('./components/marketingAction/dingtalk/Index.vue'))), MADingTalk: markRaw(defineAsyncComponent(() => import('./components/marketingAction/dingtalk/Index.vue'))),
MAAB: markRaw(defineAsyncComponent(() => import('./components/marketingAction/ab/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'))), MAXiaohongshu: markRaw(defineAsyncComponent(() => import('./components/marketingAction/xiaohongshu/Index.vue'))),
CBAttributeJudgment: markRaw(defineAsyncComponent(() => import('./components/conditionalBranch/attributeJudgment/Index.vue'))), CBAttributeJudgment: markRaw(defineAsyncComponent(() => import('./components/conditionalBranch/attributeJudgment/Index.vue'))),
CBGroupJudgment: markRaw(defineAsyncComponent(() => import('./components/conditionalBranch/groupJudgment/Index.vue'))), CBGroupJudgment: markRaw(defineAsyncComponent(() => import('./components/conditionalBranch/groupJudgment/Index.vue'))),
......
...@@ -27,7 +27,7 @@ const list = ref([ ...@@ -27,7 +27,7 @@ const list = ref([
type_name: '触发条件', type_name: '触发条件',
icon: '13', icon: '13',
component_type: 1, component_type: 1,
component_name: 'TCRealTimeTrigger' component_name: 'TCRealTimeTrigger',
}, },
{ {
name: '加入群组', name: '加入群组',
...@@ -35,7 +35,7 @@ const list = ref([ ...@@ -35,7 +35,7 @@ const list = ref([
type_name: '触发条件', type_name: '触发条件',
icon: '14', icon: '14',
component_type: 2, component_type: 2,
component_name: 'TCJoinGroup' component_name: 'TCJoinGroup',
}, },
{ {
name: '变更属性', name: '变更属性',
...@@ -43,7 +43,7 @@ const list = ref([ ...@@ -43,7 +43,7 @@ const list = ref([
type_name: '触发条件', type_name: '触发条件',
icon: '15', icon: '15',
component_type: 3, component_type: 3,
component_name: 'TCChangeProps' component_name: 'TCChangeProps',
}, },
{ {
name: '公众号', name: '公众号',
...@@ -52,7 +52,7 @@ const list = ref([ ...@@ -52,7 +52,7 @@ const list = ref([
icon: '1', icon: '1',
component_type: 4, component_type: 4,
component_name: 'TCOffiaccount', component_name: 'TCOffiaccount',
connection_type: 1 connection_type: 1,
}, },
{ {
name: '抖音', name: '抖音',
...@@ -61,7 +61,7 @@ const list = ref([ ...@@ -61,7 +61,7 @@ const list = ref([
icon: '6', icon: '6',
component_type: 5, component_type: 5,
component_name: 'TCDouyin', component_name: 'TCDouyin',
connection_type: 6 connection_type: 6,
}, },
{ {
name: '小红书', name: '小红书',
...@@ -70,7 +70,7 @@ const list = ref([ ...@@ -70,7 +70,7 @@ const list = ref([
icon: '8', icon: '8',
component_type: 6, component_type: 6,
component_name: 'TCXiaohongshu', component_name: 'TCXiaohongshu',
connection_type: 8 connection_type: 8,
}, },
{ {
name: '微博', name: '微博',
...@@ -79,7 +79,7 @@ const list = ref([ ...@@ -79,7 +79,7 @@ const list = ref([
icon: '7', icon: '7',
component_type: 7, component_type: 7,
component_name: 'TCWeibo', component_name: 'TCWeibo',
connection_type: 7 connection_type: 7,
}, },
{ {
name: '自定义', name: '自定义',
...@@ -88,7 +88,7 @@ const list = ref([ ...@@ -88,7 +88,7 @@ const list = ref([
icon: '12', icon: '12',
component_type: 10, component_type: 10,
component_name: 'TCCustom', component_name: 'TCCustom',
connection_type: 12 connection_type: 12,
}, },
{ {
name: '小鹅通', name: '小鹅通',
...@@ -97,7 +97,7 @@ const list = ref([ ...@@ -97,7 +97,7 @@ const list = ref([
icon: '3', icon: '3',
component_type: 8, component_type: 8,
component_name: 'TCXiaoetong', component_name: 'TCXiaoetong',
connection_type: 3 connection_type: 3,
}, },
{ {
name: '问卷星', name: '问卷星',
...@@ -106,9 +106,9 @@ const list = ref([ ...@@ -106,9 +106,9 @@ const list = ref([
icon: '4', icon: '4',
component_type: 9, component_type: 9,
component_name: 'TCWenjuanxing', component_name: 'TCWenjuanxing',
connection_type: 4 connection_type: 4,
} },
] ],
}, },
{ {
name: '营销动作', name: '营销动作',
...@@ -121,7 +121,7 @@ const list = ref([ ...@@ -121,7 +121,7 @@ const list = ref([
icon: '16', icon: '16',
component_type: 99, component_type: 99,
component_name: 'MAEndTrip', component_name: 'MAEndTrip',
color: '#AA1941' color: '#AA1941',
}, },
{ {
name: '加入群组', name: '加入群组',
...@@ -129,7 +129,7 @@ const list = ref([ ...@@ -129,7 +129,7 @@ const list = ref([
type_name: '营销动作', type_name: '营销动作',
icon: '14', icon: '14',
component_type: 1, component_type: 1,
component_name: 'MAJoinGroup' component_name: 'MAJoinGroup',
}, },
{ {
name: '移除群组', name: '移除群组',
...@@ -137,7 +137,7 @@ const list = ref([ ...@@ -137,7 +137,7 @@ const list = ref([
type_name: '营销动作', type_name: '营销动作',
icon: '17', icon: '17',
component_type: 2, component_type: 2,
component_name: 'MALeaveGroup' component_name: 'MALeaveGroup',
}, },
{ {
name: '变更属性', name: '变更属性',
...@@ -145,7 +145,7 @@ const list = ref([ ...@@ -145,7 +145,7 @@ const list = ref([
type_name: '营销动作', type_name: '营销动作',
icon: '15', icon: '15',
component_type: 3, component_type: 3,
component_name: 'MAChangeProps' component_name: 'MAChangeProps',
}, },
{ {
name: '延时处理', name: '延时处理',
...@@ -153,7 +153,7 @@ const list = ref([ ...@@ -153,7 +153,7 @@ const list = ref([
type_name: '营销动作', type_name: '营销动作',
icon: '18', icon: '18',
component_type: 4, component_type: 4,
component_name: 'MADelayProcess' component_name: 'MADelayProcess',
}, },
{ {
name: '内部通知', name: '内部通知',
...@@ -161,7 +161,7 @@ const list = ref([ ...@@ -161,7 +161,7 @@ const list = ref([
type_name: '营销动作', type_name: '营销动作',
icon: '19', icon: '19',
component_type: 5, component_type: 5,
component_name: 'MAInternalNotice' component_name: 'MAInternalNotice',
}, },
{ {
name: '短信', name: '短信',
...@@ -170,7 +170,7 @@ const list = ref([ ...@@ -170,7 +170,7 @@ const list = ref([
icon: '10', icon: '10',
component_type: 11, component_type: 11,
component_name: 'MASMS', component_name: 'MASMS',
connection_type: 10 connection_type: 10,
}, },
{ {
name: '邮件', name: '邮件',
...@@ -179,7 +179,7 @@ const list = ref([ ...@@ -179,7 +179,7 @@ const list = ref([
icon: '9', icon: '9',
component_type: 10, component_type: 10,
component_name: 'MAEmail', component_name: 'MAEmail',
connection_type: 9 connection_type: 9,
}, },
{ {
name: '公众号', name: '公众号',
...@@ -188,7 +188,7 @@ const list = ref([ ...@@ -188,7 +188,7 @@ const list = ref([
icon: '1', icon: '1',
component_type: 6, component_type: 6,
component_name: 'MAOffiaccount', component_name: 'MAOffiaccount',
connection_type: 1 connection_type: 1,
}, },
{ {
name: '抖音', name: '抖音',
...@@ -197,9 +197,17 @@ const list = ref([ ...@@ -197,9 +197,17 @@ const list = ref([
icon: '6', icon: '6',
component_type: 7, component_type: 7,
component_name: 'MADouyin', 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: '微博', name: '微博',
type: 2, type: 2,
...@@ -207,7 +215,7 @@ const list = ref([ ...@@ -207,7 +215,7 @@ const list = ref([
icon: '7', icon: '7',
component_type: 8, component_type: 8,
component_name: 'MAWeibo', component_name: 'MAWeibo',
connection_type: 7 connection_type: 7,
}, },
{ {
name: '钉钉', name: '钉钉',
...@@ -216,7 +224,7 @@ const list = ref([ ...@@ -216,7 +224,7 @@ const list = ref([
icon: '2', icon: '2',
component_type: 9, component_type: 9,
component_name: 'MADingTalk', component_name: 'MADingTalk',
connection_type: 2 connection_type: 2,
}, },
{ {
name: 'A/B分配', name: 'A/B分配',
...@@ -224,9 +232,17 @@ const list = ref([ ...@@ -224,9 +232,17 @@ const list = ref([
type_name: '营销动作', type_name: '营销动作',
icon: '101', icon: '101',
component_type: 12, component_type: 12,
component_name: 'MAAB' component_name: 'MAAB',
} },
] {
name: '视频号',
type: 2,
type_name: '营销动作',
icon: 'wechatVideo',
component_type: 16,
component_name: 'MAWechatVideo',
},
],
}, },
{ {
name: '条件分支', name: '条件分支',
...@@ -238,7 +254,7 @@ const list = ref([ ...@@ -238,7 +254,7 @@ const list = ref([
type_name: '条件分支', type_name: '条件分支',
icon: '20', icon: '20',
component_type: 1, component_type: 1,
component_name: 'CBAttributeJudgment' component_name: 'CBAttributeJudgment',
}, },
{ {
name: '标签判断', name: '标签判断',
...@@ -246,7 +262,7 @@ const list = ref([ ...@@ -246,7 +262,7 @@ const list = ref([
type_name: '条件分支', type_name: '条件分支',
icon: '21', icon: '21',
component_type: 2, component_type: 2,
component_name: 'CBLabelJudgment' component_name: 'CBLabelJudgment',
}, },
{ {
name: '群组判断', name: '群组判断',
...@@ -254,7 +270,7 @@ const list = ref([ ...@@ -254,7 +270,7 @@ const list = ref([
type_name: '条件分支', type_name: '条件分支',
icon: '22', icon: '22',
component_type: 3, component_type: 3,
component_name: 'CBGroupJudgment' component_name: 'CBGroupJudgment',
}, },
{ {
name: '事件判断', name: '事件判断',
...@@ -262,7 +278,7 @@ const list = ref([ ...@@ -262,7 +278,7 @@ const list = ref([
type_name: '条件分支', type_name: '条件分支',
icon: '23', icon: '23',
component_type: 5, component_type: 5,
component_name: 'CBEventJudgment' component_name: 'CBEventJudgment',
}, },
{ {
name: '时间判断', name: '时间判断',
...@@ -270,7 +286,7 @@ const list = ref([ ...@@ -270,7 +286,7 @@ const list = ref([
type_name: '条件分支', type_name: '条件分支',
icon: '24', icon: '24',
component_type: 4, component_type: 4,
component_name: 'CBTimeJudgment' component_name: 'CBTimeJudgment',
}, },
{ {
name: '公众号', name: '公众号',
...@@ -279,7 +295,7 @@ const list = ref([ ...@@ -279,7 +295,7 @@ const list = ref([
icon: '1', icon: '1',
component_type: 6, component_type: 6,
component_name: 'CBOffiaccount', component_name: 'CBOffiaccount',
connection_type: 1 connection_type: 1,
}, },
{ {
name: '小红书', name: '小红书',
...@@ -288,7 +304,7 @@ const list = ref([ ...@@ -288,7 +304,7 @@ const list = ref([
icon: '8', icon: '8',
component_type: 7, component_type: 7,
component_name: 'CBXiaohongshu', component_name: 'CBXiaohongshu',
connection_type: 8 connection_type: 8,
}, },
{ {
name: '抖音', name: '抖音',
...@@ -297,17 +313,19 @@ const list = ref([ ...@@ -297,17 +313,19 @@ const list = ref([
icon: '6', icon: '6',
component_type: 8, component_type: 8,
component_name: 'CBDouyin', component_name: 'CBDouyin',
connection_type: 6 connection_type: 6,
} },
] ],
} },
]) ])
const currentList = computed(() => { const currentList = computed(() => {
return list.value.map(item => { return list.value.map((item) => {
return { return {
...item, ...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) => { ...@@ -326,9 +344,18 @@ const onDragStart = (event: DragEvent, data: any) => {
<dt :style="`background: ${parent.background?.color}`">{{ parent.name }}</dt> <dt :style="`background: ${parent.background?.color}`">{{ parent.name }}</dt>
<dd> <dd>
<ul> <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"> <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> <Icon class="icon" color="#fff" :name="item.icon" w="24" h="24"></Icon>
</div> </div>
<p>{{ item.name }}</p> <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"> <script setup lang="ts">
export default { name: 'AppMain' } const isIframe = computed(() => {
return window.self !== window.top
})
const padding = computed(() => {
return isIframe.value ? '0 10px' : '10px'
})
</script> </script>
<template> <template>
...@@ -11,7 +17,7 @@ export default { name: 'AppMain' } ...@@ -11,7 +17,7 @@ export default { name: 'AppMain' }
<style> <style>
.app-main { .app-main {
flex: 1; flex: 1;
padding: 10px; padding: v-bind(padding);
overflow: hidden; overflow: hidden;
box-sizing: border-box; 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' ...@@ -7,6 +7,10 @@ import { useRfmRes } from '@/composables/useRFMData'
// const tagRule = ref(inject('tagRule') as TagRule) // const tagRule = ref(inject('tagRule') as TagRule)
const tagRule = defineModel<TagRule>({ default: { current_logic_operate: 'and', items: [] } }) const tagRule = defineModel<TagRule>({ default: { current_logic_operate: 'and', items: [] } })
const { buttonText = '添加条件' } = defineProps<{
buttonText?: string
}>()
const { tagList } = useTag() const { tagList } = useTag()
const { rfmResList } = useRfmRes() const { rfmResList } = useRfmRes()
...@@ -31,11 +35,11 @@ function handleRemove(items: any[], index: number) { ...@@ -31,11 +35,11 @@ function handleRemove(items: any[], index: number) {
} }
function showRfm(id: string) { 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) { 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 item.rfm_value = found?.frm_value
} }
// const a = [ // const a = [
...@@ -53,8 +57,10 @@ function handleRfmChange(rfmKey: string, item: any) { ...@@ -53,8 +57,10 @@ function handleRfmChange(rfmKey: string, item: any) {
<template> <template>
<el-card shadow="never"> <el-card shadow="never">
<template #header> <template #header>
<el-button circle color="#567722" :icon="PriceTag"></el-button> <slot name="header">
标签满足以下条件 <el-button circle color="#567722" :icon="PriceTag"></el-button>
标签满足以下条件
</slot>
</template> </template>
<div class="rule" v-if="tagRule.items.length"> <div class="rule" v-if="tagRule.items.length">
<div class="rule-operator"> <div class="rule-operator">
...@@ -66,11 +72,23 @@ function handleRfmChange(rfmKey: string, item: any) { ...@@ -66,11 +72,23 @@ function handleRfmChange(rfmKey: string, item: any) {
标签 等于 标签 等于
<el-form-item> <el-form-item>
<el-select v-model="item.tag_id" style="width: 300px"> <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> </el-select>
<template v-if="showRfm(item.tag_id)"> <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-select
<el-option v-for="item in rfmResList" :key="item.frm_key" :label="item.frm_value" :value="item.frm_key" style="height: auto"> 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"> <div style="line-height: 24px; padding: 5px 0">
<p> <p>
<span style="float: left">{{ item.frm_value }}</span> <span style="float: left">{{ item.frm_value }}</span>
...@@ -78,7 +96,9 @@ function handleRfmChange(rfmKey: string, item: any) { ...@@ -78,7 +96,9 @@ function handleRfmChange(rfmKey: string, item: any) {
{{ item.frm_extend_info.customer_marketing_strategy }} {{ item.frm_extend_info.customer_marketing_strategy }}
</span> </span>
</p> </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> </div>
</el-option> </el-option>
</el-select> </el-select>
...@@ -90,7 +110,10 @@ function handleRfmChange(rfmKey: string, item: any) { ...@@ -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_extend_info.m" label="M值" width="52" />
<el-table-column prop="frm_value" label="标签值" width="110" /> <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.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> </el-table>
<template #reference> <template #reference>
<el-icon><QuestionFilled /></el-icon> <el-icon><QuestionFilled /></el-icon>
...@@ -103,7 +126,7 @@ function handleRfmChange(rfmKey: string, item: any) { ...@@ -103,7 +126,7 @@ function handleRfmChange(rfmKey: string, item: any) {
</el-row> </el-row>
</div> </div>
</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> </el-card>
</template> </template>
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import { QuestionFilled } from '@element-plus/icons-vue' import { QuestionFilled } from '@element-plus/icons-vue'
import { useUserAttr, useMetaEvent, useUserAttrRange } from '@/composables/useRFMData' import { useUserAttr, useMetaEvent, useUserAttrRange } from '@/composables/useRFMData'
import { searchMetaMemberAttrs } from '@/api/base' import { searchMetaMemberAttrs } from '@/api/base'
import { cloneDeep } from 'lodash-es'
defineProps<{ label: string }>() defineProps<{ label: string }>()
const form = defineModel<any>() const form = defineModel<any>()
...@@ -9,12 +10,12 @@ const form = defineModel<any>() ...@@ -9,12 +10,12 @@ const form = defineModel<any>()
const ruleList = [ const ruleList = [
{ label: '属性值平均法', value: '101', basis: ['1'] }, { label: '属性值平均法', value: '101', basis: ['1'] },
{ label: '属性值分类法', value: '102', basis: ['1'] }, { label: '属性值分类法', value: '102', basis: ['1'] },
{ label: '事件发生次数平均法', value: '201', basis: ['2'] } { label: '事件发生次数平均法', value: '201', basis: ['2'] },
] ]
const defaultLevel = [ const defaultLevel = [
{ level: '高', value: '' }, { level: '高', value: '' },
{ level: '低', value: '' } { level: '低', value: '' },
] ]
const defaultScore = [ const defaultScore = [
...@@ -22,7 +23,7 @@ const defaultScore = [ ...@@ -22,7 +23,7 @@ const defaultScore = [
{ score: 2, min_value: '', max_value: '' }, { score: 2, min_value: '', max_value: '' },
{ score: 3, min_value: '', max_value: '' }, { score: 3, min_value: '', max_value: '' },
{ score: 4, min_value: '', max_value: '' }, { score: 4, min_value: '', max_value: '' },
{ score: 5, min_value: '', max_value: '' } { score: 5, min_value: '', max_value: '' },
] ]
onMounted(() => { onMounted(() => {
...@@ -33,8 +34,8 @@ onMounted(() => { ...@@ -33,8 +34,8 @@ onMounted(() => {
event_id: '-1', event_id: '-1',
attr_id: '', attr_id: '',
attr_type: '', attr_type: '',
config: [...defaultScore], config: cloneDeep(defaultScore),
extend_config: { default_score_config: { switch: false, score: undefined } } extend_config: { default_score_config: { switch: false, score: undefined } },
}, },
form.value form.value
) )
...@@ -45,7 +46,7 @@ const { metaEventList, fetchMetaEventList } = useMetaEvent() ...@@ -45,7 +46,7 @@ const { metaEventList, fetchMetaEventList } = useMetaEvent()
const { userAttrRange, fetchUserAttrRange } = useUserAttrRange() const { userAttrRange, fetchUserAttrRange } = useUserAttrRange()
const currentRuleList = computed(() => { 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(() => { const currentMetaEventList = computed(() => {
...@@ -58,21 +59,31 @@ function handleBasisChange(value: any) { ...@@ -58,21 +59,31 @@ function handleBasisChange(value: any) {
} else { } else {
form.value.rule = '201' 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) handleRuleChange(form.value.rule)
} }
function handleRuleChange(value: any) { function handleRuleChange(value: any) {
if (value === '102') { if (value === '102') {
form.value.config = [...defaultLevel] form.value.config = cloneDeep(defaultLevel)
} else { } else {
form.value.config = [...defaultScore] form.value.config = cloneDeep(defaultScore)
} }
form.value.attr_id = '' form.value.attr_id = ''
} }
function handleAttrChange(value: any) { 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 }[]>([]) const options = ref<{ label: string; value: string }[]>([])
...@@ -81,7 +92,7 @@ async function remoteMethod(search: string = '') { ...@@ -81,7 +92,7 @@ async function remoteMethod(search: string = '') {
options.value = [] options.value = []
if (form.value.attr_id) { if (form.value.attr_id) {
loading.value = true 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) => { options.value = res.data.list.map((item: any) => {
return { label: item.attr_value, value: item.attr_value } return { label: item.attr_value, value: item.attr_value }
}) })
...@@ -96,7 +107,7 @@ function querySearch(queryString: string, cb: any) { ...@@ -96,7 +107,7 @@ function querySearch(queryString: string, cb: any) {
watch( watch(
() => form.value.attr_id, () => form.value.attr_id,
attrId => { (attrId) => {
if (form.value.rule === '102') { if (form.value.rule === '102') {
remoteMethod() remoteMethod()
} else { } else {
...@@ -120,7 +131,7 @@ const a = [ ...@@ -120,7 +131,7 @@ const a = [
{ id: '002', label: '1500' }, { id: '002', label: '1500' },
{ id: '003', label: '3000' }, { id: '003', label: '3000' },
{ id: '004', label: '2200' }, { 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 })) 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 ...@@ -147,11 +158,23 @@ const defaultOptions = Array.from({ length: 5 }).map((_, index) => ({ value: ind
</el-radio-group> </el-radio-group>
<p style="margin-left: 10px">计算规则:</p> <p style="margin-left: 10px">计算规则:</p>
<el-select v-model="form.rule" style="width: 170px" @change="handleRuleChange"> <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> </el-select>
<div class="rfm-tips"> <div class="rfm-tips">
<el-popover popper-class="rfm-popover" placement="right" title="属性值平均法" :width="400" trigger="hover" v-if="form.rule == '101'"> <el-popover
<p>用于计算选中属性的平均值,通过对选定的字段中的所有记录进行数值相加,然后除以记录的数量来计算的。主要针对“数字”和“整数”两种字段类型。</p> popper-class="rfm-popover"
placement="right"
title="属性值平均法"
:width="400"
trigger="hover"
v-if="form.rule == '101'">
<p>
用于计算选中属性的平均值,通过对选定的字段中的所有记录进行数值相加,然后除以记录的数量来计算的。主要针对“数字”和“整数”两种字段类型。
</p>
<p>举例:</p> <p>举例:</p>
<el-table :data="a" border> <el-table :data="a" border>
<el-table-column prop="id" label="用户ID" /> <el-table-column prop="id" label="用户ID" />
...@@ -162,13 +185,25 @@ const defaultOptions = Array.from({ length: 5 }).map((_, index) => ({ value: ind ...@@ -162,13 +185,25 @@ const defaultOptions = Array.from({ length: 5 }).map((_, index) => ({ value: ind
<el-icon><QuestionFilled /></el-icon> <el-icon><QuestionFilled /></el-icon>
</template> </template>
</el-popover> </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> <p>将数据的属性值按照一定的规则或特性进行分类,本系统中分了“高”和“低”两类。主要针对“字符串”的字段类型。</p>
<template #reference> <template #reference>
<el-icon><QuestionFilled /></el-icon> <el-icon><QuestionFilled /></el-icon>
</template> </template>
</el-popover> </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> <p>分析事件发生频率的方法,即通过计算用户事件发生的平均次数。</p>
<template #reference> <template #reference>
<el-icon><QuestionFilled /></el-icon> <el-icon><QuestionFilled /></el-icon>
...@@ -177,19 +212,37 @@ const defaultOptions = Array.from({ length: 5 }).map((_, index) => ({ value: ind ...@@ -177,19 +212,37 @@ const defaultOptions = Array.from({ length: 5 }).map((_, index) => ({ value: ind
</div> </div>
<el-select v-model="form.event_id" placeholder="选择事件" style="width: 160px" v-if="form.basis === '2'"> <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>
<el-select v-model="form.attr_id" placeholder="选择属性" style="width: 160px" @change="handleAttrChange" v-else> <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-option v-for="item in userAttrList" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select> </el-select>
<div style="flex: 1; display: flex; justify-content: space-between" v-if="form.basis == 1 && form.rule != '102' && form.attr_id"> <div
<p>最小值:{{ userAttrRange.min }}<br />最大值:{{ userAttrRange.max }}<br />"0"值数量:{{ userAttrRange.zero_count }}</p> style="flex: 1; display: flex; justify-content: space-between"
<p>平均值:{{ userAttrRange.avg }}<br />中位数:{{ userAttrRange.median }}<br />中位数(不含0):{{ userAttrRange.no_zero_median }}</p> 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> </div>
<div class="rfm-header-extra" v-if="form.rule === '101' && form.extend_config"> <div class="rfm-header-extra" v-if="form.rule === '101' && form.extend_config">
<p>未匹配数据默认赋值</p> <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> <el-switch v-model="form.extend_config.default_score_config.switch"></el-switch>
</div> </div>
<div class="rfm-body"> <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' import { useMapStore } from '@/stores/map'
// 用户属性类型 // 用户属性类型
...@@ -36,6 +43,12 @@ export interface ConnectionType { ...@@ -36,6 +43,12 @@ export interface ConnectionType {
config_attributes: any config_attributes: any
} }
// 群组类型
export interface GroupType {
id: string
name: string
}
// 所有用户属性 // 所有用户属性
const userAttrList = ref<AttrType[]>([]) const userAttrList = ref<AttrType[]>([])
const userAttrLoading = ref(false) const userAttrLoading = ref(false)
...@@ -100,8 +113,9 @@ export function useConnection() { ...@@ -100,8 +113,9 @@ export function useConnection() {
const connectionType = useMapStore().getMapValuesByKey('experiment_connection_type') const connectionType = useMapStore().getMapValuesByKey('experiment_connection_type')
await getConnectionList().then((res: any) => { await getConnectionList().then((res: any) => {
connectionList.value = res.data.items.map((item: any) => { connectionList.value = res.data.items.map((item: any) => {
const connection = connectionType.find(type => type.value == item.type) 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 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 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 } return { ...item, config_attributes: attrs, name, type_name: connection?.label || item.type }
}) })
...@@ -142,3 +156,21 @@ export function useUser() { ...@@ -142,3 +156,21 @@ export function useUser() {
}) })
return { fetchUserList, userList, userValue } 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,50 +4,58 @@ export function useChat() { ...@@ -4,50 +4,58 @@ export function useChat() {
const messages = ref([]) const messages = ref([])
const isLoading = ref(false) const isLoading = ref(false)
async function post(message, isReplace = true) { function post(message, isReplace = true) {
isLoading.value = true isLoading.value = true
await fetchEventSource('/api/lab/v1/experiment/qwen/chat', { return new Promise((resolve, reject) => {
method: 'POST', fetchEventSource('/api/lab/v1/experiment/qwen/chat', {
headers: { 'Content-Type': 'application/json' }, method: 'POST',
body: JSON.stringify({ model: 'qwen-long', messages: [message] }), headers: { 'Content-Type': 'application/json' },
async onopen(response) { body: JSON.stringify({ model: 'qwen-long', messages: [message] }),
if (response.ok) { async onopen(response) {
return response if (response.ok) {
} else { return response
throw response } else {
} isLoading.value = false
}, reject(response)
onmessage(res) { throw response
console.log(res.data)
if (res.data === '[DONE]') {
isLoading.value = false
}
try {
const message = JSON.parse(res.data)
if (message.error) {
ElMessage.error(message.error.message)
return
} }
const id = message.id },
const messageIndex = messages.value.findIndex((session) => session.id === id) onmessage(res) {
let content = message?.choices[0]?.delta.content || '' console.log(res.data)
if (isReplace) { if (res.data === '[DONE]') {
content = content.replaceAll('\n', '<br/>') isLoading.value = false
resolve(messages.value.at(-1))
return
} }
if (messageIndex === -1) { try {
messages.value.push({ id, role: 'assistant', content }) const message = JSON.parse(res.data)
} else { if (message.error) {
messages.value[messageIndex].content = messages.value[messageIndex].content + content ElMessage.error(message.error.message)
return
}
const id = message.id
const messageIndex = messages.value.findIndex((session) => session.id === id)
let content = message?.choices[0]?.delta.content || ''
if (isReplace) {
content = content.replaceAll('\n', '<br/>')
}
if (messageIndex === -1) {
messages.value.push({ id, role: 'assistant', content })
} else {
messages.value[messageIndex].content = messages.value[messageIndex].content + content
}
} catch (error) {
console.log(error)
isLoading.value = false
reject(error)
} }
} catch (error) { },
console.log(error) onerror(err) {
isLoading.value = false isLoading.value = false
} reject(err)
}, throw err
onerror(err) { },
isLoading.value = false })
throw err
},
}) })
} }
......
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() { ...@@ -34,3 +34,8 @@ export function getMemberMetaAttrs() {
export function getUserTags(params: { sso_id: string; limit: number }) { export function getUserTags(params: { sso_id: string; limit: number }) {
return httpRequest.get('/api/lab/v1/experiment/analyse/user-tags', { params }) 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' ...@@ -8,6 +8,9 @@ import { useMapStore } from '@/stores/map'
import { getNameByValue } from '@/utils/dictionary' import { getNameByValue } from '@/utils/dictionary'
import * as api from '../api' import * as api from '../api'
const AISummaryDialog = defineAsyncComponent(() => import('../components/AISummaryDialog.vue'))
const aiDialogVisible = ref(false)
const connectionTypeList = useMapStore().getMapValuesByKey('experiment_connection_type') const connectionTypeList = useMapStore().getMapValuesByKey('experiment_connection_type')
const statusList = useMapStore().getMapValuesByKey('system_status') const statusList = useMapStore().getMapValuesByKey('system_status')
...@@ -57,7 +60,7 @@ const genderOption = computed(() => { ...@@ -57,7 +60,7 @@ const genderOption = computed(() => {
return { return {
grid: { left: '60', right: '60' }, grid: { left: '60', right: '60' },
tooltip: { tooltip: {
trigger: 'item' trigger: 'item',
}, },
yAxis: { yAxis: {
data: ['男性', '女性'], data: ['男性', '女性'],
...@@ -67,15 +70,20 @@ const genderOption = computed(() => { ...@@ -67,15 +70,20 @@ const genderOption = computed(() => {
axisLabel: { axisLabel: {
formatter: function (value, index) { formatter: function (value, index) {
const total = parseInt(man.total) + parseInt(woman.total) 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: { xAxis: {
splitLine: { show: false }, splitLine: { show: false },
axisLabel: { show: false }, axisLabel: { show: false },
axisTick: { show: false }, axisTick: { show: false },
axisLine: { show: false } axisLine: { show: false },
}, },
series: [ series: [
{ {
...@@ -86,10 +94,10 @@ const genderOption = computed(() => { ...@@ -86,10 +94,10 @@ const genderOption = computed(() => {
symbolMargin: 10, symbolMargin: 10,
data: [ data: [
{ value: man.total, symbol: manIcon, itemStyle: { color: '#767aca' } }, { 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() { ...@@ -101,7 +109,7 @@ async function fetchConnections() {
loading2.value = true loading2.value = true
try { try {
const res = await api.getMemberConnections({ sso_id: userValue.value }) 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) } return { ...item, group_name: getNameByValue(item.group_name, connectionTypeList) }
}) })
} finally { } finally {
...@@ -113,24 +121,24 @@ const connectionOption = computed(() => { ...@@ -113,24 +121,24 @@ const connectionOption = computed(() => {
return { return {
grid: { left: '5%', top: '10%', right: '5%', bottom: '5%', containLabel: true }, grid: { left: '5%', top: '10%', right: '5%', bottom: '5%', containLabel: true },
tooltip: { tooltip: {
trigger: 'axis' trigger: 'axis',
}, },
xAxis: { xAxis: {
type: 'category', type: 'category',
axisLabel: { interval: 0 }, axisLabel: { interval: 0 },
data: connection.value.map(item => item.group_name) data: connection.value.map((item) => item.group_name),
}, },
yAxis: { yAxis: {
type: 'value' type: 'value',
}, },
series: [ series: [
{ {
name: '数据', name: '数据',
type: 'bar', type: 'bar',
label: { show: true, position: 'top' }, 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() { ...@@ -142,7 +150,7 @@ async function fetchStatus() {
loading3.value = true loading3.value = true
try { try {
const res = await api.getMemberStatus({ sso_id: userValue.value }) 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 } return { name: getNameByValue(item.group_name, statusList), value: item.total }
}) })
} finally { } finally {
...@@ -155,7 +163,7 @@ const statusOption = computed(() => { ...@@ -155,7 +163,7 @@ const statusOption = computed(() => {
grid: { left: '5%', top: '10%', right: '5%', bottom: '5%', containLabel: true }, grid: { left: '5%', top: '10%', right: '5%', bottom: '5%', containLabel: true },
tooltip: { tooltip: {
trigger: 'item', trigger: 'item',
formatter: '{b}: {c}<br />{d}%' formatter: '{b}: {c}<br />{d}%',
}, },
series: [ series: [
{ {
...@@ -163,9 +171,9 @@ const statusOption = computed(() => { ...@@ -163,9 +171,9 @@ const statusOption = computed(() => {
label: { formatter: '{b}\n{d}%' }, label: { formatter: '{b}\n{d}%' },
itemStyle: { borderRadius: 6 }, itemStyle: { borderRadius: 6 },
radius: [0, '70%'], radius: [0, '70%'],
data: status.value data: status.value,
} },
] ],
} }
}) })
</script> </script>
...@@ -173,15 +181,28 @@ const statusOption = computed(() => { ...@@ -173,15 +181,28 @@ const statusOption = computed(() => {
<template> <template>
<AppCard title="用户分析"> <AppCard title="用户分析">
<el-form inline label-suffix=":"> <el-form inline label-suffix=":">
<el-form-item label="实验名称">{{ info?.name }}</el-form-item> <div style="display: flex; justify-content: space-between">
<el-form-item label="请选择学生/老师"> <div>
<el-select v-model="userValue" filterable> <el-form-item label="实验名称">{{ info?.name }}</el-form-item>
<el-option v-for="item in userList" :label="item.name" :value="item.sso_id" :key="item.sso_id"></el-option> <el-form-item label="请选择学生/老师">
</el-select> <el-select v-model="userValue" filterable>
</el-form-item> <el-option
<el-form-item> v-for="item in userList"
<el-button type="primary" :icon="DataLine" :loading="loading" @click="handleStart">分析</el-button> :label="item.name"
</el-form-item> :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-divider style="margin: 10px 0" />
<el-form-item label="用户总数"> <el-form-item label="用户总数">
<b class="total">{{ userTotal }}</b> <b class="total">{{ userTotal }}</b>
...@@ -199,6 +220,7 @@ const statusOption = computed(() => { ...@@ -199,6 +220,7 @@ const statusOption = computed(() => {
<UserChart :ssoId="userValue" /> <UserChart :ssoId="userValue" />
</div> </div>
</AppCard> </AppCard>
<AISummaryDialog v-model="aiDialogVisible" v-if="aiDialogVisible"></AISummaryDialog>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
......
...@@ -5,6 +5,7 @@ import type { DetailsProp, PlatformItem, ConfigAttribute } from '../types' ...@@ -5,6 +5,7 @@ import type { DetailsProp, PlatformItem, ConfigAttribute } from '../types'
const StepOne = defineAsyncComponent(() => import('../components/StepOne.vue')) const StepOne = defineAsyncComponent(() => import('../components/StepOne.vue'))
const StepTwo = defineAsyncComponent(() => import('../components/StepTwo.vue')) const StepTwo = defineAsyncComponent(() => import('../components/StepTwo.vue'))
const WechatVideo = defineAsyncComponent(() => import('../components/WechatVideo.vue'))
const props = defineProps<{ data?: DetailsProp }>() const props = defineProps<{ data?: DetailsProp }>()
...@@ -18,7 +19,7 @@ const platformList: PlatformItem[] = [ ...@@ -18,7 +19,7 @@ const platformList: PlatformItem[] = [
type: '1', type: '1',
type_name: '公众号', type_name: '公众号',
config_attributes: [ config_attributes: [
{ label: '连接名称', prop: 'name', value: '' } { label: '连接名称', prop: 'name', value: '' },
// { label: '公众号类型', prop: 'type', value: '' }, // { label: '公众号类型', prop: 'type', value: '' },
// { label: '授权方昵称', prop: 'nikeName', value: '' } // { label: '授权方昵称', prop: 'nikeName', value: '' }
], ],
...@@ -30,7 +31,7 @@ const platformList: PlatformItem[] = [ ...@@ -30,7 +31,7 @@ const platformList: PlatformItem[] = [
return false return false
} }
return true return true
} },
}, },
{ {
type: '2', type: '2',
...@@ -39,8 +40,8 @@ const platformList: PlatformItem[] = [ ...@@ -39,8 +40,8 @@ const platformList: PlatformItem[] = [
{ label: '连接名称', prop: 'name', value: '钉钉' }, { label: '连接名称', prop: 'name', value: '钉钉' },
{ label: 'AgentId', prop: 'agentId', value: '8441459810' }, { label: 'AgentId', prop: 'agentId', value: '8441459810' },
{ label: 'AppKey', prop: 'appKey', value: 'dingigucs3beqlotpf24' }, { label: 'AppKey', prop: 'appKey', value: 'dingigucs3beqlotpf24' },
{ label: 'AppSecret', prop: 'appSecret', value: '6dNRvuOzvX_xq5N9tFdjepdf3FeooN25yUZK6ammDbPUVq9sfdXD-sKUg' } { label: 'AppSecret', prop: 'appSecret', value: '6dNRvuOzvX_xq5N9tFdjepdf3FeooN25yUZK6ammDbPUVq9sfdXD-sKUg' },
] ],
}, },
{ {
type: '3', type: '3',
...@@ -49,8 +50,8 @@ const platformList: PlatformItem[] = [ ...@@ -49,8 +50,8 @@ const platformList: PlatformItem[] = [
{ label: '连接名称', prop: 'name', value: '小鹅通' }, { label: '连接名称', prop: 'name', value: '小鹅通' },
{ label: 'app_id', prop: 'app_id', value: 'appc4bolgenF58' }, { label: 'app_id', prop: 'app_id', value: 'appc4bolgenF58' },
{ label: 'client_id', prop: 'client_id', value: '_5e7f809dd6317_qSMuUoAi?type=2SDK' }, { 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', type: '4',
...@@ -58,8 +59,8 @@ const platformList: PlatformItem[] = [ ...@@ -58,8 +59,8 @@ const platformList: PlatformItem[] = [
config_attributes: [ config_attributes: [
{ label: '连接名称', prop: 'name', value: '问卷星' }, { label: '连接名称', prop: 'name', value: '问卷星' },
{ label: 'AppKey', prop: 'appKey', value: '82286f9c5114dc2bda214cd9567dodc' }, { 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: '今日头条' }] }, { type: '5', type_name: '今日头条', config_attributes: [{ label: '连接名称', prop: 'name', value: '今日头条' }] },
{ {
...@@ -73,11 +74,11 @@ const platformList: PlatformItem[] = [ ...@@ -73,11 +74,11 @@ const platformList: PlatformItem[] = [
label: '网站应用简介', label: '网站应用简介',
prop: 'dyInput3', prop: 'dyInput3',
value: value:
'不仅是下载抖音应用程序的官方渠道,也是一个展示抖音最新动态、功能更新和推广活动的平台。用户可以通过官网了解抖音的特色功能、查看热门视频、参与互动活动,以及获取帮助和教程等。官网还为创作者和企业提供了一个展示空间,让他们了解如何利用抖音平台进行内容创作、品牌推广和电子商务等。' '不仅是下载抖音应用程序的官方渠道,也是一个展示抖音最新动态、功能更新和推广活动的平台。用户可以通过官网了解抖音的特色功能、查看热门视频、参与互动活动,以及获取帮助和教程等。官网还为创作者和企业提供了一个展示空间,让他们了解如何利用抖音平台进行内容创作、品牌推广和电子商务等。',
}, },
{ label: '应用官网', prop: 'dyInput4', value: 'https://www.douyin.com' }, { label: '应用官网', prop: 'dyInput4', value: 'https://www.douyin.com' },
{ label: '联系人姓名', prop: 'dyInput5', value: '清控紫荆(北京)教育股份有限公司' } { label: '联系人姓名', prop: 'dyInput5', value: '清控紫荆(北京)教育股份有限公司' },
] ],
}, },
{ {
type: '7', type: '7',
...@@ -85,8 +86,8 @@ const platformList: PlatformItem[] = [ ...@@ -85,8 +86,8 @@ const platformList: PlatformItem[] = [
config_attributes: [ config_attributes: [
{ label: '连接名称', prop: 'name', value: '微博' }, { label: '连接名称', prop: 'name', value: '微博' },
{ label: 'AppKey', prop: 'appKey', value: '1206405345' }, { label: 'AppKey', prop: 'appKey', value: '1206405345' },
{ label: 'AppSecret', prop: 'appSecret', value: '6a6095e113cd28fde6e14c7b7145c5c5' } { label: 'AppSecret', prop: 'appSecret', value: '6a6095e113cd28fde6e14c7b7145c5c5' },
] ],
}, },
{ {
type: '8', type: '8',
...@@ -94,8 +95,8 @@ const platformList: PlatformItem[] = [ ...@@ -94,8 +95,8 @@ const platformList: PlatformItem[] = [
config_attributes: [ config_attributes: [
{ label: '连接名称', prop: 'name', value: '小红书' }, { label: '连接名称', prop: 'name', value: '小红书' },
{ label: 'AppKey', prop: 'appKey', value: '6c1dd8dd64d074d56124c751f6bc240b' }, { label: 'AppKey', prop: 'appKey', value: '6c1dd8dd64d074d56124c751f6bc240b' },
{ label: 'AppSecret', prop: 'appSecret', value: '' } { label: 'AppSecret', prop: 'appSecret', value: '' },
] ],
}, },
{ {
type: '9', type: '9',
...@@ -105,8 +106,8 @@ const platformList: PlatformItem[] = [ ...@@ -105,8 +106,8 @@ const platformList: PlatformItem[] = [
{ label: 'client_id', prop: 'client_id', value: 'swanzhong' }, { label: 'client_id', prop: 'client_id', value: 'swanzhong' },
{ label: 'client_secret', prop: 'client_secret', value: '563a8c6a89d2368194c1c7889c508b34' }, { label: 'client_secret', prop: 'client_secret', value: '563a8c6a89d2368194c1c7889c508b34' },
{ label: 'token URL', prop: 'token', value: 'openapi/user/get' }, { 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', type: '10',
...@@ -116,8 +117,8 @@ const platformList: PlatformItem[] = [ ...@@ -116,8 +117,8 @@ const platformList: PlatformItem[] = [
{ label: 'client_id', prop: 'client_id', value: 'FbFgN2of-mlc' }, { label: 'client_id', prop: 'client_id', value: 'FbFgN2of-mlc' },
{ label: 'SdkAppId', prop: 'sdkAppId', value: 'CV3X1%2FJG7mdNZm03l9puvwPAktmfw1aj8XvBb6sm696MqoW57' }, { label: 'SdkAppId', prop: 'sdkAppId', value: 'CV3X1%2FJG7mdNZm03l9puvwPAktmfw1aj8XvBb6sm696MqoW57' },
{ label: 'token URL', prop: 'token', value: 'https://oauth-login.cloud.ali.com/oauth2/v3/token' }, { 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: '内部消息' }] }, { type_name: '内部消息', type: '11', config_attributes: [{ label: '连接名称', prop: 'name', value: '内部消息' }] },
{ {
...@@ -126,14 +127,14 @@ const platformList: PlatformItem[] = [ ...@@ -126,14 +127,14 @@ const platformList: PlatformItem[] = [
config_attributes: [ config_attributes: [
{ label: '连接名称', prop: 'name', value: '自定义' }, { label: '连接名称', prop: 'name', value: '自定义' },
{ label: 'APP类型', prop: 'appType', 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: '13',
type_name: '紫荆表单', type_name: '紫荆表单',
icon: '99', icon: '99',
config_attributes: [{ label: '连接名称', prop: 'name', value: '紫荆表单' }] config_attributes: [{ label: '连接名称', prop: 'name', value: '紫荆表单' }],
}, },
{ {
type: '14', type: '14',
...@@ -148,14 +149,32 @@ const platformList: PlatformItem[] = [ ...@@ -148,14 +149,32 @@ const platformList: PlatformItem[] = [
return false return false
} }
return true return true
} },
}, },
{ {
icon: 'mall', icon: 'mall',
type: '15', type: '15',
type_name: '紫荆商城', 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(() => { ...@@ -172,7 +191,7 @@ watchEffect(() => {
Object.assign(params, { type, config_attributes: attributes }) 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 defaultStepActive = props.data?.id ? 2 : 1
const stepActive = ref(defaultStepActive) const stepActive = ref(defaultStepActive)
...@@ -183,13 +202,17 @@ function handleChange(data: PlatformItem) { ...@@ -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-- stepActive.value--
} }
// 下一步 // 下一步
async function handleNext() { 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) { if (isEmpty && stepActive.value === 2) {
ElMessage('请填写完整') ElMessage('请填写完整')
return return
...@@ -223,6 +246,8 @@ async function handleSave() { ...@@ -223,6 +246,8 @@ async function handleSave() {
emit('update') emit('update')
emit('update:modelValue', false) emit('update:modelValue', false)
} }
const wechatVideoRef = ref<any>(null)
</script> </script>
<template> <template>
...@@ -239,7 +264,13 @@ async function handleSave() { ...@@ -239,7 +264,13 @@ async function handleSave() {
</el-tab-pane> </el-tab-pane>
<!-- 第二步 --> <!-- 第二步 -->
<el-tab-pane disabled lazy label="配置连接信息" :name="2"> <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>
<el-tab-pane disabled lazy label="测试连接" :name="3"> <el-tab-pane disabled lazy label="测试连接" :name="3">
<el-button type="primary" @click="handleTest">测试连接</el-button> <el-button type="primary" @click="handleTest">测试连接</el-button>
......
...@@ -50,6 +50,7 @@ const edit = function () { ...@@ -50,6 +50,7 @@ const edit = function () {
const iconMap: Record<string, string> = { const iconMap: Record<string, string> = {
'13': '99', '13': '99',
'14': '100', '14': '100',
'16': 'wechatVideo'
} }
const generateUserData = function () { 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 { ...@@ -22,6 +22,7 @@ export interface PlatformItem {
type_name: string type_name: string
icon?: string icon?: string
config_attributes?: ConfigAttribute[] config_attributes?: ConfigAttribute[]
onBeforePrev?: (index: number, data: PlatformItem) => Promise<boolean> | boolean
onBeforeNext?: (index: number, data: PlatformItem) => Promise<boolean> | boolean onBeforeNext?: (index: number, data: PlatformItem) => Promise<boolean> | boolean
} }
...@@ -60,10 +61,10 @@ export interface OtherFields { ...@@ -60,10 +61,10 @@ export interface OtherFields {
rule: any rule: any
} }
export interface StudentFollow{ export interface StudentFollow {
follow_flag: string follow_flag: string
logs: any[] | undefined logs: any[] | undefined
connect_id: string connect_id: string
type: string type: string
data: any data: any
} }
\ No newline at end of file
...@@ -10,7 +10,7 @@ import { ...@@ -10,7 +10,7 @@ import {
updateDynamicGroup, updateDynamicGroup,
getGroupInfo, getGroupInfo,
createRFMGroup, createRFMGroup,
updateRFMGroup updateRFMGroup,
} from '../api' } from '../api'
import UserRule from '@/components/rule/UserRule.vue' import UserRule from '@/components/rule/UserRule.vue'
import EventRule from '@/components/rule/EventRule.vue' import EventRule from '@/components/rule/EventRule.vue'
...@@ -18,6 +18,7 @@ import LabelRule from '@/components/rule/LabelRule.vue' ...@@ -18,6 +18,7 @@ import LabelRule from '@/components/rule/LabelRule.vue'
import UserActionRule from '@/components/rule/UserActionRule.vue' import UserActionRule from '@/components/rule/UserActionRule.vue'
import RFMRule from '@/components/rule/RFMRule.vue' import RFMRule from '@/components/rule/RFMRule.vue'
import { pick, merge } from 'lodash-es' import { pick, merge } from 'lodash-es'
import { useQuestion } from '@/composables/useQuestion'
interface Props { interface Props {
data: Partial<Group> data: Partial<Group>
...@@ -47,7 +48,8 @@ const form: any = reactive({ ...@@ -47,7 +48,8 @@ const form: any = reactive({
event_attr_rule: { current_logic_operate: 'and', items: [] }, event_attr_rule: { current_logic_operate: 'and', items: [] },
tag_rule: { current_logic_operate: 'and', items: [] }, tag_rule: { current_logic_operate: 'and', items: [] },
user_action_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) { function genRuleData(data: any) {
...@@ -56,7 +58,7 @@ function genRuleData(data: any) { ...@@ -56,7 +58,7 @@ function genRuleData(data: any) {
} }
function fetchInfo() { function fetchInfo() {
if (!props.data.id) return if (!props.data.id) return
getGroupInfo({ id: props.data.id }).then(res => { getGroupInfo({ id: props.data.id }).then((res) => {
const { detail } = res.data const { detail } = res.data
const user_attr_rule = genRuleData(detail.user_attr_rule) const user_attr_rule = genRuleData(detail.user_attr_rule)
const event_attr_rule = genRuleData(detail.event_attr_rule) const event_attr_rule = genRuleData(detail.event_attr_rule)
...@@ -82,7 +84,7 @@ function fetchInfo() { ...@@ -82,7 +84,7 @@ function fetchInfo() {
event_attr_rule: { ...event_attr_rule, items: eventRuleItems }, event_attr_rule: { ...event_attr_rule, items: eventRuleItems },
user_action_rule, user_action_rule,
tag_rule: { ...tag_rule, items: tagRuleItems }, tag_rule: { ...tag_rule, items: tagRuleItems },
update_rule: { type: 1, info: 1 } update_rule: { type: 1, info: 1 },
}) })
}) })
} }
...@@ -90,9 +92,14 @@ function fetchInfo() { ...@@ -90,9 +92,14 @@ function fetchInfo() {
watchEffect(() => fetchInfo()) watchEffect(() => fetchInfo())
const rules = ref<FormRules>({ 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() { function handleSubmit() {
formRef.value?.validate().then(() => (isUpdate.value ? handleUpdate() : handleCreate())) formRef.value?.validate().then(() => (isUpdate.value ? handleUpdate() : handleCreate()))
...@@ -122,7 +129,7 @@ async function handleCreate() { ...@@ -122,7 +129,7 @@ async function handleCreate() {
user_attr_rule: JSON.stringify([form.user_attr_rule]), user_attr_rule: JSON.stringify([form.user_attr_rule]),
event_attr_rule: JSON.stringify([form.event_attr_rule]), event_attr_rule: JSON.stringify([form.event_attr_rule]),
tag_rule: JSON.stringify([{ ...form.tag_rule, ...tagRule }]), 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', 'name',
...@@ -132,7 +139,8 @@ async function handleCreate() { ...@@ -132,7 +139,8 @@ async function handleCreate() {
'user_attr_rule', 'user_attr_rule',
'event_attr_rule', 'event_attr_rule',
'tag_rule', 'tag_rule',
'user_action_rule' 'user_action_rule',
'teacher_id',
] ]
) )
await createDynamicGroup(params) await createDynamicGroup(params)
...@@ -142,9 +150,9 @@ async function handleCreate() { ...@@ -142,9 +150,9 @@ async function handleCreate() {
{ {
...form, ...form,
update_rule: JSON.stringify(form.update_rule), 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) await createRFMGroup(params)
} }
...@@ -156,7 +164,7 @@ async function handleCreate() { ...@@ -156,7 +164,7 @@ async function handleCreate() {
async function handleUpdate() { async function handleUpdate() {
if (props.data.type === '1') { if (props.data.type === '1') {
// 静态群组 // 静态群组
const params = pick(form, ['id', 'name', 'status']) const params = pick(form, ['id', 'name', 'status', 'teacher_id'])
await updateStaticGroup(params) await updateStaticGroup(params)
} else if (props.data.type === '2') { } else if (props.data.type === '2') {
// 动态群组 // 动态群组
...@@ -186,7 +194,7 @@ async function handleUpdate() { ...@@ -186,7 +194,7 @@ async function handleUpdate() {
user_attr_rule: JSON.stringify([{ ...form.user_attr_rule, items: attrRuleItems }]), user_attr_rule: JSON.stringify([{ ...form.user_attr_rule, items: attrRuleItems }]),
event_attr_rule: JSON.stringify([{ ...form.event_attr_rule, items: eventRuleItems }]), event_attr_rule: JSON.stringify([{ ...form.event_attr_rule, items: eventRuleItems }]),
tag_rule: JSON.stringify([{ ...form.tag_rule, ...tagRule }]), 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', 'id',
...@@ -197,7 +205,8 @@ async function handleUpdate() { ...@@ -197,7 +205,8 @@ async function handleUpdate() {
'user_attr_rule', 'user_attr_rule',
'event_attr_rule', 'event_attr_rule',
'tag_rule', 'tag_rule',
'user_action_rule' 'user_action_rule',
'teacher_id',
] ]
) )
await updateDynamicGroup(params) await updateDynamicGroup(params)
...@@ -209,7 +218,8 @@ async function handleUpdate() { ...@@ -209,7 +218,8 @@ async function handleUpdate() {
'status', 'status',
'update_status', 'update_status',
'update_rule', 'update_rule',
'rules' 'rules',
'teacher_id',
]) ])
await updateRFMGroup(params) await updateRFMGroup(params)
} }
...@@ -221,7 +231,7 @@ async function handleUpdate() { ...@@ -221,7 +231,7 @@ async function handleUpdate() {
<template> <template>
<el-dialog :title="title" :close-on-click-modal="false" width="980px" @closed="$emit('update:modelValue', false)"> <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-form-item label="群组名称" prop="name">
<el-input v-model="form.name" placeholder="请输入" /> <el-input v-model="form.name" placeholder="请输入" />
</el-form-item> </el-form-item>
...@@ -269,6 +279,11 @@ async function handleUpdate() { ...@@ -269,6 +279,11 @@ async function handleUpdate() {
</div> </div>
</el-form-item> </el-form-item>
</template> </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-form-item label="状态" prop="status">
<el-switch v-model="form.status" active-text="生效" active-value="1" inactive-text="失效" inactive-value="0" /> <el-switch v-model="form.status" active-text="生效" active-value="1" inactive-text="失效" inactive-value="0" />
</el-form-item> </el-form-item>
......
import httpRequest from '@/utils/axios' 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) { export function getLabelTypeList(params?: LabelTypeListRequest) {
...@@ -70,3 +77,8 @@ export function updateLabelRule(data: { id: string; rules: string }) { ...@@ -70,3 +77,8 @@ export function updateLabelRule(data: { id: string; rules: string }) {
export function getLabelMembers(params: { tag_id: string }) { export function getLabelMembers(params: { tag_id: string }) {
return httpRequest.get('/api/lab/v1/experiment/tag/bda-statistics-users', { params }) 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 ...@@ -6,6 +6,7 @@ import { updateStatusRuleList, dateUnitList, weekList, labelList } from '@/utils
import { createLabel, updateLabel } from '../api' import { createLabel, updateLabel } from '../api'
import { useLabelType } from '../composables/useLabelType' import { useLabelType } from '../composables/useLabelType'
import { pick } from 'lodash-es' import { pick } from 'lodash-es'
import { useQuestion } from '@/composables/useQuestion'
const props = defineProps<{ const props = defineProps<{
data?: Label data?: Label
...@@ -30,7 +31,8 @@ const form = reactive({ ...@@ -30,7 +31,8 @@ const form = reactive({
update_rule: { type: 1, info: 1 }, update_rule: { type: 1, info: 1 },
status: '1', status: '1',
label: '', label: '',
weight: 0 weight: 0,
teacher_id: '',
}) })
watchEffect(() => { watchEffect(() => {
if (props.data) { if (props.data) {
...@@ -48,9 +50,12 @@ const rules = ref<FormRules>({ ...@@ -48,9 +50,12 @@ const rules = ref<FormRules>({
name: [{ required: true, message: '请输入标签名称' }], name: [{ required: true, message: '请输入标签名称' }],
label: [{ required: true, message: '请选择标签类型' }], label: [{ required: true, message: '请选择标签类型' }],
type_id: [{ 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() { function handleSubmit() {
formRef?.validate().then(() => (isUpdate ? handleUpdate() : handleCreate())) formRef?.validate().then(() => (isUpdate ? handleUpdate() : handleCreate()))
...@@ -64,7 +69,8 @@ function handleCreate() { ...@@ -64,7 +69,8 @@ function handleCreate() {
'update_rule', 'update_rule',
'status', 'status',
'label', 'label',
'weight' 'weight',
'teacher_id',
]) ])
createLabel(params).then(() => { createLabel(params).then(() => {
ElMessage({ message: '创建成功', type: 'success' }) ElMessage({ message: '创建成功', type: 'success' })
...@@ -82,7 +88,8 @@ function handleUpdate() { ...@@ -82,7 +88,8 @@ function handleUpdate() {
'update_rule', 'update_rule',
'status', 'status',
'label', 'label',
'weight' 'weight',
'teacher_id',
]) ])
updateLabel(params).then(() => { updateLabel(params).then(() => {
ElMessage({ message: '修改成功', type: 'success' }) ElMessage({ message: '修改成功', type: 'success' })
...@@ -94,7 +101,7 @@ function handleUpdate() { ...@@ -94,7 +101,7 @@ function handleUpdate() {
<template> <template>
<el-dialog :title="title" :close-on-click-modal="false" width="600px" @closed="$emit('update:modelValue', false)"> <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-form-item label="标签名称" prop="name">
<el-input v-model="form.name" placeholder="请输入" /> <el-input v-model="form.name" placeholder="请输入" />
</el-form-item> </el-form-item>
...@@ -103,6 +110,11 @@ function handleUpdate() { ...@@ -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-option v-for="item in labelList" :key="item.value" :label="item.label" :value="item.value"></el-option>
</el-select> </el-select>
</el-form-item> </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-form-item label="标签目录" prop="type_id">
<el-select v-model="form.type_id" style="width: 100%"> <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> <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') ...@@ -31,13 +31,13 @@ const statusList = useMapStore().getMapValuesByKey('system_status')
const formRef = $ref<FormInstance>() const formRef = $ref<FormInstance>()
const form = reactive({ const form: any = reactive({
id: '', id: '',
rules: undefined rules: undefined,
}) })
function fetchInfo() { function fetchInfo() {
getLabelRule({ id: props.data.id }).then(res => { getLabelRule({ id: props.data.id }).then((res) => {
const { detail } = res.data const { detail } = res.data
let rules = detail.rules let rules = detail.rules
...@@ -47,9 +47,9 @@ function fetchInfo() { ...@@ -47,9 +47,9 @@ function fetchInfo() {
event_attr_rule: { event_attr_rule: {
current_logic_operate: 'and', current_logic_operate: 'and',
happen_info: { is_happened: true, event_id: '-1', event_name: '所有事件', attr_list: [] }, 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') { if (detail.label == '3') {
...@@ -57,9 +57,9 @@ function fetchInfo() { ...@@ -57,9 +57,9 @@ function fetchInfo() {
event_attr_rule: { event_attr_rule: {
current_logic_operate: 'and', current_logic_operate: 'and',
happen_info: { is_happened: true, event_id: '-1', event_name: '所有事件', attr_list: [] }, 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') { if (detail.label === '4') {
...@@ -75,7 +75,7 @@ function fetchInfo() { ...@@ -75,7 +75,7 @@ function fetchInfo() {
rules = { rules = {
user_attr_rule: { current_logic_operate: 'and', items: [] }, user_attr_rule: { current_logic_operate: 'and', items: [] },
event_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() { ...@@ -100,9 +100,15 @@ function handleUpdate() {
// item.value = Array.isArray(item.value) ? item.value.join(',') : item.value // item.value = Array.isArray(item.value) ? item.value.join(',') : item.value
// return item // 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 = { const params = {
id: form.id, id: form.id,
rules: JSON.stringify(form.rules) rules: JSON.stringify(rules),
// user_attr_rule: JSON.stringify([{ ...form.user_attr_rule, items: attrRuleItems }]), // user_attr_rule: JSON.stringify([{ ...form.user_attr_rule, items: attrRuleItems }]),
// event_attr_rule: JSON.stringify([{ ...form.event_attr_rule, items: eventRuleItems }]) // event_attr_rule: JSON.stringify([{ ...form.event_attr_rule, items: eventRuleItems }])
} }
......
...@@ -29,6 +29,7 @@ export interface Label { ...@@ -29,6 +29,7 @@ export interface Label {
updated_operator: Operator updated_operator: Operator
label: string label: string
weight: string weight: string
teacher_id?: string
} }
// 标签更新规则 // 标签更新规则
export interface LabelUpdateRule { export interface LabelUpdateRule {
...@@ -38,7 +39,10 @@ export interface LabelUpdateRule { ...@@ -38,7 +39,10 @@ export interface LabelUpdateRule {
export type LabelListRequest = Pick<Label, 'id' | 'name'> & { experiment_id?: string } 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 experiment_id?: string
} }
......
...@@ -16,6 +16,8 @@ const userStore = useUserStore() ...@@ -16,6 +16,8 @@ const userStore = useUserStore()
const LabelFormDialog = defineAsyncComponent(() => import('../components/LabelFormDialog.vue')) const LabelFormDialog = defineAsyncComponent(() => import('../components/LabelFormDialog.vue'))
const LabelViewDialog = defineAsyncComponent(() => import('../components/LabelViewDialog.vue')) const LabelViewDialog = defineAsyncComponent(() => import('../components/LabelViewDialog.vue'))
const LabelRuleDialog = defineAsyncComponent(() => import('../components/LabelRuleDialog.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 statusList = useMapStore().getMapValuesByKey('system_status')
const { typeList } = useLabelType() const { typeList } = useLabelType()
...@@ -36,7 +38,7 @@ const listOptions = computed(() => { ...@@ -36,7 +38,7 @@ const listOptions = computed(() => {
params.updated_operator = listParams.updated_operator params.updated_operator = listParams.updated_operator
} }
return params return params
} },
}, },
filters: [ filters: [
{ type: 'input', prop: 'name', placeholder: '请输入标签名称' }, { type: 'input', prop: 'name', placeholder: '请输入标签名称' },
...@@ -46,16 +48,16 @@ const listOptions = computed(() => { ...@@ -46,16 +48,16 @@ const listOptions = computed(() => {
placeholder: '请选择标签目录', placeholder: '请选择标签目录',
options: typeList.value, options: typeList.value,
labelKey: 'name', labelKey: 'name',
valueKey: 'id' valueKey: 'id',
}, },
{ {
type: 'select', type: 'select',
prop: 'label', prop: 'label',
placeholder: '请选择标签类型', placeholder: '请选择标签类型',
options: labelList options: labelList,
}, },
{ type: 'select', prop: 'status', placeholder: '请选择标签状态', options: statusList }, { 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: [ columns: [
{ type: 'selection' }, { type: 'selection' },
...@@ -67,7 +69,7 @@ const listOptions = computed(() => { ...@@ -67,7 +69,7 @@ const listOptions = computed(() => {
prop: 'label', prop: 'label',
computed({ row }: { row: Label }) { computed({ row }: { row: Label }) {
return getNameByValue(row.label, labelList) || row.label return getNameByValue(row.label, labelList) || row.label
} },
}, },
{ label: '标签目录', prop: 'tag_type.name' }, { label: '标签目录', prop: 'tag_type.name' },
{ label: '标签权重', prop: 'weight' }, { label: '标签权重', prop: 'weight' },
...@@ -76,7 +78,7 @@ const listOptions = computed(() => { ...@@ -76,7 +78,7 @@ const listOptions = computed(() => {
prop: 'update_status', prop: 'update_status',
computed({ row }: { row: Label }) { computed({ row }: { row: Label }) {
return getNameByValue(row.update_status, updateStatusRuleList) return getNameByValue(row.update_status, updateStatusRuleList)
} },
}, },
{ {
label: '状态', label: '状态',
...@@ -84,18 +86,18 @@ const listOptions = computed(() => { ...@@ -84,18 +86,18 @@ const listOptions = computed(() => {
computed({ row }: { row: Label }) { computed({ row }: { row: Label }) {
const color = row.status === '1' ? 'var(--main-success-color)' : 'var(--main-color)' const color = row.status === '1' ? 'var(--main-success-color)' : 'var(--main-color)'
return `<span style="color: ${color}">${getNameByValue(row.status, statusList)}</span>` return `<span style="color: ${color}">${getNameByValue(row.status, statusList)}</span>`
} },
}, },
{ {
label: '更新人', label: '更新人',
prop: 'updated_operator.real_name', prop: 'updated_operator.real_name',
computed({ row }: any) { computed({ row }: any) {
return row.updated_operator?.real_name || row.updated_operator?.nickname return row.updated_operator?.real_name || row.updated_operator?.nickname
} },
}, },
{ label: '更新时间', prop: 'updated_time' }, { label: '更新时间', prop: 'updated_time' },
{ label: '操作', slots: 'table-x', width: 320 } { label: '操作', slots: 'table-x', width: 320 },
] ],
} }
}) })
// 刷新 // 刷新
...@@ -151,11 +153,11 @@ function handleSelectionChange(selection: Label[]) { ...@@ -151,11 +153,11 @@ function handleSelectionChange(selection: Label[]) {
} }
const handleRemoves = async function () { const handleRemoves = async function () {
const ids = multipleSelection.map(item => item.id) const ids = multipleSelection.map((item) => item.id)
await ElMessageBox.confirm('确定要删除选中的标签数据吗?', '提示', { await ElMessageBox.confirm('确定要删除选中的标签数据吗?', '提示', {
confirmButtonText: '确认', confirmButtonText: '确认',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning' type: 'warning',
}) })
await deleteLabels({ ids: JSON.stringify(ids) }) await deleteLabels({ ids: JSON.stringify(ids) })
appList?.refetch(true) appList?.refetch(true)
...@@ -169,10 +171,23 @@ const handleRemoves = async function () { ...@@ -169,10 +171,23 @@ const handleRemoves = async function () {
<div class="label-left"><LabelType :active-id="listParams.type_id" @select="handleSelect"></LabelType></div> <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"> <AppList v-bind="listOptions" ref="appList" class="label-right" @selection-change="handleSelectionChange">
<template #header-buttons> <template #header-buttons>
<el-button type="primary" :icon="Plus" @click="handleAdd" v-if="!userStore.status.tag_status">新建</el-button> <div style="display: flex; justify-content: space-between">
<el-button type="primary" plain :icon="Delete" :disabled="!multipleSelection.length" @click="handleRemoves" v-permission="'experiment_tag_delete'" <div>
>删除</el-button <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>
<template #filter-user> <template #filter-user>
<SelectUser v-model="listParams.updated_operator" placeholder="更新人" @change="handleRefresh"></SelectUser> <SelectUser v-model="listParams.updated_operator" placeholder="更新人" @change="handleRefresh"></SelectUser>
...@@ -181,18 +196,27 @@ const handleRemoves = async function () { ...@@ -181,18 +196,27 @@ const handleRemoves = async function () {
<template #table-x="{ row }"> <template #table-x="{ row }">
<el-button type="primary" plain @click="handleRule(row)">规则</el-button> <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="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="handleUpdate(row)" v-permission="'experiment_tag_update'"
<el-button type="primary" plain @click="handleRemove(row)" v-permission="'experiment_tag_delete'">删除</el-button> >编辑</el-button
>
<el-button type="primary" plain @click="handleRemove(row)" v-permission="'experiment_tag_delete'"
>删除</el-button
>
</template> </template>
</AppList> </AppList>
</div> </div>
</AppCard> </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> <LabelViewDialog v-model="viewVisible" :data="currentRow" v-if="viewVisible && currentRow"></LabelViewDialog>
<!-- 规则 --> <!-- 规则 -->
<LabelRuleDialog v-model="ruleVisible" :data="currentRow" v-if="ruleVisible && currentRow"></LabelRuleDialog> <LabelRuleDialog v-model="ruleVisible" :data="currentRow" v-if="ruleVisible && currentRow"></LabelRuleDialog>
<AISummaryDialog v-model="aiDialogVisible" v-if="aiDialogVisible"></AISummaryDialog>
</template> </template>
<style lang="scss"> <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' ...@@ -3,19 +3,11 @@ import { useChat } from '@/composables/useChat'
import { getAttrList } from '../api' import { getAttrList } from '../api'
const form = inject('form') const form = inject('form')
const { isLoading, post, messages } = useChat() const { post } = useChat()
watch(
messages,
() => {
const lastMessage = messages.value[messages.value.length - 1]
if (lastMessage) {
form.shopping_guide_short_title = lastMessage.content.replace('推荐导购短标题:', '')
}
},
{ deep: true }
)
const descLoading = ref(false)
function handleAIGenerate() { function handleAIGenerate() {
descLoading.value = true
post({ post({
role: 'user', role: 'user',
content: `请根据以下内容,给出1个用于抖音电商使用的推荐的“导购短标题”内容:${form.title} content: `请根据以下内容,给出1个用于抖音电商使用的推荐的“导购短标题”内容:${form.title}
...@@ -25,6 +17,30 @@ function handleAIGenerate() { ...@@ -25,6 +17,30 @@ function handleAIGenerate() {
3. 标题要具有吸引力,能够激发消费者的购买欲望。 3. 标题要具有吸引力,能够激发消费者的购买欲望。
4. 标题要简洁明了,不要过于冗长或复杂。 4. 标题要简洁明了,不要过于冗长或复杂。
5. 输出结果以“推荐导购短标题:”开始`, 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(() => { ...@@ -76,7 +92,13 @@ const unimportanceTotal = computed(() => {
<div> <div>
<el-form-item label="商品标题" prop="title" :rules="[{ required: true, message: '请输入', trigger: 'blur' }]"> <el-form-item label="商品标题" prop="title" :rules="[{ required: true, message: '请输入', trigger: 'blur' }]">
<div class="form-tips">标题不规范会有可能引起商品下架,影响您的正常销售,请点击学习商品发布规范认真填写</div> <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>
<el-form-item label="导购短标题" prop="shopping_guide_short_title"> <el-form-item label="导购短标题" prop="shopping_guide_short_title">
<div class="form-tips">短标题可用于商品搜索、首页推荐、物流单等场景,请提炼商品关键信息,客观准确填写</div> <div class="form-tips">短标题可用于商品搜索、首页推荐、物流单等场景,请提炼商品关键信息,客观准确填写</div>
...@@ -86,7 +108,7 @@ const unimportanceTotal = computed(() => { ...@@ -86,7 +108,7 @@ const unimportanceTotal = computed(() => {
size="large" size="large"
v-model="form.shopping_guide_short_title" v-model="form.shopping_guide_short_title"
style="flex: 1" /> 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> </div>
</el-form-item> </el-form-item>
<el-form-item <el-form-item
......
...@@ -61,3 +61,8 @@ export function getRecordList(params: { live_practice_id: string }) { ...@@ -61,3 +61,8 @@ export function getRecordList(params: { live_practice_id: string }) {
export function getRecord(params: { id: string }) { export function getRecord(params: { id: string }) {
return httpRequest.get('/api/lab/v1/experiment/live-practice/live-practice-record', { params }) 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>
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论