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

Merge branch 'pro' into gdrtvu

This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
"cert": "node ./cert.js" "cert": "node ./cert.js"
}, },
"dependencies": { "dependencies": {
"@chuangkit/chuangkit-design": "^2.0.5",
"@element-plus/icons-vue": "^2.3.1", "@element-plus/icons-vue": "^2.3.1",
"@fortaine/fetch-event-source": "^3.0.6", "@fortaine/fetch-event-source": "^3.0.6",
"@tinymce/tinymce-vue": "^5.0.1", "@tinymce/tinymce-vue": "^5.0.1",
...@@ -30,7 +31,7 @@ ...@@ -30,7 +31,7 @@
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"nanoid": "^5.0.7", "nanoid": "^5.0.7",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"vue": "^3.4.23", "vue": "^3.4.26",
"vue-echarts": "^6.6.9", "vue-echarts": "^6.6.9",
"vue-router": "^4.3.2", "vue-router": "^4.3.2",
"xss": "^1.0.15" "xss": "^1.0.15"
...@@ -40,18 +41,18 @@ ...@@ -40,18 +41,18 @@
"@tsconfig/node20": "^20.1.4", "@tsconfig/node20": "^20.1.4",
"@types/blueimp-md5": "^2.18.2", "@types/blueimp-md5": "^2.18.2",
"@types/node": "^20.3.1", "@types/node": "^20.3.1",
"@vitejs/plugin-vue": "^4.6.2", "@vitejs/plugin-vue": "^5.0.4",
"@vue-macros/reactivity-transform": "^0.4.4", "@vue-macros/reactivity-transform": "^0.4.4",
"@vue/eslint-config-typescript": "^12.0.0", "@vue/eslint-config-typescript": "^13.0.0",
"@vue/tsconfig": "^0.5.1", "@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.57.0",
"eslint-plugin-vue": "^9.17.0", "eslint-plugin-vue": "^9.25.0",
"sass": "^1.67.0", "sass": "^1.75.0",
"typescript": "~5.4.5", "typescript": "~5.4.5",
"unplugin-auto-import": "^0.17.5", "unplugin-auto-import": "^0.17.5",
"vite": "^4.5.3", "vite": "^5.2.10",
"vue-tsc": "^1.8.27" "vue-tsc": "^1.8.27"
} }
} }
...@@ -139,3 +139,13 @@ export function uploadLocalFile(data: { file: File; file_name: string; is_contin ...@@ -139,3 +139,13 @@ export function uploadLocalFile(data: { file: File; file_name: string; is_contin
headers: { 'Content-Type': 'multipart/form-data' } headers: { 'Content-Type': 'multipart/form-data' }
}) })
} }
// 获取群组成员
export function getGroupMembers(params: { group_id: string; name?: string; id?: string }) {
return httpRequest.get('/api/lab/v1/experiment/group/bda-members', { params })
}
// 获取标签成员
export function getLabelMembers(params: { tag_id: string }) {
return httpRequest.get('/api/lab/v1/experiment/tag/bda-statistics-users', { params })
}
import httpRequest from '@/utils/axios'
// 获取实验用户成员属性
export function getMemberAttrList(params: { type: number }) {
return httpRequest.get('/api/lab/v1/experiment/tag/bda-member-attrs', { params })
}
// 获取实验事件和事件属性列表
export function getEventAttrList(params: { type: number }) {
return httpRequest.get('/api/lab/v1/experiment/tag/bda-event-attrs', { params })
}
// 获取最大最小值
export function getMemberAttrRange(params: { member_meta_id: string }) {
return httpRequest.get('/api/lab/v1/experiment/tag/bda-num-member-attr-range', { params })
}
// 获取RFM标签的结果集
export function getRfmRes() {
return httpRequest.get('/api/lab/v1/experiment/group/bda-rfm-res')
}
// 获取最近一次Rfm标签的统计结果
export function getRfmStatistics(params: { tag_id: string }) {
return httpRequest.get('/api/lab/v1/experiment/tag/bda-rfm-statistics-result', { params })
}
This source diff could not be displayed because it is too large. You can view the blob instead.
<script setup>
import AppList from '@/components/base/AppList.vue'
import { getNameByValue } from '@/utils/dictionary'
import { useMapStore } from '@/stores/map'
import { getGroupMembers } from '@/api/base'
const statusList = useMapStore().getMapValuesByKey('system_status')
const genderList = useMapStore().getMapValuesByKey('system_gender')
const connectionTypeList = useMapStore().getMapValuesByKey('experiment_connection_type')
const props = defineProps(['data'])
// 列表配置
const listOptions = computed(() => {
return {
remote: {
httpRequest: getGroupMembers,
params: { group_id: props.data.id }
},
columns: [
{ label: '序号', type: 'index', width: 60 },
{ label: '用户ID', prop: 'id' },
{ label: '姓名', prop: 'name' },
{
label: '性别',
prop: 'gender',
computed({ row }) {
return getNameByValue(row.gender, genderList)
}
},
{ label: '手机号码', prop: 'mobile' },
{
label: '来源连接',
prop: 'experiment_connection_id',
computed({ row }) {
return getNameByValue(row.connection.type.toString(), connectionTypeList)
}
},
{
label: '状态',
prop: 'status',
computed({ row }) {
return getNameByValue(row.status, statusList)
}
},
{ label: '更新人', prop: 'updated_operator.real_name' },
{ label: '更新时间', prop: 'updated_time' },
{ label: '操作', slots: 'table-x' }
]
}
})
</script>
<template>
<el-dialog title="标签用户" width="1000px">
<AppList v-bind="listOptions" ref="appList">
<template #table-x="{ row }">
<el-button type="primary" plain>
<router-link target="_blank" :to="{ path: '/user/image', query: { user_id: row.id, experiment_id: row.experiment_id } }">查看</router-link>
</el-button>
</template>
</AppList>
</el-dialog>
</template>
<script setup>
import AppList from '@/components/base/AppList.vue'
import { getNameByValue } from '@/utils/dictionary'
import { useMapStore } from '@/stores/map'
import { getLabelMembers } from '@/api/base'
const statusList = useMapStore().getMapValuesByKey('system_status')
const genderList = useMapStore().getMapValuesByKey('system_gender')
const connectionTypeList = useMapStore().getMapValuesByKey('experiment_connection_type')
const props = defineProps(['data'])
// 列表配置
const listOptions = computed(() => {
return {
remote: {
httpRequest: getLabelMembers,
params: { tag_id: props.data.id }
},
columns: [
{ label: '序号', type: 'index', width: 60 },
{ label: '用户ID', prop: 'id' },
{ label: '姓名', prop: 'name' },
{
label: '性别',
prop: 'gender',
computed({ row }) {
return getNameByValue(row.gender, genderList)
}
},
{ label: '手机号码', prop: 'mobile' },
{
label: '来源连接',
prop: 'experiment_connection_id',
computed({ row }) {
return getNameByValue(row.connection.type.toString(), connectionTypeList)
}
},
{
label: '状态',
prop: 'status',
computed({ row }) {
return getNameByValue(row.status, statusList)
}
},
{ label: '更新人', prop: 'updated_operator.real_name' },
{ label: '更新时间', prop: 'updated_time' },
{ label: '操作', slots: 'table-x' }
]
}
})
</script>
<template>
<el-dialog title="群组用户" width="1000px">
<AppList v-bind="listOptions" ref="appList">
<template #table-x="{ row }">
<el-button type="primary" plain>
<router-link target="_blank" :to="{ path: '/user/image', query: { user_id: row.id, experiment_id: row.experiment_id } }">查看</router-link>
</el-button>
</template>
</AppList>
</el-dialog>
</template>
...@@ -132,14 +132,28 @@ defineExpose({ refetch, tableRef }) ...@@ -132,14 +132,28 @@ defineExpose({ refetch, tableRef })
</template> </template>
<template v-else> <template v-else>
<!-- input --> <!-- input -->
<el-input v-model="params[item.prop]" v-bind="item" clearable @change="search" v-if="item.type === 'input'" /> <el-input
v-model="params[item.prop]"
v-bind="item"
clearable
@change="search"
v-if="item.type === 'input'"
/>
<!-- select --> <!-- select -->
<el-select v-model="params[item.prop]" v-bind="item" filterable clearable @change="search" v-if="item.type === 'select'"> <el-select
v-model="params[item.prop]"
v-bind="item"
filterable
clearable
@change="search"
v-if="item.type === 'select'"
>
<el-option <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>
...@@ -157,7 +171,13 @@ defineExpose({ refetch, tableRef }) ...@@ -157,7 +171,13 @@ 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 :header-cell-style="{ background: '#ededed' }" :data="dataList" v-loading="loading" v-bind="$attrs" ref="tableRef"> <el-table
:header-cell-style="{ background: '#ededed' }"
:data="dataList"
v-loading="loading"
v-bind="$attrs"
ref="tableRef"
>
<el-table-column align="center" v-bind="item || {}" v-for="item in columns" :key="item.prop"> <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>
...@@ -176,14 +196,15 @@ defineExpose({ refetch, tableRef }) ...@@ -176,14 +196,15 @@ defineExpose({ refetch, tableRef })
class="table-list-pagination" class="table-list-pagination"
background background
layout="total, sizes, prev, pager, next, jumper" layout="total, sizes, prev, pager, next, jumper"
:page-sizes="[10, 20, 30, 50, 100]" :page-sizes="[10, 20, 30, 50]"
:page-size="page.size" :page-size="page.size"
:total="page.total" :total="page.total"
v-model:currentPage="page.currentPage" v-model:currentPage="page.currentPage"
@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>
......
<script setup lang="ts">
import EventRule from './EventRule.vue'
import UserRule from './UserRule.vue'
import UserActionRule from './UserActionRule.vue'
const form = defineModel<any>()
onMounted(() => {
form.value = Object.assign(
{
user_attr_rule: { current_logic_operate: 'and', items: [] },
event_attr_rule: { current_logic_operate: 'and', items: [] },
user_action_rule: { current_logic_operate: 'and', items: [] }
},
form.value
)
})
</script>
<template>
<UserRule v-model="form.user_attr_rule" style="margin-top: 20px"></UserRule>
<EventRule v-model="form.event_attr_rule" style="margin-top: 20px"></EventRule>
<UserActionRule v-model="form.user_action_rule" style="margin-top: 20px"></UserActionRule>
</template>
<style src="@/assets/styles/rule.scss"></style>
...@@ -322,7 +322,7 @@ function remoteMethod(rule: EventRuleItem, attr: RuleAttr, search: string) { ...@@ -322,7 +322,7 @@ function remoteMethod(rule: EventRuleItem, attr: RuleAttr, search: string) {
</div> </div>
</div> </div>
<el-button text :icon="Plus" @click="handleAdd(eventAttrRule.items)" v-if="eventAttrRule.items.length < limit" <el-button text :icon="Plus" @click="handleAdd(eventAttrRule.items)" v-if="eventAttrRule.items.length < limit"
>添加</el-button >添加</el-button
> >
<slot name="footer"></slot> <slot name="footer"></slot>
</el-card> </el-card>
......
<script setup lang="ts"> <script setup lang="ts">
import type { TagRule } from '@/types' import type { TagRule } from '@/types'
import { PriceTag, Plus, CloseBold } from '@element-plus/icons-vue' import { PriceTag, Plus, CloseBold, QuestionFilled } from '@element-plus/icons-vue'
import { useTag } from '@/composables/useAllData' import { useTag } from '@/composables/useAllData'
import { useRfmRes } from '@/composables/useRFMData'
// const tagRule = ref(inject('tagRule') as TagRule) // const tagRule = ref(inject('tagRule') as TagRule)
const tagRule = defineModel<TagRule>({ default: { current_logic_operate: 'and', items: [] } }) const tagRule = defineModel<TagRule>({ default: { current_logic_operate: 'and', items: [] } })
const { tagList } = useTag() const { tagList } = useTag()
const { rfmResList } = useRfmRes()
// 获取逻辑运算符名称 // 获取逻辑运算符名称
function getLogicalName(value: 'and' | 'or') { function getLogicalName(value: 'and' | 'or') {
...@@ -19,14 +21,33 @@ function toggleOperate(rule: TagRule) { ...@@ -19,14 +21,33 @@ function toggleOperate(rule: TagRule) {
} }
// 添加条件 // 添加条件
function handleAdd(items: string[]) { function handleAdd(items: any[]) {
items.push('') items.push({ tag_id: '' })
} }
// 删除 // 删除
function handleRemove(items: string[], index: number) { function handleRemove(items: any[], index: number) {
items.splice(index, 1) items.splice(index, 1)
} }
function showRfm(id: string) {
return tagList.value.find(item => item.id === id)?.label == '4'
}
function handleRfmChange(rfmKey: string, item: any) {
const found = rfmResList.value.find(item => item.frm_key === rfmKey)
item.rfm_value = found?.frm_value
}
const a = [
{ label: '重要价值用户', label_des: '最近使用,使用频率高,消费金额大', r: '高', f: '高', m: '高', group: '组合1', guide: '留存与促活' },
{ label: '一般价值用户', label_des: '最近使用,使用频率高,消费金额小', r: '高', f: '高', m: '低', group: '组合2', guide: '放弃' },
{ label: '重要发展用户', label_des: '最近使用,使用频率低,消费金额大', r: '高', f: '低', m: '高', group: '组合3', guide: '拉新客户' },
{ label: '一般发展用户', label_des: '最近使用,使用频率低,消费金额小', r: '高', f: '低', m: '低', group: '组合4', guide: '放弃' },
{ label: '重要保持用户', label_des: '较长时间未使用,使用频率高,消费金额大', r: '低', f: '高', m: '高', group: '组合5', guide: '留存与促活' },
{ label: '一般保持用户', label_des: '较长时间未使用,使用频率高,消费金额小', r: '低', f: '高', m: '低', group: '组合6', guide: '放弃' },
{ label: '重要挽留用户', label_des: '较长时间未使用,使用频率低,消费金额大', r: '低', f: '低', m: '高', group: '组合7', guide: '流失客户' },
{ label: '一般挽留用户', label_des: '较长时间未使用,使用频率低,消费金额小', r: '低', f: '低', m: '低', group: '组合8', guide: '放弃' }
]
</script> </script>
<template> <template>
...@@ -44,9 +65,38 @@ function handleRemove(items: string[], index: number) { ...@@ -44,9 +65,38 @@ function handleRemove(items: string[], index: number) {
<div> <div>
标签 等于 标签 等于
<el-form-item> <el-form-item>
<el-select v-model="tagRule.items[index]"> <el-select v-model="item.tag_id" style="width: 300px">
<el-option v-for="option in tagList" :key="option.id" :label="option.name" :value="option.id"></el-option> <el-option v-for="option in tagList" :key="option.id" :label="option.name" :value="option.id"></el-option>
</el-select> </el-select>
<template v-if="showRfm(item.tag_id)">
<el-select v-model="item.rfm_key" @change="value => handleRfmChange(value, item)" style="width: 400px; margin: 0 10px">
<el-option v-for="item in rfmResList" :key="item.frm_key" :label="item.frm_value" :value="item.frm_key" style="height: auto">
<div style="line-height: 24px; padding: 5px 0">
<p>
<span style="float: left">{{ item.frm_value }}</span>
<span style="float: right; color: var(--el-text-color-secondary); font-size: 13px">
{{ item.frm_extend_info.customer_marketing_strategy }}
</span>
</p>
<p style="clear: both; color: var(--el-text-color-secondary); font-size: 13px">{{ item.frm_extend_info.label_desc }}</p>
</div>
</el-option>
</el-select>
<el-popover popper-class="rfm-popover" placement="top" :width="800" trigger="hover">
<el-table :data="a" border stripe>
<el-table-column prop="group" label="组合" width="70" />
<el-table-column prop="r" label="R值" width="52" />
<el-table-column prop="f" label="F值" width="52" />
<el-table-column prop="m" label="M值" width="52" />
<el-table-column prop="label" label="标签值" width="110" />
<el-table-column prop="label_des" label="标签说明" />
<el-table-column prop="guide" label="客户营销策略" width="110" />
</el-table>
<template #reference>
<el-icon><QuestionFilled /></el-icon>
</template>
</el-popover>
</template>
</el-form-item> </el-form-item>
</div> </div>
<el-button text :icon="CloseBold" @click="handleRemove(tagRule.items, index)"></el-button> <el-button text :icon="CloseBold" @click="handleRemove(tagRule.items, index)"></el-button>
......
...@@ -44,7 +44,7 @@ const handleInputConfirm = () => { ...@@ -44,7 +44,7 @@ const handleInputConfirm = () => {
<template> <template>
<p class="rule-tips"> <p class="rule-tips">
<el-icon><Warning /></el-icon> <el-icon><Warning /></el-icon>
自定义分层标签:将满足不同分层规则的用户打上分层标签,同一用户会被优先匹配在顺序靠前的分层 分层标签:将满足不同分层规则的用户打上分层标签,同一用户会被优先匹配在顺序靠前的分层
</p> </p>
<div> <div>
<el-tag <el-tag
......
<script setup lang="ts"> <script setup lang="ts">
import RFMRuleItem from './RFMRuleItem.vue' import RFMRuleItem from './RFMRuleItem.vue'
import { useRfmStatistics } from '@/composables/useRFMData'
const props = defineProps<{ tagId?: string }>()
const form = defineModel<any>({ const form = defineModel<any>({
default: { default: {
R: {}, R: {},
...@@ -8,10 +10,23 @@ const form = defineModel<any>({ ...@@ -8,10 +10,23 @@ const form = defineModel<any>({
M: {} M: {}
} }
}) })
const { rfmStatistics } = useRfmStatistics(props.tagId || '')
</script> </script>
<template> <template>
<RFMRuleItem label="R" v-model="form.R" /> <RFMRuleItem label="R" v-model="form.R">
<RFMRuleItem label="F" v-model="form.F" style="margin-top: 20px" /> <template #header-aside="{ data }">
<RFMRuleItem label="M" v-model="form.M" style="margin-top: 20px" /> <template v-if="data.rule === '101' && rfmStatistics.is_complete">R值计算结果为:{{ rfmStatistics.rfm_tag_res.r }} </template>
</template>
</RFMRuleItem>
<RFMRuleItem label="F" v-model="form.F" style="margin-top: 20px">
<template #header-aside="{ data }">
<template v-if="data.rule === '101' && rfmStatistics.is_complete">F值计算结果为:{{ rfmStatistics.rfm_tag_res.f }} </template>
</template>
</RFMRuleItem>
<RFMRuleItem label="M" v-model="form.M" style="margin-top: 20px">
<template #header-aside="{ data }">
<template v-if="data.rule === '101' && rfmStatistics.is_complete">M值计算结果为:{{ rfmStatistics.rfm_tag_res.m }} </template>
</template>
</RFMRuleItem>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useUserAttr, useMetaEvent } from '@/composables/useAllData' import { QuestionFilled } from '@element-plus/icons-vue'
import { useUserAttr, useMetaEvent, useUserAttrRange } from '@/composables/useRFMData'
import { searchMetaMemberAttrs } from '@/api/base' import { searchMetaMemberAttrs } from '@/api/base'
defineProps<{ label: string }>() defineProps<{ label: string }>()
...@@ -28,26 +29,16 @@ onMounted(() => { ...@@ -28,26 +29,16 @@ onMounted(() => {
form.value = Object.assign({ basis: '1', rule: '101', event_id: '-1', attr_id: '', attr_type: '', config: [...defaultScore] }, form.value) form.value = Object.assign({ basis: '1', rule: '101', event_id: '-1', attr_id: '', attr_type: '', config: [...defaultScore] }, form.value)
}) })
const { userAttrList } = useUserAttr() const { userAttrList, fetchUserAttrList } = useUserAttr()
const { metaEventList } = useMetaEvent() const { metaEventList, fetchMetaEventList } = useMetaEvent()
const { userAttrRange, fetchUserAttrRange } = useUserAttrRange()
const currentRuleList = computed(() => { const currentRuleList = computed(() => {
return ruleList.filter(item => item.basis.includes(form.value.basis)) return ruleList.filter(item => item.basis.includes(form.value.basis))
}) })
const 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(() => { const currentMetaEventList = computed(() => {
return [{ id: '-1', name: '所有事件' }, ...metaEventList.value] return [{ event_id: '-1', event_name: '所有事件' }, ...metaEventList.value]
}) })
function handleBasisChange(value: any) { function handleBasisChange(value: any) {
...@@ -70,7 +61,7 @@ function handleRuleChange(value: any) { ...@@ -70,7 +61,7 @@ function handleRuleChange(value: any) {
} }
function handleAttrChange(value: any) { function handleAttrChange(value: any) {
form.value.attr_type = currentAttrList.value.find(item => item.id === value)?.type form.value.attr_type = userAttrList.value.find(item => item.id === value)?.type
} }
const options = ref<{ label: string; value: string }[]>([]) const options = ref<{ label: string; value: string }[]>([])
...@@ -87,23 +78,56 @@ async function remoteMethod(search: string = '') { ...@@ -87,23 +78,56 @@ async function remoteMethod(search: string = '') {
loading.value = false loading.value = false
} }
} }
function querySearch(queryString: string, cb: any) {
cb(options.value)
}
watch( watch(
() => form.value.attr_id, () => form.value.attr_id,
attrId => {
if (form.value.rule === '102') {
remoteMethod()
} else {
attrId && fetchUserAttrRange(attrId)
}
},
{ immediate: true }
)
watch(
() => form.value.rule,
() => { () => {
form.value.rule === '102' && remoteMethod() let type = form.value.rule === '102' ? 1 : 2
form.value.basis === '1' ? fetchUserAttrList(type) : fetchMetaEventList(type)
}, },
{ immediate: true } { immediate: true }
) )
const a = [
{ id: '001', label: '2000' },
{ id: '002', label: '1500' },
{ id: '003', label: '3000' },
{ id: '004', label: '2200' },
{ id: '005', label: '1800' }
]
</script> </script>
<template> <template>
<el-card shadow="never"> <el-card shadow="never">
<template #header> <template #header>
<div class="rfm-top">
<div>
<el-button circle type="primary" style="width: 32px; margin-right: 10px">{{ label }}</el-button> <el-button circle type="primary" style="width: 32px; margin-right: 10px">{{ label }}</el-button>
{{ label }}值计算规则 {{ label }}值计算规则
</div>
<div>
<slot name="header-aside" :data="form"></slot>
</div>
</div>
</template> </template>
<div class="rfm-header"> <div class="rfm-header">
<p style="margin-right: 20px">{{ label }}值计算依据</p> <p style="margin-right: 10px">{{ label }}值计算依据</p>
<el-radio-group v-model="form.basis" @change="handleBasisChange"> <el-radio-group v-model="form.basis" @change="handleBasisChange">
<el-radio value="1">用户属性</el-radio> <el-radio value="1">用户属性</el-radio>
<el-radio value="2">事件属性</el-radio> <el-radio value="2">事件属性</el-radio>
...@@ -112,12 +136,42 @@ watch( ...@@ -112,12 +136,42 @@ watch(
<el-select v-model="form.rule" style="width: 170px" @change="handleRuleChange"> <el-select v-model="form.rule" style="width: 170px" @change="handleRuleChange">
<el-option v-for="item in currentRuleList" :key="item.value" :label="item.label" :value="item.value"></el-option> <el-option v-for="item in currentRuleList" :key="item.value" :label="item.label" :value="item.value"></el-option>
</el-select> </el-select>
<div class="rfm-tips">
<el-popover popper-class="rfm-popover" placement="right" title="属性值平均法" :width="400" trigger="hover" v-if="form.rule == '101'">
<p>用于计算选中属性的平均值,通过对选定的字段中的所有记录进行数值相加,然后除以记录的数量来计算的。主要针对“数字”和“整数”两种字段类型。</p>
<p>举例:</p>
<el-table :data="a" border>
<el-table-column prop="id" label="用户ID" />
<el-table-column prop="label" label="销售额" />
</el-table>
<p>“销售额”的平均值为:<br />(2000+1500+3000+2200+1800)/5=2100</p>
<template #reference>
<el-icon><QuestionFilled /></el-icon>
</template>
</el-popover>
<el-popover popper-class="rfm-popover" placement="right" title="属性值分类法" :width="400" trigger="hover" v-if="form.rule == '102'">
<p>将数据的属性值按照一定的规则或特性进行分类,本系统中分了“高”和“低”两类。主要针对“字符串”的字段类型。</p>
<template #reference>
<el-icon><QuestionFilled /></el-icon>
</template>
</el-popover>
<el-popover popper-class="rfm-popover" placement="right" title="事件发生次数平均法" :width="400" trigger="hover" v-if="form.rule == '201'">
<p>分析事件发生频率的方法,即通过计算用户事件发生的平均次数。</p>
<template #reference>
<el-icon><QuestionFilled /></el-icon>
</template>
</el-popover>
</div>
<el-select v-model="form.event_id" placeholder="选择事件" style="width: 160px" v-if="form.basis === '2'"> <el-select v-model="form.event_id" placeholder="选择事件" style="width: 160px" v-if="form.basis === '2'">
<el-option v-for="item in currentMetaEventList" :key="item.id" :label="item.name" :value="item.id"></el-option> <el-option v-for="item in currentMetaEventList" :key="item.event_id" :label="item.event_name" :value="item.event_id"></el-option>
</el-select> </el-select>
<el-select v-model="form.attr_id" placeholder="选择属性" style="width: 160px" @change="handleAttrChange" v-else> <el-select v-model="form.attr_id" placeholder="选择属性" style="width: 160px" @change="handleAttrChange" v-else>
<el-option v-for="item in currentAttrList" :key="item.id" :label="item.name" :value="item.id"></el-option> <el-option v-for="item in userAttrList" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select> </el-select>
<template v-if="form.basis == 1 && form.rule != '102' && form.attr_id">
<p>最小值:{{ userAttrRange.min }} <br />最大值:{{ userAttrRange.max }}<br />平均值:{{ userAttrRange.avg }}</p>
</template>
</div> </div>
<div class="rfm-body"> <div class="rfm-body">
<template v-if="form.rule === '102'"> <template v-if="form.rule === '102'">
...@@ -126,9 +180,10 @@ watch( ...@@ -126,9 +180,10 @@ watch(
<b>{{ item.level }}</b> <b>{{ item.level }}</b>
</div> </div>
<div class="rfm-box-body"> <div class="rfm-box-body">
<el-select placeholder="选择属性值" v-model="item.value" filterable allow-create :loading="loading"> <el-autocomplete v-model="item.value" placeholder="选择属性值" :fetch-suggestions="querySearch" />
<!-- <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-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
</el-select> </el-select> -->
</div> </div>
</div> </div>
</template> </template>
...@@ -150,6 +205,11 @@ watch( ...@@ -150,6 +205,11 @@ watch(
</template> </template>
<style lang="scss"> <style lang="scss">
.rfm-top {
display: flex;
align-items: center;
justify-content: space-between;
}
.rfm-header { .rfm-header {
display: flex; display: flex;
align-items: center; align-items: center;
...@@ -186,4 +246,12 @@ watch( ...@@ -186,4 +246,12 @@ watch(
text-align: center; text-align: center;
} }
} }
.rfm-tips {
margin-right: 10px;
}
.rfm-popover {
p {
margin: 10px 0;
}
}
</style> </style>
<script setup lang="ts">
import type { RuleAttr } from '@/types'
import { UserFilled } from '@element-plus/icons-vue'
import { useUserAttr } from '@/composables/useAllData'
import { stringOperatorList, numberOperatorList, dateOperatorList } from '@/utils/dictionary'
import { searchMetaMemberAttrs } from '@/api/base'
const form = defineModel<any>()
onMounted(() => {
form.value = Object.assign({ attr_id: '', attr: '', attr_name: '', attr_type: '', operate: '', operate_name: '', value: '' }, form.value)
})
const { userAttrList } = useUserAttr()
// 获取运算符列表
function getOperatorList(type: string) {
if (type === '1') return stringOperatorList
if (type === '2' || type === '3') return numberOperatorList
if (type === '4' || type === '5') return dateOperatorList
return stringOperatorList
}
// 属性改变
function handleAttrChange(value: string, item: RuleAttr) {
const found = userAttrList.value.find(item => item.id === value)
item.attr = found?.english_name || ''
item.attr_name = found?.name || ''
item.attr_type = found?.type || ''
// 清空条件数据
item.operate = ''
item.operate_name = ''
item.value = ''
remoteMethod(item)
}
// 条件改变
function handleOperateChange(value: string, item: RuleAttr) {
const found = getOperatorList(item.attr_type).find(item => item.value === value)
item.operate_name = found?.label || ''
item.value = ''
// 区间
if (value === 'range') {
item.value = { start: undefined, end: undefined }
}
}
function querySearch(item: RuleAttr, search: string, cb: (arg: any) => void) {
if (item.attr_id) {
searchMetaMemberAttrs({ search, member_meta_id: item.attr_id, per_page: 100 }).then(res => {
cb(res.data.list)
})
} else {
cb([])
}
}
const options = ref<{ label: string; value: string }[]>([])
const loading = ref(false)
function remoteMethod(item: RuleAttr, search: string = '') {
options.value = []
if (item.attr_id) {
loading.value = true
searchMetaMemberAttrs({ search, member_meta_id: item.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
}
}
</script>
<template>
<el-card shadow="never">
<template #header>
<el-button circle color="#006df1" :icon="UserFilled"></el-button>
用户属性满足以下条件
</template>
<div class="rule">
<div class="rule-list">
<el-row class="rule-item">
<el-form-item>
<el-select v-model="form.attr_id" @change="value => handleAttrChange(value, form)">
<el-option v-for="option in userAttrList" :key="option.id" :label="option.name" :value="option.id"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-select v-model="form.operate" @change="value => handleOperateChange(value, form)">
<el-option
v-for="option in getOperatorList(form.attr_type)"
:key="option.value"
:label="option.alias || option.label"
:value="option.value"></el-option>
</el-select>
</el-form-item>
<el-form-item v-if="!['null', 'not null'].includes(form.operate)">
<!-- 数字区间 -->
<template v-if="['2', '3'].includes(form.attr_type) && form.operate === 'range'">
<el-input-number step-strictly :controls="false" :min="0" v-model="form.value.start" />
<el-input-number step-strictly :controls="false" :min="0" v-model="form.value.end" />
</template>
<!-- 日期区间 -->
<template v-else-if="form.attr_type === '4' && form.operate === 'range'">
<el-date-picker v-model="form.value.start" type="date" value-format="YYYY-MM-DD" />
<el-date-picker v-model="form.value.end" type="date" value-format="YYYY-MM-DD" />
</template>
<!-- 时间区间 -->
<template v-else-if="form.attr_type === '5' && form.operate === 'range'">
<el-date-picker v-model="form.value.start" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" style="width: 180px" />
<el-date-picker v-model="form.value.end" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" style="width: 180px" />
</template>
<template v-else-if="form.attr_type === '4' && (form.operate === 'after' || form.operate === 'before')">
<el-date-picker v-model="form.value" type="date" value-format="YYYY-MM-DD" />
</template>
<template v-else-if="form.attr_type === '5' && (form.operate === 'after' || form.operate === 'before')">
<el-date-picker v-model="form.value" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" style="width: 180px" />
</template>
<template v-else>
<el-select
v-model="form.value"
filterable
remote
allow-create
:multiple="['in', 'not in'].includes(form.operate)"
:remote-method="(query:string) => remoteMethod(form, query)"
:loading="loading"
style="width: 320px"
v-if="['in', 'not in'].includes(form.operate)">
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<el-autocomplete
v-model="form.value"
value-key="attr_value"
:fetch-suggestions="(query, cb) => querySearch(form, query, cb)"
style="width: 320px"
v-else />
</template>
</el-form-item>
</el-row>
</div>
</div>
</el-card>
</template>
<style src="@/assets/styles/rule.scss"></style>
...@@ -248,7 +248,7 @@ function remoteMethod(event: RuleEvent, attr: RuleAttr, search: string) { ...@@ -248,7 +248,7 @@ function remoteMethod(event: RuleEvent, attr: RuleAttr, search: string) {
</section> </section>
</div> </div>
</div> </div>
<el-button text :icon="Plus" @click="handleAdd(userActionRule.items)">添加条件</el-button> <el-button text :icon="Plus" @click="handleAdd(userActionRule.items)">添加用户行为</el-button>
</el-card> </el-card>
</template> </template>
......
...@@ -100,7 +100,7 @@ function remoteMethod(item: RuleAttr, search: string = '') { ...@@ -100,7 +100,7 @@ function remoteMethod(item: RuleAttr, search: string = '') {
</div> </div>
<div class="rule-list"> <div class="rule-list">
<el-row justify="space-between" class="rule-item" v-for="(item, index) in userAttrRule.items" :key="index"> <el-row justify="space-between" class="rule-item" v-for="(item, index) in userAttrRule.items" :key="index">
<div> <div style="display: flex">
<el-form-item> <el-form-item>
<el-select v-model="item.attr_id" @change="value => handleAttrChange(value, item)"> <el-select v-model="item.attr_id" @change="value => handleAttrChange(value, item)">
<el-option v-for="option in userAttrList" :key="option.id" :label="option.name" :value="option.id"></el-option> <el-option v-for="option in userAttrList" :key="option.id" :label="option.name" :value="option.id"></el-option>
...@@ -116,8 +116,13 @@ function remoteMethod(item: RuleAttr, search: string = '') { ...@@ -116,8 +116,13 @@ function remoteMethod(item: RuleAttr, search: string = '') {
</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-if="item.attr_type === '4' && item.operate === 'range'"> <template v-else-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>
...@@ -158,7 +163,7 @@ function remoteMethod(item: RuleAttr, search: string = '') { ...@@ -158,7 +163,7 @@ function remoteMethod(item: RuleAttr, search: string = '') {
</el-row> </el-row>
</div> </div>
</div> </div>
<el-button text :icon="Plus" @click="handleAdd(userAttrRule.items)">添加条件</el-button> <el-button text :icon="Plus" @click="handleAdd(userAttrRule.items)">添加属性</el-button>
</el-card> </el-card>
</template> </template>
......
...@@ -24,6 +24,7 @@ interface MetaEventType { ...@@ -24,6 +24,7 @@ interface MetaEventType {
export interface TagType { export interface TagType {
id: string id: string
name: string name: string
label: string
} }
// 连接类型 // 连接类型
......
import { getMemberAttrList, getEventAttrList, getMemberAttrRange, getRfmRes, getRfmStatistics } from '@/api/rfm'
// 用户属性类型
export interface AttrType {
id: string
name: string
type: string
format: string
english_name: string
}
// 事件类型
interface MetaEventType {
event_id: string
event_name: string
event_english_name: string
attrs: AttrType[]
}
// 所有用户属性
export function useUserAttr() {
const userAttrList = ref<AttrType[]>([])
async function fetchUserAttrList(type: number) {
await getMemberAttrList({ type }).then((res: any) => {
userAttrList.value = res.data.items
})
}
return { fetchUserAttrList, userAttrList }
}
// 所有事件
export function useMetaEvent() {
const metaEventList = ref<MetaEventType[]>([])
async function fetchMetaEventList(type: number) {
await getEventAttrList({ type }).then((res: any) => {
metaEventList.value = res.data.items
})
}
return { fetchMetaEventList, metaEventList }
}
// 最大值最小值
export function useUserAttrRange() {
const userAttrRange = ref<{ min: string; max: string; avg: string }>({ min: '', max: '', avg: '' })
async function fetchUserAttrRange(member_meta_id: string) {
await getMemberAttrRange({ member_meta_id }).then((res: any) => {
userAttrRange.value = res.data.detail
})
}
return { fetchUserAttrRange, userAttrRange }
}
// RFM标签的结果集
interface RfmRes {
frm_key: string
frm_value: number
frm_extend_info: any
}
const rfmResList = ref<RfmRes[]>([])
export function useRfmRes() {
async function fetchRfmResList() {
await getRfmRes().then((res: any) => {
rfmResList.value = res.data.items
})
}
onMounted(() => {
fetchRfmResList()
})
return { fetchRfmResList, rfmResList }
}
interface RfmStatistics {
is_complete: boolean
rfm_tag_res: {
r: number
f: number
m: number
}
}
const rfmStatistics = ref<RfmStatistics>({ is_complete: false, rfm_tag_res: { r: 0, f: 0, m: 0 } })
export function useRfmStatistics(tagId: string) {
async function fetchRfmStatistics() {
if (!tagId) return
await getRfmStatistics({ tag_id: tagId }).then((res: any) => {
rfmStatistics.value = res.data
})
}
onMounted(() => {
fetchRfmStatistics()
})
return { fetchRfmStatistics, rfmStatistics }
}
...@@ -2,12 +2,13 @@ ...@@ -2,12 +2,13 @@
import { getUserTags } from '../api' import { getUserTags } from '../api'
const props = defineProps({ ssoId: String }) const props = defineProps({ ssoId: String })
const color = ['#af1c40', '#c17933', '#8f0034', '#d45548', '#ab3259', '#dec34c', '#8b8920', '#a25a6d']
const list = ref([]) const list = ref([])
async function fetchList() { async function fetchList() {
if (!props.ssoId) return if (!props.ssoId) return
const res = await getUserTags({ sso_id: props.ssoId, limit: 20 }) const res = await getUserTags({ sso_id: props.ssoId, limit: 20 })
list.value = res.data.items.map(item => { list.value = res.data.items.map((item, index) => {
return { ...item, weight: parseFloat(item.weight) / 5 + 1 || 0 } return { ...item, weight: parseFloat(item.weight) / 2 + 1 || 0, color: color[index % color.length] }
}) })
} }
...@@ -24,9 +25,9 @@ watch( ...@@ -24,9 +25,9 @@ watch(
<div class="user-label"> <div class="user-label">
<ul> <ul>
<li v-for="item in list" :key="item.id" :style="{ scale: item.weight }"> <li v-for="item in list" :key="item.id" :style="{ scale: item.weight }">
<div class="user-label__inner"> <div class="user-label__inner" :style="{ backgroundColor: item.color }">
{{ item.name }} {{ item.name }}
<span class="user-label__arrow"></span> <!-- <span class="user-label__arrow"></span> -->
</div> </div>
</li> </li>
</ul> </ul>
...@@ -47,21 +48,22 @@ watch( ...@@ -47,21 +48,22 @@ watch(
ul { ul {
display: grid; display: grid;
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
column-gap: 240px; column-gap: 230px;
row-gap: 20px; row-gap: 20px;
} }
.user-label__inner { .user-label__inner {
position: relative; position: relative;
display: inline-block; display: inline-block;
padding: 4px 10px; padding: 10px;
font-size: 14px; font-size: 14px;
text-align: left; text-align: left;
font-family: Roboto; font-family: Roboto;
line-height: 24px; line-height: 24px;
color: rgba(16, 16, 16, 1); // color: rgba(16, 16, 16, 1);
color: #fff;
background-color: rgba(231, 232, 232, 1); background-color: rgba(231, 232, 232, 1);
border: 1px solid rgba(187, 187, 187, 1); // border: 1px solid rgba(187, 187, 187, 1);
border-radius: 5px; border-radius: 50%;
} }
.user-label__arrow { .user-label__arrow {
position: absolute; position: absolute;
......
...@@ -36,59 +36,64 @@ const platformList: PlatformItem[] = [ ...@@ -36,59 +36,64 @@ const platformList: PlatformItem[] = [
type: '2', type: '2',
type_name: '钉钉', type_name: '钉钉',
config_attributes: [ config_attributes: [
{ label: '连接名称', prop: 'name', value: '' }, { label: '连接名称', prop: 'name', value: '钉钉' },
{ label: 'AgentId', prop: 'agentId', value: '' }, { label: 'AgentId', prop: 'agentId', value: '8441459810' },
{ label: 'AppKey', prop: 'appKey', value: '' }, { label: 'AppKey', prop: 'appKey', value: 'dingigucs3beqlotpf24' },
{ label: 'AppSecret', prop: 'appSecret', value: '' } { label: 'AppSecret', prop: 'appSecret', value: '6dNRvuOzvX_xq5N9tFdjepdf3FeooN25yUZK6ammDbPUVq9sfdXD-sKUg' }
] ]
}, },
{ {
type: '3', type: '3',
type_name: '小鹅通', type_name: '小鹅通',
config_attributes: [ config_attributes: [
{ label: '连接名称', prop: 'name', value: '' }, { label: '连接名称', prop: 'name', value: '小鹅通' },
{ label: 'app_id', prop: 'app_id', value: '' }, { label: 'app_id', prop: 'app_id', value: 'appc4bolgenF58' },
{ label: 'client_id', prop: 'client_id', value: '' }, { label: 'client_id', prop: 'client_id', value: '_5e7f809dd6317_qSMuUoAi?type=2SDK' },
{ label: 'secret_key', prop: 'secret_key', value: '' } { label: 'secret_key', prop: 'secret_key', value: 'xiao_5ac1dd24803ae_GtfAOxiS1pdf3FeooN2huhu92WRE52S-SkOh' }
] ]
}, },
{ {
type: '4', type: '4',
type_name: '问卷星', type_name: '问卷星',
config_attributes: [ config_attributes: [
{ label: '连接名称', prop: 'name', value: '' }, { label: '连接名称', prop: 'name', value: '问卷星' },
{ label: 'AppKey', prop: 'appKey', value: '' }, { label: 'AppKey', prop: 'appKey', value: '82286f9c5114dc2bda214cd9567dodc' },
{ label: 'AppSecret', prop: 'appSecret', value: '' } { label: 'AppSecret', prop: 'appSecret', value: 'pages/wjxqList/wjxqList?activityId= P251FBP' }
] ]
}, },
{ type: '5', type_name: '今日头条', config_attributes: [{ label: '连接名称', prop: 'name', value: '' }] }, { type: '5', type_name: '今日头条', config_attributes: [{ label: '连接名称', prop: 'name', value: '今日头条' }] },
{ {
type: '6', type: '6',
type_name: '抖音', type_name: '抖音',
config_attributes: [ config_attributes: [
{ label: '连接名称', prop: 'name', value: '' }, { label: '连接名称', prop: 'name', value: '抖音' },
{ label: '应用类别', prop: 'dyInput1', value: '' }, { label: '应用类别', prop: 'dyInput1', value: '短视频分享与社交平台' },
{ label: '授权域回调', prop: 'dyInput2', value: '' }, { label: '授权域回调', prop: 'dyInput2', value: 'https://douyin.xiaokefu.com.cn/douYin/push/19872884' },
{ label: '网站应用简介', prop: 'dyInput3', value: '' }, {
{ label: '应用官网', prop: 'dyInput4', value: '' }, label: '网站应用简介',
{ label: '联系人姓名', prop: 'dyInput5', value: '' } prop: 'dyInput3',
value:
'不仅是下载抖音应用程序的官方渠道,也是一个展示抖音最新动态、功能更新和推广活动的平台。用户可以通过官网了解抖音的特色功能、查看热门视频、参与互动活动,以及获取帮助和教程等。官网还为创作者和企业提供了一个展示空间,让他们了解如何利用抖音平台进行内容创作、品牌推广和电子商务等。'
},
{ label: '应用官网', prop: 'dyInput4', value: 'https://www.douyin.com' },
{ label: '联系人姓名', prop: 'dyInput5', value: '清控紫荆(北京)教育股份有限公司' }
] ]
}, },
{ {
type: '7', type: '7',
type_name: '微博', type_name: '微博',
config_attributes: [ config_attributes: [
{ label: '连接名称', prop: 'name', value: '' }, { label: '连接名称', prop: 'name', value: '微博' },
{ label: 'AppKey', prop: 'appKey', value: '' }, { label: 'AppKey', prop: 'appKey', value: '1206405345' },
{ label: 'AppSecret', prop: 'appSecret', value: '' } { label: 'AppSecret', prop: 'appSecret', value: '6a6095e113cd28fde6e14c7b7145c5c5' }
] ]
}, },
{ {
type: '8', type: '8',
type_name: '小红书', type_name: '小红书',
config_attributes: [ config_attributes: [
{ label: '连接名称', prop: 'name', value: '' }, { label: '连接名称', prop: 'name', value: '小红书' },
{ label: 'AppKey', prop: 'appKey', value: '' }, { label: 'AppKey', prop: 'appKey', value: '6c1dd8dd64d074d56124c751f6bc240b' },
{ label: 'AppSecret', prop: 'appSecret', value: '' } { label: 'AppSecret', prop: 'appSecret', value: '' }
] ]
}, },
...@@ -96,40 +101,45 @@ const platformList: PlatformItem[] = [ ...@@ -96,40 +101,45 @@ const platformList: PlatformItem[] = [
type: '9', type: '9',
type_name: '邮箱', type_name: '邮箱',
config_attributes: [ config_attributes: [
{ label: '连接名称', prop: 'name', value: '' }, { label: '连接名称', prop: 'name', value: '邮箱' },
{ label: 'client_id', prop: 'client_id', value: '' }, { label: 'client_id', prop: 'client_id', value: 'swanzhong' },
{ label: 'client_secret', prop: 'client_secret', value: '' }, { label: 'client_secret', prop: 'client_secret', value: '563a8c6a89d2368194c1c7889c508b34' },
{ label: 'token URL', prop: 'token', value: '' }, { label: 'token URL', prop: 'token', value: 'openapi/user/get' },
{ label: 'API URL', prop: 'apiUrl', value: '' } { label: 'API URL', prop: 'apiUrl', value: 'openapi/user/check' }
] ]
}, },
{ {
type: '10', type: '10',
type_name: '短信', type_name: '短信',
config_attributes: [ config_attributes: [
{ label: '连接名称', prop: 'name', value: '' }, { label: '连接名称', prop: 'name', value: '短信' },
{ label: 'client_id', prop: 'client_id', value: '' }, { label: 'client_id', prop: 'client_id', value: 'FbFgN2of-mlc' },
{ label: 'SdkAppId', prop: 'sdkAppId', value: '' }, { label: 'SdkAppId', prop: 'sdkAppId', value: 'CV3X1%2FJG7mdNZm03l9puvwPAktmfw1aj8XvBb6sm696MqoW57' },
{ label: 'token URL', prop: 'token', value: '' }, { label: 'token URL', prop: 'token', value: 'https://oauth-login.cloud.ali.com/oauth2/v3/token' },
{ label: 'API URL', prop: 'apiUrl', value: '' } { label: 'API URL', prop: 'apiUrl', value: 'oauth2v3wPAktm' }
] ]
}, },
{ type_name: '内部消息', type: '11', config_attributes: [{ label: '连接名称', prop: 'name', value: '' }] }, { type_name: '内部消息', type: '11', config_attributes: [{ label: '连接名称', prop: 'name', value: '内部消息' }] },
{ {
type: '12', type: '12',
type_name: '自定义', type_name: '自定义',
config_attributes: [ config_attributes: [
{ label: '连接名称', prop: 'name', value: '' }, { label: '连接名称', prop: 'name', value: '自定义' },
{ label: 'APP类型', prop: 'appType', value: '' }, { label: 'APP类型', prop: 'appType', value: '自定义' },
{ label: 'AppId', prop: 'appId', value: '' } { label: 'AppId', prop: 'appId', value: 'Custom App ID' }
] ]
}, },
{ type: '13', type_name: '紫荆表单', icon: '99', config_attributes: [{ label: '连接名称', prop: 'name', value: '' }] }, {
type: '13',
type_name: '紫荆表单',
icon: '99',
config_attributes: [{ label: '连接名称', prop: 'name', value: '紫荆表单' }]
},
{ {
type: '14', type: '14',
type_name: '小程序', type_name: '小程序',
icon: '100', icon: '100',
config_attributes: [{ label: '连接名称', prop: 'name', value: '' }], config_attributes: [{ label: '连接名称', prop: 'name', value: '小程序' }],
async onBeforeNext(stepActive) { async onBeforeNext(stepActive) {
if (stepActive === 2) { if (stepActive === 2) {
const res = await handleSubmit() const res = await handleSubmit()
...@@ -139,6 +149,12 @@ const platformList: PlatformItem[] = [ ...@@ -139,6 +149,12 @@ const platformList: PlatformItem[] = [
} }
return true return true
} }
},
{
icon: 'mall',
type: '15',
type_name: '紫荆商城',
config_attributes: [{ label: '连接名称', prop: 'name', value: '紫荆商城' }]
} }
] ]
...@@ -215,7 +231,8 @@ async function handleSave() { ...@@ -215,7 +231,8 @@ async function handleSave() {
:title="props.data?.id ? '编辑连接' : '新建连接'" :title="props.data?.id ? '编辑连接' : '新建连接'"
:close-on-click-modal="false" :close-on-click-modal="false"
width="1050px" width="1050px"
@update:modelValue="value => $emit('update:modelValue', value)"> @update:modelValue="value => $emit('update:modelValue', value)"
>
<el-tabs v-model="stepActive" class="demo-tabs"> <el-tabs v-model="stepActive" class="demo-tabs">
<!-- 第一步 --> <!-- 第一步 -->
<el-tab-pane disabled lazy label="选择连接类型" :name="1" v-if="!props.data?.id"> <el-tab-pane disabled lazy label="选择连接类型" :name="1" v-if="!props.data?.id">
......
<script setup lang="ts"> <script setup lang="ts">
import { Delete, Edit, MoreFilled, EditPen, User, Avatar, PieChart, UserFilled } from '@element-plus/icons-vue' import { Delete, Edit, MoreFilled, EditPen, User, Avatar, PieChart, UserFilled, View } from '@element-plus/icons-vue'
import { ElMessageBox, ElMessage } from 'element-plus' import { ElMessageBox, ElMessage } from 'element-plus'
import Icon from '@/components/ConnectionIcon.vue' import Icon from '@/components/ConnectionIcon.vue'
import { deleteConnection } from '../api' import { deleteConnection } from '../api'
...@@ -40,7 +40,7 @@ function handleRemove() { ...@@ -40,7 +40,7 @@ function handleRemove() {
}) })
} }
// 去查看 // 去查看
const routerView = function () { const handleView = function () {
router.push({ path: '/connect/view', query: { id: props.data.id } }) router.push({ path: '/connect/view', query: { id: props.data.id } })
} }
...@@ -70,7 +70,7 @@ const handleStudentFollow = function () { ...@@ -70,7 +70,7 @@ const handleStudentFollow = function () {
</script> </script>
<template> <template>
<div class="connect-item" @click="routerView"> <div class="connect-item">
<div class="connect-item_top"> <div class="connect-item_top">
<!-- <div class="connect-item__edit"> <!-- <div class="connect-item__edit">
<img @click="edit" src="https://webapp-pub.ezijing.com/pages/assa/dml_edit.png" /> <img @click="edit" src="https://webapp-pub.ezijing.com/pages/assa/dml_edit.png" />
...@@ -81,7 +81,7 @@ const handleStudentFollow = function () { ...@@ -81,7 +81,7 @@ const handleStudentFollow = function () {
<el-icon size="20" color="#333"><Delete /></el-icon> <el-icon size="20" color="#333"><Delete /></el-icon>
</div> --> </div> -->
<div class="connect-item__icon"> <div class="connect-item__icon">
<Icon w="40" h="40" :multiColor="true" class="svg" :name="iconMap[data.type] || data.type"></Icon> <Icon w="40" h="40" :multiColor="true" class="svg" :name="data.type === '15' ? 'mall' : iconMap[data.type] || data.type"></Icon>
</div> </div>
</div> </div>
<div class="connect-item_bottom"> <div class="connect-item_bottom">
...@@ -92,6 +92,10 @@ const handleStudentFollow = function () { ...@@ -92,6 +92,10 @@ const handleStudentFollow = function () {
</template> </template>
<template #default> <template #default>
<ul class="connect-item_tool"> <ul class="connect-item_tool">
<li @click.stop="handleView" v-if="userStore.role?.id !== 1">
<el-icon size="16" color="#000"><View /></el-icon>
<span>查看</span>
</li>
<li @click.stop="handleStudentFollow" v-if="userStore.role?.id === 1"> <li @click.stop="handleStudentFollow" v-if="userStore.role?.id === 1">
<el-icon size="16" color="#000"><UserFilled /></el-icon> <el-icon size="16" color="#000"><UserFilled /></el-icon>
<span>用户触达</span> <span>用户触达</span>
......
...@@ -27,7 +27,7 @@ let ruleForm = $ref<any>({ ...@@ -27,7 +27,7 @@ let ruleForm = $ref<any>({
name: 1, name: 1,
name_value: '', name_value: '',
status: 1, status: 1,
gender: 1, gender: 100,
mobile: 1, mobile: 1,
create_data: '', create_data: '',
type_name: '' type_name: ''
...@@ -60,14 +60,18 @@ const submitForm = async (formEl: FormInstance | undefined, bl: string) => { ...@@ -60,14 +60,18 @@ const submitForm = async (formEl: FormInstance | undefined, bl: string) => {
}, {}) }, {})
) )
} }
submitScheduleMember(ruleForm).then(res => { submitScheduleMember(ruleForm).then((res: any) => {
if (res.data) { if (res.code === 0) {
ElMessage({ ElMessage({
message: '保存成功', message: '保存成功',
type: 'success' type: 'success'
}) })
}
emit('update:modelValue', false) emit('update:modelValue', false)
} else {
ElMessage({
message: res.message
})
}
}) })
} else { } else {
console.log('error submit!', fields) console.log('error submit!', fields)
...@@ -76,6 +80,13 @@ const submitForm = async (formEl: FormInstance | undefined, bl: string) => { ...@@ -76,6 +80,13 @@ const submitForm = async (formEl: FormInstance | undefined, bl: string) => {
} }
const rules = [{ required: true }] const rules = [{ required: true }]
let genderWoman = ref(0)
watchEffect(() => {
if (ruleForm.gender > 100) ruleForm.gender = 100
if (ruleForm.gender < 0) ruleForm.gender = 0
genderWoman.value = 100 - ruleForm.gender
})
</script> </script>
<template> <template>
...@@ -84,9 +95,18 @@ const rules = [{ required: true }] ...@@ -84,9 +95,18 @@ const rules = [{ required: true }]
class="data-form" class="data-form"
title="自动生成用户数据" title="自动生成用户数据"
:close-on-click-modal="false" :close-on-click-modal="false"
@update:modelValue="value => $emit('update:modelValue', value)"> @update:modelValue="value => $emit('update:modelValue', value)"
>
<div class="button-flex"> <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
: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-form-item label="请输入需要生成的数据量" :rules="rules">
<el-radio-group v-model="ruleForm.size"> <el-radio-group v-model="ruleForm.size">
<el-radio :value="1000">1000</el-radio> <el-radio :value="1000">1000</el-radio>
...@@ -94,6 +114,7 @@ const rules = [{ required: true }] ...@@ -94,6 +114,7 @@ const rules = [{ required: true }]
<el-radio :value="5000">5000</el-radio> <el-radio :value="5000">5000</el-radio>
<el-radio :value="10000">10000</el-radio> <el-radio :value="10000">10000</el-radio>
</el-radio-group> </el-radio-group>
<span style="color: #ccc;font-size: 12px;line-height: 100%;">注意:为了保障系统性能,您最多只能导入10000条数据</span>
</el-form-item> </el-form-item>
<el-form-item label="请选择数据覆盖形式:" :rules="rules"> <el-form-item label="请选择数据覆盖形式:" :rules="rules">
<el-radio-group v-model="ruleForm.cover_type"> <el-radio-group v-model="ruleForm.cover_type">
...@@ -121,11 +142,17 @@ const rules = [{ required: true }] ...@@ -121,11 +142,17 @@ const rules = [{ required: true }]
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="性别" :rules="rules"> <el-form-item label="性别" :rules="rules">
<el-radio-group v-model="ruleForm.gender"> <!-- <el-radio-group v-model="ruleForm.gender">
<el-radio :value="1">随机</el-radio> <el-radio :value="1">随机</el-radio>
<el-radio :value="2"></el-radio> <el-radio :value="2"></el-radio>
<el-radio :value="3"></el-radio> <el-radio :value="3"></el-radio>
</el-radio-group> </el-radio-group> -->
<div>
<div style="display: flex">&nbsp;&nbsp;<el-input v-model="ruleForm.gender"></el-input>&nbsp;&nbsp;%</div>
<div style="display: flex; margin-top: 10px">
&nbsp;&nbsp;<el-input v-model="genderWoman"></el-input>&nbsp;&nbsp;%
</div>
</div>
</el-form-item> </el-form-item>
<el-form-item label="手机号吗" :rules="rules"> <el-form-item label="手机号吗" :rules="rules">
<el-radio-group v-model="ruleForm.mobile"> <el-radio-group v-model="ruleForm.mobile">
......
...@@ -5,9 +5,12 @@ import AppList from '@/components/base/AppList.vue' ...@@ -5,9 +5,12 @@ import AppList from '@/components/base/AppList.vue'
import ListItem from '../components/ListItem.vue' import ListItem from '../components/ListItem.vue'
import { getConnectionList, getConnectionDetails, getScheduleMember, getStudentFollow } from '../api' import { getConnectionList, getConnectionDetails, getScheduleMember, getStudentFollow } from '../api'
import { useMapStore } from '@/stores/map' import { useMapStore } from '@/stores/map'
import { useUserStore } from '@/stores/user'
const store = useMapStore() const store = useMapStore()
const userStore = useUserStore()
const FormDialog = defineAsyncComponent(() => import('../components/FormDialog.vue')) const FormDialog = defineAsyncComponent(() => import('../components/FormDialog.vue'))
const UserDataDialog = defineAsyncComponent(() => import('../components/UserDataDialog.vue')) const UserDataDialog = defineAsyncComponent(() => import('../components/UserDataDialog.vue'))
const EventDataDialog = defineAsyncComponent(() => import('../components/EventDataDialog.vue')) const EventDataDialog = defineAsyncComponent(() => import('../components/EventDataDialog.vue'))
...@@ -131,7 +134,7 @@ const handleStudentFollow = function (experimentId: string, id: string, type: st ...@@ -131,7 +134,7 @@ const handleStudentFollow = function (experimentId: string, id: string, type: st
@viewDataProgress="viewDataProgress" @viewDataProgress="viewDataProgress"
@handleStudentFollow="handleStudentFollow" @handleStudentFollow="handleStudentFollow"
></ListItem> ></ListItem>
<div class="connect-item" @click="createConnect"> <div class="connect-item" @click="createConnect" v-if="userStore.role?.id !== 1">
<div class="connect-add-button"> <div class="connect-add-button">
<el-icon><Plus /></el-icon> <el-icon><Plus /></el-icon>
<span>新建连接</span> <span>新建连接</span>
...@@ -176,4 +179,7 @@ const handleStudentFollow = function (experimentId: string, id: string, type: st ...@@ -176,4 +179,7 @@ const handleStudentFollow = function (experimentId: string, id: string, type: st
margin-right: 10px; margin-right: 10px;
} }
} }
.connect-item{
min-height: 166px;
}
</style> </style>
...@@ -41,7 +41,10 @@ const platformList = [ ...@@ -41,7 +41,10 @@ const platformList = [
{ {
title: '重新获取公众号信息', title: '重新获取公众号信息',
async onClick() { async onClick() {
await asyncOfficialAccountInfo({ connection_id: connectId.value, appid: getAttributeValueByProp('appid') }) await asyncOfficialAccountInfo({
connection_id: connectId.value,
appid: getAttributeValueByProp('appid')
})
ElMessage.success('重新获取公众号信息成功') ElMessage.success('重新获取公众号信息成功')
} }
}, },
...@@ -49,8 +52,14 @@ const platformList = [ ...@@ -49,8 +52,14 @@ const platformList = [
title: '重新获取公众号粉丝', title: '重新获取公众号粉丝',
async onClick() { async onClick() {
const nikeName = getAttributeValueByProp('nikeName') const nikeName = getAttributeValueByProp('nikeName')
await ElMessageBox.confirm(`同步微信公众号粉丝能够将微信粉丝的最新信息同步到本系统中,您确定现在要开始同步公众号“${nikeName}”的粉丝吗?`, '同步微信公众号粉丝') await ElMessageBox.confirm(
await asyncOfficialAccountUsers({ connection_id: connectId.value, appid: getAttributeValueByProp('appid') }) `同步微信公众号粉丝能够将微信粉丝的最新信息同步到本系统中,您确定现在要开始同步公众号“${nikeName}”的粉丝吗?`,
'同步微信公众号粉丝'
)
await asyncOfficialAccountUsers({
connection_id: connectId.value,
appid: getAttributeValueByProp('appid')
})
ElMessage.success(`已经开始同步公众号“${nikeName}”的粉丝,完成时间取决于您公众号的粉丝数量,请耐心等待。`) ElMessage.success(`已经开始同步公众号“${nikeName}”的粉丝,完成时间取决于您公众号的粉丝数量,请耐心等待。`)
} }
} }
...@@ -505,6 +514,25 @@ const platformList = [ ...@@ -505,6 +514,25 @@ const platformList = [
type: 14, type: 14,
type_name: '小程序', type_name: '小程序',
data: [] data: []
},
{
type: 15,
type_name: '紫荆商城',
data: [
{
title: '',
children: [
{
title: '访问紫荆商城',
onClick() {
window.open(
`https://mall-h5-web.ezijing.com?id=${route.query.id}&experiment_id=${route.query.experiment_id}`
)
}
}
]
}
]
} }
] ]
...@@ -527,7 +555,12 @@ const surveyKingDialogVisible = ref<boolean>(false) ...@@ -527,7 +555,12 @@ const surveyKingDialogVisible = ref<boolean>(false)
<AppCard title="查看链接"> <AppCard title="查看链接">
<div class="view-info" v-if="detail"> <div class="view-info" v-if="detail">
<div class="view-info_icon"> <div class="view-info_icon">
<Icon :multiColor="true" :name="iconMap[detail.type] || detail.type" w="50" h="50" /> <Icon
:multiColor="true"
:name="detail.type === '15' ? 'mall' : iconMap[detail.type] || detail.type"
w="50"
h="50"
/>
</div> </div>
<div class="view-info_content"> <div class="view-info_content">
<p>连接名称:{{ getAttributeValueByProp('name') || detail.type_name }}</p> <p>连接名称:{{ getAttributeValueByProp('name') || detail.type_name }}</p>
...@@ -544,7 +577,9 @@ const surveyKingDialogVisible = ref<boolean>(false) ...@@ -544,7 +577,9 @@ const surveyKingDialogVisible = ref<boolean>(false)
<el-tabs class="tabs-box" v-for="(item, index) in platformDataList" :key="index"> <el-tabs class="tabs-box" v-for="(item, index) in platformDataList" :key="index">
<el-tab-pane :label="item.title"> <el-tab-pane :label="item.title">
<div class="tag-box" v-if="item.children.length"> <div class="tag-box" v-if="item.children.length">
<div class="tag" v-for="cItem in item.children" :key="cItem.title" @click="handleClick(cItem)">{{ cItem.title }}</div> <div class="tag" v-for="cItem in item.children" :key="cItem.title" @click="handleClick(cItem)">
{{ cItem.title }}
</div>
</div> </div>
<div class="tag-null" v-else>无数据</div> <div class="tag-null" v-else>无数据</div>
</el-tab-pane> </el-tab-pane>
...@@ -554,7 +589,8 @@ const surveyKingDialogVisible = ref<boolean>(false) ...@@ -554,7 +589,8 @@ const surveyKingDialogVisible = ref<boolean>(false)
v-model="surveyKingDialogVisible" v-model="surveyKingDialogVisible"
:account="getAttributeValueByProp('account')" :account="getAttributeValueByProp('account')"
:password="getAttributeValueByProp('password')" :password="getAttributeValueByProp('password')"
v-if="detail?.type === '13'"></SurveyKingDialog> v-if="detail?.type === '13'"
></SurveyKingDialog>
</template> </template>
<style lang="scss"> <style lang="scss">
......
import httpRequest from '@/utils/axios' import httpRequest from '@/utils/axios'
import type { GroupListRequest, StaticGroupCreateRequest, StaticGroupUpdateRequest, DynamicGroupCreateRequest, DynamicGroupUpdateRequest } from './types' import type {
GroupListRequest,
StaticGroupCreateRequest,
StaticGroupUpdateRequest,
DynamicGroupCreateRequest,
DynamicGroupUpdateRequest,
RFMGroupCreateRequest,
RFMGroupUpdateRequest
} from './types'
// 获取群组列表 // 获取群组列表
export function getGroupList(params?: GroupListRequest) { export function getGroupList(params?: GroupListRequest) {
...@@ -21,6 +29,16 @@ export function createDynamicGroup(data: DynamicGroupCreateRequest) { ...@@ -21,6 +29,16 @@ export function createDynamicGroup(data: DynamicGroupCreateRequest) {
return httpRequest.post('/api/lab/v1/experiment/group/bda-create-dynamic-group', data) return httpRequest.post('/api/lab/v1/experiment/group/bda-create-dynamic-group', data)
} }
// 创建RFM群组
export function createRFMGroup(data: RFMGroupCreateRequest) {
return httpRequest.post('/api/lab/v1/experiment/group/bda-create-frm-group', data)
}
// 更新RFM群组
export function updateRFMGroup(data: RFMGroupUpdateRequest) {
return httpRequest.post('/api/lab/v1/experiment/group/bda-update-frm-group', data)
}
// 更新静态群组 // 更新静态群组
export function updateStaticGroup(data: StaticGroupUpdateRequest) { export function updateStaticGroup(data: StaticGroupUpdateRequest) {
return httpRequest.post('/api/lab/v1/experiment/group/bda-update-static-group', data) return httpRequest.post('/api/lab/v1/experiment/group/bda-update-static-group', data)
......
...@@ -56,7 +56,7 @@ function handleAdd() { ...@@ -56,7 +56,7 @@ function handleAdd() {
</script> </script>
<template> <template>
<el-dialog title="添加群组用户" width="800px" append-to-body @update:modelValue="value => $emit('update:modelValue', value)"> <el-dialog title="添加群组用户" width="980px" append-to-body @update:modelValue="value => $emit('update:modelValue', value)">
<el-form label-suffix=":" label-width="82px"> <el-form label-suffix=":" label-width="82px">
<el-row> <el-row>
<el-col :span="8"> <el-col :span="8">
......
...@@ -2,13 +2,14 @@ ...@@ -2,13 +2,14 @@
import type { Group } from '../types' import type { Group } from '../types'
import type { FormInstance, FormRules } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { updateStatusRuleList, dateUnitList, weekList } from '@/utils/dictionary' import { getNameByValue, updateStatusRuleList, dateUnitList, weekList, groupTypeList } from '@/utils/dictionary'
import { createStaticGroup, updateStaticGroup, createDynamicGroup, updateDynamicGroup, getGroupInfo } from '../api' import { createStaticGroup, updateStaticGroup, createDynamicGroup, updateDynamicGroup, getGroupInfo, createRFMGroup, updateRFMGroup } from '../api'
import UserRule from '@/components/rule/UserRule.vue' import UserRule from '@/components/rule/UserRule.vue'
import EventRule from '@/components/rule/EventRule.vue' import EventRule from '@/components/rule/EventRule.vue'
import LabelRule from '@/components/rule/LabelRule.vue' import LabelRule from '@/components/rule/LabelRule.vue'
import UserActionRule from '@/components/rule/UserActionRule.vue' import UserActionRule from '@/components/rule/UserActionRule.vue'
import { pick } from 'lodash-es' import RFMRule from '@/components/rule/RFMRule.vue'
import { pick, merge } from 'lodash-es'
interface Props { interface Props {
data: Partial<Group> data: Partial<Group>
...@@ -20,16 +21,13 @@ const emit = defineEmits<{ ...@@ -20,16 +21,13 @@ const emit = defineEmits<{
(e: 'update:modelValue', visible: boolean): void (e: 'update:modelValue', visible: boolean): void
}>() }>()
const isUpdate = $computed(() => !!props.data?.id) const isUpdate = computed(() => !!props.data?.id)
const title = $computed(() => { const title = computed(() => {
if (isUpdate) { const typeName = getNameByValue(props.data.type as string, groupTypeList)
return props.data.type === '1' ? '修改静态群组' : '修改动态群组' return isUpdate.value ? `修改${typeName}` : `新建${typeName}`
} else {
return props.data.type === '1' ? '新建静态群组' : '新建动态群组'
}
}) })
const formRef = $ref<FormInstance>() const formRef = ref<FormInstance>()
const form: any = reactive({ const form: any = reactive({
id: '', id: '',
name: '', name: '',
...@@ -40,30 +38,23 @@ const form: any = reactive({ ...@@ -40,30 +38,23 @@ const form: any = reactive({
user_attr_rule: { current_logic_operate: 'and', items: [] }, user_attr_rule: { current_logic_operate: 'and', items: [] },
event_attr_rule: { current_logic_operate: 'and', items: [] }, event_attr_rule: { current_logic_operate: 'and', items: [] },
tag_rule: { current_logic_operate: 'and', items: [] }, tag_rule: { current_logic_operate: 'and', items: [] },
user_action_rule: { current_logic_operate: 'and', items: [] } user_action_rule: { current_logic_operate: 'and', items: [] },
}) rules: { R: {}, F: {}, M: {} }
watchEffect(() => {
if (props.data?.id) {
let updateRule = { type: 1, info: 1 }
try {
updateRule = JSON.parse(props.data.update_rule as string)
} catch (error) {
console.log(error)
}
Object.assign(form, props.data, { update_rule: updateRule })
}
}) })
function genRuleData(data: any) {
if (Array.isArray(data)) data = data[0]
return merge({ current_logic_operate: 'and', items: [] }, data)
}
function fetchInfo() { function fetchInfo() {
if (!props.data.id) return if (!props.data.id) return
getGroupInfo({ id: props.data.id }).then(res => { getGroupInfo({ id: props.data.id }).then(res => {
const { detail } = res.data const { detail } = res.data
const [user_attr_rule = { current_logic_operate: 'and', items: [] }] = detail.user_attr_rule const user_attr_rule = genRuleData(detail.user_attr_rule)
const [event_attr_rule = { current_logic_operate: 'and', items: [] }] = detail.event_attr_rule const event_attr_rule = genRuleData(detail.event_attr_rule)
const [tag_rule = { current_logic_operate: 'and', items: [] }] = detail.tag_rule.map((item: any) => { const user_action_rule = genRuleData(detail.user_action_rule)
return { ...item, items: item.items.map((item: any) => item.id) } const tag_rule = genRuleData(detail.tag_rule)
})
const attrRuleItems = user_attr_rule.items.map((item: any) => { const attrRuleItems = user_attr_rule.items.map((item: any) => {
item.value = ['in', 'not in'].includes(item.operate) ? item.value.split(',') : item.value item.value = ['in', 'not in'].includes(item.operate) ? item.value.split(',') : item.value
return item return item
...@@ -73,11 +64,17 @@ function fetchInfo() { ...@@ -73,11 +64,17 @@ function fetchInfo() {
return item return item
}) })
Object.assign(form, { const tagRuleItems = tag_rule.items.map((item: any, index: number) => {
user_attr_rule: { ...user_attr_rule, attrRuleItems }, const rfm = tag_rule.rfm_tag_map?.[index] || {}
event_attr_rule: { ...event_attr_rule, eventRuleItems }, return { tag_id: item.id, ...rfm }
tag_rule, })
user_action_rule: detail.user_action_rule
Object.assign(form, detail, {
user_attr_rule: { ...user_attr_rule, items: attrRuleItems },
event_attr_rule: { ...event_attr_rule, items: eventRuleItems },
user_action_rule,
tag_rule: { ...tag_rule, items: tagRuleItems },
update_rule: { type: 1, info: 1 }
}) })
}) })
} }
...@@ -85,13 +82,12 @@ function fetchInfo() { ...@@ -85,13 +82,12 @@ function fetchInfo() {
watchEffect(() => fetchInfo()) watchEffect(() => fetchInfo())
const rules = ref<FormRules>({ const rules = ref<FormRules>({
name: [{ required: true, message: '请输入群组名称' }], name: [{ required: true, message: '请输入群组名称' }]
url: [{ required: true, message: '请选择标签类型图标' }]
}) })
// 提交 // 提交
function handleSubmit() { function handleSubmit() {
formRef?.validate().then(() => (isUpdate ? handleUpdate() : handleCreate())) formRef.value?.validate().then(() => (isUpdate.value ? handleUpdate() : handleCreate()))
} }
// 新建 // 新建
async function handleCreate() { async function handleCreate() {
...@@ -99,7 +95,17 @@ async function handleCreate() { ...@@ -99,7 +95,17 @@ async function handleCreate() {
// 静态群组 // 静态群组
const params = pick(form, ['name', 'status']) const params = pick(form, ['name', 'status'])
await createStaticGroup(params) await createStaticGroup(params)
} else { } else if (props.data.type === '2') {
const tagRule = form.tag_rule.items.reduce(
(result: any, item: any, index: number) => {
result.items.push(item.tag_id)
if (item.rfm_key) {
result.rfm_tag_map[index] = item
}
return result
},
{ items: [], rfm_tag_map: {} }
)
// 动态群组 // 动态群组
const params = pick( const params = pick(
{ {
...@@ -107,11 +113,23 @@ async function handleCreate() { ...@@ -107,11 +113,23 @@ async function handleCreate() {
update_rule: JSON.stringify(form.update_rule), update_rule: JSON.stringify(form.update_rule),
user_attr_rule: JSON.stringify([form.user_attr_rule]), user_attr_rule: JSON.stringify([form.user_attr_rule]),
event_attr_rule: JSON.stringify([form.event_attr_rule]), event_attr_rule: JSON.stringify([form.event_attr_rule]),
tag_rule: JSON.stringify([form.tag_rule]) tag_rule: JSON.stringify([{ ...form.tag_rule, ...tagRule }]),
user_action_rule: JSON.stringify(form.user_action_rule)
}, },
['name', 'update_status', 'update_rule', 'user_attr_rule', 'event_attr_rule', 'tag_rule', 'user_action_rule', 'status'] ['name', 'status', 'update_status', 'update_rule', 'user_attr_rule', 'event_attr_rule', 'tag_rule', 'user_action_rule']
) )
await createDynamicGroup(params) await createDynamicGroup(params)
} else {
// RFM群组
const params = pick(
{
...form,
update_rule: JSON.stringify(form.update_rule),
rules: JSON.stringify(form.rules)
},
['name', 'status', 'update_status', 'update_rule', 'rules']
)
await createRFMGroup(params)
} }
ElMessage({ message: '创建成功', type: 'success' }) ElMessage({ message: '创建成功', type: 'success' })
emit('update') emit('update')
...@@ -123,7 +141,8 @@ async function handleUpdate() { ...@@ -123,7 +141,8 @@ async function handleUpdate() {
// 静态群组 // 静态群组
const params = pick(form, ['id', 'name', 'status']) const params = pick(form, ['id', 'name', 'status'])
await updateStaticGroup(params) await updateStaticGroup(params)
} else { } else if (props.data.type === '2') {
// 动态群组
const attrRuleItems = form.user_attr_rule.items.map((item: any) => { const attrRuleItems = form.user_attr_rule.items.map((item: any) => {
item.value = Array.isArray(item.value) ? item.value.join(',') : item.value item.value = Array.isArray(item.value) ? item.value.join(',') : item.value
return item return item
...@@ -132,6 +151,16 @@ async function handleUpdate() { ...@@ -132,6 +151,16 @@ async function handleUpdate() {
item.value = Array.isArray(item.value) ? item.value.join(',') : item.value item.value = Array.isArray(item.value) ? item.value.join(',') : item.value
return item return item
}) })
const tagRule = form.tag_rule.items.reduce(
(result: any, item: any, index: number) => {
result.items.push(item.tag_id)
if (item.rfm_key) {
result.rfm_tag_map[index] = item
}
return result
},
{ items: [], rfm_tag_map: {} }
)
// 动态群组 // 动态群组
const params = pick( const params = pick(
{ {
...@@ -139,12 +168,23 @@ async function handleUpdate() { ...@@ -139,12 +168,23 @@ async function handleUpdate() {
update_rule: JSON.stringify(form.update_rule), update_rule: JSON.stringify(form.update_rule),
user_attr_rule: JSON.stringify([{ ...form.user_attr_rule, items: attrRuleItems }]), user_attr_rule: JSON.stringify([{ ...form.user_attr_rule, items: attrRuleItems }]),
event_attr_rule: JSON.stringify([{ ...form.event_attr_rule, items: eventRuleItems }]), event_attr_rule: JSON.stringify([{ ...form.event_attr_rule, items: eventRuleItems }]),
tag_rule: JSON.stringify([form.tag_rule]), tag_rule: JSON.stringify([{ ...form.tag_rule, ...tagRule }]),
user_action_rule: JSON.stringify(form.user_action_rule) user_action_rule: JSON.stringify(form.user_action_rule)
}, },
['id', 'name', 'update_status', 'update_rule', 'user_attr_rule', 'event_attr_rule', 'tag_rule', 'user_action_rule', 'status'] ['id', 'name', 'status', 'update_status', 'update_rule', 'user_attr_rule', 'event_attr_rule', 'tag_rule', 'user_action_rule']
) )
await updateDynamicGroup(params) await updateDynamicGroup(params)
} else {
// RFM群组
const params = pick({ ...form, update_rule: JSON.stringify(form.update_rule), rules: JSON.stringify(form.rules) }, [
'id',
'name',
'status',
'update_status',
'update_rule',
'rules'
])
await updateRFMGroup(params)
} }
ElMessage({ message: '修改成功', type: 'success' }) ElMessage({ message: '修改成功', type: 'success' })
emit('update') emit('update')
...@@ -153,12 +193,12 @@ async function handleUpdate() { ...@@ -153,12 +193,12 @@ async function handleUpdate() {
</script> </script>
<template> <template>
<el-dialog :title="title" :close-on-click-modal="false" width="800px" @update:modelValue="value => $emit('update:modelValue', value)"> <el-dialog :title="title" :close-on-click-modal="false" width="980px" @update:modelValue="value => $emit('update:modelValue', value)">
<el-form ref="formRef" :model="form" :rules="rules" label-suffix=":" label-width="100px"> <el-form ref="formRef" :model="form" :rules="rules" label-suffix=":" label-width="100px">
<el-form-item label="群组名称" prop="name"> <el-form-item label="群组名称" prop="name">
<el-input v-model="form.name" placeholder="请输入" /> <el-input v-model="form.name" placeholder="请输入" />
</el-form-item> </el-form-item>
<template v-if="data.type === '2'"> <template v-if="data.type !== '1'">
<el-form-item label="更新频率" prop="update_status"> <el-form-item label="更新频率" prop="update_status">
<el-radio-group v-model="form.update_status"> <el-radio-group v-model="form.update_status">
<el-radio v-for="item in updateStatusRuleList" :key="item.value" :value="item.value" :disabled="item.value === '1'"> <el-radio v-for="item in updateStatusRuleList" :key="item.value" :value="item.value" :disabled="item.value === '1'">
...@@ -202,6 +242,9 @@ async function handleUpdate() { ...@@ -202,6 +242,9 @@ async function handleUpdate() {
<LabelRule v-model="form.tag_rule" style="margin-top: 20px"></LabelRule> <LabelRule v-model="form.tag_rule" style="margin-top: 20px"></LabelRule>
<UserActionRule v-model="form.user_action_rule" style="margin-top: 20px"></UserActionRule> <UserActionRule v-model="form.user_action_rule" style="margin-top: 20px"></UserActionRule>
</template> </template>
<template v-if="data.type === '3'">
<RFMRule v-model="form.rules"></RFMRule>
</template>
</el-form> </el-form>
<template #footer> <template #footer>
<el-row justify="center"> <el-row justify="center">
......
...@@ -87,7 +87,8 @@ const listOptions = computed(() => { ...@@ -87,7 +87,8 @@ const listOptions = computed(() => {
} }
}, },
{ label: '更新人', prop: 'updated_operator.real_name' }, { label: '更新人', prop: 'updated_operator.real_name' },
{ label: '更新时间', prop: 'updated_time' } { label: '更新时间', prop: 'updated_time' },
{ label: '操作', slots: 'table-x' }
] ]
} }
}) })
...@@ -99,7 +100,7 @@ function handleRefresh() { ...@@ -99,7 +100,7 @@ function handleRefresh() {
</script> </script>
<template> <template>
<el-dialog title="查看群组信息" width="800px" @update:modelValue="value => $emit('update:modelValue', value)"> <el-dialog title="查看群组信息" width="1000px" @update:modelValue="value => $emit('update:modelValue', value)">
<el-form label-suffix=":" label-width="82px"> <el-form label-suffix=":" label-width="82px">
<el-row> <el-row>
<el-col :span="12"> <el-col :span="12">
...@@ -157,7 +158,13 @@ function handleRefresh() { ...@@ -157,7 +158,13 @@ function handleRefresh() {
</el-card> </el-card>
<el-card style="margin-top: 20px"> <el-card style="margin-top: 20px">
<template #header>群组用户</template> <template #header>群组用户</template>
<AppList v-bind="listOptions" ref="appList"></AppList> <AppList v-bind="listOptions" ref="appList">
<template #table-x="{ row }">
<el-button type="primary" plain>
<router-link target="_blank" :to="{ path: '/user/image', query: { user_id: row.id, experiment_id: row.experiment_id } }">查看</router-link>
</el-button>
</template>
</AppList>
</el-card> </el-card>
<template #footer> <template #footer>
......
...@@ -3,9 +3,8 @@ import type { Operator } from '@/types' ...@@ -3,9 +3,8 @@ import type { Operator } from '@/types'
export interface Group { export interface Group {
id: string id: string
name: string name: string
type_id: string
status: string status: string
type: '1' | '2' type: '1' | '2' | '3'
update_status: string // '1' | '2' update_status: string // '1' | '2'
update_rule: string update_rule: string
created_time: string created_time: string
...@@ -16,6 +15,7 @@ export interface Group { ...@@ -16,6 +15,7 @@ export interface Group {
event_attr_rule?: string event_attr_rule?: string
tag_rule?: string tag_rule?: string
record?: any record?: any
rules?: string
} }
export type GroupListRequest = Pick<Group, 'id' | 'name'> & { experiment_id?: string } export type GroupListRequest = Pick<Group, 'id' | 'name'> & { experiment_id?: string }
...@@ -35,6 +35,11 @@ export type DynamicGroupUpdateRequest = Pick< ...@@ -35,6 +35,11 @@ export type DynamicGroupUpdateRequest = Pick<
} }
export type DynamicGroupCreateRequest = Omit<DynamicGroupUpdateRequest, 'id'> export type DynamicGroupCreateRequest = Omit<DynamicGroupUpdateRequest, 'id'>
// RFM群组
export type RFMGroupUpdateRequest = Pick<Group, 'id' | 'name' | 'update_status' | 'update_rule' | 'rules' | 'status'> & {
experiment_id?: string
}
export type RFMGroupCreateRequest = Omit<RFMGroupUpdateRequest, 'id'>
export interface GroupMember { export interface GroupMember {
id: string id: string
name: string name: string
......
...@@ -7,6 +7,9 @@ import { getNameByValue, groupTypeList, updateStatusRuleList } from '@/utils/dic ...@@ -7,6 +7,9 @@ import { getNameByValue, groupTypeList, updateStatusRuleList } from '@/utils/dic
import { getGroupList, deleteGroup } from '../api' import { getGroupList, deleteGroup } from '../api'
import { useMapStore } from '@/stores/map' import { useMapStore } from '@/stores/map'
import SelectUser from '@/components/SelectUser.vue' import SelectUser from '@/components/SelectUser.vue'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const FormDialog = defineAsyncComponent(() => import('../components/FormDialog.vue')) const FormDialog = defineAsyncComponent(() => import('../components/FormDialog.vue'))
const ViewDialog = defineAsyncComponent(() => import('../components/ViewDialog.vue')) const ViewDialog = defineAsyncComponent(() => import('../components/ViewDialog.vue'))
...@@ -61,7 +64,13 @@ const listOptions = computed(() => { ...@@ -61,7 +64,13 @@ const listOptions = computed(() => {
return `<span style="color: ${color}">${getNameByValue(row.status, statusList)}</span>` return `<span style="color: ${color}">${getNameByValue(row.status, statusList)}</span>`
} }
}, },
{ label: '更新人', prop: 'created_operator.real_name' }, {
label: '更新人',
prop: 'created_operator.real_name',
computed({ row }: any) {
return row.updated_operator?.real_name || row.updated_operator?.nickname
}
},
{ label: '更新时间', prop: 'updated_time' }, { label: '更新时间', prop: 'updated_time' },
{ label: '操作', slots: 'table-x', width: 240 } { label: '操作', slots: 'table-x', width: 240 }
] ]
...@@ -82,7 +91,7 @@ let formVisible = $ref(false) ...@@ -82,7 +91,7 @@ let formVisible = $ref(false)
let currentRow = $ref<Partial<Group>>() let currentRow = $ref<Partial<Group>>()
// 新建 // 新建
function handleAdd(type: '1' | '2') { function handleAdd(type: '1' | '2' | '3') {
currentRow = { type } currentRow = { type }
formVisible = true formVisible = true
} }
...@@ -114,12 +123,14 @@ function handleView(row: Group) { ...@@ -114,12 +123,14 @@ function handleView(row: Group) {
<AppList v-bind="listOptions" ref="appList" @selection-change="handleSelectionChange"> <AppList v-bind="listOptions" ref="appList" @selection-change="handleSelectionChange">
<template #header-buttons> <template #header-buttons>
<el-space> <el-space>
<el-dropdown v-permission="['experiment_group_create_dynamic_group', 'experiment_group_create_static_group']"> <!-- v-permission="['experiment_group_create_dynamic_group', 'experiment_group_create_static_group']" -->
<el-button type="primary" :icon="Plus">新建</el-button> <el-dropdown>
<el-button type="primary" :icon="Plus" v-if="!userStore.status.group_status">新建</el-button>
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item @click="handleAdd('2')">新建动态群组</el-dropdown-item> <el-dropdown-item @click="handleAdd('2')">新建动态群组</el-dropdown-item>
<el-dropdown-item @click="handleAdd('1')">新建静态群组</el-dropdown-item> <el-dropdown-item @click="handleAdd('1')">新建静态群组</el-dropdown-item>
<!--<el-dropdown-item @click="handleAdd('3')">新建RFM群组</el-dropdown-item>-->
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown>
...@@ -159,7 +170,8 @@ function handleView(row: Group) { ...@@ -159,7 +170,8 @@ function handleView(row: Group) {
v-model="formVisible" v-model="formVisible"
:data="currentRow" :data="currentRow"
@update="handleRefresh" @update="handleRefresh"
v-if="formVisible && currentRow"></FormDialog> v-if="formVisible && currentRow"
></FormDialog>
<!-- 查看 --> <!-- 查看 -->
<ViewDialog v-model="viewVisible" :data="(currentRow as Group)" v-if="viewVisible && currentRow"></ViewDialog> <ViewDialog v-model="viewVisible" :data="(currentRow as Group)" v-if="viewVisible && currentRow"></ViewDialog>
</template> </template>
...@@ -3,7 +3,7 @@ import type { LabelTypeListRequest, LabelTypeCreateRequest, LabelTypeUpdateReque ...@@ -3,7 +3,7 @@ import type { LabelTypeListRequest, LabelTypeCreateRequest, LabelTypeUpdateReque
// 获取标签类型列表 // 获取标签类型列表
export function getLabelTypeList(params?: LabelTypeListRequest) { export function getLabelTypeList(params?: LabelTypeListRequest) {
return httpRequest.get('/api/lab/v1/experiment/tag-type/list', { params }) return httpRequest.get('/api/lab/v1/experiment/tag-type/bda-list', { params })
} }
// 创建标签类型 // 创建标签类型
......
...@@ -10,6 +10,9 @@ import LevelRule from '@/components/rule/LevelRule.vue' ...@@ -10,6 +10,9 @@ import LevelRule from '@/components/rule/LevelRule.vue'
import EventPreferenceRule from '@/components/rule/EventPreferenceRule.vue' import EventPreferenceRule from '@/components/rule/EventPreferenceRule.vue'
import EventTargetRule from '@/components/rule/EventTargetRule.vue' import EventTargetRule from '@/components/rule/EventTargetRule.vue'
import RFMRule from '@/components/rule/RFMRule.vue' import RFMRule from '@/components/rule/RFMRule.vue'
import CustomRule from '@/components/rule/CustomRule.vue'
import SingleUserRule from '@/components/rule/SingleUserRule.vue'
import UserRule from '@/components/rule/UserRule.vue'
const props = defineProps<{ const props = defineProps<{
data: Label data: Label
...@@ -22,7 +25,7 @@ const emit = defineEmits<{ ...@@ -22,7 +25,7 @@ const emit = defineEmits<{
const userStore = useUserStore() const userStore = useUserStore()
const disabled = computed(() => userStore.status) const disabled = computed(() => userStore.status.tag_status)
const statusList = useMapStore().getMapValuesByKey('system_status') const statusList = useMapStore().getMapValuesByKey('system_status')
...@@ -36,16 +39,7 @@ const form = reactive({ ...@@ -36,16 +39,7 @@ const form = reactive({
function fetchInfo() { function fetchInfo() {
getLabelRule({ id: props.data.id }).then(res => { getLabelRule({ id: props.data.id }).then(res => {
const { detail } = res.data const { detail } = res.data
// const [user_attr_rule = { current_logic_operate: 'and', items: [] }] = detail.user_attr_rule
// const [event_attr_rule = { current_logic_operate: 'and', items: [] }] = detail.event_attr_rule
// const attrRuleItems = user_attr_rule.items.map((item: any) => {
// item.value = ['in', 'not in'].includes(item.operate) ? item.value.split(',') : item.value
// return item
// })
// const eventRuleItems = event_attr_rule.items.map((item: any) => {
// item.value = ['in', 'not in'].includes(item.operate) ? item.value.split(',') : item.value
// return item
// })
let rules = detail.rules let rules = detail.rules
if (detail.rules.length === 0) { if (detail.rules.length === 0) {
if (detail.label == '2') { if (detail.label == '2') {
...@@ -71,6 +65,19 @@ function fetchInfo() { ...@@ -71,6 +65,19 @@ function fetchInfo() {
if (detail.label === '4') { if (detail.label === '4') {
rules = { R: {}, F: {}, M: {} } rules = { R: {}, F: {}, M: {} }
} }
if (detail.label === '5') {
rules = { attr_id: '', attr: '', attr_name: '', attr_type: '', operate: '', operate_name: '', value: '' }
}
if (detail.label === '6') {
rules = { current_logic_operate: 'and', items: [] }
}
if (detail.label === '7') {
rules = {
user_attr_rule: { current_logic_operate: 'and', items: [] },
event_attr_rule: { current_logic_operate: 'and', items: [] },
user_action_rule: { current_logic_operate: 'and', items: [] }
}
}
} }
Object.assign(form, { id: props.data.id, rules }) Object.assign(form, { id: props.data.id, rules })
}) })
...@@ -132,16 +139,20 @@ function handleUpdate() { ...@@ -132,16 +139,20 @@ function handleUpdate() {
</el-row> </el-row>
</el-form> </el-form>
<el-form :model="form" inline ref="formRef" :disabled="disabled" @submit.prevent v-if="form.id"> <el-form :model="form" inline ref="formRef" :disabled="disabled" @submit.prevent v-if="form.id">
<!-- 自定义分层 --> <!-- 分层 -->
<LevelRule v-model="form.rules" v-if="data.label == '1'"></LevelRule> <LevelRule v-model="form.rules" v-if="data.label == '1'"></LevelRule>
<!-- 事件偏好 --> <!-- 事件偏好 -->
<EventPreferenceRule v-model="form.rules" v-if="data.label == '2'"></EventPreferenceRule> <EventPreferenceRule v-model="form.rules" v-if="data.label == '2'"></EventPreferenceRule>
<!-- 事件指标 --> <!-- 事件指标 -->
<EventTargetRule v-model="form.rules" v-if="data.label == '3'"></EventTargetRule> <EventTargetRule v-model="form.rules" v-if="data.label == '3'"></EventTargetRule>
<!-- RFM模型 --> <!-- RFM模型 -->
<RFMRule v-model="form.rules" v-if="data.label == '4'"></RFMRule> <RFMRule v-model="form.rules" :tagId="form.id" v-if="data.label == '4'"></RFMRule>
<!-- 用户属性规则 --> <!-- 自定义 -->
<!-- <UserRule></UserRule> --> <CustomRule v-model="form.rules" v-if="data.label == '7'"></CustomRule>
<!-- 单属性 -->
<SingleUserRule v-model="form.rules" v-if="data.label == '5'"></SingleUserRule>
<!-- 多属性规则 -->
<UserRule v-model="form.rules" v-if="data.label == '6'"></UserRule>
<!-- 事件属性规则 --> <!-- 事件属性规则 -->
<!-- <EventRule style="margin-top: 20px"></EventRule> --> <!-- <EventRule style="margin-top: 20px"></EventRule> -->
</el-form> </el-form>
......
...@@ -4,6 +4,9 @@ import { Edit, Delete } from '@element-plus/icons-vue' ...@@ -4,6 +4,9 @@ import { Edit, Delete } from '@element-plus/icons-vue'
import { ElMessageBox, ElMessage } from 'element-plus' import { ElMessageBox, ElMessage } from 'element-plus'
import { deleteLabelType } from '../api' import { deleteLabelType } from '../api'
import { useLabelType } from '../composables/useLabelType' import { useLabelType } from '../composables/useLabelType'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
defineProps<{ activeId: string }>() defineProps<{ activeId: string }>()
...@@ -40,33 +43,58 @@ function handleRemove(row: LabelType) { ...@@ -40,33 +43,58 @@ function handleRemove(row: LabelType) {
</script> </script>
<template> <template>
<el-button type="primary" style="width: 100%" @click="handleAdd" v-permission="'experiment_tag_type_create'">添加标签目录</el-button> <el-button type="primary" style="width: 100%" @click="handleAdd" v-if="!userStore.status.tag_status"
>添加标签目录</el-button
>
<div class="label-type-total" @click="$emit('select', '')"> <div class="label-type-total" @click="$emit('select', '')">
<h4>全部标签</h4> <h4>全部标签</h4>
<p>{{ labelCount }}</p> <p>{{ labelCount }}</p>
</div> </div>
<ul> <ul>
<li class="label-type-item" :class="{ 'is-active': item.id === activeId }" v-for="item in typeList" :key="item.id" @click="$emit('select', item.id)"> <li
class="label-type-item"
:class="{ 'is-active': item.id === activeId }"
v-for="item in typeList"
:key="item.id"
@click="$emit('select', item.id)"
>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
:fill="item.url" :fill="item.url"
width="16.67333984375" width="16.67333984375"
height="16.669540405273438" height="16.669540405273438"
viewBox="0 0 16.67333984375 16.669540405273438"> viewBox="0 0 16.67333984375 16.669540405273438"
>
<g> <g>
<path <path
d="M16.6733,8.79462Q16.6733,7.76343,15.9463,7.03212L15.9442,7.03002L9.20711,0.292893C9.01957,0.105357,8.76522,0,8.5,0L1,0C0.447715,0,0,0.447715,0,1L0,8.5C0,8.76541,0.10551,9.01993,0.293287,9.2075L7.02965,15.9364Q7.76207,16.6695,8.79838,16.6695Q9.83479,16.6695,10.5667,15.9367L15.9463,10.5571Q16.6733,9.82582,16.6733,8.79462ZM6.41683,5.37499C6.41683,5.95029,5.95045,6.41666,5.37516,6.41666C4.79987,6.41666,4.3335,5.95029,4.3335,5.37499C4.3335,4.7997,4.79987,4.33333,5.37516,4.33333C5.95045,4.33333,6.41683,4.7997,6.41683,5.37499Z" /> d="M16.6733,8.79462Q16.6733,7.76343,15.9463,7.03212L15.9442,7.03002L9.20711,0.292893C9.01957,0.105357,8.76522,0,8.5,0L1,0C0.447715,0,0,0.447715,0,1L0,8.5C0,8.76541,0.10551,9.01993,0.293287,9.2075L7.02965,15.9364Q7.76207,16.6695,8.79838,16.6695Q9.83479,16.6695,10.5667,15.9367L15.9463,10.5571Q16.6733,9.82582,16.6733,8.79462ZM6.41683,5.37499C6.41683,5.95029,5.95045,6.41666,5.37516,6.41666C4.79987,6.41666,4.3335,5.95029,4.3335,5.37499C4.3335,4.7997,4.79987,4.33333,5.37516,4.33333C5.95045,4.33333,6.41683,4.7997,6.41683,5.37499Z"
/>
</g> </g>
</svg> </svg>
<p>{{ item.name }}</p> <p>{{ item.name }}</p>
<div class="label-type-actions"> <div class="label-type-actions" v-if="!item.is_default">
<el-icon class="label-type-item__edit" @click.stop="handleUpdate(item)" v-permission="'experiment_tag_type_update'"><Edit /></el-icon> <el-icon
<el-icon class="label-type-item__remove" @click.stop="handleRemove(item)" v-permission="'experiment_tag_type_delete'"><Delete /></el-icon> class="label-type-item__edit"
@click.stop="handleUpdate(item)"
v-permission="'experiment_tag_type_update'"
><Edit
/></el-icon>
<el-icon
class="label-type-item__remove"
@click.stop="handleRemove(item)"
v-permission="'experiment_tag_type_delete'"
><Delete
/></el-icon>
</div> </div>
</li> </li>
</ul> </ul>
<LabelTypeFormDialog v-model="formVisible" :data="currentRow" @update="fetchTypeList" v-if="formVisible"></LabelTypeFormDialog> <LabelTypeFormDialog
v-model="formVisible"
:data="currentRow"
@update="fetchTypeList"
v-if="formVisible"
></LabelTypeFormDialog>
</template> </template>
<style lang="scss"> <style lang="scss">
......
...@@ -65,14 +65,15 @@ const listOptions = computed(() => { ...@@ -65,14 +65,15 @@ const listOptions = computed(() => {
} }
}, },
{ label: '更新人', prop: 'updated_operator.real_name' }, { label: '更新人', prop: 'updated_operator.real_name' },
{ label: '更新时间', prop: 'updated_time' } { label: '更新时间', prop: 'updated_time' },
{ label: '操作', slots: 'table-x' }
] ]
} }
}) })
</script> </script>
<template> <template>
<el-dialog title="查看标签信息" width="800px"> <el-dialog title="查看标签信息" width="1000px">
<el-form label-suffix=":" label-width="82px"> <el-form label-suffix=":" label-width="82px">
<el-row> <el-row>
<el-col :span="12"> <el-col :span="12">
...@@ -119,7 +120,13 @@ const listOptions = computed(() => { ...@@ -119,7 +120,13 @@ const listOptions = computed(() => {
</el-card> </el-card>
<el-card style="margin-top: 20px"> <el-card style="margin-top: 20px">
<template #header>标签用户</template> <template #header>标签用户</template>
<AppList v-bind="listOptions" ref="appList"></AppList> <AppList v-bind="listOptions" ref="appList">
<template #table-x="{ row }">
<el-button type="primary" plain>
<router-link target="_blank" :to="{ path: '/user/image', query: { user_id: row.id, experiment_id: row.experiment_id } }">查看</router-link>
</el-button>
</template>
</AppList>
</el-card> </el-card>
<template #footer> <template #footer>
<el-row justify="center"> <el-row justify="center">
......
...@@ -7,7 +7,11 @@ export function useLabelType() { ...@@ -7,7 +7,11 @@ export function useLabelType() {
function fetchTypeList() { function fetchTypeList() {
getLabelTypeList().then(res => { getLabelTypeList().then(res => {
labelCount.value = res.data.tag_count labelCount.value = res.data.tag_count
typeList.value = res.data.items let defaultItems = res.data.default_items || []
defaultItems = defaultItems.map((item: LabelType) => {
return { ...item, is_default: true }
})
typeList.value = [...defaultItems, ...res.data.items]
}) })
} }
onMounted(() => { onMounted(() => {
......
...@@ -5,6 +5,7 @@ export interface LabelType { ...@@ -5,6 +5,7 @@ export interface LabelType {
id: string id: string
name: string name: string
url: string url: string
is_default?: boolean
} }
export type LabelTypeListRequest = Pick<LabelType, 'id' | 'name'> & { experiment_id?: string } export type LabelTypeListRequest = Pick<LabelType, 'id' | 'name'> & { experiment_id?: string }
......
...@@ -9,6 +9,9 @@ import { useMapStore } from '@/stores/map' ...@@ -9,6 +9,9 @@ import { useMapStore } from '@/stores/map'
import { getNameByValue, updateStatusRuleList, labelList } from '@/utils/dictionary' import { getNameByValue, updateStatusRuleList, labelList } from '@/utils/dictionary'
import { useLabelType } from '../composables/useLabelType' import { useLabelType } from '../composables/useLabelType'
import SelectUser from '@/components/SelectUser.vue' import SelectUser from '@/components/SelectUser.vue'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const LabelFormDialog = defineAsyncComponent(() => import('../components/LabelFormDialog.vue')) const LabelFormDialog = defineAsyncComponent(() => import('../components/LabelFormDialog.vue'))
const LabelViewDialog = defineAsyncComponent(() => import('../components/LabelViewDialog.vue')) const LabelViewDialog = defineAsyncComponent(() => import('../components/LabelViewDialog.vue'))
...@@ -82,7 +85,13 @@ const listOptions = computed(() => { ...@@ -82,7 +85,13 @@ const listOptions = computed(() => {
return `<span style="color: ${color}">${getNameByValue(row.status, statusList)}</span>` return `<span style="color: ${color}">${getNameByValue(row.status, statusList)}</span>`
} }
}, },
{ label: '更新人', prop: 'updated_operator.real_name' }, {
label: '更新人',
prop: 'updated_operator.real_name',
computed({ row }: any) {
return row.updated_operator?.real_name || row.updated_operator?.nickname
}
},
{ label: '更新时间', prop: 'updated_time' }, { label: '更新时间', prop: 'updated_time' },
{ label: '操作', slots: 'table-x', width: 320 } { label: '操作', slots: 'table-x', width: 320 }
] ]
...@@ -135,14 +144,15 @@ function handleSelect(id: string) { ...@@ -135,14 +144,15 @@ function handleSelect(id: string) {
}) })
} }
</script> </script>
<!-- import { useUserStore } from '@/stores/user'
const userStore = useUserStore() -->
<template> <template>
<AppCard> <AppCard>
<div class="label-wrap"> <div class="label-wrap">
<div class="label-left"><LabelType :active-id="listParams.type_id" @select="handleSelect"></LabelType></div> <div class="label-left"><LabelType :active-id="listParams.type_id" @select="handleSelect"></LabelType></div>
<AppList v-bind="listOptions" ref="appList" class="label-right"> <AppList v-bind="listOptions" ref="appList" class="label-right">
<template #header-buttons> <template #header-buttons>
<el-button type="primary" :icon="Plus" @click="handleAdd" v-permission="'experiment_tag_create'">新建</el-button> <el-button type="primary" :icon="Plus" @click="handleAdd" v-if="!userStore.status.tag_status">新建</el-button>
</template> </template>
<template #filter-user> <template #filter-user>
<SelectUser v-model="listParams.updated_operator" placeholder="更新人" @change="handleRefresh"></SelectUser> <SelectUser v-model="listParams.updated_operator" placeholder="更新人" @change="handleRefresh"></SelectUser>
...@@ -151,14 +161,23 @@ function handleSelect(id: string) { ...@@ -151,14 +161,23 @@ function handleSelect(id: string) {
<template #table-x="{ row }"> <template #table-x="{ row }">
<el-button type="primary" plain @click="handleRule(row)">规则</el-button> <el-button type="primary" plain @click="handleRule(row)">规则</el-button>
<el-button type="primary" plain @click="handleView(row)">查看</el-button> <el-button type="primary" plain @click="handleView(row)">查看</el-button>
<el-button type="primary" plain @click="handleUpdate(row)" v-permission="'experiment_tag_update'">编辑</el-button> <el-button type="primary" plain @click="handleUpdate(row)" v-permission="'experiment_tag_update'"
<el-button type="primary" plain @click="handleRemove(row)" v-permission="'experiment_tag_delete'">删除</el-button> >编辑</el-button
>
<el-button type="primary" plain @click="handleRemove(row)" v-permission="'experiment_tag_delete'"
>删除</el-button
>
</template> </template>
</AppList> </AppList>
</div> </div>
</AppCard> </AppCard>
<!-- 新建/修改标签 --> <!-- 新建/修改标签 -->
<LabelFormDialog v-model="formVisible" :data="currentRow" @update="handleRefresh" v-if="formVisible"></LabelFormDialog> <LabelFormDialog
v-model="formVisible"
:data="currentRow"
@update="handleRefresh"
v-if="formVisible"
></LabelFormDialog>
<!-- 查看标签 --> <!-- 查看标签 -->
<LabelViewDialog v-model="viewVisible" :data="currentRow" v-if="viewVisible && currentRow"></LabelViewDialog> <LabelViewDialog v-model="viewVisible" :data="currentRow" v-if="viewVisible && currentRow"></LabelViewDialog>
<!-- 规则 --> <!-- 规则 -->
...@@ -180,6 +199,7 @@ function handleSelect(id: string) { ...@@ -180,6 +199,7 @@ function handleSelect(id: string) {
} }
.label-right { .label-right {
flex: 1; flex: 1;
overflow: hidden; overflow-x: hidden;
overflow-y: auto;
} }
</style> </style>
import httpRequest from '@/utils/axios' import httpRequest from '@/utils/axios'
// 新建资料 // 新建资料
export function createMaterial(data: { name: string; type: string; content: string; status: string }) { export function createMaterial(data: any) {
return httpRequest.post('/api/lab/v1/experiment/marketing-ai/create', data) return httpRequest.post('/api/lab/v1/experiment/marketing-ai/create', data)
} }
...@@ -44,3 +44,17 @@ export function getAIUsage(params: { marketing_material_id: string }) { ...@@ -44,3 +44,17 @@ export function getAIUsage(params: { marketing_material_id: string }) {
export function postAIChat(data: { marketing_material_id: string; context: string; type: number; chart_id: string | null }) { export function postAIChat(data: { marketing_material_id: string; context: string; type: number; chart_id: string | null }) {
return httpRequest.post('/api/lab/v1/experiment/marketing-ai/sky-agents-chat', data) return httpRequest.post('/api/lab/v1/experiment/marketing-ai/sky-agents-chat', data)
} }
// 天工3.0文字生成图片
export function postGenerateImage(data: { marketing_material_id: string; context: string; type: number; chart_id: string | null }) {
return httpRequest.post('/api/lab/v1/experiment/marketing-ai/sky-agent3-generate-image', data)
}
// 创客贴-查询我的设计(排序+筛选)
export function getChuanKitDesignList(data: { user_flag: string; page_no: number; page_size: number; time_order: number }) {
return httpRequest.post('/api/lab/v1/experiment/marketing-ai/chuangkit-designs', data)
}
// 创客贴-服务端图片获取
export function getChuanKitSourceImage(data: { userFlag: string; designId: string }) {
return httpRequest.post('/api/lab/v1/experiment/marketing-ai/chuangkit-source-image', data)
}
...@@ -5,11 +5,21 @@ import { useMapStore } from '@/stores/map' ...@@ -5,11 +5,21 @@ import { useMapStore } from '@/stores/map'
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
import { useConnection } from '../composables/useConnection' import { useConnection } from '../composables/useConnection'
import { useIndustry } from '../composables/useIndustry' import { useIndustry } from '../composables/useIndustry'
import { getNameByValue, materialMethodList, materialUsageList, materialUsersList } from '@/utils/dictionary' import {
getNameByValue,
materialMethodList,
materialUsageList,
materialUsersList,
materialPictureStyleList,
textPurposeList
} from '@/utils/dictionary'
import IconComputer from './IconComputer.vue' import IconComputer from './IconComputer.vue'
import IconUser from './IconUser.vue' import IconUser from './IconUser.vue'
import IconAI from './IconAI.vue' import IconAI from './IconAI.vue'
import xss from 'xss' import xss from 'xss'
import { uploadFileByUrl } from '@/utils/upload'
const emit = defineEmits(['submit'])
const form = defineModel() const form = defineModel()
...@@ -18,25 +28,39 @@ const { industryList } = useIndustry() ...@@ -18,25 +28,39 @@ const { industryList } = useIndustry()
const { connectionList } = useConnection() const { connectionList } = useConnection()
const welcomeMessage = computed(() => { const welcomeMessage = computed(() => {
const way = getNameByValue(form.value.way, materialMethodList) const data = form.value
const type = getNameByValue(form.value.type, materialType) const way = getNameByValue(data.way, materialMethodList)
const industry = industryList.value.find(item => item.id == form.value.industry_id)?.name const type = getNameByValue(data.type, materialType)
const personnel = getNameByValue(form.value.personnel_type, materialUsersList) const industry = industryList.value.find(item => item.id == data.industry_id)?.name
const scenario = getNameByValue(form.value.scenario_type, materialUsageList) const personnel = getNameByValue(data.personnel_type, materialUsersList)
const connection = connectionList.value.find(item => item.id == form.value.channel)?.type_name const scenario = getNameByValue(data.scenario_type, materialUsageList)
const key = form.value.key_points const connection = connectionList.value.find(item => item.id == data.channel)?.type_name
return `你将以<b class="bold">${way}</b>的方式创作一个<b class="bold">${type}内容</b>,该营销内容的所属行业是<b class="bold">${industry}</b>,主要使用人员是<b class="bold">${personnel}</b>,主要使用的场景是用于<b class="bold">${scenario}</b>,主要投放渠道是在<b class="bold">${connection}</b>,内容的关键突出点包含了<b class="bold">${key}</b>。` const extendInfo = data.extend_info || {}
const pictureStyle = getNameByValue(extendInfo.picture_style, materialPictureStyleList)
const textPurpose = getNameByValue(extendInfo.text_use, textPurposeList)
console.log(textPurpose, 'textPurpose')
if (data.type == 1) {
return `请帮我创作一个在<b class="bold">“${industry}行业”</b>,使用的“<b class="bold">${textPurpose}”</b>,这个内容的使用人员是“<b class="bold">${personnel}”</b>,通过<b class="bold">“${connection}”</b>渠道进行投放,使用场景是<b class="bold">“${scenario}”</b>,字数控制在“<b class="bold">${extendInfo.text_count}字以内</b>“,关键点需要包含:<b class="bold">${data.key_points}</b>。`
} else if (data.type == 2) {
return `<b class="bold">${extendInfo.person_des}${extendInfo.scene_des}</b>,重点突出<b class="bold">${extendInfo.important_info_desc}${pictureStyle}</b>`
}
return `你将以<b class="bold">${way}</b>的方式创作一个<b class="bold">${type}内容</b>,该营销内容的所属行业是<b class="bold">${industry}</b>,主要使用人员是<b class="bold">${personnel}</b>,主要使用的场景是用于<b class="bold">${scenario}</b>,主要投放渠道是在<b class="bold">${connection}</b>,内容的关键突出点包含了<b class="bold">${data.key_points}</b>。`
}) })
const content = ref('') const content = ref('')
const route = useRoute() const route = useRoute()
const { usages, messages, post, isLoading } = useChat({ experiment_id: route.query.experiment_id, marketing_material_id: form.value.id }) const { usages, messages, post, isLoading } = useChat({
experiment_id: route.query.experiment_id,
marketing_material_id: form.value.id,
fileType: form.value.type
})
onMounted(() => { onMounted(() => {
messages.value.push({ role: 'system', content: welcomeMessage.value }) messages.value.push({ role: 'system', content: welcomeMessage.value })
if (form.value.content) { // if (form.value.content) {
messages.value.push({ role: 'bot', content: form.value.content }) // messages.value.push({ role: 'bot', content: form.value.content })
} // }
}) })
watch(welcomeMessage, () => { watch(welcomeMessage, () => {
...@@ -53,9 +77,8 @@ watch(welcomeMessage, () => { ...@@ -53,9 +77,8 @@ watch(welcomeMessage, () => {
async function postMessage() { async function postMessage() {
if (!content.value) return if (!content.value) return
console.log(content.value)
messages.value.push({ role: 'user', content: content.value }) messages.value.push({ role: 'user', content: content.value })
post({ context: content.value, type: '1' }) post({ content: content.value, type: '1' })
content.value = '' content.value = ''
} }
...@@ -65,26 +88,31 @@ async function handleSend(event) { ...@@ -65,26 +88,31 @@ async function handleSend(event) {
await postMessage() await postMessage()
} }
async function handleSendType(type, context) { async function handleSendType(type, content) {
const userName = useUserStore().user.name const userName = useUserStore().user.name
context = xss(context, { content = xss(content, {
whiteList: {}, // 白名单为空,表示过滤所有标签 whiteList: {}, // 白名单为空,表示过滤所有标签
stripIgnoreTag: true, // 过滤所有非白名单标签的HTML stripIgnoreTag: true, // 过滤所有非白名单标签的HTML
stripIgnoreTagBody: ['script'] // script标签较特殊,需要过滤标签中间的内容 stripIgnoreTagBody: ['script'] // script标签较特殊,需要过滤标签中间的内容
}) })
switch (type) { switch (type) {
case 2: case 2:
context = `我是${userName},请帮我创作一个文本内容,${context.replace('你将以在线AI 的方式创作一个文本内容,', '')}` content = `${content}`
break break
case 3: case 3:
context = `我是${userName},请帮我润色一个文本内容,${context.replace('你将以在线AI 的方式创作一个文本内容,', '')}` content = `我是${userName},请帮我改写以下内容:${content.replace('请帮我创作一个', '')}`
break break
case 4: case 4:
context = `我是${userName},请帮我扩写一个文本内容,${context.replace('你将以在线AI 的方式创作一个文本内容,', '')}` content = `我是${userName},请帮我扩写以下内容:${content.replace('请帮我创作一个', '')}`
break
case 7:
content = `我是${userName},请帮我缩写以下内容:${content.replace('请帮我创作一个', '')}`
break
case 8:
content = `我是${userName},请帮我总结以下内容:${content.replace('请帮我创作一个', '')}`
break break
} }
post({ type, content })
post({ type, context })
} }
const chatRef = ref() const chatRef = ref()
...@@ -109,6 +137,12 @@ function handleCopy(content) { ...@@ -109,6 +137,12 @@ function handleCopy(content) {
function parseHtml(content) { function parseHtml(content) {
return content.replaceAll('\n', '<br/>') return content.replaceAll('\n', '<br/>')
} }
// 保存图片
async function handleSave(message) {
const url = await uploadFileByUrl(message.image_url)
form.value.content = url
emit('submit', form.value)
}
</script> </script>
<template> <template>
...@@ -120,21 +154,54 @@ function parseHtml(content) { ...@@ -120,21 +154,54 @@ function parseHtml(content) {
<IconAI v-else /> <IconAI v-else />
</div> </div>
<div class="chat-message-main"> <div class="chat-message-main">
<div class="chat-message-content" v-html="parseHtml(item.content)"></div> <div class="chat-message-content">
<div class="chat-message-extra" v-if="item.role !== 'user'"> <div v-if="item.type === 'image'">
<img :src="item.image_url" />
</div>
<div v-else v-html="parseHtml(item.content)"></div>
</div>
<div class="chat-message-extra">
<!-- 文本 -->
<template v-if="form.type == 1 && item.role !== 'user'">
<el-button size="small" type="primary" @click="handleCopy(item.content)">复制</el-button> <el-button size="small" type="primary" @click="handleCopy(item.content)">复制</el-button>
<el-button size="small" type="primary" @click="handleSendType(5, item.input || item.content)" v-if="item.role == 'bot'" <el-button
size="small"
type="primary"
@click="handleSendType(5, item.input || item.content)"
v-if="item.role == 'bot'"
>刷新({{ usages.ai_refresh_count }}/{{ usages.ai_refresh_max_count }})</el-button >刷新({{ usages.ai_refresh_count }}/{{ usages.ai_refresh_max_count }})</el-button
> >
<el-button size="small" type="primary" @click="handleSendType(2, item.content)" <el-button size="small" type="primary" @click="handleSendType(2, item.content)"
>AI创作({{ usages.ai_creation_count }}/{{ usages.ai_creation_max_count }})</el-button >AI创作({{ usages.ai_creation_count }}/{{ usages.ai_creation_max_count }})</el-button
> >
<el-button size="small" type="primary" @click="handleSendType(3, item.content)" <el-button size="small" type="primary" @click="handleSendType(3, item.content)"
>AI润色({{ usages.ai_polish_count }}/{{ usages.ai_polish_max_count }})</el-button >AI改写({{ usages.ai_polish_count }}/{{ usages.ai_polish_max_count }})</el-button
> >
<el-button size="small" type="primary" @click="handleSendType(4, item.content)" <el-button size="small" type="primary" @click="handleSendType(4, item.content)"
>AI扩写({{ usages.ai_expand_count }}/{{ usages.ai_expand_max_count }})</el-button >AI扩写({{ usages.ai_expand_count }}/{{ usages.ai_expand_max_count }})</el-button
> >
<el-button size="small" type="primary" @click="handleSendType(7, item.content)"
>AI缩写({{ usages.ai_abbr_count }}/{{ usages.ai_abbr_max_count }})</el-button
>
<el-button size="small" type="primary" @click="handleSendType(8, item.content)"
>AI总结({{ usages.ai_summary_count }}/{{ usages.ai_summary_max_count }})</el-button
>
</template>
<!-- 图片 -->
<template v-if="form.type == 2">
<template v-if="item.role == 'system'">
<el-button size="small" type="primary" @click="handleCopy(item.content)">复制</el-button>
<el-button size="small" type="primary" @click="handleSendType(99, item.content)"
>生成({{ usages.ai_generate_image_count }}/{{ usages.ai_generate_image_max_count }})</el-button
>
</template>
<template v-if="item.role == 'bot'">
<el-button size="small" type="primary" @click="handleSendType(99, item.content)"
>重画({{ usages.ai_generate_image_count }}/{{ usages.ai_generate_image_max_count }})</el-button
>
<el-button size="small" type="primary" @click="handleSave(item)">保存</el-button>
</template>
</template>
</div> </div>
</div> </div>
</div> </div>
...@@ -146,7 +213,13 @@ function parseHtml(content) { ...@@ -146,7 +213,13 @@ function parseHtml(content) {
</div> </div>
</div> </div>
<div class="chat-footer"> <div class="chat-footer">
<el-input type="textarea" :autosize="{ minRows: 1, maxRows: 12 }" placeholder="发消息" v-model="content" @keydown.enter="handleSend"></el-input> <el-input
type="textarea"
:autosize="{ minRows: 1, maxRows: 12 }"
placeholder="发消息"
v-model="content"
@keydown.enter="handleSend"
></el-input>
<el-button text type="primary" @click="handleSend">发送</el-button> <el-button text type="primary" @click="handleSend">发送</el-button>
</div> </div>
</template> </template>
...@@ -195,14 +268,23 @@ function parseHtml(content) { ...@@ -195,14 +268,23 @@ function parseHtml(content) {
.chat-message-content { .chat-message-content {
max-width: 100%; max-width: 100%;
word-break: break-word; word-break: break-word;
* {
max-width: 100%;
}
} }
.user .chat-message-main { .user .chat-message-main {
color: #fff; color: #fff;
background-color: var(--main-color); background-color: var(--main-color);
} }
.chat-message-extra { .chat-message-extra:not(:empty) {
margin-top: 20px; margin-top: 20px;
padding-top: 12px;
border-top: 1px solid #edeff1;
}
.chat-message-extra button {
margin-bottom: 10px;
} }
.dot-flashing { .dot-flashing {
......
<script setup>
import { onMounted, onUnmounted } from 'vue'
import CktDesign from '@chuangkit/chuangkit-design'
import md5 from 'blueimp-md5'
import { useUserStore } from '@/stores/user'
import { uploadFileByUrl } from '@/utils/upload'
const model = defineModel()
const emit = defineEmits(['close'])
const userStore = useUserStore()
/**
* 构建签名
* @param obj 参数对象,对象中的所有属性全部参与签名的生成
* @returns {string} 签名
*/
const buildSign = obj => {
let signParameterArray = []
for (let key in obj) {
signParameterArray.push(`${key}=${obj[key]}`)
}
let signPlaintext = signParameterArray.sort().join('&')
return md5(signPlaintext).toUpperCase()
}
/**
* 构建2.0版本签名
* @param appId 第三方企业id
* @param expireTime 时间戳,取当前时间即可
* @param userFlag 用户标记
* @param appSecret 企业密钥
* @returns {string} 签名
*/
const buildVersion2Sign = (appId, expireTime, userFlag, appSecret) => {
let signParameterObj = {
app_id: appId,
expire_time: expireTime,
user_flag: userFlag,
app_secret: appSecret
}
return buildSign(signParameterObj)
}
window.chuangkitComplete = async result => {
if (!result.cktMessage) {
return
}
if (result.kind == 2) {
for (const url of result['source-urls']) {
const uploadedURL = await uploadFileByUrl(url)
model.value = uploadedURL
}
}
if ([1, 2, 3].includes(result.kind)) {
emit('close')
}
}
let cktInstance
function openDesignPage() {
const appId = '54d9adec77d0402794018d166110f3dd'
const appSecret = '08097010E0EF4B85EE2B8CE438328249'
const userFlag = userStore.user.id
const expireTime = Date.now()
const sign = buildVersion2Sign(appId, expireTime, userFlag, appSecret)
let params = {
app_id: appId,
expire_time: expireTime,
user_flag: userFlag,
device_type: 1,
kind_id: 447,
version: '2.0',
sign: sign,
enable_authorize: '1',
taxpayer_name: 'chuangkit',
taxpayer_phone: '13820659475',
taxpayer_number: '91120116636067462H'
}
cktInstance = new CktDesign(params)
cktInstance.open()
console.log(cktInstance)
}
function closeDesignPage() {
if (cktInstance) {
cktInstance.close()
}
}
onMounted(() => openDesignPage())
onUnmounted(() => closeDesignPage())
</script>
<template>
<div id="ckt-design-page"></div>
</template>
<script setup>
import { getChuanKitDesignList } from '../api'
import { useUserStore } from '@/stores/user'
import { uploadFileByUrl } from '@/utils/upload'
const ChuangKitDesign = defineAsyncComponent(() => import('./ChuangKitDesign.vue'))
const userStore = useUserStore()
const model = defineModel()
const active = ref('')
const designVisible = ref(false)
const data = reactive({
list: [],
count: 0
})
async function fetchList() {
const res = await getChuanKitDesignList({ user_flag: userStore.user.id, page_no: 1, page_size: 1000, time_order: 1 })
Object.assign(data, res.data)
}
onMounted(fetchList)
async function handleClick(item) {
active.value = item.designId
model.value = await uploadFileByUrl(item.thumbUrl)
}
function onClose() {
designVisible.value = false
fetchList()
}
</script>
<template>
<div class="image-design">
<el-button type="primary" @click="designVisible = true">打开编辑器</el-button>
<ul>
<li v-for="item in data.list" :key="item.designId" :class="{ active: item.designId === active }" @click="handleClick(item)">
<img :src="item.thumbUrl" />
</li>
</ul>
<ChuangKitDesign v-model="model" @close="onClose" v-if="designVisible"></ChuangKitDesign>
</div>
</template>
<style lang="scss">
.image-design {
width: 100%;
ul {
margin: 30px auto;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 30px;
}
li {
width: 200px;
border-radius: 20px;
overflow: hidden;
cursor: pointer;
box-sizing: border-box;
&.active {
border: 5px solid var(--main-color);
}
}
img {
width: 100%;
}
}
</style>
...@@ -17,13 +17,6 @@ const rules = ref({ ...@@ -17,13 +17,6 @@ const rules = ref({
name: [{ required: true, message: '请输入内容名称' }] name: [{ required: true, message: '请输入内容名称' }]
}) })
watch(
() => form.value.type,
() => {
form.value.way = '2'
}
)
async function handleValidate() { async function handleValidate() {
await formRef.value.validate() await formRef.value.validate()
} }
...@@ -32,6 +25,16 @@ async function handleNext() { ...@@ -32,6 +25,16 @@ async function handleNext() {
await handleValidate() await handleValidate()
emit('next') emit('next')
} }
function wayDisabled(item, type) {
if (item.value == 1) {
return !['1', '2'].includes(type)
}
if (item.value == 3) {
return !['2', '8'].includes(type)
}
return false
}
</script> </script>
<template> <template>
...@@ -39,8 +42,8 @@ async function handleNext() { ...@@ -39,8 +42,8 @@ async function handleNext() {
<template #header>基础信息</template> <template #header>基础信息</template>
<el-form label-suffix=":" label-width="130" :model="form" :rules="rules" ref="formRef" :disabled="action === 'view'"> <el-form label-suffix=":" label-width="130" :model="form" :rules="rules" ref="formRef" :disabled="action === 'view'">
<el-form-item label="营销内容类型" prop="type"> <el-form-item label="营销内容类型" prop="type">
<el-radio-group v-model="form.type" :disabled="action === 'update'"> <el-radio-group v-model="form.type" :disabled="action === 'update'" @change="form.way = '2'">
<el-radio v-for="item in materialType" :key="item.id" :value="item.value">{{ item.label }}</el-radio> <el-radio v-for="item in materialType" :key="item.id" :value="item.value" :disabled="$route.query.type ? item.value !== $route.query.type : false">{{ item.label }}</el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="内容名称" prop="name"> <el-form-item label="内容名称" prop="name">
...@@ -48,7 +51,7 @@ async function handleNext() { ...@@ -48,7 +51,7 @@ async function handleNext() {
</el-form-item> </el-form-item>
<el-form-item label="创作方式" prop="way"> <el-form-item label="创作方式" prop="way">
<el-radio-group v-model="form.way"> <el-radio-group v-model="form.way">
<el-radio v-for="item in materialMethodList" :key="item.value" :value="item.value" :disabled="item.value == 1 && form.type != 1">{{ <el-radio v-for="item in materialMethodList" :key="item.value" :value="item.value" :disabled="wayDisabled(item, form.type)">{{
item.label item.label
}}</el-radio> }}</el-radio>
</el-radio-group> </el-radio-group>
......
...@@ -4,6 +4,8 @@ import { getNameByValue, materialMethodList } from '@/utils/dictionary' ...@@ -4,6 +4,8 @@ import { getNameByValue, materialMethodList } from '@/utils/dictionary'
import AppUpload from '@/components/base/AppUpload.vue' import AppUpload from '@/components/base/AppUpload.vue'
import AIChat from './AIChat.vue' import AIChat from './AIChat.vue'
const ImageDesign = defineAsyncComponent(() => import('./ImageDesign.vue'))
defineProps(['action']) defineProps(['action'])
const emit = defineEmits(['prev', 'submit']) const emit = defineEmits(['prev', 'submit'])
...@@ -15,7 +17,7 @@ const form = defineModel() ...@@ -15,7 +17,7 @@ const form = defineModel()
const formRef = ref() const formRef = ref()
const rules = ref({ const rules = ref({
content: [{ required: true, message: '请输入内容名称' }] content: [{ required: true, message: '请输入' }]
}) })
const typeName = computed(() => { const typeName = computed(() => {
...@@ -39,7 +41,7 @@ async function handleSubmit() { ...@@ -39,7 +41,7 @@ async function handleSubmit() {
<template> <template>
<div class="three"> <div class="three">
<el-card shadow="never" v-if="form.way == 1"> <el-card shadow="never" v-if="form.way == 1">
<AIChat v-model="form" v-if="form.id"></AIChat> <AIChat v-model="form" @submit="handleSubmit" v-if="form.id"></AIChat>
</el-card> </el-card>
<el-card shadow="never"> <el-card shadow="never">
<template #header>内容创作</template> <template #header>内容创作</template>
...@@ -49,16 +51,21 @@ async function handleSubmit() { ...@@ -49,16 +51,21 @@ async function handleSubmit() {
<el-form-item label="创作方式">{{ getNameByValue(form.way, materialMethodList) }}</el-form-item> <el-form-item label="创作方式">{{ getNameByValue(form.way, materialMethodList) }}</el-form-item>
</el-form> </el-form>
<el-divider></el-divider> <el-divider></el-divider>
<el-form label-suffix=":" label-width="110" :model="form" :rules="rules" ref="formRef" :disabled="action === 'view'"> <el-form :model="form" :rules="rules" ref="formRef" :disabled="action === 'view'">
<el-form-item :label="`${typeName}资源`" prop="content"> <el-form-item prop="content">
<template v-if="form.type == 1"> <template v-if="form.type == 1">
<!-- 文本 --> <!-- 文本 -->
<el-input type="textarea" rows="14" v-model="form.content"></el-input> <el-input type="textarea" rows="14" v-model="form.content"></el-input>
</template> </template>
<template v-if="['2', '6', '7', '8'].includes(form.type)"> <template v-if="['2', '6', '7', '8'].includes(form.type)">
<!-- 图片|二维码|小程序|卡券 --> <!-- 图片|二维码|小程序|卡券 -->
<template v-if="form.way == 3">
<ImageDesign v-model="form.content"></ImageDesign>
</template>
<template v-else>
<AppUpload v-model="form.content" accept="image/*"></AppUpload> <AppUpload v-model="form.content" accept="image/*"></AppUpload>
</template> </template>
</template>
<template v-if="form.type == 3"> <template v-if="form.type == 3">
<!-- 语音 --> <!-- 语音 -->
<div> <div>
......
...@@ -2,7 +2,14 @@ ...@@ -2,7 +2,14 @@
import { useMapStore } from '@/stores/map' import { useMapStore } from '@/stores/map'
import { useConnection } from '../composables/useConnection' import { useConnection } from '../composables/useConnection'
import { useIndustry } from '../composables/useIndustry' import { useIndustry } from '../composables/useIndustry'
import { getNameByValue, materialMethodList, materialUsageList, materialUsersList } from '@/utils/dictionary' import {
getNameByValue,
materialMethodList,
materialUsageList,
materialUsersList,
materialPictureStyleList,
textPurposeList
} from '@/utils/dictionary'
defineProps(['action']) defineProps(['action'])
...@@ -19,7 +26,13 @@ const rules = ref({ ...@@ -19,7 +26,13 @@ const rules = ref({
industry_id: [{ required: true, message: '请选择所属行业' }], industry_id: [{ required: true, message: '请选择所属行业' }],
scenario_type: [{ required: true, message: '请选择使用场景' }], scenario_type: [{ required: true, message: '请选择使用场景' }],
personnel_type: [{ required: true, message: '请选择使用人员' }], personnel_type: [{ required: true, message: '请选择使用人员' }],
channel: [{ required: true, message: '请选择内容投放渠道' }] channel: [{ required: true, message: '请选择内容投放渠道' }],
'extend_info.text_count': [{ required: true, message: '请选择文本字数' }],
'extend_info.picture_style': [{ required: true, message: '请选择图片风格' }],
'extend_info.person_des': [{ required: true, message: '请输入人物描述' }],
'extend_info.scene_des': [{ required: true, message: '请输入场景描述' }],
'extend_info.important_info_desc': [{ required: true, message: '请输入突出重点信息描述' }],
'extend_info.text_use': [{ required: true, message: '请选择文本用途' }]
}) })
async function handleValidate() { async function handleValidate() {
...@@ -45,7 +58,39 @@ async function handleNext() { ...@@ -45,7 +58,39 @@ async function handleNext() {
<el-form-item label="创作方式">{{ getNameByValue(form.way, materialMethodList) }}</el-form-item> <el-form-item label="创作方式">{{ getNameByValue(form.way, materialMethodList) }}</el-form-item>
</el-form> </el-form>
<el-divider></el-divider> <el-divider></el-divider>
<el-form label-suffix=":" label-width="130" :model="form" :rules="rules" ref="formRef" :disabled="action === 'view'"> <el-form
label-suffix=":"
label-width="130"
:model="form"
:rules="rules"
ref="formRef"
:disabled="action === 'view'"
>
<template v-if="form.type == 2 && form.way == 1">
<!-- 图片AI -->
<el-form-item label="图片风格" prop="extend_info.picture_style">
<el-radio-group v-model="form.extend_info.picture_style">
<el-radio v-for="item in materialPictureStyleList" :key="item.id" :value="item.value">{{
item.label
}}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="人物描述" prop="extend_info.person_des">
<el-input type="textarea" :rows="3" v-model="form.extend_info.person_des" />
</el-form-item>
<el-form-item label="场景描述" prop="extend_info.scene_des">
<el-input type="textarea" :rows="3" v-model="form.extend_info.scene_des" />
</el-form-item>
<el-form-item label="突出重点信息描述" prop="extend_info.important_info_desc">
<el-input type="textarea" :rows="3" v-model="form.extend_info.important_info_desc" />
</el-form-item>
</template>
<template v-else>
<el-form-item label="文本用途" prop="extend_info.text_use">
<el-radio-group v-model="form.extend_info.text_use">
<el-radio v-for="item in textPurposeList" :key="item.id" :value="item.value">{{ item.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="所属行业" prop="industry_id"> <el-form-item label="所属行业" prop="industry_id">
<el-select v-model="form.industry_id"> <el-select v-model="form.industry_id">
<el-option v-for="item in industryList" :key="item.id" :label="item.name" :value="item.id + ''"></el-option> <el-option v-for="item in industryList" :key="item.id" :label="item.name" :value="item.id + ''"></el-option>
...@@ -66,9 +111,32 @@ async function handleNext() { ...@@ -66,9 +111,32 @@ async function handleNext() {
<el-radio v-for="item in connectionList" :key="item.id" :value="item.id">{{ item.type_name }}</el-radio> <el-radio v-for="item in connectionList" :key="item.id" :value="item.id">{{ item.type_name }}</el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="文本字数" prop="extend_info.text_count" v-if="form.type == 1 && form.way == 1">
<el-radio-group v-model="form.extend_info.text_count">
<template v-if="form.extend_info.text_use !== '2' && form.extend_info.text_use !== '3'">
<el-radio :value="50">50</el-radio>
<el-radio :value="100">100</el-radio>
</template>
<template v-if="form.extend_info.text_use === '2'">
<el-radio :value="300">300</el-radio>
<el-radio :value="500">500</el-radio>
<el-radio :value="1000">1000</el-radio>
</template>
<template v-if="form.extend_info.text_use === '3'">
<el-radio :value="500">500</el-radio>
<el-radio :value="1000">1000</el-radio>
</template>
</el-radio-group>
</el-form-item>
<el-form-item label="关键点" prop="key_points"> <el-form-item label="关键点" prop="key_points">
<el-input type="textarea" :rows="4" v-model="form.key_points" placeholder="请输入内容的核心内容或者关键点,多个请使用英文“,”号进行隔离。" /> <el-input
type="textarea"
:rows="4"
v-model="form.key_points"
placeholder="请输入内容的核心内容或者关键点,多个请使用英文“,”号进行隔离。"
/>
</el-form-item> </el-form-item>
</template>
</el-form> </el-form>
<el-row justify="center"> <el-row justify="center">
<el-button type="primary" @click="handlePrev">上一步</el-button> <el-button type="primary" @click="handlePrev">上一步</el-button>
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
import { useMapStore } from '@/stores/map' import { useMapStore } from '@/stores/map'
import { useConnection } from '../composables/useConnection' import { useConnection } from '../composables/useConnection'
import { useIndustry } from '../composables/useIndustry' import { useIndustry } from '../composables/useIndustry'
import { getNameByValue, materialMethodList, materialUsageList, materialUsersList } from '@/utils/dictionary' import { getNameByValue, materialMethodList, materialUsageList, materialUsersList, materialPictureStyleList } from '@/utils/dictionary'
import AppUpload from '@/components/base/AppUpload.vue' import AppUpload from '@/components/base/AppUpload.vue'
const props = defineProps(['data']) const props = defineProps(['data'])
...@@ -25,6 +25,24 @@ const typeName = computed(() => { ...@@ -25,6 +25,24 @@ const typeName = computed(() => {
</el-form> </el-form>
<el-divider></el-divider> <el-divider></el-divider>
<el-form label-suffix=":" label-width="120" ref="formRef"> <el-form label-suffix=":" label-width="120" ref="formRef">
<template v-if="data.type == 2 && data.way == 1">
<!-- 图片AI -->
<el-form-item label="图片风格" prop="extend_info.picture_style">
<el-radio-group :modelValue="data.extend_info.picture_style">
<el-radio v-for="item in materialPictureStyleList" :key="item.id" :value="item.value">{{ item.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="人物描述" prop="extend_info.person_des">
<el-input type="textarea" :rows="3" :modelValue="data.extend_info.person_des" />
</el-form-item>
<el-form-item label="场景描述" prop="extend_info.scene_des">
<el-input type="textarea" :rows="3" :modelValue="data.extend_info.scene_des" />
</el-form-item>
<el-form-item label="突出重点信息描述" prop="extend_info.important_info_desc">
<el-input type="textarea" :rows="3" :modelValue="data.extend_info.important_info_desc" />
</el-form-item>
</template>
<template v-else>
<el-form-item label="所属行业" prop="industry_id"> <el-form-item label="所属行业" prop="industry_id">
<el-select :model-value="data.industry_id"> <el-select :model-value="data.industry_id">
<el-option v-for="item in industryList" :key="item.id" :label="item.name" :value="item.id + ''"></el-option> <el-option v-for="item in industryList" :key="item.id" :label="item.name" :value="item.id + ''"></el-option>
...@@ -48,6 +66,7 @@ const typeName = computed(() => { ...@@ -48,6 +66,7 @@ const typeName = computed(() => {
<el-form-item label="关键点" prop="key_points"> <el-form-item label="关键点" prop="key_points">
<el-input type="textarea" :rows="4" :model-value="data.key_points" placeholder="请输入内容的核心内容或者关键点,多个请使用英文“,”号进行隔离。" /> <el-input type="textarea" :rows="4" :model-value="data.key_points" placeholder="请输入内容的核心内容或者关键点,多个请使用英文“,”号进行隔离。" />
</el-form-item> </el-form-item>
</template>
<el-form-item :label="`${typeName}资源`" prop="content"> <el-form-item :label="`${typeName}资源`" prop="content">
<template v-if="data.type == 1"> <template v-if="data.type == 1">
<!-- 文本 --> <!-- 文本 -->
......
import { fetchEventSource } from '@fortaine/fetch-event-source' import { fetchEventSource } from '@fortaine/fetch-event-source'
import { getAIUsage } from '../api' import { getAIUsage, postGenerateImage } from '../api'
import type { Message } from '../types' import type { Message } from '../types'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
...@@ -32,13 +32,88 @@ export function useChat(options: any) { ...@@ -32,13 +32,88 @@ export function useChat(options: any) {
}) })
async function post(data: any) { async function post(data: any) {
if (options.fileType == 1) {
await generateText(data)
} else {
await generateImage(data)
}
}
// 生成文本
// async function generateText(data: any) {
// isLoading.value = true
// await fetchEventSource('/api/lab/v1/experiment/marketing-ai/sky-agents-chat', {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/json'
// },
// body: JSON.stringify({ ...options, ...data, context: data.content, chart_id: chatId.value }),
// async onopen(response) {
// if (response.ok) {
// return
// } else {
// throw response
// }
// },
// onmessage(res) {
// const message = JSON.parse(res.data)
// if (message.code === 0) {
// ElMessage.error(message.message)
// return
// }
// chatId.value = message.chatId + ''
// const conversationId = message.conversationId
// const messageIndex = messages.value.findIndex(session => session.conversationId === conversationId)
// const content = message.content || ''
// // if (message.content === '\n') content = '<br/>'
// if (messageIndex === -1) {
// messages.value.push({ conversationId, role: 'bot', content, input: data.context })
// } else {
// messages.value[messageIndex].content = messages.value[messageIndex].content + content
// }
// isLoading.value = false
// },
// onclose() {
// fetchUsages()
// isLoading.value = false
// },
// onerror(err) {
// console.log(err)
// isLoading.value = false
// throw err
// }
// })
// }
async function generateText(data: any) {
isLoading.value = true isLoading.value = true
await fetchEventSource('/api/lab/v1/experiment/marketing-ai/sky-agents-chat', { let params = {}
if (data.type === '1') {
params = {
chat_history: messages.value
}
} else {
const docAction: any = {
2: 'write',
3: 'rewrite',
4: 'expand',
5: 'rewrite',
7: 'abbreviate',
8: 'summary'
}
params = {
content: data.content,
doc_action: docAction[data.type],
full_text: !!(data.type === 2)
}
}
await fetchEventSource('/api/lab/v1/experiment/marketing-ai/sky-agent3-chat', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ ...options, ...data, chart_id: chatId.value }), body: JSON.stringify({
...options, ...data, api_type: parseInt(data.type) === 1 ? 1 : 2, context: data.content, params: params
}),
async onopen(response) { async onopen(response) {
if (response.ok) { if (response.ok) {
return return
...@@ -47,21 +122,35 @@ export function useChat(options: any) { ...@@ -47,21 +122,35 @@ export function useChat(options: any) {
} }
}, },
onmessage(res) { onmessage(res) {
// console.log(res.data)
const message = JSON.parse(res.data) const message = JSON.parse(res.data)
// 聊天返回内容
if (data.type === '1') {
if (message.code === 0) { if (message.code === 0) {
ElMessage.error(message.message) ElMessage.error(message.message)
return return
} }
chatId.value = message.chatId + '' const conversationId = message.conversation_id
const conversationId = message.conversationId
const messageIndex = messages.value.findIndex(session => session.conversationId === conversationId) const messageIndex = messages.value.findIndex(session => session.conversationId === conversationId)
const content = message.content || '' const content = message?.arguments?.reduce((a: any, b: any) => {
// if (message.content === '\n') content = '<br/>' a = b?.messages[0]?.text || ''
return a
}, '')
if (messageIndex === -1) { if (messageIndex === -1) {
messages.value.push({ conversationId, role: 'bot', content, input: data.context }) messages.value.push({ conversationId, role: 'bot', content, input: data.context })
} else { } else {
messages.value[messageIndex].content = messages.value[messageIndex].content + content if (content) {
messages.value[messageIndex].content = content
}
}
} else {
// 按钮功能返回内容
const requestId = message.request_id
const messageIndex = messages.value.findIndex(session => session.conversationId === requestId)
if (messageIndex === -1) {
messages.value.push({ conversationId: requestId, role: 'bot', content: message.data?.text || '', input: data.context })
} else {
messages.value[messageIndex].content = message.data?.text
}
} }
isLoading.value = false isLoading.value = false
}, },
...@@ -77,5 +166,22 @@ export function useChat(options: any) { ...@@ -77,5 +166,22 @@ export function useChat(options: any) {
}) })
} }
return { usages, chatId, messages, post, isLoading } // 生成图片
async function generateImage(data: any) {
isLoading.value = true
try {
const res = await postGenerateImage({ ...options, ...data })
if (res.data.detail.image_url) {
messages.value.push({ type: 'image', role: 'bot', ...res.data.detail })
} else {
ElMessage.error(res.data.detail.failure_reason)
}
fetchUsages()
} catch (error) {
console.log(error)
}
isLoading.value = false
}
return { usages, chatId, messages, post, generateImage, isLoading }
} }
...@@ -6,6 +6,9 @@ import AppList from '@/components/base/AppList.vue' ...@@ -6,6 +6,9 @@ import AppList from '@/components/base/AppList.vue'
import { getMaterialList, updateMaterial, deleteMaterial } from '../api' import { getMaterialList, updateMaterial, deleteMaterial } from '../api'
import { getNameByValue, materialMethodList, materialUsageList, materialUsersList } from '@/utils/dictionary' import { getNameByValue, materialMethodList, materialUsageList, materialUsersList } from '@/utils/dictionary'
import { useMapStore } from '@/stores/map' import { useMapStore } from '@/stores/map'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const ViewDialog = defineAsyncComponent(() => import('../components/ViewDialog.vue')) const ViewDialog = defineAsyncComponent(() => import('../components/ViewDialog.vue'))
...@@ -75,22 +78,22 @@ const listOptions = computed(() => { ...@@ -75,22 +78,22 @@ const listOptions = computed(() => {
return getNameByValue(row.way, materialMethodList) return getNameByValue(row.way, materialMethodList)
} }
}, },
{ label: '行业', prop: 'industry_name' }, // { label: '行业', prop: 'industry_name' },
{ // {
label: '使用场景', // label: '使用场景',
prop: 'scenario_type', // prop: 'scenario_type',
computed: ({ row }: { row: MaterialProp }) => { // computed: ({ row }: { row: MaterialProp }) => {
return getNameByValue(row.scenario_type, materialUsageList) // return getNameByValue(row.scenario_type, materialUsageList)
} // }
}, // },
{ // {
label: '使用人员', // label: '使用人员',
prop: 'personnel_type', // prop: 'personnel_type',
computed: ({ row }: { row: MaterialProp }) => { // computed: ({ row }: { row: MaterialProp }) => {
return getNameByValue(row.personnel_type, materialUsersList) // return getNameByValue(row.personnel_type, materialUsersList)
} // }
}, // },
{ label: '投放渠道', prop: 'channel_detail.type_name' }, // { label: '投放渠道', prop: 'channel_detail.type_name' },
{ {
label: '状态', label: '状态',
prop: 'status_name', prop: 'status_name',
...@@ -100,12 +103,11 @@ const listOptions = computed(() => { ...@@ -100,12 +103,11 @@ const listOptions = computed(() => {
{ {
label: '更新人', label: '更新人',
prop: 'updated_operator_name', prop: 'updated_operator_name',
width: 100,
computed: ({ row }: { row: MaterialProp }) => { computed: ({ row }: { row: MaterialProp }) => {
return row.updated_operator.real_name || row.updated_operator.nickname || row.updated_operator.username return row.updated_operator.real_name || row.updated_operator.nickname || row.updated_operator.username
} }
}, },
{ label: '更新时间', prop: 'updated_time', width: 180 }, { label: '更新时间', prop: 'updated_time' },
{ label: '操作', slots: 'table-x', width: 240 } { label: '操作', slots: 'table-x', width: 240 }
] ]
} }
...@@ -150,19 +152,24 @@ async function handleChangeStatus(row: MaterialProp) { ...@@ -150,19 +152,24 @@ async function handleChangeStatus(row: MaterialProp) {
<AppCard> <AppCard>
<AppList border v-bind="listOptions" ref="appList"> <AppList border v-bind="listOptions" ref="appList">
<template #header-buttons> <template #header-buttons>
<el-space> <el-space v-if="!userStore.status.material_status">
<router-link to="/material/create"> <router-link :to="{ path: '/material/create', query: $route.query }">
<el-button type="primary" :icon="Plus" v-permission="'v1-experiment-marketing-material-create'">营销内容创作</el-button> <el-button type="primary" :icon="Plus">营销内容创作</el-button>
</router-link> </router-link>
</el-space> </el-space>
</template> </template>
<template #table-status="{ row }"> <template #table-status="{ row }">
<el-switch v-model="row.status" active-value="1" inactive-value="0" :before-change="() => handleChangeStatus(row)"></el-switch> <el-switch
v-model="row.status"
active-value="1"
inactive-value="0"
:before-change="() => handleChangeStatus(row)"
></el-switch>
</template> </template>
<template #table-x="{ row }"> <template #table-x="{ row }">
<el-button type="primary" plain @click="handleView(row)">查看</el-button> <el-button type="primary" plain @click="handleView(row)">查看</el-button>
<el-button type="primary" plain @click="handleEdit(row)" v-permission="'v1-experiment-marketing-material-update'">编辑</el-button> <el-button type="primary" plain @click="handleEdit(row)">编辑</el-button>
<el-button type="primary" plain @click="handleRemove(row)" v-permission="'v1-experiment-marketing-material-delete'">删除</el-button> <el-button type="primary" plain @click="handleRemove(row)">删除</el-button>
</template> </template>
</AppList> </AppList>
<ViewDialog :data="currentRow" v-if="dialogVisible" v-model="dialogVisible"></ViewDialog> <ViewDialog :data="currentRow" v-if="dialogVisible" v-model="dialogVisible"></ViewDialog>
......
<script setup lang="ts"> <script setup lang="ts">
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { createMaterial, updateMaterial, getMaterial } from '../api' import { createMaterial, updateMaterial, getMaterial } from '../api'
import { pickBy } from 'lodash-es'
const props = defineProps<{ action: string }>() const props = defineProps<{ action: string }>()
...@@ -14,15 +15,16 @@ const activeName = ref(1) ...@@ -14,15 +15,16 @@ const activeName = ref(1)
const form: any = reactive({ const form: any = reactive({
id: '', id: '',
type: '1', type: route.query.type || '1',
name: '', name: '',
way: '2', way: '2',
industry_id: '1', industry_id: '',
scenario_type: '1', scenario_type: '1',
personnel_type: '1', personnel_type: '1',
channel: '', channel: '',
key_points: '', key_points: '',
content: '' content: '',
extend_info: {}
}) })
const detail = ref() const detail = ref()
...@@ -48,7 +50,8 @@ function handleNext() { ...@@ -48,7 +50,8 @@ function handleNext() {
// 下一步提交 // 下一步提交
async function handleNextAndSubmit() { async function handleNextAndSubmit() {
if (!form.id && form.way == 1) { if (!form.id && form.way == 1) {
const res = await createMaterial(form) const params = pickBy(form, item => item !== '' && item != '0')
const res = await createMaterial(params)
form.id = res.data.id form.id = res.data.id
} }
handleNext() handleNext()
...@@ -59,14 +62,16 @@ async function handleSubmit() { ...@@ -59,14 +62,16 @@ async function handleSubmit() {
} }
// 创建 // 创建
async function handleCreate() { async function handleCreate() {
await createMaterial(form) const params = pickBy(form, item => item !== '' && item != '0')
await createMaterial(params)
ElMessage.success('创建成功') ElMessage.success('创建成功')
router.replace('/material') router.replace('/material')
} }
// 修改 // 修改
async function handleUpdate() { async function handleUpdate() {
await updateMaterial(form) const params = pickBy(form, item => item !== '' && item != '0')
await updateMaterial(params)
ElMessage.success('修改成功') ElMessage.success('修改成功')
router.replace('/material') router.replace('/material')
} }
...@@ -78,10 +83,10 @@ async function handleUpdate() { ...@@ -78,10 +83,10 @@ async function handleUpdate() {
<el-tab-pane lazy label="第1步" :name="1" disabled> <el-tab-pane lazy label="第1步" :name="1" disabled>
<StepOne v-model="form" :action="action" style="max-width: 1000px; margin: 0 auto" @next="handleNext"></StepOne> <StepOne v-model="form" :action="action" style="max-width: 1000px; margin: 0 auto" @next="handleNext"></StepOne>
</el-tab-pane> </el-tab-pane>
<el-tab-pane lazy label="第2步" :name="2" disabled> <el-tab-pane lazy label="第2步" :name="2" disabled v-if="form.way !== '3'">
<StepTwo v-model="form" :action="action" style="max-width: 1000px; margin: 0 auto" @prev="handlePrev" @next="handleNextAndSubmit"></StepTwo> <StepTwo v-model="form" :action="action" style="max-width: 1000px; margin: 0 auto" @prev="handlePrev" @next="handleNextAndSubmit"></StepTwo>
</el-tab-pane> </el-tab-pane>
<el-tab-pane lazy label="第3步" :name="3" disabled> <el-tab-pane lazy :label="form.way === '3' ? '第二步' : '第3步'" :name="form.way === '3' ? 2 : 3" disabled>
<StepThree v-model="form" :action="action" @prev="handlePrev" @submit="handleSubmit"></StepThree> <StepThree v-model="form" :action="action" @prev="handlePrev" @submit="handleSubmit"></StepThree>
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
......
...@@ -4,6 +4,9 @@ import AppList from '@/components/base/AppList.vue' ...@@ -4,6 +4,9 @@ import AppList from '@/components/base/AppList.vue'
import { getMaterialList, deleteMaterial } from '@/api/base' import { getMaterialList, deleteMaterial } from '@/api/base'
import type { MaterialProp } from '@/types' import type { MaterialProp } from '@/types'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const UpdateMaterialDialog = defineAsyncComponent(() => import('@/components/base/UpdateMaterialDialog.vue')) const UpdateMaterialDialog = defineAsyncComponent(() => import('@/components/base/UpdateMaterialDialog.vue'))
...@@ -117,6 +120,7 @@ const deleteMembers = function (ids: string) { ...@@ -117,6 +120,7 @@ const deleteMembers = function (ids: string) {
<template #header-buttons> <template #header-buttons>
<el-space> <el-space>
<el-button <el-button
v-if="!userStore.status.material_status"
type="primary" type="primary"
:icon="Plus" :icon="Plus"
@click="handleAdd" @click="handleAdd"
...@@ -151,6 +155,7 @@ const deleteMembers = function (ids: string) { ...@@ -151,6 +155,7 @@ const deleteMembers = function (ids: string) {
:data="currentRow" :data="currentRow"
@update="handleRefresh()" @update="handleRefresh()"
v-if="updateVisible" v-if="updateVisible"
v-model="updateVisible"></UpdateMaterialDialog> v-model="updateVisible"
></UpdateMaterialDialog>
</AppCard> </AppCard>
</template> </template>
...@@ -4,6 +4,9 @@ import AppList from '@/components/base/AppList.vue' ...@@ -4,6 +4,9 @@ import AppList from '@/components/base/AppList.vue'
import { getMaterialList, deleteMaterial } from '@/api/base' import { getMaterialList, deleteMaterial } from '@/api/base'
import type { MaterialProp } from '@/types' import type { MaterialProp } from '@/types'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const UpdateMaterialDialog = defineAsyncComponent(() => import('@/components/base/UpdateMaterialDialog.vue')) const UpdateMaterialDialog = defineAsyncComponent(() => import('@/components/base/UpdateMaterialDialog.vue'))
...@@ -117,6 +120,7 @@ const deleteMembers = function (ids: string) { ...@@ -117,6 +120,7 @@ const deleteMembers = function (ids: string) {
<template #header-buttons> <template #header-buttons>
<el-space> <el-space>
<el-button <el-button
v-if="!userStore.status.material_status"
type="primary" type="primary"
:icon="Plus" :icon="Plus"
@click="handleAdd" @click="handleAdd"
...@@ -151,6 +155,7 @@ const deleteMembers = function (ids: string) { ...@@ -151,6 +155,7 @@ const deleteMembers = function (ids: string) {
:data="currentRow" :data="currentRow"
@update="handleRefresh()" @update="handleRefresh()"
v-if="updateVisible" v-if="updateVisible"
v-model="updateVisible"></UpdateMaterialDialog> v-model="updateVisible"
></UpdateMaterialDialog>
</AppCard> </AppCard>
</template> </template>
...@@ -4,6 +4,9 @@ import AppList from '@/components/base/AppList.vue' ...@@ -4,6 +4,9 @@ import AppList from '@/components/base/AppList.vue'
import { getMaterialList, deleteMaterial } from '@/api/base' import { getMaterialList, deleteMaterial } from '@/api/base'
import type { MaterialProp } from '@/types' import type { MaterialProp } from '@/types'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const UpdateMaterialDialog = defineAsyncComponent(() => import('@/components/base/UpdateMaterialDialog.vue')) const UpdateMaterialDialog = defineAsyncComponent(() => import('@/components/base/UpdateMaterialDialog.vue'))
...@@ -117,6 +120,7 @@ const deleteMembers = function (ids: string) { ...@@ -117,6 +120,7 @@ const deleteMembers = function (ids: string) {
<template #header-buttons> <template #header-buttons>
<el-space> <el-space>
<el-button <el-button
v-if="!userStore.status.material_status"
type="primary" type="primary"
:icon="Plus" :icon="Plus"
@click="handleAdd" @click="handleAdd"
...@@ -151,6 +155,7 @@ const deleteMembers = function (ids: string) { ...@@ -151,6 +155,7 @@ const deleteMembers = function (ids: string) {
:data="currentRow" :data="currentRow"
@update="handleRefresh()" @update="handleRefresh()"
v-if="updateVisible" v-if="updateVisible"
v-model="updateVisible"></UpdateMaterialDialog> v-model="updateVisible"
></UpdateMaterialDialog>
</AppCard> </AppCard>
</template> </template>
...@@ -4,6 +4,9 @@ import AppList from '@/components/base/AppList.vue' ...@@ -4,6 +4,9 @@ import AppList from '@/components/base/AppList.vue'
import { getMaterialList, deleteMaterial } from '@/api/base' import { getMaterialList, deleteMaterial } from '@/api/base'
import type { MaterialProp } from '@/types' import type { MaterialProp } from '@/types'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const UpdateMaterialDialog = defineAsyncComponent(() => import('@/components/base/UpdateMaterialDialog.vue')) const UpdateMaterialDialog = defineAsyncComponent(() => import('@/components/base/UpdateMaterialDialog.vue'))
...@@ -117,6 +120,7 @@ const deleteMembers = function (ids: string) { ...@@ -117,6 +120,7 @@ const deleteMembers = function (ids: string) {
<template #header-buttons> <template #header-buttons>
<el-space> <el-space>
<el-button <el-button
v-if="!userStore.status.material_status"
type="primary" type="primary"
:icon="Plus" :icon="Plus"
@click="handleAdd" @click="handleAdd"
...@@ -151,6 +155,7 @@ const deleteMembers = function (ids: string) { ...@@ -151,6 +155,7 @@ const deleteMembers = function (ids: string) {
:data="currentRow" :data="currentRow"
@update="handleRefresh()" @update="handleRefresh()"
v-if="updateVisible" v-if="updateVisible"
v-model="updateVisible"></UpdateMaterialDialog> v-model="updateVisible"
></UpdateMaterialDialog>
</AppCard> </AppCard>
</template> </template>
...@@ -4,6 +4,9 @@ import AppList from '@/components/base/AppList.vue' ...@@ -4,6 +4,9 @@ import AppList from '@/components/base/AppList.vue'
import { getMaterialList, deleteMaterial } from '@/api/base' import { getMaterialList, deleteMaterial } from '@/api/base'
import type { MaterialProp } from '@/types' import type { MaterialProp } from '@/types'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const UpdateMaterialDialog = defineAsyncComponent(() => import('@/components/base/UpdateMaterialDialog.vue')) const UpdateMaterialDialog = defineAsyncComponent(() => import('@/components/base/UpdateMaterialDialog.vue'))
...@@ -115,12 +118,13 @@ const deleteMembers = function (ids: string) { ...@@ -115,12 +118,13 @@ const deleteMembers = function (ids: string) {
<AppCard> <AppCard>
<AppList v-bind="listOptions" ref="appList" @selection-change="handleSelectionChange"> <AppList v-bind="listOptions" ref="appList" @selection-change="handleSelectionChange">
<template #header-buttons> <template #header-buttons>
<el-space> <el-space v-if="!userStore.status.material_status">
<!-- v-permission="'v1-experiment-marketing-material-create'" -->
<el-button <el-button
v-if="!userStore.status.material_status"
type="primary" type="primary"
:icon="Plus" :icon="Plus"
@click="handleAdd" @click="handleAdd"
v-permission="'v1-experiment-marketing-material-create'"
>新建</el-button >新建</el-button
> >
<!-- <el-button type="danger" plain :icon="Delete" :disabled="!multipleSelection.length" @click="handleRemoves()" <!-- <el-button type="danger" plain :icon="Delete" :disabled="!multipleSelection.length" @click="handleRemoves()"
...@@ -151,6 +155,7 @@ const deleteMembers = function (ids: string) { ...@@ -151,6 +155,7 @@ const deleteMembers = function (ids: string) {
:data="currentRow" :data="currentRow"
@update="handleRefresh()" @update="handleRefresh()"
v-if="updateVisible" v-if="updateVisible"
v-model="updateVisible"></UpdateMaterialDialog> v-model="updateVisible"
></UpdateMaterialDialog>
</AppCard> </AppCard>
</template> </template>
...@@ -4,6 +4,9 @@ import AppList from '@/components/base/AppList.vue' ...@@ -4,6 +4,9 @@ import AppList from '@/components/base/AppList.vue'
import { getMaterialList, deleteMaterial } from '@/api/base' import { getMaterialList, deleteMaterial } from '@/api/base'
import type { MaterialProp } from '@/types' import type { MaterialProp } from '@/types'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const UpdateMaterialDialog = defineAsyncComponent(() => import('@/components/base/UpdateMaterialDialog.vue')) const UpdateMaterialDialog = defineAsyncComponent(() => import('@/components/base/UpdateMaterialDialog.vue'))
...@@ -115,12 +118,13 @@ const deleteMembers = function (ids: string) { ...@@ -115,12 +118,13 @@ const deleteMembers = function (ids: string) {
<AppCard> <AppCard>
<AppList v-bind="listOptions" ref="appList" @selection-change="handleSelectionChange"> <AppList v-bind="listOptions" ref="appList" @selection-change="handleSelectionChange">
<template #header-buttons> <template #header-buttons>
<el-space> <el-space v-if="!userStore.status.material_status">
<!-- v-permission="'v1-experiment-marketing-material-create'" -->
<el-button <el-button
v-if="!userStore.status.material_status"
type="primary" type="primary"
:icon="Plus" :icon="Plus"
@click="handleAdd" @click="handleAdd"
v-permission="'v1-experiment-marketing-material-create'"
>新建</el-button >新建</el-button
> >
<!-- <el-button type="danger" plain :icon="Delete" :disabled="!multipleSelection.length" @click="handleRemoves()" <!-- <el-button type="danger" plain :icon="Delete" :disabled="!multipleSelection.length" @click="handleRemoves()"
...@@ -151,6 +155,7 @@ const deleteMembers = function (ids: string) { ...@@ -151,6 +155,7 @@ const deleteMembers = function (ids: string) {
:data="currentRow" :data="currentRow"
@update="handleRefresh()" @update="handleRefresh()"
v-if="updateVisible" v-if="updateVisible"
v-model="updateVisible"></UpdateMaterialDialog> v-model="updateVisible"
></UpdateMaterialDialog>
</AppCard> </AppCard>
</template> </template>
...@@ -4,6 +4,9 @@ import AppList from '@/components/base/AppList.vue' ...@@ -4,6 +4,9 @@ import AppList from '@/components/base/AppList.vue'
import { getMaterialList, deleteMaterial } from '@/api/base' import { getMaterialList, deleteMaterial } from '@/api/base'
import type { MaterialProp } from '@/types' import type { MaterialProp } from '@/types'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const UpdateMaterialDialog = defineAsyncComponent(() => import('@/components/base/UpdateMaterialDialog.vue')) const UpdateMaterialDialog = defineAsyncComponent(() => import('@/components/base/UpdateMaterialDialog.vue'))
...@@ -115,12 +118,13 @@ const deleteMembers = function (ids: string) { ...@@ -115,12 +118,13 @@ const deleteMembers = function (ids: string) {
<AppCard> <AppCard>
<AppList v-bind="listOptions" ref="appList" @selection-change="handleSelectionChange"> <AppList v-bind="listOptions" ref="appList" @selection-change="handleSelectionChange">
<template #header-buttons> <template #header-buttons>
<el-space> <el-space v-if="!userStore.status.material_status">
<!-- v-permission="'v1-experiment-marketing-material-create'" -->
<el-button <el-button
v-if="!userStore.status.material_status"
type="primary" type="primary"
:icon="Plus" :icon="Plus"
@click="handleAdd" @click="handleAdd"
v-permission="'v1-experiment-marketing-material-create'"
>新建</el-button >新建</el-button
> >
<!-- <el-button type="danger" plain :icon="Delete" :disabled="!multipleSelection.length" @click="handleRemoves()" <!-- <el-button type="danger" plain :icon="Delete" :disabled="!multipleSelection.length" @click="handleRemoves()"
...@@ -151,6 +155,7 @@ const deleteMembers = function (ids: string) { ...@@ -151,6 +155,7 @@ const deleteMembers = function (ids: string) {
:data="currentRow" :data="currentRow"
@update="handleRefresh()" @update="handleRefresh()"
v-if="updateVisible" v-if="updateVisible"
v-model="updateVisible"></UpdateMaterialDialog> v-model="updateVisible"
></UpdateMaterialDialog>
</AppCard> </AppCard>
</template> </template>
...@@ -4,6 +4,9 @@ import AppList from '@/components/base/AppList.vue' ...@@ -4,6 +4,9 @@ import AppList from '@/components/base/AppList.vue'
import { getMaterialList, deleteMaterial } from '@/api/base' import { getMaterialList, deleteMaterial } from '@/api/base'
import type { MaterialProp } from '@/types' import type { MaterialProp } from '@/types'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const UpdateMaterialDialog = defineAsyncComponent(() => import('@/components/base/UpdateMaterialDialog.vue')) const UpdateMaterialDialog = defineAsyncComponent(() => import('@/components/base/UpdateMaterialDialog.vue'))
...@@ -117,6 +120,7 @@ const deleteMembers = function (ids: string) { ...@@ -117,6 +120,7 @@ const deleteMembers = function (ids: string) {
<template #header-buttons> <template #header-buttons>
<el-space> <el-space>
<el-button <el-button
v-if="!userStore.status.material_status"
type="primary" type="primary"
:icon="Plus" :icon="Plus"
@click="handleAdd" @click="handleAdd"
...@@ -151,6 +155,7 @@ const deleteMembers = function (ids: string) { ...@@ -151,6 +155,7 @@ const deleteMembers = function (ids: string) {
:data="currentRow" :data="currentRow"
@update="handleRefresh()" @update="handleRefresh()"
v-if="updateVisible" v-if="updateVisible"
v-model="updateVisible"></UpdateMaterialDialog> v-model="updateVisible"
></UpdateMaterialDialog>
</AppCard> </AppCard>
</template> </template>
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
import type { EventProp, EventDetailProp, EventAttributesProp } from '../types' import type { EventProp, EventDetailProp, EventAttributesProp } from '../types'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { useMapStore } from '@/stores/map' import { useMapStore } from '@/stores/map'
import { Close } from '@element-plus/icons-vue' import { Close, QuestionFilled } from '@element-plus/icons-vue'
import { getMetaEventDetail, updateAttributes, getIsDeleteAttribute } from '../api' import { getMetaEventDetail, updateAttributes, getIsDeleteAttribute } from '../api'
const store = useMapStore() const store = useMapStore()
...@@ -76,7 +76,7 @@ const changeFormatType = function (row: any) { ...@@ -76,7 +76,7 @@ const changeFormatType = function (row: any) {
row.format = '4' row.format = '4'
break break
case '3': case '3':
row.format = '2' row.format = '20'
break break
case '4': case '4':
row.format = 'yyyy-mm-dd' row.format = 'yyyy-mm-dd'
...@@ -86,10 +86,37 @@ const changeFormatType = function (row: any) { ...@@ -86,10 +86,37 @@ const changeFormatType = function (row: any) {
break break
} }
} }
const popoverText = function (row: any) {
let t = ''
if (row.type === '1') {
t = '你可以在这个字段中输入最多25个字符'
}
if (row.type === '2') {
t = '该字段可以存储最多四位数的整数'
}
if (row.type === '3') {
t = '数字的总长度为20位,包括整数部分和小数部分'
}
if (row.type === '4') {
t =
'yyyy: 表示四位数的年份,例如2023年会被表示为“2023”。<br/>mm: 表示两位数的月份,从01到12,例如3月表示为“03”。<br/>dd: 表示两位数的日期,从01到31,具体取决于月份和是否为闰年。<br/>例如“2023-04-01”代表的是2023年4月1日'
}
if (row.type === '5') {
t =
'yyyy: 表示四位数的年份,例如2023。<br/>mm: 表示两位数的月份,从01到12表示1月到12月。<br/>dd: 表示两位数的日期,从01到31,依据具体月份和是否为闰年。<br/>hh: 表示两位数的小时,使用24小时制,从00到23表示。<br/>mm: 表示两位数的分钟,从00到59。<br/>ss: 表示两位数的秒,从00到59。<br/>例如"2023-04-01 15:30:45"表示的是2023年4月1日下午3点30分45秒'
}
return t
}
</script> </script>
<template> <template>
<el-dialog title="事件属性" :close-on-click-modal="false" width="800px" @update:modelValue="value => $emit('update:modelValue', value)"> <el-dialog
title="事件属性"
:close-on-click-modal="false"
width="800px"
@update:modelValue="value => $emit('update:modelValue', value)"
>
<div style="display: flex; justify-content: space-around"> <div style="display: flex; justify-content: space-around">
<el-form label-width="120px"> <el-form label-width="120px">
<el-form-item label="事件英文名称:">{{ eventDetail?.english_name }}</el-form-item> <el-form-item label="事件英文名称:">{{ eventDetail?.english_name }}</el-form-item>
...@@ -104,7 +131,7 @@ const changeFormatType = function (row: any) { ...@@ -104,7 +131,7 @@ const changeFormatType = function (row: any) {
<template #header> <template #header>
<div class="card-header"> <div class="card-header">
<span>事件属性字段</span> <span>事件属性字段</span>
<el-button type="primary" @click="addField">添加</el-button> <el-button type="primary" @click="addField" :disabled="props.data?.can_edit === '0'">添加</el-button>
</div> </div>
</template> </template>
<el-table :data="tableData" style="width: 100%"> <el-table :data="tableData" style="width: 100%">
...@@ -120,26 +147,58 @@ const changeFormatType = function (row: any) { ...@@ -120,26 +147,58 @@ const changeFormatType = function (row: any) {
</el-table-column> </el-table-column>
<el-table-column label="字段类型"> <el-table-column label="字段类型">
<template #default="scope"> <template #default="scope">
<el-select @change="changeFormatType(scope.row)" :disabled="scope.row.id !== ''" v-model="scope.row.type" placeholder="请选择"> <el-select
<el-option :label="item.label" :value="item.value" :key="item.id" v-for="item in experimentAttributeOptions"></el-option> @change="changeFormatType(scope.row)"
:disabled="scope.row.id !== ''"
v-model="scope.row.type"
placeholder="请选择"
>
<el-option
:label="item.label"
:value="item.value"
:key="item.id"
v-for="item in experimentAttributeOptions"
></el-option>
</el-select> </el-select>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="字段格式"> <el-table-column label="字段格式">
<template #default="scope"> <template #default="scope">
<el-select :disabled="scope.row.id !== ''" v-if="scope.row.type === '4' || scope.row.type === '5'" v-model="scope.row.format" placeholder="请选择"> <el-select
:disabled="scope.row.id !== ''"
v-if="scope.row.type === '4' || scope.row.type === '5'"
v-model="scope.row.format"
placeholder="请选择"
>
<el-option v-if="scope.row.type !== '5'" label="yyyy-mm-dd" value="yyyy-mm-dd"></el-option> <el-option v-if="scope.row.type !== '5'" label="yyyy-mm-dd" value="yyyy-mm-dd"></el-option>
<el-option v-if="scope.row.type !== '4'" label="yyyy-mm-dd hh:mm:ss" value="yyyy-mm-dd hh:mm:ss"></el-option> <el-option
v-if="scope.row.type !== '4'"
label="yyyy-mm-dd hh:mm:ss"
value="yyyy-mm-dd hh:mm:ss"
></el-option>
</el-select> </el-select>
<el-input <el-input
v-else v-else
:disabled="scope.row.id !== ''" :disabled="scope.row.id !== ''"
type="number" type="number"
v-model="scope.row.format" v-model="scope.row.format"
:placeholder="scope.row.type === '1' ? '请输入字符串长度' : '请输入长度'"></el-input> :placeholder="scope.row.type === '1' ? '请输入字符串长度' : '请输入长度'"
></el-input>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column width="30"> <el-table-column width="30">
<template #default="scope">
<el-popover placement="top-start" :width="300" trigger="hover" :title="scope?.row.type_name">
<template #reference>
<div style="display: flex; justify-content: center; cursor: pointer">
<el-icon size="20"><QuestionFilled /></el-icon>
</div>
</template>
<div v-html="popoverText(scope.row)"></div>
</el-popover>
</template>
</el-table-column>
<el-table-column width="30" v-if="props.data?.can_edit !== '0'">
<template #default="scope"> <template #default="scope">
<div @click="deleteField(scope)" style="display: flex; justify-content: center; cursor: pointer"> <div @click="deleteField(scope)" style="display: flex; justify-content: center; cursor: pointer">
<el-icon size="20"><Close /></el-icon> <el-icon size="20"><Close /></el-icon>
...@@ -151,7 +210,9 @@ const changeFormatType = function (row: any) { ...@@ -151,7 +210,9 @@ const changeFormatType = function (row: any) {
<template #footer> <template #footer>
<el-row justify="center"> <el-row justify="center">
<el-button round auto-insert-space @click="$emit('update:modelValue', false)">关闭</el-button> <el-button round auto-insert-space @click="$emit('update:modelValue', false)">关闭</el-button>
<el-button type="primary" round auto-insert-space @click="handleSubmit">保存</el-button> <el-button type="primary" round auto-insert-space @click="handleSubmit" v-if="props.data?.can_edit !== '0'"
>保存</el-button
>
</el-row> </el-row>
</template> </template>
</el-dialog> </el-dialog>
......
...@@ -8,6 +8,7 @@ export interface EventProp { ...@@ -8,6 +8,7 @@ export interface EventProp {
status_name: string status_name: string
updated_operator_name: string updated_operator_name: string
updated_time: string updated_time: string
can_edit: string
} }
export interface ConnectionOptionProp { type_name: string; id: string } export interface ConnectionOptionProp { type_name: string; id: string }
......
...@@ -55,7 +55,9 @@ const listOptions = computed(() => { ...@@ -55,7 +55,9 @@ const listOptions = computed(() => {
label: '状态', label: '状态',
prop: 'status_name', prop: 'status_name',
computed: (row: any) => { computed: (row: any) => {
return row.row.status === '0' ? `<span style="color: rgb(170, 2, 49)">${row.row.status_name}</span>` : `<span style="color: #00ac27">${row.row.status_name}</span>` return row.row.status === '0'
? `<span style="color: rgb(170, 2, 49)">${row.row.status_name}</span>`
: `<span style="color: #00ac27">${row.row.status_name}</span>`
} }
}, },
{ label: '更新人', prop: 'updated_operator_name' }, { label: '更新人', prop: 'updated_operator_name' },
...@@ -120,14 +122,20 @@ const handleField = function (row: EventProp) { ...@@ -120,14 +122,20 @@ const handleField = function (row: EventProp) {
</template> </template>
<template #table-x="{ row }"> <template #table-x="{ row }">
<el-button type="primary" plain @click="handleView(row)">查看</el-button> <el-button type="primary" plain @click="handleView(row)">查看</el-button>
<el-button type="primary" plain @click="handleUpdate(row)">编辑</el-button> <el-button type="primary" plain @click="handleUpdate(row)" :disabled="row?.can_edit === '0'">编辑</el-button>
<el-button type="primary" plain @click="handleRemove(row)">删除</el-button> <el-button type="primary" plain @click="handleRemove(row)" :disabled="row?.can_edit === '0'">删除</el-button>
<el-button type="primary" plain @click="handleField(row)">字段</el-button> <el-button type="primary" plain @click="handleField(row)">字段</el-button>
</template> </template>
</AppList> </AppList>
</AppCard> </AppCard>
<!-- 新建/修改 --> <!-- 新建/修改 -->
<FormDialog v-model="formVisible" :data="currentRow" @update="handleRefresh" :option="experimentConnectionOptions || []" v-if="formVisible" /> <FormDialog
v-model="formVisible"
:data="currentRow"
@update="handleRefresh"
:option="experimentConnectionOptions || []"
v-if="formVisible"
/>
<!-- 查看 --> <!-- 查看 -->
<ViewDialog v-model="viewVisible" :data="currentRow" v-if="viewVisible && currentRow" /> <ViewDialog v-model="viewVisible" :data="currentRow" v-if="viewVisible && currentRow" />
<FieldDialog v-model="fieldVisible" :data="currentRow" v-if="fieldVisible && currentRow"></FieldDialog> <FieldDialog v-model="fieldVisible" :data="currentRow" v-if="fieldVisible && currentRow"></FieldDialog>
......
...@@ -4,6 +4,7 @@ import type { FormInstance, FormRules } from 'element-plus' ...@@ -4,6 +4,7 @@ import type { FormInstance, FormRules } from 'element-plus'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { useMapStore } from '@/stores/map' import { useMapStore } from '@/stores/map'
import { createMemberMeta, updateMemberMeta } from '../api' import { createMemberMeta, updateMemberMeta } from '../api'
import { QuestionFilled } from '@element-plus/icons-vue'
const store = useMapStore() const store = useMapStore()
...@@ -31,7 +32,8 @@ const form = reactive( ...@@ -31,7 +32,8 @@ const form = reactive(
english_name: '', english_name: '',
type: '', type: '',
format: '', format: '',
status: '1' status: '1',
type_name: ''
} }
) )
...@@ -84,7 +86,7 @@ const changeFormatType = function () { ...@@ -84,7 +86,7 @@ const changeFormatType = function () {
form.format = '4' form.format = '4'
break break
case '3': case '3':
form.format = '2' form.format = '20'
break break
case '4': case '4':
form.format = 'yyyy-mm-dd' form.format = 'yyyy-mm-dd'
...@@ -94,10 +96,37 @@ const changeFormatType = function () { ...@@ -94,10 +96,37 @@ const changeFormatType = function () {
break break
} }
} }
const popoverText = function (type: any) {
let t = ''
if (type === '1') {
t = '你可以在这个字段中输入最多25个字符'
}
if (type === '2') {
t = '该字段可以存储最多四位数的整数'
}
if (type === '3') {
t = '数字的总长度为20位,包括整数部分和小数部分'
}
if (type === '4') {
t =
'yyyy: 表示四位数的年份,例如2023年会被表示为“2023”。<br/>mm: 表示两位数的月份,从01到12,例如3月表示为“03”。<br/>dd: 表示两位数的日期,从01到31,具体取决于月份和是否为闰年。<br/>例如“2023-04-01”代表的是2023年4月1日'
}
if (type === '5') {
t =
'yyyy: 表示四位数的年份,例如2023。<br/>mm: 表示两位数的月份,从01到12表示1月到12月。<br/>dd: 表示两位数的日期,从01到31,依据具体月份和是否为闰年。<br/>hh: 表示两位数的小时,使用24小时制,从00到23表示。<br/>mm: 表示两位数的分钟,从00到59。<br/>ss: 表示两位数的秒,从00到59。<br/>例如"2023-04-01 15:30:45"表示的是2023年4月1日下午3点30分45秒'
}
return t
}
</script> </script>
<template> <template>
<el-dialog :title="title" :close-on-click-modal="false" width="600px" @update:modelValue="value => $emit('update:modelValue', value)"> <el-dialog
:title="title"
:close-on-click-modal="false"
width="600px"
@update:modelValue="value => $emit('update:modelValue', value)"
>
<el-form ref="formRef" :model="form" :rules="rules" label-suffix=":" label-width="122px"> <el-form ref="formRef" :model="form" :rules="rules" label-suffix=":" label-width="122px">
<el-form-item label="属性ID" v-if="isUpdate"> <el-form-item label="属性ID" v-if="isUpdate">
{{ props.data?.id }} {{ props.data?.id }}
...@@ -109,16 +138,48 @@ const changeFormatType = function () { ...@@ -109,16 +138,48 @@ const changeFormatType = function () {
<el-input v-model="form.name" placeholder="请输入" /> <el-input v-model="form.name" placeholder="请输入" />
</el-form-item> </el-form-item>
<el-form-item label="属性字段类型" prop="type"> <el-form-item label="属性字段类型" prop="type">
<el-select :disabled="isUpdate" @change="changeFormatType" v-model="form.type" style="width: 100%" placeholder="请选择"> <el-select
<el-option :label="item.label" :value="item.value" :key="item.id" v-for="item in experimentAttributeOptions"></el-option> :disabled="isUpdate"
@change="changeFormatType"
v-model="form.type"
style="width: 100%"
placeholder="请选择"
>
<el-option
:label="item.label"
:value="item.value"
:key="item.id"
v-for="item in experimentAttributeOptions"
></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="属性字段格式" prop="format" v-if="form.type !== ''"> <el-form-item label="属性字段格式" prop="format" v-if="form.type !== ''">
<el-select :disabled="isUpdate" v-if="form.type === '4' || form.type === '5'" v-model="form.format" style="width: 100%" placeholder="请选择"> <el-select
:disabled="isUpdate"
v-if="form.type === '4' || form.type === '5'"
v-model="form.format"
style="width: 93%; margin-right: 10px"
placeholder="请选择"
>
<el-option v-if="form.type !== '5'" label="yyyy-mm-dd" value="yyyy-mm-dd"></el-option> <el-option v-if="form.type !== '5'" label="yyyy-mm-dd" value="yyyy-mm-dd"></el-option>
<el-option v-if="form.type !== '4'" label="yyyy-mm-dd hh:mm:ss" value="yyyy-mm-dd hh:mm:ss"></el-option> <el-option v-if="form.type !== '4'" label="yyyy-mm-dd hh:mm:ss" value="yyyy-mm-dd hh:mm:ss"></el-option>
</el-select> </el-select>
<el-input :disabled="isUpdate" type="number" v-else v-model="form.format" :placeholder="formatPlaceholder" /> <el-input
style="width: 93%; margin-right: 10px"
:disabled="isUpdate"
type="number"
v-else
v-model="form.format"
:placeholder="formatPlaceholder"
/>
<el-popover placement="top-start" :width="300" trigger="hover" :title="form.type_name">
<template #reference>
<div style="display: flex; justify-content: center; cursor: pointer">
<el-icon size="20"><QuestionFilled /></el-icon>
</div>
</template>
<div v-html="popoverText(form.type)"></div>
</el-popover>
</el-form-item> </el-form-item>
<el-form-item label="状态" prop="status"> <el-form-item label="状态" prop="status">
<el-switch v-model="form.status" active-text="生效" inactive-text="失效" active-value="1" inactive-value="0" /> <el-switch v-model="form.status" active-text="生效" inactive-text="失效" active-value="1" inactive-value="0" />
......
...@@ -48,7 +48,9 @@ const listOptions = computed(() => { ...@@ -48,7 +48,9 @@ const listOptions = computed(() => {
label: '状态', label: '状态',
prop: 'status_name', prop: 'status_name',
computed: (row: any) => { computed: (row: any) => {
return row.row.status === '0' ? `<span style="color: rgb(170, 2, 49)">${row.row.status_name}</span>` : `<span style="color: #00ac27">${row.row.status_name}</span>` return row.row.status === '0'
? `<span style="color: rgb(170, 2, 49)">${row.row.status_name}</span>`
: `<span style="color: #00ac27">${row.row.status_name}</span>`
} }
}, },
{ label: '更新人', prop: 'updated_operator_name' }, { label: '更新人', prop: 'updated_operator_name' },
......
<script setup lang="ts"> <script setup lang="ts">
import { getProgress } from '../api' import { getProgress } from '../api'
import AppList from '@/components/base/AppList.vue'
defineEmits<{ defineEmits<{
(e: 'update'): void (e: 'update'): void
...@@ -13,7 +14,7 @@ const listOptions = computed(() => { ...@@ -13,7 +14,7 @@ const listOptions = computed(() => {
httpRequest: getProgress, httpRequest: getProgress,
params: {} params: {}
}, },
limit: 5, limit: 10,
columns: [ columns: [
{ label: '文件名', prop: 'name' }, { label: '文件名', prop: 'name' },
{ {
...@@ -23,6 +24,7 @@ const listOptions = computed(() => { ...@@ -23,6 +24,7 @@ const listOptions = computed(() => {
return bytesToSize(row.row.size) return bytesToSize(row.row.size)
} }
}, },
{ label: '来源链接', prop: 'connection_name' },
{ {
label: '状态', label: '状态',
prop: 'status_name', prop: 'status_name',
...@@ -32,7 +34,9 @@ const listOptions = computed(() => { ...@@ -32,7 +34,9 @@ const listOptions = computed(() => {
: `<span style="color: #00ac27">${row.row.status_name}</span>` : `<span style="color: #00ac27">${row.row.status_name}</span>`
} }
}, },
{ label: '导入时间', prop: 'created_time' } { label: '更新人', prop: 'updated_operator_name' },
{ label: '导入时间', prop: 'created_time' },
{ label: '操作', slots: 'table-x', width: '100' }
] ]
} }
}) })
...@@ -45,14 +49,25 @@ const bytesToSize = (bytes: number) => { ...@@ -45,14 +49,25 @@ const bytesToSize = (bytes: number) => {
return (bytes / Math.pow(k, i)).toPrecision(3) + ' ' + sizes[i] return (bytes / Math.pow(k, i)).toPrecision(3) + ' ' + sizes[i]
} }
const appList = $ref<InstanceType<typeof AppList> | null>(null)
const refetch = function () {
appList?.refetch()
}
</script> </script>
<template> <template>
<el-dialog <el-dialog
class="connect-form" class="connect-form"
title="导入用户数据" title="导入用户数据"
:close-on-click-modal="false" :close-on-click-modal="false"
width="800px" width="900px"
@update:modelValue="value => $emit('update:modelValue', value)"> @update:modelValue="value => $emit('update:modelValue', value)"
<AppList v-bind="listOptions" ref="appList"></AppList> >
<AppList v-bind="listOptions" ref="appList">
<template #table-x="{ row }">
<el-button type="primary" plain @click="refetch" v-permission="'v1-experiment-member-delete'">刷新</el-button>
</template>
</AppList>
</el-dialog> </el-dialog>
</template> </template>
...@@ -62,6 +62,9 @@ export interface ImageProp { ...@@ -62,6 +62,9 @@ export interface ImageProp {
history_tags: string[] history_tags: string[]
static_groups: string[] static_groups: string[]
dynamic_groups: string[] dynamic_groups: string[]
dynamic_group_list: any[]
static_group_list: any[]
tag_list: any[]
events: { events: {
list: { list: {
updated_time: string updated_time: string
......
...@@ -5,6 +5,8 @@ import type { MemberFieldsProp, ImageProp, ConnectionsProp } from '../types' ...@@ -5,6 +5,8 @@ import type { MemberFieldsProp, ImageProp, ConnectionsProp } from '../types'
import Icon from '@/components/ConnectionIcon.vue' import Icon from '@/components/ConnectionIcon.vue'
const ViewEvent = defineAsyncComponent(() => import('@/components/ViewEvent.vue')) const ViewEvent = defineAsyncComponent(() => import('@/components/ViewEvent.vue'))
const ViewLabel = defineAsyncComponent(() => import('@/components/ViewLabel.vue'))
const ViewGroup = defineAsyncComponent(() => import('@/components/ViewGroup.vue'))
const route = useRoute() const route = useRoute()
...@@ -42,12 +44,28 @@ const getDate = function (date: string) { ...@@ -42,12 +44,28 @@ const getDate = function (date: string) {
return parseInt(date.slice(date.indexOf(' '), date.indexOf(' ') + 3)) > 12 ? '下午' : '上午' return parseInt(date.slice(date.indexOf(' '), date.indexOf(' ') + 3)) > 12 ? '下午' : '上午'
} }
// 查看事件
const viewEventVisible = ref(false) const viewEventVisible = ref(false)
const currentViewEvent = ref() const currentViewEvent = ref()
function handleViewEvent(item: any) { function handleViewEvent(item: any) {
viewEventVisible.value = true viewEventVisible.value = true
currentViewEvent.value = item currentViewEvent.value = item
} }
// 查看标签
const viewLabelVisible = ref(false)
const currentViewLabel = ref()
function handleViewLabel(item: any) {
viewLabelVisible.value = true
currentViewLabel.value = item
}
// 查看群组
const viewGroupVisible = ref(false)
const currentViewGroup = ref()
function handleViewGroup(item: any) {
viewGroupVisible.value = true
currentViewGroup.value = item
}
// 获取链接 // 获取链接
const connectionList = ref<ConnectionsProp[]>([]) const connectionList = ref<ConnectionsProp[]>([])
...@@ -130,7 +148,7 @@ watch(currentConnection, () => { ...@@ -130,7 +148,7 @@ watch(currentConnection, () => {
<el-tabs class="demo-tabs"> <el-tabs class="demo-tabs">
<el-tab-pane label="当前标签"> <el-tab-pane label="当前标签">
<div class="scroll" v-if="data?.tags && data.tags.length"> <div class="scroll" v-if="data?.tags && data.tags.length">
<el-tag class="ml-2" type="success" v-for="(item, index) in data.tags" :key="index">{{ item }}</el-tag> <el-tag class="ml-2" type="success" v-for="(item, index) in data.tag_list" :key="index" @click="handleViewLabel(item)">{{ item.name }}</el-tag>
</div> </div>
<el-empty description="暂无数据" :image-size="80" v-else /> <el-empty description="暂无数据" :image-size="80" v-else />
</el-tab-pane> </el-tab-pane>
...@@ -148,7 +166,9 @@ watch(currentConnection, () => { ...@@ -148,7 +166,9 @@ watch(currentConnection, () => {
<el-tabs class="demo-tabs"> <el-tabs class="demo-tabs">
<el-tab-pane label="静态群组"> <el-tab-pane label="静态群组">
<div class="scroll" v-if="data?.static_groups && data.static_groups.length"> <div class="scroll" v-if="data?.static_groups && data.static_groups.length">
<el-tag class="ml-2" type="success" v-for="(item, index) in data.static_groups" :key="index">{{ item }}</el-tag> <el-tag class="ml-2" type="success" v-for="(item, index) in data.static_group_list" :key="index" @click="handleViewGroup(item)">{{
item.name
}}</el-tag>
</div> </div>
<el-empty description="暂无数据" :image-size="80" v-else /> <el-empty description="暂无数据" :image-size="80" v-else />
</el-tab-pane> </el-tab-pane>
...@@ -156,7 +176,9 @@ watch(currentConnection, () => { ...@@ -156,7 +176,9 @@ watch(currentConnection, () => {
<el-tabs class="demo-tabs"> <el-tabs class="demo-tabs">
<el-tab-pane label="动态群组"> <el-tab-pane label="动态群组">
<div class="scroll" v-if="data?.dynamic_groups && data.dynamic_groups.length"> <div class="scroll" v-if="data?.dynamic_groups && data.dynamic_groups.length">
<el-tag class="ml-2" type="success" v-for="(item, index) in data.dynamic_groups" :key="index">{{ item }}</el-tag> <el-tag class="ml-2" type="success" v-for="(item, index) in data.dynamic_group_list" :key="index" @click="handleViewGroup(item)">{{
item.name
}}</el-tag>
</div> </div>
<el-empty description="暂无数据" :image-size="80" v-else /> <el-empty description="暂无数据" :image-size="80" v-else />
</el-tab-pane> </el-tab-pane>
...@@ -191,6 +213,10 @@ watch(currentConnection, () => { ...@@ -191,6 +213,10 @@ watch(currentConnection, () => {
</div> </div>
<!-- 事件详情 --> <!-- 事件详情 -->
<ViewEvent v-model="viewEventVisible" :event="currentViewEvent" :user="data" v-if="viewEventVisible && currentViewEvent"></ViewEvent> <ViewEvent v-model="viewEventVisible" :event="currentViewEvent" :user="data" v-if="viewEventVisible && currentViewEvent"></ViewEvent>
<!-- 查看标签 -->
<ViewLabel v-model="viewLabelVisible" :data="currentViewLabel" v-if="viewLabelVisible && currentViewLabel"></ViewLabel>
<!-- 查看群组 -->
<ViewGroup v-model="viewGroupVisible" :data="currentViewGroup" v-if="viewGroupVisible && currentViewGroup"></ViewGroup>
</template> </template>
<style lang="scss"> <style lang="scss">
.info-box { .info-box {
......
...@@ -4,6 +4,9 @@ import AppList from '@/components/base/AppList.vue' ...@@ -4,6 +4,9 @@ import AppList from '@/components/base/AppList.vue'
import { getMemberList, deleteMember, getMemberConnectionsList } from '../api' import { getMemberList, deleteMember, getMemberConnectionsList } from '../api'
import { ElMessage, ElMessageBox, ElLoading } from 'element-plus' import { ElMessage, ElMessageBox, ElLoading } from 'element-plus'
import type { MemberProp, ConnectionsProp } from '../types' import type { MemberProp, ConnectionsProp } from '../types'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const UpdateDialog = defineAsyncComponent(() => import('../components/UpdateDialog.vue')) const UpdateDialog = defineAsyncComponent(() => import('../components/UpdateDialog.vue'))
const UploadEventsDialog = defineAsyncComponent(() => import('../components/UploadEventsDialog.vue')) const UploadEventsDialog = defineAsyncComponent(() => import('../components/UploadEventsDialog.vue'))
...@@ -207,17 +210,27 @@ const downloadMember = function (isAll?: boolean) { ...@@ -207,17 +210,27 @@ const downloadMember = function (isAll?: boolean) {
<template #header-buttons> <template #header-buttons>
<el-row justify="space-between"> <el-row justify="space-between">
<el-space> <el-space>
<el-button type="primary" :icon="Plus" @click="handleAdd" v-permission="'v1-experiment-member-create'">新建</el-button> <el-button
v-if="!userStore.status.status"
type="primary"
:icon="Plus"
@click="handleAdd"
v-permission="'v1-experiment-member-create'"
>新建</el-button
>
<el-dropdown v-permission="'v1-experiment-member-download'"> <el-dropdown v-permission="'v1-experiment-member-download'">
<el-button type="primary" :icon="Download">导出</el-button> <el-button type="primary" :icon="Download">导出</el-button>
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item @click="downloadMember(true)">全部用户数据</el-dropdown-item> <el-dropdown-item @click="downloadMember(true)">全部用户数据</el-dropdown-item>
<el-dropdown-item :disabled="!multipleSelection.length" @click="downloadMember(false)">勾选用户数据</el-dropdown-item> <el-dropdown-item :disabled="!multipleSelection.length" @click="downloadMember(false)"
>勾选用户数据</el-dropdown-item
>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown>
<el-dropdown v-permission="['v1-experiment-member-member-upload', 'v1-experiment-member-event-upload']"> <!-- v-permission="['v1-experiment-member-member-upload', 'v1-experiment-member-event-upload']" -->
<el-dropdown>
<el-button type="primary" :icon="Upload">导入</el-button> <el-button type="primary" :icon="Upload">导入</el-button>
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
...@@ -226,14 +239,19 @@ const downloadMember = function (isAll?: boolean) { ...@@ -226,14 +239,19 @@ const downloadMember = function (isAll?: boolean) {
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown>
<el-button type="primary" @click="progressVisible = true" v-permission="'v1-experiment-member-tasks'">数据导入进度</el-button> <el-button type="primary" @click="progressVisible = true" v-permission="'v1-experiment-member-tasks'"
>数据导入进度</el-button
>
<!-- <el-button type="danger" plain :icon="Delete" :disabled="!multipleSelection.length" @click="handleRemoves()" v-permission="'v1-experiment-member-delete'">删除</el-button> --> <!-- <el-button type="danger" plain :icon="Delete" :disabled="!multipleSelection.length" @click="handleRemoves()" v-permission="'v1-experiment-member-delete'">删除</el-button> -->
<el-dropdown v-permission="'v1-experiment-member-delete'"> <!-- v-permission="'v1-experiment-member-delete'" -->
<el-dropdown>
<el-button type="danger" :icon="Delete">删除</el-button> <el-button type="danger" :icon="Delete">删除</el-button>
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item @click="handleRemoves(true)">删除全部用户</el-dropdown-item> <el-dropdown-item @click="handleRemoves(true)">删除全部用户</el-dropdown-item>
<el-dropdown-item :disabled="!multipleSelection.length" @click="handleRemoves(false)">删除勾选用户</el-dropdown-item> <el-dropdown-item :disabled="!multipleSelection.length" @click="handleRemoves(false)"
>删除勾选用户</el-dropdown-item
>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown>
...@@ -244,8 +262,12 @@ const downloadMember = function (isAll?: boolean) { ...@@ -244,8 +262,12 @@ const downloadMember = function (isAll?: boolean) {
<template #table-x="{ row }"> <template #table-x="{ row }">
<el-button type="primary" plain @click="handleImage(row)">画像</el-button> <el-button type="primary" plain @click="handleImage(row)">画像</el-button>
<el-button type="primary" plain @click="handleView(row)">查看</el-button> <el-button type="primary" plain @click="handleView(row)">查看</el-button>
<el-button type="primary" plain @click="handleEdit(row)" v-permission="'v1-experiment-member-update'">编辑</el-button> <el-button type="primary" plain @click="handleEdit(row)" v-permission="'v1-experiment-member-update'"
<el-button type="primary" plain @click="handleRemove(row)" v-permission="'v1-experiment-member-delete'">删除</el-button> >编辑</el-button
>
<el-button type="primary" plain @click="handleRemove(row)" v-permission="'v1-experiment-member-delete'"
>删除</el-button
>
<el-button type="primary" plain @click="goPage(row)">事件</el-button> <el-button type="primary" plain @click="goPage(row)">事件</el-button>
</template> </template>
</AppList> </AppList>
......
...@@ -25,7 +25,7 @@ router.beforeEach(async (to, from, next) => { ...@@ -25,7 +25,7 @@ router.beforeEach(async (to, from, next) => {
path: to.path, path: to.path,
query: { query: {
...to.query, ...to.query,
experiment_id: from.query.experiment_id || '7165149417073278976', experiment_id: from.query.experiment_id || '7028276368903241728',
student_id: from.query.student_id, student_id: from.query.student_id,
force_tgc: from.query.force_tgc force_tgc: from.query.force_tgc
} }
......
...@@ -72,50 +72,57 @@ const studentMenus: IMenuItem[] = [ ...@@ -72,50 +72,57 @@ const studentMenus: IMenuItem[] = [
name: '文本资料管理', name: '文本资料管理',
path: '/material?type=1', path: '/material?type=1',
icon: markRaw(IconText), icon: markRaw(IconText),
tag: 'v1-experiment-marketing-material-list' // tag: 'v1-experiment-marketing-material-list'
tag: ''
}, },
{ {
name: '图片资料管理', name: '图片资料管理',
path: '/material?type=2', path: '/material?type=2',
icon: markRaw(IconImage), icon: markRaw(IconImage),
tag: 'v1-experiment-marketing-material-list' // tag: 'v1-experiment-marketing-material-list'
tag: ''
}, },
{ {
name: '卡券资料管理', name: '卡券资料管理',
path: '/material?type=8', path: '/material?type=8',
icon: markRaw(IconCard), icon: markRaw(IconCard),
tag: 'v1-experiment-marketing-material-list' // tag: 'v1-experiment-marketing-material-list'
tag: ''
}, },
{ {
name: '语音资料管理', name: '语音资料管理',
path: '/material?type=3', path: '/material?type=3',
icon: markRaw(IconAudio), icon: markRaw(IconAudio),
tag: 'v1-experiment-marketing-material-list' // tag: 'v1-experiment-marketing-material-list'
tag: ''
}, },
{ {
name: '视频资料管理', name: '视频资料管理',
path: '/material?type=4', path: '/material?type=4',
icon: markRaw(IconVideo), icon: markRaw(IconVideo),
tag: 'v1-experiment-marketing-material-list' // tag: 'v1-experiment-marketing-material-list'
tag: ''
}, },
{ name: 'H5资料管理', path: '/material?type=5', icon: markRaw(IconH5), tag: 'v1-experiment-marketing-material-list' }, { name: 'H5资料管理', path: '/material?type=5', icon: markRaw(IconH5), tag: 'v1-experiment-marketing-material-list' },
{ {
name: '二维码资料管理', name: '二维码资料管理',
path: '/material?type=6', path: '/material?type=6',
icon: markRaw(IconQrcode), icon: markRaw(IconQrcode),
tag: 'v1-experiment-marketing-material-list' // tag: 'v1-experiment-marketing-material-list'
tag: ''
}, },
{ {
name: '小程序资料管理', name: '小程序资料管理',
path: '/material?type=7', path: '/material?type=7',
icon: markRaw(IconMiniProgram), icon: markRaw(IconMiniProgram),
tag: 'v1-experiment-marketing-material-list' // tag: 'v1-experiment-marketing-material-list'
tag: ''
} }
] ]
}, },
{ {
name: '自动化营销', name: '自动化营销',
path: '/trip', path: '/trip/my',
icon: markRaw(IconTrip) icon: markRaw(IconTrip)
}, },
{ {
......
...@@ -15,7 +15,7 @@ interface State { ...@@ -15,7 +15,7 @@ interface State {
organization: OrganizationType | null organization: OrganizationType | null
roles: RoleType[] roles: RoleType[]
permissions: PermissionType[] permissions: PermissionType[]
status: boolean status: any
} }
export const useUserStore = defineStore({ export const useUserStore = defineStore({
...@@ -27,7 +27,7 @@ export const useUserStore = defineStore({ ...@@ -27,7 +27,7 @@ export const useUserStore = defineStore({
project: null, project: null,
roles: [], roles: [],
permissions: [], permissions: [],
status: false status: {}
}), }),
getters: { getters: {
isLogin: state => !!state.user isLogin: state => !!state.user
...@@ -45,7 +45,7 @@ export const useUserStore = defineStore({ ...@@ -45,7 +45,7 @@ export const useUserStore = defineStore({
this.permissions = permissions this.permissions = permissions
await useMapStore().getMapList() await useMapStore().getMapList()
await checkDataStatus().then(res => { await checkDataStatus().then(res => {
this.status = res.data.status this.status = res.data
}) })
}, },
async logout() { async logout() {
......
...@@ -108,7 +108,7 @@ export interface EventRuleItem { ...@@ -108,7 +108,7 @@ export interface EventRuleItem {
// 标签规则 // 标签规则
export interface TagRule { export interface TagRule {
current_logic_operate: 'and' | 'or' current_logic_operate: 'and' | 'or'
items: string[] items: any[]
} }
// 用户行为规则 // 用户行为规则
......
...@@ -16,7 +16,8 @@ export const tripTemplateTypeList = [ ...@@ -16,7 +16,8 @@ export const tripTemplateTypeList = [
// 群组类型 // 群组类型
export const groupTypeList = [ export const groupTypeList = [
{ label: '静态群组', value: '1' }, { label: '静态群组', value: '1' },
{ label: '动态群组', value: '2' } { label: '动态群组', value: '2' },
{ label: 'RFM群组', value: '3' }
] ]
// 更新方式 // 更新方式
...@@ -96,9 +97,12 @@ export const happenInfoList = [ ...@@ -96,9 +97,12 @@ export const happenInfoList = [
export const triggerInfoList = [{ label: '触发次数', value: '触发次数' }] export const triggerInfoList = [{ label: '触发次数', value: '触发次数' }]
export const labelList = [ export const labelList = [
{ label: '自定义分层标签 ', value: '1' }, { label: '单属性标签', value: '5' },
{ label: '多属性标签', value: '6' },
{ label: '事件偏好标签 ', value: '2' }, { label: '事件偏好标签 ', value: '2' },
{ label: '事件指标标签 ', value: '3' }, { label: '事件指标标签 ', value: '3' },
{ label: '自定义标签', value: '7' },
{ label: '分层标签 ', value: '1' },
{ label: 'RFM模型标签 ', value: '4' } { label: 'RFM模型标签 ', value: '4' }
] ]
...@@ -110,7 +114,8 @@ export const wayList = [ ...@@ -110,7 +114,8 @@ export const wayList = [
export const materialMethodList = [ export const materialMethodList = [
{ label: '离线上传 ', value: '2' }, { label: '离线上传 ', value: '2' },
{ label: '在线AI ', value: '1' } { label: '在线AI', value: '1' },
{ label: '在线设计', value: '3' }
] ]
// 使用场景 // 使用场景
...@@ -129,3 +134,24 @@ export const materialUsersList = [ ...@@ -129,3 +134,24 @@ export const materialUsersList = [
{ label: '运维人员 ', value: '4' }, { label: '运维人员 ', value: '4' },
{ label: '机器系统 ', value: '5' } { label: '机器系统 ', value: '5' }
] ]
// 图片风格
export const materialPictureStyleList = [
{ label: '商务风格 ', value: '1' },
{ label: '扁平化 ', value: '2' },
{ label: '清新文艺 ', value: '3' },
{ label: '古典中国风 ', value: '4' },
{ label: '创意简约风 ', value: '5' },
{ label: '艺术插画 ', value: '6' },
{ label: '大气渐变 ', value: '7' },
{ label: '手绘水彩风 ', value: '8' },
{ label: '时尚杂志风 ', value: '9' },
{ label: '漫画风格 ', value: '10' }
]
// 文本用途
export const textPurposeList = [
{ label: '消息/短息', value: '1' },
{ label: '长文本/文章 ', value: '2' },
{ label: '短视频脚本 ', value: '3' }
]
...@@ -5,7 +5,7 @@ import type { DirectiveBinding } from 'vue' ...@@ -5,7 +5,7 @@ import type { DirectiveBinding } from 'vue'
export function checkPermission(value: string | string[]): boolean { export function checkPermission(value: string | string[]): boolean {
const userStore = useUserStore() const userStore = useUserStore()
// true 是学员且使用公共数据(学员不能自己创建数据) false 学员可以自己创建数据 // true 是学员且使用公共数据(学员不能自己创建数据) false 学员可以自己创建数据
if (!userStore.status) return true if (!userStore.status.status || !userStore.status.group_status || !userStore.status.material_status || !userStore.status.tag_status) return true
const permissions = userStore.permissions const permissions = userStore.permissions
if (Array.isArray(value)) { if (Array.isArray(value)) {
return permissions.some(item => value.includes(item.tag)) return permissions.some(item => value.includes(item.tag))
...@@ -13,7 +13,6 @@ export function checkPermission(value: string | string[]): boolean { ...@@ -13,7 +13,6 @@ export function checkPermission(value: string | string[]): boolean {
return !!permissions.find(item => item.tag === value) return !!permissions.find(item => item.tag === value)
} }
} }
// 权限指令 // 权限指令
export function permissionDirective(el: HTMLElement, binding: DirectiveBinding) { export function permissionDirective(el: HTMLElement, binding: DirectiveBinding) {
const { value } = binding const { value } = binding
......
import axios from 'axios'
import md5 from 'blueimp-md5' import md5 from 'blueimp-md5'
import { getSignature, uploadFile } from '@/api/base' import { getSignature, uploadFile } from '@/api/base'
...@@ -17,3 +18,8 @@ export async function upload(blob: Blob) { ...@@ -17,3 +18,8 @@ export async function upload(blob: Blob) {
await uploadFile(params) await uploadFile(params)
return params.url return params.url
} }
export async function uploadFileByUrl(url: string) {
const res = await axios.get(url, { responseType: 'blob' })
return upload(res.data)
}
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论