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

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

上级 602d12af
<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>
...@@ -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 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论