提交 db1e84fd authored 作者: lhh's avatar lhh

链接管理,学生端老师端开发

上级 4e3f32ad
......@@ -2,7 +2,13 @@
import type { EventRule, EventRuleItem, RuleAttr } from '@/types'
import { Operation, Plus, CloseBold } from '@element-plus/icons-vue'
import { useMetaEvent } from '@/composables/useAllData'
import { stringOperatorList, numberOperatorList, dateOperatorList, happenInfoList, triggerInfoList } from '@/utils/dictionary'
import {
stringOperatorList,
numberOperatorList,
dateOperatorList,
happenInfoList,
triggerInfoList
} from '@/utils/dictionary'
import { searchEventAttrs } from '@/api/base'
const { limit = Infinity } = defineProps<{ limit?: number }>()
......@@ -113,7 +119,12 @@ function handleTriggerOperateChange(value: string, item: EventRuleItem) {
function querySearch(rule: EventRuleItem, attr: RuleAttr, search: string, cb: (arg: any) => void) {
const found = getEventAttrList(rule.happen_info.event_id).find(item => item.id === attr.attr_id)
const experiment_meta_event_id = found?.experiment_meta_event_id || ''
searchEventAttrs({ search, experiment_meta_event_id: experiment_meta_event_id, experiment_meta_event_attr_id: attr.attr_id, per_page: 1000 }).then(res => {
searchEventAttrs({
search,
experiment_meta_event_id: experiment_meta_event_id,
experiment_meta_event_attr_id: attr.attr_id,
per_page: 1000
}).then(res => {
const list: string[] = []
res.data.list.forEach((item: any) => {
try {
......@@ -134,7 +145,12 @@ function remoteMethod(rule: EventRuleItem, attr: RuleAttr, search: string) {
const found = getEventAttrList(rule.happen_info.event_id).find(item => item.id === attr.attr_id)
const experiment_meta_event_id = found?.experiment_meta_event_id || ''
loading.value = true
searchEventAttrs({ search, experiment_meta_event_id: experiment_meta_event_id, experiment_meta_event_attr_id: attr.attr_id, per_page: 1000 }).then(res => {
searchEventAttrs({
search,
experiment_meta_event_id: experiment_meta_event_id,
experiment_meta_event_attr_id: attr.attr_id,
per_page: 1000
}).then(res => {
const list: string[] = []
res.data.list.forEach((item: any) => {
try {
......@@ -166,24 +182,47 @@ function remoteMethod(rule: EventRuleItem, attr: RuleAttr, search: string) {
<el-row>
<!-- 发生 -->
<el-form-item>
<el-select v-model="rule.happen_info.is_happened" @change="value => handleHappenOperateChange(value, rule)">
<el-select
v-model="rule.happen_info.is_happened"
@change="value => handleHappenOperateChange(value, rule)"
>
<el-option v-for="option in happenInfoList" :key="option.label" v-bind="option"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-select v-model="rule.happen_info.event_id" @change="value => handleEventChange(value, rule)">
<el-option v-for="option in currentMetaEventList" :key="option.id" :label="option.name" :value="option.id"></el-option>
<el-option
v-for="option in currentMetaEventList"
:key="option.id"
:label="option.name"
:value="option.id"
></el-option>
</el-select>
</el-form-item>
<el-button text :icon="Plus" @click="handleAttrAdd(rule.happen_info.attr_list)">添加条件</el-button>
<el-button text :icon="CloseBold" @click="handleRemove(eventAttrRule.items, index)" v-if="limit !== 1"></el-button>
<el-button
text
:icon="CloseBold"
@click="handleRemove(eventAttrRule.items, index)"
v-if="limit !== 1"
></el-button>
</el-row>
<!-- 属性条件 -->
<el-row justify="space-between" class="rule-item" v-for="(attr, index) in rule.happen_info.attr_list" :key="index">
<el-row
justify="space-between"
class="rule-item"
v-for="(attr, index) in rule.happen_info.attr_list"
:key="index"
>
<div>
<el-form-item>
<el-select v-model="attr.attr_id" @change="value => handleAttrChange(value, attr, rule)">
<el-option v-for="option in getEventAttrList(rule.happen_info.event_id)" :key="option.id" :label="option.name" :value="option.id"></el-option>
<el-option
v-for="option in getEventAttrList(rule.happen_info.event_id)"
:key="option.id"
:label="option.name"
:value="option.id"
></el-option>
</el-select>
</el-form-item>
<el-form-item>
......@@ -192,7 +231,8 @@ function remoteMethod(rule: EventRuleItem, attr: RuleAttr, search: string) {
v-for="option in getOperatorList(attr.attr_type)"
:key="option.value"
:label="option.alias || option.label"
:value="option.value"></el-option>
:value="option.value"
></el-option>
</el-select>
</el-form-item>
<el-form-item v-if="!['null', 'not null'].includes(attr.operate)">
......@@ -208,14 +248,29 @@ function remoteMethod(rule: EventRuleItem, attr: RuleAttr, search: string) {
</template>
<!-- 时间区间 -->
<template v-else-if="attr.attr_type === '5' && attr.operate === 'range'">
<el-date-picker v-model="attr.value.start" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" style="width: 180px" />
<el-date-picker v-model="attr.value.end" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" style="width: 180px" />
<el-date-picker
v-model="attr.value.start"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
style="width: 180px"
/>
<el-date-picker
v-model="attr.value.end"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
style="width: 180px"
/>
</template>
<template v-else-if="attr.attr_type === '4' && (attr.operate === 'after' || attr.operate === 'before')">
<el-date-picker v-model="attr.value" type="date" value-format="YYYY-MM-DD" />
</template>
<template v-else-if="attr.attr_type === '5' && (attr.operate === 'after' || attr.operate === 'before')">
<el-date-picker v-model="attr.value" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" style="width: 180px" />
<el-date-picker
v-model="attr.value"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
style="width: 180px"
/>
</template>
<template v-else>
<el-select
......@@ -227,10 +282,16 @@ function remoteMethod(rule: EventRuleItem, attr: RuleAttr, search: string) {
:remote-method="(query:string) => remoteMethod(rule, attr, query)"
:loading="loading"
style="width: 320px"
v-if="['in', 'not in'].includes(attr.operate)">
v-if="['in', 'not in'].includes(attr.operate)"
>
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<el-autocomplete v-model="attr.value" :fetch-suggestions="(query, cb) => querySearch(rule, attr, query, cb)" style="width: 320px" v-else />
<el-autocomplete
v-model="attr.value"
:fetch-suggestions="(query, cb) => querySearch(rule, attr, query, cb)"
style="width: 320px"
v-else
/>
</template>
</el-form-item>
</div>
......@@ -245,7 +306,12 @@ function remoteMethod(rule: EventRuleItem, attr: RuleAttr, search: string) {
</el-form-item>
<el-form-item>
<el-select v-model="rule.trigger_info.operate" @change="value => handleTriggerOperateChange(value, rule)">
<el-option v-for="option in triggerNumberOperatorList" :key="option.value" :label="option.alias || option.label" :value="option.value" />
<el-option
v-for="option in triggerNumberOperatorList"
:key="option.value"
:label="option.alias || option.label"
:value="option.value"
/>
</el-select>
</el-form-item>
<el-form-item>
......@@ -255,7 +321,9 @@ function remoteMethod(rule: EventRuleItem, attr: RuleAttr, search: string) {
</section>
</div>
</div>
<el-button text :icon="Plus" @click="handleAdd(eventAttrRule.items)" v-if="eventAttrRule.items.length < limit">添加条件</el-button>
<el-button text :icon="Plus" @click="handleAdd(eventAttrRule.items)" v-if="eventAttrRule.items.length < limit"
>添加条件</el-button
>
<slot name="footer"></slot>
</el-card>
</template>
......
......@@ -54,3 +54,82 @@ export function asyncOfficialAccountInfo(params: { connection_id: string; appid:
export function asyncOfficialAccountUsers(params: { connection_id: string; appid: string }) {
return httpRequest.get('/api/lab/v1/experiment/wechat-platform/async-official-account-users', { params })
}
// 获取生成用户数据
export function getScheduleMember(params: {
experiment_id: string; connect_id
: string
}) {
return httpRequest.get('/api/lab/v1/experiment/connection/schedule-member', { params })
}
// 生成用户数据保存
interface ScheduleMember {
experiment_id: string
connect_id: string
size: number
cover_type: number
name: number
name_value?: string
status: number
gender: number
mobile: number
create_data: string
}
export function submitScheduleMember(data: ScheduleMember) {
return httpRequest.post('/api/lab/v1/experiment/connection/schedule-member', data)
}
// 获取生成事件数据
export function getScheduleEvent(params: {
experiment_id: string; connect_id
: string;
event_id: string
}) {
return httpRequest.get('/api/lab/v1/experiment/connection/schedule-event', { params })
}
// 生成用户数据保存
interface ScheduleEvent {
experiment_id: string
connect_id: string
event_id: number
member_rate: number
size: number
cover_type: number
create_data: string
other_fields: string
}
export function submitScheduleEvent(data: ScheduleEvent) {
return httpRequest.post('/api/lab/v1/experiment/connection/schedule-event', data)
}
// 获取数据进度
export function getScheduleList(params: {
experiment_id: string
}) {
return httpRequest.get('/api/lab/v1/experiment/connection/schedule-list', { params })
}
// 删除进度
export function scheduleDelete(data: { id: string }) {
return httpRequest.post('/api/lab/v1/experiment/connection/schedule-delete', data)
}
// 用户触达信息
export function getStudentFollow(params: {
experiment_id: string
experiment_connection_id: string
}) {
return httpRequest.get('/api/lab/v1/experiment/connection/student-follow', { params })
}
// 用户触达提交表单信息
export function studentSubmitForm(data: { experiment_connection_id: any; gender: string; name: string; mobile: string }) {
return httpRequest.post('/api/lab/v1/experiment/connection/student-submit-form', data)
}
// 用户触达提交聊天信息
export function studentSubmitLog(data: { experiment_connection_id: any; logs: string }) {
return httpRequest.post('/api/lab/v1/experiment/connection/student-submit-log', data)
}
<script setup lang="ts">
import { getScheduleList, scheduleDelete } from '../api'
import { ElMessage } from 'element-plus'
const appList = $ref<InstanceType<typeof AppList> | null>(null)
const listOptions = computed(() => {
return {
remote: {
httpRequest: getScheduleList
},
columns: [
{ label: '数据属性', prop: 'type_name' },
{ label: '来源链接', prop: 'connect_name' },
{ label: '数据量', prop: 'size' },
{
label: '状态',
prop: 'status_name',
computed: (row: any) => {
let sName = row.row.status_name
if (row.row.message !== '') {
sName = `<span>${row.row.status_name}</span><br/><span style="margin-top:10px">${row.row.message}</span>`
}
return sName
}
},
{ label: '生成时间', prop: 'created_time' },
{ label: '操作', slots: 'table-x' }
]
}
})
const handleDelete = function (item: any) {
scheduleDelete({ id: item.id }).then(res => {
if (res.data) {
appList?.refetch()
ElMessage({
message: '删除成功',
type: 'success'
})
}
})
}
</script>
<template>
<el-dialog
style="width: 800px"
class="data-form"
title="数据生成进度"
:close-on-click-modal="false"
@update:modelValue="$emit('update:modelValue')"
>
<AppList v-bind="listOptions" ref="appList">
<template #table-x="{ row }">
<el-button type="primary" plain @click="handleDelete(row)" v-permission="'v1-experiment-member-delete'"
>删除</el-button
>
</template></AppList
>
</el-dialog>
</template>
<style lang="scss">
.connect-form {
.el-dialog__body {
padding-top: 10px;
}
}
.button-flex {
// margin-top: 40px;
display: flex;
justify-content: center;
}
</style>
<script setup lang="ts">
import { ElMessage } from 'element-plus'
import { submitScheduleEvent, getScheduleEvent } from '../api'
import type { ScheduleEvent } from '../types'
import type { FormInstance } from 'element-plus'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const OtherFields = defineAsyncComponent(() => import('../components/OtherFields.vue'))
const props = defineProps<{ data?: ScheduleEvent }>()
const emit = defineEmits<{
(e: 'update'): void
(e: 'update:modelValue', visible: boolean): void
}>()
// const formSize = ref('default')
const ruleFormRef = ref<FormInstance>()
let ruleForm = $ref<any>({
experiment_id: props.data?.experiment_id,
connect_id: props.data?.connect_id,
event_id: '',
member_rate: '',
size: '',
cover_type: 1
})
let fieldsValue = $ref<any[]>([])
const submitForm = async (formEl: FormInstance | undefined, bl: string) => {
if (!formEl) return
await formEl.validate((valid, fields) => {
if (valid) {
ruleForm.create_data = bl
ruleForm.other_fields = JSON.stringify(
fieldsValue.reduce((a: any, b: any) => {
a[b.id] = {
type: b.rule?.type,
fixed_value: b.rule?.fixed_value,
rand_value: b.rule?.rand_value
}
return a
}, {})
)
submitScheduleEvent(ruleForm).then((res: any) => {
if (res.code === 0) {
ElMessage({
message: '保存成功',
type: 'success'
})
emit('update:modelValue', false)
} else {
ElMessage({
message: res.message
})
}
})
} else {
console.log('error submit!', fields)
}
})
}
const rules = [{ required: true }]
onMounted(() => {
if (props.data?.last_event !== '') {
eventChange(props.data?.last_event)
}
})
// 用户事件改变的时候
const eventChange = function (eId?: string) {
getScheduleEvent({
experiment_id: ruleForm.experiment_id,
connect_id: ruleForm.connect_id,
event_id: eId || ruleForm.event_id
}).then(res => {
if (res.data) {
mergeJson(ruleForm, res.data)
fieldsValue = res.data.other_fields.map((item: any) => {
if (item?.rule) {
item.rule.type = parseInt(item.rule.type)
}
return item
})
}
})
}
const mergeJson = function (target: any, source: any) {
if (source) {
Object.keys(target).forEach(function (key) {
if (source.hasOwnProperty(key)) {
target[key] = source[key]
}
})
}
}
</script>
<template>
<el-dialog
style="width: 800px"
class="data-form"
title="自动生成用户事件数据"
:close-on-click-modal="false"
@update:modelValue="$emit('update:modelValue')"
>
<div class="button-flex">
<el-form
:disabled="userStore.role?.id === 1"
label-suffix=":"
ref="ruleFormRef"
:model="ruleForm"
label-width="auto"
class="demo-ruleForm"
status-icon
>
<el-form :model="ruleForm" label-suffix=":">
<el-form-item label="用户事件" :rules="rules" label-width="205">
<el-select @change="eventChange" v-model="ruleForm.event_id" placeholder="请选择">
<el-option v-for="item in props.data?.events" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
</el-form>
<el-form-item label="请输入随机调取用户比例" :rules="rules">
<el-input v-model="ruleForm.member_rate" placeholder="请输入">
<template #append>%</template>
</el-input>
</el-form-item>
<el-form-item label="请输入每个用户生成事件数" :rules="rules">
<el-input v-model="ruleForm.size" placeholder="请输入"> </el-input>
</el-form-item>
<el-form-item label="请选择数据覆盖形式" :rules="rules">
<el-radio-group v-model="ruleForm.cover_type">
<el-radio :label="1">全新覆盖</el-radio>
<el-radio :label="2">追加</el-radio>
</el-radio-group>
</el-form-item>
<el-divider />
<OtherFields :data="fieldsValue || []"></OtherFields>
<el-form :model="ruleForm">
<el-form-item style="justify-content: center">
<div style="justify-content: center; display: flex; width: 100%">
<el-button @click="$emit('update:modelValue', false)">取消</el-button>
<el-button type="primary" @click="submitForm(ruleFormRef, 'false')"> 保存 </el-button>
<el-button type="primary" @click="submitForm(ruleFormRef, 'true')"> 提交生成数据 </el-button>
</div>
</el-form-item>
</el-form>
</el-form>
</div>
</el-dialog>
</template>
<style lang="scss">
.connect-form {
.el-dialog__body {
padding-top: 10px;
}
}
.button-flex {
// margin-top: 40px;
display: flex;
justify-content: center;
}
</style>
<script setup lang="ts">
import { Delete, Edit } from '@element-plus/icons-vue'
import { Delete, Edit, MoreFilled, EditPen, User, Avatar, PieChart, UserFilled } from '@element-plus/icons-vue'
import { ElMessageBox, ElMessage } from 'element-plus'
import Icon from '@/components/ConnectionIcon.vue'
import { deleteConnection } from '../api'
import { useUserStore } from '@/stores/user'
const props = defineProps<{ data: { id: string; type_name: string; type: string; config_attributes: any } }>()
const userStore = useUserStore()
const props = defineProps<{
data: {
experiment_id: string
id: string
type_name: string
type: string
config_attributes: any
events: any
last_event: string
}
}>()
const router = useRouter()
const emits = defineEmits(['update', 'edit'])
const emits = defineEmits([
'update',
'edit',
'generateUserData',
'generateEventData',
'viewDataProgress',
'handleStudentFollow'
])
// 删除
function handleRemove() {
......@@ -31,38 +51,102 @@ const iconMap: Record<string, string> = {
'13': '99',
'14': '100'
}
const generateUserData = function () {
emits('generateUserData', props.data.experiment_id, props.data.id, props.data.type_name)
}
const generateEventData = function () {
emits('generateEventData', props.data.experiment_id, props.data.id, props.data.events, props.data.last_event)
}
const viewDataProgress = function () {
emits('viewDataProgress', props.data.experiment_id)
}
const handleStudentFollow = function () {
emits('handleStudentFollow', props.data.experiment_id, props.data.id, props.data.type)
}
</script>
<template>
<div class="connect-item" @click="routerView">
<div class="connect-item__edit">
<!-- <img @click="edit" src="https://webapp-pub.ezijing.com/pages/assa/dml_edit.png" /> -->
<el-icon size="20" color="#333" @click.stop="edit"><Edit /></el-icon>
</div>
<div class="connect-item__remove" @click.stop="handleRemove">
<!-- <img src="https://webapp-pub.ezijing.com/pages/assa/dml_delete.png" /> -->
<el-icon size="20" color="#333"><Delete /></el-icon>
<div class="connect-item_top">
<!-- <div class="connect-item__edit">
<img @click="edit" src="https://webapp-pub.ezijing.com/pages/assa/dml_edit.png" />
<el-icon size="20" color="#333" @click.stop="edit"><Edit /></el-icon>
</div>
<div class="connect-item__remove" @click.stop="handleRemove">
<img src="https://webapp-pub.ezijing.com/pages/assa/dml_delete.png" />
<el-icon size="20" color="#333"><Delete /></el-icon>
</div> -->
<div class="connect-item__icon">
<Icon w="40" h="40" :multiColor="true" class="svg" :name="iconMap[data.type] || data.type"></Icon>
</div>
</div>
<div class="connect-item__icon">
<Icon w="40" h="40" :multiColor="true" class="svg" :name="iconMap[data.type] || data.type"></Icon>
<div class="connect-item_bottom">
<p>{{ data.type === '12' ? data.config_attributes[0].value : data.type_name }}</p>
<el-popover popper-class="popper">
<template #reference>
<el-icon size="14" color="#333"><MoreFilled /></el-icon>
</template>
<template #default>
<ul class="connect-item_tool">
<li @click.stop="handleStudentFollow" v-if="userStore.role?.id === 1">
<el-icon size="16" color="#000"><UserFilled /></el-icon>
<span>用户触达</span>
</li>
<li @click.stop="edit" v-if="userStore.role?.id !== 1">
<el-icon size="16" color="#000"><EditPen /></el-icon>
<span>编辑</span>
</li>
<li @click.stop="generateUserData">
<el-icon size="16" color="#000"><User /></el-icon>
<span>自动生成用户数据</span>
</li>
<li @click.stop="generateEventData">
<el-icon size="16" color="#000" @click.stop="edit"><Avatar /></el-icon>
<span>自动生成事件数据</span>
</li>
<li @click.top="viewDataProgress">
<el-icon size="16" color="#000" @click.stop="edit"><PieChart /></el-icon>
<span>数据生成进度</span>
</li>
<li @click.stop="handleRemove" v-if="userStore.role?.id !== 1">
<el-icon size="16" color="#000" @click.stop="edit"><Delete /></el-icon>
<span>删除</span>
</li>
</ul>
</template>
</el-popover>
</div>
<p>
{{ data.type === '12' ? data.config_attributes[0].value : data.type_name }}
</p>
</div>
</template>
<style lang="scss">
.connect-item {
position: relative;
height: 124px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #fff;
border: 1px dashed #bbb;
background-color: #f3f3f3;
border: 1px solid #f3f3f3;
border-radius: 5px;
overflow: hidden;
cursor: pointer;
.connect-item_bottom {
height: 40px;
background-color: #fff;
display: flex;
// flex-direction: column;
align-items: center;
justify-content: space-between;
padding: 0 20px;
}
.connect-item_top {
height: 124px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
&:hover {
box-shadow: rgb(0 0 0 / 40%) 0px 2px 6px 0px;
.connect-item__remove {
......@@ -73,8 +157,8 @@ const iconMap: Record<string, string> = {
}
}
p {
font-size: 18px;
margin-top: 8px;
font-size: 12px;
// margin-top: 8px;
}
}
.connect-item__remove {
......@@ -91,4 +175,32 @@ const iconMap: Record<string, string> = {
top: 8px;
z-index: 999;
}
.connect-item_tool {
li {
display: flex;
}
}
.popper {
padding: 20px 0 !important;
width: fit-content !important;
min-width: fit-content !important;
border-radius: 10px !important;
ul {
li {
display: flex;
height: 35px;
align-items: center;
padding: 0 15px;
&:hover {
background-color: #f1f1f1;
}
cursor: pointer;
span {
margin-left: 8px;
font-size: 12px;
line-height: 100%;
}
}
}
}
</style>
<script setup lang="ts">
import { ElMessage, ElMessageBox } from 'element-plus'
import type { OtherFields } from '../types'
import { RemoveFilled, CirclePlusFilled } from '@element-plus/icons-vue'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const props = defineProps<{ data?: OtherFields[] }>()
const fieldsType: any = {
1: '字符串',
2: '整数',
3: '数字',
4: '日期',
5: '时间'
}
const ruleTips: any = {
1: '最大字符数',
2: '最大位数',
3: '小数点后位数',
4: '格式',
5: '格式'
}
const add = function (item: any) {
item.push({ min: '', max: '', value: '', rate: '' })
}
const remove = function (item: any, index: number) {
item.splice(index, 1)
}
</script>
<template>
<el-form-item v-for="item in props.data" :label="item.name">
<div>
<el-radio-group v-model="item.rule.type">
<el-radio :label="2">指定随机</el-radio>
<el-radio :label="1">固定</el-radio>
</el-radio-group>
<p class="specify-tips">
字段类型:{{ fieldsType[item.type] }}&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{{ ruleTips[item.type] }}{{
item.format
}}
</p>
<div class="specify" v-if="item.rule.type === 2">
<div class="specify-item" v-for="(specify, index) in item.rule.rand_value">
<!-- 字符串 -->
<template v-if="item.type === 1">
<el-input v-model="specify.value" placeholder="请输入"></el-input>
<el-input v-model="specify.rate" placeholder="请输入随机几率" style="margin-left: 15px"></el-input>
&nbsp;&nbsp;%
</template>
<!-- 整数 -->
<template v-if="item.type === 2">
<el-input v-model="specify.min" placeholder="请输入随机整数最小值"></el-input>
<el-input v-model="specify.max" placeholder="请输入随机整数最大值" style="margin-left: 15px"></el-input>
</template>
<!-- 数字 -->
<template v-if="item.type === 3">
<el-input v-model="specify.min" placeholder="请输入随机数字最小值"></el-input>
<el-input v-model="specify.max" placeholder="请输入随机数字最大值" style="margin-left: 15px"></el-input>
</template>
<!-- 日期 -->
<template v-if="item.type === 4">
<el-date-picker
placeholder="请选择随机日期最小值"
style="width: 200px"
value-format="YYYY-MM-DD"
v-model="specify.min"
/>
<el-date-picker
placeholder="请选择随机日期最大值"
style="width: 200px; margin-left: 15px"
value-format="YYYY-MM-DD"
v-model="specify.max"
/>
</template>
<template v-if="item.type === 5">
<el-date-picker
placeholder="请选择随机时间最小值"
v-model="specify.min"
style="width: 200px"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
/>
<el-date-picker
placeholder="请选择随机时间最大值"
v-model="specify.max"
style="width: 200px; margin-left: 15px"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</template>
<template v-if="userStore.role?.id !== 1">
<el-icon
v-if="item.rule.rand_value.length - 1 === index || item.rule.rand_value.length === 0"
@click="add(item.rule.rand_value)"
size="20"
style="margin-left: 10px; cursor: pointer"
><CirclePlusFilled
/></el-icon>
<el-icon
@click="remove(item.rule.rand_value, index)"
size="20"
style="margin-left: 10px; cursor: pointer"
v-if="item.rule.rand_value.length > 1"
><RemoveFilled
/></el-icon>
</template>
</div>
<!-- <el-icon
v-if="item.rule.rand_value.length === 0"
@click="add(item.rule.rand_value)"
size="20"
style="margin-left: 10px; cursor: pointer"
><CirclePlusFilled
/></el-icon> -->
</div>
<div v-else>
<template v-if="item.type === 4">
<el-date-picker
placeholder="请选择固定属性值"
style="width: 200px"
value-format="YYYY-MM-DD"
v-model="item.rule.fixed_value"
/>
</template>
<template v-else-if="item.type === 5">
<el-date-picker
placeholder="请选择固定属性值"
v-model="item.rule.fixed_value"
style="width: 200px"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</template>
<template v-else>
<el-input v-model="item.rule.fixed_value" placeholder="请输入固定属性值"></el-input>
</template>
</div>
</div>
</el-form-item>
</template>
<style lang="scss">
.connect-form {
.el-dialog__body {
padding-top: 10px;
}
}
.button-flex {
// margin-top: 40px;
display: flex;
justify-content: center;
}
.specify-item {
display: flex;
align-items: center;
margin-bottom: 10px;
.el-input {
width: 200px;
}
}
.specify-tips {
line-height: 100%;
font-size: 12px;
color: #ccc;
margin-bottom: 10px;
}
</style>
<script setup lang="ts">
import { studentSubmitForm, studentSubmitLog } from '../api'
import { ElMessage } from 'element-plus'
import type { StudentFollow } from '../types'
import type { FormInstance, FormRules } from 'element-plus'
import Icon from '@/components/ConnectionIcon.vue'
import { Check } from '@element-plus/icons-vue'
const props = defineProps<{ data?: StudentFollow }>()
let step = $ref(1)
let logs = $ref<any>([])
watchEffect(() => {
if (props.data?.follow_flag) {
step = 3
}
if (props.data?.data) {
if (props.data?.data?.logs && props.data?.data?.logs !== '') {
console.log(props.data?.data?.logs, '11')
logs = JSON.parse(props.data?.data?.logs)
}
}
})
interface RuleForm {
name: string
mobile: string
gender: string
experiment_connection_id: any
}
const ruleFormRef = ref<FormInstance>()
const ruleForm = reactive<RuleForm>({
name: '',
mobile: '',
gender: '1',
experiment_connection_id: ''
})
const rules = reactive<FormRules<RuleForm>>({
name: [{ required: true, message: '请输入', trigger: 'blur' }],
mobile: [{ required: true, message: '请输入', trigger: 'blur' }],
gender: [{ required: true, message: '请输入', trigger: 'blur' }]
})
const submitForm = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid, fields) => {
if (valid) {
ruleForm.experiment_connection_id = props.data?.connect_id
studentSubmitForm(ruleForm).then((res: any) => {
if (res.code === 0) {
step = 3
}
})
} else {
console.log('error submit!', fields)
}
})
}
let textarea = $ref('')
const handleKeyUp = function (e: any) {
if (e.key === 'Enter' && textarea !== '') {
const currentDate = new Date()
const currentDateTime = currentDate.toLocaleString()
console.log(currentDateTime)
logs.push({ time: currentDateTime, text: textarea })
studentSubmitLog({
experiment_connection_id: props.data?.connect_id,
logs: JSON.stringify([{ time: currentDateTime, text: textarea }])
}).then()
textarea = ''
setTimeout(() => {
scrollToBottom()
}, 10)
}
}
const scrollToBottom = function () {
const scrollContainer: any = document.getElementById('scrollContainer')
if (scrollContainer) {
scrollContainer.scrollTop = 10000
}
}
const sendChat = function () {
step = 4
setTimeout(() => {
scrollToBottom()
}, 10)
}
</script>
<template>
<el-dialog
class="data-form"
title="关注"
:close-on-click-modal="false"
:style="`width: fit-content; ${step === 4 ? 'background-color: #f1f1f1;' : 'background-color: #fff;'}`"
@update:modelValue="$emit('update:modelValue')"
>
<div class="step1" v-if="step === 1">
<div class="connect-item__icon">
<Icon w="60" h="60" :multiColor="true" class="svg" :name="props.data?.type || '1'"></Icon>
</div>
<p>紫荆教育EDU</p>
<div class="step1-btn">
<el-button type="success" style="width: 100px" color="rgb(25, 170, 32)" @click="step = 2">关注</el-button>
</div>
</div>
<div class="step2" v-if="step === 2">
<el-form
ref="ruleFormRef"
style="width: 400px"
:model="ruleForm"
:rules="rules"
label-width="auto"
class="demo-ruleForm"
status-icon
center
>
<p style="color: #ccc; font-size: 12px; text-align: center; margin-bottom: 20px">
注:填写信息保存之后,关注成功
</p>
<el-form-item label="性别" prop="gender">
<el-radio-group v-model="ruleForm.gender">
<el-radio label="1"></el-radio>
<el-radio label="2"></el-radio>
<el-radio label="0">未知</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="手机号" prop="mobile">
<el-input v-model="ruleForm.mobile" placeholder="请输入" />
</el-form-item>
<el-form-item label="姓名" prop="name">
<el-input v-model="ruleForm.name" placeholder="请输入" />
</el-form-item>
<el-form-item>
<div style="display: flex; justify-content: center; width: 100%">
<el-button type="primary" @click="submitForm(ruleFormRef)"> 保存 </el-button>
</div>
</el-form-item>
</el-form>
</div>
<div class="step3" v-if="step === 3">
<div class="step3-t">
<Icon w="20" h="20" :multiColor="true" class="svg" :name="props.data?.type || '1'"></Icon>
<div class="e-name">紫荆教育EDU</div>
</div>
<div class="step3-btn">
<el-button disabled :icon="Check" type="primary"> 已关注 </el-button>
<el-button type="primary" @click="sendChat"> 发消息 </el-button>
</div>
</div>
<div class="step4" v-if="step === 4">
<div class="chat-box">
<div class="chat-view" id="scrollContainer">
<div class="chat-view_left">
<!-- <div class="view__time">16:29</div> -->
<div class="view-left_content">
<div class="view-left__avatar">
<img src="https://webapp-pub.oss-cn-beijing.aliyuncs.com/highway/h5/banner-0420.png" />
</div>
<div class="view-left__text">
欢迎关注“清控紫荆EDU”<br /><br />
紫荆教育以教育为本、坚持国际化品质标准,通过科技创新、专业创新,为企业、院校及个人学习者,提供国际在线学位教育、国际留学、数字经济产业学院整体解决方案等产品服务,为各行各业培养和输送高质量的国际化和产业化人才。
</div>
</div>
</div>
<div class="chat-view_right" v-for="item in logs">
<div class="view__time">{{ item.time }}</div>
<div class="view-right_content">
<div class="view-right__text">{{ item.text }}</div>
<div class="view-right__avatar">
<img src="https://webapp-pub.oss-cn-beijing.aliyuncs.com/highway/h5/banner-0420.png" />
</div>
</div>
</div>
</div>
<div class="chat-input">
<el-input @keyup="handleKeyUp" v-model="textarea" style="height: 100%" placeholder="请输入" type="textarea" />
</div>
</div>
</div>
</el-dialog>
</template>
<style lang="scss">
.data-form {
// background-color: #f1f1f1;
.el-dialog__body {
padding: 0;
}
}
.button-flex {
// margin-top: 40px;
display: flex;
justify-content: center;
}
.step1 {
padding: 20px;
width: 300px;
.connect-item__icon {
display: flex;
justify-content: center;
padding-top: 20px;
}
p {
text-align: center;
font-size: 18px;
color: #333;
}
.step1-btn {
padding: 100px 0 50px;
text-align: center;
}
}
.step2 {
padding: 20px;
}
.step3 {
padding: 20px;
width: 300px;
.step3-t {
display: flex;
justify-content: center;
.e-name {
margin-left: 5px;
}
}
.step3-btn {
padding-top: 20px;
text-align: center;
}
}
.step4 {
width: 550px;
.chat-view {
height: 300px;
// background-color: #000;
border-bottom: 1px solid #ccc;
overflow-y: scroll;
padding-bottom: 30px;
box-sizing: border-box;
.view__time {
color: #ccc;
text-align: center;
font-size: 12px;
padding-top: 20px;
}
.chat-view_left {
padding-left: 20px;
.view-left_content {
margin-top: 10px;
display: flex;
img {
width: 30px;
height: 30px;
border-radius: 3px;
}
.view-left__text {
background-color: #fff;
margin-left: 10px;
padding: 10px;
border-radius: 3px;
max-width: 300px;
}
}
}
.chat-view_right {
padding-right: 20px;
.view-right_content {
margin-top: 10px;
display: flex;
justify-content: right;
img {
width: 30px;
height: 30px;
border-radius: 3px;
}
.view-right__text {
background-color: #b7e687;
margin-right: 10px;
padding: 10px;
border-radius: 3px;
max-width: 300px;
}
}
}
}
.chat-input {
height: 150px;
padding-top: 5px;
box-sizing: border-box;
.el-textarea__inner {
height: 100%;
background-color: rgba(0, 0, 0, 0);
border: none;
resize: none;
box-shadow: none;
&:focus {
box-shadow: none;
}
}
}
}
</style>
<script setup lang="ts">
import { ElMessage, ElMessageBox } from 'element-plus'
import { submitScheduleMember } from '../api'
import type { ScheduleMember } from '../types'
import type { FormInstance } from 'element-plus'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const OtherFields = defineAsyncComponent(() => import('../components/OtherFields.vue'))
const props = defineProps<{ data?: ScheduleMember }>()
const emit = defineEmits<{
(e: 'update'): void
(e: 'update:modelValue', visible: boolean): void
}>()
// const formSize = ref('default')
const ruleFormRef = ref<FormInstance>()
let ruleForm = $ref<any>({
experiment_id: '',
connect_id: '',
size: 1000,
cover_type: 1,
name: 1,
name_value: '',
status: 1,
gender: 1,
mobile: 1,
create_data: '',
type_name: ''
})
watchEffect(() => {
const mergeJson = function (target: any, source: any) {
if (source) {
Object.keys(target).forEach(function (key) {
if (source.hasOwnProperty(key)) {
if (key !== 'other_fields') {
target[key] = source[key]
}
}
})
}
}
mergeJson(ruleForm, props.data)
})
const submitForm = async (formEl: FormInstance | undefined, bl: string) => {
if (!formEl) return
await formEl.validate((valid, fields) => {
if (valid) {
ruleForm.create_data = bl
if (props.data?.other_fields) {
ruleForm.other_fields = JSON.stringify(
props.data?.other_fields.reduce((a: any, b: any) => {
a[b.id] = { type: b.rule.type, fixed_value: b.rule.fixed_value, rand_value: b.rule.rand_value }
return a
}, {})
)
}
submitScheduleMember(ruleForm).then(res => {
if (res.data) {
ElMessage({
message: '保存成功',
type: 'success'
})
emit('update:modelValue', false)
}
})
} else {
console.log('error submit!', fields)
}
})
}
const resetForm = (formEl: FormInstance | undefined) => {
if (!formEl) return
formEl.resetFields()
}
const options = Array.from({ length: 10000 }).map((_, idx) => ({
value: `${idx + 1}`,
label: `${idx + 1}`
}))
const rules = [{ required: true }]
</script>
<template>
<el-dialog
style="width: 800px"
class="data-form"
title="自动生成用户数据"
:close-on-click-modal="false"
@update:modelValue="$emit('update:modelValue')"
>
<div class="button-flex">
<el-form
:disabled="userStore.role?.id === 1"
label-suffix=":"
ref="ruleFormRef"
:model="ruleForm"
label-width="auto"
class="demo-ruleForm"
status-icon
>
<el-form-item label="请输入需要生成的数据量" :rules="rules">
<el-radio-group v-model="ruleForm.size">
<el-radio :label="1000">1000</el-radio>
<el-radio :label="3000">3000</el-radio>
<el-radio :label="5000">5000</el-radio>
<el-radio :label="10000">10000</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="请选择数据覆盖形式:" :rules="rules">
<el-radio-group v-model="ruleForm.cover_type">
<el-radio :label="1">全新覆盖</el-radio>
<el-radio :label="2">去重追加</el-radio>
</el-radio-group>
</el-form-item>
<el-divider />
<el-form-item label="来源链接" :rules="rules">
{{ ruleForm.type_name }}
</el-form-item>
<el-form-item label="姓名" :rules="rules">
<el-radio-group v-model="ruleForm.name">
<el-radio :label="1">随机</el-radio>
<el-radio :label="2">固定</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="ruleForm.name === 2" label="固定姓名" :rules="rules">
<el-input v-model="ruleForm.name_value" placeholder="请输入固定姓名"></el-input>
</el-form-item>
<el-form-item label="状态" :rules="rules">
<el-radio-group v-model="ruleForm.status">
<el-radio :label="1">生效</el-radio>
<el-radio :label="2">失效</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="性别" :rules="rules">
<el-radio-group v-model="ruleForm.gender">
<el-radio :label="1">随机</el-radio>
<el-radio :label="2"></el-radio>
<el-radio :label="3"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="手机号吗" :rules="rules">
<el-radio-group v-model="ruleForm.mobile">
<el-radio :label="1">随机</el-radio>
</el-radio-group>
</el-form-item>
<OtherFields :data="props.data?.other_fields || []"></OtherFields>
<el-form :model="ruleForm">
<el-form-item style="justify-content: center">
<div style="justify-content: center; display: flex; width: 100%">
<el-button @click="$emit('update:modelValue', false)">取消</el-button>
<el-button type="primary" @click="submitForm(ruleFormRef, 'false')"> 保存 </el-button>
<el-button type="primary" @click="submitForm(ruleFormRef, 'true')"> 提交生成数据 </el-button>
</div>
</el-form-item>
</el-form>
</el-form>
</div>
</el-dialog>
</template>
<style lang="scss">
.connect-form {
.el-dialog__body {
padding-top: 10px;
}
}
.button-flex {
// margin-top: 40px;
display: flex;
justify-content: center;
}
</style>
......@@ -24,3 +24,46 @@ export interface PlatformItem {
config_attributes?: ConfigAttribute[]
onBeforeNext?: (index: number, data: PlatformItem) => Promise<boolean> | boolean
}
export interface ScheduleMember {
experiment_id?: string
connect_id?: string
type_name: string
size: number
cover_type: number
name: number
name_value: string
status: number
gender: number
mobile: number
connect_name: string
other_fields?: OtherFields[]
}
export interface ScheduleEvent {
experiment_id?: string
connect_id?: string
member_rate: string
size: number
cover_type: number
event_name: string
other_fields?: OtherFields[]
events?: any[]
last_event: string
}
export interface OtherFields {
id: string
name: string
type: number
format: string
rule: any
}
export interface StudentFollow{
follow_flag: string
logs: any[] | undefined
connect_id: string
type: string
data: any
}
\ No newline at end of file
<script setup lang="ts">
import type { DetailsProp } from '../types'
import type { DetailsProp, ScheduleMember, ScheduleEvent, StudentFollow } from '../types'
import { Plus } from '@element-plus/icons-vue'
import AppList from '@/components/base/AppList.vue'
import ListItem from '../components/ListItem.vue'
import { getConnectionList, getConnectionDetails } from '../api'
import { getConnectionList, getConnectionDetails, getScheduleMember, getStudentFollow } from '../api'
import { useMapStore } from '@/stores/map'
const store = useMapStore()
const FormDialog = defineAsyncComponent(() => import('../components/FormDialog.vue'))
const UserDataDialog = defineAsyncComponent(() => import('../components/UserDataDialog.vue'))
const EventDataDialog = defineAsyncComponent(() => import('../components/EventDataDialog.vue'))
const DataProgressDialog = defineAsyncComponent(() => import('../components/DataProgressDialog.vue'))
const StudentFollowDialog = defineAsyncComponent(() => import('../components/StudentFollowDialog.vue'))
const appList = $ref<InstanceType<typeof AppList> | null>(null)
// 列表配置
......@@ -36,14 +40,14 @@ function handleRefresh() {
appList?.refetch()
}
// 新建
// 新建链接
let formVisible = $ref(false)
const createConnect = function () {
formData = undefined
formVisible = true
}
// 编辑
// 编辑链接
let formData = $ref<DetailsProp | undefined>()
const editConnect = function (id: string) {
getConnectionDetails({ id: id }).then(res => {
......@@ -51,6 +55,64 @@ const editConnect = function (id: string) {
formVisible = true
})
}
// 生成用户数据相关
let userDataVisible = $ref(false)
let userData = $ref<ScheduleMember>()
const handleGenerateUserData = function (experimentId: string, id: string, name: string) {
getScheduleMember({ experiment_id: experimentId, connect_id: id }).then((res: { data: ScheduleMember }) => {
userData = res.data
if (res.data?.other_fields) {
userData.other_fields = res.data.other_fields.map((item: any) => {
if (item?.rule) {
item.rule.type = parseInt(item.rule.type)
}
return item
})
}
userData.type_name = name
userData.experiment_id = experimentId
userData.connect_id = id
})
userDataVisible = true
}
// 生成事件数据
let eventDataVisible = $ref(false)
let eventData = $ref<any>({})
const handleGenerateEventData = function (experimentId: string, id: string, events: any, last_event: string) {
eventData = {
experiment_id: experimentId,
connect_id: id,
events: events,
last_event: last_event
}
eventDataVisible = true
}
// 数据生成进度
let dataProgressVisible = $ref(false)
const viewDataProgress = function () {
dataProgressVisible = true
}
// 用户触达
let studentFollowVisible = $ref(false)
let studentFollowData = $ref<StudentFollow>()
const handleStudentFollow = function (experimentId: string, id: string, type: string) {
studentFollowVisible = true
getStudentFollow({ experiment_id: experimentId, experiment_connection_id: id }).then((res: any) => {
if (res.code === 0) {
const data = res.data
data.connect_id = id
data.type = type
studentFollowData = data
}
})
}
</script>
<template>
......@@ -64,6 +126,10 @@ const editConnect = function (id: string) {
v-for="item in data"
:key="item.id"
@update="handleRefresh"
@generateUserData="handleGenerateUserData"
@generateEventData="handleGenerateEventData"
@viewDataProgress="viewDataProgress"
@handleStudentFollow="handleStudentFollow"
></ListItem>
<div class="connect-item" @click="createConnect">
<div class="connect-add-button">
......@@ -77,6 +143,18 @@ const editConnect = function (id: string) {
</AppCard>
<!-- 新建链接 -->
<FormDialog :data="formData" v-model="formVisible" v-if="formVisible" @update="handleRefresh" />
<!-- 生成用户数据 -->
<UserDataDialog :data="userData" v-model="userDataVisible" v-if="userDataVisible"></UserDataDialog>
<!-- 生成事件数据 -->
<EventDataDialog :data="eventData" v-model="eventDataVisible" v-if="eventDataVisible"></EventDataDialog>
<!-- 数据进度 -->
<DataProgressDialog v-model="dataProgressVisible" v-if="dataProgressVisible"></DataProgressDialog>
<!-- 用户触达 -->
<StudentFollowDialog
:data="studentFollowData"
v-model="studentFollowVisible"
v-if="studentFollowVisible"
></StudentFollowDialog>
</template>
<style lang="scss">
......@@ -85,13 +163,14 @@ const editConnect = function (id: string) {
grid-template-columns: repeat(5, 1fr);
gap: 20px;
padding: 20px;
background-color: #efefef;
// background-color: #efefef;
border-radius: 5px;
}
.connect-add-button {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
.el-icon {
font-size: 20px;
margin-right: 10px;
......
......@@ -26,11 +26,11 @@ export default defineConfig(({ mode }) => ({
cert: fs.readFileSync(path.join(__dirname, './https/ezijing.com.pem'))
},
proxy: {
// '/api/resource': {
// target: 'http://com-resource-admin-test.ezijing.com/',
// changeOrigin: true,
// rewrite: path => path.replace(/^\/api\/resource/, '')
// },
'/api/lab': {
target: 'http://localhost-resource-experiment.ezijing.com/',
changeOrigin: true,
rewrite: path => path.replace(/^\/api\/lab/, '')
},
'/api': 'https://saas-dml.ezijing.com'
}
},
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论