提交 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 @@
"cert": "node ./cert.js"
},
"dependencies": {
"@chuangkit/chuangkit-design": "^2.0.5",
"@element-plus/icons-vue": "^2.3.1",
"@fortaine/fetch-event-source": "^3.0.6",
"@tinymce/tinymce-vue": "^5.0.1",
......@@ -30,7 +31,7 @@
"lodash-es": "^4.17.21",
"nanoid": "^5.0.7",
"pinia": "^2.1.7",
"vue": "^3.4.23",
"vue": "^3.4.26",
"vue-echarts": "^6.6.9",
"vue-router": "^4.3.2",
"xss": "^1.0.15"
......@@ -40,18 +41,18 @@
"@tsconfig/node20": "^20.1.4",
"@types/blueimp-md5": "^2.18.2",
"@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/eslint-config-typescript": "^12.0.0",
"@vue/eslint-config-typescript": "^13.0.0",
"@vue/tsconfig": "^0.5.1",
"ali-oss": "^6.18.1",
"chalk": "^5.2.0",
"eslint": "^8.43.0",
"eslint-plugin-vue": "^9.17.0",
"sass": "^1.67.0",
"eslint": "^8.57.0",
"eslint-plugin-vue": "^9.25.0",
"sass": "^1.75.0",
"typescript": "~5.4.5",
"unplugin-auto-import": "^0.17.5",
"vite": "^4.5.3",
"vite": "^5.2.10",
"vue-tsc": "^1.8.27"
}
}
......@@ -139,3 +139,13 @@ export function uploadLocalFile(data: { file: File; file_name: string; is_contin
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 })
</template>
<template v-else>
<!-- 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 -->
<el-select v-model="params[item.prop]" v-bind="item" filterable clearable @change="search" v-if="item.type === 'select'">
<el-select
v-model="params[item.prop]"
v-bind="item"
filterable
clearable
@change="search"
v-if="item.type === 'select'"
>
<el-option
v-for="(option, index) in item.options"
:label="option[item.labelKey] || option.label || option"
:value="option[item.valueKey] || option.value || option"
:key="index" />
:key="index"
/>
</el-select>
</template>
</el-form-item>
......@@ -157,7 +171,13 @@ defineExpose({ refetch, tableRef })
<!-- 主体 -->
<div class="table-list-bd">
<slot name="body" v-bind="{ data: dataList }">
<el-table :header-cell-style="{ background: '#ededed' }" :data="dataList" v-loading="loading" v-bind="$attrs" ref="tableRef">
<el-table
:header-cell-style="{ background: '#ededed' }"
:data="dataList"
v-loading="loading"
v-bind="$attrs"
ref="tableRef"
>
<el-table-column align="center" v-bind="item || {}" v-for="item in columns" :key="item.prop">
<template #default="scope" v-if="item.slots || item.computed">
<slot :name="item.slots" v-bind="scope" v-if="item.slots"></slot>
......@@ -176,14 +196,15 @@ defineExpose({ refetch, tableRef })
class="table-list-pagination"
background
layout="total, sizes, prev, pager, next, jumper"
:page-sizes="[10, 20, 30, 50, 100]"
:page-sizes="[10, 20, 30, 50]"
:page-size="page.size"
:total="page.total"
v-model:currentPage="page.currentPage"
@size-change="pageSizeChange"
@current-change="fetchList()"
:hide-on-single-page="true"
v-if="hasPagination">
v-if="hasPagination"
>
</el-pagination>
</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) {
</div>
</div>
<el-button text :icon="Plus" @click="handleAdd(eventAttrRule.items)" v-if="eventAttrRule.items.length < limit"
>添加</el-button
>添加</el-button
>
<slot name="footer"></slot>
</el-card>
......
<script setup lang="ts">
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 { useRfmRes } from '@/composables/useRFMData'
// const tagRule = ref(inject('tagRule') as TagRule)
const tagRule = defineModel<TagRule>({ default: { current_logic_operate: 'and', items: [] } })
const { tagList } = useTag()
const { rfmResList } = useRfmRes()
// 获取逻辑运算符名称
function getLogicalName(value: 'and' | 'or') {
......@@ -19,14 +21,33 @@ function toggleOperate(rule: TagRule) {
}
// 添加条件
function handleAdd(items: string[]) {
items.push('')
function handleAdd(items: any[]) {
items.push({ tag_id: '' })
}
// 删除
function handleRemove(items: string[], index: number) {
function handleRemove(items: any[], index: number) {
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>
<template>
......@@ -44,9 +65,38 @@ function handleRemove(items: string[], index: number) {
<div>
标签 等于
<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-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>
</div>
<el-button text :icon="CloseBold" @click="handleRemove(tagRule.items, index)"></el-button>
......
......@@ -44,7 +44,7 @@ const handleInputConfirm = () => {
<template>
<p class="rule-tips">
<el-icon><Warning /></el-icon>
自定义分层标签:将满足不同分层规则的用户打上分层标签,同一用户会被优先匹配在顺序靠前的分层
分层标签:将满足不同分层规则的用户打上分层标签,同一用户会被优先匹配在顺序靠前的分层
</p>
<div>
<el-tag
......
<script setup lang="ts">
import RFMRuleItem from './RFMRuleItem.vue'
import { useRfmStatistics } from '@/composables/useRFMData'
const props = defineProps<{ tagId?: string }>()
const form = defineModel<any>({
default: {
R: {},
......@@ -8,10 +10,23 @@ const form = defineModel<any>({
M: {}
}
})
const { rfmStatistics } = useRfmStatistics(props.tagId || '')
</script>
<template>
<RFMRuleItem label="R" v-model="form.R" />
<RFMRuleItem label="F" v-model="form.F" style="margin-top: 20px" />
<RFMRuleItem label="M" v-model="form.M" style="margin-top: 20px" />
<RFMRuleItem label="R" v-model="form.R">
<template #header-aside="{ data }">
<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>
<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'
defineProps<{ label: string }>()
......@@ -28,26 +29,16 @@ onMounted(() => {
form.value = Object.assign({ basis: '1', rule: '101', event_id: '-1', attr_id: '', attr_type: '', config: [...defaultScore] }, form.value)
})
const { userAttrList } = useUserAttr()
const { metaEventList } = useMetaEvent()
const { userAttrList, fetchUserAttrList } = useUserAttr()
const { metaEventList, fetchMetaEventList } = useMetaEvent()
const { userAttrRange, fetchUserAttrRange } = useUserAttrRange()
const currentRuleList = computed(() => {
return ruleList.filter(item => item.basis.includes(form.value.basis))
})
const currentAttrList = computed(() => {
let list = userAttrList.value
return list.filter(item => {
if (form.value.rule === '101') {
return ['2', '3', '4', '5'].includes(item.type)
} else if (form.value.rule === '102') {
return ['1'].includes(item.type)
}
return true
})
})
const currentMetaEventList = computed(() => {
return [{ id: '-1', name: '所有事件' }, ...metaEventList.value]
return [{ event_id: '-1', event_name: '所有事件' }, ...metaEventList.value]
})
function handleBasisChange(value: any) {
......@@ -70,7 +61,7 @@ function handleRuleChange(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 }[]>([])
......@@ -87,23 +78,56 @@ async function remoteMethod(search: string = '') {
loading.value = false
}
}
function querySearch(queryString: string, cb: any) {
cb(options.value)
}
watch(
() => 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 }
)
const a = [
{ id: '001', label: '2000' },
{ id: '002', label: '1500' },
{ id: '003', label: '3000' },
{ id: '004', label: '2200' },
{ id: '005', label: '1800' }
]
</script>
<template>
<el-card shadow="never">
<template #header>
<div class="rfm-top">
<div>
<el-button circle type="primary" style="width: 32px; margin-right: 10px">{{ label }}</el-button>
{{ label }}值计算规则
</div>
<div>
<slot name="header-aside" :data="form"></slot>
</div>
</div>
</template>
<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 value="1">用户属性</el-radio>
<el-radio value="2">事件属性</el-radio>
......@@ -112,12 +136,42 @@ watch(
<el-select v-model="form.rule" style="width: 170px" @change="handleRuleChange">
<el-option v-for="item in currentRuleList" :key="item.value" :label="item.label" :value="item.value"></el-option>
</el-select>
<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-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 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>
<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 class="rfm-body">
<template v-if="form.rule === '102'">
......@@ -126,9 +180,10 @@ watch(
<b>{{ item.level }}</b>
</div>
<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-select>
</el-select> -->
</div>
</div>
</template>
......@@ -150,6 +205,11 @@ watch(
</template>
<style lang="scss">
.rfm-top {
display: flex;
align-items: center;
justify-content: space-between;
}
.rfm-header {
display: flex;
align-items: center;
......@@ -186,4 +246,12 @@ watch(
text-align: center;
}
}
.rfm-tips {
margin-right: 10px;
}
.rfm-popover {
p {
margin: 10px 0;
}
}
</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) {
</section>
</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>
</template>
......
......@@ -100,7 +100,7 @@ function remoteMethod(item: RuleAttr, search: string = '') {
</div>
<div class="rule-list">
<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-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>
......@@ -116,8 +116,13 @@ function remoteMethod(item: RuleAttr, search: string = '') {
</el-select>
</el-form-item>
<el-form-item v-if="!['null', 'not null'].includes(item.operate)">
<!-- 数字区间 -->
<template v-if="['2', '3'].includes(item.attr_type) && item.operate === 'range'">
<el-input-number step-strictly :controls="false" :min="0" v-model="item.value.start" />
<el-input-number step-strictly :controls="false" :min="0" v-model="item.value.end" />
</template>
<!-- 日期区间 -->
<template v-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.end" type="date" value-format="YYYY-MM-DD" />
</template>
......@@ -158,7 +163,7 @@ function remoteMethod(item: RuleAttr, search: string = '') {
</el-row>
</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>
</template>
......
......@@ -24,6 +24,7 @@ interface MetaEventType {
export interface TagType {
id: 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 @@
import { getUserTags } from '../api'
const props = defineProps({ ssoId: String })
const color = ['#af1c40', '#c17933', '#8f0034', '#d45548', '#ab3259', '#dec34c', '#8b8920', '#a25a6d']
const list = ref([])
async function fetchList() {
if (!props.ssoId) return
const res = await getUserTags({ sso_id: props.ssoId, limit: 20 })
list.value = res.data.items.map(item => {
return { ...item, weight: parseFloat(item.weight) / 5 + 1 || 0 }
list.value = res.data.items.map((item, index) => {
return { ...item, weight: parseFloat(item.weight) / 2 + 1 || 0, color: color[index % color.length] }
})
}
......@@ -24,9 +25,9 @@ watch(
<div class="user-label">
<ul>
<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 }}
<span class="user-label__arrow"></span>
<!-- <span class="user-label__arrow"></span> -->
</div>
</li>
</ul>
......@@ -47,21 +48,22 @@ watch(
ul {
display: grid;
grid-template-columns: repeat(2, 1fr);
column-gap: 240px;
column-gap: 230px;
row-gap: 20px;
}
.user-label__inner {
position: relative;
display: inline-block;
padding: 4px 10px;
padding: 10px;
font-size: 14px;
text-align: left;
font-family: Roboto;
line-height: 24px;
color: rgba(16, 16, 16, 1);
// color: rgba(16, 16, 16, 1);
color: #fff;
background-color: rgba(231, 232, 232, 1);
border: 1px solid rgba(187, 187, 187, 1);
border-radius: 5px;
// border: 1px solid rgba(187, 187, 187, 1);
border-radius: 50%;
}
.user-label__arrow {
position: absolute;
......
......@@ -36,59 +36,64 @@ const platformList: PlatformItem[] = [
type: '2',
type_name: '钉钉',
config_attributes: [
{ label: '连接名称', prop: 'name', value: '' },
{ label: 'AgentId', prop: 'agentId', value: '' },
{ label: 'AppKey', prop: 'appKey', value: '' },
{ label: 'AppSecret', prop: 'appSecret', value: '' }
{ label: '连接名称', prop: 'name', value: '钉钉' },
{ label: 'AgentId', prop: 'agentId', value: '8441459810' },
{ label: 'AppKey', prop: 'appKey', value: 'dingigucs3beqlotpf24' },
{ label: 'AppSecret', prop: 'appSecret', value: '6dNRvuOzvX_xq5N9tFdjepdf3FeooN25yUZK6ammDbPUVq9sfdXD-sKUg' }
]
},
{
type: '3',
type_name: '小鹅通',
config_attributes: [
{ label: '连接名称', prop: 'name', value: '' },
{ label: 'app_id', prop: 'app_id', value: '' },
{ label: 'client_id', prop: 'client_id', value: '' },
{ label: 'secret_key', prop: 'secret_key', value: '' }
{ label: '连接名称', prop: 'name', value: '小鹅通' },
{ label: 'app_id', prop: 'app_id', value: 'appc4bolgenF58' },
{ label: 'client_id', prop: 'client_id', value: '_5e7f809dd6317_qSMuUoAi?type=2SDK' },
{ label: 'secret_key', prop: 'secret_key', value: 'xiao_5ac1dd24803ae_GtfAOxiS1pdf3FeooN2huhu92WRE52S-SkOh' }
]
},
{
type: '4',
type_name: '问卷星',
config_attributes: [
{ label: '连接名称', prop: 'name', value: '' },
{ label: 'AppKey', prop: 'appKey', value: '' },
{ label: 'AppSecret', prop: 'appSecret', value: '' }
{ label: '连接名称', prop: 'name', value: '问卷星' },
{ label: 'AppKey', prop: 'appKey', value: '82286f9c5114dc2bda214cd9567dodc' },
{ 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_name: '抖音',
config_attributes: [
{ label: '连接名称', prop: 'name', value: '' },
{ label: '应用类别', prop: 'dyInput1', value: '' },
{ label: '授权域回调', prop: 'dyInput2', value: '' },
{ label: '网站应用简介', prop: 'dyInput3', value: '' },
{ label: '应用官网', prop: 'dyInput4', value: '' },
{ label: '联系人姓名', prop: 'dyInput5', value: '' }
{ label: '连接名称', prop: 'name', value: '抖音' },
{ label: '应用类别', prop: 'dyInput1', value: '短视频分享与社交平台' },
{ label: '授权域回调', prop: 'dyInput2', value: 'https://douyin.xiaokefu.com.cn/douYin/push/19872884' },
{
label: '网站应用简介',
prop: 'dyInput3',
value:
'不仅是下载抖音应用程序的官方渠道,也是一个展示抖音最新动态、功能更新和推广活动的平台。用户可以通过官网了解抖音的特色功能、查看热门视频、参与互动活动,以及获取帮助和教程等。官网还为创作者和企业提供了一个展示空间,让他们了解如何利用抖音平台进行内容创作、品牌推广和电子商务等。'
},
{ label: '应用官网', prop: 'dyInput4', value: 'https://www.douyin.com' },
{ label: '联系人姓名', prop: 'dyInput5', value: '清控紫荆(北京)教育股份有限公司' }
]
},
{
type: '7',
type_name: '微博',
config_attributes: [
{ label: '连接名称', prop: 'name', value: '' },
{ label: 'AppKey', prop: 'appKey', value: '' },
{ label: 'AppSecret', prop: 'appSecret', value: '' }
{ label: '连接名称', prop: 'name', value: '微博' },
{ label: 'AppKey', prop: 'appKey', value: '1206405345' },
{ label: 'AppSecret', prop: 'appSecret', value: '6a6095e113cd28fde6e14c7b7145c5c5' }
]
},
{
type: '8',
type_name: '小红书',
config_attributes: [
{ label: '连接名称', prop: 'name', value: '' },
{ label: 'AppKey', prop: 'appKey', value: '' },
{ label: '连接名称', prop: 'name', value: '小红书' },
{ label: 'AppKey', prop: 'appKey', value: '6c1dd8dd64d074d56124c751f6bc240b' },
{ label: 'AppSecret', prop: 'appSecret', value: '' }
]
},
......@@ -96,40 +101,45 @@ const platformList: PlatformItem[] = [
type: '9',
type_name: '邮箱',
config_attributes: [
{ label: '连接名称', prop: 'name', value: '' },
{ label: 'client_id', prop: 'client_id', value: '' },
{ label: 'client_secret', prop: 'client_secret', value: '' },
{ label: 'token URL', prop: 'token', value: '' },
{ label: 'API URL', prop: 'apiUrl', value: '' }
{ label: '连接名称', prop: 'name', value: '邮箱' },
{ label: 'client_id', prop: 'client_id', value: 'swanzhong' },
{ label: 'client_secret', prop: 'client_secret', value: '563a8c6a89d2368194c1c7889c508b34' },
{ label: 'token URL', prop: 'token', value: 'openapi/user/get' },
{ label: 'API URL', prop: 'apiUrl', value: 'openapi/user/check' }
]
},
{
type: '10',
type_name: '短信',
config_attributes: [
{ label: '连接名称', prop: 'name', value: '' },
{ label: 'client_id', prop: 'client_id', value: '' },
{ label: 'SdkAppId', prop: 'sdkAppId', value: '' },
{ label: 'token URL', prop: 'token', value: '' },
{ label: 'API URL', prop: 'apiUrl', value: '' }
{ label: '连接名称', prop: 'name', value: '短信' },
{ label: 'client_id', prop: 'client_id', value: 'FbFgN2of-mlc' },
{ label: 'SdkAppId', prop: 'sdkAppId', value: 'CV3X1%2FJG7mdNZm03l9puvwPAktmfw1aj8XvBb6sm696MqoW57' },
{ label: 'token URL', prop: 'token', value: 'https://oauth-login.cloud.ali.com/oauth2/v3/token' },
{ 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_name: '自定义',
config_attributes: [
{ label: '连接名称', prop: 'name', value: '' },
{ label: 'APP类型', prop: 'appType', value: '' },
{ label: 'AppId', prop: 'appId', value: '' }
{ label: '连接名称', prop: 'name', value: '自定义' },
{ label: 'APP类型', prop: 'appType', 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_name: '小程序',
icon: '100',
config_attributes: [{ label: '连接名称', prop: 'name', value: '' }],
config_attributes: [{ label: '连接名称', prop: 'name', value: '小程序' }],
async onBeforeNext(stepActive) {
if (stepActive === 2) {
const res = await handleSubmit()
......@@ -139,6 +149,12 @@ const platformList: PlatformItem[] = [
}
return true
}
},
{
icon: 'mall',
type: '15',
type_name: '紫荆商城',
config_attributes: [{ label: '连接名称', prop: 'name', value: '紫荆商城' }]
}
]
......@@ -215,7 +231,8 @@ async function handleSave() {
:title="props.data?.id ? '编辑连接' : '新建连接'"
:close-on-click-modal="false"
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-tab-pane disabled lazy label="选择连接类型" :name="1" v-if="!props.data?.id">
......
<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 Icon from '@/components/ConnectionIcon.vue'
import { deleteConnection } from '../api'
......@@ -40,7 +40,7 @@ function handleRemove() {
})
}
// 去查看
const routerView = function () {
const handleView = function () {
router.push({ path: '/connect/view', query: { id: props.data.id } })
}
......@@ -70,7 +70,7 @@ const handleStudentFollow = function () {
</script>
<template>
<div class="connect-item" @click="routerView">
<div class="connect-item">
<div class="connect-item_top">
<!-- <div class="connect-item__edit">
<img @click="edit" src="https://webapp-pub.ezijing.com/pages/assa/dml_edit.png" />
......@@ -81,7 +81,7 @@ const handleStudentFollow = function () {
<el-icon size="20" color="#333"><Delete /></el-icon>
</div> -->
<div class="connect-item__icon">
<Icon w="40" h="40" :multiColor="true" class="svg" :name="iconMap[data.type] || data.type"></Icon>
<Icon w="40" h="40" :multiColor="true" class="svg" :name="data.type === '15' ? 'mall' : iconMap[data.type] || data.type"></Icon>
</div>
</div>
<div class="connect-item_bottom">
......@@ -92,6 +92,10 @@ const handleStudentFollow = function () {
</template>
<template #default>
<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">
<el-icon size="16" color="#000"><UserFilled /></el-icon>
<span>用户触达</span>
......
......@@ -27,7 +27,7 @@ let ruleForm = $ref<any>({
name: 1,
name_value: '',
status: 1,
gender: 1,
gender: 100,
mobile: 1,
create_data: '',
type_name: ''
......@@ -60,14 +60,18 @@ const submitForm = async (formEl: FormInstance | undefined, bl: string) => {
}, {})
)
}
submitScheduleMember(ruleForm).then(res => {
if (res.data) {
submitScheduleMember(ruleForm).then((res: any) => {
if (res.code === 0) {
ElMessage({
message: '保存成功',
type: 'success'
})
}
emit('update:modelValue', false)
} else {
ElMessage({
message: res.message
})
}
})
} else {
console.log('error submit!', fields)
......@@ -76,6 +80,13 @@ const submitForm = async (formEl: FormInstance | undefined, bl: string) => {
}
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>
<template>
......@@ -84,9 +95,18 @@ const rules = [{ required: true }]
class="data-form"
title="自动生成用户数据"
:close-on-click-modal="false"
@update:modelValue="value => $emit('update:modelValue', value)">
@update:modelValue="value => $emit('update:modelValue', value)"
>
<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-radio-group v-model="ruleForm.size">
<el-radio :value="1000">1000</el-radio>
......@@ -94,6 +114,7 @@ const rules = [{ required: true }]
<el-radio :value="5000">5000</el-radio>
<el-radio :value="10000">10000</el-radio>
</el-radio-group>
<span style="color: #ccc;font-size: 12px;line-height: 100%;">注意:为了保障系统性能,您最多只能导入10000条数据</span>
</el-form-item>
<el-form-item label="请选择数据覆盖形式:" :rules="rules">
<el-radio-group v-model="ruleForm.cover_type">
......@@ -121,11 +142,17 @@ const rules = [{ required: true }]
</el-radio-group>
</el-form-item>
<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="2"></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 label="手机号吗" :rules="rules">
<el-radio-group v-model="ruleForm.mobile">
......
......@@ -5,9 +5,12 @@ import AppList from '@/components/base/AppList.vue'
import ListItem from '../components/ListItem.vue'
import { getConnectionList, getConnectionDetails, getScheduleMember, getStudentFollow } from '../api'
import { useMapStore } from '@/stores/map'
import { useUserStore } from '@/stores/user'
const store = useMapStore()
const userStore = useUserStore()
const FormDialog = defineAsyncComponent(() => import('../components/FormDialog.vue'))
const UserDataDialog = defineAsyncComponent(() => import('../components/UserDataDialog.vue'))
const EventDataDialog = defineAsyncComponent(() => import('../components/EventDataDialog.vue'))
......@@ -131,7 +134,7 @@ const handleStudentFollow = function (experimentId: string, id: string, type: st
@viewDataProgress="viewDataProgress"
@handleStudentFollow="handleStudentFollow"
></ListItem>
<div class="connect-item" @click="createConnect">
<div class="connect-item" @click="createConnect" v-if="userStore.role?.id !== 1">
<div class="connect-add-button">
<el-icon><Plus /></el-icon>
<span>新建连接</span>
......@@ -176,4 +179,7 @@ const handleStudentFollow = function (experimentId: string, id: string, type: st
margin-right: 10px;
}
}
.connect-item{
min-height: 166px;
}
</style>
......@@ -41,7 +41,10 @@ const platformList = [
{
title: '重新获取公众号信息',
async onClick() {
await asyncOfficialAccountInfo({ connection_id: connectId.value, appid: getAttributeValueByProp('appid') })
await asyncOfficialAccountInfo({
connection_id: connectId.value,
appid: getAttributeValueByProp('appid')
})
ElMessage.success('重新获取公众号信息成功')
}
},
......@@ -49,8 +52,14 @@ const platformList = [
title: '重新获取公众号粉丝',
async onClick() {
const nikeName = getAttributeValueByProp('nikeName')
await ElMessageBox.confirm(`同步微信公众号粉丝能够将微信粉丝的最新信息同步到本系统中,您确定现在要开始同步公众号“${nikeName}”的粉丝吗?`, '同步微信公众号粉丝')
await asyncOfficialAccountUsers({ connection_id: connectId.value, appid: getAttributeValueByProp('appid') })
await ElMessageBox.confirm(
`同步微信公众号粉丝能够将微信粉丝的最新信息同步到本系统中,您确定现在要开始同步公众号“${nikeName}”的粉丝吗?`,
'同步微信公众号粉丝'
)
await asyncOfficialAccountUsers({
connection_id: connectId.value,
appid: getAttributeValueByProp('appid')
})
ElMessage.success(`已经开始同步公众号“${nikeName}”的粉丝,完成时间取决于您公众号的粉丝数量,请耐心等待。`)
}
}
......@@ -505,6 +514,25 @@ const platformList = [
type: 14,
type_name: '小程序',
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)
<AppCard title="查看链接">
<div class="view-info" v-if="detail">
<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 class="view-info_content">
<p>连接名称:{{ getAttributeValueByProp('name') || detail.type_name }}</p>
......@@ -544,7 +577,9 @@ const surveyKingDialogVisible = ref<boolean>(false)
<el-tabs class="tabs-box" v-for="(item, index) in platformDataList" :key="index">
<el-tab-pane :label="item.title">
<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 class="tag-null" v-else>无数据</div>
</el-tab-pane>
......@@ -554,7 +589,8 @@ const surveyKingDialogVisible = ref<boolean>(false)
v-model="surveyKingDialogVisible"
:account="getAttributeValueByProp('account')"
:password="getAttributeValueByProp('password')"
v-if="detail?.type === '13'"></SurveyKingDialog>
v-if="detail?.type === '13'"
></SurveyKingDialog>
</template>
<style lang="scss">
......
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) {
......@@ -21,6 +29,16 @@ export function createDynamicGroup(data: DynamicGroupCreateRequest) {
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) {
return httpRequest.post('/api/lab/v1/experiment/group/bda-update-static-group', data)
......
......@@ -56,7 +56,7 @@ function handleAdd() {
</script>
<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-row>
<el-col :span="8">
......
......@@ -2,13 +2,14 @@
import type { Group } from '../types'
import type { FormInstance, FormRules } from 'element-plus'
import { ElMessage } from 'element-plus'
import { updateStatusRuleList, dateUnitList, weekList } from '@/utils/dictionary'
import { createStaticGroup, updateStaticGroup, createDynamicGroup, updateDynamicGroup, getGroupInfo } from '../api'
import { getNameByValue, updateStatusRuleList, dateUnitList, weekList, groupTypeList } from '@/utils/dictionary'
import { createStaticGroup, updateStaticGroup, createDynamicGroup, updateDynamicGroup, getGroupInfo, createRFMGroup, updateRFMGroup } from '../api'
import UserRule from '@/components/rule/UserRule.vue'
import EventRule from '@/components/rule/EventRule.vue'
import LabelRule from '@/components/rule/LabelRule.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 {
data: Partial<Group>
......@@ -20,16 +21,13 @@ const emit = defineEmits<{
(e: 'update:modelValue', visible: boolean): void
}>()
const isUpdate = $computed(() => !!props.data?.id)
const title = $computed(() => {
if (isUpdate) {
return props.data.type === '1' ? '修改静态群组' : '修改动态群组'
} else {
return props.data.type === '1' ? '新建静态群组' : '新建动态群组'
}
const isUpdate = computed(() => !!props.data?.id)
const title = computed(() => {
const typeName = getNameByValue(props.data.type as string, groupTypeList)
return isUpdate.value ? `修改${typeName}` : `新建${typeName}`
})
const formRef = $ref<FormInstance>()
const formRef = ref<FormInstance>()
const form: any = reactive({
id: '',
name: '',
......@@ -40,30 +38,23 @@ const form: any = reactive({
user_attr_rule: { current_logic_operate: 'and', items: [] },
event_attr_rule: { current_logic_operate: 'and', items: [] },
tag_rule: { current_logic_operate: 'and', items: [] },
user_action_rule: { current_logic_operate: 'and', items: [] }
})
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 })
}
user_action_rule: { current_logic_operate: 'and', items: [] },
rules: { R: {}, F: {}, M: {} }
})
function genRuleData(data: any) {
if (Array.isArray(data)) data = data[0]
return merge({ current_logic_operate: 'and', items: [] }, data)
}
function fetchInfo() {
if (!props.data.id) return
getGroupInfo({ id: props.data.id }).then(res => {
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 [tag_rule = { current_logic_operate: 'and', items: [] }] = detail.tag_rule.map((item: any) => {
return { ...item, items: item.items.map((item: any) => item.id) }
})
const user_attr_rule = genRuleData(detail.user_attr_rule)
const event_attr_rule = genRuleData(detail.event_attr_rule)
const user_action_rule = genRuleData(detail.user_action_rule)
const tag_rule = genRuleData(detail.tag_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
......@@ -73,11 +64,17 @@ function fetchInfo() {
return item
})
Object.assign(form, {
user_attr_rule: { ...user_attr_rule, attrRuleItems },
event_attr_rule: { ...event_attr_rule, eventRuleItems },
tag_rule,
user_action_rule: detail.user_action_rule
const tagRuleItems = tag_rule.items.map((item: any, index: number) => {
const rfm = tag_rule.rfm_tag_map?.[index] || {}
return { tag_id: item.id, ...rfm }
})
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() {
watchEffect(() => fetchInfo())
const rules = ref<FormRules>({
name: [{ required: true, message: '请输入群组名称' }],
url: [{ required: true, message: '请选择标签类型图标' }]
name: [{ required: true, message: '请输入群组名称' }]
})
// 提交
function handleSubmit() {
formRef?.validate().then(() => (isUpdate ? handleUpdate() : handleCreate()))
formRef.value?.validate().then(() => (isUpdate.value ? handleUpdate() : handleCreate()))
}
// 新建
async function handleCreate() {
......@@ -99,7 +95,17 @@ async function handleCreate() {
// 静态群组
const params = pick(form, ['name', 'status'])
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(
{
......@@ -107,11 +113,23 @@ async function handleCreate() {
update_rule: JSON.stringify(form.update_rule),
user_attr_rule: JSON.stringify([form.user_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)
} 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' })
emit('update')
......@@ -123,7 +141,8 @@ async function handleUpdate() {
// 静态群组
const params = pick(form, ['id', 'name', 'status'])
await updateStaticGroup(params)
} else {
} else if (props.data.type === '2') {
// 动态群组
const attrRuleItems = form.user_attr_rule.items.map((item: any) => {
item.value = Array.isArray(item.value) ? item.value.join(',') : item.value
return item
......@@ -132,6 +151,16 @@ async function handleUpdate() {
item.value = Array.isArray(item.value) ? item.value.join(',') : item.value
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(
{
......@@ -139,12 +168,23 @@ async function handleUpdate() {
update_rule: JSON.stringify(form.update_rule),
user_attr_rule: JSON.stringify([{ ...form.user_attr_rule, items: attrRuleItems }]),
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)
},
['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)
} 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' })
emit('update')
......@@ -153,12 +193,12 @@ async function handleUpdate() {
</script>
<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-item label="群组名称" prop="name">
<el-input v-model="form.name" placeholder="请输入" />
</el-form-item>
<template v-if="data.type === '2'">
<template v-if="data.type !== '1'">
<el-form-item label="更新频率" prop="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'">
......@@ -202,6 +242,9 @@ async function handleUpdate() {
<LabelRule v-model="form.tag_rule" style="margin-top: 20px"></LabelRule>
<UserActionRule v-model="form.user_action_rule" style="margin-top: 20px"></UserActionRule>
</template>
<template v-if="data.type === '3'">
<RFMRule v-model="form.rules"></RFMRule>
</template>
</el-form>
<template #footer>
<el-row justify="center">
......
......@@ -87,7 +87,8 @@ const listOptions = computed(() => {
}
},
{ label: '更新人', prop: 'updated_operator.real_name' },
{ label: '更新时间', prop: 'updated_time' }
{ label: '更新时间', prop: 'updated_time' },
{ label: '操作', slots: 'table-x' }
]
}
})
......@@ -99,7 +100,7 @@ function handleRefresh() {
</script>
<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-row>
<el-col :span="12">
......@@ -157,7 +158,13 @@ function handleRefresh() {
</el-card>
<el-card style="margin-top: 20px">
<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>
<template #footer>
......
......@@ -3,9 +3,8 @@ import type { Operator } from '@/types'
export interface Group {
id: string
name: string
type_id: string
status: string
type: '1' | '2'
type: '1' | '2' | '3'
update_status: string // '1' | '2'
update_rule: string
created_time: string
......@@ -16,6 +15,7 @@ export interface Group {
event_attr_rule?: string
tag_rule?: string
record?: any
rules?: string
}
export type GroupListRequest = Pick<Group, 'id' | 'name'> & { experiment_id?: string }
......@@ -35,6 +35,11 @@ export type DynamicGroupUpdateRequest = Pick<
}
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 {
id: string
name: string
......
......@@ -7,6 +7,9 @@ import { getNameByValue, groupTypeList, updateStatusRuleList } from '@/utils/dic
import { getGroupList, deleteGroup } from '../api'
import { useMapStore } from '@/stores/map'
import SelectUser from '@/components/SelectUser.vue'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const FormDialog = defineAsyncComponent(() => import('../components/FormDialog.vue'))
const ViewDialog = defineAsyncComponent(() => import('../components/ViewDialog.vue'))
......@@ -61,7 +64,13 @@ const listOptions = computed(() => {
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: '操作', slots: 'table-x', width: 240 }
]
......@@ -82,7 +91,7 @@ let formVisible = $ref(false)
let currentRow = $ref<Partial<Group>>()
// 新建
function handleAdd(type: '1' | '2') {
function handleAdd(type: '1' | '2' | '3') {
currentRow = { type }
formVisible = true
}
......@@ -114,12 +123,14 @@ function handleView(row: Group) {
<AppList v-bind="listOptions" ref="appList" @selection-change="handleSelectionChange">
<template #header-buttons>
<el-space>
<el-dropdown v-permission="['experiment_group_create_dynamic_group', 'experiment_group_create_static_group']">
<el-button type="primary" :icon="Plus">新建</el-button>
<!-- v-permission="['experiment_group_create_dynamic_group', 'experiment_group_create_static_group']" -->
<el-dropdown>
<el-button type="primary" :icon="Plus" v-if="!userStore.status.group_status">新建</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="handleAdd('2')">新建动态群组</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>
</template>
</el-dropdown>
......@@ -159,7 +170,8 @@ function handleView(row: Group) {
v-model="formVisible"
:data="currentRow"
@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>
</template>
......@@ -3,7 +3,7 @@ import type { LabelTypeListRequest, LabelTypeCreateRequest, LabelTypeUpdateReque
// 获取标签类型列表
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'
import EventPreferenceRule from '@/components/rule/EventPreferenceRule.vue'
import EventTargetRule from '@/components/rule/EventTargetRule.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<{
data: Label
......@@ -22,7 +25,7 @@ const emit = defineEmits<{
const userStore = useUserStore()
const disabled = computed(() => userStore.status)
const disabled = computed(() => userStore.status.tag_status)
const statusList = useMapStore().getMapValuesByKey('system_status')
......@@ -36,16 +39,7 @@ const form = reactive({
function fetchInfo() {
getLabelRule({ id: props.data.id }).then(res => {
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
if (detail.rules.length === 0) {
if (detail.label == '2') {
......@@ -71,6 +65,19 @@ function fetchInfo() {
if (detail.label === '4') {
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 })
})
......@@ -132,16 +139,20 @@ function handleUpdate() {
</el-row>
</el-form>
<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>
<!-- 事件偏好 -->
<EventPreferenceRule v-model="form.rules" v-if="data.label == '2'"></EventPreferenceRule>
<!-- 事件指标 -->
<EventTargetRule v-model="form.rules" v-if="data.label == '3'"></EventTargetRule>
<!-- RFM模型 -->
<RFMRule v-model="form.rules" v-if="data.label == '4'"></RFMRule>
<!-- 用户属性规则 -->
<!-- <UserRule></UserRule> -->
<RFMRule v-model="form.rules" :tagId="form.id" v-if="data.label == '4'"></RFMRule>
<!-- 自定义 -->
<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> -->
</el-form>
......
......@@ -4,6 +4,9 @@ import { Edit, Delete } from '@element-plus/icons-vue'
import { ElMessageBox, ElMessage } from 'element-plus'
import { deleteLabelType } from '../api'
import { useLabelType } from '../composables/useLabelType'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
defineProps<{ activeId: string }>()
......@@ -40,33 +43,58 @@ function handleRemove(row: LabelType) {
</script>
<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', '')">
<h4>全部标签</h4>
<p>{{ labelCount }}</p>
</div>
<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
xmlns="http://www.w3.org/2000/svg"
:fill="item.url"
width="16.67333984375"
height="16.669540405273438"
viewBox="0 0 16.67333984375 16.669540405273438">
viewBox="0 0 16.67333984375 16.669540405273438"
>
<g>
<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>
</svg>
<p>{{ item.name }}</p>
<div class="label-type-actions">
<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 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
class="label-type-item__remove"
@click.stop="handleRemove(item)"
v-permission="'experiment_tag_type_delete'"
><Delete
/></el-icon>
</div>
</li>
</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>
<style lang="scss">
......
......@@ -65,14 +65,15 @@ const listOptions = computed(() => {
}
},
{ label: '更新人', prop: 'updated_operator.real_name' },
{ label: '更新时间', prop: 'updated_time' }
{ label: '更新时间', prop: 'updated_time' },
{ label: '操作', slots: 'table-x' }
]
}
})
</script>
<template>
<el-dialog title="查看标签信息" width="800px">
<el-dialog title="查看标签信息" width="1000px">
<el-form label-suffix=":" label-width="82px">
<el-row>
<el-col :span="12">
......@@ -119,7 +120,13 @@ const listOptions = computed(() => {
</el-card>
<el-card style="margin-top: 20px">
<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>
<template #footer>
<el-row justify="center">
......
......@@ -7,7 +7,11 @@ export function useLabelType() {
function fetchTypeList() {
getLabelTypeList().then(res => {
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(() => {
......
......@@ -5,6 +5,7 @@ export interface LabelType {
id: string
name: string
url: string
is_default?: boolean
}
export type LabelTypeListRequest = Pick<LabelType, 'id' | 'name'> & { experiment_id?: string }
......
......@@ -9,6 +9,9 @@ import { useMapStore } from '@/stores/map'
import { getNameByValue, updateStatusRuleList, labelList } from '@/utils/dictionary'
import { useLabelType } from '../composables/useLabelType'
import SelectUser from '@/components/SelectUser.vue'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const LabelFormDialog = defineAsyncComponent(() => import('../components/LabelFormDialog.vue'))
const LabelViewDialog = defineAsyncComponent(() => import('../components/LabelViewDialog.vue'))
......@@ -82,7 +85,13 @@ const listOptions = computed(() => {
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: '操作', slots: 'table-x', width: 320 }
]
......@@ -135,14 +144,15 @@ function handleSelect(id: string) {
})
}
</script>
<!-- import { useUserStore } from '@/stores/user'
const userStore = useUserStore() -->
<template>
<AppCard>
<div class="label-wrap">
<div class="label-left"><LabelType :active-id="listParams.type_id" @select="handleSelect"></LabelType></div>
<AppList v-bind="listOptions" ref="appList" class="label-right">
<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 #filter-user>
<SelectUser v-model="listParams.updated_operator" placeholder="更新人" @change="handleRefresh"></SelectUser>
......@@ -151,14 +161,23 @@ function handleSelect(id: string) {
<template #table-x="{ row }">
<el-button type="primary" plain @click="handleRule(row)">规则</el-button>
<el-button type="primary" plain @click="handleView(row)">查看</el-button>
<el-button type="primary" plain @click="handleUpdate(row)" v-permission="'experiment_tag_update'">编辑</el-button>
<el-button type="primary" plain @click="handleRemove(row)" v-permission="'experiment_tag_delete'">删除</el-button>
<el-button type="primary" plain @click="handleUpdate(row)" v-permission="'experiment_tag_update'"
>编辑</el-button
>
<el-button type="primary" plain @click="handleRemove(row)" v-permission="'experiment_tag_delete'"
>删除</el-button
>
</template>
</AppList>
</div>
</AppCard>
<!-- 新建/修改标签 -->
<LabelFormDialog v-model="formVisible" :data="currentRow" @update="handleRefresh" v-if="formVisible"></LabelFormDialog>
<LabelFormDialog
v-model="formVisible"
:data="currentRow"
@update="handleRefresh"
v-if="formVisible"
></LabelFormDialog>
<!-- 查看标签 -->
<LabelViewDialog v-model="viewVisible" :data="currentRow" v-if="viewVisible && currentRow"></LabelViewDialog>
<!-- 规则 -->
......@@ -180,6 +199,7 @@ function handleSelect(id: string) {
}
.label-right {
flex: 1;
overflow: hidden;
overflow-x: hidden;
overflow-y: auto;
}
</style>
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)
}
......@@ -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 }) {
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'
import { useUserStore } from '@/stores/user'
import { useConnection } from '../composables/useConnection'
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 IconUser from './IconUser.vue'
import IconAI from './IconAI.vue'
import xss from 'xss'
import { uploadFileByUrl } from '@/utils/upload'
const emit = defineEmits(['submit'])
const form = defineModel()
......@@ -18,25 +28,39 @@ const { industryList } = useIndustry()
const { connectionList } = useConnection()
const welcomeMessage = computed(() => {
const way = getNameByValue(form.value.way, materialMethodList)
const type = getNameByValue(form.value.type, materialType)
const industry = industryList.value.find(item => item.id == form.value.industry_id)?.name
const personnel = getNameByValue(form.value.personnel_type, materialUsersList)
const scenario = getNameByValue(form.value.scenario_type, materialUsageList)
const connection = connectionList.value.find(item => item.id == form.value.channel)?.type_name
const key = form.value.key_points
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 data = form.value
const way = getNameByValue(data.way, materialMethodList)
const type = getNameByValue(data.type, materialType)
const industry = industryList.value.find(item => item.id == data.industry_id)?.name
const personnel = getNameByValue(data.personnel_type, materialUsersList)
const scenario = getNameByValue(data.scenario_type, materialUsageList)
const connection = connectionList.value.find(item => item.id == data.channel)?.type_name
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 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(() => {
messages.value.push({ role: 'system', content: welcomeMessage.value })
if (form.value.content) {
messages.value.push({ role: 'bot', content: form.value.content })
}
// if (form.value.content) {
// messages.value.push({ role: 'bot', content: form.value.content })
// }
})
watch(welcomeMessage, () => {
......@@ -53,9 +77,8 @@ watch(welcomeMessage, () => {
async function postMessage() {
if (!content.value) return
console.log(content.value)
messages.value.push({ role: 'user', content: content.value })
post({ context: content.value, type: '1' })
post({ content: content.value, type: '1' })
content.value = ''
}
......@@ -65,26 +88,31 @@ async function handleSend(event) {
await postMessage()
}
async function handleSendType(type, context) {
async function handleSendType(type, content) {
const userName = useUserStore().user.name
context = xss(context, {
content = xss(content, {
whiteList: {}, // 白名单为空,表示过滤所有标签
stripIgnoreTag: true, // 过滤所有非白名单标签的HTML
stripIgnoreTagBody: ['script'] // script标签较特殊,需要过滤标签中间的内容
})
switch (type) {
case 2:
context = `我是${userName},请帮我创作一个文本内容,${context.replace('你将以在线AI 的方式创作一个文本内容,', '')}`
content = `${content}`
break
case 3:
context = `我是${userName},请帮我润色一个文本内容,${context.replace('你将以在线AI 的方式创作一个文本内容,', '')}`
content = `我是${userName},请帮我改写以下内容:${content.replace('请帮我创作一个', '')}`
break
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
}
post({ type, context })
post({ type, content })
}
const chatRef = ref()
......@@ -109,6 +137,12 @@ function handleCopy(content) {
function parseHtml(content) {
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>
<template>
......@@ -120,21 +154,54 @@ function parseHtml(content) {
<IconAI v-else />
</div>
<div class="chat-message-main">
<div class="chat-message-content" v-html="parseHtml(item.content)"></div>
<div class="chat-message-extra" v-if="item.role !== 'user'">
<div class="chat-message-content">
<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="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
>
<el-button size="small" type="primary" @click="handleSendType(2, item.content)"
>AI创作({{ usages.ai_creation_count }}/{{ usages.ai_creation_max_count }})</el-button
>
<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)"
>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>
......@@ -146,7 +213,13 @@ function parseHtml(content) {
</div>
</div>
<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>
</div>
</template>
......@@ -195,14 +268,23 @@ function parseHtml(content) {
.chat-message-content {
max-width: 100%;
word-break: break-word;
* {
max-width: 100%;
}
}
.user .chat-message-main {
color: #fff;
background-color: var(--main-color);
}
.chat-message-extra {
.chat-message-extra:not(:empty) {
margin-top: 20px;
padding-top: 12px;
border-top: 1px solid #edeff1;
}
.chat-message-extra button {
margin-bottom: 10px;
}
.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({
name: [{ required: true, message: '请输入内容名称' }]
})
watch(
() => form.value.type,
() => {
form.value.way = '2'
}
)
async function handleValidate() {
await formRef.value.validate()
}
......@@ -32,6 +25,16 @@ async function handleNext() {
await handleValidate()
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>
<template>
......@@ -39,8 +42,8 @@ async function handleNext() {
<template #header>基础信息</template>
<el-form label-suffix=":" label-width="130" :model="form" :rules="rules" ref="formRef" :disabled="action === 'view'">
<el-form-item label="营销内容类型" prop="type">
<el-radio-group v-model="form.type" :disabled="action === 'update'">
<el-radio v-for="item in materialType" :key="item.id" :value="item.value">{{ item.label }}</el-radio>
<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" :disabled="$route.query.type ? item.value !== $route.query.type : false">{{ item.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="内容名称" prop="name">
......@@ -48,7 +51,7 @@ async function handleNext() {
</el-form-item>
<el-form-item label="创作方式" prop="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
}}</el-radio>
</el-radio-group>
......
......@@ -4,6 +4,8 @@ import { getNameByValue, materialMethodList } from '@/utils/dictionary'
import AppUpload from '@/components/base/AppUpload.vue'
import AIChat from './AIChat.vue'
const ImageDesign = defineAsyncComponent(() => import('./ImageDesign.vue'))
defineProps(['action'])
const emit = defineEmits(['prev', 'submit'])
......@@ -15,7 +17,7 @@ const form = defineModel()
const formRef = ref()
const rules = ref({
content: [{ required: true, message: '请输入内容名称' }]
content: [{ required: true, message: '请输入' }]
})
const typeName = computed(() => {
......@@ -39,7 +41,7 @@ async function handleSubmit() {
<template>
<div class="three">
<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 shadow="never">
<template #header>内容创作</template>
......@@ -49,16 +51,21 @@ async function handleSubmit() {
<el-form-item label="创作方式">{{ getNameByValue(form.way, materialMethodList) }}</el-form-item>
</el-form>
<el-divider></el-divider>
<el-form label-suffix=":" label-width="110" :model="form" :rules="rules" ref="formRef" :disabled="action === 'view'">
<el-form-item :label="`${typeName}资源`" prop="content">
<el-form :model="form" :rules="rules" ref="formRef" :disabled="action === 'view'">
<el-form-item prop="content">
<template v-if="form.type == 1">
<!-- 文本 -->
<el-input type="textarea" rows="14" v-model="form.content"></el-input>
</template>
<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>
</template>
</template>
<template v-if="form.type == 3">
<!-- 语音 -->
<div>
......
......@@ -2,7 +2,14 @@
import { useMapStore } from '@/stores/map'
import { useConnection } from '../composables/useConnection'
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'])
......@@ -19,7 +26,13 @@ const rules = ref({
industry_id: [{ required: true, message: '请选择所属行业' }],
scenario_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() {
......@@ -45,7 +58,39 @@ async function handleNext() {
<el-form-item label="创作方式">{{ getNameByValue(form.way, materialMethodList) }}</el-form-item>
</el-form>
<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-select v-model="form.industry_id">
<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() {
<el-radio v-for="item in connectionList" :key="item.id" :value="item.id">{{ item.type_name }}</el-radio>
</el-radio-group>
</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-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>
</template>
</el-form>
<el-row justify="center">
<el-button type="primary" @click="handlePrev">上一步</el-button>
......
......@@ -2,7 +2,7 @@
import { useMapStore } from '@/stores/map'
import { useConnection } from '../composables/useConnection'
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'
const props = defineProps(['data'])
......@@ -25,6 +25,24 @@ const typeName = computed(() => {
</el-form>
<el-divider></el-divider>
<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-select :model-value="data.industry_id">
<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(() => {
<el-form-item label="关键点" prop="key_points">
<el-input type="textarea" :rows="4" :model-value="data.key_points" placeholder="请输入内容的核心内容或者关键点,多个请使用英文“,”号进行隔离。" />
</el-form-item>
</template>
<el-form-item :label="`${typeName}资源`" prop="content">
<template v-if="data.type == 1">
<!-- 文本 -->
......
import { fetchEventSource } from '@fortaine/fetch-event-source'
import { getAIUsage } from '../api'
import { getAIUsage, postGenerateImage } from '../api'
import type { Message } from '../types'
import { ElMessage } from 'element-plus'
......@@ -32,13 +32,88 @@ export function useChat(options: 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
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',
headers: {
'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) {
if (response.ok) {
return
......@@ -47,21 +122,35 @@ export function useChat(options: any) {
}
},
onmessage(res) {
// console.log(res.data)
const message = JSON.parse(res.data)
// 聊天返回内容
if (data.type === '1') {
if (message.code === 0) {
ElMessage.error(message.message)
return
}
chatId.value = message.chatId + ''
const conversationId = message.conversationId
const conversationId = message.conversation_id
const messageIndex = messages.value.findIndex(session => session.conversationId === conversationId)
const content = message.content || ''
// if (message.content === '\n') content = '<br/>'
const content = message?.arguments?.reduce((a: any, b: any) => {
a = b?.messages[0]?.text || ''
return a
}, '')
if (messageIndex === -1) {
messages.value.push({ conversationId, role: 'bot', content, input: data.context })
} 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
},
......@@ -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'
import { getMaterialList, updateMaterial, deleteMaterial } from '../api'
import { getNameByValue, materialMethodList, materialUsageList, materialUsersList } from '@/utils/dictionary'
import { useMapStore } from '@/stores/map'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const ViewDialog = defineAsyncComponent(() => import('../components/ViewDialog.vue'))
......@@ -75,22 +78,22 @@ const listOptions = computed(() => {
return getNameByValue(row.way, materialMethodList)
}
},
{ label: '行业', prop: 'industry_name' },
{
label: '使用场景',
prop: 'scenario_type',
computed: ({ row }: { row: MaterialProp }) => {
return getNameByValue(row.scenario_type, materialUsageList)
}
},
{
label: '使用人员',
prop: 'personnel_type',
computed: ({ row }: { row: MaterialProp }) => {
return getNameByValue(row.personnel_type, materialUsersList)
}
},
{ label: '投放渠道', prop: 'channel_detail.type_name' },
// { label: '行业', prop: 'industry_name' },
// {
// label: '使用场景',
// prop: 'scenario_type',
// computed: ({ row }: { row: MaterialProp }) => {
// return getNameByValue(row.scenario_type, materialUsageList)
// }
// },
// {
// label: '使用人员',
// prop: 'personnel_type',
// computed: ({ row }: { row: MaterialProp }) => {
// return getNameByValue(row.personnel_type, materialUsersList)
// }
// },
// { label: '投放渠道', prop: 'channel_detail.type_name' },
{
label: '状态',
prop: 'status_name',
......@@ -100,12 +103,11 @@ const listOptions = computed(() => {
{
label: '更新人',
prop: 'updated_operator_name',
width: 100,
computed: ({ row }: { row: MaterialProp }) => {
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 }
]
}
......@@ -150,19 +152,24 @@ async function handleChangeStatus(row: MaterialProp) {
<AppCard>
<AppList border v-bind="listOptions" ref="appList">
<template #header-buttons>
<el-space>
<router-link to="/material/create">
<el-button type="primary" :icon="Plus" v-permission="'v1-experiment-marketing-material-create'">营销内容创作</el-button>
<el-space v-if="!userStore.status.material_status">
<router-link :to="{ path: '/material/create', query: $route.query }">
<el-button type="primary" :icon="Plus">营销内容创作</el-button>
</router-link>
</el-space>
</template>
<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 #table-x="{ row }">
<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="handleRemove(row)" v-permission="'v1-experiment-marketing-material-delete'">删除</el-button>
<el-button type="primary" plain @click="handleEdit(row)">编辑</el-button>
<el-button type="primary" plain @click="handleRemove(row)">删除</el-button>
</template>
</AppList>
<ViewDialog :data="currentRow" v-if="dialogVisible" v-model="dialogVisible"></ViewDialog>
......
<script setup lang="ts">
import { ElMessage } from 'element-plus'
import { createMaterial, updateMaterial, getMaterial } from '../api'
import { pickBy } from 'lodash-es'
const props = defineProps<{ action: string }>()
......@@ -14,15 +15,16 @@ const activeName = ref(1)
const form: any = reactive({
id: '',
type: '1',
type: route.query.type || '1',
name: '',
way: '2',
industry_id: '1',
industry_id: '',
scenario_type: '1',
personnel_type: '1',
channel: '',
key_points: '',
content: ''
content: '',
extend_info: {}
})
const detail = ref()
......@@ -48,7 +50,8 @@ function handleNext() {
// 下一步提交
async function handleNextAndSubmit() {
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
}
handleNext()
......@@ -59,14 +62,16 @@ async function handleSubmit() {
}
// 创建
async function handleCreate() {
await createMaterial(form)
const params = pickBy(form, item => item !== '' && item != '0')
await createMaterial(params)
ElMessage.success('创建成功')
router.replace('/material')
}
// 修改
async function handleUpdate() {
await updateMaterial(form)
const params = pickBy(form, item => item !== '' && item != '0')
await updateMaterial(params)
ElMessage.success('修改成功')
router.replace('/material')
}
......@@ -78,10 +83,10 @@ async function handleUpdate() {
<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>
</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>
</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>
</el-tab-pane>
</el-tabs>
......
......@@ -4,6 +4,9 @@ import AppList from '@/components/base/AppList.vue'
import { getMaterialList, deleteMaterial } from '@/api/base'
import type { MaterialProp } from '@/types'
import { ElMessage } from 'element-plus'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const UpdateMaterialDialog = defineAsyncComponent(() => import('@/components/base/UpdateMaterialDialog.vue'))
......@@ -117,6 +120,7 @@ const deleteMembers = function (ids: string) {
<template #header-buttons>
<el-space>
<el-button
v-if="!userStore.status.material_status"
type="primary"
:icon="Plus"
@click="handleAdd"
......@@ -151,6 +155,7 @@ const deleteMembers = function (ids: string) {
:data="currentRow"
@update="handleRefresh()"
v-if="updateVisible"
v-model="updateVisible"></UpdateMaterialDialog>
v-model="updateVisible"
></UpdateMaterialDialog>
</AppCard>
</template>
......@@ -4,6 +4,9 @@ import AppList from '@/components/base/AppList.vue'
import { getMaterialList, deleteMaterial } from '@/api/base'
import type { MaterialProp } from '@/types'
import { ElMessage } from 'element-plus'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const UpdateMaterialDialog = defineAsyncComponent(() => import('@/components/base/UpdateMaterialDialog.vue'))
......@@ -117,6 +120,7 @@ const deleteMembers = function (ids: string) {
<template #header-buttons>
<el-space>
<el-button
v-if="!userStore.status.material_status"
type="primary"
:icon="Plus"
@click="handleAdd"
......@@ -151,6 +155,7 @@ const deleteMembers = function (ids: string) {
:data="currentRow"
@update="handleRefresh()"
v-if="updateVisible"
v-model="updateVisible"></UpdateMaterialDialog>
v-model="updateVisible"
></UpdateMaterialDialog>
</AppCard>
</template>
......@@ -4,6 +4,9 @@ import AppList from '@/components/base/AppList.vue'
import { getMaterialList, deleteMaterial } from '@/api/base'
import type { MaterialProp } from '@/types'
import { ElMessage } from 'element-plus'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const UpdateMaterialDialog = defineAsyncComponent(() => import('@/components/base/UpdateMaterialDialog.vue'))
......@@ -117,6 +120,7 @@ const deleteMembers = function (ids: string) {
<template #header-buttons>
<el-space>
<el-button
v-if="!userStore.status.material_status"
type="primary"
:icon="Plus"
@click="handleAdd"
......@@ -151,6 +155,7 @@ const deleteMembers = function (ids: string) {
:data="currentRow"
@update="handleRefresh()"
v-if="updateVisible"
v-model="updateVisible"></UpdateMaterialDialog>
v-model="updateVisible"
></UpdateMaterialDialog>
</AppCard>
</template>
......@@ -4,6 +4,9 @@ import AppList from '@/components/base/AppList.vue'
import { getMaterialList, deleteMaterial } from '@/api/base'
import type { MaterialProp } from '@/types'
import { ElMessage } from 'element-plus'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const UpdateMaterialDialog = defineAsyncComponent(() => import('@/components/base/UpdateMaterialDialog.vue'))
......@@ -117,6 +120,7 @@ const deleteMembers = function (ids: string) {
<template #header-buttons>
<el-space>
<el-button
v-if="!userStore.status.material_status"
type="primary"
:icon="Plus"
@click="handleAdd"
......@@ -151,6 +155,7 @@ const deleteMembers = function (ids: string) {
:data="currentRow"
@update="handleRefresh()"
v-if="updateVisible"
v-model="updateVisible"></UpdateMaterialDialog>
v-model="updateVisible"
></UpdateMaterialDialog>
</AppCard>
</template>
......@@ -4,6 +4,9 @@ import AppList from '@/components/base/AppList.vue'
import { getMaterialList, deleteMaterial } from '@/api/base'
import type { MaterialProp } from '@/types'
import { ElMessage } from 'element-plus'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const UpdateMaterialDialog = defineAsyncComponent(() => import('@/components/base/UpdateMaterialDialog.vue'))
......@@ -115,12 +118,13 @@ const deleteMembers = function (ids: string) {
<AppCard>
<AppList v-bind="listOptions" ref="appList" @selection-change="handleSelectionChange">
<template #header-buttons>
<el-space>
<el-space v-if="!userStore.status.material_status">
<!-- v-permission="'v1-experiment-marketing-material-create'" -->
<el-button
v-if="!userStore.status.material_status"
type="primary"
:icon="Plus"
@click="handleAdd"
v-permission="'v1-experiment-marketing-material-create'"
>新建</el-button
>
<!-- <el-button type="danger" plain :icon="Delete" :disabled="!multipleSelection.length" @click="handleRemoves()"
......@@ -151,6 +155,7 @@ const deleteMembers = function (ids: string) {
:data="currentRow"
@update="handleRefresh()"
v-if="updateVisible"
v-model="updateVisible"></UpdateMaterialDialog>
v-model="updateVisible"
></UpdateMaterialDialog>
</AppCard>
</template>
......@@ -4,6 +4,9 @@ import AppList from '@/components/base/AppList.vue'
import { getMaterialList, deleteMaterial } from '@/api/base'
import type { MaterialProp } from '@/types'
import { ElMessage } from 'element-plus'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const UpdateMaterialDialog = defineAsyncComponent(() => import('@/components/base/UpdateMaterialDialog.vue'))
......@@ -115,12 +118,13 @@ const deleteMembers = function (ids: string) {
<AppCard>
<AppList v-bind="listOptions" ref="appList" @selection-change="handleSelectionChange">
<template #header-buttons>
<el-space>
<el-space v-if="!userStore.status.material_status">
<!-- v-permission="'v1-experiment-marketing-material-create'" -->
<el-button
v-if="!userStore.status.material_status"
type="primary"
:icon="Plus"
@click="handleAdd"
v-permission="'v1-experiment-marketing-material-create'"
>新建</el-button
>
<!-- <el-button type="danger" plain :icon="Delete" :disabled="!multipleSelection.length" @click="handleRemoves()"
......@@ -151,6 +155,7 @@ const deleteMembers = function (ids: string) {
:data="currentRow"
@update="handleRefresh()"
v-if="updateVisible"
v-model="updateVisible"></UpdateMaterialDialog>
v-model="updateVisible"
></UpdateMaterialDialog>
</AppCard>
</template>
......@@ -4,6 +4,9 @@ import AppList from '@/components/base/AppList.vue'
import { getMaterialList, deleteMaterial } from '@/api/base'
import type { MaterialProp } from '@/types'
import { ElMessage } from 'element-plus'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const UpdateMaterialDialog = defineAsyncComponent(() => import('@/components/base/UpdateMaterialDialog.vue'))
......@@ -115,12 +118,13 @@ const deleteMembers = function (ids: string) {
<AppCard>
<AppList v-bind="listOptions" ref="appList" @selection-change="handleSelectionChange">
<template #header-buttons>
<el-space>
<el-space v-if="!userStore.status.material_status">
<!-- v-permission="'v1-experiment-marketing-material-create'" -->
<el-button
v-if="!userStore.status.material_status"
type="primary"
:icon="Plus"
@click="handleAdd"
v-permission="'v1-experiment-marketing-material-create'"
>新建</el-button
>
<!-- <el-button type="danger" plain :icon="Delete" :disabled="!multipleSelection.length" @click="handleRemoves()"
......@@ -151,6 +155,7 @@ const deleteMembers = function (ids: string) {
:data="currentRow"
@update="handleRefresh()"
v-if="updateVisible"
v-model="updateVisible"></UpdateMaterialDialog>
v-model="updateVisible"
></UpdateMaterialDialog>
</AppCard>
</template>
......@@ -4,6 +4,9 @@ import AppList from '@/components/base/AppList.vue'
import { getMaterialList, deleteMaterial } from '@/api/base'
import type { MaterialProp } from '@/types'
import { ElMessage } from 'element-plus'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const UpdateMaterialDialog = defineAsyncComponent(() => import('@/components/base/UpdateMaterialDialog.vue'))
......@@ -117,6 +120,7 @@ const deleteMembers = function (ids: string) {
<template #header-buttons>
<el-space>
<el-button
v-if="!userStore.status.material_status"
type="primary"
:icon="Plus"
@click="handleAdd"
......@@ -151,6 +155,7 @@ const deleteMembers = function (ids: string) {
:data="currentRow"
@update="handleRefresh()"
v-if="updateVisible"
v-model="updateVisible"></UpdateMaterialDialog>
v-model="updateVisible"
></UpdateMaterialDialog>
</AppCard>
</template>
......@@ -2,7 +2,7 @@
import type { EventProp, EventDetailProp, EventAttributesProp } from '../types'
import { ElMessage } from 'element-plus'
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'
const store = useMapStore()
......@@ -76,7 +76,7 @@ const changeFormatType = function (row: any) {
row.format = '4'
break
case '3':
row.format = '2'
row.format = '20'
break
case '4':
row.format = 'yyyy-mm-dd'
......@@ -86,10 +86,37 @@ const changeFormatType = function (row: any) {
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>
<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">
<el-form label-width="120px">
<el-form-item label="事件英文名称:">{{ eventDetail?.english_name }}</el-form-item>
......@@ -104,7 +131,7 @@ const changeFormatType = function (row: any) {
<template #header>
<div class="card-header">
<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>
</template>
<el-table :data="tableData" style="width: 100%">
......@@ -120,26 +147,58 @@ const changeFormatType = function (row: any) {
</el-table-column>
<el-table-column label="字段类型">
<template #default="scope">
<el-select @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
@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>
</template>
</el-table-column>
<el-table-column label="字段格式">
<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 !== '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-input
v-else
:disabled="scope.row.id !== ''"
type="number"
v-model="scope.row.format"
:placeholder="scope.row.type === '1' ? '请输入字符串长度' : '请输入长度'"></el-input>
:placeholder="scope.row.type === '1' ? '请输入字符串长度' : '请输入长度'"
></el-input>
</template>
</el-table-column>
<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">
<div @click="deleteField(scope)" style="display: flex; justify-content: center; cursor: pointer">
<el-icon size="20"><Close /></el-icon>
......@@ -151,7 +210,9 @@ const changeFormatType = function (row: any) {
<template #footer>
<el-row justify="center">
<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>
</template>
</el-dialog>
......
......@@ -8,6 +8,7 @@ export interface EventProp {
status_name: string
updated_operator_name: string
updated_time: string
can_edit: string
}
export interface ConnectionOptionProp { type_name: string; id: string }
......
......@@ -55,7 +55,9 @@ const listOptions = computed(() => {
label: '状态',
prop: 'status_name',
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' },
......@@ -120,14 +122,20 @@ const handleField = function (row: EventProp) {
</template>
<template #table-x="{ row }">
<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="handleRemove(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)" :disabled="row?.can_edit === '0'">删除</el-button>
<el-button type="primary" plain @click="handleField(row)">字段</el-button>
</template>
</AppList>
</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" />
<FieldDialog v-model="fieldVisible" :data="currentRow" v-if="fieldVisible && currentRow"></FieldDialog>
......
......@@ -4,6 +4,7 @@ import type { FormInstance, FormRules } from 'element-plus'
import { ElMessage } from 'element-plus'
import { useMapStore } from '@/stores/map'
import { createMemberMeta, updateMemberMeta } from '../api'
import { QuestionFilled } from '@element-plus/icons-vue'
const store = useMapStore()
......@@ -31,7 +32,8 @@ const form = reactive(
english_name: '',
type: '',
format: '',
status: '1'
status: '1',
type_name: ''
}
)
......@@ -84,7 +86,7 @@ const changeFormatType = function () {
form.format = '4'
break
case '3':
form.format = '2'
form.format = '20'
break
case '4':
form.format = 'yyyy-mm-dd'
......@@ -94,10 +96,37 @@ const changeFormatType = function () {
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>
<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-item label="属性ID" v-if="isUpdate">
{{ props.data?.id }}
......@@ -109,16 +138,48 @@ const changeFormatType = function () {
<el-input v-model="form.name" placeholder="请输入" />
</el-form-item>
<el-form-item label="属性字段类型" prop="type">
<el-select :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
: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-form-item>
<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 !== '4'" label="yyyy-mm-dd hh:mm:ss" value="yyyy-mm-dd hh:mm:ss"></el-option>
</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 label="状态" prop="status">
<el-switch v-model="form.status" active-text="生效" inactive-text="失效" active-value="1" inactive-value="0" />
......
......@@ -48,7 +48,9 @@ const listOptions = computed(() => {
label: '状态',
prop: 'status_name',
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' },
......
<script setup lang="ts">
import { getProgress } from '../api'
import AppList from '@/components/base/AppList.vue'
defineEmits<{
(e: 'update'): void
......@@ -13,7 +14,7 @@ const listOptions = computed(() => {
httpRequest: getProgress,
params: {}
},
limit: 5,
limit: 10,
columns: [
{ label: '文件名', prop: 'name' },
{
......@@ -23,6 +24,7 @@ const listOptions = computed(() => {
return bytesToSize(row.row.size)
}
},
{ label: '来源链接', prop: 'connection_name' },
{
label: '状态',
prop: 'status_name',
......@@ -32,7 +34,9 @@ const listOptions = computed(() => {
: `<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) => {
return (bytes / Math.pow(k, i)).toPrecision(3) + ' ' + sizes[i]
}
const appList = $ref<InstanceType<typeof AppList> | null>(null)
const refetch = function () {
appList?.refetch()
}
</script>
<template>
<el-dialog
class="connect-form"
title="导入用户数据"
:close-on-click-modal="false"
width="800px"
@update:modelValue="value => $emit('update:modelValue', value)">
<AppList v-bind="listOptions" ref="appList"></AppList>
width="900px"
@update:modelValue="value => $emit('update:modelValue', value)"
>
<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>
</template>
......@@ -62,6 +62,9 @@ export interface ImageProp {
history_tags: string[]
static_groups: string[]
dynamic_groups: string[]
dynamic_group_list: any[]
static_group_list: any[]
tag_list: any[]
events: {
list: {
updated_time: string
......
......@@ -5,6 +5,8 @@ import type { MemberFieldsProp, ImageProp, ConnectionsProp } from '../types'
import Icon from '@/components/ConnectionIcon.vue'
const ViewEvent = defineAsyncComponent(() => import('@/components/ViewEvent.vue'))
const ViewLabel = defineAsyncComponent(() => import('@/components/ViewLabel.vue'))
const ViewGroup = defineAsyncComponent(() => import('@/components/ViewGroup.vue'))
const route = useRoute()
......@@ -42,12 +44,28 @@ const getDate = function (date: string) {
return parseInt(date.slice(date.indexOf(' '), date.indexOf(' ') + 3)) > 12 ? '下午' : '上午'
}
// 查看事件
const viewEventVisible = ref(false)
const currentViewEvent = ref()
function handleViewEvent(item: any) {
viewEventVisible.value = true
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[]>([])
......@@ -130,7 +148,7 @@ watch(currentConnection, () => {
<el-tabs class="demo-tabs">
<el-tab-pane label="当前标签">
<div class="scroll" v-if="data?.tags && data.tags.length">
<el-tag class="ml-2" type="success" v-for="(item, index) in data.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>
<el-empty description="暂无数据" :image-size="80" v-else />
</el-tab-pane>
......@@ -148,7 +166,9 @@ watch(currentConnection, () => {
<el-tabs class="demo-tabs">
<el-tab-pane label="静态群组">
<div class="scroll" v-if="data?.static_groups && data.static_groups.length">
<el-tag class="ml-2" type="success" v-for="(item, index) in data.static_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>
<el-empty description="暂无数据" :image-size="80" v-else />
</el-tab-pane>
......@@ -156,7 +176,9 @@ watch(currentConnection, () => {
<el-tabs class="demo-tabs">
<el-tab-pane label="动态群组">
<div class="scroll" v-if="data?.dynamic_groups && data.dynamic_groups.length">
<el-tag class="ml-2" type="success" v-for="(item, index) in data.dynamic_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>
<el-empty description="暂无数据" :image-size="80" v-else />
</el-tab-pane>
......@@ -191,6 +213,10 @@ watch(currentConnection, () => {
</div>
<!-- 事件详情 -->
<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>
<style lang="scss">
.info-box {
......
......@@ -4,6 +4,9 @@ import AppList from '@/components/base/AppList.vue'
import { getMemberList, deleteMember, getMemberConnectionsList } from '../api'
import { ElMessage, ElMessageBox, ElLoading } from 'element-plus'
import type { MemberProp, ConnectionsProp } from '../types'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const UpdateDialog = defineAsyncComponent(() => import('../components/UpdateDialog.vue'))
const UploadEventsDialog = defineAsyncComponent(() => import('../components/UploadEventsDialog.vue'))
......@@ -207,17 +210,27 @@ const downloadMember = function (isAll?: boolean) {
<template #header-buttons>
<el-row justify="space-between">
<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-button type="primary" :icon="Download">导出</el-button>
<template #dropdown>
<el-dropdown-menu>
<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>
</template>
</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>
<template #dropdown>
<el-dropdown-menu>
......@@ -226,14 +239,19 @@ const downloadMember = function (isAll?: boolean) {
</el-dropdown-menu>
</template>
</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-dropdown v-permission="'v1-experiment-member-delete'">
<!-- v-permission="'v1-experiment-member-delete'" -->
<el-dropdown>
<el-button type="danger" :icon="Delete">删除</el-button>
<template #dropdown>
<el-dropdown-menu>
<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>
</template>
</el-dropdown>
......@@ -244,8 +262,12 @@ const downloadMember = function (isAll?: boolean) {
<template #table-x="{ row }">
<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="handleEdit(row)" v-permission="'v1-experiment-member-update'">编辑</el-button>
<el-button type="primary" plain @click="handleRemove(row)" v-permission="'v1-experiment-member-delete'">删除</el-button>
<el-button type="primary" plain @click="handleEdit(row)" v-permission="'v1-experiment-member-update'"
>编辑</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>
</template>
</AppList>
......
......@@ -25,7 +25,7 @@ router.beforeEach(async (to, from, next) => {
path: to.path,
query: {
...to.query,
experiment_id: from.query.experiment_id || '7165149417073278976',
experiment_id: from.query.experiment_id || '7028276368903241728',
student_id: from.query.student_id,
force_tgc: from.query.force_tgc
}
......
......@@ -72,50 +72,57 @@ const studentMenus: IMenuItem[] = [
name: '文本资料管理',
path: '/material?type=1',
icon: markRaw(IconText),
tag: 'v1-experiment-marketing-material-list'
// tag: 'v1-experiment-marketing-material-list'
tag: ''
},
{
name: '图片资料管理',
path: '/material?type=2',
icon: markRaw(IconImage),
tag: 'v1-experiment-marketing-material-list'
// tag: 'v1-experiment-marketing-material-list'
tag: ''
},
{
name: '卡券资料管理',
path: '/material?type=8',
icon: markRaw(IconCard),
tag: 'v1-experiment-marketing-material-list'
// tag: 'v1-experiment-marketing-material-list'
tag: ''
},
{
name: '语音资料管理',
path: '/material?type=3',
icon: markRaw(IconAudio),
tag: 'v1-experiment-marketing-material-list'
// tag: 'v1-experiment-marketing-material-list'
tag: ''
},
{
name: '视频资料管理',
path: '/material?type=4',
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: '二维码资料管理',
path: '/material?type=6',
icon: markRaw(IconQrcode),
tag: 'v1-experiment-marketing-material-list'
// tag: 'v1-experiment-marketing-material-list'
tag: ''
},
{
name: '小程序资料管理',
path: '/material?type=7',
icon: markRaw(IconMiniProgram),
tag: 'v1-experiment-marketing-material-list'
// tag: 'v1-experiment-marketing-material-list'
tag: ''
}
]
},
{
name: '自动化营销',
path: '/trip',
path: '/trip/my',
icon: markRaw(IconTrip)
},
{
......
......@@ -15,7 +15,7 @@ interface State {
organization: OrganizationType | null
roles: RoleType[]
permissions: PermissionType[]
status: boolean
status: any
}
export const useUserStore = defineStore({
......@@ -27,7 +27,7 @@ export const useUserStore = defineStore({
project: null,
roles: [],
permissions: [],
status: false
status: {}
}),
getters: {
isLogin: state => !!state.user
......@@ -45,7 +45,7 @@ export const useUserStore = defineStore({
this.permissions = permissions
await useMapStore().getMapList()
await checkDataStatus().then(res => {
this.status = res.data.status
this.status = res.data
})
},
async logout() {
......
......@@ -108,7 +108,7 @@ export interface EventRuleItem {
// 标签规则
export interface TagRule {
current_logic_operate: 'and' | 'or'
items: string[]
items: any[]
}
// 用户行为规则
......
......@@ -16,7 +16,8 @@ export const tripTemplateTypeList = [
// 群组类型
export const groupTypeList = [
{ label: '静态群组', value: '1' },
{ label: '动态群组', value: '2' }
{ label: '动态群组', value: '2' },
{ label: 'RFM群组', value: '3' }
]
// 更新方式
......@@ -96,9 +97,12 @@ export const happenInfoList = [
export const triggerInfoList = [{ label: '触发次数', value: '触发次数' }]
export const labelList = [
{ label: '自定义分层标签 ', value: '1' },
{ label: '单属性标签', value: '5' },
{ label: '多属性标签', value: '6' },
{ label: '事件偏好标签 ', value: '2' },
{ label: '事件指标标签 ', value: '3' },
{ label: '自定义标签', value: '7' },
{ label: '分层标签 ', value: '1' },
{ label: 'RFM模型标签 ', value: '4' }
]
......@@ -110,7 +114,8 @@ export const wayList = [
export const materialMethodList = [
{ label: '离线上传 ', value: '2' },
{ label: '在线AI ', value: '1' }
{ label: '在线AI', value: '1' },
{ label: '在线设计', value: '3' }
]
// 使用场景
......@@ -129,3 +134,24 @@ export const materialUsersList = [
{ label: '运维人员 ', value: '4' },
{ 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'
export function checkPermission(value: string | string[]): boolean {
const userStore = useUserStore()
// 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
if (Array.isArray(value)) {
return permissions.some(item => value.includes(item.tag))
......@@ -13,7 +13,6 @@ export function checkPermission(value: string | string[]): boolean {
return !!permissions.find(item => item.tag === value)
}
}
// 权限指令
export function permissionDirective(el: HTMLElement, binding: DirectiveBinding) {
const { value } = binding
......
import axios from 'axios'
import md5 from 'blueimp-md5'
import { getSignature, uploadFile } from '@/api/base'
......@@ -17,3 +18,8 @@ export async function upload(blob: Blob) {
await uploadFile(params)
return params.url
}
export async function uploadFileByUrl(url: string) {
const res = await axios.get(url, { responseType: 'blob' })
return upload(res.data)
}
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论