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

chore: 修改内容营销设计

上级 24fbc9de
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "^2.1.0", "@element-plus/icons-vue": "^2.1.0",
"@fortaine/fetch-event-source": "^3.0.6",
"@tinymce/tinymce-vue": "^5.0.1", "@tinymce/tinymce-vue": "^5.0.1",
"@vue-flow/controls": "^1.0.4", "@vue-flow/controls": "^1.0.4",
"@vue-flow/core": "^1.17.4", "@vue-flow/core": "^1.17.4",
...@@ -991,6 +992,14 @@ ...@@ -991,6 +992,14 @@
"@floating-ui/core": "^1.0.5" "@floating-ui/core": "^1.0.5"
} }
}, },
"node_modules/@fortaine/fetch-event-source": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@fortaine/fetch-event-source/-/fetch-event-source-3.0.6.tgz",
"integrity": "sha512-621GAuLMvKtyZQ3IA6nlDWhV1V/7PGOTNIGLUifxt0KzM+dZIweJ6F3XvQF3QnqeNfS1N7WQ0Kil1Di/lhChEw==",
"engines": {
"node": ">=16.15"
}
},
"node_modules/@humanwhocodes/config-array": { "node_modules/@humanwhocodes/config-array": {
"version": "0.11.10", "version": "0.11.10",
"resolved": "https://registry.npmmirror.com/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", "resolved": "https://registry.npmmirror.com/@humanwhocodes/config-array/-/config-array-0.11.10.tgz",
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
}, },
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "^2.1.0", "@element-plus/icons-vue": "^2.1.0",
"@fortaine/fetch-event-source": "^3.0.6",
"@tinymce/tinymce-vue": "^5.0.1", "@tinymce/tinymce-vue": "^5.0.1",
"@vue-flow/controls": "^1.0.4", "@vue-flow/controls": "^1.0.4",
"@vue-flow/core": "^1.17.4", "@vue-flow/core": "^1.17.4",
......
...@@ -4,7 +4,7 @@ export default { name: 'AppMain' } ...@@ -4,7 +4,7 @@ export default { name: 'AppMain' }
<template> <template>
<section class="app-main"> <section class="app-main">
<router-view></router-view> <router-view :key="$route.fullPath"></router-view>
</section> </section>
</template> </template>
......
import { getMetaUserAttrList, getMetaEventList, getTagList, getConnectionList, getUserList } from '@/api/base' import { getMetaUserAttrList, getMetaEventList, getTagList, getConnectionList, getUserList } from '@/api/base'
import { useMapStore } from '@/stores/map'
// 用户属性类型 // 用户属性类型
export interface AttrType { export interface AttrType {
...@@ -78,13 +79,16 @@ export function useTag() { ...@@ -78,13 +79,16 @@ export function useTag() {
// 所有连接 // 所有连接
const connectionList = ref<ConnectionType[]>([]) const connectionList = ref<ConnectionType[]>([])
export function useConnection() { export function useConnection() {
function fetchConnectionList() { async function fetchConnectionList() {
const connectionType = useMapStore().getMapValuesByKey('experiment_connection_type')
getConnectionList().then((res: any) => { getConnectionList().then((res: any) => {
connectionList.value = res.data.items.map((item: any) => { connectionList.value = res.data.items.map((item: any) => {
const connection = connectionType.find(type => type.value == item.type)
const attrs = typeof item.config_attributes === 'string' ? JSON.parse(item.config_attributes) : item.config_attributes const attrs = typeof item.config_attributes === 'string' ? JSON.parse(item.config_attributes) : item.config_attributes
const name = Array.isArray(attrs) ? attrs.find((item: any) => item.prop === 'name')?.value : attrs.name const name = Array.isArray(attrs) ? attrs.find((item: any) => item.prop === 'name')?.value : attrs.name
return { ...item, config_attributes: attrs, name } return { ...item, config_attributes: attrs, name, type_name: connection?.label || item.type }
}) })
}) })
} }
......
import httpRequest from '@/utils/axios'
// 新建资料
export function createMaterial(data: { name: string; type: string; content: string; status: string }) {
return httpRequest.post('/api/lab/v1/experiment/marketing-ai/create', data)
}
// 更新资料
export function updateMaterial(data: any) {
return httpRequest.post('/api/lab/v1/experiment/marketing-ai/update', data)
}
// 资料列表
export function getMaterialList(params?: { name: string; type: string; way: string; id: string; status: string; updated_operator: string }) {
return httpRequest.get('/api/lab/v1/experiment/marketing-ai/list', { params })
}
// 资料详情
export function getMaterial(params: { id: string }) {
return httpRequest.get('/api/lab/v1/experiment/marketing-ai/detail', { params })
}
// 删除资料
export function deleteMaterial(data: { id: string }) {
return httpRequest.post('/api/lab/v1/experiment/marketing-ai/delete', data)
}
// 获取所属行业列表
export function getIndustryList() {
return httpRequest.get('/api/lab/v1/experiment/marketing-ai/industries')
}
// 获取实验下绑定的所有连接
export function getConnectionList() {
return httpRequest.get('/api/lab/v1/experiment/marketing-ai/connections')
}
// 获取天工AI的使用详情
export function getAIUsage(params: { marketing_material_id: string }) {
return httpRequest.get('/api/lab/v1/experiment/marketing-ai/ai-usage-detail', { params })
}
// 天工AI-聊天
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)
}
<script setup>
import { useClipboard } from '@vueuse/core'
import { useChat } from '../composables/useChat'
import { useMapStore } from '@/stores/map'
import { useConnection } from '../composables/useConnection'
import { useIndustry } from '../composables/useIndustry'
import { getNameByValue, materialMethodList, materialUsageList, materialUsersList } from '@/utils/dictionary'
import IconComputer from './IconComputer.vue'
import IconUser from './IconUser.vue'
import IconAI from './IconAI.vue'
const form = defineModel()
const materialType = useMapStore().getMapValuesByKey('experiment_marketing_material_type')
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 content = ref('')
const route = useRoute()
const { usages, messages, post, isLoading } = useChat({ experiment_id: route.query.experiment_id, marketing_material_id: form.value.id })
onMounted(() => {
messages.value.push({ role: 'system', content: welcomeMessage.value })
})
// 设置为最后一条ai回复的内容
watchEffect(() => {
const botLastMessage = messages.value.findLast(item => item.role === 'bot')
if (botLastMessage) {
form.value.content = botLastMessage.content
}
})
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' })
content.value = ''
}
async function handleSend(event) {
if (event.shiftKey) return
event.preventDefault()
await postMessage()
}
async function handleSendType(type, context) {
post({ type, context })
}
const chatRef = ref()
function scrollToBottom() {
if (!chatRef.value) return
const parentEl = chatRef.value.parentElement
parentEl.scrollTo(0, parentEl.scrollHeight)
}
watch(messages.value, () => nextTick(() => scrollToBottom()))
const { copy } = useClipboard()
</script>
<template>
<div class="chat" ref="chatRef">
<div class="chat-message" :class="item.role" v-for="(item, index) in messages" :key="index">
<div class="chat-message-avatar">
<IconComputer v-if="item.role === 'system'" />
<IconUser v-else-if="item.role === 'user'" />
<IconAI v-else />
</div>
<div class="chat-message-main">
<div class="chat-message-content" v-html="item.content"></div>
<div class="chat-message-extra" v-if="item.role !== 'user'">
<el-button size="small" type="primary" @click="copy(item.content)">复制</el-button>
<el-button size="small" type="primary" @click="handleSendType(5, 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
>
<el-button size="small" type="primary" @click="handleSendType(4, item.content)"
>AI扩写({{ usages.ai_expand_count }}/{{ usages.ai_expand_max_count }})</el-button
>
</div>
</div>
</div>
<div class="chat-message" v-if="isLoading">
<div class="chat-message-avatar"><IconAI /></div>
<div class="chat-message-main">
<div class="dot-flashing"></div>
</div>
</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-button text type="primary" @click="handleSend">发送</el-button>
</div>
</template>
<style lang="scss">
.chat-footer {
position: relative;
margin: 40px 0;
.el-textarea__inner {
padding: 16px;
resize: none;
}
.el-button {
position: absolute;
right: 10px;
bottom: 10px;
}
}
.chat-message {
margin: 30px 0;
display: flex;
flex-direction: row;
gap: 10px;
}
.chat-message-avatar {
flex: 0 0 40px;
height: 40px;
background-color: #fff;
border-radius: 50%;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.chat-message-main {
color: #000;
max-width: 100%;
padding: 20px;
border-radius: 16px;
background-color: #fff;
}
.chat-message-content {
max-width: 100%;
word-break: break-word;
}
.user .chat-message-main {
color: #fff;
background-color: var(--main-color);
}
.chat-message-extra {
margin-top: 20px;
}
.dot-flashing {
animation: dot-flashing 0.8s infinite alternate;
animation-delay: -0.2s;
animation-timing-function: ease;
margin: 7px 18px;
overflow: visible !important;
position: relative;
}
.dot-flashing,
.dot-flashing:after,
.dot-flashing:before {
background-color: rgba(0, 0, 0, 0.1);
border-radius: 4px;
color: rgba(0, 0, 0, 0.1);
height: 8px;
width: 8px;
}
.dot-flashing:after,
.dot-flashing:before {
animation: dot-flashing 0.8s infinite alternate;
animation-timing-function: ease;
content: '';
display: inline-block;
position: absolute;
top: 0;
}
.dot-flashing:before {
left: -15px;
animation-delay: -0.4s;
}
.dot-flashing:after {
left: 15px;
animation-delay: 0s;
}
@keyframes dot-flashing {
0% {
background-color: #000;
}
50% {
background-color: rgba(0, 0, 0, 0.1);
}
to {
background-color: #000;
}
}
.bold {
font-size: 16px;
font-weight: bold;
color: var(--main-color);
}
</style>
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" width="22">
<g>
<path
d="M200.838737 832l30.800842-105.633684h223.851789l31.232 105.633684h205.608421l-239.023157-640H239.023158L0 832h200.838737z m213.423158-244.035368h-140.099369l69.847579-230.076632 70.278737 230.076632z m545.738105 244.035368V192h-196.958316v640h196.958316z"
fill="#ba143e"></path>
</g>
</svg>
</template>
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" width="22">
<g>
<path
d="M672 768c19.2 0 32 12.8 32 32 0 16-12.8 28.8-28.8 32H352c-19.2 0-32-12.8-32-32 0-16 12.8-28.8 28.8-32h323.2z m128-576c19.2 0 32 12.8 32 32v448c0 19.2-12.8 32-32 32h-576c-19.2 0-32-12.8-32-32v-448c0-19.2 12.8-32 32-32h576zM768 256H256v384h512V256z"
fill="#1677FF"></path>
<path d="M640 416c19.2 0 32 12.8 32 32 0 16-12.8 28.8-28.8 32H384c-19.2 0-32-12.8-32-32 0-16 12.8-28.8 28.8-32H640z" fill="#6AA7FB"></path>
</g>
</svg>
</template>
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="64 64 896 896" width="20">
<g>
<path
d="M858.5 763.6a374 374 0 0 0-80.6-119.5 375.63 375.63 0 0 0-119.5-80.6c-.4-.2-.8-.3-1.2-.5C719.5 518 760 444.7 760 362c0-137-111-248-248-248S264 225 264 362c0 82.7 40.5 156 102.8 201.1-.4.2-.8.3-1.2.5-44.8 18.9-85 46-119.5 80.6a375.63 375.63 0 0 0-80.6 119.5A371.7 371.7 0 0 0 136 901.8a8 8 0 0 0 8 8.2h60c4.4 0 7.9-3.5 8-7.8 2-77.2 33-149.5 87.8-204.3 56.7-56.7 132-87.9 212.2-87.9s155.5 31.2 212.2 87.9C779 752.7 810 825 812 902.2c.1 4.4 3.6 7.8 8 7.8h60a8 8 0 0 0 8-8.2c-1-47.8-10.9-94.3-29.5-138.2zM512 534c-45.9 0-89.1-17.9-121.6-50.4S340 407.9 340 362c0-45.9 17.9-89.1 50.4-121.6S466.1 190 512 190s89.1 17.9 121.6 50.4S684 316.1 684 362c0 45.9-17.9 89.1-50.4 121.6S557.9 534 512 534z"
fill="#ba143e"></path>
</g>
</svg>
</template>
<script setup>
import { useMapStore } from '@/stores/map'
import { materialMethodList } from '@/utils/dictionary'
const materialType = useMapStore().getMapValuesByKey('experiment_marketing_material_type')
defineProps(['action'])
const emit = defineEmits(['next'])
const form = defineModel()
const formRef = ref()
const rules = ref({
type: [{ required: true, message: '请选择营销内容类型' }],
name: [{ required: true, message: '请输入内容名称' }]
})
async function handleValidate() {
await formRef.value.validate()
}
async function handleNext() {
await handleValidate()
emit('next')
}
</script>
<template>
<el-card shadow="never">
<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" :label="item.value">{{ item.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="内容名称" prop="name">
<el-input v-model="form.name" placeholder="请输入内容名称" />
</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" :label="item.value" :disabled="item.value == 1 && form.type != 1">{{
item.label
}}</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<el-row justify="center">
<el-button type="primary" @click="handleNext">下一步</el-button>
</el-row>
</el-card>
</template>
<script setup>
import { useMapStore } from '@/stores/map'
import { getNameByValue, materialMethodList } from '@/utils/dictionary'
import AppUpload from '@/components/base/AppUpload.vue'
import AIChat from './AIChat.vue'
defineProps(['action'])
const emit = defineEmits(['prev', 'submit'])
const materialType = useMapStore().getMapValuesByKey('experiment_marketing_material_type')
const form = defineModel()
const formRef = ref()
const rules = ref({
content: [{ required: true, message: '请输入内容名称' }]
})
const typeName = computed(() => {
return getNameByValue(form.value.type, materialType)
})
async function handleValidate() {
await formRef.value.validate()
}
async function handlePrev() {
emit('prev')
}
async function handleSubmit() {
await handleValidate()
emit('submit')
}
</script>
<template>
<el-card shadow="never">
<template #header>内容创作</template>
<el-form label-suffix=":" inline class="info-form">
<el-form-item label="内容名称">{{ form.name }}</el-form-item>
<el-form-item label="内容类型">{{ typeName }}</el-form-item>
<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-item :label="`${typeName}资源`" prop="content" v-if="form.way == 2">
<template v-if="form.type == 1">
<!-- 文本 -->
<el-input type="textarea" rows="6" v-model="form.content"></el-input>
</template>
<template v-if="['2', '6', '7', '8'].includes(form.type)">
<!-- 图片|二维码|小程序|卡券 -->
<AppUpload v-model="form.content" accept="image/*"></AppUpload>
</template>
<template v-if="form.type == 3">
<!-- 语音 -->
<div>
<AppUpload v-model="form.content" accept=".mp3">
<el-button type="primary">上传语音</el-button>
</AppUpload>
<audio :src="form.content" controls v-if="form.content"></audio>
</div>
</template>
<template v-if="form.type == 4">
<!-- 视频 -->
<div>
<AppUpload v-model="form.content" accept=".mp4">
<el-button type="primary">上传视频</el-button>
</AppUpload>
<video controls :src="form.content" style="max-width: 600px; width: 100%" v-if="form.content"></video>
</div>
</template>
<template v-if="form.type == 5">
<el-input v-model="form.content" placeholder="请输入">
<template #prepend>https://</template>
<template #append v-if="form.content">
<a :href="form.content" target="_blank">查看</a>
</template>
</el-input>
</template>
</el-form-item>
<template v-else>
<AIChat v-model="form"></AIChat>
</template>
</el-form>
<el-row justify="center">
<el-button type="primary" @click="handlePrev">上一步</el-button>
<el-button type="primary" @click="handleSubmit" v-if="action !== 'view'">提交</el-button>
</el-row>
</el-card>
</template>
<style lang="scss">
.info-form {
display: flex;
justify-content: space-evenly;
.el-form-item {
margin-bottom: 0;
}
}
</style>
<script setup>
import { useMapStore } from '@/stores/map'
import { useConnection } from '../composables/useConnection'
import { useIndustry } from '../composables/useIndustry'
import { getNameByValue, materialMethodList, materialUsageList, materialUsersList } from '@/utils/dictionary'
defineProps(['action'])
const emit = defineEmits(['prev', 'next'])
const materialType = useMapStore().getMapValuesByKey('experiment_marketing_material_type')
const { connectionList } = useConnection()
const { industryList } = useIndustry()
const form = defineModel()
const formRef = ref()
const rules = ref({
industry_id: [{ required: true, message: '请选择所属行业' }],
scenario_type: [{ required: true, message: '请选择使用场景' }],
personnel_type: [{ required: true, message: '请选择使用人员' }],
channel: [{ required: true, message: '请选择内容投放渠道' }]
})
async function handleValidate() {
await formRef.value.validate()
}
async function handlePrev() {
emit('prev')
}
async function handleNext() {
await handleValidate()
emit('next')
}
</script>
<template>
<el-card shadow="never">
<template #header>扩展信息</template>
<el-form label-suffix=":" inline class="info-form">
<el-form-item label="内容名称">{{ form.name }}</el-form-item>
<el-form-item label="内容类型">{{ getNameByValue(form.type, materialType) }}</el-form-item>
<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-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>
</el-select>
</el-form-item>
<el-form-item label="使用场景" prop="scenario_type">
<el-radio-group v-model="form.scenario_type">
<el-radio v-for="item in materialUsageList" :key="item.id" :label="item.value">{{ item.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="使用人员" prop="personnel_type">
<el-radio-group v-model="form.personnel_type">
<el-radio v-for="item in materialUsersList" :key="item.id" :label="item.value">{{ item.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="内容投放渠道" prop="channel">
<el-radio-group v-model="form.channel">
<el-radio v-for="item in connectionList" :key="item.id" :label="item.id">{{ item.type_name }}</el-radio>
</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-form-item>
</el-form>
<el-row justify="center">
<el-button type="primary" @click="handlePrev">上一步</el-button>
<el-button type="primary" @click="handleNext">下一步</el-button>
</el-row>
</el-card>
</template>
<style lang="scss">
.info-form {
display: flex;
justify-content: space-evenly;
.el-form-item {
margin-bottom: 0;
}
}
</style>
<script setup>
import { useMapStore } from '@/stores/map'
import { useConnection } from '../composables/useConnection'
import { useIndustry } from '../composables/useIndustry'
import { getNameByValue, materialMethodList, materialUsageList, materialUsersList } from '@/utils/dictionary'
import AppUpload from '@/components/base/AppUpload.vue'
const props = defineProps(['data'])
const materialType = useMapStore().getMapValuesByKey('experiment_marketing_material_type')
const { connectionList } = useConnection()
const { industryList } = useIndustry()
const typeName = computed(() => {
return getNameByValue(props.data.type, materialType)
})
</script>
<template>
<el-dialog title="查看资料信息" width="800px">
<el-form label-suffix=":" inline class="info-form">
<el-form-item label="内容名称">{{ data.name }}</el-form-item>
<el-form-item label="内容类型">{{ typeName }}</el-form-item>
<el-form-item label="创作方式">{{ getNameByValue(data.way, materialMethodList) }}</el-form-item>
</el-form>
<el-divider></el-divider>
<el-form label-suffix=":" label-width="120" ref="formRef">
<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>
</el-select>
</el-form-item>
<el-form-item label="使用场景" prop="scenario_type">
<el-radio-group :model-value="data.scenario_type">
<el-radio v-for="item in materialUsageList" :key="item.id" :label="item.value">{{ item.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="使用人员" prop="personnel_type">
<el-radio-group :model-value="data.personnel_type">
<el-radio v-for="item in materialUsersList" :key="item.id" :label="item.value">{{ item.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="内容投放渠道" prop="channel">
<el-radio-group :model-value="data.channel">
<el-radio v-for="item in connectionList" :key="item.id" :label="item.id">{{ item.type_name }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="关键点" prop="key_points">
<el-input type="textarea" :rows="4" :model-value="data.key_points" placeholder="请输入内容的核心内容或者关键点,多个请使用英文“,”号进行隔离。" />
</el-form-item>
<el-form-item :label="`${typeName}资源`" prop="content">
<template v-if="data.type == 1">
<!-- 文本 -->
<el-input type="textarea" rows="6" :model-value="data.content"></el-input>
</template>
<template v-if="['2', '6', '7', '8'].includes(data.type)">
<!-- 图片|二维码|小程序|卡券 -->
<AppUpload :model-value="data.content" accept="image/*" disabled></AppUpload>
</template>
<template v-if="data.type == 3">
<!-- 语音 -->
<div>
<audio :src="data.content" controls v-if="data.content"></audio>
</div>
</template>
<template v-if="data.type == 4">
<!-- 视频 -->
<div>
<video controls :src="data.content" style="width: 100%" v-if="data.content"></video>
</div>
</template>
<template v-if="data.type == 5">
<el-input :model-value="data.content" placeholder="请输入">
<template #prepend>https://</template>
<template #append v-if="data.content">
<a :href="data.content" target="_blank">查看</a>
</template>
</el-input>
</template></el-form-item
>
</el-form>
<template #footer>
<el-button type="primary" @click="$emit('update:modelValue')">关闭</el-button>
</template>
</el-dialog>
</template>
<style lang="scss">
.info-form {
display: flex;
justify-content: space-evenly;
.el-form-item {
margin-bottom: 0;
}
}
</style>
import { fetchEventSource } from '@fortaine/fetch-event-source'
import { getAIUsage } from '../api'
import type { Message } from '../types'
import { ElMessage } from 'element-plus'
export function useChat(options: any) {
const messages = ref<Message[]>([])
const chatId = ref<string | null>(null)
const isLoading = ref(false)
const usages = ref({
chart_count: 0,
ai_creation_count: 2,
ai_polish_count: 0,
ai_expand_count: 0,
ai_refresh_count: 0,
experiment_id: 121,
chart_max_count: 20,
ai_creation_max_count: 5,
ai_polish_max_count: 5,
ai_expand_max_count: 5,
ai_refresh_max_count: 5
})
async function fetchUsages() {
const res = await getAIUsage(options)
usages.value = res.data.detail
}
onMounted(() => {
fetchUsages()
})
async function post(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, chart_id: chatId.value }),
async onopen(response) {
if (response.ok) {
return
} else {
throw response
}
},
onmessage(res) {
// console.log(res.data)
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)
let content = message.content || ''
if (message.content === '\n') content = '<br/>'
if (messageIndex === -1) {
messages.value.push({ conversationId, role: 'bot', content })
} 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
}
})
}
return { usages, chatId, messages, post, isLoading }
}
import { getConnectionList } from '../api'
export interface ConnectionType {
id: string
type: string
type_name: string
}
const connectionList = ref<ConnectionType[]>([])
export function useConnection() {
async function fetchConnectionList() {
const res = await getConnectionList()
connectionList.value = res.data.items
}
onMounted(() => {
if (!connectionList.value?.length) fetchConnectionList()
})
return { fetchConnectionList, connectionList }
}
import { getIndustryList } from '../api'
export interface IndustryType {
id: string
name: string
pinyin: string
}
const industryList = ref<IndustryType[]>([])
export function useIndustry() {
async function fetchIndustryList() {
const res = await getIndustryList()
industryList.value = res.data.items
}
onMounted(() => {
if (!industryList.value?.length) fetchIndustryList()
})
return { fetchIndustryList, industryList }
}
import type { RouteRecordRaw } from 'vue-router'
import Layout from '@/components/layout/Index.vue'
const routes: RouteRecordRaw[] = [
{
path: '/material',
component: Layout,
children: [
{ path: '', component: () => import('./views/Index.vue') },
{ path: 'create', component: () => import('./views/Update.vue'), props: { action: 'create' } },
{ path: 'update', component: () => import('./views/Update.vue'), props: { action: 'update' } },
{ path: 'view', component: () => import('./views/Update.vue'), props: { action: 'view' } }
]
}
]
export { routes }
export interface Operator {
id: string
username: string
nickname: string
real_name: string
avatar: string
}
// 资料管理
export interface MaterialProp {
id: string
type: string
status: string
name: string
way: string
industry_id: string
scenario_type: string
personnel_type: string
channel: string
key_points: string
content: string
updated_operator: Operator
created_operator: Operator
created_time: string
updated_time: string
}
export interface Message {
role: string
content: string
chatId?: number
conversationId?: string
complete?: boolean
finish?: boolean
type?: string
}
<script setup lang="ts">
import type { MaterialProp } from '../types'
import { Plus } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
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'
const ViewDialog = defineAsyncComponent(() => import('../components/ViewDialog.vue'))
const materialTypeList = useMapStore().getMapValuesByKey('experiment_marketing_material_type')
const router = useRouter()
const route = useRoute()
const appList = ref<InstanceType<typeof AppList> | null>(null)
// 资料类型
const materialType = ref('')
// 列表配置
const listOptions = computed(() => {
return {
remote: {
httpRequest: getMaterialList,
params: { name: '', id: '', status: '', way: '', type: route.query.type || '', updated_operator: '' },
beforeRequest(params: any) {
materialType.value = params.type
return params
}
},
filters: [
{
type: 'select',
prop: 'type',
placeholder: '请选择资料类型',
options: materialTypeList
},
{
type: 'select',
prop: 'status',
placeholder: '请选择资料状态',
options: [
{ label: '有效', value: '1' },
{ label: '失效', value: '0' }
]
},
{
type: 'select',
prop: 'way',
placeholder: '请选择创作方式',
options: materialMethodList
},
{ type: 'input', prop: 'name', placeholder: '请输入资料名称' },
{ type: 'input', prop: 'id', placeholder: '请输入资料ID' },
{ type: 'input', prop: 'updated_operator', placeholder: '更新人' }
],
columns: [
{ label: '序号', type: 'index', width: 60 },
{
label: '资料类型',
prop: 'type_name',
width: 100,
computed: ({ row }: { row: MaterialProp }) => {
return getNameByValue(row.type, materialTypeList)
}
},
{ label: '资料名称', prop: 'name' },
{ label: '资料ID', prop: 'id' },
{
label: '创作方式',
prop: 'way',
computed: ({ row }: { row: MaterialProp }) => {
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: 'status_name',
width: 90,
slots: 'table-status'
},
{
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: '操作', slots: 'table-x', width: 240 }
]
}
})
// 刷新
function handleRefresh() {
appList.value?.refetch()
}
let dialogVisible = $ref(false)
// 编辑
let currentRow = ref<MaterialProp>()
const handleEdit = function (row: MaterialProp) {
router.push({ path: 'material/update', query: { id: row.id } })
}
// 查看
const handleView = function (row: MaterialProp) {
materialType.value = row.type
currentRow.value = row
dialogVisible = true
}
// 删除
async function handleRemove(row: MaterialProp) {
await deleteMaterial({ id: row.id })
ElMessage({ message: '删除成功', type: 'success' })
handleRefresh()
}
// 修改状态
async function handleChangeStatus(row: MaterialProp) {
const status = row.status === '1' ? '0' : '1'
await updateMaterial({ id: row.id, status })
ElMessage({ message: '更新成功', type: 'success' })
return true
}
</script>
<template>
<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>
</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>
</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>
</template>
</AppList>
<ViewDialog :data="currentRow" v-if="dialogVisible" v-model="dialogVisible"></ViewDialog>
</AppCard>
</template>
<script setup lang="ts">
import { ElMessage } from 'element-plus'
import { createMaterial, updateMaterial, getMaterial } from '../api'
const props = defineProps<{ action: string }>()
const StepOne = defineAsyncComponent(() => import('../components/StepOne.vue'))
const StepTwo = defineAsyncComponent(() => import('../components/StepTwo.vue'))
const StepThree = defineAsyncComponent(() => import('../components/StepThree.vue'))
const router = useRouter()
const route = useRoute()
const activeName = ref(1)
const form: any = reactive({
id: '',
type: '1',
name: '',
way: '2',
industry_id: '1',
scenario_type: '1',
personnel_type: '1',
channel: '',
key_points: '',
content: ''
})
const detail = ref()
async function fetchInfo() {
const res = await getMaterial({ id: route.query.id as string })
detail.value = res.data.detail
Object.assign(form, res.data.detail)
}
onMounted(() => {
if (props.action === 'update' || props.action === 'view') fetchInfo()
})
// 上一步
function handlePrev() {
activeName.value = activeName.value - 1
}
// 下一步
function handleNext() {
activeName.value = activeName.value + 1
}
// 下一步提交
async function handleNextAndSubmit() {
if (!form.id && form.way == 1) {
const res = await createMaterial(form)
form.id = res.data.id
}
handleNext()
}
// 提交
async function handleSubmit() {
form.id ? await handleUpdate() : await handleCreate()
}
// 创建
async function handleCreate() {
await createMaterial(form)
ElMessage.success('创建成功')
router.replace('/material')
}
// 修改
async function handleUpdate() {
await updateMaterial(form)
ElMessage.success('修改成功')
router.replace('/material')
}
</script>
<template>
<AppCard title="营销内容创作" full class="material">
<el-tabs v-model="activeName" stretch class="material-tabs">
<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>
<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步" style="max-width: 1000px; margin: 0 auto" :name="3" disabled>
<StepThree v-model="form" :action="action" @prev="handlePrev" @submit="handleSubmit"></StepThree>
</el-tab-pane>
</el-tabs>
</AppCard>
</template>
<style lang="scss">
.material-tabs {
.el-tabs__header {
width: 300px;
margin: 0 auto 20px;
}
.el-tabs__nav-wrap::after {
display: none;
}
}
.material {
.el-card {
background-color: rgba(242, 242, 242, 0.49);
}
}
</style>
...@@ -2,10 +2,6 @@ import type { RouteRecordRaw } from 'vue-router' ...@@ -2,10 +2,6 @@ import type { RouteRecordRaw } from 'vue-router'
import Layout from '@/components/layout/Index.vue' import Layout from '@/components/layout/Index.vue'
const routes: RouteRecordRaw[] = [ const routes: RouteRecordRaw[] = [
{
path: '/material',
redirect: '/material/text'
},
{ {
path: '/material/text', path: '/material/text',
component: Layout, component: Layout,
......
...@@ -70,39 +70,46 @@ const studentMenus: IMenuItem[] = [ ...@@ -70,39 +70,46 @@ const studentMenus: IMenuItem[] = [
children: [ children: [
{ {
name: '文本资料管理', name: '文本资料管理',
path: '/material/text', path: '/material?type=1',
icon: markRaw(IconText) icon: markRaw(IconText),
tag: 'v1-experiment-marketing-material-list'
}, },
{ {
name: '图片资料管理', name: '图片资料管理',
path: '/material/image', path: '/material?type=2',
icon: markRaw(IconImage) icon: markRaw(IconImage),
tag: 'v1-experiment-marketing-material-list'
}, },
{ {
name: '语音资料管理', name: '语音资料管理',
path: '/material/audio', path: '/material?type=3',
icon: markRaw(IconAudio) icon: markRaw(IconAudio),
tag: 'v1-experiment-marketing-material-list'
}, },
{ {
name: '视频资料管理', name: '视频资料管理',
path: '/material/video', path: '/material?type=4',
icon: markRaw(IconVideo) icon: markRaw(IconVideo),
tag: 'v1-experiment-marketing-material-list'
}, },
{ name: 'H5资料管理', path: '/material/h5', icon: markRaw(IconH5) }, { name: 'H5资料管理', path: '/material?type=5', icon: markRaw(IconH5), tag: 'v1-experiment-marketing-material-list' },
{ {
name: '二维码资料管理', name: '二维码资料管理',
path: '/material/qrcode', path: '/material?type=6',
icon: markRaw(IconQrcode) icon: markRaw(IconQrcode),
tag: 'v1-experiment-marketing-material-list'
}, },
{ {
name: '小程序资料管理', name: '小程序资料管理',
path: '/material/mini', path: '/material?type=7',
icon: markRaw(IconMiniProgram) icon: markRaw(IconMiniProgram),
tag: 'v1-experiment-marketing-material-list'
}, },
{ {
name: '卡券资料管理', name: '卡券资料管理',
path: '/material/card', path: '/material?type=8',
icon: markRaw(IconCard) icon: markRaw(IconCard),
tag: 'v1-experiment-marketing-material-list'
} }
] ]
}, },
...@@ -148,26 +155,6 @@ const adminMenus: IMenuItem[] = [ ...@@ -148,26 +155,6 @@ const adminMenus: IMenuItem[] = [
icon: markRaw(IconEvent), icon: markRaw(IconEvent),
tag: 'v1-experiment-meta-event' tag: 'v1-experiment-meta-event'
} }
// {
// name: '元数据管理',
// path: '/metadata',
// icon: markRaw(IconMetadata),
// tag: 'v1-experiment-meta',
// children: [
// {
// name: '用户属性管理',
// path: '/metadata/user',
// icon: markRaw(IconUser2),
// tag: 'v1-experiment-meta-member'
// },
// {
// name: '事件属性管理',
// path: '/metadata/event',
// icon: markRaw(IconEvent),
// tag: 'v1-experiment-meta-event'
// }
// ]
// },
] ]
}, },
{ {
...@@ -203,44 +190,44 @@ const adminMenus: IMenuItem[] = [ ...@@ -203,44 +190,44 @@ const adminMenus: IMenuItem[] = [
children: [ children: [
{ {
name: '文本资料管理', name: '文本资料管理',
path: '/material/text', path: '/material?type=1',
icon: markRaw(IconText), icon: markRaw(IconText),
tag: 'v1-experiment-marketing-material-list' tag: 'v1-experiment-marketing-material-list'
}, },
{ {
name: '图片资料管理', name: '图片资料管理',
path: '/material/image', path: '/material?type=2',
icon: markRaw(IconImage), icon: markRaw(IconImage),
tag: 'v1-experiment-marketing-material-list' tag: 'v1-experiment-marketing-material-list'
}, },
{ {
name: '语音资料管理', name: '语音资料管理',
path: '/material/audio', path: '/material?type=3',
icon: markRaw(IconAudio), icon: markRaw(IconAudio),
tag: 'v1-experiment-marketing-material-list' tag: 'v1-experiment-marketing-material-list'
}, },
{ {
name: '视频资料管理', name: '视频资料管理',
path: '/material/video', path: '/material?type=4',
icon: markRaw(IconVideo), icon: markRaw(IconVideo),
tag: 'v1-experiment-marketing-material-list' tag: 'v1-experiment-marketing-material-list'
}, },
{ name: 'H5资料管理', path: '/material/h5', icon: markRaw(IconH5), tag: 'v1-experiment-marketing-material-list' }, { name: 'H5资料管理', path: '/material?type=5', icon: markRaw(IconH5), tag: 'v1-experiment-marketing-material-list' },
{ {
name: '二维码资料管理', name: '二维码资料管理',
path: '/material/qrcode', path: '/material?type=6',
icon: markRaw(IconQrcode), icon: markRaw(IconQrcode),
tag: 'v1-experiment-marketing-material-list' tag: 'v1-experiment-marketing-material-list'
}, },
{ {
name: '小程序资料管理', name: '小程序资料管理',
path: '/material/mini', path: '/material?type=7',
icon: markRaw(IconMiniProgram), icon: markRaw(IconMiniProgram),
tag: 'v1-experiment-marketing-material-list' tag: 'v1-experiment-marketing-material-list'
}, },
{ {
name: '卡券资料管理', name: '卡券资料管理',
path: '/material/card', path: '/material?type=8',
icon: markRaw(IconCard), icon: markRaw(IconCard),
tag: 'v1-experiment-marketing-material-list' tag: 'v1-experiment-marketing-material-list'
} }
......
...@@ -106,3 +106,25 @@ export const wayList = [ ...@@ -106,3 +106,25 @@ export const wayList = [
{ label: '总次数 ', value: '2' }, { label: '总次数 ', value: '2' },
{ label: '平均金额 ', value: '3' } { label: '平均金额 ', value: '3' }
] ]
export const materialMethodList = [
{ label: '离线上传 ', value: '2' },
{ label: '在线AI ', value: '1' }
]
// 使用场景
export const materialUsageList = [
{ label: '用户拉新 ', value: '1' },
{ label: '产品宣传 ', value: '2' },
{ label: '用户关怀 ', value: '3' },
{ label: '系统通知 ', value: '4' }
]
// 使用人员
export const materialUsersList = [
{ label: '销售人员 ', value: '1' },
{ label: '品牌人员 ', value: '2' },
{ label: '客服人员 ', value: '3' },
{ label: '运维人员 ', value: '4' },
{ label: '机器系统 ', value: '5' }
]
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论