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

feat: 成绩管理支持全媒体运营

上级 602d12af
<script setup>
import { useMapStore } from '@/stores/map'
import {
getNameByValue,
importType,
requiredType,
deliveryMode,
deliveryTime,
shippingTemplate,
afterSalesPolicy,
} from '@/utils/dictionary'
import ScoreCard from './ScoreCard.vue'
import ScoreCardLive from './ScoreCardLive.vue'
import Preview from '@/components/Preview.vue'
const statusList = useMapStore().getMapValuesByKey('system_status')
const props = defineProps({
detail: { type: Object, default: () => ({}) },
})
const scoreDetails = defineModel('scoreDetails', {
required: true,
default: () => ({}),
})
// 定义需要的所有 key 及其默认值
const requiredKeys = {
commodity_type: { score: null, comment: '' },
commodity_attr: { score: null, comment: '' },
commodity: { score: null, comment: '' },
speech: { score: null, comment: '' },
practice_record1: { score: null, comment: '' },
improvement_plan: { score: null, comment: '' },
practice_record2: { score: null, comment: '' },
report: { score: null, comment: '' },
}
// 确保所有必需的 key 都存在,如果不存在则初始化
watchEffect(() => {
Object.keys(requiredKeys).forEach((key) => {
if (!scoreDetails.value[key]) {
scoreDetails.value[key] = { ...requiredKeys[key] }
} else if (!scoreDetails.value[key].hasOwnProperty('score') || !scoreDetails.value[key].hasOwnProperty('comment')) {
// 如果存在但结构不完整(缺少 score 或 comment),则合并默认值
scoreDetails.value[key] = { ...requiredKeys[key], ...scoreDetails.value[key] }
}
})
})
defineEmits(['save'])
// 商品类别
const categoryTableOptions = {
columns: [
{ label: '商品品类名称', prop: 'name', align: 'left' },
{ label: '层级', prop: 'level' },
{
label: '状态',
prop: 'status',
computed({ row }) {
const color = row.status === '1' ? 'var(--main-success-color)' : 'var(--main-color)'
return `<span style="color: ${color}">${getNameByValue(row.status, statusList)}</span>`
},
},
],
}
// 商品属性
const attrsTableOptions = {
columns: [
{ label: '序号', type: 'index', width: 60 },
{ label: '商品属性名称', prop: 'name' },
{ label: '所属商品品类', prop: 'live_commodity_type_full_name' },
{
label: '重要性',
prop: 'is_importance',
computed({ row }) {
return getNameByValue(row.is_importance, importType)
},
},
{
label: '必要性',
prop: 'is_essential',
computed({ row }) {
return getNameByValue(row.is_essential, requiredType)
},
},
{
label: '状态',
prop: 'status',
computed({ row }) {
const color = row.status === '1' ? 'var(--main-success-color)' : 'var(--main-color)'
return `<span style="color: ${color}">${getNameByValue(row.status, statusList)}</span>`
},
},
{ label: '更新时间', prop: 'updated_time' },
],
}
// 商品管理
const productList = computed(() => {
return props.detail.live_data?.commodities?.map((item) => {
try {
item.info = JSON.parse(item.info)
item.info.delivery_mode_name = getNameByValue(item.info.delivery_mode, deliveryMode)
item.info.delivery_time_name = getNameByValue(item.info.delivery_time, deliveryTime)
item.info.shipping_template_name = getNameByValue(item.info.shipping_template, shippingTemplate)
item.info.after_sales_policy_name = getNameByValue(item.info.after_sales_policy, afterSalesPolicy)
item.live_commodity_attrs = JSON.parse(item.live_commodity_attrs)
item.picture_34_addreses = JSON.parse(item.picture_34_addreses)
item.picture_addreses = JSON.parse(item.picture_addreses)
} catch (error) {
console.log(error)
}
console.log(item)
return item
})
})
const productTableOptions = {
columns: [
{
type: 'expand',
slots: 'table-expand',
},
{ label: '序号', type: 'index', width: 60 },
{ label: '商品标题', prop: 'title' },
{ label: '所属商品品类', prop: 'live_commodity_type_full_name' },
{ label: '参考价(¥)', prop: 'info.reference_price' },
{ label: '发货模式', prop: 'info.delivery_mode_name' },
{ label: '发货时效', prop: 'info.delivery_time_name' },
{ label: '运费模板', prop: 'info.shipping_template_name' },
{ label: '售后政策', prop: 'info.after_sales_policy_name' },
],
}
// 商品规格
const productSpecsTableOptions = {
columns: [
{ label: '规格名称', prop: 'name' },
{
label: '规格值',
prop: 'value',
computed({ row }) {
return row.values.join(' / ')
},
},
],
}
// 商品sku
const productSkuTableOptions = {
columns: [
{
label: '规格',
prop: 'name',
computed({ row }) {
return row.specs.join(' / ')
},
},
{ label: '价格(¥)', prop: 'price' },
{ label: '库存', prop: 'stock' },
],
}
// 直播话术
const talkTableOptions = {
columns: [
{ type: 'expand', slots: 'table-expand' },
{ label: '序号', type: 'index', width: 60 },
{ label: '直播话术名称', prop: 'name' },
{ label: '直播主题标题', prop: 'live_commodity_title' },
{
label: '话术时长',
prop: 'duration',
computed({ row }) {
return `${row.duration}分钟`
},
},
{ label: '所属直播主题品类', prop: 'live_commodity_type_full_name' },
],
}
const activeTab = ref(1)
const handleNext = () => {
activeTab.value++
}
</script>
<template>
<el-tabs stretch v-model="activeTab" class="score-tabs">
<el-tab-pane label="商品品类管理" :name="1">
<ScoreCard
:maxScore="5"
:hasSaveButton="detail.status != '2'"
v-model:score="scoreDetails.commodity_type.score"
v-model:comment="scoreDetails.commodity_type.comment"
@save="$emit('save')"
@next="handleNext">
<AppList v-bind="categoryTableOptions" row-key="id" :data="detail.live_data.commodity_types" />
</ScoreCard>
</el-tab-pane>
<el-tab-pane label="商品属性管理" :name="2" lazy>
<ScoreCard
:maxScore="5"
:hasSaveButton="detail.status != '2'"
v-model:score="scoreDetails.commodity_attr.score"
v-model:comment="scoreDetails.commodity_attr.comment"
@save="$emit('save')"
@next="handleNext">
<AppList v-bind="attrsTableOptions" :data="detail.live_data.commodity_attrs" />
</ScoreCard>
</el-tab-pane>
<el-tab-pane label="商品管理" :name="3" lazy>
<ScoreCard
:maxScore="15"
:hasSaveButton="detail.status != '2'"
v-model:score="scoreDetails.commodity.score"
v-model:comment="scoreDetails.commodity.comment"
@save="$emit('save')"
@next="handleNext">
<AppList v-bind="productTableOptions" :data="productList" :default-expand-all="productList.length === 1">
<template #table-expand="{ row }">
<div class="table-expand-box">
<el-form label-position="top">
<el-row>
<el-col :span="12">
<el-form-item label="主图">
<el-image
:src="item.url"
v-for="item in row.picture_addreses"
:key="item.url"
fit="cover"
preview-teleported
:preview-src-list="row.picture_addreses.map((item) => item.url)"
style="width: 100px; height: 100px; margin-right: 10px"></el-image>
</el-form-item>
<el-form-item label="主图3:4">
<el-image
:src="item.url"
v-for="item in row.picture_34_addreses"
:key="item.url"
fit="cover"
preview-teleported
:preview-src-list="row.picture_addreses.map((item) => item.url)"
style="width: 100px; height: 134px; margin-right: 10px"></el-image>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="主图视频">
<video
:src="row.video_url"
controls
style="width: 100%; height: 282px"
v-if="row.video_url"></video>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="商品规格">
<AppList v-bind="productSpecsTableOptions" :data="row.info?.specs" style="width: 100%"></AppList>
</el-form-item>
<el-form-item label="价格与库存">
<AppList v-bind="productSkuTableOptions" :data="row.info?.sku" style="width: 100%"></AppList>
</el-form-item>
</el-form>
</div>
</template>
</AppList>
</ScoreCard>
</el-tab-pane>
<el-tab-pane label="直播话术管理" :name="4" lazy>
<ScoreCard
:maxScore="15"
:hasSaveButton="detail.status != '2'"
v-model:score="scoreDetails.speech.score"
v-model:comment="scoreDetails.speech.comment"
@save="$emit('save')"
@next="handleNext">
<AppList
v-bind="talkTableOptions"
:data="detail.live_data.speeches"
:default-expand-all="detail.live_data.speeches.length === 1">
<template #table-expand="{ row }">
<div class="table-expand-box">
<el-form label-position="top">
<el-form-item label="直播主题卖点">
<div class="form-box" v-html="row.selling_point"></div>
</el-form-item>
<el-form-item label="营销活动">
<div class="form-box" v-html="row.marketing_campaign"></div>
</el-form-item>
<el-form-item label="直播话术内容">
<div class="form-box" v-html="row.content"></div>
</el-form-item>
</el-form>
</div>
</template>
</AppList>
</ScoreCard>
</el-tab-pane>
<el-tab-pane label="首次直播演练" :name="5" lazy>
<ScoreCard
:maxScore="20"
:hasSaveButton="detail.status != '2'"
v-model:score="scoreDetails.practice_record1.score"
v-model:comment="scoreDetails.practice_record1.comment"
@save="$emit('save')"
@next="handleNext">
<ScoreCardLive :data="detail.live_data.practice_records[0]" v-if="detail.live_data.practice_records[0]" />
<el-empty v-else />
</ScoreCard>
</el-tab-pane>
<el-tab-pane label="直播复盘分析" :name="6" lazy>
<ScoreCard
:maxScore="10"
:hasSaveButton="detail.status != '2'"
v-model:score="scoreDetails.improvement_plan.score"
v-model:comment="scoreDetails.improvement_plan.comment"
@save="$emit('save')"
@next="handleNext">
<el-form label-position="top" v-if="detail.live_data.practice_records[0]?.improvement_plan">
<el-form-item label="改进方案">
<div class="form-box" v-html="detail.live_data.practice_records[0]?.improvement_plan"></div>
</el-form-item>
</el-form>
<el-empty v-else />
</ScoreCard>
</el-tab-pane>
<el-tab-pane label="二次直播演练" :name="7" lazy>
<ScoreCard
:maxScore="15"
:hasSaveButton="detail.status != '2'"
v-model:score="scoreDetails.practice_record2.score"
v-model:comment="scoreDetails.practice_record2.comment"
@save="$emit('save')"
@next="handleNext">
<ScoreCardLive :data="detail.live_data.practice_records[1]" v-if="detail.live_data.practice_records[1]" />
<el-empty v-else />
</ScoreCard>
</el-tab-pane>
<el-tab-pane label="直播总结汇报" :name="8" lazy>
<ScoreCard
:maxScore="15"
:hasSaveButton="detail.status != '2'"
:hasNextButton="false"
v-model:score="scoreDetails.report.score"
v-model:comment="scoreDetails.report.comment"
@save="$emit('save')"
@next="handleNext">
<template v-if="detail.live_data.reports.length > 0">
<div v-for="item in detail.live_data.reports" :key="item.report_url" style="height: 600px">
<Preview :url="item.report_url" />
</div>
</template>
<el-empty v-else />
</ScoreCard>
</el-tab-pane>
</el-tabs>
</template>
<style lang="scss" scoped>
.score-tabs {
:deep(.el-tabs__header) {
width: 1000px;
margin: 0 auto 20px;
}
:deep(.el-tabs__nav-wrap::after) {
display: none;
}
}
.score-header {
display: flex;
align-items: center;
.el-form {
flex: 1;
}
}
.score-box {
width: 100px;
border: 1px solid #dcdfe6;
padding: 10px;
border-radius: 10px;
font-size: 30px;
color: var(--main-color);
display: flex;
align-items: center;
justify-content: center;
}
.table-expand-box {
border: 1px solid #dcdfe6;
padding: 20px;
border-radius: 10px;
}
.form-box {
width: 100%;
border: 1px solid #dcdfe6;
padding: 10px;
border-radius: 6px;
}
</style>
<script setup>
import ModuleCard from './ModuleCard.vue'
import ScoreCardSub from './ScoreCardSub.vue'
const props = defineProps({
detail: { type: Object, default: () => ({}) },
})
const scoreDetails = defineModel('scoreDetails', {
required: true,
default: () => ({}),
})
// 定义需要的所有 key 及其默认值
const requiredKeys = {
plan_theme: { score: null, comment: '' },
plan_path: { score: null, comment: '' },
plan_difficulty: { score: null, comment: '' },
plan_framework: { score: null, comment: '' },
audiovisual_title: { score: null, comment: '' },
audiovisual_intro: { score: null, comment: '' },
audiovisual_content: { score: null, comment: '' },
audiovisual_images: { score: null, comment: '' },
audiovisual_video: { score: null, comment: '' },
flow_platforms: { score: null, comment: '' },
flow_traffic: { score: null, comment: '' },
flow_risk: { score: null, comment: '' },
}
// 确保所有必需的 key 都存在,如果不存在则初始化
watchEffect(() => {
Object.keys(requiredKeys).forEach((key) => {
if (!scoreDetails.value[key]) {
scoreDetails.value[key] = { ...requiredKeys[key] }
} else if (!scoreDetails.value[key].hasOwnProperty('score') || !scoreDetails.value[key].hasOwnProperty('comment')) {
// 如果存在但结构不完整(缺少 score 或 comment),则合并默认值
scoreDetails.value[key] = { ...requiredKeys[key], ...scoreDetails.value[key] }
}
})
})
defineEmits(['save'])
const json = computed(() => {
try {
return JSON.parse(props.detail?.operation_data?.content || '{}')
} catch (error) {
console.log(error)
return { plan: {}, audiovisual: {}, flow: {} }
}
})
const activeTab = ref(1)
const handleNext = () => {
activeTab.value++
}
</script>
<template>
<el-tabs stretch v-model="activeTab" class="score-tabs">
<el-tab-pane label="创意策划方案" :name="1">
<ModuleCard :maxScore="20" :hasSaveButton="detail.status != '2'" @next="handleNext" @save="$emit('save')">
<ScoreCardSub
title="全媒体运营的主题(方向)描述(4分)"
:maxScore="4"
v-model:score="scoreDetails.plan_theme.score"
v-model:comment="scoreDetails.plan_theme.comment">
<div class="html-content" v-html="json.plan?.theme"></div>
</ScoreCardSub>
<ScoreCardSub
title="运营的渠道路径(5分)"
:maxScore="5"
v-model:score="scoreDetails.plan_path.score"
v-model:comment="scoreDetails.plan_path.comment">
<div class="html-content" v-html="json.plan?.path"></div>
</ScoreCardSub>
<ScoreCardSub
title="运营的重点和难点(5分)"
:maxScore="5"
v-model:score="scoreDetails.plan_difficulty.score"
v-model:comment="scoreDetails.plan_difficulty.comment">
<div class="html-content" v-html="json.plan?.path"></div>
</ScoreCardSub>
<ScoreCardSub
title="运营策划框架方案(从媒介技术、加工匹配、传播、反馈等,要点式表述)(6分)"
:maxScore="6"
v-model:score="scoreDetails.plan_framework.score"
v-model:comment="scoreDetails.plan_framework.comment">
<div class="html-content" v-html="json.plan?.framework"></div>
</ScoreCardSub>
</ModuleCard>
</el-tab-pane>
<el-tab-pane label="视听运营" :name="2">
<ModuleCard :maxScore="20" :hasSaveButton="detail.status != '2'" @next="handleNext" @save="$emit('save')">
<ScoreCardSub
title="综合稿件标题(5分)"
:maxScore="5"
v-model:score="scoreDetails.audiovisual_title.score"
v-model:comment="scoreDetails.audiovisual_title.comment">
<div class="html-content" v-html="json.audiovisual?.title"></div>
</ScoreCardSub>
<ScoreCardSub
title="导语(5分)"
:maxScore="5"
v-model:score="scoreDetails.audiovisual_intro.score"
v-model:comment="scoreDetails.audiovisual_intro.comment">
<div class="html-content" v-html="json.audiovisual?.intro"></div>
</ScoreCardSub>
<ScoreCardSub
title="正文报道文字(不少于200字)(5分)"
:maxScore="5"
v-model:score="scoreDetails.audiovisual_content.score"
v-model:comment="scoreDetails.audiovisual_content.comment">
<div class="html-content" v-html="json.audiovisual?.content"></div>
</ScoreCardSub>
<ScoreCardSub
title="主题活动(场景)现场照片(不少于2张)(10分)"
:maxScore="10"
v-model:score="scoreDetails.audiovisual_images.score"
v-model:comment="scoreDetails.audiovisual_images.comment">
<div class="html-content">
<ul class="image-list" v-if="json.audiovisual?.images">
<li v-for="url in json.audiovisual?.images" :key="url">
<img :src="url" />
</li>
</ul>
</div>
</ScoreCardSub>
<ScoreCardSub
title="原创视频短片(不少于60秒)。该视频至少包括:字幕、音乐或音效、一段同期声采访(或现场声)等要素。(30分)"
:maxScore="30"
v-model:score="scoreDetails.audiovisual_video.score"
v-model:comment="scoreDetails.audiovisual_video.comment">
<div class="html-content">
<video class="video-player" v-if="json.audiovisual?.video" :src="json.audiovisual?.video" controls></video>
</div>
</ScoreCardSub>
</ModuleCard>
</el-tab-pane>
<el-tab-pane label="流量运营" :name="3">
<ModuleCard :maxScore="20" :hasSaveButton="detail.status != '2'" @next="handleNext" @save="$emit('save')">
<ScoreCardSub
title="上述全媒体综合文稿拟分发的媒体平台并逐一说明理由(10分)"
:maxScore="10"
v-model:score="scoreDetails.flow_platforms.score"
v-model:comment="scoreDetails.flow_platforms.comment">
<div class="html-content" v-html="json.flow?.platforms"></div>
</ScoreCardSub>
<ScoreCardSub
title="流量运营及直播运营的预期成效分析(5分)"
:maxScore="5"
v-model:score="scoreDetails.flow_traffic.score"
v-model:comment="scoreDetails.flow_traffic.comment">
<div class="html-content" v-html="json.flow?.traffic"></div>
</ScoreCardSub>
<ScoreCardSub
title="运营风险管控解析(5分)"
:maxScore="5"
v-model:score="scoreDetails.flow_risk.score"
v-model:comment="scoreDetails.flow_risk.comment">
<div class="html-content" v-html="json.flow?.risk"></div>
</ScoreCardSub>
</ModuleCard>
</el-tab-pane>
</el-tabs>
</template>
<style lang="scss" scoped>
.html-content {
padding: 20px;
border-radius: 10px;
border: 1px solid #dcdfe6;
background-color: #dcdfe6;
.image-list {
display: flex;
flex-wrap: wrap;
gap: 20px;
li {
position: relative;
width: 180px;
height: 240px;
background-color: #fff;
border: 1px solid #eee;
img {
width: 100%;
height: 100%;
object-fit: contain;
}
.el-icon {
display: none;
position: absolute;
top: 10px;
right: 10px;
cursor: pointer;
color: #999;
font-size: 22px;
}
&:hover {
.el-icon {
display: block;
}
}
}
}
.video-player {
max-height: 400px;
}
}
</style>
<script setup lang="ts">
withDefaults(
defineProps<{
hasSaveButton?: boolean
hasNextButton?: boolean
maxScore: number
}>(),
{ hasSaveButton: true, hasNextButton: true }
)
const emits = defineEmits<{
(e: 'save'): void
(e: 'next'): void
}>()
</script>
<template>
<div class="module-card">
<div class="module-card-title">
本模块分值:<span>{{ maxScore }}</span>
</div>
<div class="module-card-form">
<el-button type="primary" auto-insert-space @click="emits('save')" v-if="hasSaveButton">保存</el-button>
<el-button type="primary" plain auto-insert-space @click="emits('next')" v-if="hasNextButton">下一步</el-button>
</div>
<div class="module-card-content">
<slot></slot>
</div>
</div>
</template>
<style lang="scss" scoped>
.module-card {
position: relative;
display: flex;
flex-direction: column;
gap: 20px;
border-radius: 10px;
border: 1px solid #dcdfe6;
margin: 20px 0 40px;
}
.module-card-title {
position: absolute;
left: 20px;
top: 0;
background-color: #fff;
font-size: 16px;
font-weight: bold;
transform: translateY(-50%);
padding: 0 10px;
span {
font-size: 18px;
color: var(--main-color);
}
}
.module-card-form {
display: flex;
justify-content: flex-end;
gap: 10px;
padding: 30px 30px 0;
}
.module-card-content {
border-top: 1px solid #dcdfe6;
padding: 30px;
}
</style>
<script setup lang="ts">
defineProps<{ title: string; maxScore: number }>()
const score = defineModel<number | undefined>('score', { required: true })
const comment = defineModel<string>('comment', { required: true })
</script>
<template>
<div class="score-card">
<div class="score-card-title">
{{ title }}
</div>
<div class="score-card-form">
<el-form label-suffix=":" label-width="46px">
<el-form-item label="得分">
<el-input-number v-model="score" :min="0" :max="maxScore" :controls="false" />
</el-form-item>
<el-form-item label="评语">
<el-input v-model="comment" type="textarea" :rows="6" />
</el-form-item>
</el-form>
</div>
<div class="score-card-content">
<slot></slot>
</div>
</div>
</template>
<style lang="scss" scoped>
.score-card {
position: relative;
display: flex;
flex-direction: column;
gap: 20px;
border-radius: 10px;
border: 1px solid #dcdfe6;
margin: 20px 0 40px;
}
.score-card-title {
position: absolute;
left: 20px;
top: 0;
background-color: #fff;
font-size: 16px;
font-weight: bold;
transform: translateY(-50%);
padding: 0 10px;
color: var(--main-color);
}
.score-card-form {
display: flex;
flex-direction: column;
gap: 10px;
padding: 40px 30px 0;
}
.score-card-content {
border-top: 1px solid #dcdfe6;
padding: 30px;
}
</style>
<script setup> <script setup>
import { getScoreDetail, updateScore } from '../api' import { getScoreDetail, updateScore } from '../api'
import { useMapStore } from '@/stores/map'
import {
getNameByValue,
importType,
requiredType,
deliveryMode,
deliveryTime,
shippingTemplate,
afterSalesPolicy,
} from '@/utils/dictionary'
import ScoreCard from '../components/ScoreCard.vue'
import ScoreCardLive from '../components/ScoreCardLive.vue'
import Preview from '@/components/Preview.vue'
import { ElMessageBox, ElMessage } from 'element-plus' import { ElMessageBox, ElMessage } from 'element-plus'
import { useChat } from '@ezijing/ai-vue' import { useChat } from '@ezijing/ai-vue'
import { generatePrompt } from '../prompt' import { generatePrompt } from '../prompt'
const CompetitionLive = defineAsyncComponent(() => import('../components/CompetitionLive.vue'))
const CompetitionOperations = defineAsyncComponent(() => import('../components/CompetitionOperations.vue'))
const route = useRoute() const route = useRoute()
const id = route.query.id const id = route.query.id
const { isLoading, generateText } = useChat({ provider: 'volcano' }) const { isLoading, generateText } = useChat({ provider: 'volcano' })
const statusList = useMapStore().getMapValuesByKey('system_status')
const detail = ref(null) const detail = ref(null)
const scoreDetails = reactive({ const scoreDetails = reactive({})
commodity_type: { score: null, comment: '' },
commodity_attr: { score: null, comment: '' },
commodity: { score: null, comment: '' },
speech: { score: null, comment: '' },
practice_record1: { score: null, comment: '' },
improvement_plan: { score: null, comment: '' },
practice_record2: { score: null, comment: '' },
report: { score: null, comment: '' },
})
const totalScore = computed(() => { const totalScore = computed(() => {
return Object.values(scoreDetails).reduce((acc, curr) => acc + (curr.score || 0), 0) return Object.values(scoreDetails).reduce((acc, curr) => acc + (curr.score || 0), 0)
...@@ -51,136 +29,6 @@ onMounted(() => { ...@@ -51,136 +29,6 @@ onMounted(() => {
fetchDetail() fetchDetail()
}) })
// 商品类别
const categoryTableOptions = {
columns: [
{ label: '商品品类名称', prop: 'name', align: 'left' },
{ label: '层级', prop: 'level' },
{
label: '状态',
prop: 'status',
computed({ row }) {
const color = row.status === '1' ? 'var(--main-success-color)' : 'var(--main-color)'
return `<span style="color: ${color}">${getNameByValue(row.status, statusList)}</span>`
},
},
],
}
// 商品属性
const attrsTableOptions = {
columns: [
{ label: '序号', type: 'index', width: 60 },
{ label: '商品属性名称', prop: 'name' },
{ label: '所属商品品类', prop: 'live_commodity_type_full_name' },
{
label: '重要性',
prop: 'is_importance',
computed({ row }) {
return getNameByValue(row.is_importance, importType)
},
},
{
label: '必要性',
prop: 'is_essential',
computed({ row }) {
return getNameByValue(row.is_essential, requiredType)
},
},
{
label: '状态',
prop: 'status',
computed({ row }) {
const color = row.status === '1' ? 'var(--main-success-color)' : 'var(--main-color)'
return `<span style="color: ${color}">${getNameByValue(row.status, statusList)}</span>`
},
},
{ label: '更新时间', prop: 'updated_time' },
],
}
// 商品管理
const productList = computed(() => {
return detail.value.live_data?.commodities?.map((item) => {
try {
item.info = JSON.parse(item.info)
item.info.delivery_mode_name = getNameByValue(item.info.delivery_mode, deliveryMode)
item.info.delivery_time_name = getNameByValue(item.info.delivery_time, deliveryTime)
item.info.shipping_template_name = getNameByValue(item.info.shipping_template, shippingTemplate)
item.info.after_sales_policy_name = getNameByValue(item.info.after_sales_policy, afterSalesPolicy)
item.live_commodity_attrs = JSON.parse(item.live_commodity_attrs)
item.picture_34_addreses = JSON.parse(item.picture_34_addreses)
item.picture_addreses = JSON.parse(item.picture_addreses)
} catch (error) {
console.log(error)
}
console.log(item)
return item
})
})
const productTableOptions = {
columns: [
{
type: 'expand',
slots: 'table-expand',
},
{ label: '序号', type: 'index', width: 60 },
{ label: '商品标题', prop: 'title' },
{ label: '所属商品品类', prop: 'live_commodity_type_full_name' },
{ label: '参考价(¥)', prop: 'info.reference_price' },
{ label: '发货模式', prop: 'info.delivery_mode_name' },
{ label: '发货时效', prop: 'info.delivery_time_name' },
{ label: '运费模板', prop: 'info.shipping_template_name' },
{ label: '售后政策', prop: 'info.after_sales_policy_name' },
],
}
// 商品规格
const productSpecsTableOptions = {
columns: [
{ label: '规格名称', prop: 'name' },
{
label: '规格值',
prop: 'value',
computed({ row }) {
return row.values.join(' / ')
},
},
],
}
// 商品sku
const productSkuTableOptions = {
columns: [
{
label: '规格',
prop: 'name',
computed({ row }) {
return row.specs.join(' / ')
},
},
{ label: '价格(¥)', prop: 'price' },
{ label: '库存', prop: 'stock' },
],
}
// 直播话术
const talkTableOptions = {
columns: [
{ type: 'expand', slots: 'table-expand' },
{ label: '序号', type: 'index', width: 60 },
{ label: '直播话术名称', prop: 'name' },
{ label: '直播主题标题', prop: 'live_commodity_title' },
{
label: '话术时长',
prop: 'duration',
computed({ row }) {
return `${row.duration}分钟`
},
},
{ label: '所属直播主题品类', prop: 'live_commodity_type_full_name' },
],
}
const activeTab = ref(1)
const handleAIScore = async () => { const handleAIScore = async () => {
console.log('AI一键评分') console.log('AI一键评分')
const result = await generateText({ const result = await generateText({
...@@ -198,20 +46,14 @@ const handleAIScore = async () => { ...@@ -198,20 +46,14 @@ const handleAIScore = async () => {
ElMessage.error('评分失败') ElMessage.error('评分失败')
return return
} }
console.log(result.content)
} }
const handleSave = (key, params) => { const handleSave = () => {
scoreDetails[key] = params
commitScore(0).then(() => { commitScore(0).then(() => {
ElMessage.success('保存成功') ElMessage.success('保存成功')
}) })
} }
const handleNext = () => {
activeTab.value++
}
const commitScore = (status = 0) => { const commitScore = (status = 0) => {
return updateScore({ return updateScore({
id: route.query.id, id: route.query.id,
...@@ -243,7 +85,12 @@ const handlePublishScore = () => { ...@@ -243,7 +85,12 @@ const handlePublishScore = () => {
<el-form-item label="状态">{{ detail.status_name }}</el-form-item> <el-form-item label="状态">{{ detail.status_name }}</el-form-item>
<el-form-item label="提交时间">{{ detail.commit_time }}</el-form-item> <el-form-item label="提交时间">{{ detail.commit_time }}</el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" :disabled="detail.status == '2'" :loading="isLoading" @click="handleAIScore" <el-button
type="primary"
:disabled="detail.status == '2'"
:loading="isLoading"
@click="handleAIScore"
v-if="detail.competition_rule?.competition == 1"
>AI一键评分</el-button >AI一键评分</el-button
> >
<el-button type="primary" :disabled="detail.status == '2'" @click="handlePublishScore">发布成绩</el-button> <el-button type="primary" :disabled="detail.status == '2'" @click="handlePublishScore">发布成绩</el-button>
...@@ -254,174 +101,12 @@ const handlePublishScore = () => { ...@@ -254,174 +101,12 @@ const handlePublishScore = () => {
</div> </div>
</div> </div>
<el-divider /> <el-divider />
<el-tabs stretch v-model="activeTab" class="score-tabs"> <CompetitionLive
<el-tab-pane label="商品品类管理" :name="1"> :detail="detail"
<ScoreCard @save="handleSave"
:maxScore="5" v-model:scoreDetails="scoreDetails"
:hasSaveButton="detail.status != '2'" v-if="detail.competition_rule?.competition == 1" />
:score="scoreDetails.commodity_type.score" <CompetitionOperations :detail="detail" @save="handleSave" v-model:scoreDetails="scoreDetails" v-else />
:comment="scoreDetails.commodity_type.comment"
@save="handleSave('commodity_type', $event)"
@next="handleNext">
<AppList v-bind="categoryTableOptions" row-key="id" :data="detail.live_data.commodity_types" />
</ScoreCard>
</el-tab-pane>
<el-tab-pane label="商品属性管理" :name="2" lazy>
<ScoreCard
:maxScore="5"
:hasSaveButton="detail.status != '2'"
:score="scoreDetails.commodity_attr.score"
:comment="scoreDetails.commodity_attr.comment"
@save="handleSave('commodity_attr', $event)"
@next="handleNext">
<AppList v-bind="attrsTableOptions" :data="detail.live_data.commodity_attrs" />
</ScoreCard>
</el-tab-pane>
<el-tab-pane label="商品管理" :name="3" lazy>
<ScoreCard
:maxScore="15"
:hasSaveButton="detail.status != '2'"
:score="scoreDetails.commodity.score"
:comment="scoreDetails.commodity.comment"
@save="handleSave('commodity', $event)"
@next="handleNext">
<AppList v-bind="productTableOptions" :data="productList" :default-expand-all="productList.length === 1">
<template #table-expand="{ row }">
<div class="table-expand-box">
<el-form label-position="top">
<el-row>
<el-col :span="12">
<el-form-item label="主图">
<el-image
:src="item.url"
v-for="item in row.picture_addreses"
:key="item.url"
fit="cover"
preview-teleported
:preview-src-list="row.picture_addreses.map((item) => item.url)"
style="width: 100px; height: 100px; margin-right: 10px"></el-image>
</el-form-item>
<el-form-item label="主图3:4">
<el-image
:src="item.url"
v-for="item in row.picture_34_addreses"
:key="item.url"
fit="cover"
preview-teleported
:preview-src-list="row.picture_addreses.map((item) => item.url)"
style="width: 100px; height: 134px; margin-right: 10px"></el-image>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="主图视频">
<video
:src="row.video_url"
controls
style="width: 100%; height: 282px"
v-if="row.video_url"></video>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="商品规格">
<AppList v-bind="productSpecsTableOptions" :data="row.info?.specs" style="width: 100%"></AppList>
</el-form-item>
<el-form-item label="价格与库存">
<AppList v-bind="productSkuTableOptions" :data="row.info?.sku" style="width: 100%"></AppList>
</el-form-item>
</el-form>
</div>
</template>
</AppList>
</ScoreCard>
</el-tab-pane>
<el-tab-pane label="直播话术管理" :name="4" lazy>
<ScoreCard
:maxScore="15"
:hasSaveButton="detail.status != '2'"
:score="scoreDetails.speech.score"
:comment="scoreDetails.speech.comment"
@save="handleSave('speech', $event)"
@next="handleNext">
<AppList
v-bind="talkTableOptions"
:data="detail.live_data.speeches"
:default-expand-all="detail.live_data.speeches.length === 1">
<template #table-expand="{ row }">
<div class="table-expand-box">
<el-form label-position="top">
<el-form-item label="直播主题卖点">
<div class="form-box" v-html="row.selling_point"></div>
</el-form-item>
<el-form-item label="营销活动">
<div class="form-box" v-html="row.marketing_campaign"></div>
</el-form-item>
<el-form-item label="直播话术内容">
<div class="form-box" v-html="row.content"></div>
</el-form-item>
</el-form>
</div>
</template>
</AppList>
</ScoreCard>
</el-tab-pane>
<el-tab-pane label="首次直播演练" :name="5" lazy>
<ScoreCard
:maxScore="20"
:hasSaveButton="detail.status != '2'"
:score="scoreDetails.practice_record1.score"
:comment="scoreDetails.practice_record1.comment"
@save="handleSave('practice_record1', $event)"
@next="handleNext">
<ScoreCardLive :data="detail.live_data.practice_records[0]" v-if="detail.live_data.practice_records[0]" />
<el-empty v-else />
</ScoreCard>
</el-tab-pane>
<el-tab-pane label="直播复盘分析" :name="6" lazy>
<ScoreCard
:maxScore="10"
:hasSaveButton="detail.status != '2'"
:score="scoreDetails.improvement_plan.score"
:comment="scoreDetails.improvement_plan.comment"
@save="handleSave('improvement_plan', $event)"
@next="handleNext">
<el-form label-position="top" v-if="detail.live_data.practice_records[0]?.improvement_plan">
<el-form-item label="改进方案">
<div class="form-box" v-html="detail.live_data.practice_records[0]?.improvement_plan"></div>
</el-form-item>
</el-form>
<el-empty v-else />
</ScoreCard>
</el-tab-pane>
<el-tab-pane label="二次直播演练" :name="7" lazy>
<ScoreCard
:maxScore="15"
:hasSaveButton="detail.status != '2'"
:score="scoreDetails.practice_record2.score"
:comment="scoreDetails.practice_record2.comment"
@save="handleSave('practice_record2', $event)"
@next="handleNext">
<ScoreCardLive :data="detail.live_data.practice_records[1]" v-if="detail.live_data.practice_records[1]" />
<el-empty v-else />
</ScoreCard>
</el-tab-pane>
<el-tab-pane label="直播总结汇报" :name="8" lazy>
<ScoreCard
:maxScore="15"
:hasSaveButton="detail.status != '2'"
:hasNextButton="false"
:score="scoreDetails.report.score"
:comment="scoreDetails.report.comment"
@save="handleSave('report', $event)"
@next="handleNext">
<template v-if="detail.live_data.reports.length > 0">
<div v-for="item in detail.live_data.reports" :key="item.report_url" style="height: 600px">
<Preview :url="item.report_url" />
</div>
</template>
<el-empty v-else />
</ScoreCard>
</el-tab-pane>
</el-tabs>
</AppCard> </AppCard>
</template> </template>
......
...@@ -15,6 +15,14 @@ const fetchRecord = async () => { ...@@ -15,6 +15,14 @@ const fetchRecord = async () => {
const res = await getRecord({ id: props.recordId }) const res = await getRecord({ id: props.recordId })
const resDetail = res.data.detail const resDetail = res.data.detail
detail.value = { ...resDetail, live_info: JSON.parse(resDetail.live_info) } detail.value = { ...resDetail, live_info: JSON.parse(resDetail.live_info) }
try {
const subtitle = JSON.parse(resDetail.subtitle)
if (subtitle?.Result?.Sentences && !resDetail.ai_level) {
handleAIScore()
}
} catch (error) {
console.log(error)
}
} }
onMounted(() => { onMounted(() => {
fetchRecord() fetchRecord()
...@@ -72,9 +80,15 @@ const failedWords = computed(() => { ...@@ -72,9 +80,15 @@ const failedWords = computed(() => {
return detail.value?.words?.filter((item) => !item.time) || [] return detail.value?.words?.filter((item) => !item.time) || []
}) })
const isLoading = ref(false)
const handleAIScore = () => { const handleAIScore = () => {
aiScore({ id: props.recordId }).then((res) => { isLoading.value = true
console.log(res) aiScore({ id: props.recordId })
.then(() => {
fetchRecord()
})
.finally(() => {
isLoading.value = false
}) })
} }
...@@ -86,7 +100,7 @@ function formatDuration(seconds) { ...@@ -86,7 +100,7 @@ function formatDuration(seconds) {
</script> </script>
<template> <template>
<div class="live-view" v-if="detail"> <div class="live-view" v-if="detail" v-loading="isLoading" element-loading-text="AI打分中...">
<AppCard> <AppCard>
<div class="live-info"> <div class="live-info">
<div class="live-info-header"> <div class="live-info-header">
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论