Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
S
saas-dml
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
EzijingWeb
saas-dml
Commits
6a3cdb5f
提交
6a3cdb5f
authored
11月 13, 2025
作者:
王鹏飞
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
feat: 成绩管理支持全媒体运营
上级
602d12af
隐藏空白字符变更
内嵌
并排
正在显示
6 个修改的文件
包含
769 行增加
和
335 行删除
+769
-335
CompetitionLive.vue
src/modules/live/score/components/CompetitionLive.vue
+397
-0
CompetitionOperations.vue
src/modules/live/score/components/CompetitionOperations.vue
+210
-0
ModuleCard.vue
src/modules/live/score/components/ModuleCard.vue
+67
-0
ScoreCardSub.vue
src/modules/live/score/components/ScoreCardSub.vue
+61
-0
View.vue
src/modules/live/score/views/View.vue
+16
-331
RecordView.vue
src/modules/live/test/components/RecordView.vue
+18
-4
没有找到文件。
src/modules/live/score/components/CompetitionLive.vue
0 → 100644
浏览文件 @
6a3cdb5f
<
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
>
src/modules/live/score/components/CompetitionOperations.vue
0 → 100644
浏览文件 @
6a3cdb5f
<
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
>
src/modules/live/score/components/ModuleCard.vue
0 → 100644
浏览文件 @
6a3cdb5f
<
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
>
src/modules/live/score/components/ScoreCardSub.vue
0 → 100644
浏览文件 @
6a3cdb5f
<
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
>
src/modules/live/score/views/View.vue
浏览文件 @
6a3cdb5f
<
script
setup
>
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
{
useChat
}
from
'@ezijing/ai-vue'
import
{
generatePrompt
}
from
'../prompt'
const
CompetitionLive
=
defineAsyncComponent
(()
=>
import
(
'../components/CompetitionLive.vue'
))
const
CompetitionOperations
=
defineAsyncComponent
(()
=>
import
(
'../components/CompetitionOperations.vue'
))
const
route
=
useRoute
()
const
id
=
route
.
query
.
id
const
{
isLoading
,
generateText
}
=
useChat
({
provider
:
'volcano'
})
const
statusList
=
useMapStore
().
getMapValuesByKey
(
'system_status'
)
const
detail
=
ref
(
null
)
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
scoreDetails
=
reactive
({})
const
totalScore
=
computed
(()
=>
{
return
Object
.
values
(
scoreDetails
).
reduce
((
acc
,
curr
)
=>
acc
+
(
curr
.
score
||
0
),
0
)
...
...
@@ -51,136 +29,6 @@ onMounted(() => {
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
()
=>
{
console
.
log
(
'AI一键评分'
)
const
result
=
await
generateText
({
...
...
@@ -198,20 +46,14 @@ const handleAIScore = async () => {
ElMessage
.
error
(
'评分失败'
)
return
}
console
.
log
(
result
.
content
)
}
const
handleSave
=
(
key
,
params
)
=>
{
scoreDetails
[
key
]
=
params
const
handleSave
=
()
=>
{
commitScore
(
0
).
then
(()
=>
{
ElMessage
.
success
(
'保存成功'
)
})
}
const
handleNext
=
()
=>
{
activeTab
.
value
++
}
const
commitScore
=
(
status
=
0
)
=>
{
return
updateScore
({
id
:
route
.
query
.
id
,
...
...
@@ -243,7 +85,12 @@ const handlePublishScore = () => {
<el-form-item
label=
"状态"
>
{{
detail
.
status_name
}}
</el-form-item>
<el-form-item
label=
"提交时间"
>
{{
detail
.
commit_time
}}
</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
>
<el-button
type=
"primary"
:disabled=
"detail.status == '2'"
@
click=
"handlePublishScore"
>
发布成绩
</el-button>
...
...
@@ -254,174 +101,12 @@ const handlePublishScore = () => {
</div>
</div>
<el-divider
/>
<el-tabs
stretch
v-model=
"activeTab"
class=
"score-tabs"
>
<el-tab-pane
label=
"商品品类管理"
:name=
"1"
>
<ScoreCard
:maxScore=
"5"
:hasSaveButton=
"detail.status != '2'"
:score=
"scoreDetails.commodity_type.score"
: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>
<CompetitionLive
:detail=
"detail"
@
save=
"handleSave"
v-model:scoreDetails=
"scoreDetails"
v-if=
"detail.competition_rule?.competition == 1"
/>
<CompetitionOperations
:detail=
"detail"
@
save=
"handleSave"
v-model:scoreDetails=
"scoreDetails"
v-else
/>
</AppCard>
</
template
>
...
...
src/modules/live/test/components/RecordView.vue
浏览文件 @
6a3cdb5f
...
...
@@ -15,6 +15,14 @@ const fetchRecord = async () => {
const
res
=
await
getRecord
({
id
:
props
.
recordId
})
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
)
{
handleAIScore
()
}
}
catch
(
error
)
{
console
.
log
(
error
)
}
}
onMounted
(()
=>
{
fetchRecord
()
...
...
@@ -72,10 +80,16 @@ const failedWords = computed(() => {
return
detail
.
value
?.
words
?.
filter
((
item
)
=>
!
item
.
time
)
||
[]
})
const
isLoading
=
ref
(
false
)
const
handleAIScore
=
()
=>
{
aiScore
({
id
:
props
.
recordId
}).
then
((
res
)
=>
{
console
.
log
(
res
)
})
isLoading
.
value
=
true
aiScore
({
id
:
props
.
recordId
})
.
then
(()
=>
{
fetchRecord
()
})
.
finally
(()
=>
{
isLoading
.
value
=
false
})
}
function
formatDuration
(
seconds
)
{
...
...
@@ -86,7 +100,7 @@ function formatDuration(seconds) {
</
script
>
<
template
>
<div
class=
"live-view"
v-if=
"detail"
>
<div
class=
"live-view"
v-if=
"detail"
v-loading=
"isLoading"
element-loading-text=
"AI打分中..."
>
<AppCard>
<div
class=
"live-info"
>
<div
class=
"live-info-header"
>
...
...
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论