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

Merge branch 'pro' into gdrtvu

......@@ -288,6 +288,13 @@
"useParentElement": true,
"usePerformanceObserver": true,
"watchDeep": true,
"watchImmediate": true
"watchImmediate": true,
"ExtractDefaultPropTypes": true,
"ExtractPropTypes": true,
"ExtractPublicPropTypes": true,
"WritableComputedRef": true,
"injectLocal": true,
"provideLocal": true,
"useClipboardItems": true
}
}
......@@ -44,6 +44,7 @@ declare global {
const h: typeof import('vue')['h']
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
const inject: typeof import('vue')['inject']
const injectLocal: typeof import('@vueuse/core')['injectLocal']
const isDefined: typeof import('@vueuse/core')['isDefined']
const isProxy: typeof import('vue')['isProxy']
const isReactive: typeof import('vue')['isReactive']
......@@ -73,6 +74,7 @@ declare global {
const onUpdated: typeof import('vue')['onUpdated']
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
const provide: typeof import('vue')['provide']
const provideLocal: typeof import('@vueuse/core')['provideLocal']
const reactify: typeof import('@vueuse/core')['reactify']
const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
const reactive: typeof import('vue')['reactive']
......@@ -136,6 +138,7 @@ declare global {
const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
const useCached: typeof import('@vueuse/core')['useCached']
const useClipboard: typeof import('@vueuse/core')['useClipboard']
const useClipboardItems: typeof import('@vueuse/core')['useClipboardItems']
const useCloned: typeof import('@vueuse/core')['useCloned']
const useColorMode: typeof import('@vueuse/core')['useColorMode']
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
......@@ -291,5 +294,6 @@ declare global {
// for type re-export
declare global {
// @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, InjectionKey, PropType, Ref, VNode } from 'vue'
export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
import('vue')
}
......@@ -10,5 +10,6 @@
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
<script src="https://webapp-pub.ezijing.com/plugins/sky-agents/sky-agent.umd.cjs?v=1"></script>
</body>
</html>
差异被折叠。
......@@ -15,37 +15,43 @@
"cert": "node ./cert.js"
},
"dependencies": {
"@element-plus/icons-vue": "^2.1.0",
"@element-plus/icons-vue": "^2.3.1",
"@fortaine/fetch-event-source": "^3.0.6",
"@tinymce/tinymce-vue": "^5.0.1",
"@vue-flow/controls": "^1.0.4",
"@vue-flow/core": "^1.17.4",
"@vueuse/core": "^10.3.0",
"axios": "^1.5.0",
"@vueuse/core": "^10.9.0",
"axios": "^1.6.8",
"blueimp-md5": "^2.19.0",
"echarts": "^5.4.3",
"element-plus": "^2.3.14",
"dayjs": "^1.11.10",
"echarts": "^5.5.0",
"echarts-wordcloud": "^2.1.0",
"element-plus": "^2.6.3",
"lodash-es": "^4.17.21",
"nanoid": "^4.0.2",
"pinia": "^2.1.6",
"vue": "^3.3.4",
"vue-echarts": "^6.6.1",
"vue-router": "^4.2.4"
"nanoid": "^5.0.7",
"pinia": "^2.1.7",
"vue": "^3.4.23",
"vue-echarts": "^6.6.9",
"vue-router": "^4.3.2",
"xss": "^1.0.15"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.2.0",
"@types/blueimp-md5": "^2.18.0",
"@tsconfig/node20": "^20.1.4",
"@types/blueimp-md5": "^2.18.2",
"@types/node": "^20.3.1",
"@vitejs/plugin-vue": "^4.3.4",
"@vitejs/plugin-vue": "^4.6.2",
"@vue-macros/reactivity-transform": "^0.4.4",
"@vue/eslint-config-typescript": "^12.0.0",
"@vue/tsconfig": "^0.1.3",
"@vue/tsconfig": "^0.5.1",
"ali-oss": "^6.18.1",
"chalk": "^5.2.0",
"eslint": "^8.43.0",
"eslint-plugin-vue": "^9.17.0",
"sass": "^1.67.0",
"typescript": "~4.9.5",
"unplugin-auto-import": "^0.16.6",
"vite": "^4.4.9",
"vue-tsc": "^1.8.11"
"typescript": "~5.4.5",
"unplugin-auto-import": "^0.17.5",
"vite": "^4.5.3",
"vue-tsc": "^1.8.27"
}
}
......@@ -116,6 +116,11 @@ export function searchEventAttrs(params?: {
return httpRequest.get('/api/lab/v1/experiment/event/search-attributes', { params })
}
// 获取当前实验下可选的人员列表
export function getUserList() {
return httpRequest.get('/api/lab/v1/experiment/analyse/users')
}
// 获取分片大小和唯一文件名
export function getLocalFileChunk(params: { file_size: number; file_name: string }) {
return httpRequest.get('/api/lab/v1/common/file/chunk', { params })
......
......@@ -81,13 +81,13 @@ textarea:focus {
--main-success-color: #00ac27;
}
.el-dialog__header {
margin-right: 0 !important;
border-bottom: 1px solid #e6e6e6;
margin: 0 -16px 30px;
padding: 0 16px;
}
.info .el-form-item {
margin-bottom: 0;
}
.info tr:last-child td {
padding-bottom: 0 !important;
}
......@@ -14,3 +14,15 @@
// 如果只是按需导入,则可以忽略以下内容。
// 如果你想导入所有样式:
@use 'element-plus/theme-chalk/src/index.scss' as *;
.el-form--inline {
.el-form-item {
.el-input,
.el-cascader,
.el-select,
.el-date-editor,
.el-autocomplete {
width: 200px;
}
}
}
......@@ -56,8 +56,27 @@
.rule-item {
margin: 10px 0;
}
.event-rule {
.event-rule,
.user-action-rule {
section + section {
margin-top: 30px;
}
}
.rule-tips {
color: #999;
margin-bottom: 10px;
display: flex;
align-items: center;
.el-icon {
margin-right: 5px;
color: var(--main-color);
}
}
.rule-tag {
margin-right: 10px;
}
.rule-input {
width: 100px;
}
<script setup lang="ts">
import { useFullscreen } from '@vueuse/core'
import { FullScreen } from '@element-plus/icons-vue'
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { BarChart, PieChart, LineChart, PictorialBarChart, FunnelChart, RadarChart } from 'echarts/charts'
import { TitleComponent, TooltipComponent, LegendComponent, GridComponent } from 'echarts/components'
import VChart from 'vue-echarts'
import 'echarts-wordcloud'
use([
CanvasRenderer,
BarChart,
PieChart,
LineChart,
PictorialBarChart,
FunnelChart,
RadarChart,
TitleComponent,
TooltipComponent,
LegendComponent,
GridComponent
])
const props = defineProps<{ title?: string; options?: any; loading?: boolean }>()
const el = ref<HTMLElement | null>(null)
const { isFullscreen, toggle } = useFullscreen(el)
const isEmpty = computed(() => {
if (!props.options) return true
return !Object.keys(props.options)
})
const color = ['#af1c40', '#c17933', '#8f0034', '#d45548', '#ab3259', '#dec34c', '#8b8920', '#a25a6d']
</script>
<template>
<div class="chart-card" ref="el" :class="{ isFullscreen }">
<div class="chart-hd">
<slot name="title">
<h3>{{ props.title }}</h3>
</slot>
<div class="tools">
<slot name="tools"></slot>
<el-tooltip effect="dark" :content="isFullscreen ? '退出全屏' : '全屏'">
<el-icon class="icon-fullscreen" @click="toggle"><FullScreen /></el-icon>
</el-tooltip>
</div>
</div>
<div class="chart-bd" v-loading="loading">
<slot>
<el-empty v-if="isEmpty" />
<v-chart class="chart" :option="{ ...options, color }" autoresize ref="chart" v-else />
</slot>
</div>
</div>
</template>
<style lang="scss">
.chart-card {
display: flex;
flex-direction: column;
flex: 1;
background-color: rgba(234, 234, 234, 0.6);
border-radius: 6px;
overflow: hidden;
&.isFullscreen {
.chart {
height: 100%;
}
}
}
.chart-hd {
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
padding: 10px 20px;
height: 30px;
color: rgba(0, 0, 0, 0.8);
h3 {
font-weight: bold;
font-size: 16px;
line-height: 30px;
}
.tools {
display: flex;
align-items: center;
}
.icon-fullscreen {
margin-left: 10px;
font-size: 16px;
cursor: pointer;
}
}
.chart-bd {
flex: 1;
background-color: #fff;
margin: 0 20px 20px;
border-radius: 4px;
.chart {
height: 290px;
}
}
</style>
......@@ -23,12 +23,8 @@ const remoteMethod = (q: string) => {
</script>
<template>
<el-select remote filterable value-key="id" :loading="loading" :remote-method="remoteMethod" style="width: 100%">
<el-option
v-for="item in userList"
:key="item.id"
:label="item.realname || item.nickname || item.username"
:value="item.id">
<el-select remote filterable value-key="id" :loading="loading" :remote-method="remoteMethod">
<el-option v-for="item in userList" :key="item.id" :label="item.realname || item.nickname || item.username" :value="item.id">
<span>{{ item.realname || item.nickname || item.username }}</span>
<template v-if="item.mobile">
<el-divider direction="vertical" />
......
......@@ -132,29 +132,14 @@ defineExpose({ refetch, tableRef })
</template>
<template v-else>
<!-- input -->
<el-input
v-model="params[item.prop]"
v-bind="item"
clearable
@change="search"
style="width: 200px"
v-if="item.type === 'input'"
/>
<el-input v-model="params[item.prop]" v-bind="item" clearable @change="search" v-if="item.type === 'input'" />
<!-- select -->
<el-select
v-model="params[item.prop]"
v-bind="item"
filterable
clearable
@change="search"
v-if="item.type === 'select'"
>
<el-select v-model="params[item.prop]" v-bind="item" filterable clearable @change="search" v-if="item.type === 'select'">
<el-option
v-for="(option, index) in item.options"
:label="option[item.labelKey] || option.label || option"
:value="option[item.valueKey] || option.value || option"
:key="index"
/>
:key="index" />
</el-select>
</template>
</el-form-item>
......@@ -172,13 +157,7 @@ defineExpose({ refetch, tableRef })
<!-- 主体 -->
<div class="table-list-bd">
<slot name="body" v-bind="{ data: dataList }">
<el-table
:header-cell-style="{ background: '#ededed' }"
:data="dataList"
v-loading="loading"
v-bind="$attrs"
ref="tableRef"
>
<el-table :header-cell-style="{ background: '#ededed' }" :data="dataList" v-loading="loading" v-bind="$attrs" ref="tableRef">
<el-table-column align="center" v-bind="item || {}" v-for="item in columns" :key="item.prop">
<template #default="scope" v-if="item.slots || item.computed">
<slot :name="item.slots" v-bind="scope" v-if="item.slots"></slot>
......@@ -204,8 +183,7 @@ defineExpose({ refetch, tableRef })
@size-change="pageSizeChange"
@current-change="fetchList()"
:hide-on-single-page="true"
v-if="hasPagination"
>
v-if="hasPagination">
</el-pagination>
</div>
</div>
......
......@@ -74,7 +74,7 @@ const typeName = computed(() => {
'7': '小程序',
'8': '卡券'
}
return json[props.type]
return json[props.type] || ''
})
</script>
......@@ -84,7 +84,7 @@ const typeName = computed(() => {
:title="props.data ? (props.data?.isView ? `查看${typeName}资料` : `编辑${typeName}资料`) : `新建${typeName}资料`"
:close-on-click-modal="false"
width="800px"
@update:modelValue="$emit('update:modelValue')">
@update:modelValue="value => $emit('update:modelValue', value)">
<el-form
:disabled="props.data?.isView"
ref="ruleFormRef"
......
......@@ -4,56 +4,35 @@ const props = defineProps<{ node: NodeProps }>()
const component = computed(() => {
const allComponent: any = {
TCRealTimeTrigger: markRaw(
defineAsyncComponent(() => import('./components/triggeringConditions/realTimeTrigger/Index.vue'))
),
TCRealTimeTrigger: markRaw(defineAsyncComponent(() => import('./components/triggeringConditions/realTimeTrigger/Index.vue'))),
TCJoinGroup: markRaw(defineAsyncComponent(() => import('./components/triggeringConditions/joinGroup/Index.vue'))),
TCChangeProps: markRaw(
defineAsyncComponent(() => import('./components/triggeringConditions/changeProps/Index.vue'))
),
TCOffiaccount: markRaw(
defineAsyncComponent(() => import('./components/triggeringConditions/offiaccount/Index.vue'))
),
TCChangeProps: markRaw(defineAsyncComponent(() => import('./components/triggeringConditions/changeProps/Index.vue'))),
TCOffiaccount: markRaw(defineAsyncComponent(() => import('./components/triggeringConditions/offiaccount/Index.vue'))),
TCDouyin: markRaw(defineAsyncComponent(() => import('./components/triggeringConditions/douyin/Index.vue'))),
TCXiaohongshu: markRaw(
defineAsyncComponent(() => import('./components/triggeringConditions/xiaohongshu/Index.vue'))
),
TCXiaohongshu: markRaw(defineAsyncComponent(() => import('./components/triggeringConditions/xiaohongshu/Index.vue'))),
TCWeibo: markRaw(defineAsyncComponent(() => import('./components/triggeringConditions/weibo/Index.vue'))),
TCCustom: markRaw(defineAsyncComponent(() => import('./components/triggeringConditions/custom/Index.vue'))),
TCXiaoetong: markRaw(defineAsyncComponent(() => import('./components/triggeringConditions/xiaoetong/Index.vue'))),
TCWenjuanxing: markRaw(
defineAsyncComponent(() => import('./components/triggeringConditions/wenjuanxing/Index.vue'))
),
TCWenjuanxing: markRaw(defineAsyncComponent(() => import('./components/triggeringConditions/wenjuanxing/Index.vue'))),
MAEndTrip: markRaw(defineAsyncComponent(() => import('./components/marketingAction/endTrip/Index.vue'))),
MAJoinGroup: markRaw(defineAsyncComponent(() => import('./components/marketingAction/joinGroup/Index.vue'))),
MALeaveGroup: markRaw(defineAsyncComponent(() => import('./components/marketingAction/leaveGroup/Index.vue'))),
MAChangeProps: markRaw(defineAsyncComponent(() => import('./components/marketingAction/changeProps/Index.vue'))),
MADelayProcess: markRaw(defineAsyncComponent(() => import('./components/marketingAction/delayProcess/Index.vue'))),
MAInternalNotice: markRaw(
defineAsyncComponent(() => import('./components/marketingAction/internalNotice/Index.vue'))
),
MAInternalNotice: markRaw(defineAsyncComponent(() => import('./components/marketingAction/internalNotice/Index.vue'))),
MAOffiaccount: markRaw(defineAsyncComponent(() => import('./components/marketingAction/offiaccount/Index.vue'))),
MAEmail: markRaw(defineAsyncComponent(() => import('./components/marketingAction/email/Index.vue'))),
MASMS: markRaw(defineAsyncComponent(() => import('./components/marketingAction/sms/Index.vue'))),
MADouyin: markRaw(defineAsyncComponent(() => import('./components/marketingAction/douyin/Index.vue'))),
MAWeibo: markRaw(defineAsyncComponent(() => import('./components/marketingAction/weibo/Index.vue'))),
MADingTalk: markRaw(defineAsyncComponent(() => import('./components/marketingAction/dingtalk/Index.vue'))),
CBAttributeJudgment: markRaw(
defineAsyncComponent(() => import('./components/conditionalBranch/attributeJudgment/Index.vue'))
),
CBGroupJudgment: markRaw(
defineAsyncComponent(() => import('./components/conditionalBranch/groupJudgment/Index.vue'))
),
CBEventJudgment: markRaw(
defineAsyncComponent(() => import('./components/conditionalBranch/eventJudgment/Index.vue'))
),
CBTimeJudgment: markRaw(
defineAsyncComponent(() => import('./components/conditionalBranch/timeJudgment/Index.vue'))
),
MAAB: markRaw(defineAsyncComponent(() => import('./components/marketingAction/ab/Index.vue'))),
CBAttributeJudgment: markRaw(defineAsyncComponent(() => import('./components/conditionalBranch/attributeJudgment/Index.vue'))),
CBGroupJudgment: markRaw(defineAsyncComponent(() => import('./components/conditionalBranch/groupJudgment/Index.vue'))),
CBEventJudgment: markRaw(defineAsyncComponent(() => import('./components/conditionalBranch/eventJudgment/Index.vue'))),
CBTimeJudgment: markRaw(defineAsyncComponent(() => import('./components/conditionalBranch/timeJudgment/Index.vue'))),
CBOffiaccount: markRaw(defineAsyncComponent(() => import('./components/conditionalBranch/offiaccount/Index.vue'))),
CBLabelJudgment: markRaw(
defineAsyncComponent(() => import('./components/conditionalBranch/labelJudgment/Index.vue'))
)
CBLabelJudgment: markRaw(defineAsyncComponent(() => import('./components/conditionalBranch/labelJudgment/Index.vue')))
}
return allComponent[props.node?.data.component_name || props.node?.data.componentName]
})
......
......@@ -51,7 +51,9 @@ onConnect(params => {
'handle-yes': { label: '是', style: { stroke: '#81b337' } },
'handle-no': { label: '否', style: { stroke: '#a16222' } },
'handle-success': { label: '成功', style: { stroke: '#81b337' } },
'handle-failure': { label: '失败', style: { stroke: '#a16222' } }
'handle-failure': { label: '失败', style: { stroke: '#a16222' } },
'handle-a': { label: 'A', style: { stroke: '#81b337' } },
'handle-b': { label: 'B', style: { stroke: '#a16222' } }
}
let customParams = {}
if (params.sourceHandle) {
......
......@@ -216,6 +216,14 @@ const list = ref([
component_type: 9,
component_name: 'MADingTalk',
connection_type: 2
},
{
name: 'A/B分配',
type: 2,
type_name: '营销动作',
icon: '101',
component_type: 12,
component_name: 'MAAB'
}
]
},
......@@ -280,9 +288,7 @@ const currentList = computed(() => {
return list.value.map(item => {
return {
...item,
children: item.children.filter(item =>
item.connection_type ? connections.value.find(connection => connection.type === item.connection_type) : true
)
children: item.children.filter(item => (item.connection_type ? connections.value.find(connection => connection.type === item.connection_type) : true))
}
})
})
......@@ -301,18 +307,9 @@ const onDragStart = (event: DragEvent, data: any) => {
<dt :style="`background: ${parent.background?.color}`">{{ parent.name }}</dt>
<dd>
<ul>
<li
v-for="(item, index) in parent.children"
:key="index"
:draggable="true"
@dragstart="event => onDragStart(event, item)">
<li v-for="(item, index) in parent.children" :key="index" :draggable="true" @dragstart="event => onDragStart(event, item)">
<div class="icon-box">
<Icon
class="circle"
:color="item.color || parent.background?.color"
:name="parent.background?.icon || ''"
w="60"
h="60"></Icon>
<Icon class="circle" :color="item.color || parent.background?.color" :name="parent.background?.icon || ''" w="60" h="60"></Icon>
<Icon class="icon" color="#fff" :name="item.icon" w="24" h="24"></Icon>
</div>
<p>{{ item.name }}</p>
......
......@@ -3,10 +3,10 @@ import type { NodeProps } from '@vue-flow/core'
import { useVueFlow, Handle, Position } from '@vue-flow/core'
import { Setting, Delete } from '@element-plus/icons-vue'
const props = withDefaults(
defineProps<{ node: NodeProps; connectionType?: number; canSetting?: boolean; canConnect?: boolean }>(),
{ canSetting: true, canConnect: true }
)
const props = withDefaults(defineProps<{ node: NodeProps; connectionType?: number; canSetting?: boolean; canConnect?: boolean }>(), {
canSetting: true,
canConnect: true
})
const emit = defineEmits<{ (e: 'setting'): void }>()
......@@ -42,6 +42,10 @@ function onRemove() {
<Handle id="handle-failure" class="handle-link is-no" :position="Position.Left">失败</Handle>
<Handle id="handle-any" class="handle-link is-any" :position="Position.Left">继续</Handle>
</template>
<template v-else-if="connectionType === 3">
<Handle id="handle-a" class="handle-link is-yes" :position="Position.Left">A</Handle>
<Handle id="handle-b" class="handle-link is-no" :position="Position.Left">B</Handle>
</template>
<template v-else>
<Handle id="handle-any" class="handle-default" :position="Position.Left">
<svg
......
......@@ -58,9 +58,9 @@ function handleSubmit() {
<el-table-column label="属性生成规则" align="center" width="400">
<template #default="{ row }">
<el-radio-group v-model="row.form.mode">
<el-radio :label="1">随机值</el-radio>
<el-radio :label="2">固定值</el-radio>
<el-radio :label="3">历史随机值</el-radio>
<el-radio :value="1">随机值</el-radio>
<el-radio :value="2">固定值</el-radio>
<el-radio :value="3">历史随机值</el-radio>
</el-radio-group>
<div class="rule-box">
<!-- 字符串 -->
......@@ -68,8 +68,8 @@ function handleSubmit() {
<!-- 随机值 -->
<template v-if="row.form.mode === 1">
<el-radio-group v-model="row.form.rule.type">
<el-radio :label="1">来自于系统数据</el-radio>
<el-radio :label="4">完全随机</el-radio>
<el-radio :value="1">来自于系统数据</el-radio>
<el-radio :value="4">完全随机</el-radio>
</el-radio-group>
</template>
<!-- 固定值 -->
......@@ -81,8 +81,10 @@ function handleSubmit() {
<template v-if="row.type === '2' || row.type === '3'">
<!-- 随机值 -->
<template v-if="row.form.mode === 1">
Min<el-input-number :controls="false" size="small" v-model="row.form.rule.num_min" />
Max<el-input-number :controls="false" size="small" v-model="row.form.rule.num_max" />
Min<el-input-number :controls="false" size="small" v-model="row.form.rule.num_min" /> Max<el-input-number
:controls="false"
size="small"
v-model="row.form.rule.num_max" />
</template>
<!-- 固定值 -->
<template v-if="row.form.mode === 2">
......@@ -94,31 +96,17 @@ function handleSubmit() {
<!-- 随机值 -->
<template v-if="row.form.mode === 1">
<el-radio-group v-model="row.form.rule.date_type">
<el-radio :label="1">完全随机(不早于当前时间)</el-radio>
<el-radio :label="2">
时间区间&nbsp;<el-date-picker
size="small"
value-format="YYYY-MM-DD"
v-model="row.form.rule.date_start"
style="width: 120px" />
<el-radio :value="1">完全随机(不早于当前时间)</el-radio>
<el-radio :value="2">
时间区间&nbsp;<el-date-picker size="small" value-format="YYYY-MM-DD" v-model="row.form.rule.date_start" style="width: 120px" />
&nbsp;&nbsp;
<el-date-picker
size="small"
value-format="YYYY-MM-DD"
v-model="row.form.rule.date_end"
style="width: 120px" />
<el-date-picker size="small" value-format="YYYY-MM-DD" v-model="row.form.rule.date_end" style="width: 120px" />
</el-radio>
<el-radio :label="3">
当前时间前&nbsp;<el-input-number
:controls="false"
size="small"
v-model="row.form.rule.date_current_before" />&nbsp;
<el-radio :value="3">
当前时间前&nbsp;<el-input-number :controls="false" size="small" v-model="row.form.rule.date_current_before" />&nbsp;
</el-radio>
<el-radio :label="4">
当前时间后&nbsp;<el-input-number
:controls="false"
size="small"
v-model="row.form.rule.date_current_after" />&nbsp;
<el-radio :value="4">
当前时间后&nbsp;<el-input-number :controls="false" size="small" v-model="row.form.rule.date_current_after" />&nbsp;
</el-radio>
</el-radio-group>
</template>
......
......@@ -57,8 +57,8 @@ function onSaveEventRule(rules: []) {
<template #step-body-after>
<el-row justify="center" style="margin-bottom: 20px">
<el-radio-group v-model="radio" size="large">
<el-radio-button label="1">判断事件数据</el-radio-button>
<el-radio-button label="2">生成事件数据</el-radio-button>
<el-radio-button value="1">判断事件数据</el-radio-button>
<el-radio-button value="2">生成事件数据</el-radio-button>
</el-radio-group>
</el-row>
</template>
......@@ -70,13 +70,13 @@ function onSaveEventRule(rules: []) {
</p>
<el-form-item label="生成规则">
<el-radio-group :model-value="1">
<el-radio :label="1">随机</el-radio>
<el-radio :value="1">随机</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="生成方式">
<el-radio-group v-model="form.data_rule.generate_way">
<el-radio :label="1">百分比</el-radio>
<el-radio :label="2">数值</el-radio>
<el-radio :value="1">百分比</el-radio>
<el-radio :value="2">数值</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="请输入分支条件数值"></el-form-item>
......@@ -98,20 +98,12 @@ function onSaveEventRule(rules: []) {
<el-row>
<el-col :span="12">
<el-form-item label="Min">
<el-input-number
v-model="form.data_rule.event_count_min"
:controls="false"
:max="3"
style="width: 80px" />
<el-input-number v-model="form.data_rule.event_count_min" :controls="false" :max="3" style="width: 80px" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="Max">
<el-input-number
v-model="form.data_rule.event_count_max"
:controls="false"
:max="5"
style="width: 80px" />
<el-input-number v-model="form.data_rule.event_count_max" :controls="false" :max="5" style="width: 80px" />
</el-form-item>
</el-col>
</el-row>
......@@ -127,13 +119,13 @@ function onSaveEventRule(rules: []) {
<el-form-item label="生成规则">
<el-radio-group :model-value="1">
<el-radio :label="1">随机</el-radio>
<el-radio :value="1">随机</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="生成方式">
<el-radio-group v-model="form.data_rule.generate_way">
<el-radio :label="1">百分比</el-radio>
<el-radio :label="2">数值</el-radio>
<el-radio :value="1">百分比</el-radio>
<el-radio :value="2">数值</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="请输入分支条件数值"></el-form-item>
......@@ -155,20 +147,12 @@ function onSaveEventRule(rules: []) {
<el-row>
<el-col :span="12">
<el-form-item label="Min">
<el-input-number
v-model="form.data_rule.event_count_min"
:controls="false"
:max="3"
style="width: 80px" />
<el-input-number v-model="form.data_rule.event_count_min" :controls="false" :max="3" style="width: 80px" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="Max">
<el-input-number
v-model="form.data_rule.event_count_max"
:controls="false"
:max="5"
style="width: 80px" />
<el-input-number v-model="form.data_rule.event_count_max" :controls="false" :max="5" style="width: 80px" />
</el-form-item>
</el-col>
</el-row>
......@@ -198,10 +182,5 @@ function onSaveEventRule(rules: []) {
</section>
</template>
</RuleTemplate>
<Generate
:node="node"
:rules="form.event_attr_rule"
v-model="generateVisible"
@save="onSaveEventRule"
v-if="generateVisible"></Generate>
<Generate :node="node" :rules="form.event_attr_rule" v-model="generateVisible" @save="onSaveEventRule" v-if="generateVisible"></Generate>
</template>
......@@ -27,8 +27,8 @@ watchEffect(() => {
<ConfigTemplate :model="form" :node="node">
<el-form-item>
<el-radio-group v-model="form.in_group">
<el-radio label="0">在群组中</el-radio>
<el-radio label="1">不在群组中</el-radio>
<el-radio value="0">在群组中</el-radio>
<el-radio value="1">不在群组中</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item>
......
......@@ -35,7 +35,7 @@ watchEffect(() => {
<el-form-item>
<template v-if="step === 0">
<el-radio-group v-model="form.operate" v-if="step === 0">
<el-radio v-for="item in operateList" :key="item.value" :label="item.value" style="width: 105px">
<el-radio v-for="item in operateList" :key="item.value" :value="item.value" style="width: 105px">
{{ item.label }}
</el-radio>
</el-radio-group>
......
......@@ -44,7 +44,7 @@ watchEffect(() => {
<ConfigTemplate :model="form" :node="node">
<el-form-item label="时间判断类型">
<el-radio-group v-model="form.date_type">
<el-radio v-for="item in dateTypeList" :key="item.value" :label="item.value">{{ item.label }}</el-radio>
<el-radio v-for="item in dateTypeList" :key="item.value" :value="item.value">{{ item.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="进入该步骤的时间">
......@@ -69,7 +69,7 @@ watchEffect(() => {
</template>
<template v-else>
<el-checkbox-group v-model="form.week">
<el-checkbox v-for="item in weekList" :key="item" :label="item" />
<el-checkbox v-for="item in weekList" :key="item" :value="item" />
</el-checkbox-group>
</template>
</el-form-item>
......
<script setup lang="ts">
import ConfigTemplate from '../../ConfigTemplate.vue'
const props = defineProps<{ node: any }>()
const role = inject('role') as string
const form = reactive({
a: 60,
b: 40
})
watchEffect(() => {
Object.assign(form, props.node.data[role])
})
watchEffect(() => {
form.b = 100 - (form.a || 0)
})
</script>
<template>
<ConfigTemplate :model="form" :node="node">
<el-form-item label="A路径">
<el-input-number v-model="form.a" :max="100" :min="0" :controls="false" style="width: 100%" />
</el-form-item>
<el-form-item label="B路径">
<el-input-number v-model="form.b" :controls="false" disabled style="width: 100%" />
</el-form-item>
</ConfigTemplate>
</template>
<script setup lang="ts">
import ConfigViewTemplate from '../../ConfigViewTemplate.vue'
const role = inject('role') as string
defineProps<{ node: any }>()
</script>
<template>
<ConfigViewTemplate :node="node">
<el-form-item :label="role === 'student' ? '我的答案' : '学生答案'"> A路径:{{ node.data.student?.a }} B路径:{{ node.data.student?.b }} </el-form-item>
<el-form-item label="正确答案"> A路径:{{ node.data.teacher?.a }} B路径:{{ node.data.teacher?.b }} </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="3" @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="101" 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'
import { useUserAttr } from '../../../composables/useAllData'
const props = defineProps<{ node: any }>()
const { getUserAttr } = useUserAttr()
const config = computed(() => {
return props.node.data.teacher || {}
})
function paramsParse(data: any) {
return data.rules
}
</script>
<template>
<RuleTemplate :node="node" step :paramsParse="paramsParse">
<template #header-answer>
<p v-for="(item, index) in config.rules" :key="index">
<span>{{ getUserAttr(item.attr_id)?.name }}</span>
<span class="is-operate">&nbsp;&nbsp;{{ item.operate }}&nbsp;&nbsp;</span>
<span class="is-answer">{{ item.value }}</span>
&nbsp;&nbsp;&nbsp;&nbsp;
</p>
</template>
</RuleTemplate>
</template>
......@@ -37,32 +37,19 @@ watchEffect(() => {
<ConfigTemplate :model="form" :node="node">
<el-form-item>
<el-radio-group v-model="form.time_type" style="display: block">
<el-radio label="0" size="large" style="display: block">
<el-radio value="0" size="large" style="display: block">
<span>延时 </span>
<el-input
:disabled="form.time_type !== '0'"
v-model="form.time_num"
placeholder="请输入"
class="input-with-select">
<el-input :disabled="form.time_type !== '0'" v-model="form.time_num" placeholder="请输入" class="input-with-select">
<template #append>
<el-select
v-model="form.time_unit"
placeholder="请选择"
style="width: 115px"
:disabled="form.time_type !== '0'">
<el-select v-model="form.time_unit" placeholder="请选择" style="width: 115px" :disabled="form.time_type !== '0'">
<el-option v-for="item in timeUnitList" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</template>
</el-input>
</el-radio>
<el-radio label="1" size="large" style="display: block">
<el-radio value="1" size="large" style="display: block">
<span>延时至 </span>
<el-date-picker
:disabled="form.time_type !== '1'"
v-model="form.time"
type="datetime"
placeholder="请选择"
value-format="YYYY-MM-DD HH:mm:ss" />
<el-date-picker :disabled="form.time_type !== '1'" v-model="form.time" type="datetime" placeholder="请选择" value-format="YYYY-MM-DD HH:mm:ss" />
</el-radio>
</el-radio-group>
</el-form-item>
......
......@@ -35,7 +35,7 @@ const { connectionList } = useConnection(2)
<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" :label="item.value">
<el-radio v-for="item in operateList" :key="item.value" :value="item.value">
{{ item.label }}
</el-radio>
</el-radio-group>
......@@ -51,7 +51,7 @@ const { connectionList } = useConnection(2)
<template v-else-if="step === 2">
<el-form-item>
<el-radio-group v-model="form.material_type" @change="form.material_id = ''">
<el-radio v-for="item in materialTypeList" :key="item.id" :label="item.value" style="width: 105px">
<el-radio v-for="item in materialTypeList" :key="item.id" :value="item.value" style="width: 105px">
发送{{ item.label }}
</el-radio>
</el-radio-group>
......
......@@ -39,7 +39,7 @@ const { connectionList } = useConnection(6)
<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" :label="item.value">
<el-radio v-for="item in operateList" :key="item.value" :value="item.value">
{{ item.label }}
</el-radio>
</el-radio-group>
......@@ -55,7 +55,7 @@ const { connectionList } = useConnection(6)
<template v-else-if="step === 2">
<el-form-item>
<el-radio-group v-model="form.material_type" @change="form.material_id = ''">
<el-radio v-for="item in materialTypeList" :key="item.id" :label="item.value" style="width: 105px">
<el-radio v-for="item in materialTypeList" :key="item.id" :value="item.value" style="width: 105px">
发送{{ item.label }}
</el-radio>
</el-radio-group>
......
......@@ -34,7 +34,7 @@ const { connectionList } = useConnection(9)
<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" :label="item.value">
<el-radio v-for="item in operateList" :key="item.value" :value="item.value">
{{ item.label }}
</el-radio>
</el-radio-group>
......@@ -50,7 +50,7 @@ const { connectionList } = useConnection(9)
<template v-else-if="step === 2">
<el-form-item>
<el-radio-group v-model="form.material_type" @change="form.material_id = ''">
<el-radio v-for="item in materialTypeList" :key="item.id" :label="item.value" style="width: 105px">
<el-radio v-for="item in materialTypeList" :key="item.id" :value="item.value" style="width: 105px">
发送{{ item.label }}
</el-radio>
</el-radio-group>
......
......@@ -32,7 +32,7 @@ const { connectionList } = useConnection(1)
<template v-if="step === 0">
<el-form-item>
<el-radio-group v-model="form.material_type" @change="form.material_id = ''">
<el-radio v-for="item in materialTypeList" :key="item.id" :label="item.value" style="width: 105px">
<el-radio v-for="item in materialTypeList" :key="item.id" :value="item.value" style="width: 105px">
发送{{ item.label }}
</el-radio>
</el-radio-group>
......
......@@ -26,7 +26,7 @@ const { connectionList } = useConnection(10)
<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" :label="item.value">
<el-radio v-for="item in operateList" :key="item.value" :value="item.value">
{{ item.label }}
</el-radio>
</el-radio-group>
......
......@@ -38,7 +38,7 @@ const { connectionList } = useConnection(7)
<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" :label="item.value">
<el-radio v-for="item in operateList" :key="item.value" :value="item.value">
{{ item.label }}
</el-radio>
</el-radio-group>
......@@ -54,7 +54,7 @@ const { connectionList } = useConnection(7)
<template v-else-if="step === 2">
<el-form-item>
<el-radio-group v-model="form.material_type" @change="form.material_id = ''">
<el-radio v-for="item in materialTypeList" :key="item.id" :label="item.value" style="width: 105px">
<el-radio v-for="item in materialTypeList" :key="item.id" :value="item.value" style="width: 105px">
发送{{ item.label }}
</el-radio>
</el-radio-group>
......
......@@ -31,7 +31,7 @@ const operateList = ref([
<el-form-item>
<template v-if="step === 0">
<el-radio-group v-model="form.operate" v-if="step === 0">
<el-radio v-for="item in operateList" :key="item.value" :label="item.value" style="width: 105px">
<el-radio v-for="item in operateList" :key="item.value" :value="item.value" style="width: 105px">
{{ item.label }}
</el-radio>
</el-radio-group>
......
......@@ -31,7 +31,7 @@ const operateList = ref([
<el-form-item>
<template v-if="step === 0">
<el-radio-group v-model="form.operate" v-if="step === 0">
<el-radio v-for="item in operateList" :key="item.value" :label="item.value" style="width: 140px">
<el-radio v-for="item in operateList" :key="item.value" :value="item.value" style="width: 140px">
{{ item.label }}
</el-radio>
</el-radio-group>
......
......@@ -37,7 +37,7 @@ const operateList = ref([
<el-form-item>
<template v-if="step === 0">
<el-radio-group v-model="form.operate" v-if="step === 0">
<el-radio v-for="item in operateList" :key="item.value" :label="item.value" style="width: 105px">
<el-radio v-for="item in operateList" :key="item.value" :value="item.value" style="width: 105px">
{{ item.label }}
</el-radio>
</el-radio-group>
......
......@@ -19,9 +19,9 @@ watchEffect(() => {
<ConfigTemplate :model="form" :node="node">
<el-form-item label="触发类型" prop="trigger_type">
<el-radio-group v-model="form.trigger_type">
<el-radio label="0">单次触发</el-radio>
<el-radio label="1">重复触发</el-radio>
<el-radio label="2">立即触发</el-radio>
<el-radio value="0">单次触发</el-radio>
<el-radio value="1">重复触发</el-radio>
<el-radio value="2">立即触发</el-radio>
</el-radio-group>
</el-form-item>
<template v-if="form.trigger_type === '0'">
......
......@@ -29,7 +29,7 @@ const operateList = ref([
<el-form-item>
<template v-if="step === 0">
<el-radio-group v-model="form.operate" v-if="step === 0">
<el-radio v-for="item in operateList" :key="item.value" :label="item.value" style="width: 130px">
<el-radio v-for="item in operateList" :key="item.value" :value="item.value" style="width: 130px">
{{ item.label }}
</el-radio>
</el-radio-group>
......
......@@ -26,7 +26,7 @@ const operateList = ref([{ label: '提交表单', value: '0' }])
<el-form-item>
<template v-if="step === 0">
<el-radio-group v-model="form.operate" v-if="step === 0">
<el-radio v-for="item in operateList" :key="item.value" :label="item.value" style="width: 130px">
<el-radio v-for="item in operateList" :key="item.value" :value="item.value" style="width: 130px">
{{ item.label }}
</el-radio>
</el-radio-group>
......
......@@ -26,7 +26,7 @@ const operateList = ref([{ label: '新用户注册', value: '0' }])
<el-form-item>
<template v-if="step === 0">
<el-radio-group v-model="form.operate" v-if="step === 0">
<el-radio v-for="item in operateList" :key="item.value" :label="item.value" style="width: 130px">
<el-radio v-for="item in operateList" :key="item.value" :value="item.value" style="width: 130px">
{{ item.label }}
</el-radio>
</el-radio-group>
......
......@@ -33,7 +33,7 @@ const operateList = ref([
<el-form-item>
<template v-if="step === 0">
<el-radio-group v-model="form.operate" v-if="step === 0">
<el-radio v-for="item in operateList" :key="item.value" :label="item.value" style="width: 130px">
<el-radio v-for="item in operateList" :key="item.value" :value="item.value" style="width: 130px">
{{ item.label }}
</el-radio>
</el-radio-group>
......
<template>
<svg
version="1.1"
id="图层_1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px"
y="0px"
viewBox="0 0 200 200"
xml:space="preserve">
<path
class="st0"
d="M125.4,118.8c13.1,0,21.9,10.1,21.9,24.1l0,19.7H52.7l0-19.7c0-14,8.8-24.1,21.9-24.1H125.4z M15.4,125h33.4
c-4.4,5.6-7.1,12.9-7.7,21.1l-0.1,3.1l0,7H0l0-14c0-9.3,5.4-16.2,13.6-17.1L15.4,125h33.4H15.4z M151.3,125h33.4
c8.6,0,14.5,6.3,15.3,15.2l0.1,2l0,14H159l0-7C159.1,139.8,156.2,131.3,151.3,125l33.4,0L151.3,125z M40.9,68.7
c12.9,0,23.4,11.2,23.4,25c0,13.8-10.5,25-23.4,25c-12.9,0-23.4-11.2-23.4-25C17.5,79.9,28,68.7,40.9,68.7z M159.1,68.7
c12.9,0,23.4,11.2,23.4,25c0,13.8-10.5,25-23.4,25c-12.9,0-23.4-11.2-23.4-25C135.7,79.9,146.1,68.7,159.1,68.7z M100,37.5
c17.8,0,32.2,15.4,32.2,34.4s-14.4,34.4-32.2,34.4c-17.8,0-32.2-15.4-32.2-34.4C67.9,52.9,82.3,37.5,100,37.5L100,37.5z" />
</svg>
</template>
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18" width="18" height="18">
<defs data-reactroot=""></defs>
<g>
<path
d="M511.8976 1024l-443.648-256V256l443.648-256 443.648 256v512z m-300.544-338.4832l300.544 173.1584 300.544-173.1584V338.4832l-300.544-173.1584-300.544 173.1584z"
fill="#B3B5C2"
p-id="12512"></path>
<path d="M385.536 511.7952a126.5664 126.5664 0 1 0 126.5664-126.5664 126.5664 126.5664 0 0 0-126.5664 126.5664z" fill="#B3B5C2" p-id="12513"></path>
</g>
</svg>
</template>
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
class="styles__StyledSVGIconPathComponent-sc-4n1c4t-0 fJSruJ svg-icon-path-icon fill"
viewBox="0 0 1024 1024"
width="31"
height="31">
<defs data-reactroot=""></defs>
<g>
<path
d="M719.403 575.659a660.907 660.907 0 0 1 77.909 13.973 181.333 181.333 0 0 1 137.216 159.403l3.883 42.09a56.96 56.96 0 0 1-56.726 62.208l-30.656 0.022c5.718-9.878 8.534-21.547 7.403-33.771l-5.803-62.955a217.621 217.621 0 0 0-133.226-180.97zM448 568.896c77.141 0 143.787 6.912 199.979 20.736a181.333 181.333 0 0 1 137.216 159.403l3.882 42.09a56.96 56.96 0 0 1-56.725 62.208H163.648a56.96 56.96 0 0 1-56.747-62.208l3.904-42.09a181.333 181.333 0 0 1 137.216-159.403C304.213 575.808 370.86 568.896 448 568.896z m149.333-398.23c101.099 0 183.04 80.64 183.04 180.14 0 99.498-81.941 180.16-183.04 180.16a187.072 187.072 0 0 1-32-2.753c70.72-28.842 120.427-97.408 120.427-177.408 0-79.978-49.707-148.544-120.363-177.408a186.816 186.816 0 0 1 31.936-2.73z m-149.333 0c101.099 0 183.04 80.64 183.04 180.14 0 99.498-81.941 180.16-183.04 180.16s-183.04-80.64-183.04-180.16c0-99.478 81.92-180.14 183.04-180.14z"
p-id="8853"></path>
</g>
</svg>
</template>
......@@ -14,7 +14,7 @@ const currentMenu = computed(() => {
})
const currentSubmenu = computed(() => {
return currentMenu.value?.children ? findMenu(route.path, currentMenu.value.children) : null
return currentMenu.value?.children ? findMenu(route.fullPath, currentMenu.value.children) : null
})
function findMenu(path: string, menus: IMenuItem[]) {
......@@ -22,13 +22,13 @@ function findMenu(path: string, menus: IMenuItem[]) {
if (
item.children &&
item.children.find(item => {
const regExp = new RegExp(`^${item.path}`)
const regExp = new RegExp(`^${item.path.replaceAll('?', '\\?')}`)
return regExp.test(path)
})
) {
return item
}
const regExp = new RegExp(`^${item.path}`)
const regExp = new RegExp(`^${item.path.replaceAll('?', '\\?')}`)
return regExp.test(path)
})
}
......@@ -38,11 +38,7 @@ function findMenu(path: string, menus: IMenuItem[]) {
<aside class="app-aside">
<nav class="menu">
<ul>
<li
v-for="item in menus"
:key="item.path"
:class="{ 'is-active': item.path === currentMenu?.path }"
v-permission="item.tag">
<li v-for="item in menus" :key="item.path" :class="{ 'is-active': item.path === currentMenu?.path }" v-permission="item.tag">
<div class="menu-item">
<template v-if="item.children">
<RouterLink :to="item.path">
......@@ -101,6 +97,7 @@ function findMenu(path: string, menus: IMenuItem[]) {
align-items: center;
justify-content: center;
svg {
width: 24px;
fill: var(--main-color);
}
}
......@@ -144,6 +141,7 @@ function findMenu(path: string, menus: IMenuItem[]) {
fill: #9a9a9a;
}
.submenu-icon {
width: 18px;
margin-right: 10px;
}
&.is-active {
......
......@@ -4,7 +4,7 @@ export default { name: 'AppMain' }
<template>
<section class="app-main">
<router-view></router-view>
<router-view :key="$route.fullPath"></router-view>
</section>
</template>
......
<script setup lang="ts">
import { Warning } from '@element-plus/icons-vue'
import EventRule from './EventRule.vue'
import { useMetaEvent } from '@/composables/useAllData'
const { metaEventList } = useMetaEvent()
const eventAttrRule = defineModel<any>({
default: () => ({
event_attr_rule: {
current_logic_operate: 'and',
happen_info: { is_happened: true, event_id: '-1', event_name: '所有事件', attr_list: [] },
trigger_info: { operate: '', operate_name: '', value: '' }
},
tag_rule: { event_id: '', event_name: '', attr_id: '', attr_name: '', type: 1, value: undefined }
})
})
const form: any = reactive({
event_attr_rule: {
current_logic_operate: 'and',
items: [
{
happen_info: { is_happened: true, event_id: '-1', event_name: '所有事件', attr_list: [] },
trigger_info: { operate: '', operate_name: '', value: undefined }
}
]
},
tag_rule: { event_id: '', event_name: '', attr_id: '', attr_name: '', type: 1, value: '' }
})
const { current_logic_operate, ...rest } = eventAttrRule.value.event_attr_rule
Object.assign(form, { tag_rule: eventAttrRule.value.tag_rule, event_attr_rule: { current_logic_operate, items: [rest] } })
const first = computed(() => form.event_attr_rule.items[0])
watch(
form,
() => {
eventAttrRule.value = {
event_attr_rule: { current_logic_operate: form.event_attr_rule.current_logic_operate, ...first.value },
tag_rule: form.tag_rule
}
},
{ deep: true }
)
watch(
() => first.value.happen_info.event_id,
() => {
form.tag_rule.event_id = first.value.happen_info.event_id
form.tag_rule.event_name = first.value.happen_info.event_name
form.tag_rule.attr_id = ''
form.tag_rule.attr_name = ''
}
)
// 获取事件属性列表
function getEventAttrList(eventId: string) {
return metaEventList.value.find(item => item.id === eventId)?.event_attrs || []
}
function handleAttrChange(value: string) {
const found = getEventAttrList(form.tag_rule.event_id).find(item => item.id === value)
form.tag_rule.attr_name = found?.name
}
</script>
<template>
<p class="rule-tips">
<el-icon><Warning /></el-icon>
事件偏好标签:将事件按照某个规则分组排序,使用排名前几个的事件属性作为用户标签值。
</p>
<!-- 事件属性规则 -->
<EventRule :limit="1" v-model="form.event_attr_rule" style="margin-top: 20px">
<template #footer>
<el-row align="middle" style="margin-top: 10px" v-if="first.happen_info.event_id">
<p></p>
<el-button type="info" style="margin: 0 10px">{{ form.tag_rule.event_name }}</el-button>
<el-select v-model="form.tag_rule.attr_id" style="width: 110px" @change="handleAttrChange">
<el-option v-for="option in getEventAttrList(form.tag_rule.event_id)" :key="option.id" :label="option.name" :value="option.id"></el-option>
</el-select>
<el-button type="info" style="margin: 0 10px">出现次数最多</el-button>
<p></p>
<el-input-number v-model="form.tag_rule.value" style="margin: 0 10px; width: 60px" :controls="false"></el-input-number>
<p></p>
<el-button type="info" style="margin: 0 10px">{{ form.tag_rule.attr_name }}</el-button>
<p>作为用户标签</p>
</el-row>
</template>
</EventRule>
</template>
<style src="@/assets/styles/rule.scss"></style>
<script setup lang="ts">
import { Warning } from '@element-plus/icons-vue'
import EventRule from './EventRule.vue'
import { wayList } from '@/utils/dictionary'
const eventAttrRule = defineModel<any>({
default: () => ({
event_attr_rule: {
current_logic_operate: 'and',
happen_info: { is_happened: true, event_id: '-1', event_name: '所有事件', attr_list: [] },
trigger_info: { operate: '', operate_name: '', value: '' }
},
tag_rule: { way: '' }
})
})
const form: any = reactive({
event_attr_rule: {
current_logic_operate: 'and',
items: [
{
happen_info: { is_happened: true, event_id: '-1', event_name: '所有事件', attr_list: [] },
trigger_info: { operate: '', operate_name: '', value: '' }
}
]
},
tag_rule: { way: '' }
})
const { current_logic_operate, ...rest } = eventAttrRule.value.event_attr_rule
Object.assign(form, { tag_rule: eventAttrRule.value.tag_rule, event_attr_rule: { current_logic_operate, items: [rest] } })
const first = computed(() => form.event_attr_rule.items[0])
watch(
form,
() => {
eventAttrRule.value = {
event_attr_rule: { current_logic_operate: form.event_attr_rule.current_logic_operate, ...first.value },
tag_rule: form.tag_rule
}
},
{ deep: true }
)
</script>
<template>
<p class="rule-tips">
<el-icon><Warning /></el-icon>
事件指标标签:将事件指标作为用户的标签。
</p>
<!-- 事件属性规则 -->
<EventRule :limit="1" v-model="form.event_attr_rule" style="margin-top: 20px">
<template #footer>
<el-row align="middle" style="margin-top: 10px" v-if="first.happen_info.event_id">
<p></p>
<el-button type="info" style="margin: 0 10px">{{ first.happen_info.event_name }}</el-button>
<el-select v-model="form.tag_rule.way" style="margin-right: 10px; width: 130px">
<el-option v-for="item in wayList" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<p>作为用户标签</p>
</el-row>
</template>
</EventRule>
</template>
<style src="@/assets/styles/rule.scss"></style>
......@@ -3,7 +3,8 @@ import type { TagRule } from '@/types'
import { PriceTag, Plus, CloseBold } from '@element-plus/icons-vue'
import { useTag } from '@/composables/useAllData'
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 { tagList } = useTag()
......@@ -44,11 +45,7 @@ function handleRemove(items: string[], index: number) {
标签 等于
<el-form-item>
<el-select v-model="tagRule.items[index]">
<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-form-item>
</div>
......
<script setup lang="ts">
import { Warning } from '@element-plus/icons-vue'
import { ElInput } from 'element-plus'
import UserRule from '@/components/rule/UserRule.vue'
import EventRule from '@/components/rule/EventRule.vue'
import UserActionRule from '@/components/rule/UserActionRule.vue'
const rules = defineModel<Array<any>>({ default: [] })
const inputValue = ref('')
const inputVisible = ref(false)
const InputRef = ref<InstanceType<typeof ElInput>>()
const activeIndex = ref(0)
const handleClose = (index: number) => {
rules.value.splice(index, 1)
activeIndex.value = 0
}
const showInput = () => {
inputVisible.value = true
nextTick(() => {
InputRef.value!.input!.focus()
})
}
const handleInputConfirm = () => {
if (inputValue.value) {
rules.value.push({
level: rules.value.length,
level_name: inputValue.value,
user_attr_rule: { current_logic_operate: 'and', items: [] },
event_attr_rule: { current_logic_operate: 'and', items: [] },
user_action_rule: { current_logic_operate: 'and', items: [] }
})
activeIndex.value = rules.value.length - 1
}
inputVisible.value = false
inputValue.value = ''
}
</script>
<template>
<p class="rule-tips">
<el-icon><Warning /></el-icon>
自定义分层标签:将满足不同分层规则的用户打上分层标签,同一用户会被优先匹配在顺序靠前的分层
</p>
<div>
<el-tag
class="rule-tag"
v-for="(rule, index) in rules"
size="large"
effect="light"
:type="index !== activeIndex ? 'info' : 'primary'"
:key="rule.level"
closable
:disable-transitions="false"
@close="handleClose(index)"
@click="activeIndex = index">
{{ rule.level_name }}
</el-tag>
<el-input v-model="inputValue" class="rule-input" ref="InputRef" @keyup.enter="handleInputConfirm" @blur="handleInputConfirm" v-if="inputVisible" />
<el-button v-else style="width: 100px" @click="showInput"> + 添加分层 </el-button>
</div>
<template v-if="rules.length">
<!-- 用户属性规则 -->
<UserRule v-model="rules[activeIndex].user_attr_rule" style="margin-top: 20px"></UserRule>
<!-- 事件属性规则 -->
<EventRule v-model="rules[activeIndex].event_attr_rule" style="margin-top: 20px"></EventRule>
<!-- 用户行为序列规则 -->
<UserActionRule v-model="rules[activeIndex].user_action_rule" style="margin-top: 20px"></UserActionRule>
</template>
</template>
<style src="@/assets/styles/rule.scss"></style>
<script setup lang="ts">
import RFMRuleItem from './RFMRuleItem.vue'
const form = defineModel<any>({
default: {
R: {},
F: {},
M: {}
}
})
</script>
<template>
<RFMRuleItem label="R" v-model="form.R" />
<RFMRuleItem label="F" v-model="form.F" style="margin-top: 20px" />
<RFMRuleItem label="M" v-model="form.M" style="margin-top: 20px" />
</template>
<script setup lang="ts">
import { useUserAttr, useMetaEvent } from '@/composables/useAllData'
import { searchMetaMemberAttrs } from '@/api/base'
defineProps<{ label: string }>()
const form = defineModel<any>()
const ruleList = [
{ label: '属性值平均法', value: '101', basis: ['1'] },
{ label: '属性值分类法', value: '102', basis: ['1'] },
{ label: '事件发生次数平均法', value: '201', basis: ['2'] }
]
const defaultLevel = [
{ level: '高', value: '' },
{ level: '低', value: '' }
]
const defaultScore = [
{ score: 1, min_value: '', max_value: '' },
{ score: 2, min_value: '', max_value: '' },
{ score: 3, min_value: '', max_value: '' },
{ score: 4, min_value: '', max_value: '' },
{ score: 5, min_value: '', max_value: '' }
]
onMounted(() => {
form.value = Object.assign({ basis: '1', rule: '101', event_id: '-1', attr_id: '', attr_type: '', config: [...defaultScore] }, form.value)
})
const { userAttrList } = useUserAttr()
const { metaEventList } = useMetaEvent()
const currentRuleList = computed(() => {
return ruleList.filter(item => item.basis.includes(form.value.basis))
})
const currentAttrList = computed(() => {
let list = userAttrList.value
return list.filter(item => {
if (form.value.rule === '101') {
return ['2', '3', '4', '5'].includes(item.type)
} else if (form.value.rule === '102') {
return ['1'].includes(item.type)
}
return true
})
})
const currentMetaEventList = computed(() => {
return [{ id: '-1', name: '所有事件' }, ...metaEventList.value]
})
function handleBasisChange(value: any) {
if (value === '1') {
form.value.rule = '101'
} else {
form.value.rule = '201'
}
form.value.attr_id = ''
handleRuleChange(form.value.rule)
}
function handleRuleChange(value: any) {
if (value === '102') {
form.value.config = [...defaultLevel]
} else {
form.value.config = [...defaultScore]
}
form.value.attr_id = ''
}
function handleAttrChange(value: any) {
form.value.attr_type = currentAttrList.value.find(item => item.id === value)?.type
}
const options = ref<{ label: string; value: string }[]>([])
const loading = ref(false)
async function remoteMethod(search: string = '') {
options.value = []
if (form.value.attr_id) {
loading.value = true
await searchMetaMemberAttrs({ search, member_meta_id: form.value.attr_id, per_page: 100 }).then(res => {
options.value = res.data.list.map((item: any) => {
return { label: item.attr_value, value: item.attr_value }
})
})
loading.value = false
}
}
watch(
() => form.value.attr_id,
() => {
form.value.rule === '102' && remoteMethod()
},
{ immediate: true }
)
</script>
<template>
<el-card shadow="never">
<template #header>
<el-button circle type="primary" style="width: 32px; margin-right: 10px">{{ label }}</el-button>
{{ label }}值计算规则
</template>
<div class="rfm-header">
<p style="margin-right: 20px">{{ label }}值计算依据</p>
<el-radio-group v-model="form.basis" @change="handleBasisChange">
<el-radio value="1">用户属性</el-radio>
<el-radio value="2">事件属性</el-radio>
</el-radio-group>
<p style="margin-left: 10px">计算规则:</p>
<el-select v-model="form.rule" style="width: 170px" @change="handleRuleChange">
<el-option v-for="item in currentRuleList" :key="item.value" :label="item.label" :value="item.value"></el-option>
</el-select>
<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.id" :label="item.name" :value="item.id"></el-option>
</el-select>
<el-select v-model="form.attr_id" placeholder="选择属性" style="width: 160px" @change="handleAttrChange" v-else>
<el-option v-for="item in currentAttrList" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</div>
<div class="rfm-body">
<template v-if="form.rule === '102'">
<div class="rfm-box" v-for="item in form.config" :key="item.level">
<div class="rfm-box-header">
<b>{{ item.level }}</b>
</div>
<div class="rfm-box-body">
<el-select placeholder="选择属性值" v-model="item.value" filterable allow-create :loading="loading">
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
</div>
</template>
<template v-else>
<div class="rfm-box" v-for="item in form.config" :key="item.score">
<div class="rfm-box-header">
<b>{{ item.score }}</b
>
</div>
<div class="rfm-box-body">
<el-input v-model="item.min_value" class="rfm-box-input"></el-input>
~
<el-input v-model="item.max_value" class="rfm-box-input"></el-input>
</div>
</div>
</template>
</div>
</el-card>
</template>
<style lang="scss">
.rfm-header {
display: flex;
align-items: center;
.el-radio {
margin-right: 10px;
}
.el-select {
margin-right: 10px;
}
}
.rfm-body {
margin-top: 20px;
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 20px;
}
.rfm-box {
padding: 10px;
text-align: center;
border-radius: 4px;
background-color: rgb(240, 240, 240);
}
.rfm-box-header {
color: var(--main-color);
margin-bottom: 10px;
b {
font-weight: bold;
font-size: 18px;
}
}
.rfm-box-input {
width: 63px;
.el-input__inner {
text-align: center;
}
}
</style>
差异被折叠。
......@@ -5,7 +5,8 @@ import { useUserAttr } from '@/composables/useAllData'
import { stringOperatorList, numberOperatorList, dateOperatorList } from '@/utils/dictionary'
import { searchMetaMemberAttrs } from '@/api/base'
const userAttrRule = ref(inject('userAttrRule') as UserAttrRule)
// const userAttrRule = ref(inject('userAttrRule') as UserAttrRule)
const userAttrRule = defineModel<UserAttrRule>({ default: { current_logic_operate: 'and', items: [] } })
const { userAttrList } = useUserAttr()
......@@ -107,17 +108,16 @@ function remoteMethod(item: RuleAttr, search: string = '') {
</el-form-item>
<el-form-item>
<el-select v-model="item.operate" @change="value => handleOperateChange(value, item)">
<el-option v-for="option in getOperatorList(item.attr_type)" :key="option.value" :label="option.alias || option.label" :value="option.value"></el-option>
<el-option
v-for="option in getOperatorList(item.attr_type)"
:key="option.value"
:label="option.alias || option.label"
:value="option.value"></el-option>
</el-select>
</el-form-item>
<el-form-item v-if="!['null', 'not null'].includes(item.operate)">
<!-- 数字区间 -->
<template v-if="['2', '3'].includes(item.attr_type) && item.operate === 'range'">
<el-input-number step-strictly :controls="false" :min="0" v-model="item.value.start" />
<el-input-number step-strictly :controls="false" :min="0" v-model="item.value.end" />
</template>
<!-- 日期区间 -->
<template v-else-if="item.attr_type === '4' && item.operate === 'range'">
<template v-if="item.attr_type === '4' && item.operate === 'range'">
<el-date-picker v-model="item.value.start" type="date" value-format="YYYY-MM-DD" />
<el-date-picker v-model="item.value.end" type="date" value-format="YYYY-MM-DD" />
</template>
......@@ -145,7 +145,12 @@ function remoteMethod(item: RuleAttr, search: string = '') {
v-if="['in', 'not in'].includes(item.operate)">
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<el-autocomplete v-model="item.value" value-key="attr_value" :fetch-suggestions="(query, cb) => querySearch(item, query, cb)" style="width: 320px" v-else />
<el-autocomplete
v-model="item.value"
value-key="attr_value"
:fetch-suggestions="(query, cb) => querySearch(item, query, cb)"
style="width: 320px"
v-else />
</template>
</el-form-item>
</div>
......
import { getMetaUserAttrList, getMetaEventList, getTagList, getConnectionList } from '@/api/base'
import { getMetaUserAttrList, getMetaEventList, getTagList, getConnectionList, getUserList } from '@/api/base'
import { useMapStore } from '@/stores/map'
// 用户属性类型
export interface AttrType {
......@@ -36,60 +37,107 @@ export interface ConnectionType {
// 所有用户属性
const userAttrList = ref<AttrType[]>([])
const userAttrLoading = ref(false)
export function useUserAttr() {
function fetchUserAttrList() {
getMetaUserAttrList({ check_role: true }).then((res: any) => {
async function fetchUserAttrList() {
if (userAttrLoading.value) return
userAttrLoading.value = true
await getMetaUserAttrList({ check_role: true }).then((res: any) => {
userAttrList.value = res.data.items
})
userAttrLoading.value = false
}
onMounted(() => {
if (!userAttrList.value?.length) fetchUserAttrList()
})
return { fetchUserAttrList, userAttrList }
return { fetchUserAttrList, userAttrList, userAttrLoading }
}
// 所有事件
const metaEventList = ref<MetaEventType[]>([])
const metaEventLoading = ref(false)
export function useMetaEvent() {
function fetchMetaEventList() {
getMetaEventList({ check_role: true }).then((res: any) => {
async function fetchMetaEventList() {
if (metaEventLoading.value) return
metaEventLoading.value = true
await getMetaEventList({ check_role: true }).then((res: any) => {
metaEventList.value = res.data.items
})
metaEventLoading.value = false
}
onMounted(() => {
if (!metaEventList.value?.length) fetchMetaEventList()
})
return { fetchMetaEventList, metaEventList }
return { fetchMetaEventList, metaEventList, metaEventLoading }
}
// 所有标签
const tagList = ref<TagType[]>([])
const tagLoading = ref(false)
export function useTag() {
function fetchTagList() {
getTagList({ check_role: 1 }).then((res: any) => {
async function fetchTagList() {
if (tagLoading.value) return
tagLoading.value = true
await getTagList({ check_role: 1 }).then((res: any) => {
tagList.value = res.data.items
})
tagLoading.value = false
}
onMounted(() => {
if (!tagList.value?.length) fetchTagList()
})
return { fetchTagList, tagList }
return { fetchTagList, tagList, tagLoading }
}
// 所有连接
const connectionList = ref<ConnectionType[]>([])
const connectionLoading = ref(false)
export function useConnection() {
function fetchConnectionList() {
getConnectionList().then((res: any) => {
async function fetchConnectionList() {
if (connectionLoading.value) return
connectionLoading.value = true
const connectionType = useMapStore().getMapValuesByKey('experiment_connection_type')
await getConnectionList().then((res: any) => {
connectionList.value = res.data.items.map((item: any) => {
const connection = connectionType.find(type => type.value == item.type)
const attrs = typeof item.config_attributes === 'string' ? JSON.parse(item.config_attributes) : item.config_attributes
const name = Array.isArray(attrs) ? attrs.find((item: any) => item.prop === 'name')?.value : attrs.name
return { ...item, config_attributes: attrs, name }
return { ...item, config_attributes: attrs, name, type_name: connection?.label || item.type }
})
})
connectionLoading.value = false
}
onMounted(() => {
if (!connectionList.value?.length) fetchConnectionList()
})
return { fetchConnectionList, connectionList }
return { fetchConnectionList, connectionList, connectionLoading }
}
// 所有成员
export interface UserType {
sso_id: string
name: string
pinyin: number
}
const userList = ref<UserType[]>([])
export function useUser() {
const [me] = userList.value
const userValue = ref(me?.sso_id)
async function fetchUserList() {
const res = await getUserList()
let { me, students = [], teachers = [] } = res.data.items
me = { ...me, role: 'me' }
students = students.map((item: any) => {
return { ...item, role: 'student' }
})
teachers = teachers.map((item: any) => {
return { ...item, role: 'teacher' }
})
userValue.value = me.sso_id
userList.value = [me, ...teachers, ...students]
}
onMounted(() => {
if (!userList.value?.length) fetchUserList()
})
return { fetchUserList, userList, userValue }
}
import httpRequest from '@/utils/axios'
// 获取实验详情
export function getExperiment() {
return httpRequest.get('/api/lab/v1/experiment/once/experiment')
}
// 获取实验下的所有事件
export function getEventList() {
return httpRequest.get('/api/lab/v1/experiment/analyse/events')
}
// 事件行为统计
export function getEventActionList(data: { soo_id: string; event_ids: Array<string>; start_date: string; end_date: string }) {
return httpRequest.post('/api/lab/v1/experiment/analyse/event-action-statistics', data)
}
// 事件行为数量走势统计
export function getEventActionTrendList(data: { soo_id: string; event_ids: Array<string>; start_date: string; end_date: string }) {
return httpRequest.post('/api/lab/v1/experiment/analyse/event-action-date-statistics', data)
}
// 事件用户人数统计
export function getEventMemberList(data: { soo_id: string; event_ids: Array<string>; start_date: string; end_date: string }) {
return httpRequest.post('/api/lab/v1/experiment/analyse/event-member-statistics', data)
}
// 事件用户人数统计
export function getEventTimeList(data: { soo_id: string; event_ids: Array<string>; start_date: string; end_date: string }) {
return httpRequest.post('/api/lab/v1/experiment/analyse/event-time-statistics', data)
}
import { getEventList } from '../api'
export interface EventType {
id: string
name: string
english_name: string
}
const eventList = ref<EventType[]>([])
export function useEvent() {
const eventValues = ref([])
async function fetchEventList() {
const res = await getEventList()
eventList.value = res.data.items
}
onMounted(() => {
// if (!eventList.value?.length) fetchEventList()
fetchEventList()
})
return { fetchEventList, eventList, eventValues }
}
import type { RouteRecordRaw } from 'vue-router'
import Layout from '@/components/layout/Index.vue'
const routes: RouteRecordRaw[] = [
{
path: '/analyze/event',
component: Layout,
children: [{ path: '', component: () => import('./views/Index.vue') }]
}
]
export { routes }
<script setup>
import ChartCard from '@/components/ChartCard.vue'
import { DataLine } from '@element-plus/icons-vue'
import { useUser } from '@/composables/useAllData'
import { useEvent } from '../composables/useEvent'
import * as api from '../api'
const { userValue, userList } = useUser()
const { eventValues, eventList } = useEvent()
const date = ref('')
const info = ref()
async function fetchInfo() {
const res = await api.getExperiment()
info.value = res.data.detail
}
onMounted(fetchInfo)
async function handleStart() {
fetchEventAction()
fetchEventActionTrend()
fetchEventMember()
fetchEventTime()
}
const loading = computed(() => {
return loading1.value || loading2.value || loading3.value || loading4.value
})
const params = computed(() => {
const [startDate, endDate] = date.value || []
return { sso_id: userValue.value, event_ids: eventValues.value, start_date: startDate, end_date: endDate }
})
// 事件行为分布
const loading1 = ref(false)
const eventAction = ref([])
async function fetchEventAction() {
if (!userValue.value) return
loading1.value = true
const res = await api.getEventActionList(params.value)
eventAction.value = res.data.items
loading1.value = false
}
const eventActionOption = computed(() => {
if (!eventAction.value.length) return
const value = eventAction.value.map(item => item.total)
const max = Math.max(...value)
return {
grid: { left: '5%', top: '5%', right: '5%', bottom: '5%', containLabel: true },
tooltip: { trigger: 'axis' },
radar: { indicator: eventAction.value.map(item => ({ name: item.group_name, max })) },
series: [
{
name: '事件',
type: 'radar',
tooltip: { trigger: 'item' },
label: { show: true, position: 'top' },
data: [{ value }]
}
]
}
})
// 事件行为数量走势
const loading2 = ref(false)
const eventActionTrend = ref([])
async function fetchEventActionTrend() {
if (!userValue.value) return
loading2.value = true
const res = await api.getEventActionTrendList(params.value)
eventActionTrend.value = res.data.items
loading2.value = false
}
const eventActionTrendOption = computed(() => {
if (!eventActionTrend.value.length) return
const series = eventActionTrend.value.map(group => {
return {
name: group.event_name,
type: 'line',
smooth: true,
label: { show: true, position: 'top' },
data: group.items.map(item => item.total)
}
})
const [first = {}] = eventActionTrend.value || []
return {
grid: { left: '5%', top: '10%', right: '5%', bottom: '5%', containLabel: true },
tooltip: { trigger: 'axis' },
legend: {
bottom: '10',
data: eventActionTrend.value.map(item => item.event_name)
},
xAxis: {
type: 'category',
boundaryGap: ['20%', '20%'],
data: first.items.map(item => item.group_name)
},
yAxis: { type: 'value' },
series
}
})
// 事件用户分布
const loading3 = ref(false)
const eventMember = ref([])
async function fetchEventMember() {
if (!userValue.value) return
loading3.value = true
const res = await api.getEventMemberList(params.value)
eventMember.value = res.data.items
loading3.value = false
}
const eventMemberOption = computed(() => {
if (!eventMember.value.length) return
return {
grid: { left: '5%', top: '10%', right: '5%', bottom: '5%', containLabel: true },
tooltip: { trigger: 'axis' },
xAxis: {
type: 'category',
// axisLabel: { interval: 0, rotate: 30 },
data: eventMember.value.map(item => item.group_name)
},
yAxis: {
type: 'value'
},
series: [
{
name: '用户',
type: 'bar',
label: { show: true, position: 'top' },
itemStyle: { borderRadius: 2 },
data: eventMember.value.map(item => item.total)
}
]
}
})
// 事件发生时间分析
const loading4 = ref(false)
const eventTime = ref([])
async function fetchEventTime() {
if (!userValue.value) return
loading4.value = true
const res = await api.getEventTimeList(params.value)
eventTime.value = res.data.items
loading4.value = false
}
const eventTimeOption = computed(() => {
if (!eventTime.value.length) return
const series = eventTime.value.map(group => {
return {
name: group.event_name,
type: 'line',
// label: { show: true, position: 'top' },
data: group.items.map(item => item.total)
}
})
const [first = {}] = eventTime.value || []
return {
grid: { left: '5%', top: '10%', right: '5%', bottom: '5%', containLabel: true },
tooltip: { trigger: 'axis' },
legend: {
bottom: '10',
data: eventTime.value.map(item => item.event_name)
},
xAxis: {
type: 'category',
boundaryGap: ['20%', '20%'],
data: first.items.map(item => item.group_name)
},
yAxis: { type: 'value' },
series
}
})
</script>
<template>
<AppCard title="事件分析">
<el-form inline label-suffix=":" v-if="info">
<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 label="时间区间">
<el-date-picker type="monthrange" v-model="date" value-format="YYYY-MM"></el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" :icon="DataLine" :loading="loading" @click="handleStart">分析</el-button>
</el-form-item>
<el-form-item>
<el-checkbox-group v-model="eventValues">
<el-checkbox v-for="item in eventList" :key="item.id" :value="item.id">{{ item.name }}</el-checkbox>
</el-checkbox-group>
</el-form-item>
</el-form>
<el-divider />
<div class="row">
<ChartCard title="事件行为分布" :options="eventActionOption" :loading="loading1"></ChartCard>
<ChartCard title="事件行为数量走势" :options="eventActionTrendOption" :loading="loading2" style="flex: 2.5"></ChartCard>
</div>
<div class="row">
<ChartCard title="事件用户分布" :options="eventMemberOption" :loading="loading3"></ChartCard>
<ChartCard title="事件发生时间分析" :options="eventTimeOption" :loading="loading4" style="flex: 2.5"></ChartCard>
</div>
</AppCard>
</template>
<style lang="scss" scoped>
.total {
font-size: 16px;
font-weight: bold;
color: var(--main-color);
}
.row {
display: flex;
gap: 20px;
& + .row {
margin-top: 20px;
}
}
</style>
import httpRequest from '@/utils/axios'
// 获取实验详情
export function getExperiment() {
return httpRequest.get('/api/lab/v1/experiment/once/experiment')
}
// 获取热门标签
export function getHotTags(params: { sso_id: string; number?: number }) {
return httpRequest.get('/api/lab/v1/experiment/analyse/hot-tags', { params })
}
// 标签人数分析(TOP5)
export function getTagTop(params: { sso_id: string; number?: number }) {
return httpRequest.get('/api/lab/v1/experiment/analyse/tag-top', { params })
}
// 用户标签数分析(TOP10)
export function getMemberTagTop(params: { sso_id: string; number?: number }) {
return httpRequest.get('/api/lab/v1/experiment/analyse/member-tag-top', { params })
}
// 热门群组
export function getHotGroups(params: { sso_id: string; number?: number }) {
return httpRequest.get('/api/lab/v1/experiment/analyse/hot-groups', { params })
}
// 群组人数分析(TOP5)
export function getGroupTop(params: { sso_id: string; number?: number }) {
return httpRequest.get('/api/lab/v1/experiment/analyse/group-top', { params })
}
// 用户群组数分析(TOP10)
export function getMemberGroupTop(params: { sso_id: string; number?: number }) {
return httpRequest.get('/api/lab/v1/experiment/analyse/member-group-top', { params })
}
import type { RouteRecordRaw } from 'vue-router'
import Layout from '@/components/layout/Index.vue'
const routes: RouteRecordRaw[] = [
{
path: '/analyze/label',
component: Layout,
children: [{ path: '', component: () => import('./views/Index.vue') }]
}
]
export { routes }
<script setup>
import ChartCard from '@/components/ChartCard.vue'
import { DataLine } from '@element-plus/icons-vue'
import { useUser } from '@/composables/useAllData'
import * as api from '../api'
const { userValue, userList } = useUser()
const info = ref()
async function fetchInfo() {
const res = await api.getExperiment()
info.value = res.data.detail
}
onMounted(fetchInfo)
watch(userValue, () => {
handleStart()
})
async function handleStart() {
fetchLabelHot()
fetchLabelTop()
fetchLabelMemberTop()
fetchGroupHot()
fetchGroupTop()
fetchGroupMemberTop()
}
const loading = computed(() => {
return loading1.value || loading2.value || loading3.value || loading4.value || loading5.value || loading6.value
})
// 热门标签
const loading1 = ref(false)
const labelHot = ref([])
async function fetchLabelHot() {
if (!userValue.value) return
loading1.value = true
const res = await api.getHotTags({ sso_id: userValue.value })
labelHot.value = res.data.items
loading1.value = false
}
const labelHotOption = computed(() => {
if (!labelHot.value.length) return
return {
grid: { left: '10%', top: '10%', right: '10%', bottom: '10%', containLabel: true },
tooltip: {},
series: [
{
type: 'wordCloud',
gridSize: 15,
sizeRange: [12, 50],
rotationRange: [0, 0],
shape: 'circle',
width: '100%',
height: '100%',
drawOutOfBound: false,
textStyle: {
color: function () {
return 'rgb(' + [Math.round(Math.random() * 160), Math.round(Math.random() * 160), Math.round(Math.random() * 160)].join(',') + ')'
}
},
emphasis: {
focus: 'self',
textStyle: {
textShadowBlur: 10,
textShadowColor: '#333'
}
},
data: labelHot.value.map(item => {
return { name: item.group_name, value: item.total }
})
}
]
}
})
// 标签用户分布Top5
const loading2 = ref(false)
const labelTop = ref([])
async function fetchLabelTop() {
if (!userValue.value) return
loading2.value = true
const res = await api.getTagTop({ sso_id: userValue.value })
labelTop.value = res.data.items
loading2.value = false
}
const labelTopOption = computed(() => {
if (!labelTop.value.length) return
return {
grid: { left: '5%', top: '10%', right: '5%', bottom: '5%', containLabel: true },
tooltip: {
trigger: 'axis'
},
xAxis: {
type: 'category',
axisLabel: { interval: 0 },
data: labelTop.value.map(item => item.group_name)
},
yAxis: {
type: 'value'
},
series: [
{
name: '用户',
type: 'bar',
label: { show: true, position: 'top' },
data: labelTop.value.map(item => item.total)
}
]
}
})
// 用户标签Top10
const loading3 = ref(false)
const labelMemberTop = ref([])
async function fetchLabelMemberTop() {
if (!userValue.value) return
loading3.value = true
const res = await api.getMemberTagTop({ sso_id: userValue.value })
labelMemberTop.value = res.data.items
loading3.value = false
}
const labelMemberTopOption = computed(() => {
if (!labelMemberTop.value.length) return
return {
grid: { left: '5%', top: '10%', right: '5%', bottom: '5%', containLabel: true },
tooltip: {
trigger: 'axis'
},
xAxis: { type: 'value' },
yAxis: {
type: 'category',
inverse: true,
data: labelMemberTop.value.map(item => item.group_name)
},
series: [
{
name: '标签',
type: 'bar',
label: { show: true, position: 'right' },
data: labelMemberTop.value.map(item => item.total)
}
]
}
})
// 热门群组
const loading4 = ref(false)
const groupHot = ref([])
async function fetchGroupHot() {
if (!userValue.value) return
loading4.value = true
const res = await api.getHotGroups({ sso_id: userValue.value })
groupHot.value = res.data.items
loading4.value = false
}
const groupHotOption = computed(() => {
if (!groupHot.value.length) return
return {
grid: { left: '10%', top: '10%', right: '10%', bottom: '10%', containLabel: true },
tooltip: {},
series: [
{
type: 'wordCloud',
gridSize: 15,
sizeRange: [12, 50],
rotationRange: [0, 0],
shape: 'circle',
width: '100%',
height: '100%',
drawOutOfBound: false,
textStyle: {
color: function () {
return 'rgb(' + [Math.round(Math.random() * 160), Math.round(Math.random() * 160), Math.round(Math.random() * 160)].join(',') + ')'
}
},
emphasis: {
focus: 'self',
textStyle: {
textShadowBlur: 10,
textShadowColor: '#333'
}
},
data: groupHot.value.map(item => {
return { name: item.group_name, value: item.total }
})
}
]
}
})
// 群组用户分布Top5
const loading5 = ref(false)
const groupTop = ref([])
async function fetchGroupTop() {
if (!userValue.value) return
loading5.value = true
const res = await api.getGroupTop({ sso_id: userValue.value })
groupTop.value = res.data.items
loading5.value = false
}
const groupTopOption = computed(() => {
if (!groupTop.value.length) return
return {
grid: { left: '5%', top: '10%', right: '5%', bottom: '5%', containLabel: true },
tooltip: {
trigger: 'axis'
},
xAxis: {
type: 'category',
// axisLabel: { interval: 0, rotate: 30 },
data: groupTop.value.map(item => item.group_name)
},
yAxis: {
type: 'value'
},
series: [
{
name: '用户',
type: 'bar',
label: { show: true, position: 'top' },
itemStyle: { borderRadius: 2 },
data: groupTop.value.map(item => item.total)
}
]
}
})
// 用户群组Top10
const loading6 = ref(false)
const groupMemberTop = ref([])
async function fetchGroupMemberTop() {
if (!userValue.value) return
loading6.value = true
const res = await api.getMemberGroupTop({ sso_id: userValue.value })
groupMemberTop.value = res.data.items
loading6.value = false
}
const groupMemberTopOption = computed(() => {
if (!groupMemberTop.value.length) return
return {
grid: { left: '5%', top: '10%', right: '5%', bottom: '5%', containLabel: true },
tooltip: {
trigger: 'axis'
},
xAxis: { type: 'value' },
yAxis: {
type: 'category',
inverse: true,
data: groupMemberTop.value.map(item => item.group_name)
},
series: [
{
name: '群组',
type: 'bar',
label: { show: true, position: 'right' },
data: groupMemberTop.value.map(item => item.total)
}
]
}
})
</script>
<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>
</el-form>
<el-divider />
<div class="row">
<ChartCard title="热门标签" :options="labelHotOption" :loading="loading1"></ChartCard>
<ChartCard title="标签用户分布Top5" :options="labelTopOption" :loading="loading2"></ChartCard>
<ChartCard title="用户标签Top10" :options="labelMemberTopOption" :loading="loading3"></ChartCard>
</div>
<div class="row">
<ChartCard title="热门群组" :options="groupHotOption" :loading="loading4"></ChartCard>
<ChartCard title="群组用户分布Top5" :options="groupTopOption" :loading="loading5"></ChartCard>
<ChartCard title="用户群组Top10" :options="groupMemberTopOption" :loading="loading6"></ChartCard>
</div>
</AppCard>
</template>
<style lang="scss" scoped>
.total {
font-size: 16px;
font-weight: bold;
color: var(--main-color);
}
.row {
display: flex;
gap: 20px;
& + .row {
margin-top: 20px;
}
}
</style>
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论