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

+37 -34
View File
@@ -1,46 +1,49 @@
<script setup>
import PageHeading from '@/views/components/PageHeading.vue'
import CreatorCenterPage from './components/CreatorCenterPage.vue'
import { DASHBOARD_GROUPS, useCreatorDashboard } from './composables/useCreatorDashboard'
const { loading, board, lastUpdated, load, display, groupValue } = useCreatorDashboard()
</script>
<template>
<PageHeading
eyebrow="创作者中心"
title="数据看板"
description="我的作品在播放、互动与解锁收入上的表现。"
>
<el-button :loading="loading" @click="load">刷新</el-button>
</PageHeading>
<CreatorCenterPage>
<PageHeading
eyebrow="创作者中心"
title="数据看板"
description="我的作品在播放、互动与解锁收入上的表现。"
>
<el-button :loading="loading" @click="load">刷新</el-button>
</PageHeading>
<section v-loading="loading" class="panel">
<p class="console-hint">数据时间:{{ lastUpdated }}(统计范围仅限本人作品)</p>
<div v-for="group in DASHBOARD_GROUPS" :key="group.key" class="board-group">
<h3 class="board-title">{{ group.title }}</h3>
<div class="board-grid">
<div v-for="metric in group.metrics" :key="metric.key" class="board-card">
<span class="board-label">{{ metric.label }}</span>
<span
class="board-value"
:class="{ 'board-value-empty': groupValue(group.key, metric.key) === null }"
>
{{ display(groupValue(group.key, metric.key)) }}
</span>
<section v-loading="loading" class="panel">
<p class="console-hint">数据时间:{{ lastUpdated }}(统计范围仅限本人作品)</p>
<div v-for="group in DASHBOARD_GROUPS" :key="group.key" class="board-group">
<h3 class="board-title">{{ group.title }}</h3>
<div class="board-grid">
<div v-for="metric in group.metrics" :key="metric.key" class="board-card">
<span class="board-label">{{ metric.label }}</span>
<span
class="board-value"
:class="{ 'board-value-empty': groupValue(group.key, metric.key) === null }"
>
{{ display(groupValue(group.key, metric.key)) }}
</span>
</div>
</div>
</div>
</div>
<el-alert
v-if="board?.notes?.length"
class="board-notes"
type="info"
:closable="false"
show-icon
title="口径与缺失数据源说明"
>
<ul>
<li v-for="note in board.notes" :key="note">{{ note }}</li>
</ul>
</el-alert>
</section>
<el-alert
v-if="board?.notes?.length"
class="board-notes"
type="info"
:closable="false"
show-icon
title="口径与缺失数据源说明"
>
<ul>
<li v-for="note in board.notes" :key="note">{{ note }}</li>
</ul>
</el-alert>
</section>
</CreatorCenterPage>
</template>
+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>
+113 -110
View File
@@ -1,5 +1,6 @@
<script setup>
import PageHeading from '@/views/components/PageHeading.vue'
import CreatorCenterPage from './components/CreatorCenterPage.vue'
import { useRbac } from '@/stores/rbac'
import { useCreatorPrice } from './composables/useCreatorPrice'
import { dramaStatusMeta } from './composables/useCreatorWorkspace'
@@ -22,120 +23,122 @@ const {
</script>
<template>
<PageHeading
eyebrow="创作者中心"
title="收费配置"
description="设置免费集数、解锁金币与试看秒数;改完立即影响播放与下载的付费判定。"
>
<el-button
type="primary"
:loading="saving"
:disabled="!canUpdate || !priceEditable"
@click="save"
>保存配置</el-button
<CreatorCenterPage>
<PageHeading
eyebrow="创作者中心"
title="收费配置"
description="设置免费集数、解锁金币与试看秒数;改完立即影响播放与下载的付费判定。"
>
</PageHeading>
<section class="panel">
<div class="console-toolbar">
<el-select
v-model="dramaId"
class="console-drama"
placeholder="选择作品"
:loading="dramas.loading"
@change="changeDrama"
<el-button
type="primary"
:loading="saving"
:disabled="!canUpdate || !priceEditable"
@click="save"
>保存配置</el-button
>
<el-option
v-for="item in dramas.rows"
:key="item.id"
:label="item.title"
:value="item.id"
/>
</el-select>
<el-tag v-if="current" :type="dramaStatusMeta(current.status).tag" effect="light">
{{ dramaStatusMeta(current.status).label }}
</el-tag>
<span class="console-count">{{ config.episodes.length }} 集</span>
</div>
</PageHeading>
<el-alert
v-if="readonlyReason"
class="creator-edit-alert"
type="warning"
:closable="false"
show-icon
:title="readonlyReason"
/>
<el-empty v-if="!dramas.rows.length" description="还没有作品,先去「我的作品」新建一部" />
</section>
<section v-if="dramaId" v-loading="loading" class="panel">
<h3 class="creator-section-title">整剧默认</h3>
<el-form class="creator-form" label-position="top" :disabled="!canUpdate || !priceEditable">
<div class="creator-form-grid">
<el-form-item label="免费集数">
<el-input-number
v-model="config.freeEpisodes"
:min="0"
:max="999"
class="console-number"
<section class="panel">
<div class="console-toolbar">
<el-select
v-model="dramaId"
class="console-drama"
placeholder="选择作品"
:loading="dramas.loading"
@change="changeDrama"
>
<el-option
v-for="item in dramas.rows"
:key="item.id"
:label="item.title"
:value="item.id"
/>
<span class="console-muted">前 N 集免费,其余按解锁金币收费</span>
</el-form-item>
<el-form-item label="整剧解锁金币">
<el-input-number
v-model="config.unlockCoin"
:min="0"
:max="9999"
class="console-number"
/>
<span class="console-muted">逐集未单独指定时生效</span>
</el-form-item>
</el-select>
<el-tag v-if="current" :type="dramaStatusMeta(current.status).tag" effect="light">
{{ dramaStatusMeta(current.status).label }}
</el-tag>
<span class="console-count">{{ config.episodes.length }} 集</span>
</div>
</el-form>
<h3 class="creator-section-title">分集明细</h3>
<el-table
:data="config.episodes"
row-key="episodeId"
border
class="drama-el-table"
empty-text="还没有分集"
>
<el-table-column prop="number" label="集号" width="70" />
<el-table-column prop="title" label="标题" min-width="160" show-overflow-tooltip />
<el-table-column label="免费" width="90">
<template #default="{ row }">
<el-switch v-model="row.free" :disabled="!canUpdate || !priceEditable" />
</template>
</el-table-column>
<el-table-column label="解锁金币" width="160">
<template #default="{ row }">
<el-input-number
v-model="row.unlockCoin"
:min="0"
:max="9999"
size="small"
:disabled="!canUpdate || !priceEditable || row.free"
/>
</template>
</el-table-column>
<el-table-column label="试看秒数" width="170">
<template #default="{ row }">
<el-input-number
v-model="row.previewSeconds"
:min="0"
:max="600"
size="small"
:disabled="!canUpdate || !priceEditable || row.free"
/>
</template>
</el-table-column>
<el-table-column label="说明" min-width="200">
<template #default="{ row }">
<span class="console-muted">{{ row.free ? '免费观看' : '0 秒 = 不限制试看' }}</span>
</template>
</el-table-column>
</el-table>
</section>
<el-alert
v-if="readonlyReason"
class="creator-edit-alert"
type="warning"
:closable="false"
show-icon
:title="readonlyReason"
/>
<el-empty v-if="!dramas.rows.length" description="还没有作品,先去「我的作品」新建一部" />
</section>
<section v-if="dramaId" v-loading="loading" class="panel">
<h3 class="creator-section-title">整剧默认</h3>
<el-form class="creator-form" label-position="top" :disabled="!canUpdate || !priceEditable">
<div class="creator-form-grid">
<el-form-item label="免费集数">
<el-input-number
v-model="config.freeEpisodes"
:min="0"
:max="999"
class="console-number"
/>
<span class="console-muted">前 N 集免费,其余按解锁金币收费</span>
</el-form-item>
<el-form-item label="整剧解锁金币">
<el-input-number
v-model="config.unlockCoin"
:min="0"
:max="9999"
class="console-number"
/>
<span class="console-muted">逐集未单独指定时生效</span>
</el-form-item>
</div>
</el-form>
<h3 class="creator-section-title">分集明细</h3>
<el-table
:data="config.episodes"
row-key="episodeId"
border
class="drama-el-table"
empty-text="还没有分集"
>
<el-table-column prop="number" label="集号" width="70" />
<el-table-column prop="title" label="标题" min-width="160" show-overflow-tooltip />
<el-table-column label="免费" width="90">
<template #default="{ row }">
<el-switch v-model="row.free" :disabled="!canUpdate || !priceEditable" />
</template>
</el-table-column>
<el-table-column label="解锁金币" width="160">
<template #default="{ row }">
<el-input-number
v-model="row.unlockCoin"
:min="0"
:max="9999"
size="small"
:disabled="!canUpdate || !priceEditable || row.free"
/>
</template>
</el-table-column>
<el-table-column label="试看秒数" width="170">
<template #default="{ row }">
<el-input-number
v-model="row.previewSeconds"
:min="0"
:max="600"
size="small"
:disabled="!canUpdate || !priceEditable || row.free"
/>
</template>
</el-table-column>
<el-table-column label="说明" min-width="200">
<template #default="{ row }">
<span class="console-muted">{{ row.free ? '免费观看' : '0 秒 = 不限制试看' }}</span>
</template>
</el-table-column>
</el-table>
</section>
</CreatorCenterPage>
</template>
+254 -239
View File
@@ -3,6 +3,8 @@ import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { Search } from '@element-plus/icons-vue'
import PageHeading from '@/views/components/PageHeading.vue'
import CreatorCenterPage from './components/CreatorCenterPage.vue'
import { useCreatorAccess } from './composables/useCreatorAccess'
import { useRbac } from '@/stores/rbac'
import { dateCell } from '@/utils/format'
import {
@@ -17,6 +19,8 @@ import {
const router = useRouter()
const { can } = useRbac()
const canEdit = can('creator-center:drama:edit')
// 代看是只读:写操作按钮直接不渲染(后端也会拒,界面不该给出做不到的入口)
const { impersonating } = useCreatorAccess()
const {
profile,
@@ -47,249 +51,260 @@ function openEdit(row) {
</script>
<template>
<PageHeading
eyebrow="创作者中心"
title="我的作品"
description="查看作品与审核进度,提交上线 / 下线申请。"
>
<el-button v-if="canEdit" type="primary" @click="openCreate">新建作品</el-button>
</PageHeading>
<CreatorCenterPage>
<PageHeading
eyebrow="创作者中心"
title="我的作品"
description="查看作品与审核进度,提交上线 / 下线申请。"
>
<el-button v-if="canEdit && !impersonating" type="primary" @click="openCreate"
>新建作品</el-button
>
</PageHeading>
<section v-loading="profileLoading" class="panel creator-profile">
<div class="creator-profile-main">
<span class="creator-profile-name">{{ profile?.name || '—' }}</span>
<el-tag
size="small"
effect="light"
:type="profile?.status === 'approved' ? 'success' : 'warning'"
>
{{ profile?.statusText || '—' }}
</el-tag>
<span class="console-muted">{{ profile?.code || '' }}</span>
</div>
<div class="creator-profile-meta">
<span
>作品数 <b>{{ profile?.dramaTotal ?? 0 }}</b></span
>
<span
>联系人 <b>{{ profile?.contactName || '—' }}</b></span
>
<span
>联系邮箱 <b>{{ profile?.contactEmail || '—' }}</b></span
>
</div>
<el-alert
v-if="profile && profile.status === 'rejected'"
class="creator-profile-alert"
type="error"
:closable="false"
show-icon
:title="`入驻审核未通过:${profile.auditNote || '未填写原因'}`"
/>
<el-alert
v-else-if="profile && profile.status !== 'approved'"
class="creator-profile-alert"
type="warning"
:closable="false"
show-icon
title="入驻审核通过后才能创建作品与提交审核"
/>
</section>
<section class="panel">
<el-tabs v-model="activeTab" @tab-change="onTabChange">
<el-tab-pane label="我的作品" name="dramas">
<div class="console-toolbar">
<el-input
v-model="dramas.keyword"
class="console-keyword"
:prefix-icon="Search"
clearable
placeholder="搜索作品名称"
@keyup.enter="queryDramas"
@clear="queryDramas"
/>
<el-select
v-model="dramas.status"
class="console-select"
clearable
placeholder="全部状态"
@change="queryDramas"
>
<el-option
v-for="item in DRAMA_STATUS"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-button type="primary" @click="queryDramas">查询</el-button>
<span class="console-count">共 {{ dramas.total }} 部作品</span>
</div>
<el-table
v-loading="dramas.loading"
:data="dramas.rows"
row-key="id"
border
class="drama-el-table"
empty-text="还没有作品,点右上角「新建作品」开始"
<section v-loading="profileLoading" class="panel creator-profile">
<div class="creator-profile-main">
<span class="creator-profile-name">{{ profile?.name || '—' }}</span>
<el-tag
size="small"
effect="light"
:type="profile?.status === 'approved' ? 'success' : 'warning'"
>
<el-table-column label="作品" min-width="220">
<template #default="{ row }">
<div class="creator-drama-cell">
<div class="creator-drama-cover">
<img v-if="row.cover" :src="row.cover" alt="" />
<span v-else>{{ (row.title || '?').slice(0, 1) }}</span>
</div>
<div class="creator-drama-meta">
<span class="creator-drama-title">{{ row.title }}</span>
<span class="creator-drama-code">{{ row.code }}</span>
</div>
</div>
</template>
</el-table-column>
<el-table-column label="状态" width="100">
<template #default="{ row }">
<el-tag :type="dramaStatusMeta(row.status).tag" effect="light">{{
dramaStatusMeta(row.status).label
}}</el-tag>
</template>
</el-table-column>
<el-table-column prop="episodes" label="分集" width="80" />
<el-table-column prop="views" label="播放" width="100" />
<el-table-column label="审核进度" min-width="200">
<template #default="{ row }">
<div v-if="row.submissionStatus" class="creator-progress">
<el-tag
size="small"
:type="submissionStatusMeta(row.submissionStatus).tag"
effect="plain"
>
{{ submissionTypeText(row.submissionType) }}·{{
submissionStatusMeta(row.submissionStatus).label
}}
</el-tag>
<span v-if="row.reviewNote" class="console-muted" :title="row.reviewNote">{{
row.reviewNote
}}</span>
</div>
<span v-else class="console-muted">尚未提交</span>
</template>
</el-table-column>
<el-table-column
prop="updatedAt"
label="更新时间"
min-width="165"
:formatter="dateCell"
show-overflow-tooltip
/>
<el-table-column label="操作" width="210" fixed="right">
<template #default="{ row }">
<div class="table-actions">
<el-button v-if="canEdit" type="primary" size="small" @click="openEdit(row)"
>编辑</el-button
>
<el-dropdown
v-if="canEdit && submitOptions(row).length"
trigger="click"
@command="(type) => submit(row, type)"
>
<el-button size="small">提交审核</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="type in submitOptions(row)"
:key="type"
:command="type"
>
{{ submissionTypeText(type) }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-button
v-if="
canEdit &&
row.submissionId &&
['SUBMITTED', 'REVIEWING'].includes(row.submissionStatus)
"
size="small"
class="danger-action"
@click="withdraw(row)"
>撤回</el-button
>
</div>
</template>
</el-table-column>
</el-table>
<div class="pagination">
<el-pagination
v-model:current-page="dramas.page"
v-model:page-size="dramas.size"
:page-sizes="[10, 20, 50]"
:total="dramas.total"
layout="total, prev, pager, next, sizes"
background
@current-change="loadDramas"
@size-change="queryDramas"
/>
</div>
</el-tab-pane>
<el-tab-pane label="通知" name="notices">
<div v-loading="notices.loading" class="creator-notice-list">
<el-empty v-if="!notices.rows.length" description="还没有审核结果通知" />
<div
v-for="item in notices.rows"
:key="`${item.bizType}-${item.id}`"
class="creator-notice"
>
<div class="creator-notice-head">
<span class="creator-notice-title">{{ item.title }}</span>
<span class="console-muted">{{
item.time ? String(item.time).replace('T', ' ').slice(0, 19) : ''
}}</span>
</div>
<p class="creator-notice-body">{{ item.content || '—' }}</p>
</div>
</div>
</el-tab-pane>
</el-tabs>
</section>
<el-dialog v-model="createVisible" title="新建作品" width="480px">
<el-form label-position="top">
<el-form-item label="作品名称" required>
<el-input
v-model="createForm.title"
maxlength="64"
placeholder="例如:闪婚后,傅先生每天都在撒糖"
/>
</el-form-item>
<el-form-item label="题材">
<el-input v-model="createForm.genre" maxlength="32" placeholder="例如:都市情感" />
</el-form-item>
<el-form-item label="简介">
<el-input
v-model="createForm.description"
type="textarea"
:rows="3"
maxlength="200"
show-word-limit
/>
</el-form-item>
{{ profile?.statusText || '—' }}
</el-tag>
<span class="console-muted">{{ profile?.code || '' }}</span>
</div>
<div class="creator-profile-meta">
<span
>作品数 <b>{{ profile?.dramaTotal ?? 0 }}</b></span
>
<span
>联系人 <b>{{ profile?.contactName || '—' }}</b></span
>
<span
>联系邮箱 <b>{{ profile?.contactEmail || '—' }}</b></span
>
</div>
<el-alert
type="info"
v-if="profile && profile.status === 'rejected'"
class="creator-profile-alert"
type="error"
:closable="false"
show-icon
title="先建草稿,再去作品编辑页完善信息、分集与视频,然后提交审核"
:title="`入驻审核未通过:${profile.auditNote || '未填写原因'}`"
/>
</el-form>
<template #footer>
<el-button @click="createVisible = false">取消</el-button>
<el-button type="primary" :loading="createSaving" @click="confirmCreate">创建草稿</el-button>
</template>
</el-dialog>
<el-alert
v-else-if="profile && profile.status !== 'approved'"
class="creator-profile-alert"
type="warning"
:closable="false"
show-icon
title="入驻审核通过后才能创建作品与提交审核"
/>
</section>
<section class="panel">
<el-tabs v-model="activeTab" @tab-change="onTabChange">
<el-tab-pane label="我的作品" name="dramas">
<div class="console-toolbar">
<el-input
v-model="dramas.keyword"
class="console-keyword"
:prefix-icon="Search"
clearable
placeholder="搜索作品名称"
@keyup.enter="queryDramas"
@clear="queryDramas"
/>
<el-select
v-model="dramas.status"
class="console-select"
clearable
placeholder="全部状态"
@change="queryDramas"
>
<el-option
v-for="item in DRAMA_STATUS"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-button type="primary" @click="queryDramas">查询</el-button>
<span class="console-count">共 {{ dramas.total }} 部作品</span>
</div>
<el-table
v-loading="dramas.loading"
:data="dramas.rows"
row-key="id"
border
class="drama-el-table"
empty-text="还没有作品,点右上角「新建作品」开始"
>
<el-table-column label="作品" min-width="220">
<template #default="{ row }">
<div class="creator-drama-cell">
<div class="creator-drama-cover">
<img v-if="row.cover" :src="row.cover" alt="" />
<span v-else>{{ (row.title || '?').slice(0, 1) }}</span>
</div>
<div class="creator-drama-meta">
<span class="creator-drama-title">{{ row.title }}</span>
<span class="creator-drama-code">{{ row.code }}</span>
</div>
</div>
</template>
</el-table-column>
<el-table-column label="状态" width="100">
<template #default="{ row }">
<el-tag :type="dramaStatusMeta(row.status).tag" effect="light">{{
dramaStatusMeta(row.status).label
}}</el-tag>
</template>
</el-table-column>
<el-table-column prop="episodes" label="分集" width="80" />
<el-table-column prop="views" label="播放" width="100" />
<el-table-column label="审核进度" min-width="200">
<template #default="{ row }">
<div v-if="row.submissionStatus" class="creator-progress">
<el-tag
size="small"
:type="submissionStatusMeta(row.submissionStatus).tag"
effect="plain"
>
{{ submissionTypeText(row.submissionType) }}·{{
submissionStatusMeta(row.submissionStatus).label
}}
</el-tag>
<span v-if="row.reviewNote" class="console-muted" :title="row.reviewNote">{{
row.reviewNote
}}</span>
</div>
<span v-else class="console-muted">尚未提交</span>
</template>
</el-table-column>
<el-table-column
prop="updatedAt"
label="更新时间"
min-width="165"
:formatter="dateCell"
show-overflow-tooltip
/>
<el-table-column label="操作" width="210" fixed="right">
<template #default="{ row }">
<div class="table-actions">
<el-button
v-if="canEdit && !impersonating"
type="primary"
size="small"
@click="openEdit(row)"
>编辑</el-button
>
<el-dropdown
v-if="canEdit && !impersonating && submitOptions(row).length"
trigger="click"
@command="(type) => submit(row, type)"
>
<el-button size="small">提交审核</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="type in submitOptions(row)"
:key="type"
:command="type"
>
{{ submissionTypeText(type) }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-button
v-if="
canEdit &&
!impersonating &&
row.submissionId &&
['SUBMITTED', 'REVIEWING'].includes(row.submissionStatus)
"
size="small"
class="danger-action"
@click="withdraw(row)"
>撤回</el-button
>
</div>
</template>
</el-table-column>
</el-table>
<div class="pagination">
<el-pagination
v-model:current-page="dramas.page"
v-model:page-size="dramas.size"
:page-sizes="[10, 20, 50]"
:total="dramas.total"
layout="total, prev, pager, next, sizes"
background
@current-change="loadDramas"
@size-change="queryDramas"
/>
</div>
</el-tab-pane>
<el-tab-pane label="通知" name="notices">
<div v-loading="notices.loading" class="creator-notice-list">
<el-empty v-if="!notices.rows.length" description="还没有审核结果通知" />
<div
v-for="item in notices.rows"
:key="`${item.bizType}-${item.id}`"
class="creator-notice"
>
<div class="creator-notice-head">
<span class="creator-notice-title">{{ item.title }}</span>
<span class="console-muted">{{
item.time ? String(item.time).replace('T', ' ').slice(0, 19) : ''
}}</span>
</div>
<p class="creator-notice-body">{{ item.content || '—' }}</p>
</div>
</div>
</el-tab-pane>
</el-tabs>
</section>
<el-dialog v-model="createVisible" title="新建作品" width="480px">
<el-form label-position="top">
<el-form-item label="作品名称" required>
<el-input
v-model="createForm.title"
maxlength="64"
placeholder="例如:闪婚后,傅先生每天都在撒糖"
/>
</el-form-item>
<el-form-item label="题材">
<el-input v-model="createForm.genre" maxlength="32" placeholder="例如:都市情感" />
</el-form-item>
<el-form-item label="简介">
<el-input
v-model="createForm.description"
type="textarea"
:rows="3"
maxlength="200"
show-word-limit
/>
</el-form-item>
<el-alert
type="info"
:closable="false"
show-icon
title="先建草稿,再去作品编辑页完善信息、分集与视频,然后提交审核"
/>
</el-form>
<template #footer>
<el-button @click="createVisible = false">取消</el-button>
<el-button type="primary" :loading="createSaving" @click="confirmCreate"
>创建草稿</el-button
>
</template>
</el-dialog>
</CreatorCenterPage>
</template>
@@ -0,0 +1,32 @@
<script setup>
defineProps({
status: { type: String, default: 'unbound' }
})
</script>
<template>
<section class="panel creator-guide">
<template v-if="status === 'need-target'">
<h3 class="creator-guide-title">请选择要代看的创作者</h3>
<p class="creator-guide-text">
你这个账号是后台账号,未绑定创作者主体,但拥有「代看创作者」权限。
在页面顶部的「当前创作者」下拉里选一位,即可只读查看他的作品、分集、收费与数据看板。
</p>
<p class="creator-guide-text">代看是只读的:编辑、上传、提交审核、改价等写操作都会被拒绝。</p>
</template>
<template v-else>
<h3 class="creator-guide-title">当前账号还没有创作者身份</h3>
<p class="creator-guide-text">
创作者中心只能由「已绑定创作者主体的后台账号」进入:先完成创作者入驻审核,再由运营把后台账号绑定到这个创作者主体上。
</p>
<ul class="creator-guide-list">
<li>入驻与审核:运营在后台「创作者管理 → 创作者入驻」里审核主体与资质。</li>
<li>账号绑定:审核通过后由运营(或 DBA)在 `creator.admin_user_id` 上绑定你的后台账号。</li>
<li>绑定后重新登录,就能看到自己的作品、分集、收费配置与数据看板。</li>
</ul>
<p class="creator-guide-text console-muted">
如果你需要只读查看其他创作者的数据,请联系管理员开通「代看创作者」权限。
</p>
</template>
</section>
</template>
@@ -0,0 +1,22 @@
<script setup>
import { computed, onMounted } from 'vue'
import CreatorAccessGuide from './CreatorAccessGuide.vue'
import ImpersonationBar from './ImpersonationBar.vue'
import { useCreatorAccess } from '../composables/useCreatorAccess'
/**
* 创作者中心四个页面共用的外壳:
* 顶部渲染「当前创作者」下拉(仅超管/有代看权限的账号可见),
* 身份不满足时渲染引导页,满足时才渲染页面内容。
*/
const { status, ensureAccess } = useCreatorAccess()
const ok = computed(() => status.value === 'granted')
onMounted(ensureAccess)
</script>
<template>
<ImpersonationBar />
<CreatorAccessGuide v-if="status === 'unbound' || status === 'need-target'" :status="status" />
<slot v-else-if="ok" />
</template>
@@ -0,0 +1,33 @@
<script setup>
import { onMounted } from 'vue'
import { useCreatorAccess } from '../composables/useCreatorAccess'
const { canImpersonate, impersonating, targets, loadTargets, changeTarget, currentCreatorId } =
useCreatorAccess()
onMounted(() => {
if (canImpersonate) loadTargets()
})
</script>
<template>
<div v-if="canImpersonate" class="creator-impersonate-bar">
<span class="console-muted">当前创作者</span>
<el-select
:model-value="currentCreatorId"
class="creator-impersonate-select"
clearable
placeholder="请选择要代看的创作者"
@change="changeTarget"
>
<el-option
v-for="item in targets"
:key="item.id"
:label="`${item.name}(${item.code}·${item.statusText})`"
:value="item.id"
/>
</el-select>
<el-tag v-if="impersonating" type="warning" effect="light">只读代看</el-tag>
<span v-if="impersonating" class="console-muted">代看状态下所有修改操作都会被拒绝</span>
</div>
</template>
@@ -0,0 +1,81 @@
import { computed, ref } from 'vue'
import { listImpersonationTargetsApi, probeCreatorAccessApi } from '@/api/admin/creatorCenter'
import { useRbac } from '@/stores/rbac'
import { impersonateCreatorId, setImpersonateCreatorId } from '@/utils/creatorImpersonation'
/** 后端可识别的身份类错误码,与 ErrorCode 保持一致 */
const CODE_NOT_BOUND = 200004
const CODE_TARGET_REQUIRED = 200005
/** 一次会话里只探测一次:多个页面切换不重复打接口 */
let probePromise = null
/**
* 创作者中心的"身份三态":
* - granted:已绑定创作者,或超管指定了代看目标 → 正常渲染页面
* - unbound:未绑定且没有代看权限 → 渲染引导页
* - need-target:有代看权限但还没选目标 → 渲染引导页(提示去顶部选)
*/
export function useCreatorAccess() {
const { can } = useRbac()
const canImpersonate = can('creator-center:impersonate')
const status = ref('unknown')
const impersonating = computed(() => canImpersonate && !!impersonateCreatorId.value)
const targets = ref([])
const targetsLoaded = ref(false)
async function ensureAccess() {
if (status.value === 'granted') return true
if (probePromise) return probePromise
probePromise = probeCreatorAccessApi()
.then((res) => {
if (res?.code === 0) {
status.value = 'granted'
return true
}
if (res?.code === CODE_TARGET_REQUIRED) {
status.value = 'need-target'
return false
}
if (res?.code === CODE_NOT_BOUND) {
status.value = 'unbound'
return false
}
status.value = canImpersonate ? 'need-target' : 'unbound'
return false
})
.catch(() => {
status.value = canImpersonate ? 'need-target' : 'unbound'
return false
})
.finally(() => {
probePromise = null
})
return probePromise
}
async function loadTargets() {
if (!canImpersonate || targetsLoaded.value) return
const res = await listImpersonationTargetsApi()
targets.value = res?.data || []
targetsLoaded.value = true
}
/** 切换代看目标:重新探测身份并刷新页面数据(数据加载都挂在页面挂载时,最省事也最不易漏) */
function changeTarget(id) {
setImpersonateCreatorId(id || null)
window.location.reload()
}
return {
canImpersonate,
status,
impersonating,
targets,
ensureAccess,
loadTargets,
changeTarget,
currentCreatorId: impersonateCreatorId
}
}
@@ -1,5 +1,6 @@
import { onMounted, ref } from 'vue'
import { getCreatorDashboardApi } from '@/api/admin/creatorCenter'
import { useCreatorAccess } from './useCreatorAccess'
/**
* 指标展示配置:key 与后端 /admin/creator-center/dashboard 的字段一一对应,
@@ -56,6 +57,7 @@ export const DASHBOARD_GROUPS = [
]
export function useCreatorDashboard() {
const { ensureAccess } = useCreatorAccess()
const loading = ref(false)
const board = ref(null)
const lastUpdated = ref('')
@@ -81,7 +83,10 @@ export function useCreatorDashboard() {
return board.value?.[groupKey]?.[metricKey]
}
onMounted(load)
onMounted(async () => {
if (!(await ensureAccess())) return
await load()
})
return { loading, board, lastUpdated, load, display, groupValue }
}
@@ -2,8 +2,10 @@ import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { getPricesApi, pageMyDramasApi, savePricesApi } from '@/api/admin/creatorCenter'
import { dramaStatusMeta } from './useCreatorWorkspace'
import { useCreatorAccess } from './useCreatorAccess'
export function useCreatorPrice() {
const { ensureAccess, impersonating } = useCreatorAccess()
const dramas = reactive({ rows: [], loading: false })
const dramaId = ref(null)
const config = reactive({ freeEpisodes: 0, unlockCoin: 0, episodes: [] })
@@ -17,6 +19,7 @@ export function useCreatorPrice() {
* 已上线要改价得先申请下线,避免创作者无审核地改已上线内容的价格。
*/
const priceEditable = computed(() => {
if (impersonating.value) return false
const status = current.value?.status
return status === 'draft' || status === 'offline'
})
@@ -80,6 +83,7 @@ export function useCreatorPrice() {
}
onMounted(async () => {
if (!(await ensureAccess())) return
await loadDramas()
await loadPrices()
})
@@ -8,6 +8,7 @@ import {
submitDramaApi,
withdrawSubmissionApi
} from '@/api/admin/creatorCenter'
import { useCreatorAccess } from './useCreatorAccess'
/** 作品状态:与后端 ContentStatus 一一对应 */
export const DRAMA_STATUS = [
@@ -70,6 +71,7 @@ export function submitOptions(row) {
}
export function useCreatorWorkspace() {
const { ensureAccess } = useCreatorAccess()
const profileLoading = ref(false)
const profile = ref(null)
@@ -190,7 +192,9 @@ export function useCreatorWorkspace() {
}
}
onMounted(() => {
onMounted(async () => {
// 身份不满足(未绑定 / 未选代看目标)时不打业务接口,交给外层渲染引导页
if (!(await ensureAccess())) return
loadProfile()
loadDramas()
})
@@ -17,6 +17,7 @@ import {
import { dateCell } from '@/utils/format'
import { probeVideoDuration, resumeUpload, uploadFile } from '@/utils/uploader'
import { submissionStatusMeta, submissionTypeText, submitOptions } from './useCreatorWorkspace'
import { useCreatorAccess } from './useCreatorAccess'
/** 中断的上传会话按「作品 + 文件名 + 大小」记住,刷新后还能接着传 */
const RESUME_KEY = 'creator-upload-sessions'
@@ -37,6 +38,7 @@ function writeSession(key, value) {
}
export function useDramaEdit() {
const { ensureAccess, impersonating } = useCreatorAccess()
const route = useRoute()
const dramaId = Number(route.params.id)
@@ -56,10 +58,13 @@ export function useDramaEdit() {
})
/** 只有草稿与已下线能改(与后端 requireEditable 一致),其余状态页面转只读 */
const editable = computed(
() => drama.value?.status === 'draft' || drama.value?.status === 'offline'
)
const editable = computed(() => {
// 代看只读:整页禁用,与后端 requireWritable 的边界保持一致
if (impersonating.value) return false
return drama.value?.status === 'draft' || drama.value?.status === 'offline'
})
const readonlyReason = computed(() => {
if (impersonating.value) return '代看模式只读:可以查看作品与分集,但不能编辑、上传或提交'
switch (drama.value?.status) {
case 'published':
return '已上线的作品只读,要改内容请先提交「申请下线」'
@@ -304,7 +309,10 @@ export function useDramaEdit() {
await loadAll()
}
onMounted(loadAll)
onMounted(async () => {
if (!(await ensureAccess())) return
await loadAll()
})
return {
dramaId,