feat(web): 创作者中心「当前创作者」代看下拉 + 未绑定引导页

为什么:超管点进创作者中心原本是一片"未绑定创作者主体"的报错,既没说明原因也没有出路。
这批把两件事做出来:超管能在顶部选一个创作者只读代看;没有创作者身份的账号看到明确的引导页而不是白屏。

- 新增 useCreatorAccess(身份三态:granted / need-target / unbound)+ ImpersonationBar + CreatorAccessGuide,
  四个页面用 CreatorCenterPage 统一套壳;身份不满足时不打业务接口,避免一串无意义的报错提示
- 代看目标存 localStorage,由 axios.request 统一给 /admin/creator-center 的请求加 X-Creator-Id 头
  (不做查询参数;后端只对具备 impersonate 权限的账号认这个头)
- axios 支持 silent 请求:业务码非 0 时不弹全局提示、把 Result 交给调用方判断(引导页据此分流)
- 代看只读体验:代看态下「新建作品 / 编辑 / 提交审核 / 撤回」不渲染,编辑页整页禁用并提示原因
- 顶部下拉只在有 impersonate 权限时显示;无权限账号看不到、也不会请求 targets 接口
This commit is contained in:
Vicky committed 2026-10-15 15:25:53 +08:00
1 parent 0a2e3e0b81
commit 685c912313
19 files changed
+995 -658

No files matched your search

