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

Merge branch 'pro' into gdrtvu

...@@ -288,6 +288,13 @@ ...@@ -288,6 +288,13 @@
"useParentElement": true, "useParentElement": true,
"usePerformanceObserver": true, "usePerformanceObserver": true,
"watchDeep": 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 { ...@@ -44,6 +44,7 @@ declare global {
const h: typeof import('vue')['h'] const h: typeof import('vue')['h']
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch'] const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
const inject: typeof import('vue')['inject'] const inject: typeof import('vue')['inject']
const injectLocal: typeof import('@vueuse/core')['injectLocal']
const isDefined: typeof import('@vueuse/core')['isDefined'] const isDefined: typeof import('@vueuse/core')['isDefined']
const isProxy: typeof import('vue')['isProxy'] const isProxy: typeof import('vue')['isProxy']
const isReactive: typeof import('vue')['isReactive'] const isReactive: typeof import('vue')['isReactive']
...@@ -73,6 +74,7 @@ declare global { ...@@ -73,6 +74,7 @@ declare global {
const onUpdated: typeof import('vue')['onUpdated'] const onUpdated: typeof import('vue')['onUpdated']
const pausableWatch: typeof import('@vueuse/core')['pausableWatch'] const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
const provide: typeof import('vue')['provide'] const provide: typeof import('vue')['provide']
const provideLocal: typeof import('@vueuse/core')['provideLocal']
const reactify: typeof import('@vueuse/core')['reactify'] const reactify: typeof import('@vueuse/core')['reactify']
const reactifyObject: typeof import('@vueuse/core')['reactifyObject'] const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
const reactive: typeof import('vue')['reactive'] const reactive: typeof import('vue')['reactive']
...@@ -136,6 +138,7 @@ declare global { ...@@ -136,6 +138,7 @@ declare global {
const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation'] const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
const useCached: typeof import('@vueuse/core')['useCached'] const useCached: typeof import('@vueuse/core')['useCached']
const useClipboard: typeof import('@vueuse/core')['useClipboard'] const useClipboard: typeof import('@vueuse/core')['useClipboard']
const useClipboardItems: typeof import('@vueuse/core')['useClipboardItems']
const useCloned: typeof import('@vueuse/core')['useCloned'] const useCloned: typeof import('@vueuse/core')['useCloned']
const useColorMode: typeof import('@vueuse/core')['useColorMode'] const useColorMode: typeof import('@vueuse/core')['useColorMode']
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog'] const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
...@@ -291,5 +294,6 @@ declare global { ...@@ -291,5 +294,6 @@ declare global {
// for type re-export // for type re-export
declare global { declare global {
// @ts-ignore // @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 @@ ...@@ -10,5 +10,6 @@
<body> <body>
<div id="app"></div> <div id="app"></div>
<script type="module" src="/src/main.ts"></script> <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> </body>
</html> </html>
差异被折叠。
...@@ -15,37 +15,43 @@ ...@@ -15,37 +15,43 @@
"cert": "node ./cert.js" "cert": "node ./cert.js"
}, },
"dependencies": { "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", "@tinymce/tinymce-vue": "^5.0.1",
"@vue-flow/controls": "^1.0.4", "@vue-flow/controls": "^1.0.4",
"@vue-flow/core": "^1.17.4", "@vue-flow/core": "^1.17.4",
"@vueuse/core": "^10.3.0", "@vueuse/core": "^10.9.0",
"axios": "^1.5.0", "axios": "^1.6.8",
"blueimp-md5": "^2.19.0", "blueimp-md5": "^2.19.0",
"echarts": "^5.4.3", "dayjs": "^1.11.10",
"element-plus": "^2.3.14", "echarts": "^5.5.0",
"echarts-wordcloud": "^2.1.0",
"element-plus": "^2.6.3",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"nanoid": "^4.0.2", "nanoid": "^5.0.7",
"pinia": "^2.1.6", "pinia": "^2.1.7",
"vue": "^3.3.4", "vue": "^3.4.23",
"vue-echarts": "^6.6.1", "vue-echarts": "^6.6.9",
"vue-router": "^4.2.4" "vue-router": "^4.3.2",
"xss": "^1.0.15"
}, },
"devDependencies": { "devDependencies": {
"@rushstack/eslint-patch": "^1.2.0", "@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", "@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/eslint-config-typescript": "^12.0.0",
"@vue/tsconfig": "^0.1.3", "@vue/tsconfig": "^0.5.1",
"ali-oss": "^6.18.1", "ali-oss": "^6.18.1",
"chalk": "^5.2.0", "chalk": "^5.2.0",
"eslint": "^8.43.0", "eslint": "^8.43.0",
"eslint-plugin-vue": "^9.17.0", "eslint-plugin-vue": "^9.17.0",
"sass": "^1.67.0", "sass": "^1.67.0",
"typescript": "~4.9.5", "typescript": "~5.4.5",
"unplugin-auto-import": "^0.16.6", "unplugin-auto-import": "^0.17.5",
"vite": "^4.4.9", "vite": "^4.5.3",
"vue-tsc": "^1.8.11" "vue-tsc": "^1.8.27"
} }
} }
...@@ -116,6 +116,11 @@ export function searchEventAttrs(params?: { ...@@ -116,6 +116,11 @@ export function searchEventAttrs(params?: {
return httpRequest.get('/api/lab/v1/experiment/event/search-attributes', { 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 }) { export function getLocalFileChunk(params: { file_size: number; file_name: string }) {
return httpRequest.get('/api/lab/v1/common/file/chunk', { params }) return httpRequest.get('/api/lab/v1/common/file/chunk', { params })
......
...@@ -81,13 +81,13 @@ textarea:focus { ...@@ -81,13 +81,13 @@ textarea:focus {
--main-success-color: #00ac27; --main-success-color: #00ac27;
} }
.el-dialog__header { .el-dialog__header {
margin-right: 0 !important;
border-bottom: 1px solid #e6e6e6; border-bottom: 1px solid #e6e6e6;
margin: 0 -16px 30px;
padding: 0 16px;
} }
.info .el-form-item { .info .el-form-item {
margin-bottom: 0; margin-bottom: 0;
} }
.info tr:last-child td { .info tr:last-child td {
padding-bottom: 0 !important; padding-bottom: 0 !important;
} }
...@@ -14,3 +14,15 @@ ...@@ -14,3 +14,15 @@
// 如果只是按需导入,则可以忽略以下内容。 // 如果只是按需导入,则可以忽略以下内容。
// 如果你想导入所有样式: // 如果你想导入所有样式:
@use 'element-plus/theme-chalk/src/index.scss' as *; @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 @@ ...@@ -56,8 +56,27 @@
.rule-item { .rule-item {
margin: 10px 0; margin: 10px 0;
} }
.event-rule { .event-rule,
.user-action-rule {
section + section { section + section {
margin-top: 30px; 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) => { ...@@ -23,12 +23,8 @@ const remoteMethod = (q: string) => {
</script> </script>
<template> <template>
<el-select remote filterable value-key="id" :loading="loading" :remote-method="remoteMethod" style="width: 100%"> <el-select remote filterable value-key="id" :loading="loading" :remote-method="remoteMethod">
<el-option <el-option v-for="item in userList" :key="item.id" :label="item.realname || item.nickname || item.username" :value="item.id">
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> <span>{{ item.realname || item.nickname || item.username }}</span>
<template v-if="item.mobile"> <template v-if="item.mobile">
<el-divider direction="vertical" /> <el-divider direction="vertical" />
......
...@@ -132,29 +132,14 @@ defineExpose({ refetch, tableRef }) ...@@ -132,29 +132,14 @@ defineExpose({ refetch, tableRef })
</template> </template>
<template v-else> <template v-else>
<!-- input --> <!-- input -->
<el-input <el-input v-model="params[item.prop]" v-bind="item" clearable @change="search" v-if="item.type === 'input'" />
v-model="params[item.prop]"
v-bind="item"
clearable
@change="search"
style="width: 200px"
v-if="item.type === 'input'"
/>
<!-- select --> <!-- select -->
<el-select <el-select v-model="params[item.prop]" v-bind="item" filterable clearable @change="search" v-if="item.type === 'select'">
v-model="params[item.prop]"
v-bind="item"
filterable
clearable
@change="search"
v-if="item.type === 'select'"
>
<el-option <el-option
v-for="(option, index) in item.options" v-for="(option, index) in item.options"
:label="option[item.labelKey] || option.label || option" :label="option[item.labelKey] || option.label || option"
:value="option[item.valueKey] || option.value || option" :value="option[item.valueKey] || option.value || option"
:key="index" :key="index" />
/>
</el-select> </el-select>
</template> </template>
</el-form-item> </el-form-item>
...@@ -172,13 +157,7 @@ defineExpose({ refetch, tableRef }) ...@@ -172,13 +157,7 @@ defineExpose({ refetch, tableRef })
<!-- 主体 --> <!-- 主体 -->
<div class="table-list-bd"> <div class="table-list-bd">
<slot name="body" v-bind="{ data: dataList }"> <slot name="body" v-bind="{ data: dataList }">
<el-table <el-table :header-cell-style="{ background: '#ededed' }" :data="dataList" v-loading="loading" v-bind="$attrs" ref="tableRef">
: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"> <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"> <template #default="scope" v-if="item.slots || item.computed">
<slot :name="item.slots" v-bind="scope" v-if="item.slots"></slot> <slot :name="item.slots" v-bind="scope" v-if="item.slots"></slot>
...@@ -204,8 +183,7 @@ defineExpose({ refetch, tableRef }) ...@@ -204,8 +183,7 @@ defineExpose({ refetch, tableRef })
@size-change="pageSizeChange" @size-change="pageSizeChange"
@current-change="fetchList()" @current-change="fetchList()"
:hide-on-single-page="true" :hide-on-single-page="true"
v-if="hasPagination" v-if="hasPagination">
>
</el-pagination> </el-pagination>
</div> </div>
</div> </div>
......
...@@ -74,7 +74,7 @@ const typeName = computed(() => { ...@@ -74,7 +74,7 @@ const typeName = computed(() => {
'7': '小程序', '7': '小程序',
'8': '卡券' '8': '卡券'
} }
return json[props.type] return json[props.type] || ''
}) })
</script> </script>
...@@ -84,7 +84,7 @@ const typeName = computed(() => { ...@@ -84,7 +84,7 @@ const typeName = computed(() => {
:title="props.data ? (props.data?.isView ? `查看${typeName}资料` : `编辑${typeName}资料`) : `新建${typeName}资料`" :title="props.data ? (props.data?.isView ? `查看${typeName}资料` : `编辑${typeName}资料`) : `新建${typeName}资料`"
:close-on-click-modal="false" :close-on-click-modal="false"
width="800px" width="800px"
@update:modelValue="$emit('update:modelValue')"> @update:modelValue="value => $emit('update:modelValue', value)">
<el-form <el-form
:disabled="props.data?.isView" :disabled="props.data?.isView"
ref="ruleFormRef" ref="ruleFormRef"
......
...@@ -4,56 +4,35 @@ const props = defineProps<{ node: NodeProps }>() ...@@ -4,56 +4,35 @@ const props = defineProps<{ node: NodeProps }>()
const component = computed(() => { const component = computed(() => {
const allComponent: any = { const allComponent: any = {
TCRealTimeTrigger: markRaw( TCRealTimeTrigger: markRaw(defineAsyncComponent(() => import('./components/triggeringConditions/realTimeTrigger/Index.vue'))),
defineAsyncComponent(() => import('./components/triggeringConditions/realTimeTrigger/Index.vue'))
),
TCJoinGroup: markRaw(defineAsyncComponent(() => import('./components/triggeringConditions/joinGroup/Index.vue'))), TCJoinGroup: markRaw(defineAsyncComponent(() => import('./components/triggeringConditions/joinGroup/Index.vue'))),
TCChangeProps: markRaw( TCChangeProps: markRaw(defineAsyncComponent(() => import('./components/triggeringConditions/changeProps/Index.vue'))),
defineAsyncComponent(() => import('./components/triggeringConditions/changeProps/Index.vue')) TCOffiaccount: markRaw(defineAsyncComponent(() => import('./components/triggeringConditions/offiaccount/Index.vue'))),
),
TCOffiaccount: markRaw(
defineAsyncComponent(() => import('./components/triggeringConditions/offiaccount/Index.vue'))
),
TCDouyin: markRaw(defineAsyncComponent(() => import('./components/triggeringConditions/douyin/Index.vue'))), TCDouyin: markRaw(defineAsyncComponent(() => import('./components/triggeringConditions/douyin/Index.vue'))),
TCXiaohongshu: markRaw( TCXiaohongshu: markRaw(defineAsyncComponent(() => import('./components/triggeringConditions/xiaohongshu/Index.vue'))),
defineAsyncComponent(() => import('./components/triggeringConditions/xiaohongshu/Index.vue'))
),
TCWeibo: markRaw(defineAsyncComponent(() => import('./components/triggeringConditions/weibo/Index.vue'))), TCWeibo: markRaw(defineAsyncComponent(() => import('./components/triggeringConditions/weibo/Index.vue'))),
TCCustom: markRaw(defineAsyncComponent(() => import('./components/triggeringConditions/custom/Index.vue'))), TCCustom: markRaw(defineAsyncComponent(() => import('./components/triggeringConditions/custom/Index.vue'))),
TCXiaoetong: markRaw(defineAsyncComponent(() => import('./components/triggeringConditions/xiaoetong/Index.vue'))), TCXiaoetong: markRaw(defineAsyncComponent(() => import('./components/triggeringConditions/xiaoetong/Index.vue'))),
TCWenjuanxing: markRaw( TCWenjuanxing: markRaw(defineAsyncComponent(() => import('./components/triggeringConditions/wenjuanxing/Index.vue'))),
defineAsyncComponent(() => import('./components/triggeringConditions/wenjuanxing/Index.vue'))
),
MAEndTrip: markRaw(defineAsyncComponent(() => import('./components/marketingAction/endTrip/Index.vue'))), MAEndTrip: markRaw(defineAsyncComponent(() => import('./components/marketingAction/endTrip/Index.vue'))),
MAJoinGroup: markRaw(defineAsyncComponent(() => import('./components/marketingAction/joinGroup/Index.vue'))), MAJoinGroup: markRaw(defineAsyncComponent(() => import('./components/marketingAction/joinGroup/Index.vue'))),
MALeaveGroup: markRaw(defineAsyncComponent(() => import('./components/marketingAction/leaveGroup/Index.vue'))), MALeaveGroup: markRaw(defineAsyncComponent(() => import('./components/marketingAction/leaveGroup/Index.vue'))),
MAChangeProps: markRaw(defineAsyncComponent(() => import('./components/marketingAction/changeProps/Index.vue'))), MAChangeProps: markRaw(defineAsyncComponent(() => import('./components/marketingAction/changeProps/Index.vue'))),
MADelayProcess: markRaw(defineAsyncComponent(() => import('./components/marketingAction/delayProcess/Index.vue'))), MADelayProcess: markRaw(defineAsyncComponent(() => import('./components/marketingAction/delayProcess/Index.vue'))),
MAInternalNotice: markRaw( MAInternalNotice: markRaw(defineAsyncComponent(() => import('./components/marketingAction/internalNotice/Index.vue'))),
defineAsyncComponent(() => import('./components/marketingAction/internalNotice/Index.vue'))
),
MAOffiaccount: markRaw(defineAsyncComponent(() => import('./components/marketingAction/offiaccount/Index.vue'))), MAOffiaccount: markRaw(defineAsyncComponent(() => import('./components/marketingAction/offiaccount/Index.vue'))),
MAEmail: markRaw(defineAsyncComponent(() => import('./components/marketingAction/email/Index.vue'))), MAEmail: markRaw(defineAsyncComponent(() => import('./components/marketingAction/email/Index.vue'))),
MASMS: markRaw(defineAsyncComponent(() => import('./components/marketingAction/sms/Index.vue'))), MASMS: markRaw(defineAsyncComponent(() => import('./components/marketingAction/sms/Index.vue'))),
MADouyin: markRaw(defineAsyncComponent(() => import('./components/marketingAction/douyin/Index.vue'))), MADouyin: markRaw(defineAsyncComponent(() => import('./components/marketingAction/douyin/Index.vue'))),
MAWeibo: markRaw(defineAsyncComponent(() => import('./components/marketingAction/weibo/Index.vue'))), MAWeibo: markRaw(defineAsyncComponent(() => import('./components/marketingAction/weibo/Index.vue'))),
MADingTalk: markRaw(defineAsyncComponent(() => import('./components/marketingAction/dingtalk/Index.vue'))), MADingTalk: markRaw(defineAsyncComponent(() => import('./components/marketingAction/dingtalk/Index.vue'))),
CBAttributeJudgment: markRaw( MAAB: markRaw(defineAsyncComponent(() => import('./components/marketingAction/ab/Index.vue'))),
defineAsyncComponent(() => import('./components/conditionalBranch/attributeJudgment/Index.vue')) CBAttributeJudgment: markRaw(defineAsyncComponent(() => import('./components/conditionalBranch/attributeJudgment/Index.vue'))),
), CBGroupJudgment: markRaw(defineAsyncComponent(() => import('./components/conditionalBranch/groupJudgment/Index.vue'))),
CBGroupJudgment: markRaw( CBEventJudgment: markRaw(defineAsyncComponent(() => import('./components/conditionalBranch/eventJudgment/Index.vue'))),
defineAsyncComponent(() => import('./components/conditionalBranch/groupJudgment/Index.vue')) CBTimeJudgment: markRaw(defineAsyncComponent(() => import('./components/conditionalBranch/timeJudgment/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'))), CBOffiaccount: markRaw(defineAsyncComponent(() => import('./components/conditionalBranch/offiaccount/Index.vue'))),
CBLabelJudgment: markRaw( CBLabelJudgment: markRaw(defineAsyncComponent(() => import('./components/conditionalBranch/labelJudgment/Index.vue')))
defineAsyncComponent(() => import('./components/conditionalBranch/labelJudgment/Index.vue'))
)
} }
return allComponent[props.node?.data.component_name || props.node?.data.componentName] return allComponent[props.node?.data.component_name || props.node?.data.componentName]
}) })
......
...@@ -51,7 +51,9 @@ onConnect(params => { ...@@ -51,7 +51,9 @@ onConnect(params => {
'handle-yes': { label: '是', style: { stroke: '#81b337' } }, 'handle-yes': { label: '是', style: { stroke: '#81b337' } },
'handle-no': { label: '否', style: { stroke: '#a16222' } }, 'handle-no': { label: '否', style: { stroke: '#a16222' } },
'handle-success': { label: '成功', style: { stroke: '#81b337' } }, '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 = {} let customParams = {}
if (params.sourceHandle) { if (params.sourceHandle) {
......
...@@ -216,6 +216,14 @@ const list = ref([ ...@@ -216,6 +216,14 @@ const list = ref([
component_type: 9, component_type: 9,
component_name: 'MADingTalk', component_name: 'MADingTalk',
connection_type: 2 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(() => { ...@@ -280,9 +288,7 @@ const currentList = computed(() => {
return list.value.map(item => { return list.value.map(item => {
return { return {
...item, ...item,
children: item.children.filter(item => children: item.children.filter(item => (item.connection_type ? connections.value.find(connection => connection.type === item.connection_type) : true))
item.connection_type ? connections.value.find(connection => connection.type === item.connection_type) : true
)
} }
}) })
}) })
...@@ -301,18 +307,9 @@ const onDragStart = (event: DragEvent, data: any) => { ...@@ -301,18 +307,9 @@ const onDragStart = (event: DragEvent, data: any) => {
<dt :style="`background: ${parent.background?.color}`">{{ parent.name }}</dt> <dt :style="`background: ${parent.background?.color}`">{{ parent.name }}</dt>
<dd> <dd>
<ul> <ul>
<li <li v-for="(item, index) in parent.children" :key="index" :draggable="true" @dragstart="event => onDragStart(event, item)">
v-for="(item, index) in parent.children"
:key="index"
:draggable="true"
@dragstart="event => onDragStart(event, item)">
<div class="icon-box"> <div class="icon-box">
<Icon <Icon class="circle" :color="item.color || parent.background?.color" :name="parent.background?.icon || ''" w="60" h="60"></Icon>
class="circle"
:color="item.color || parent.background?.color"
:name="parent.background?.icon || ''"
w="60"
h="60"></Icon>
<Icon class="icon" color="#fff" :name="item.icon" w="24" h="24"></Icon> <Icon class="icon" color="#fff" :name="item.icon" w="24" h="24"></Icon>
</div> </div>
<p>{{ item.name }}</p> <p>{{ item.name }}</p>
......
...@@ -3,10 +3,10 @@ import type { NodeProps } from '@vue-flow/core' ...@@ -3,10 +3,10 @@ import type { NodeProps } from '@vue-flow/core'
import { useVueFlow, Handle, Position } from '@vue-flow/core' import { useVueFlow, Handle, Position } from '@vue-flow/core'
import { Setting, Delete } from '@element-plus/icons-vue' import { Setting, Delete } from '@element-plus/icons-vue'
const props = withDefaults( const props = withDefaults(defineProps<{ node: NodeProps; connectionType?: number; canSetting?: boolean; canConnect?: boolean }>(), {
defineProps<{ node: NodeProps; connectionType?: number; canSetting?: boolean; canConnect?: boolean }>(), canSetting: true,
{ canSetting: true, canConnect: true } canConnect: true
) })
const emit = defineEmits<{ (e: 'setting'): void }>() const emit = defineEmits<{ (e: 'setting'): void }>()
...@@ -42,6 +42,10 @@ function onRemove() { ...@@ -42,6 +42,10 @@ function onRemove() {
<Handle id="handle-failure" class="handle-link is-no" :position="Position.Left">失败</Handle> <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> <Handle id="handle-any" class="handle-link is-any" :position="Position.Left">继续</Handle>
</template> </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> <template v-else>
<Handle id="handle-any" class="handle-default" :position="Position.Left"> <Handle id="handle-any" class="handle-default" :position="Position.Left">
<svg <svg
......
...@@ -58,9 +58,9 @@ function handleSubmit() { ...@@ -58,9 +58,9 @@ function handleSubmit() {
<el-table-column label="属性生成规则" align="center" width="400"> <el-table-column label="属性生成规则" align="center" width="400">
<template #default="{ row }"> <template #default="{ row }">
<el-radio-group v-model="row.form.mode"> <el-radio-group v-model="row.form.mode">
<el-radio :label="1">随机值</el-radio> <el-radio :value="1">随机值</el-radio>
<el-radio :label="2">固定值</el-radio> <el-radio :value="2">固定值</el-radio>
<el-radio :label="3">历史随机值</el-radio> <el-radio :value="3">历史随机值</el-radio>
</el-radio-group> </el-radio-group>
<div class="rule-box"> <div class="rule-box">
<!-- 字符串 --> <!-- 字符串 -->
...@@ -68,8 +68,8 @@ function handleSubmit() { ...@@ -68,8 +68,8 @@ function handleSubmit() {
<!-- 随机值 --> <!-- 随机值 -->
<template v-if="row.form.mode === 1"> <template v-if="row.form.mode === 1">
<el-radio-group v-model="row.form.rule.type"> <el-radio-group v-model="row.form.rule.type">
<el-radio :label="1">来自于系统数据</el-radio> <el-radio :value="1">来自于系统数据</el-radio>
<el-radio :label="4">完全随机</el-radio> <el-radio :value="4">完全随机</el-radio>
</el-radio-group> </el-radio-group>
</template> </template>
<!-- 固定值 --> <!-- 固定值 -->
...@@ -81,8 +81,10 @@ function handleSubmit() { ...@@ -81,8 +81,10 @@ function handleSubmit() {
<template v-if="row.type === '2' || row.type === '3'"> <template v-if="row.type === '2' || row.type === '3'">
<!-- 随机值 --> <!-- 随机值 -->
<template v-if="row.form.mode === 1"> <template v-if="row.form.mode === 1">
Min<el-input-number :controls="false" size="small" v-model="row.form.rule.num_min" /> Min<el-input-number :controls="false" size="small" v-model="row.form.rule.num_min" /> Max<el-input-number
Max<el-input-number :controls="false" size="small" v-model="row.form.rule.num_max" /> :controls="false"
size="small"
v-model="row.form.rule.num_max" />
</template> </template>
<!-- 固定值 --> <!-- 固定值 -->
<template v-if="row.form.mode === 2"> <template v-if="row.form.mode === 2">
...@@ -94,31 +96,17 @@ function handleSubmit() { ...@@ -94,31 +96,17 @@ function handleSubmit() {
<!-- 随机值 --> <!-- 随机值 -->
<template v-if="row.form.mode === 1"> <template v-if="row.form.mode === 1">
<el-radio-group v-model="row.form.rule.date_type"> <el-radio-group v-model="row.form.rule.date_type">
<el-radio :label="1">完全随机(不早于当前时间)</el-radio> <el-radio :value="1">完全随机(不早于当前时间)</el-radio>
<el-radio :label="2"> <el-radio :value="2">
时间区间&nbsp;<el-date-picker 时间区间&nbsp;<el-date-picker size="small" value-format="YYYY-MM-DD" v-model="row.form.rule.date_start" style="width: 120px" />
size="small"
value-format="YYYY-MM-DD"
v-model="row.form.rule.date_start"
style="width: 120px" />
&nbsp;&nbsp; &nbsp;&nbsp;
<el-date-picker <el-date-picker size="small" value-format="YYYY-MM-DD" v-model="row.form.rule.date_end" style="width: 120px" />
size="small"
value-format="YYYY-MM-DD"
v-model="row.form.rule.date_end"
style="width: 120px" />
</el-radio> </el-radio>
<el-radio :label="3"> <el-radio :value="3">
当前时间前&nbsp;<el-input-number 当前时间前&nbsp;<el-input-number :controls="false" size="small" v-model="row.form.rule.date_current_before" />&nbsp;
:controls="false"
size="small"
v-model="row.form.rule.date_current_before" />&nbsp;
</el-radio> </el-radio>
<el-radio :label="4"> <el-radio :value="4">
当前时间后&nbsp;<el-input-number 当前时间后&nbsp;<el-input-number :controls="false" size="small" v-model="row.form.rule.date_current_after" />&nbsp;
:controls="false"
size="small"
v-model="row.form.rule.date_current_after" />&nbsp;
</el-radio> </el-radio>
</el-radio-group> </el-radio-group>
</template> </template>
......
...@@ -57,8 +57,8 @@ function onSaveEventRule(rules: []) { ...@@ -57,8 +57,8 @@ function onSaveEventRule(rules: []) {
<template #step-body-after> <template #step-body-after>
<el-row justify="center" style="margin-bottom: 20px"> <el-row justify="center" style="margin-bottom: 20px">
<el-radio-group v-model="radio" size="large"> <el-radio-group v-model="radio" size="large">
<el-radio-button label="1">判断事件数据</el-radio-button> <el-radio-button value="1">判断事件数据</el-radio-button>
<el-radio-button label="2">生成事件数据</el-radio-button> <el-radio-button value="2">生成事件数据</el-radio-button>
</el-radio-group> </el-radio-group>
</el-row> </el-row>
</template> </template>
...@@ -70,13 +70,13 @@ function onSaveEventRule(rules: []) { ...@@ -70,13 +70,13 @@ function onSaveEventRule(rules: []) {
</p> </p>
<el-form-item label="生成规则"> <el-form-item label="生成规则">
<el-radio-group :model-value="1"> <el-radio-group :model-value="1">
<el-radio :label="1">随机</el-radio> <el-radio :value="1">随机</el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="生成方式"> <el-form-item label="生成方式">
<el-radio-group v-model="form.data_rule.generate_way"> <el-radio-group v-model="form.data_rule.generate_way">
<el-radio :label="1">百分比</el-radio> <el-radio :value="1">百分比</el-radio>
<el-radio :label="2">数值</el-radio> <el-radio :value="2">数值</el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="请输入分支条件数值"></el-form-item> <el-form-item label="请输入分支条件数值"></el-form-item>
...@@ -98,20 +98,12 @@ function onSaveEventRule(rules: []) { ...@@ -98,20 +98,12 @@ function onSaveEventRule(rules: []) {
<el-row> <el-row>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="Min"> <el-form-item label="Min">
<el-input-number <el-input-number v-model="form.data_rule.event_count_min" :controls="false" :max="3" style="width: 80px" />
v-model="form.data_rule.event_count_min"
:controls="false"
:max="3"
style="width: 80px" />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="Max"> <el-form-item label="Max">
<el-input-number <el-input-number v-model="form.data_rule.event_count_max" :controls="false" :max="5" style="width: 80px" />
v-model="form.data_rule.event_count_max"
:controls="false"
:max="5"
style="width: 80px" />
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
...@@ -127,13 +119,13 @@ function onSaveEventRule(rules: []) { ...@@ -127,13 +119,13 @@ function onSaveEventRule(rules: []) {
<el-form-item label="生成规则"> <el-form-item label="生成规则">
<el-radio-group :model-value="1"> <el-radio-group :model-value="1">
<el-radio :label="1">随机</el-radio> <el-radio :value="1">随机</el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="生成方式"> <el-form-item label="生成方式">
<el-radio-group v-model="form.data_rule.generate_way"> <el-radio-group v-model="form.data_rule.generate_way">
<el-radio :label="1">百分比</el-radio> <el-radio :value="1">百分比</el-radio>
<el-radio :label="2">数值</el-radio> <el-radio :value="2">数值</el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="请输入分支条件数值"></el-form-item> <el-form-item label="请输入分支条件数值"></el-form-item>
...@@ -155,20 +147,12 @@ function onSaveEventRule(rules: []) { ...@@ -155,20 +147,12 @@ function onSaveEventRule(rules: []) {
<el-row> <el-row>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="Min"> <el-form-item label="Min">
<el-input-number <el-input-number v-model="form.data_rule.event_count_min" :controls="false" :max="3" style="width: 80px" />
v-model="form.data_rule.event_count_min"
:controls="false"
:max="3"
style="width: 80px" />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="Max"> <el-form-item label="Max">
<el-input-number <el-input-number v-model="form.data_rule.event_count_max" :controls="false" :max="5" style="width: 80px" />
v-model="form.data_rule.event_count_max"
:controls="false"
:max="5"
style="width: 80px" />
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
...@@ -198,10 +182,5 @@ function onSaveEventRule(rules: []) { ...@@ -198,10 +182,5 @@ function onSaveEventRule(rules: []) {
</section> </section>
</template> </template>
</RuleTemplate> </RuleTemplate>
<Generate <Generate :node="node" :rules="form.event_attr_rule" v-model="generateVisible" @save="onSaveEventRule" v-if="generateVisible"></Generate>
:node="node"
:rules="form.event_attr_rule"
v-model="generateVisible"
@save="onSaveEventRule"
v-if="generateVisible"></Generate>
</template> </template>
...@@ -27,8 +27,8 @@ watchEffect(() => { ...@@ -27,8 +27,8 @@ watchEffect(() => {
<ConfigTemplate :model="form" :node="node"> <ConfigTemplate :model="form" :node="node">
<el-form-item> <el-form-item>
<el-radio-group v-model="form.in_group"> <el-radio-group v-model="form.in_group">
<el-radio label="0">在群组中</el-radio> <el-radio value="0">在群组中</el-radio>
<el-radio label="1">不在群组中</el-radio> <el-radio value="1">不在群组中</el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
......
...@@ -35,7 +35,7 @@ watchEffect(() => { ...@@ -35,7 +35,7 @@ watchEffect(() => {
<el-form-item> <el-form-item>
<template v-if="step === 0"> <template v-if="step === 0">
<el-radio-group v-model="form.operate" 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 }} {{ item.label }}
</el-radio> </el-radio>
</el-radio-group> </el-radio-group>
......
...@@ -44,7 +44,7 @@ watchEffect(() => { ...@@ -44,7 +44,7 @@ watchEffect(() => {
<ConfigTemplate :model="form" :node="node"> <ConfigTemplate :model="form" :node="node">
<el-form-item label="时间判断类型"> <el-form-item label="时间判断类型">
<el-radio-group v-model="form.date_type"> <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-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="进入该步骤的时间"> <el-form-item label="进入该步骤的时间">
...@@ -69,7 +69,7 @@ watchEffect(() => { ...@@ -69,7 +69,7 @@ watchEffect(() => {
</template> </template>
<template v-else> <template v-else>
<el-checkbox-group v-model="form.week"> <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> </el-checkbox-group>
</template> </template>
</el-form-item> </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(() => { ...@@ -37,32 +37,19 @@ watchEffect(() => {
<ConfigTemplate :model="form" :node="node"> <ConfigTemplate :model="form" :node="node">
<el-form-item> <el-form-item>
<el-radio-group v-model="form.time_type" style="display: block"> <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> <span>延时 </span>
<el-input <el-input :disabled="form.time_type !== '0'" v-model="form.time_num" placeholder="请输入" class="input-with-select">
:disabled="form.time_type !== '0'"
v-model="form.time_num"
placeholder="请输入"
class="input-with-select">
<template #append> <template #append>
<el-select <el-select v-model="form.time_unit" placeholder="请选择" style="width: 115px" :disabled="form.time_type !== '0'">
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-option v-for="item in timeUnitList" :key="item.value" :label="item.label" :value="item.value" />
</el-select> </el-select>
</template> </template>
</el-input> </el-input>
</el-radio> </el-radio>
<el-radio label="1" size="large" style="display: block"> <el-radio value="1" size="large" style="display: block">
<span>延时至 </span> <span>延时至 </span>
<el-date-picker <el-date-picker :disabled="form.time_type !== '1'" v-model="form.time" type="datetime" placeholder="请选择" value-format="YYYY-MM-DD HH:mm:ss" />
:disabled="form.time_type !== '1'"
v-model="form.time"
type="datetime"
placeholder="请选择"
value-format="YYYY-MM-DD HH:mm:ss" />
</el-radio> </el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
......
...@@ -35,7 +35,7 @@ const { connectionList } = useConnection(2) ...@@ -35,7 +35,7 @@ const { connectionList } = useConnection(2)
<template v-if="step === 0"> <template v-if="step === 0">
<el-form-item> <el-form-item>
<el-radio-group v-model="form.operate" 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"> <el-radio v-for="item in operateList" :key="item.value" :value="item.value">
{{ item.label }} {{ item.label }}
</el-radio> </el-radio>
</el-radio-group> </el-radio-group>
...@@ -51,7 +51,7 @@ const { connectionList } = useConnection(2) ...@@ -51,7 +51,7 @@ const { connectionList } = useConnection(2)
<template v-else-if="step === 2"> <template v-else-if="step === 2">
<el-form-item> <el-form-item>
<el-radio-group v-model="form.material_type" @change="form.material_id = ''"> <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 }} 发送{{ item.label }}
</el-radio> </el-radio>
</el-radio-group> </el-radio-group>
......
...@@ -39,7 +39,7 @@ const { connectionList } = useConnection(6) ...@@ -39,7 +39,7 @@ const { connectionList } = useConnection(6)
<template v-if="step === 0"> <template v-if="step === 0">
<el-form-item> <el-form-item>
<el-radio-group v-model="form.operate" 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"> <el-radio v-for="item in operateList" :key="item.value" :value="item.value">
{{ item.label }} {{ item.label }}
</el-radio> </el-radio>
</el-radio-group> </el-radio-group>
...@@ -55,7 +55,7 @@ const { connectionList } = useConnection(6) ...@@ -55,7 +55,7 @@ const { connectionList } = useConnection(6)
<template v-else-if="step === 2"> <template v-else-if="step === 2">
<el-form-item> <el-form-item>
<el-radio-group v-model="form.material_type" @change="form.material_id = ''"> <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 }} 发送{{ item.label }}
</el-radio> </el-radio>
</el-radio-group> </el-radio-group>
......
...@@ -34,7 +34,7 @@ const { connectionList } = useConnection(9) ...@@ -34,7 +34,7 @@ const { connectionList } = useConnection(9)
<template v-if="step === 0"> <template v-if="step === 0">
<el-form-item> <el-form-item>
<el-radio-group v-model="form.operate" 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"> <el-radio v-for="item in operateList" :key="item.value" :value="item.value">
{{ item.label }} {{ item.label }}
</el-radio> </el-radio>
</el-radio-group> </el-radio-group>
...@@ -50,7 +50,7 @@ const { connectionList } = useConnection(9) ...@@ -50,7 +50,7 @@ const { connectionList } = useConnection(9)
<template v-else-if="step === 2"> <template v-else-if="step === 2">
<el-form-item> <el-form-item>
<el-radio-group v-model="form.material_type" @change="form.material_id = ''"> <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 }} 发送{{ item.label }}
</el-radio> </el-radio>
</el-radio-group> </el-radio-group>
......
...@@ -32,7 +32,7 @@ const { connectionList } = useConnection(1) ...@@ -32,7 +32,7 @@ const { connectionList } = useConnection(1)
<template v-if="step === 0"> <template v-if="step === 0">
<el-form-item> <el-form-item>
<el-radio-group v-model="form.material_type" @change="form.material_id = ''"> <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 }} 发送{{ item.label }}
</el-radio> </el-radio>
</el-radio-group> </el-radio-group>
......
...@@ -26,7 +26,7 @@ const { connectionList } = useConnection(10) ...@@ -26,7 +26,7 @@ const { connectionList } = useConnection(10)
<template v-if="step === 0"> <template v-if="step === 0">
<el-form-item> <el-form-item>
<el-radio-group v-model="form.operate" 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"> <el-radio v-for="item in operateList" :key="item.value" :value="item.value">
{{ item.label }} {{ item.label }}
</el-radio> </el-radio>
</el-radio-group> </el-radio-group>
......
...@@ -38,7 +38,7 @@ const { connectionList } = useConnection(7) ...@@ -38,7 +38,7 @@ const { connectionList } = useConnection(7)
<template v-if="step === 0"> <template v-if="step === 0">
<el-form-item> <el-form-item>
<el-radio-group v-model="form.operate" 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"> <el-radio v-for="item in operateList" :key="item.value" :value="item.value">
{{ item.label }} {{ item.label }}
</el-radio> </el-radio>
</el-radio-group> </el-radio-group>
...@@ -54,7 +54,7 @@ const { connectionList } = useConnection(7) ...@@ -54,7 +54,7 @@ const { connectionList } = useConnection(7)
<template v-else-if="step === 2"> <template v-else-if="step === 2">
<el-form-item> <el-form-item>
<el-radio-group v-model="form.material_type" @change="form.material_id = ''"> <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 }} 发送{{ item.label }}
</el-radio> </el-radio>
</el-radio-group> </el-radio-group>
......
...@@ -31,7 +31,7 @@ const operateList = ref([ ...@@ -31,7 +31,7 @@ const operateList = ref([
<el-form-item> <el-form-item>
<template v-if="step === 0"> <template v-if="step === 0">
<el-radio-group v-model="form.operate" 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 }} {{ item.label }}
</el-radio> </el-radio>
</el-radio-group> </el-radio-group>
......
...@@ -31,7 +31,7 @@ const operateList = ref([ ...@@ -31,7 +31,7 @@ const operateList = ref([
<el-form-item> <el-form-item>
<template v-if="step === 0"> <template v-if="step === 0">
<el-radio-group v-model="form.operate" 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 }} {{ item.label }}
</el-radio> </el-radio>
</el-radio-group> </el-radio-group>
......
...@@ -37,7 +37,7 @@ const operateList = ref([ ...@@ -37,7 +37,7 @@ const operateList = ref([
<el-form-item> <el-form-item>
<template v-if="step === 0"> <template v-if="step === 0">
<el-radio-group v-model="form.operate" 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 }} {{ item.label }}
</el-radio> </el-radio>
</el-radio-group> </el-radio-group>
......
...@@ -19,9 +19,9 @@ watchEffect(() => { ...@@ -19,9 +19,9 @@ watchEffect(() => {
<ConfigTemplate :model="form" :node="node"> <ConfigTemplate :model="form" :node="node">
<el-form-item label="触发类型" prop="trigger_type"> <el-form-item label="触发类型" prop="trigger_type">
<el-radio-group v-model="form.trigger_type"> <el-radio-group v-model="form.trigger_type">
<el-radio label="0">单次触发</el-radio> <el-radio value="0">单次触发</el-radio>
<el-radio label="1">重复触发</el-radio> <el-radio value="1">重复触发</el-radio>
<el-radio label="2">立即触发</el-radio> <el-radio value="2">立即触发</el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<template v-if="form.trigger_type === '0'"> <template v-if="form.trigger_type === '0'">
......
...@@ -29,7 +29,7 @@ const operateList = ref([ ...@@ -29,7 +29,7 @@ const operateList = ref([
<el-form-item> <el-form-item>
<template v-if="step === 0"> <template v-if="step === 0">
<el-radio-group v-model="form.operate" 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 }} {{ item.label }}
</el-radio> </el-radio>
</el-radio-group> </el-radio-group>
......
...@@ -26,7 +26,7 @@ const operateList = ref([{ label: '提交表单', value: '0' }]) ...@@ -26,7 +26,7 @@ const operateList = ref([{ label: '提交表单', value: '0' }])
<el-form-item> <el-form-item>
<template v-if="step === 0"> <template v-if="step === 0">
<el-radio-group v-model="form.operate" 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 }} {{ item.label }}
</el-radio> </el-radio>
</el-radio-group> </el-radio-group>
......
...@@ -26,7 +26,7 @@ const operateList = ref([{ label: '新用户注册', value: '0' }]) ...@@ -26,7 +26,7 @@ const operateList = ref([{ label: '新用户注册', value: '0' }])
<el-form-item> <el-form-item>
<template v-if="step === 0"> <template v-if="step === 0">
<el-radio-group v-model="form.operate" 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 }} {{ item.label }}
</el-radio> </el-radio>
</el-radio-group> </el-radio-group>
......
...@@ -33,7 +33,7 @@ const operateList = ref([ ...@@ -33,7 +33,7 @@ const operateList = ref([
<el-form-item> <el-form-item>
<template v-if="step === 0"> <template v-if="step === 0">
<el-radio-group v-model="form.operate" 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 }} {{ item.label }}
</el-radio> </el-radio>
</el-radio-group> </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(() => { ...@@ -14,7 +14,7 @@ const currentMenu = computed(() => {
}) })
const currentSubmenu = 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[]) { function findMenu(path: string, menus: IMenuItem[]) {
...@@ -22,13 +22,13 @@ function findMenu(path: string, menus: IMenuItem[]) { ...@@ -22,13 +22,13 @@ function findMenu(path: string, menus: IMenuItem[]) {
if ( if (
item.children && item.children &&
item.children.find(item => { item.children.find(item => {
const regExp = new RegExp(`^${item.path}`) const regExp = new RegExp(`^${item.path.replaceAll('?', '\\?')}`)
return regExp.test(path) return regExp.test(path)
}) })
) { ) {
return item return item
} }
const regExp = new RegExp(`^${item.path}`) const regExp = new RegExp(`^${item.path.replaceAll('?', '\\?')}`)
return regExp.test(path) return regExp.test(path)
}) })
} }
...@@ -38,11 +38,7 @@ function findMenu(path: string, menus: IMenuItem[]) { ...@@ -38,11 +38,7 @@ function findMenu(path: string, menus: IMenuItem[]) {
<aside class="app-aside"> <aside class="app-aside">
<nav class="menu"> <nav class="menu">
<ul> <ul>
<li <li v-for="item in menus" :key="item.path" :class="{ 'is-active': item.path === currentMenu?.path }" v-permission="item.tag">
v-for="item in menus"
:key="item.path"
:class="{ 'is-active': item.path === currentMenu?.path }"
v-permission="item.tag">
<div class="menu-item"> <div class="menu-item">
<template v-if="item.children"> <template v-if="item.children">
<RouterLink :to="item.path"> <RouterLink :to="item.path">
...@@ -101,6 +97,7 @@ function findMenu(path: string, menus: IMenuItem[]) { ...@@ -101,6 +97,7 @@ function findMenu(path: string, menus: IMenuItem[]) {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
svg { svg {
width: 24px;
fill: var(--main-color); fill: var(--main-color);
} }
} }
...@@ -144,6 +141,7 @@ function findMenu(path: string, menus: IMenuItem[]) { ...@@ -144,6 +141,7 @@ function findMenu(path: string, menus: IMenuItem[]) {
fill: #9a9a9a; fill: #9a9a9a;
} }
.submenu-icon { .submenu-icon {
width: 18px;
margin-right: 10px; margin-right: 10px;
} }
&.is-active { &.is-active {
......
...@@ -4,7 +4,7 @@ export default { name: 'AppMain' } ...@@ -4,7 +4,7 @@ export default { name: 'AppMain' }
<template> <template>
<section class="app-main"> <section class="app-main">
<router-view></router-view> <router-view :key="$route.fullPath"></router-view>
</section> </section>
</template> </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' ...@@ -3,7 +3,8 @@ import type { TagRule } from '@/types'
import { PriceTag, Plus, CloseBold } from '@element-plus/icons-vue' import { PriceTag, Plus, CloseBold } from '@element-plus/icons-vue'
import { useTag } from '@/composables/useAllData' 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() const { tagList } = useTag()
...@@ -44,11 +45,7 @@ function handleRemove(items: string[], index: number) { ...@@ -44,11 +45,7 @@ function handleRemove(items: string[], index: number) {
标签 等于 标签 等于
<el-form-item> <el-form-item>
<el-select v-model="tagRule.items[index]"> <el-select v-model="tagRule.items[index]">
<el-option <el-option v-for="option in tagList" :key="option.id" :label="option.name" :value="option.id"></el-option>
v-for="option in tagList"
:key="option.id"
:label="option.name"
:value="option.id"></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
</div> </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' ...@@ -5,7 +5,8 @@ import { useUserAttr } from '@/composables/useAllData'
import { stringOperatorList, numberOperatorList, dateOperatorList } from '@/utils/dictionary' import { stringOperatorList, numberOperatorList, dateOperatorList } from '@/utils/dictionary'
import { searchMetaMemberAttrs } from '@/api/base' 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() const { userAttrList } = useUserAttr()
...@@ -107,17 +108,16 @@ function remoteMethod(item: RuleAttr, search: string = '') { ...@@ -107,17 +108,16 @@ function remoteMethod(item: RuleAttr, search: string = '') {
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-select v-model="item.operate" @change="value => handleOperateChange(value, 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-select>
</el-form-item> </el-form-item>
<el-form-item v-if="!['null', 'not null'].includes(item.operate)"> <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.start" type="date" value-format="YYYY-MM-DD" />
<el-date-picker v-model="item.value.end" type="date" value-format="YYYY-MM-DD" /> <el-date-picker v-model="item.value.end" type="date" value-format="YYYY-MM-DD" />
</template> </template>
...@@ -145,7 +145,12 @@ function remoteMethod(item: RuleAttr, search: string = '') { ...@@ -145,7 +145,12 @@ function remoteMethod(item: RuleAttr, search: string = '') {
v-if="['in', 'not in'].includes(item.operate)"> 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-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
</el-select> </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> </template>
</el-form-item> </el-form-item>
</div> </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 { export interface AttrType {
...@@ -36,60 +37,107 @@ export interface ConnectionType { ...@@ -36,60 +37,107 @@ export interface ConnectionType {
// 所有用户属性 // 所有用户属性
const userAttrList = ref<AttrType[]>([]) const userAttrList = ref<AttrType[]>([])
const userAttrLoading = ref(false)
export function useUserAttr() { export function useUserAttr() {
function fetchUserAttrList() { async function fetchUserAttrList() {
getMetaUserAttrList({ check_role: true }).then((res: any) => { if (userAttrLoading.value) return
userAttrLoading.value = true
await getMetaUserAttrList({ check_role: true }).then((res: any) => {
userAttrList.value = res.data.items userAttrList.value = res.data.items
}) })
userAttrLoading.value = false
} }
onMounted(() => { onMounted(() => {
if (!userAttrList.value?.length) fetchUserAttrList() if (!userAttrList.value?.length) fetchUserAttrList()
}) })
return { fetchUserAttrList, userAttrList } return { fetchUserAttrList, userAttrList, userAttrLoading }
} }
// 所有事件 // 所有事件
const metaEventList = ref<MetaEventType[]>([]) const metaEventList = ref<MetaEventType[]>([])
const metaEventLoading = ref(false)
export function useMetaEvent() { export function useMetaEvent() {
function fetchMetaEventList() { async function fetchMetaEventList() {
getMetaEventList({ check_role: true }).then((res: any) => { if (metaEventLoading.value) return
metaEventLoading.value = true
await getMetaEventList({ check_role: true }).then((res: any) => {
metaEventList.value = res.data.items metaEventList.value = res.data.items
}) })
metaEventLoading.value = false
} }
onMounted(() => { onMounted(() => {
if (!metaEventList.value?.length) fetchMetaEventList() if (!metaEventList.value?.length) fetchMetaEventList()
}) })
return { fetchMetaEventList, metaEventList } return { fetchMetaEventList, metaEventList, metaEventLoading }
} }
// 所有标签 // 所有标签
const tagList = ref<TagType[]>([]) const tagList = ref<TagType[]>([])
const tagLoading = ref(false)
export function useTag() { export function useTag() {
function fetchTagList() { async function fetchTagList() {
getTagList({ check_role: 1 }).then((res: any) => { if (tagLoading.value) return
tagLoading.value = true
await getTagList({ check_role: 1 }).then((res: any) => {
tagList.value = res.data.items tagList.value = res.data.items
}) })
tagLoading.value = false
} }
onMounted(() => { onMounted(() => {
if (!tagList.value?.length) fetchTagList() if (!tagList.value?.length) fetchTagList()
}) })
return { fetchTagList, tagList } return { fetchTagList, tagList, tagLoading }
} }
// 所有连接 // 所有连接
const connectionList = ref<ConnectionType[]>([]) const connectionList = ref<ConnectionType[]>([])
const connectionLoading = ref(false)
export function useConnection() { export function useConnection() {
function fetchConnectionList() { async function fetchConnectionList() {
getConnectionList().then((res: any) => { 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) => { 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 attrs = typeof item.config_attributes === 'string' ? JSON.parse(item.config_attributes) : item.config_attributes
const name = Array.isArray(attrs) ? attrs.find((item: any) => item.prop === 'name')?.value : attrs.name const name = Array.isArray(attrs) ? attrs.find((item: any) => item.prop === 'name')?.value : attrs.name
return { ...item, config_attributes: attrs, name } return { ...item, config_attributes: attrs, name, type_name: connection?.label || item.type }
}) })
}) })
connectionLoading.value = false
} }
onMounted(() => { onMounted(() => {
if (!connectionList.value?.length) fetchConnectionList() 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 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论