feat(creator): 内容审核展示创作者与分集信息

This commit is contained in:
Zhou Weidong committed 2026-11-09 10:27:42 +08:00
1 parent 71ee469563
commit 837266ce7e
4 files changed
+325 -29

No files matched your search

+24 -9
View File
@@ -43,6 +43,7 @@ const {
openAudit,
confirmAudit,
freeze,
unfreeze,
createVisible,
createSaving,
createForm,
@@ -61,8 +62,8 @@ const {
<template>
<PageHeading
eyebrow="创作者管理"
title="创作者入驻"
description="审核创作者主体与资质,通过后创作者才能提交内容。"
title="创作者账号"
description="录入创作者并直接开通后台账号,账号即开即用,无需入驻审核。"
>
<el-button v-if="canAudit" type="primary" @click="openCreate">新增创作者</el-button>
</PageHeading>
@@ -170,13 +171,13 @@ const {
<template #default="{ row }">
<div class="table-actions">
<el-button type="primary" size="small" @click="openDocuments(row)">资质</el-button>
<!-- 状态机:通过可从 pending/rejected/frozen 发起,驳回只允许 pending,冻结只允许 approved。
已通过的行走「冻结 / 重置密码」,不再给「审核」入口(点了必然被后端拒) -->
<!-- 账号由后台直接发放:已通过 / 冻结行都不再给「审核」入口;
C 端提交的入驻申请仍是 pending,可在这里一次性通过 -->
<el-button
v-if="canAudit && row.status !== 'approved'"
v-if="canAudit && row.status === 'pending'"
size="small"
@click="openAudit(row, 'APPROVE')"
>审核</el-button
>通过</el-button
>
<el-button
v-if="canAudit && row.status === 'approved'"
@@ -185,12 +186,18 @@ const {
@click="freeze(row)"
>冻结</el-button
>
<el-button
v-if="canAudit && row.status === 'frozen'"
size="small"
@click="unfreeze(row)"
>解冻</el-button
>
<el-button
v-if="canOpenAccount && !row.adminUserId"
type="primary"
size="small"
:loading="accountSaving"
:disabled="row.status !== 'approved'"
:disabled="row.status === 'frozen'"
@click="openAccount(row)"
>开通账号</el-button
>
@@ -299,16 +306,24 @@ const {
<el-form-item label="国家 / 地区">
<el-input v-model="createForm.countryCode" maxlength="8" placeholder="ID" />
</el-form-item>
<el-form-item label="同时开通后台账号">
<el-switch v-model="createForm.openAccount" />
<span class="console-muted creator-switch-hint"
>开启后保存即生成登录账号与初始密码,一次完成</span
>
</el-form-item>
<el-alert
type="info"
:closable="false"
show-icon
title="编号自动生成(CR+日期+序号),也是开通账号时默认的登录名;录入后需要先通过入驻审核才能开通账号"
title="编号自动生成(CR+日期+序号),也是开通账号时默认的登录名;无需入驻审核"
/>
</el-form>
<template #footer>
<el-button @click="createVisible = false">取消</el-button>
<el-button type="primary" :loading="createSaving" @click="confirmCreate">录入</el-button>
<el-button type="primary" :loading="createSaving" @click="confirmCreate">{{
createForm.openAccount ? '创建并开通账号' : '仅创建'
}}</el-button>
</template>
</el-dialog>
+264 -16
View File
@@ -4,6 +4,7 @@
import PageHeading from '@/views/components/PageHeading.vue'
import { useRbac } from '@/stores/rbac'
import { dateCell } from '@/utils/format'
import { detailSubmissionApi } from '@/api/admin/creator'
import {
SUBMISSION_STATUS,
SUBMISSION_TYPE,
@@ -11,6 +12,7 @@ import {
submissionTypeText,
useSubmissionAudit
} from './composables/useSubmissionAudit'
import { ref } from 'vue'
const { can } = useRbac()
const canAudit = can('submission:audit')
@@ -32,9 +34,25 @@ const {
query,
reset,
handleSizeChange,
openAudit,
openAudit: rawOpenAudit,
confirmAudit
} = useSubmissionAudit()
const auditDetailLoading = ref(false)
async function openAudit(row, defaultAction = 'APPROVE') {
rawOpenAudit(row, defaultAction)
auditDetailLoading.value = true
try {
const detail = await detailSubmissionApi(row.id)
// 用 detail 里的完整字段覆盖 auditTarget(保留原始 openAudit 设置的 action)
Object.assign(auditTarget.value, detail)
} catch (e) {
console.warn('加载提交详情失败', e)
} finally {
auditDetailLoading.value = false
}
}
</script>
<template>
@@ -94,9 +112,19 @@ const {
class="drama-el-table"
empty-text="没有符合条件的提交记录"
>
<el-table-column prop="id" label="提交ID" width="90" />
<el-table-column prop="dramaId" label="短剧ID" width="90" />
<el-table-column prop="creatorId" label="创作者ID" width="100" />
<el-table-column prop="id" label="提交ID" width="140" />
<el-table-column prop="dramaId" label="短剧ID" width="140" />
<el-table-column label="创作者" min-width="180">
<template #default="{ row }">
<div style="display: flex; flex-direction: column; line-height: 1.4">
<span style="font-weight: 500">{{ row.creatorName || '—' }}</span>
<span style="font-size: 12px; color: var(--el-text-color-secondary)">
{{ row.creatorCode || '' }}{{ row.creatorCode && row.creatorMobile ? ' · ' : ''
}}{{ row.creatorMobile || '' }}
</span>
</div>
</template>
</el-table-column>
<el-table-column label="提交类型" width="110">
<template #default="{ row }">{{ submissionTypeText(row.type) }}</template>
</el-table-column>
@@ -153,18 +181,93 @@ const {
</div>
</section>
<el-dialog v-model="auditVisible" title="内容提交审核" width="480px">
<el-form label-position="top">
<el-form-item label="提交信息">
<el-input
:model-value="
auditTarget
? `提交 #${auditTarget.id} · 短剧 ${auditTarget.dramaId} · ${submissionTypeText(auditTarget.type)}`
: ''
"
disabled
/>
</el-form-item>
<el-dialog v-model="auditVisible" title="内容提交审核" width="640px">
<div v-loading="auditDetailLoading" class="audit-detail">
<!-- 顶部:创作者 + 提交信息 -->
<div class="audit-meta">
<div class="audit-meta-item">
<span class="audit-meta-label">创作者</span>
<span class="audit-meta-value">
{{ auditTarget?.creatorName || '—' }}
<span v-if="auditTarget?.creatorCode" class="audit-meta-sub">
· {{ auditTarget.creatorCode }}
</span>
<span v-if="auditTarget?.creatorMobile" class="audit-meta-sub">
· {{ auditTarget.creatorMobile }}
</span>
</span>
</div>
<div class="audit-meta-item">
<span class="audit-meta-label">提交</span>
<span class="audit-meta-value">
#{{ auditTarget?.id }} ·
<el-tag size="small" effect="plain">
{{ submissionTypeText(auditTarget?.type) }}
</el-tag>
·
<el-tag size="small" :type="submissionStatusMeta(auditTarget?.status).tag">
{{ submissionStatusMeta(auditTarget?.status).label }}
</el-tag>
<span v-if="auditTarget?.version" class="audit-meta-sub">
· v{{ auditTarget.version }}
</span>
</span>
</div>
</div>
<!-- 短剧预览卡片 -->
<div v-if="auditTarget?.dramaTitle" class="audit-drama">
<div class="audit-drama-cover">
<img
v-if="auditTarget.dramaCover"
:src="auditTarget.dramaCover"
:alt="auditTarget.dramaTitle"
/>
<div v-else class="audit-drama-cover-placeholder">无封面</div>
</div>
<div class="audit-drama-info">
<h4 class="audit-drama-title">{{ auditTarget.dramaTitle }}</h4>
<div class="audit-drama-tags">
<el-tag v-if="auditTarget.dramaGenre" size="small">{{ auditTarget.dramaGenre }}</el-tag>
<el-tag v-if="auditTarget.dramaLanguage" size="small" type="info">
{{ auditTarget.dramaLanguage }}
</el-tag>
<el-tag v-if="auditTarget.dramaTotalEpisodes != null" size="small" type="success">
共 {{ auditTarget.dramaTotalEpisodes }} 集
</el-tag>
</div>
<p v-if="auditTarget.dramaDescription" class="audit-drama-desc">
{{ auditTarget.dramaDescription }}
</p>
</div>
</div>
<div v-else class="audit-drama audit-drama-empty">短剧预览加载中…</div>
<!-- 分集预览 -->
<div v-if="auditTarget?.episodePreviews?.length" class="audit-episodes">
<div class="audit-section-title">分集列表({{ auditTarget.episodePreviews.length }})</div>
<div class="audit-episode-list">
<div v-for="ep in auditTarget.episodePreviews" :key="ep.id" class="audit-episode-item">
<div class="audit-episode-no">第{{ ep.epNo }}集</div>
<div class="audit-episode-title">{{ ep.title || '(未命名)' }}</div>
<div class="audit-episode-meta">
<span v-if="ep.durationSec">{{ ep.durationSec }}s</span>
<el-tag
v-if="ep.status"
size="small"
:type="
ep.status === 'success' ? 'success' : ep.status === 'failed' ? 'danger' : 'info'
"
>
{{ ep.status }}
</el-tag>
</div>
</div>
</div>
</div>
</div>
<el-form label-position="top" style="margin-top: 16px">
<el-form-item label="审核结果">
<el-radio-group v-model="auditForm.action">
<el-radio label="APPROVE">通过</el-radio>
@@ -201,3 +304,148 @@ const {
</template>
</el-dialog>
</template>
<style scoped>
.audit-detail {
display: flex;
flex-direction: column;
gap: 14px;
}
.audit-meta {
display: flex;
flex-wrap: wrap;
gap: 16px 28px;
padding: 10px 14px;
background: var(--el-fill-color-light);
border-radius: 8px;
font-size: 13px;
}
.audit-meta-item {
display: flex;
flex-direction: column;
gap: 2px;
}
.audit-meta-label {
font-size: 12px;
color: var(--el-text-color-secondary);
}
.audit-meta-value {
font-weight: 500;
color: var(--el-text-color-primary);
}
.audit-meta-sub {
font-weight: 400;
color: var(--el-text-color-secondary);
}
.audit-drama {
display: flex;
gap: 14px;
padding: 14px;
background: var(--el-bg-color);
border: 1px solid var(--el-border-color-lighter);
border-radius: 10px;
}
.audit-drama-cover {
width: 84px;
height: 112px;
border-radius: 6px;
overflow: hidden;
flex-shrink: 0;
background: var(--el-fill-color-light);
display: flex;
align-items: center;
justify-content: center;
}
.audit-drama-cover img {
width: 100%;
height: 100%;
object-fit: cover;
}
.audit-drama-cover-placeholder {
color: var(--el-text-color-placeholder);
font-size: 12px;
}
.audit-drama-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 6px;
}
.audit-drama-title {
margin: 0;
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.audit-drama-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.audit-drama-desc {
margin: 0;
font-size: 13px;
color: var(--el-text-color-secondary);
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.audit-drama-empty {
justify-content: center;
align-items: center;
color: var(--el-text-color-placeholder);
font-size: 13px;
}
.audit-episodes {
display: flex;
flex-direction: column;
gap: 8px;
}
.audit-section-title {
font-size: 13px;
font-weight: 500;
color: var(--el-text-color-secondary);
}
.audit-episode-list {
display: flex;
flex-direction: column;
gap: 6px;
max-height: 180px;
overflow-y: auto;
padding-right: 4px;
}
.audit-episode-item {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 12px;
background: var(--el-fill-color-light);
border-radius: 6px;
font-size: 13px;
}
.audit-episode-no {
flex-shrink: 0;
color: var(--el-text-color-secondary);
font-weight: 500;
min-width: 56px;
}
.audit-episode-title {
flex: 1;
min-width: 0;
color: var(--el-text-color-primary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.audit-episode-meta {
flex-shrink: 0;
display: flex;
align-items: center;
gap: 8px;
color: var(--el-text-color-secondary);
font-size: 12px;
}
</style>
@@ -146,6 +146,22 @@ export function useCreatorAudit() {
await load()
}
/** 解冻:冻结是可控状态,解冻后即可恢复使用与提交 */
async function unfreeze(row) {
try {
await ElMessageBox.confirm(`解冻后「${row.name}」可继续使用,确认解冻?`, '解冻创作者', {
confirmButtonText: '确认解冻',
cancelButtonText: '取消',
type: 'warning'
})
} catch {
return
}
await auditCreatorApi(row.id, { action: 'APPROVE', reason: '运营解冻' })
ElMessage.success('已解冻')
await load()
}
onMounted(load)
// ---------- 手工录入创作者主体 ----------
@@ -156,7 +172,9 @@ export function useCreatorAudit() {
type: 'COMPANY',
contactName: '',
contactEmail: '',
countryCode: 'ID'
countryCode: 'ID',
// 录入即开号:默认勾选,保存后直接弹出账号密码
openAccount: true
})
function openCreate() {
@@ -165,7 +183,8 @@ export function useCreatorAudit() {
type: 'COMPANY',
contactName: '',
contactEmail: '',
countryCode: 'ID'
countryCode: 'ID',
openAccount: true
})
createVisible.value = true
}
@@ -174,9 +193,17 @@ export function useCreatorAudit() {
if (!createForm.name.trim()) return ElMessage.warning('请填写创作者名称')
createSaving.value = true
try {
await createCreatorApi({ ...createForm, name: createForm.name.trim() })
ElMessage.success('已录入,待入驻审核通过后才能开通账号')
const created = await createCreatorApi({ ...createForm, name: createForm.name.trim() })
const creatorId = created.data
createVisible.value = false
if (createForm.openAccount && creatorId) {
// 录入后立刻开号,凭据只在这里出现一次
const res = await openCreatorAccountApi(creatorId)
accountResult.value = res.data || null
accountVisible.value = true
} else {
ElMessage.success('创作者已创建')
}
await load()
} finally {
createSaving.value = false
@@ -264,6 +291,7 @@ export function useCreatorAudit() {
openAudit,
confirmAudit,
freeze,
unfreeze,
createVisible,
createSaving,
createForm,