+276 -268
View File
@@ -1,13 +1,17 @@
<script setup>
import { ArrowLeft, Upload } from '@element-plus/icons-vue'
import PageHeading from '@/views/components/PageHeading.vue'
import CreatorCenterPage from './components/CreatorCenterPage.vue'
import { useRbac } from '@/stores/rbac'
import { useDramaEdit } from './composables/useDramaEdit'
import { useCreatorAccess } from './composables/useCreatorAccess'
import { dramaStatusMeta } from './composables/useCreatorWorkspace'
const { can } = useRbac()
const canEdit = can('creator-center:drama:edit')
const canUpload = can('creator-center:upload')
// 代看只读:提交/撤回这些写入口直接不渲染
const { impersonating } = useCreatorAccess()
const {
loading,
@@ -60,295 +64,299 @@ function requestDramaCover(options) {
</script>
<template>
<PageHeading
eyebrow="创作者中心"
title="作品编辑"
:description="drama ? `${drama.title}(${drama.code})` : ''"
>
<el-button :icon="ArrowLeft" @click="back">返回</el-button>
</PageHeading>
<CreatorCenterPage>
<PageHeading
eyebrow="创作者中心"
title="作品编辑"
:description="drama ? `${drama.title}(${drama.code})` : ''"
>
<el-button :icon="ArrowLeft" @click="back">返回</el-button>
</PageHeading>
<section v-loading="loading" class="panel">
<div class="creator-edit-head">
<el-tag :type="dramaStatusMeta(drama?.status).tag" effect="light">
{{ dramaStatusMeta(drama?.status).label }}
</el-tag>
<span class="console-muted"
>分集 {{ drama?.episodes ?? 0 }} 集 · 播放 {{ drama?.views ?? 0 }}</span
>
<div class="creator-edit-actions">
<section v-loading="loading" class="panel">
<div class="creator-edit-head">
<el-tag :type="dramaStatusMeta(drama?.status).tag" effect="light">
{{ dramaStatusMeta(drama?.status).label }}
</el-tag>
<span class="console-muted"
>分集 {{ drama?.episodes ?? 0 }} 集 · 播放 {{ drama?.views ?? 0 }}</span
>
<div class="creator-edit-actions">
<el-button
v-for="type in canEdit && !impersonating ? submitOptions(drama || {}) : []"
:key="type"
type="primary"
plain
size="small"
@click="submit(type)"
>{{ submissionTypeText(type) }}</el-button
>
</div>
</div>
<el-alert
v-if="readonlyReason"
class="creator-edit-alert"
type="warning"
:closable="false"
show-icon
:title="readonlyReason"
/>
</section>
<section class="panel">
<h3 class="creator-section-title">作品信息</h3>
<el-form class="creator-form" label-position="top" :disabled="!editable">
<div class="creator-form-grid">
<el-form-item label="作品名称" required>
<el-input v-model="infoForm.title" maxlength="64" />
</el-form-item>
<el-form-item label="副标题">
<el-input v-model="infoForm.subtitle" maxlength="64" />
</el-form-item>
<el-form-item label="题材">
<el-input v-model="infoForm.genre" maxlength="32" placeholder="例如:都市情感" />
</el-form-item>
<el-form-item label="语言">
<el-input v-model="infoForm.language" maxlength="16" placeholder="zh-CN / id-ID" />
</el-form-item>
</div>
<el-form-item label="简介">
<el-input
v-model="infoForm.description"
type="textarea"
:rows="3"
maxlength="500"
show-word-limit
/>
</el-form-item>
<div class="creator-form-grid">
<el-form-item label="作品封面">
<div class="creator-cover-row">
<div class="creator-cover-preview">
<img v-if="infoForm.cover" :src="infoForm.cover" alt="" />
<span v-else>无</span>
</div>
<el-upload
v-if="canUpload"
:show-file-list="false"
accept="image/*"
:http-request="requestDramaCover"
:disabled="uploadingId !== null"
>
<el-button size="small" :icon="Upload">上传封面</el-button>
</el-upload>
</div>
</el-form-item>
<el-form-item label="预告片地址">
<el-input
v-model="infoForm.trailerUrl"
maxlength="512"
placeholder="整剧一支预告片,可留空"
/>
</el-form-item>
</div>
<el-button
v-for="type in canEdit ? submitOptions(drama || {}) : []"
:key="type"
v-if="canEdit"
type="primary"
:loading="savingInfo"
:disabled="!editable"
@click="saveInfo"
>保存作品信息</el-button
>
</el-form>
</section>
<section class="panel">
<div class="creator-section-head">
<h3 class="creator-section-title">分集管理</h3>
<el-button
v-if="canEdit"
type="primary"
plain
size="small"
@click="submit(type)"
>{{ submissionTypeText(type) }}</el-button
:disabled="!editable"
@click="openEpisode(null)"
>新增分集</el-button
>
</div>
</div>
<el-alert
v-if="readonlyReason"
class="creator-edit-alert"
type="warning"
:closable="false"
show-icon
:title="readonlyReason"
/>
</section>
<el-alert
v-if="resumeHint"
class="creator-edit-alert"
type="info"
:closable="false"
show-icon
:title="resumeHint"
/>
<el-table :data="episodes" row-key="id" border class="drama-el-table" empty-text="还没有分集">
<el-table-column prop="number" label="集号" width="70" />
<el-table-column label="封面" width="88">
<template #default="{ row }">
<div class="creator-episode-cover">
<img v-if="row.cover" :src="row.cover" alt="" />
<span v-else>无</span>
</div>
</template>
</el-table-column>
<el-table-column prop="title" label="标题" min-width="150" show-overflow-tooltip />
<el-table-column prop="duration" label="时长" width="90" />
<el-table-column label="片源" min-width="120">
<template #default="{ row }">
<span class="console-muted">{{ row.videoUrl ? '已上传' : '未上传' }}</span>
</template>
</el-table-column>
<el-table-column label="状态" width="90">
<template #default="{ row }">
<el-tag
size="small"
effect="light"
:type="row.status === 'published' ? 'success' : 'info'"
>
{{ row.statusText }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="300" fixed="right">
<template #default="{ row, $index }">
<div class="table-actions">
<el-button
size="small"
:disabled="!editable || $index === 0"
@click="moveEpisode(row, -1)"
>上移</el-button
>
<el-button
size="small"
:disabled="!editable || $index === episodes.length - 1"
@click="moveEpisode(row, 1)"
>下移</el-button
>
<el-button size="small" :disabled="!editable" @click="openEpisode(row)"
>编辑</el-button
>
<el-upload
v-if="canUpload"
:show-file-list="false"
accept="video/*"
:http-request="(options) => requestVideo(row, options)"
:disabled="!editable || uploadingId !== null"
>
<el-button size="small">{{ hasResumeSession ? '继续上传' : '上传视频' }}</el-button>
</el-upload>
<el-button
size="small"
class="danger-action"
:disabled="!editable"
@click="removeEpisode(row)"
>删除</el-button
>
</div>
</template>
</el-table-column>
</el-table>
<p v-if="uploadingId !== null" class="creator-upload-hint"
>上传中 {{ uploadPercent }}%(分片上传,可断点续传)</p
>
</section>
<section class="panel">
<h3 class="creator-section-title">作品信息</h3>
<el-form class="creator-form" label-position="top" :disabled="!editable">
<div class="creator-form-grid">
<el-form-item label="作品名称" required>
<el-input v-model="infoForm.title" maxlength="64" />
</el-form-item>
<el-form-item label="副标题">
<el-input v-model="infoForm.subtitle" maxlength="64" />
</el-form-item>
<el-form-item label="题材">
<el-input v-model="infoForm.genre" maxlength="32" placeholder="例如:都市情感" />
</el-form-item>
<el-form-item label="语言">
<el-input v-model="infoForm.language" maxlength="16" placeholder="zh-CN / id-ID" />
</el-form-item>
</div>
<el-form-item label="简介">
<el-input
v-model="infoForm.description"
type="textarea"
:rows="3"
maxlength="500"
show-word-limit
<section class="panel">
<h3 class="creator-section-title">提交记录</h3>
<el-table
:data="submissions"
row-key="id"
border
class="drama-el-table"
empty-text="还没有提交记录"
>
<el-table-column prop="version" label="版本" width="70" />
<el-table-column label="类型" width="100">
<template #default="{ row }">{{ submissionTypeText(row.type) }}</template>
</el-table-column>
<el-table-column label="状态" width="100">
<template #default="{ row }">
<el-tag size="small" :type="submissionStatusMeta(row.status).tag" effect="light">
{{ submissionStatusMeta(row.status).label }}
</el-tag>
</template>
</el-table-column>
<el-table-column
prop="submitAt"
label="提交时间"
min-width="165"
:formatter="dateCell"
show-overflow-tooltip
/>
</el-form-item>
<div class="creator-form-grid">
<el-form-item label="作品封面">
<el-table-column
prop="reviewAt"
label="审核时间"
min-width="165"
:formatter="dateCell"
show-overflow-tooltip
/>
<el-table-column prop="reviewNote" label="审核意见" min-width="200" show-overflow-tooltip />
<el-table-column label="操作" width="110" fixed="right">
<template #default="{ row }">
<div class="table-actions">
<el-button
v-if="canEdit && ['SUBMITTED', 'REVIEWING'].includes(row.status)"
size="small"
class="danger-action"
@click="withdraw(row.id)"
>撤回</el-button
>
<span v-else class="console-muted">—</span>
</div>
</template>
</el-table-column>
</el-table>
</section>
<el-dialog
v-model="episodeVisible"
:title="episodeEditingId ? '编辑分集' : '新增分集'"
width="480px"
>
<el-form label-position="top">
<el-form-item label="分集标题">
<el-input v-model="episodeForm.title" maxlength="64" />
</el-form-item>
<el-form-item label="时长(秒)">
<el-input-number
v-model="episodeForm.durationSec"
:min="0"
:max="36000"
class="console-number"
/>
<span class="console-muted">上传视频后会自动带出时长</span>
</el-form-item>
<el-form-item v-if="episodeEditingId" label="封面">
<div class="creator-cover-row">
<div class="creator-cover-preview">
<img v-if="infoForm.cover" :src="infoForm.cover" alt="" />
<img v-if="episodeForm.cover" :src="episodeForm.cover" alt="" />
<span v-else>无</span>
</div>
<el-upload
v-if="canUpload"
:show-file-list="false"
accept="image/*"
:http-request="requestDramaCover"
:disabled="uploadingId !== null"
:http-request="(options) => requestEpisodeCover({ id: episodeEditingId }, options)"
>
<el-button size="small" :icon="Upload">上传封面</el-button>
</el-upload>
</div>
</el-form-item>
<el-form-item label="预告片地址">
<el-input
v-model="infoForm.trailerUrl"
maxlength="512"
placeholder="整剧一支预告片,可留空"
/>
<el-form-item v-if="episodeEditingId" label="集号">
<el-input-number v-model="episodeForm.epNo" :min="1" :max="9999" class="console-number" />
</el-form-item>
</div>
<el-button
v-if="canEdit"
type="primary"
:loading="savingInfo"
:disabled="!editable"
@click="saveInfo"
>保存作品信息</el-button
>
</el-form>
</section>
<section class="panel">
<div class="creator-section-head">
<h3 class="creator-section-title">分集管理</h3>
<el-button
v-if="canEdit"
type="primary"
size="small"
:disabled="!editable"
@click="openEpisode(null)"
>新增分集</el-button
>
</div>
<el-alert
v-if="resumeHint"
class="creator-edit-alert"
type="info"
:closable="false"
show-icon
:title="resumeHint"
/>
<el-table :data="episodes" row-key="id" border class="drama-el-table" empty-text="还没有分集">
<el-table-column prop="number" label="集号" width="70" />
<el-table-column label="封面" width="88">
<template #default="{ row }">
<div class="creator-episode-cover">
<img v-if="row.cover" :src="row.cover" alt="" />
<span v-else>无</span>
</div>
</template>
</el-table-column>
<el-table-column prop="title" label="标题" min-width="150" show-overflow-tooltip />
<el-table-column prop="duration" label="时长" width="90" />
<el-table-column label="片源" min-width="120">
<template #default="{ row }">
<span class="console-muted">{{ row.videoUrl ? '已上传' : '未上传' }}</span>
</template>
</el-table-column>
<el-table-column label="状态" width="90">
<template #default="{ row }">
<el-tag
size="small"
effect="light"
:type="row.status === 'published' ? 'success' : 'info'"
>
{{ row.statusText }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="300" fixed="right">
<template #default="{ row, $index }">
<div class="table-actions">
<el-button
size="small"
:disabled="!editable || $index === 0"
@click="moveEpisode(row, -1)"
>上移</el-button
>
<el-button
size="small"
:disabled="!editable || $index === episodes.length - 1"
@click="moveEpisode(row, 1)"
>下移</el-button
>
<el-button size="small" :disabled="!editable" @click="openEpisode(row)">编辑</el-button>
<el-upload
v-if="canUpload"
:show-file-list="false"
accept="video/*"
:http-request="(options) => requestVideo(row, options)"
:disabled="!editable || uploadingId !== null"
>
<el-button size="small">{{ hasResumeSession ? '继续上传' : '上传视频' }}</el-button>
</el-upload>
<el-button
size="small"
class="danger-action"
:disabled="!editable"
@click="removeEpisode(row)"
>删除</el-button
>
</div>
</template>
</el-table-column>
</el-table>
<p v-if="uploadingId !== null" class="creator-upload-hint"
>上传中 {{ uploadPercent }}%(分片上传,可断点续传)</p
>
</section>
<section class="panel">
<h3 class="creator-section-title">提交记录</h3>
<el-table
:data="submissions"
row-key="id"
border
class="drama-el-table"
empty-text="还没有提交记录"
>
<el-table-column prop="version" label="版本" width="70" />
<el-table-column label="类型" width="100">
<template #default="{ row }">{{ submissionTypeText(row.type) }}</template>
</el-table-column>
<el-table-column label="状态" width="100">
<template #default="{ row }">
<el-tag size="small" :type="submissionStatusMeta(row.status).tag" effect="light">
{{ submissionStatusMeta(row.status).label }}
</el-tag>
</template>
</el-table-column>
<el-table-column
prop="submitAt"
label="提交时间"
min-width="165"
:formatter="dateCell"
show-overflow-tooltip
/>
<el-table-column
prop="reviewAt"
label="审核时间"
min-width="165"
:formatter="dateCell"
show-overflow-tooltip
/>
<el-table-column prop="reviewNote" label="审核意见" min-width="200" show-overflow-tooltip />
<el-table-column label="操作" width="110" fixed="right">
<template #default="{ row }">
<div class="table-actions">
<el-button
v-if="canEdit && ['SUBMITTED', 'REVIEWING'].includes(row.status)"
size="small"
class="danger-action"
@click="withdraw(row.id)"
>撤回</el-button
>
<span v-else class="console-muted">—</span>
</div>
</template>
</el-table-column>
</el-table>
</section>
<el-dialog
v-model="episodeVisible"
:title="episodeEditingId ? '编辑分集' : '新增分集'"
width="480px"
>
<el-form label-position="top">
<el-form-item label="分集标题">
<el-input v-model="episodeForm.title" maxlength="64" />
</el-form-item>
<el-form-item label="时长(秒)">
<el-input-number
v-model="episodeForm.durationSec"
:min="0"
:max="36000"
class="console-number"
<el-alert
type="info"
:closable="false"
show-icon
title="免费 / 收费与试看秒数在「收费配置」页设置;新分集默认免费"
/>
<span class="console-muted">上传视频后会自动带出时长</span>
</el-form-item>
<el-form-item v-if="episodeEditingId" label="封面">
<div class="creator-cover-row">
<div class="creator-cover-preview">
<img v-if="episodeForm.cover" :src="episodeForm.cover" alt="" />
<span v-else>无</span>
</div>
<el-upload
v-if="canUpload"
:show-file-list="false"
accept="image/*"
:http-request="(options) => requestEpisodeCover({ id: episodeEditingId }, options)"
>
<el-button size="small" :icon="Upload">上传封面</el-button>
</el-upload>
</div>
</el-form-item>
<el-form-item v-if="episodeEditingId" label="集号">
<el-input-number v-model="episodeForm.epNo" :min="1" :max="9999" class="console-number" />
</el-form-item>
<el-alert
type="info"
:closable="false"
show-icon
title="免费 / 收费与试看秒数在「收费配置」页设置;新分集默认免费"
/>
</el-form>
<template #footer>
<el-button @click="episodeVisible = false">取消</el-button>
<el-button type="primary" :loading="episodeSaving" @click="saveEpisode">保存</el-button>
</template>
</el-dialog>
</el-form>
<template #footer>
<el-button @click="episodeVisible = false">取消</el-button>
<el-button type="primary" :loading="episodeSaving" @click="saveEpisode">保存</el-button>
</template>
</el-dialog>
</CreatorCenterPage>
</template>