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

feat: 增加AI分析与总结

上级 78ec16fa
......@@ -34,3 +34,8 @@ export function getMemberMetaAttrs() {
export function getUserTags(params: { sso_id: string; limit: number }) {
return httpRequest.get('/api/lab/v1/experiment/analyse/user-tags', { params })
}
// AI分析与总结
export function getAISummary() {
return httpRequest.get('/api/lab/v1/experiment/member/ai-all-person')
}
<script setup>
import { getAISummary } from '../api'
const content = ref('')
const isLoading = ref(false)
async function fetchAI() {
isLoading.value = true
const res = await getAISummary()
content.value = res.data.result
isLoading.value = false
}
onMounted(() => {
fetchAI()
})
</script>
<template>
<el-dialog title="AI用户整体画像分析与建议">
<div v-loading="isLoading">
<el-input type="textarea" :rows="15" :value="content"></el-input>
</div>
<template #footer>
<el-row justify="center">
<el-button round @click="$emit('update:modelValue', false)">关闭</el-button>
</el-row>
</template>
</el-dialog>
</template>
......@@ -8,6 +8,9 @@ import { useMapStore } from '@/stores/map'
import { getNameByValue } from '@/utils/dictionary'
import * as api from '../api'
const AISummaryDialog = defineAsyncComponent(() => import('../components/AISummaryDialog.vue'))
const aiDialogVisible = ref(false)
const connectionTypeList = useMapStore().getMapValuesByKey('experiment_connection_type')
const statusList = useMapStore().getMapValuesByKey('system_status')
......@@ -57,7 +60,7 @@ const genderOption = computed(() => {
return {
grid: { left: '60', right: '60' },
tooltip: {
trigger: 'item'
trigger: 'item',
},
yAxis: {
data: ['男性', '女性'],
......@@ -67,15 +70,20 @@ const genderOption = computed(() => {
axisLabel: {
formatter: function (value, index) {
const total = parseInt(man.total) + parseInt(woman.total)
return value + '\n' + (index === 0 ? ((man.total / total) * 100).toFixed(1) : ((woman.total / total) * 100).toFixed(1)) + '%'
}
}
return (
value +
'\n' +
(index === 0 ? ((man.total / total) * 100).toFixed(1) : ((woman.total / total) * 100).toFixed(1)) +
'%'
)
},
},
},
xAxis: {
splitLine: { show: false },
axisLabel: { show: false },
axisTick: { show: false },
axisLine: { show: false }
axisLine: { show: false },
},
series: [
{
......@@ -86,10 +94,10 @@ const genderOption = computed(() => {
symbolMargin: 10,
data: [
{ value: man.total, symbol: manIcon, itemStyle: { color: '#767aca' } },
{ value: woman.total, symbol: womanIcon, itemStyle: { color: '#d26080' } }
]
}
]
{ value: woman.total, symbol: womanIcon, itemStyle: { color: '#d26080' } },
],
},
],
}
})
......@@ -101,7 +109,7 @@ async function fetchConnections() {
loading2.value = true
try {
const res = await api.getMemberConnections({ sso_id: userValue.value })
connection.value = res.data.items.map(item => {
connection.value = res.data.items.map((item) => {
return { ...item, group_name: getNameByValue(item.group_name, connectionTypeList) }
})
} finally {
......@@ -113,24 +121,24 @@ const connectionOption = computed(() => {
return {
grid: { left: '5%', top: '10%', right: '5%', bottom: '5%', containLabel: true },
tooltip: {
trigger: 'axis'
trigger: 'axis',
},
xAxis: {
type: 'category',
axisLabel: { interval: 0 },
data: connection.value.map(item => item.group_name)
data: connection.value.map((item) => item.group_name),
},
yAxis: {
type: 'value'
type: 'value',
},
series: [
{
name: '数据',
type: 'bar',
label: { show: true, position: 'top' },
data: connection.value.map(item => item.total)
}
]
data: connection.value.map((item) => item.total),
},
],
}
})
......@@ -142,7 +150,7 @@ async function fetchStatus() {
loading3.value = true
try {
const res = await api.getMemberStatus({ sso_id: userValue.value })
status.value = res.data.items.map(item => {
status.value = res.data.items.map((item) => {
return { name: getNameByValue(item.group_name, statusList), value: item.total }
})
} finally {
......@@ -155,7 +163,7 @@ const statusOption = computed(() => {
grid: { left: '5%', top: '10%', right: '5%', bottom: '5%', containLabel: true },
tooltip: {
trigger: 'item',
formatter: '{b}: {c}<br />{d}%'
formatter: '{b}: {c}<br />{d}%',
},
series: [
{
......@@ -163,9 +171,9 @@ const statusOption = computed(() => {
label: { formatter: '{b}\n{d}%' },
itemStyle: { borderRadius: 6 },
radius: [0, '70%'],
data: status.value
}
]
data: status.value,
},
],
}
})
</script>
......@@ -173,15 +181,28 @@ const statusOption = computed(() => {
<template>
<AppCard title="用户分析">
<el-form inline label-suffix=":">
<el-form-item label="实验名称">{{ info?.name }}</el-form-item>
<el-form-item label="请选择学生/老师">
<el-select v-model="userValue" filterable>
<el-option v-for="item in userList" :label="item.name" :value="item.sso_id" :key="item.sso_id"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" :icon="DataLine" :loading="loading" @click="handleStart">分析</el-button>
</el-form-item>
<div style="display: flex; justify-content: space-between">
<div>
<el-form-item label="实验名称">{{ info?.name }}</el-form-item>
<el-form-item label="请选择学生/老师">
<el-select v-model="userValue" filterable>
<el-option
v-for="item in userList"
:label="item.name"
:value="item.sso_id"
:key="item.sso_id"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" :icon="DataLine" :loading="loading" @click="handleStart">分析</el-button>
</el-form-item>
</div>
<div>
<el-form-item>
<el-button type="primary" @click="aiDialogVisible = true">AI分析与总结</el-button>
</el-form-item>
</div>
</div>
<el-divider style="margin: 10px 0" />
<el-form-item label="用户总数">
<b class="total">{{ userTotal }}</b>
......@@ -199,6 +220,7 @@ const statusOption = computed(() => {
<UserChart :ssoId="userValue" />
</div>
</AppCard>
<AISummaryDialog v-model="aiDialogVisible" v-if="aiDialogVisible"></AISummaryDialog>
</template>
<style lang="scss" scoped>
......
......@@ -70,3 +70,8 @@ export function updateLabelRule(data: { id: string; rules: string }) {
export function getLabelMembers(params: { tag_id: string }) {
return httpRequest.get('/api/lab/v1/experiment/tag/bda-statistics-users', { params })
}
// AI分析与总结
export function getAISummary() {
return httpRequest.get('/api/lab/v1/experiment/member/ai-tag')
}
<script setup>
import { getAISummary } from '../api'
const content = ref('')
const isLoading = ref(false)
async function fetchAI() {
isLoading.value = true
const res = await getAISummary()
content.value = res.data.result
isLoading.value = false
}
onMounted(() => {
fetchAI()
})
</script>
<template>
<el-dialog title="AI用户整体画像分析与建议">
<div v-loading="isLoading">
<el-input type="textarea" :rows="15" :value="content"></el-input>
</div>
<template #footer>
<el-row justify="center">
<el-button round @click="$emit('update:modelValue', false)">关闭</el-button>
</el-row>
</template>
</el-dialog>
</template>
......@@ -16,6 +16,8 @@ const userStore = useUserStore()
const LabelFormDialog = defineAsyncComponent(() => import('../components/LabelFormDialog.vue'))
const LabelViewDialog = defineAsyncComponent(() => import('../components/LabelViewDialog.vue'))
const LabelRuleDialog = defineAsyncComponent(() => import('../components/LabelRuleDialog.vue'))
const AISummaryDialog = defineAsyncComponent(() => import('../components/AISummaryDialog.vue'))
const aiDialogVisible = ref(false)
const statusList = useMapStore().getMapValuesByKey('system_status')
const { typeList } = useLabelType()
......@@ -36,7 +38,7 @@ const listOptions = computed(() => {
params.updated_operator = listParams.updated_operator
}
return params
}
},
},
filters: [
{ type: 'input', prop: 'name', placeholder: '请输入标签名称' },
......@@ -46,16 +48,16 @@ const listOptions = computed(() => {
placeholder: '请选择标签目录',
options: typeList.value,
labelKey: 'name',
valueKey: 'id'
valueKey: 'id',
},
{
type: 'select',
prop: 'label',
placeholder: '请选择标签类型',
options: labelList
options: labelList,
},
{ type: 'select', prop: 'status', placeholder: '请选择标签状态', options: statusList },
{ type: 'input', prop: 'updated_operator', placeholder: '更新人', slots: 'filter-user' }
{ type: 'input', prop: 'updated_operator', placeholder: '更新人', slots: 'filter-user' },
],
columns: [
{ type: 'selection' },
......@@ -67,7 +69,7 @@ const listOptions = computed(() => {
prop: 'label',
computed({ row }: { row: Label }) {
return getNameByValue(row.label, labelList) || row.label
}
},
},
{ label: '标签目录', prop: 'tag_type.name' },
{ label: '标签权重', prop: 'weight' },
......@@ -76,7 +78,7 @@ const listOptions = computed(() => {
prop: 'update_status',
computed({ row }: { row: Label }) {
return getNameByValue(row.update_status, updateStatusRuleList)
}
},
},
{
label: '状态',
......@@ -84,18 +86,18 @@ const listOptions = computed(() => {
computed({ row }: { row: Label }) {
const color = row.status === '1' ? 'var(--main-success-color)' : 'var(--main-color)'
return `<span style="color: ${color}">${getNameByValue(row.status, statusList)}</span>`
}
},
},
{
label: '更新人',
prop: 'updated_operator.real_name',
computed({ row }: any) {
return row.updated_operator?.real_name || row.updated_operator?.nickname
}
},
},
{ label: '更新时间', prop: 'updated_time' },
{ label: '操作', slots: 'table-x', width: 320 }
]
{ label: '操作', slots: 'table-x', width: 320 },
],
}
})
// 刷新
......@@ -151,11 +153,11 @@ function handleSelectionChange(selection: Label[]) {
}
const handleRemoves = async function () {
const ids = multipleSelection.map(item => item.id)
const ids = multipleSelection.map((item) => item.id)
await ElMessageBox.confirm('确定要删除选中的标签数据吗?', '提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning'
type: 'warning',
})
await deleteLabels({ ids: JSON.stringify(ids) })
appList?.refetch(true)
......@@ -169,10 +171,23 @@ const handleRemoves = async function () {
<div class="label-left"><LabelType :active-id="listParams.type_id" @select="handleSelect"></LabelType></div>
<AppList v-bind="listOptions" ref="appList" class="label-right" @selection-change="handleSelectionChange">
<template #header-buttons>
<el-button type="primary" :icon="Plus" @click="handleAdd" v-if="!userStore.status.tag_status">新建</el-button>
<el-button type="primary" plain :icon="Delete" :disabled="!multipleSelection.length" @click="handleRemoves" v-permission="'experiment_tag_delete'"
>删除</el-button
>
<div style="display: flex; justify-content: space-between">
<div>
<el-button type="primary" :icon="Plus" @click="handleAdd" v-if="!userStore.status.tag_status"
>新建</el-button
>
<el-button
type="primary"
plain
:icon="Delete"
:disabled="!multipleSelection.length"
@click="handleRemoves"
v-permission="'experiment_tag_delete'"
>删除</el-button
>
</div>
<el-button type="primary" @click="aiDialogVisible = true">AI建议</el-button>
</div>
</template>
<template #filter-user>
<SelectUser v-model="listParams.updated_operator" placeholder="更新人" @change="handleRefresh"></SelectUser>
......@@ -181,18 +196,27 @@ const handleRemoves = async function () {
<template #table-x="{ row }">
<el-button type="primary" plain @click="handleRule(row)">规则</el-button>
<el-button type="primary" plain @click="handleView(row)">查看</el-button>
<el-button type="primary" plain @click="handleUpdate(row)" v-permission="'experiment_tag_update'">编辑</el-button>
<el-button type="primary" plain @click="handleRemove(row)" v-permission="'experiment_tag_delete'">删除</el-button>
<el-button type="primary" plain @click="handleUpdate(row)" v-permission="'experiment_tag_update'"
>编辑</el-button
>
<el-button type="primary" plain @click="handleRemove(row)" v-permission="'experiment_tag_delete'"
>删除</el-button
>
</template>
</AppList>
</div>
</AppCard>
<!-- 新建/修改标签 -->
<LabelFormDialog v-model="formVisible" :data="currentRow" @update="handleRefresh" v-if="formVisible"></LabelFormDialog>
<LabelFormDialog
v-model="formVisible"
:data="currentRow"
@update="handleRefresh"
v-if="formVisible"></LabelFormDialog>
<!-- 查看标签 -->
<LabelViewDialog v-model="viewVisible" :data="currentRow" v-if="viewVisible && currentRow"></LabelViewDialog>
<!-- 规则 -->
<LabelRuleDialog v-model="ruleVisible" :data="currentRow" v-if="ruleVisible && currentRow"></LabelRuleDialog>
<AISummaryDialog v-model="aiDialogVisible" v-if="aiDialogVisible"></AISummaryDialog>
</template>
<style lang="scss">
......
......@@ -125,3 +125,8 @@ export function syncMember() {
export function clearMember() {
return httpRequest.get('/api/lab/v1/experiment/member/clear')
}
// AI分析与总结
export function getAISummary(params: { member_id: string }) {
return httpRequest.get('/api/lab/v1/experiment/member/ai-one-person', { params })
}
<script setup>
import { getAISummary } from '../api'
const props = defineProps(['id'])
const content = ref('')
const isLoading = ref(false)
async function fetchAI() {
isLoading.value = true
const res = await getAISummary({ member_id: props.id })
content.value = res.data.result
isLoading.value = false
}
onMounted(() => {
fetchAI()
})
</script>
<template>
<el-dialog title="AI用户画像分析与建议">
<div v-loading="isLoading">
<el-input type="textarea" :rows="15" :value="content"></el-input>
</div>
<template #footer>
<el-row justify="center">
<el-button round @click="$emit('update:modelValue', false)">关闭</el-button>
</el-row>
</template>
</el-dialog>
</template>
......@@ -7,6 +7,8 @@ import Icon from '@/components/ConnectionIcon.vue'
const ViewEvent = defineAsyncComponent(() => import('@/components/ViewEvent.vue'))
const ViewLabel = defineAsyncComponent(() => import('@/components/ViewLabel.vue'))
const ViewGroup = defineAsyncComponent(() => import('@/components/ViewGroup.vue'))
const AISummaryDialog = defineAsyncComponent(() => import('../components/AISummaryDialog.vue'))
const aiDialogVisible = ref(false)
const route = useRoute()
......@@ -20,7 +22,7 @@ let data = $ref<ImageProp>()
let fieldsList = $ref<MemberFieldsProp[]>([])
onMounted(() => {
// 画像
getMemberImage({ id: userId.value, 'per-page': 100 }).then(res => {
getMemberImage({ id: userId.value, 'per-page': 100 }).then((res) => {
data = res.data
getFields(res.data)
})
......@@ -28,7 +30,7 @@ onMounted(() => {
})
const getFields = function (data: { fields: any }) {
getMemberFieldsList().then(res => {
getMemberFieldsList().then((res) => {
fieldsList = res.data.map((item: MemberFieldsProp) => {
if (data.fields[item.id]) {
item.value = data.fields[item.id]
......@@ -86,7 +88,12 @@ async function fetchEvent(isReset = false) {
if (isReset) {
Object.assign(event, { page: 1, total: 0, list: [] })
}
const { data } = await getMemberImage({ id: userId.value, connection_id: currentConnection.value, page: event.page, 'per-page': 20 })
const { data } = await getMemberImage({
id: userId.value,
connection_id: currentConnection.value,
page: event.page,
'per-page': 20,
})
Object.assign(event, { page: event.page + 1, total: data.events.total, list: [...event.list, ...data.events.list] })
}
watch(currentConnection, () => {
......@@ -100,7 +107,11 @@ watch(currentConnection, () => {
<div class="info-name" style="min-width: 300px">
<div class="tx">
<img
:src="data.gender === '1' ? 'https://webapp-pub.ezijing.com/pages/assa/dml_boy.png' : 'https://webapp-pub.ezijing.com/pages/assa/dml_girl.png'" />
:src="
data.gender === '1'
? 'https://webapp-pub.ezijing.com/pages/assa/dml_boy.png'
: 'https://webapp-pub.ezijing.com/pages/assa/dml_girl.png'
" />
<!-- https://webapp-pub.ezijing.com/pages/assa/dml_boy.png -->
<!-- <el-icon :size="50" color="#fff"><UserFilled /></el-icon> -->
</div>
......@@ -117,12 +128,16 @@ watch(currentConnection, () => {
<el-form label-suffix=":" label-width="110px">
<el-form-item label="用户ID">{{ data.id }} </el-form-item>
<el-form-item label="状态">
<span :style="`color: ${data.status === '1' ? 'rgba(0,172,39,1)' : '#ba143e'}`">{{ data.status_name }}</span>
<span :style="`color: ${data.status === '1' ? 'rgba(0,172,39,1)' : '#ba143e'}`">{{
data.status_name
}}</span>
</el-form-item>
</el-form>
<el-form label-suffix=":" label-width="110px">
<el-form-item label="最近活跃时间">{{ data.updated_time }} </el-form-item>
<el-form-item label="最近活跃时间" style="opacity: 0">{{ data.updated_time }} </el-form-item>
<el-form-item label-width="0">
<el-button type="primary" @click="aiDialogVisible = true">AI分析与总结</el-button>
</el-form-item>
</el-form>
</div>
</div>
......@@ -148,7 +163,14 @@ watch(currentConnection, () => {
<el-tabs class="demo-tabs">
<el-tab-pane label="当前标签">
<div class="scroll" v-if="data?.tags && data.tags.length">
<el-tag class="ml-2" type="success" v-for="(item, index) in data.tag_list" :key="index" @click="handleViewLabel(item)">{{ item.name }}</el-tag>
<el-tag
class="ml-2"
type="success"
v-for="(item, index) in data.tag_list"
:key="index"
@click="handleViewLabel(item)"
>{{ item.name }}</el-tag
>
</div>
<el-empty description="暂无数据" :image-size="80" v-else />
</el-tab-pane>
......@@ -156,7 +178,9 @@ watch(currentConnection, () => {
<el-tabs class="demo-tabs">
<el-tab-pane label="历史标签">
<div class="scroll" v-if="data?.history_tags && data.history_tags.length">
<el-tag class="ml-2" type="success" v-for="(item, index) in data.history_tags" :key="index">{{ item }}</el-tag>
<el-tag class="ml-2" type="success" v-for="(item, index) in data.history_tags" :key="index">{{
item
}}</el-tag>
</div>
<el-empty description="暂无数据" :image-size="80" v-else />
</el-tab-pane>
......@@ -166,9 +190,14 @@ watch(currentConnection, () => {
<el-tabs class="demo-tabs">
<el-tab-pane label="静态群组">
<div class="scroll" v-if="data?.static_groups && data.static_groups.length">
<el-tag class="ml-2" type="success" v-for="(item, index) in data.static_group_list" :key="index" @click="handleViewGroup(item)">{{
item.name
}}</el-tag>
<el-tag
class="ml-2"
type="success"
v-for="(item, index) in data.static_group_list"
:key="index"
@click="handleViewGroup(item)"
>{{ item.name }}</el-tag
>
</div>
<el-empty description="暂无数据" :image-size="80" v-else />
</el-tab-pane>
......@@ -176,9 +205,14 @@ watch(currentConnection, () => {
<el-tabs class="demo-tabs">
<el-tab-pane label="动态群组">
<div class="scroll" v-if="data?.dynamic_groups && data.dynamic_groups.length">
<el-tag class="ml-2" type="success" v-for="(item, index) in data.dynamic_group_list" :key="index" @click="handleViewGroup(item)">{{
item.name
}}</el-tag>
<el-tag
class="ml-2"
type="success"
v-for="(item, index) in data.dynamic_group_list"
:key="index"
@click="handleViewGroup(item)"
>{{ item.name }}</el-tag
>
</div>
<el-empty description="暂无数据" :image-size="80" v-else />
</el-tab-pane>
......@@ -187,7 +221,9 @@ watch(currentConnection, () => {
<AppCard class="card" title="用户行为轨迹">
<div style="text-align: center">
<el-radio-group v-model="currentConnection">
<el-radio-button :value="item.id" v-for="item in connectionList" :key="item.id">{{ item.type_name }}</el-radio-button>
<el-radio-button :value="item.id" v-for="item in connectionList" :key="item.id">{{
item.type_name
}}</el-radio-button>
</el-radio-group>
</div>
<template v-if="event.list.length">
......@@ -214,11 +250,23 @@ watch(currentConnection, () => {
</AppCard>
</div>
<!-- 事件详情 -->
<ViewEvent v-model="viewEventVisible" :event="currentViewEvent" :user="data" v-if="viewEventVisible && currentViewEvent"></ViewEvent>
<ViewEvent
v-model="viewEventVisible"
:event="currentViewEvent"
:user="data"
v-if="viewEventVisible && currentViewEvent"></ViewEvent>
<!-- 查看标签 -->
<ViewLabel v-model="viewLabelVisible" :data="currentViewLabel" v-if="viewLabelVisible && currentViewLabel"></ViewLabel>
<ViewLabel
v-model="viewLabelVisible"
:data="currentViewLabel"
v-if="viewLabelVisible && currentViewLabel"></ViewLabel>
<!-- 查看群组 -->
<ViewGroup v-model="viewGroupVisible" :data="currentViewGroup" v-if="viewGroupVisible && currentViewGroup"></ViewGroup>
<ViewGroup
v-model="viewGroupVisible"
:data="currentViewGroup"
v-if="viewGroupVisible && currentViewGroup"></ViewGroup>
<AISummaryDialog v-model="aiDialogVisible" :id="userId" v-if="aiDialogVisible"></AISummaryDialog>
</template>
<style lang="scss">
.info-box {
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论