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:
1 parent
0a2e3e0b81
commit
685c912313
19 files changed
+995
-658
No files matched your search
@@ -19,6 +19,15 @@ export interface CreatorProfile {
|
||||
dramaTotal: number
|
||||
}
|
||||
|
||||
/** 代看目标下拉项(后端只给 id / 名称 / 状态) */
|
||||
export interface CreatorCenterTarget {
|
||||
id: number
|
||||
code: string
|
||||
name: string
|
||||
status: string
|
||||
statusText: string
|
||||
}
|
||||
|
||||
/** 我的作品 + 最近一次提交的审核进度 */
|
||||
export interface CreatorDrama {
|
||||
id: number
|
||||
@@ -132,6 +141,19 @@ export const getCreatorProfileApi = () => {
|
||||
return request.get<CreatorProfile>({ url: '/admin/creator-center/profile' })
|
||||
}
|
||||
|
||||
/**
|
||||
* 静默探测当前账号的创作者身份:不弹全局提示,直接把 Result 交给调用方判断。
|
||||
* 用于「已绑定 / 可代看但没选目标 / 未绑定」三态分流,渲染引导页。
|
||||
*/
|
||||
export const probeCreatorAccessApi = () => {
|
||||
return request.get<CreatorProfile>({ url: '/admin/creator-center/profile', silent: true })
|
||||
}
|
||||
|
||||
/** 代看目标下拉的数据源(仅 impersonate 权限可调) */
|
||||
export const listImpersonationTargetsApi = () => {
|
||||
return request.get<CreatorCenterTarget[]>({ url: '/admin/creator-center/impersonation/targets' })
|
||||
}
|
||||
|
||||
export const pageMyDramasApi = (params: Record<string, unknown>) => {
|
||||
return request.get<PageResult<CreatorDrama>>({ url: '/admin/creator-center/dramas/page', params })
|
||||
}
|
||||
|
||||
@@ -41,6 +41,10 @@ const defaultResponseInterceptors = (response: AxiosResponse) => {
|
||||
} else if (response.data.code === SUCCESS_CODE) {
|
||||
return response.data
|
||||
} else {
|
||||
// 静默请求:调用方要自己看业务码(例如创作者中心据此渲染引导页),不弹全局提示
|
||||
if ((response.config as { silent?: boolean })?.silent) {
|
||||
return response.data
|
||||
}
|
||||
ElMessage.error(response?.data?.message)
|
||||
if (response?.data?.code === 401) {
|
||||
const userStore = useUserStoreWithOut()
|
||||
|
||||
+8
-1
@@ -1,23 +1,30 @@
|
||||
import service from './service'
|
||||
import { CONTENT_TYPE } from '@/constants'
|
||||
import { useUserStoreWithOut } from '@/store/modules/user'
|
||||
import { creatorCenterHeaders } from '@/utils/creatorImpersonation'
|
||||
|
||||
const request = (option: AxiosConfig) => {
|
||||
const { url, method, params, data, headers, responseType } = option
|
||||
const { url, method, params, data, headers, responseType, silent } = option
|
||||
const userStore = useUserStoreWithOut()
|
||||
// FormData 不能带 JSON 的 Content-Type,否则 axios 会把表单转成 JSON,浏览器也无法补上 boundary
|
||||
const isFormData = typeof FormData !== 'undefined' && data instanceof FormData
|
||||
// 创作者中心的请求统一带上代看目标头(超管选了代看创作者时才有值;
|
||||
// 后端只对具备 impersonate 权限的账号认这个头,创作者带了也不生效)
|
||||
const impersonationHeaders =
|
||||
typeof url === 'string' && url.startsWith('/admin/creator-center') ? creatorCenterHeaders() : {}
|
||||
return service.request({
|
||||
url: url,
|
||||
method,
|
||||
params,
|
||||
data,
|
||||
responseType: responseType,
|
||||
silent,
|
||||
headers: {
|
||||
...(isFormData ? {} : { 'Content-Type': CONTENT_TYPE }),
|
||||
[userStore.getTokenKey ?? 'Authorization']: userStore.getToken
|
||||
? `Bearer ${userStore.getToken}`
|
||||
: '',
|
||||
...impersonationHeaders,
|
||||
...headers
|
||||
}
|
||||
})
|
||||
|
||||
@@ -18,6 +18,8 @@ interface RequestInterceptors<T> {
|
||||
|
||||
interface RequestConfig<T = AxiosResponse> extends AxiosRequestConfig {
|
||||
interceptors?: RequestInterceptors<T>
|
||||
/** 静默请求:业务码非 0 时由调用方处理,不弹全局提示(见 axios/config.ts) */
|
||||
silent?: boolean
|
||||
}
|
||||
|
||||
export {
|
||||
|
||||
@@ -217,6 +217,59 @@
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* 超管代看:顶部「当前创作者」下拉 */
|
||||
.creator-impersonate-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
padding: 10px 14px;
|
||||
margin-bottom: 14px;
|
||||
border: 1px solid #f0e6cf;
|
||||
border-radius: 6px;
|
||||
background: #fdf9ef;
|
||||
}
|
||||
|
||||
.creator-impersonate-select {
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
/* 未绑定 / 未选目标时的引导页 */
|
||||
.creator-guide {
|
||||
max-width: 720px;
|
||||
}
|
||||
|
||||
.creator-guide-title {
|
||||
margin: 0 0 10px;
|
||||
color: #302d44;
|
||||
font-size: 15px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.creator-guide-text {
|
||||
margin: 0 0 10px;
|
||||
color: #727983;
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.creator-guide-list {
|
||||
margin: 0 0 10px;
|
||||
padding-left: 18px;
|
||||
color: #727983;
|
||||
font-size: 13px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
html.dark .creator-impersonate-bar {
|
||||
border-color: var(--el-border-color-light);
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
|
||||
html.dark .creator-guide-title {
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
html.dark .creator-profile-name,
|
||||
html.dark .creator-profile-meta b,
|
||||
html.dark .creator-drama-title,
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { ref } from 'vue'
|
||||
|
||||
/**
|
||||
* 超管「代看创作者」的当前目标。
|
||||
*
|
||||
* 目标走独立请求头 `X-Creator-Id`(不做查询参数),并且**只有带 `creator-center:impersonate`
|
||||
* 权限的账号带上它才生效** —— 创作者自己带这个头没有任何作用。
|
||||
* 这里只负责"记住用户选了谁",权限判断在后端。
|
||||
*/
|
||||
const STORAGE_KEY = 'creator-center:impersonate-id'
|
||||
|
||||
function readStored(): number | null {
|
||||
const raw = localStorage.getItem(STORAGE_KEY)
|
||||
const value = raw ? Number(raw) : NaN
|
||||
return Number.isFinite(value) && value > 0 ? value : null
|
||||
}
|
||||
|
||||
export const impersonateCreatorId = ref<number | null>(readStored())
|
||||
|
||||
export function setImpersonateCreatorId(id: number | null) {
|
||||
impersonateCreatorId.value = id
|
||||
if (id) localStorage.setItem(STORAGE_KEY, String(id))
|
||||
else localStorage.removeItem(STORAGE_KEY)
|
||||
}
|
||||
|
||||
/** 创作者中心相关请求统一带上目标头(没选目标就不带) */
|
||||
export function creatorCenterHeaders(): Record<string, string> {
|
||||
return impersonateCreatorId.value ? { 'X-Creator-Id': String(impersonateCreatorId.value) } : {}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
|
||||
Vendored
+2
@@ -40,6 +40,8 @@ declare global {
|
||||
method?: AxiosMethod
|
||||
headers?: RawAxiosRequestHeaders
|
||||
responseType?: AxiosResponseType
|
||||
/** 静默请求:业务码非 0 时不弹全局提示,并把完整 Result 交给调用方判断 */
|
||||
silent?: boolean
|
||||
}
|
||||
|
||||
declare interface IResponse<T = any> {
|
||||
|
||||
Reference in new issue
Block a user