提交 2c83c129 authored 作者: 王鹏飞's avatar 王鹏飞

bug fixes

上级 6a3cdb5f
......@@ -11,7 +11,7 @@ if (!form.info.specs) {
// 初始化sku列表
if (!form.info.sku) {
form.info.sku = [{ price: '', stock: 0 }]
form.info.sku = [{ price: '', stock: 0, reference_price: '' }]
}
// 确保在没有规格时至少有一个SKU
......@@ -22,7 +22,7 @@ onMounted(() => {
function ensureSkuExists() {
const hasValidSpecs = form.info.specs && form.info.specs.some((spec) => spec.name && spec.name.trim())
if (!hasValidSpecs && (!form.info.sku || form.info.sku.length === 0)) {
form.info.sku = [{ price: '', stock: 0 }]
form.info.sku = [{ price: '', stock: 0, reference_price: '' }]
}
}
......@@ -79,12 +79,13 @@ function generateSkuList() {
// 如果没有有效规格,确保至少有一个SKU
if (validSpecs.length === 0) {
if (!form.info.sku || form.info.sku.length === 0) {
form.info.sku = [{ price: '', stock: 0 }]
form.info.sku = [{ price: '', stock: 0, reference_price: '' }]
} else if (form.info.sku.length === 1 && !form.info.sku[0].specs) {
// 保持单个SKU,但不添加specs字段
form.info.sku[0] = {
price: form.info.sku[0].price || '',
stock: form.info.sku[0].stock || 0,
reference_price: form.info.sku[0].reference_price || '',
}
} else {
// 如果之前有多个SKU(有规格时),现在没有规格了,只保留第一个或创建一个新的
......@@ -93,6 +94,7 @@ function generateSkuList() {
{
price: firstSku?.price || '',
stock: firstSku?.stock || 0,
reference_price: firstSku?.reference_price || '',
},
]
}
......@@ -107,11 +109,12 @@ function generateSkuList() {
// 如果任何规格没有有效值,确保至少有一个SKU
if (specValuesArrays.some((arr) => arr.length === 0)) {
if (!form.info.sku || form.info.sku.length === 0) {
form.info.sku = [{ price: '', stock: 0 }]
form.info.sku = [{ price: '', stock: 0, reference_price: '' }]
} else if (form.info.sku.length === 1 && !form.info.sku[0].specs) {
form.info.sku[0] = {
price: form.info.sku[0].price || '',
stock: form.info.sku[0].stock || 0,
reference_price: form.info.sku[0].reference_price || '',
}
}
return
......@@ -120,12 +123,16 @@ function generateSkuList() {
// 生成笛卡尔积
const combinations = cartesianProduct(specValuesArrays)
// 生成SKU列表,保留已存在的价格和库存数据
// 生成SKU列表,保留已存在的价格、库存和参考价数据
const existingSkuMap = new Map()
form.info.sku.forEach((sku) => {
// 如果有specs字段,使用specs作为key;否则使用空数组作为key
const key = JSON.stringify(sku.specs || [])
existingSkuMap.set(key, { price: sku.price || '', stock: sku.stock || 0 })
existingSkuMap.set(key, {
price: sku.price || '',
stock: sku.stock || 0,
reference_price: sku.reference_price || '',
})
})
form.info.sku = combinations.map((combo) => {
......@@ -135,6 +142,7 @@ function generateSkuList() {
specs: combo,
price: existing?.price || '',
stock: existing?.stock || 0,
reference_price: existing?.reference_price || '',
}
})
}
......@@ -164,6 +172,7 @@ watch(hasSpecs, (newVal) => {
// 批量设置相关
const batchPrice = ref('')
const batchStock = ref('')
const batchReferencePrice = ref('')
// 批量设置价格
function batchSetPrice() {
......@@ -188,6 +197,17 @@ function batchSetStock() {
})
batchStock.value = ''
}
// 批量设置参考价
function batchSetReferencePrice() {
if (batchReferencePrice.value === '' || batchReferencePrice.value === null || batchReferencePrice.value === undefined)
return
const referencePrice = batchReferencePrice.value
form.info.sku.forEach((sku) => {
sku.reference_price = referencePrice
})
batchReferencePrice.value = ''
}
</script>
<template>
......@@ -278,6 +298,14 @@ function batchSetStock() {
</el-input>
<el-button type="primary" plain @click="batchSetStock" style="margin-left: 8px"> 批量设置库存 </el-button>
</div>
<div class="batch-set-item">
<el-input v-model="batchReferencePrice" placeholder="批量设置参考价" type="number" style="width: 200px">
<template #prefix>¥</template>
</el-input>
<el-button type="primary" plain @click="batchSetReferencePrice" style="margin-left: 8px">
批量设置参考价
</el-button>
</div>
</div>
<el-table :data="form.info.sku" border :header-cell-style="{ backgroundColor: '#f5f7fa' }" style="width: 100%">
<el-table-column v-if="hasSpecs" label="规格组合" align="center" width="200">
......@@ -292,6 +320,13 @@ function batchSetStock() {
</el-input>
</template>
</el-table-column>
<el-table-column prop="reference_price" label="参考价" align="center">
<template #default="{ row }">
<el-input v-model="row.reference_price" placeholder="请输入参考价" type="number">
<template #prefix>¥</template>
</el-input>
</template>
</el-table-column>
<el-table-column prop="stock" label="库存" align="center">
<template #default="{ row }">
<el-input v-model.number="row.stock" placeholder="请输入库存" type="number">
......@@ -301,7 +336,7 @@ function batchSetStock() {
</el-table-column>
</el-table>
</el-form-item>
<el-form-item label="参考价" prop="info.reference_price">
<el-form-item label="参考价" prop="info.reference_price" v-if="false">
<template #label>
<span>参考价</span>
<el-popover :width="200" trigger="hover" placement="right">
......
<script setup>
import { ElMessage } from 'element-plus'
import { Document } from '@element-plus/icons-vue'
import { createReport, getReport, getTestList } from '../api'
import AppUpload from '@/components/base/AppUpload.vue'
......@@ -17,6 +16,7 @@ const form = reactive({
live_practice_id: '',
report_name: '',
report_url: '',
file: [],
})
watchEffect(() => {
if (props.data) Object.assign(form, props.data)
......@@ -36,7 +36,9 @@ onMounted(() => {
async function fetchInfo() {
const res = await getReport({ id: props.data.id })
Object.assign(form, res.data.detail)
Object.assign(form, res.data.detail, {
file: [{ name: res.data.detail.report_name || '', url: res.data.detail.report_url || '' }],
})
}
watchEffect(() => {
if (props.action !== 'add') fetchInfo()
......@@ -65,9 +67,8 @@ async function handleUpdate() {
}
function handleSuccess(file) {
if (!form.report_name) {
form.report_name = file.name
}
form.report_name = file.name
form.report_url = file.raw?.url
}
</script>
......@@ -91,16 +92,10 @@ function handleSuccess(file) {
</el-form-item>
<el-form-item label="总结报告文件" prop="report_url">
<div style="width: 100%">
<AppUpload v-model="form.report_url" accept=".doc,.docx,.pdf" @success="handleSuccess">
<AppUpload v-model="form.file" accept=".doc,.docx,.ppt,.pptx" :limit="1" @success="handleSuccess">
<el-button type="primary" plain round>本地上传</el-button>
<template #tip>报告文件只能上传一个,支持格式包含:doc docx pdf,大小不超过50M</template>
<template #tip>报告文件只能上传一个,支持格式包含:ppt docx,大小不超过50M</template>
</AppUpload>
<div class="upload-preview" v-if="form.report_url">
<a :href="form.report_url" target="_blank">
<el-icon><Document /></el-icon>
<span>{{ form.report_name || form.report_url }}</span>
</a>
</div>
</div>
</el-form-item>
</el-form>
......@@ -114,16 +109,3 @@ function handleSuccess(file) {
</template>
</el-dialog>
</template>
<style lang="scss" scoped>
.upload-preview {
a {
display: flex;
align-items: center;
white-space: nowrap;
gap: 5px;
overflow: hidden;
text-overflow: ellipsis;
}
}
</style>
......@@ -194,7 +194,11 @@ const handleNext = () => {
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" />
<AppList
v-bind="categoryTableOptions"
row-key="id"
:data="detail.live_data.commodity_types"
default-expand-all />
</ScoreCard>
</el-tab-pane>
<el-tab-pane label="商品属性管理" :name="2" lazy>
......@@ -216,7 +220,7 @@ const handleNext = () => {
v-model:comment="scoreDetails.commodity.comment"
@save="$emit('save')"
@next="handleNext">
<AppList v-bind="productTableOptions" :data="productList" :default-expand-all="productList.length === 1">
<AppList v-bind="productTableOptions" :data="productList" default-expand-all>
<template #table-expand="{ row }">
<div class="table-expand-box">
<el-form label-position="top">
......@@ -273,10 +277,7 @@ const handleNext = () => {
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">
<AppList v-bind="talkTableOptions" :data="detail.live_data.speeches" default-expand-all>
<template #table-expand="{ row }">
<div class="table-expand-box">
<el-form label-position="top">
......
......@@ -216,6 +216,7 @@ function handleAIGenerate(index) {
plain
size="small"
@click="handleAIGenerate(1)"
:disabled="isLoading"
:loading="isLoading && aiActive === 1"
>AI一键生成</el-button
>
......@@ -233,6 +234,7 @@ function handleAIGenerate(index) {
plain
size="small"
@click="handleAIGenerate(2)"
:disabled="isLoading"
:loading="isLoading && aiActive === 2"
>AI一键生成</el-button
>
......@@ -253,7 +255,12 @@ function handleAIGenerate(index) {
<el-col :sm="24" :md="12" style="border-left: 1px solid #dcdfe6">
<div style="text-align: center; margin-bottom: 20px">
<h2 style="margin-bottom: 20px">直播话术</h2>
<el-button type="primary" size="large" @click="handleAIGenerate(3)" :loading="isLoading && aiActive === 3">
<el-button
type="primary"
size="large"
@click="handleAIGenerate(3)"
:disabled="isLoading"
:loading="isLoading && aiActive === 3">
{{ messages.length ? '再次生成直播话术' : 'AI生成直播话术' }}
</el-button>
</div>
......@@ -272,7 +279,13 @@ function handleAIGenerate(index) {
<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" v-if="action !== 'view'"
<el-button
type="primary"
round
auto-insert-space
:disabled="isLoading"
@click="handleSubmit"
v-if="action !== 'view'"
>保存</el-button
>
</el-row>
......
......@@ -72,7 +72,7 @@ export function updateImprovementPlan(data: { id: string; improvement_plan: stri
return httpRequest.post('/api/lab/v1/experiment/live-practice/submit-improvement-plan', data)
}
// AI 打分
export function aiScore(data: { id: string }) {
return httpRequest.post('/api/lab/v1/experiment/live-practice/score-live-practice-record', data)
// 更新直播记录的AI分析结果
export function updateRecord(data: { id: string; ai_analyze: string }) {
return httpRequest.post('/api/lab/v1/experiment/live-practice/update-live-practice-record', data)
}
......@@ -22,7 +22,7 @@ const props = defineProps({
onStatsChange: { type: Function, default: () => {} },
})
const { messages, viewers, stats, currentTime, start: startChat } = useLiveChat()
const { messages, viewers, stats, currentTime, start: startChat, stop: stopChat } = useLiveChat()
watch(
() => [stats, viewers],
() => {
......@@ -96,6 +96,7 @@ const {
console.log('结束直播')
if (enabled.value) return
props.onStop && props.onStop(blob)
stopChat()
handleUpdateRecord({ live_video_size: blob.size.toString() })
// 保存录像到本地
if (props.isLocalUpload) {
......
......@@ -2,7 +2,10 @@
import VueMarkdown from 'vue-markdown-render'
import Demo from '../components/Demo.vue'
import ImprovementPlan from '../components/ImprovementPlan.vue'
import { getRecord, aiScore } from '../api'
import { getRecord, updateRecord } from '../api'
import { useChat } from '@ezijing/ai-vue'
const { isLoading, generateText } = useChat({ provider: 'volcano' })
const props = defineProps({
isView: { type: Boolean, default: true },
......@@ -16,8 +19,7 @@ const fetchRecord = async () => {
const resDetail = res.data.detail
detail.value = { ...resDetail, live_info: JSON.parse(resDetail.live_info) }
try {
const subtitle = JSON.parse(resDetail.subtitle)
if (subtitle?.Result?.Sentences && !resDetail.ai_level) {
if (!resDetail.ai_analyze) {
handleAIScore()
}
} catch (error) {
......@@ -36,7 +38,7 @@ const dialogVisible = ref(false)
const result = computed(() => {
try {
return JSON.parse(detail.value.illegal_word) || {}
return JSON.parse(detail.value.ai_analyze) || {}
} catch (error) {
console.log(error)
}
......@@ -80,16 +82,326 @@ const failedWords = computed(() => {
return detail.value?.words?.filter((item) => !item.time) || []
})
const isLoading = ref(false)
const handleAIScore = () => {
isLoading.value = true
aiScore({ id: props.recordId })
.then(() => {
fetchRecord()
})
.finally(() => {
isLoading.value = false
})
const handleAIScore = async () => {
const messages = [
{
role: 'system',
content: '你是一位专业的直播电商分析师,需要为选手的直播演练生成一份全面、详细的AI评价报告。',
},
{
role: 'user',
content: `
[输入信息]
- 直播视频/音频记录(${detail.value?.live_duration}
- 字幕文本:${detail.value?.subtitle}
- 直播话术脚本:${detail.value?.live_practice_info.live_speech.content}
- 预设卖点:${detail.value?.live_practice_info.live_speech.selling_point}
- 营销活动:${detail.value?.live_practice_info.live_speech.marketing_campaign}
- 商品信息:${JSON.stringify(detail.value?.live_practice_info.live_commodity)}
【输出要求】
请基于以上信息生成结构化的 Markdown 报告,并保证 JSON 字段与现有前端对接保持一致:
{
"sellingPoints": "...卖点讲解覆盖情况的总结内容...",
"forbiddenWords": "...违禁词分析报告的 Markdown 内容...",
"speechSpeed": "...语速分析报告的 Markdown 内容...",
"optimizationTips": "...直播优化建议的 Markdown 内容..."
}
报告中必须覆盖以下四大板块,并按照既定标题层级输出,避免新增或删除节点:
一、卖点讲解覆盖情况
卖点讲解综合评价:
【整体表现】
【用几句话总结卖点讲解的整体情况,如:本次直播时长XX,累计口播XX字,本次直播共讲解了X个卖点,覆盖率达到XX%。讲解较为全面/部分遗漏,重点突出/略显平淡。建议在二次直播中...
二、违禁词分析报告
检测结果总览:
共发现:X 个违禁词/敏感词
严重程度评估:严重/中等/轻微/零违禁
(严重程度评估标准:严重≥5 个,中等 3-4 个,轻微 1-2 个,零违禁 0 个。此说明不写入报告。)
违禁词详细列表:(若为零违禁,则不展示下面的统计内容)
【违禁词 #1】━━━━━━━━━━━━
违禁词:"【具体违禁词】"
类型:绝对化用语 夸大宣传 医疗用语 虚假宣传
出现时间:第XX
出现次数:X
上下文原文:
"【引用完整句子,用红色标注违禁词】"
例:"这款面膜效果最好,市场上独一无二的配方..."
↑违禁 ↑违禁
【违禁词 #2】━━━━━━━━━━━━
违禁词:"【具体违禁词】"
类型:绝对化用语 夸大宣传 医疗用语 虚假宣传
出现时间:第XX
出现次数:X
上下文原文:
"【引用完整句子】"
例:"这款面膜能够治疗痘痘,快速修复受损肌肤..."
↑违禁
【违禁词 #3】━━━━━━━━━━━━
违禁词:"【具体违禁词】"
类型:绝对化用语 夸大宣传 医疗用语 虚假宣传
出现时间:第XX
出现次数:X
上下文原文:
"【引用完整句子】"
例:"使用后100%能看到效果,保证满意..."
↑违禁 ↑违禁
(依此类推,列出所有违禁词...
违规情况统计(若为零违禁,则不展示下面的统计内容)
违禁词总数:X
违禁词出现总次数:X
类型分布:
- 绝对化用语:X个(如:最、第一、唯一、极致、顶级、完美等)
- 夸大宣传:X个(如:100%、必然、保证、绝对、立即见效等)
- 医疗用语:X个(如:治疗、疗效、药用、治愈、医治等)
- 虚假宣传:X个(如:国家级、世界级、最高级、最低价等)
高频违禁词:
1. "【违禁词】" - 出现X
2. "【违禁词】" - 出现X
3. "【违禁词】" - 出现X
常见违禁词快速对照表(不需要体现在报告里面)
类别1:绝对化用语(共20个)
最、最佳、最好、最优、最大、最小、最新、最先进
第一、首个、首选、唯一、独一无二、前无古人
极致、完美、顶级、至尊、终极、巅峰
类别2:夸大宣传(共15个)
100%、百分百、保证、必然、绝对、肯定
立竿见影、立即见效、马上见效、即刻
永久、终身、永远、一劳永逸、彻底
类别3:医疗用语(共20个)
治疗、治愈、医治、疗效、药用、处方
抗炎、消炎、杀菌、灭菌、防癌、抗癌
降血压、降血糖、降血脂、治痤疮、祛痘、去斑
修复、再生(需谨慎使用)
类别4:虚假宣传(共10个)
国家级、世界级、全球级、宇宙级
最高级、最低价、史无前例、前所未有
销量第一、市场占有率第一
三、直播优化建议
综合评价:
【用几句话总结本次直播的整体表现,如:本次直播整体表现良好/一般/优秀,在卖点讲解和互动方面表现突出,但在语速控制和促单技巧方面还有提升空间。
以下 5 个维度需逐项输出:
1、语速与节奏
【现状描述】详细描述当前语速和节奏的表现,如:平均语速约XXX/分钟,整体偏快/适中/偏慢。在产品讲解环节语速过快,在互动环节节奏较好。
【存在问题】示例如下:
问题1,如:产品讲解环节(XX-XX秒)语速过快,达到XXX/分钟,观众可能跟不上
问题2,如:停顿不当,关键信息没有留出思考时间
问题3,如:节奏不稳定,时快时慢,影响观看体验
【改进建议】示例如下:
具体建议1,如:在讲解三重玻尿酸成分时,有意识放慢语速至200/分钟以内,并在关键数据后 停顿1-2
具体建议2,如:使用"那么""接下来"等连接词自然控制节奏
具体建议3,如:提前充分练习话术脚本,避免因不熟练导致语速失控
(语速标准参考内容需保留原文,不写入报告主体)
语速标准参考
理想语速:180-220 /分钟
- 观众能够轻松跟上
- 有足够时间理解信息
- 节奏舒适,不紧不慢
偏快语速:221-240 /分钟
- 部分观众可能跟不上
- 影响信息吸收效果
- 建议适当放慢
过快语速:>240 /分钟
- 大部分观众难以跟上
- 严重影响直播效果
- 必须改进
偏慢语速:150-179 /分钟
- 节奏稍显拖沓
- 可能影响直播间活跃度
- 建议适当加快
过慢语速:<150 /分钟
- 节奏过于拖沓
- 观众容易失去耐心
- 必须改进
2、互动能力
【现状描述】详细描述当前互动情况,如:互动频率一般/良好/不足,共发起X次互动,回应了X条观众评论。互动形式包括...
【存在问题】示例如下:
问题1,如:互动频率过低,整场直播仅发起X次互动,直播间氛围不够活跃
问题2,如:互动问题设计不够吸引人,观众参与度不高
问题3,如:对观众评论回应不够及时,错过了多次互动机会
问题4,如:抽奖活动执行不到位,未能充分调动观众积极性
【改进建议】示例如下:
具体建议1,如:增加互动频率,建议每3-5分钟发起一次互动,如"有宝宝用过玻尿酸面膜吗?效果如何?"
具体建议2,如:设计更有趣的互动问题,如"猜猜这款面膜的秒杀价是多少?猜中送小样!"
具体建议3,如:及时关注评论区,对高频问题立即回应,如"敏感肌能用吗?"
具体建议4,如:在点赞数接近目标时,强调"还差XX个就到10000啦,大家帮忙点一下!"营造参与感
3、卖点讲解能力
【现状描述】详细描述当前卖点讲解情况,如:共讲解了X个卖点,覆盖率XX%。讲解较为清晰/逻辑性强/略显混乱。
【存在问题】示例如下:
问题1,如:卖点讲解不够深入,对"三重玻尿酸"的概念解释不清
问题2,如:缺少具体案例和数据支撑,说服力不足
问题3,如:卖点讲解顺序混乱,没有逻辑层次
问题4,如:遗漏重要卖点"神经酰胺和角鲨烷",影响产品价值传达
【改进建议】示例如下:
具体建议1,如:讲解"三重玻尿酸"时,要说明"大中小分子玻尿酸,分别作用于肌肤不同层次,大分子锁水、中分子补水、小分子深层渗透"
具体建议2,如:引用具体数据"真人实测48小时后肌肤水润度依然提升40%"增强说服力
具体建议3,如:按照"成分→功效→效果→价格"的逻辑顺序讲解卖点
具体建议4,如:将所有5个卖点都讲解到位,每个卖点至少讲解30
4、促单能力
【现状描述】详细描述当前促单话术使用情况,如:促单环节共X次,促单话术包括...促单效果一般/良好/不足。
【存在问题】示例如下:
问题1,如:促单话术力度不够,缺乏紧迫感营造
问题2,如:未充分强调价格优惠,"秒杀价69元"只提了一次
问题3,如:限时限量信息传达不清,未能激发购买欲望
问题4,如:组合优惠活动讲解不够清晰,观众可能不理解
【改进建议】示例如下:
具体建议1,如:在秒杀环节强调"只剩最后5分钟!只有100份!现在还剩XX份!"营造紧迫感
具体建议2,如:多次强调价格对比"平时99元,今天秒杀只要69元,一片才6.9元,太划算了!"
具体建议3,如:在促销环节使用催单话术"已经有XX位宝宝下单了,还在犹豫的宝宝抓紧啦!"
具体建议4,如:清晰讲解组合优惠"买10片装送2片,相当于12片只要69元,比单买划算多了!"
5、整体表现力
【现状描述】详细描述整体表现,如:仪表整洁,妆容得体。肢体语言自然/略显僵硬。镜头感良好/需加强。整体自信度较好/略显紧张。
【存在问题】示例如下:
问题1,如:肢体语言不够丰富,全程较为僵硬
问题2,如:眼神交流不够,经常低头看脚本
问题3,如:产品展示不够充分,没有特写展示
问题4,如:整体略显紧张,影响自信度
【改进建议】示例如下:
具体建议1,如:增加手势配合讲解,如讲"三重玻尿酸"时用手势比划"三层"
具体建议2,如:尽量看镜头而不是脚本,熟记话术内容
具体建议3,如:展示产品时,将产品靠近镜头,展示包装细节和文字说明
具体建议4,如:直播前深呼吸放松,告诉自己"我准备充分,一定可以"
四、语速分析报告
━━ (一)语速分析 ━━
整体语速数据
平均语速:XXX /分钟
最快语速:XXX /分钟(出现在第XX秒)
最慢语速:XXX /分钟(出现在第XX秒)
语速波动:XX /分钟(标准差)
━━ (二)语速标准参考 ━━
理想语速:180-220 /分钟
- 观众能够轻松跟上
- 有足够时间理解信息
- 节奏舒适,不紧不慢
偏快语速:221-240 /分钟
- 部分观众可能跟不上
- 影响信息吸收效果
- 建议适当放慢
过快语速:>240 /分钟
- 大部分观众难以跟上
- 严重影响直播效果
- 必须改进
偏慢语速:150-179 /分钟
- 节奏稍显拖沓
- 可能影响直播间活跃度
- 建议适当加快
过慢语速:<150 /分钟
- 节奏过于拖沓
- 观众容易失去耐心
- 必须改进
━━ (三)语速曲线分析 ━━
【语速统计区间】(输出为表格,表头和示例行保持不变)
时间段 | 语速(/) | 评价 | 对应环节
---------------|-------------|--------|-------------
00:00-01:00 | XXX | ✅正常 | 开场白
01:00-02:00 | XXX | ✅正常 | 品牌介绍
02:00-03:00 | XXX | ⚠️偏慢 | 品牌介绍
03:00-04:00 | XXX | ✅正常 | 产品讲解-开始
04:00-05:00 | XXX | ⚡偏快 | 产品讲解-成分
05:00-06:00 | XXX | ❌过快 | 产品讲解-卖点
06:00-07:00 | XXX | ✅正常 | 产品讲解-效果
07:00-08:00 | XXX | ✅正常 | 产品讲解-价格
08:00-09:00 | XXX | ⚠️偏慢 | 互动环节
09:00-10:00 | XXX | ⚡偏快 | 促销推动
10:00-11:00 | XXX | ⚡偏快 | 促销推动
11:00-12:00 | XXX | ✅正常 | 催单话术
12:00-13:00 | XXX | ✅正常 | 组合优惠
13:00-14:00 | XXX | ✅正常 | 结束语
14:00-15:00 | XXX | ✅正常 | 结束语
━━ (四)语速异常时间段详细分析 ━━
示例如下:
【异常片段 #1】━━━━━━━━━━━━
时间:XX:XX-XX:XXX分钟)
问题:语速过快
数据:XXX /分钟(超标准XX/分钟)
对应内容:【具体内容,如:产品卖点讲解-三重玻尿酸成分介绍】
【异常片段 #2】━━━━━━━━━━━━
时间:XX:XX-XX:XXX分钟)
问题:语速过快/持续偏快
数据:XXX /分钟(超标准XX/分钟)
对应内容:【具体内容,如:促销推动环节】
【异常片段 #3】━━━━━━━━━━━━
时间:XX:XX-XX:XXX分钟)
问题:语速过慢
数据:XXX /分钟(低于标准XX/分钟)
对应内容:【具体内容,如:品牌介绍环节】
【异常片段 #4】━━━━━━━━━━━━
时间:XX:XX-XX:XXX分钟)
问题:语速过慢
数据:XXX /分钟(低于标准XX/分钟)
对应内容:【具体内容,如:互动环节】
━━ (五)语速变化趋势分析 ━━
【整体趋势】描述语速在整场直播中的变化趋势,如:开场语速正常,在产品讲解环节(4-6分钟)明显加快,互动环节放慢,促销环节又再次加快,结束语恢复正常。整体呈现"波浪形"变化,稳定性不足。
【问题诊断】示例如下:
主要问题:【如:在需要重点讲解的环节(产品卖点)语速反而过快,影响效果】
次要问题:【如:语速波动过大,缺乏整体节奏控制】
# 风格与注意事项
- 语言保持专业、客观、具体,结合提供的数据或示例描述。
- 如遇缺失信息,可使用“暂无数据”或根据上下文合理推断,但避免胡乱编造。
- 若违禁词为零,自动隐藏“违禁词详细列表”“违规情况统计”“高频违禁词”等相关段落。
- JSON 输出中的四个字段必须全部返回,值为字符串类型,内容为对应 Markdown 片段。
- 不允许调整前端依赖的标题、排版符号、表格结构与占位变量名称。`,
},
]
const result = await generateText({ messages, response_format: { type: 'json_object' } })
const aiAnalyze = result.content
.replaceAll('一、卖点讲解覆盖情况', '')
.replaceAll('二、违禁词分析报告', '')
.replaceAll('三、直播优化建议', '')
.replaceAll('四、语速分析报告', '')
await updateRecord({ id: props.recordId, ai_analyze: aiAnalyze })
fetchRecord()
}
function formatDuration(seconds) {
......@@ -100,7 +412,7 @@ function formatDuration(seconds) {
</script>
<template>
<div class="live-view" v-if="detail" v-loading="isLoading" element-loading-text="AI打分中...">
<div class="live-view" v-if="detail" v-loading="isLoading" element-loading-text="AI分析中...">
<AppCard>
<div class="live-info">
<div class="live-info-header">
......
......@@ -120,12 +120,13 @@ export function useLiveChat(options: UseLiveChatOptions = {}) {
// 随机事件生成
const generateRandomEvents = () => {
const rand = Math.random()
if (rand < 0.9) randomJoin()
if (rand < 0.8) randomJoin()
if (rand < 0.4) randomLeave()
if (rand < 0.7) randomGift()
if (rand < 0.8) randomLike()
}
let timer: number | null = null
// 开始
const start = () => {
// 初始化观众
......@@ -133,7 +134,11 @@ export function useLiveChat(options: UseLiveChatOptions = {}) {
randomJoin()
}
// 开始定时更新
setInterval(generateRandomEvents, updateInterval)
timer = setInterval(generateRandomEvents, updateInterval)
}
const stop = () => {
if (timer) clearInterval(timer)
timer = null
}
if (autoStart) start()
......@@ -161,5 +166,5 @@ export function useLiveChat(options: UseLiveChatOptions = {}) {
{ immediate: true }
)
return { viewers, messages, stats, currentTime, start, reset }
return { viewers, messages, stats, currentTime, start, stop, reset }
}
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论