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
+350 -13

No files matched your search

+22
View File
@@ -19,6 +19,15 @@ export interface CreatorProfile {
dramaTotal: number dramaTotal: number
} }
/** 代看目标下拉项(后端只给 id / 名称 / 状态) */
export interface CreatorCenterTarget {
id: number
code: string
name: string
status: string
statusText: string
}
/** 我的作品 + 最近一次提交的审核进度 */ /** 我的作品 + 最近一次提交的审核进度 */
export interface CreatorDrama { export interface CreatorDrama {
id: number id: number
@@ -132,6 +141,19 @@ export const getCreatorProfileApi = () => {
return request.get<CreatorProfile>({ url: '/admin/creator-center/profile' }) 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>) => { export const pageMyDramasApi = (params: Record<string, unknown>) => {
return request.get<PageResult<CreatorDrama>>({ url: '/admin/creator-center/dramas/page', params }) return request.get<PageResult<CreatorDrama>>({ url: '/admin/creator-center/dramas/page', params })
} }
+4
View File
@@ -41,6 +41,10 @@ const defaultResponseInterceptors = (response: AxiosResponse) => {
} else if (response.data.code === SUCCESS_CODE) { } else if (response.data.code === SUCCESS_CODE) {
return response.data return response.data
} else { } else {
// 静默请求:调用方要自己看业务码(例如创作者中心据此渲染引导页),不弹全局提示
if ((response.config as { silent?: boolean })?.silent) {
return response.data
}
ElMessage.error(response?.data?.message) ElMessage.error(response?.data?.message)
if (response?.data?.code === 401) { if (response?.data?.code === 401) {
const userStore = useUserStoreWithOut() const userStore = useUserStoreWithOut()
+8 -1
View File
@@ -1,23 +1,30 @@
import service from './service' import service from './service'
import { CONTENT_TYPE } from '@/constants' import { CONTENT_TYPE } from '@/constants'
import { useUserStoreWithOut } from '@/store/modules/user' import { useUserStoreWithOut } from '@/store/modules/user'
import { creatorCenterHeaders } from '@/utils/creatorImpersonation'
const request = (option: AxiosConfig) => { const request = (option: AxiosConfig) => {
const { url, method, params, data, headers, responseType } = option const { url, method, params, data, headers, responseType, silent } = option
const userStore = useUserStoreWithOut() const userStore = useUserStoreWithOut()
// FormData 不能带 JSON 的 Content-Type,否则 axios 会把表单转成 JSON,浏览器也无法补上 boundary // FormData 不能带 JSON 的 Content-Type,否则 axios 会把表单转成 JSON,浏览器也无法补上 boundary
const isFormData = typeof FormData !== 'undefined' && data instanceof FormData const isFormData = typeof FormData !== 'undefined' && data instanceof FormData
// 创作者中心的请求统一带上代看目标头(超管选了代看创作者时才有值;
// 后端只对具备 impersonate 权限的账号认这个头,创作者带了也不生效)
const impersonationHeaders =
typeof url === 'string' && url.startsWith('/admin/creator-center') ? creatorCenterHeaders() : {}
return service.request({ return service.request({
url: url, url: url,
method, method,
params, params,
data, data,
responseType: responseType, responseType: responseType,
silent,
headers: { headers: {
...(isFormData ? {} : { 'Content-Type': CONTENT_TYPE }), ...(isFormData ? {} : { 'Content-Type': CONTENT_TYPE }),
[userStore.getTokenKey ?? 'Authorization']: userStore.getToken [userStore.getTokenKey ?? 'Authorization']: userStore.getToken
? `Bearer ${userStore.getToken}` ? `Bearer ${userStore.getToken}`
: '', : '',
...impersonationHeaders,
...headers ...headers
} }
}) })
+2
View File
@@ -18,6 +18,8 @@ interface RequestInterceptors<T> {
interface RequestConfig<T = AxiosResponse> extends AxiosRequestConfig { interface RequestConfig<T = AxiosResponse> extends AxiosRequestConfig {
interceptors?: RequestInterceptors<T> interceptors?: RequestInterceptors<T>
/** 静默请求:业务码非 0 时由调用方处理,不弹全局提示(见 axios/config.ts) */
silent?: boolean
} }
export { export {
+53
View File
@@ -217,6 +217,59 @@
font-size: 13px; 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-name,
html.dark .creator-profile-meta b, html.dark .creator-profile-meta b,
html.dark .creator-drama-title, html.dark .creator-drama-title,
+29
View File
@@ -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,11 +1,13 @@
<script setup> <script setup>
import PageHeading from '@/views/components/PageHeading.vue' import PageHeading from '@/views/components/PageHeading.vue'
import CreatorCenterPage from './components/CreatorCenterPage.vue'
import { DASHBOARD_GROUPS, useCreatorDashboard } from './composables/useCreatorDashboard' import { DASHBOARD_GROUPS, useCreatorDashboard } from './composables/useCreatorDashboard'
const { loading, board, lastUpdated, load, display, groupValue } = useCreatorDashboard() const { loading, board, lastUpdated, load, display, groupValue } = useCreatorDashboard()
</script> </script>
<template> <template>
<CreatorCenterPage>
<PageHeading <PageHeading
eyebrow="创作者中心" eyebrow="创作者中心"
title="数据看板" title="数据看板"
@@ -43,4 +45,5 @@ const { loading, board, lastUpdated, load, display, groupValue } = useCreatorDas
</ul> </ul>
</el-alert> </el-alert>
</section> </section>
</CreatorCenterPage>
</template> </template>
+10 -2
View File
@@ -1,13 +1,17 @@
<script setup> <script setup>
import { ArrowLeft, Upload } from '@element-plus/icons-vue' import { ArrowLeft, Upload } from '@element-plus/icons-vue'
import PageHeading from '@/views/components/PageHeading.vue' import PageHeading from '@/views/components/PageHeading.vue'
import CreatorCenterPage from './components/CreatorCenterPage.vue'
import { useRbac } from '@/stores/rbac' import { useRbac } from '@/stores/rbac'
import { useDramaEdit } from './composables/useDramaEdit' import { useDramaEdit } from './composables/useDramaEdit'
import { useCreatorAccess } from './composables/useCreatorAccess'
import { dramaStatusMeta } from './composables/useCreatorWorkspace' import { dramaStatusMeta } from './composables/useCreatorWorkspace'
const { can } = useRbac() const { can } = useRbac()
const canEdit = can('creator-center:drama:edit') const canEdit = can('creator-center:drama:edit')
const canUpload = can('creator-center:upload') const canUpload = can('creator-center:upload')
// 代看只读:提交/撤回这些写入口直接不渲染
const { impersonating } = useCreatorAccess()
const { const {
loading, loading,
@@ -60,6 +64,7 @@ function requestDramaCover(options) {
</script> </script>
<template> <template>
<CreatorCenterPage>
<PageHeading <PageHeading
eyebrow="创作者中心" eyebrow="创作者中心"
title="作品编辑" title="作品编辑"
@@ -78,7 +83,7 @@ function requestDramaCover(options) {
> >
<div class="creator-edit-actions"> <div class="creator-edit-actions">
<el-button <el-button
v-for="type in canEdit ? submitOptions(drama || {}) : []" v-for="type in canEdit && !impersonating ? submitOptions(drama || {}) : []"
:key="type" :key="type"
type="primary" type="primary"
plain plain
@@ -224,7 +229,9 @@ function requestDramaCover(options) {
@click="moveEpisode(row, 1)" @click="moveEpisode(row, 1)"
>下移</el-button >下移</el-button
> >
<el-button size="small" :disabled="!editable" @click="openEpisode(row)">编辑</el-button> <el-button size="small" :disabled="!editable" @click="openEpisode(row)"
>编辑</el-button
>
<el-upload <el-upload
v-if="canUpload" v-if="canUpload"
:show-file-list="false" :show-file-list="false"
@@ -351,4 +358,5 @@ function requestDramaCover(options) {
<el-button type="primary" :loading="episodeSaving" @click="saveEpisode">保存</el-button> <el-button type="primary" :loading="episodeSaving" @click="saveEpisode">保存</el-button>
</template> </template>
</el-dialog> </el-dialog>
</CreatorCenterPage>
</template> </template>
@@ -1,5 +1,6 @@
<script setup> <script setup>
import PageHeading from '@/views/components/PageHeading.vue' import PageHeading from '@/views/components/PageHeading.vue'
import CreatorCenterPage from './components/CreatorCenterPage.vue'
import { useRbac } from '@/stores/rbac' import { useRbac } from '@/stores/rbac'
import { useCreatorPrice } from './composables/useCreatorPrice' import { useCreatorPrice } from './composables/useCreatorPrice'
import { dramaStatusMeta } from './composables/useCreatorWorkspace' import { dramaStatusMeta } from './composables/useCreatorWorkspace'
@@ -22,6 +23,7 @@ const {
</script> </script>
<template> <template>
<CreatorCenterPage>
<PageHeading <PageHeading
eyebrow="创作者中心" eyebrow="创作者中心"
title="收费配置" title="收费配置"
@@ -138,4 +140,5 @@ const {
</el-table-column> </el-table-column>
</el-table> </el-table>
</section> </section>
</CreatorCenterPage>
</template> </template>
+19 -4
View File
@@ -3,6 +3,8 @@ import { ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { Search } from '@element-plus/icons-vue' import { Search } from '@element-plus/icons-vue'
import PageHeading from '@/views/components/PageHeading.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 { useRbac } from '@/stores/rbac'
import { dateCell } from '@/utils/format' import { dateCell } from '@/utils/format'
import { import {
@@ -17,6 +19,8 @@ import {
const router = useRouter() const router = useRouter()
const { can } = useRbac() const { can } = useRbac()
const canEdit = can('creator-center:drama:edit') const canEdit = can('creator-center:drama:edit')
// 代看是只读:写操作按钮直接不渲染(后端也会拒,界面不该给出做不到的入口)
const { impersonating } = useCreatorAccess()
const { const {
profile, profile,
@@ -47,12 +51,15 @@ function openEdit(row) {
</script> </script>
<template> <template>
<CreatorCenterPage>
<PageHeading <PageHeading
eyebrow="创作者中心" eyebrow="创作者中心"
title="我的作品" title="我的作品"
description="查看作品与审核进度,提交上线 / 下线申请。" description="查看作品与审核进度,提交上线 / 下线申请。"
> >
<el-button v-if="canEdit" type="primary" @click="openCreate">新建作品</el-button> <el-button v-if="canEdit && !impersonating" type="primary" @click="openCreate"
>新建作品</el-button
>
</PageHeading> </PageHeading>
<section v-loading="profileLoading" class="panel creator-profile"> <section v-loading="profileLoading" class="panel creator-profile">
@@ -187,11 +194,15 @@ function openEdit(row) {
<el-table-column label="操作" width="210" fixed="right"> <el-table-column label="操作" width="210" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
<div class="table-actions"> <div class="table-actions">
<el-button v-if="canEdit" type="primary" size="small" @click="openEdit(row)" <el-button
v-if="canEdit && !impersonating"
type="primary"
size="small"
@click="openEdit(row)"
>编辑</el-button >编辑</el-button
> >
<el-dropdown <el-dropdown
v-if="canEdit && submitOptions(row).length" v-if="canEdit && !impersonating && submitOptions(row).length"
trigger="click" trigger="click"
@command="(type) => submit(row, type)" @command="(type) => submit(row, type)"
> >
@@ -211,6 +222,7 @@ function openEdit(row) {
<el-button <el-button
v-if=" v-if="
canEdit && canEdit &&
!impersonating &&
row.submissionId && row.submissionId &&
['SUBMITTED', 'REVIEWING'].includes(row.submissionStatus) ['SUBMITTED', 'REVIEWING'].includes(row.submissionStatus)
" "
@@ -289,7 +301,10 @@ function openEdit(row) {
</el-form> </el-form>
<template #footer> <template #footer>
<el-button @click="createVisible = false">取消</el-button> <el-button @click="createVisible = false">取消</el-button>
<el-button type="primary" :loading="createSaving" @click="confirmCreate">创建草稿</el-button> <el-button type="primary" :loading="createSaving" @click="confirmCreate"
>创建草稿</el-button
>
</template> </template>
</el-dialog> </el-dialog>
</CreatorCenterPage>
</template> </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 { onMounted, ref } from 'vue'
import { getCreatorDashboardApi } from '@/api/admin/creatorCenter' import { getCreatorDashboardApi } from '@/api/admin/creatorCenter'
import { useCreatorAccess } from './useCreatorAccess'
/** /**
* 指标展示配置:key 与后端 /admin/creator-center/dashboard 的字段一一对应, * 指标展示配置:key 与后端 /admin/creator-center/dashboard 的字段一一对应,
@@ -56,6 +57,7 @@ export const DASHBOARD_GROUPS = [
] ]
export function useCreatorDashboard() { export function useCreatorDashboard() {
const { ensureAccess } = useCreatorAccess()
const loading = ref(false) const loading = ref(false)
const board = ref(null) const board = ref(null)
const lastUpdated = ref('') const lastUpdated = ref('')
@@ -81,7 +83,10 @@ export function useCreatorDashboard() {
return board.value?.[groupKey]?.[metricKey] return board.value?.[groupKey]?.[metricKey]
} }
onMounted(load) onMounted(async () => {
if (!(await ensureAccess())) return
await load()
})
return { loading, board, lastUpdated, load, display, groupValue } return { loading, board, lastUpdated, load, display, groupValue }
} }
@@ -2,8 +2,10 @@ import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { getPricesApi, pageMyDramasApi, savePricesApi } from '@/api/admin/creatorCenter' import { getPricesApi, pageMyDramasApi, savePricesApi } from '@/api/admin/creatorCenter'
import { dramaStatusMeta } from './useCreatorWorkspace' import { dramaStatusMeta } from './useCreatorWorkspace'
import { useCreatorAccess } from './useCreatorAccess'
export function useCreatorPrice() { export function useCreatorPrice() {
const { ensureAccess, impersonating } = useCreatorAccess()
const dramas = reactive({ rows: [], loading: false }) const dramas = reactive({ rows: [], loading: false })
const dramaId = ref(null) const dramaId = ref(null)
const config = reactive({ freeEpisodes: 0, unlockCoin: 0, episodes: [] }) const config = reactive({ freeEpisodes: 0, unlockCoin: 0, episodes: [] })
@@ -17,6 +19,7 @@ export function useCreatorPrice() {
* 已上线要改价得先申请下线,避免创作者无审核地改已上线内容的价格。 * 已上线要改价得先申请下线,避免创作者无审核地改已上线内容的价格。
*/ */
const priceEditable = computed(() => { const priceEditable = computed(() => {
if (impersonating.value) return false
const status = current.value?.status const status = current.value?.status
return status === 'draft' || status === 'offline' return status === 'draft' || status === 'offline'
}) })
@@ -80,6 +83,7 @@ export function useCreatorPrice() {
} }
onMounted(async () => { onMounted(async () => {
if (!(await ensureAccess())) return
await loadDramas() await loadDramas()
await loadPrices() await loadPrices()
}) })
@@ -8,6 +8,7 @@ import {
submitDramaApi, submitDramaApi,
withdrawSubmissionApi withdrawSubmissionApi
} from '@/api/admin/creatorCenter' } from '@/api/admin/creatorCenter'
import { useCreatorAccess } from './useCreatorAccess'
/** 作品状态:与后端 ContentStatus 一一对应 */ /** 作品状态:与后端 ContentStatus 一一对应 */
export const DRAMA_STATUS = [ export const DRAMA_STATUS = [
@@ -70,6 +71,7 @@ export function submitOptions(row) {
} }
export function useCreatorWorkspace() { export function useCreatorWorkspace() {
const { ensureAccess } = useCreatorAccess()
const profileLoading = ref(false) const profileLoading = ref(false)
const profile = ref(null) const profile = ref(null)
@@ -190,7 +192,9 @@ export function useCreatorWorkspace() {
} }
} }
onMounted(() => { onMounted(async () => {
// 身份不满足(未绑定 / 未选代看目标)时不打业务接口,交给外层渲染引导页
if (!(await ensureAccess())) return
loadProfile() loadProfile()
loadDramas() loadDramas()
}) })
@@ -17,6 +17,7 @@ import {
import { dateCell } from '@/utils/format' import { dateCell } from '@/utils/format'
import { probeVideoDuration, resumeUpload, uploadFile } from '@/utils/uploader' import { probeVideoDuration, resumeUpload, uploadFile } from '@/utils/uploader'
import { submissionStatusMeta, submissionTypeText, submitOptions } from './useCreatorWorkspace' import { submissionStatusMeta, submissionTypeText, submitOptions } from './useCreatorWorkspace'
import { useCreatorAccess } from './useCreatorAccess'
/** 中断的上传会话按「作品 + 文件名 + 大小」记住,刷新后还能接着传 */ /** 中断的上传会话按「作品 + 文件名 + 大小」记住,刷新后还能接着传 */
const RESUME_KEY = 'creator-upload-sessions' const RESUME_KEY = 'creator-upload-sessions'
@@ -37,6 +38,7 @@ function writeSession(key, value) {
} }
export function useDramaEdit() { export function useDramaEdit() {
const { ensureAccess, impersonating } = useCreatorAccess()
const route = useRoute() const route = useRoute()
const dramaId = Number(route.params.id) const dramaId = Number(route.params.id)
@@ -56,10 +58,13 @@ export function useDramaEdit() {
}) })
/** 只有草稿与已下线能改(与后端 requireEditable 一致),其余状态页面转只读 */ /** 只有草稿与已下线能改(与后端 requireEditable 一致),其余状态页面转只读 */
const editable = computed( const editable = computed(() => {
() => drama.value?.status === 'draft' || drama.value?.status === 'offline' // 代看只读:整页禁用,与后端 requireWritable 的边界保持一致
) if (impersonating.value) return false
return drama.value?.status === 'draft' || drama.value?.status === 'offline'
})
const readonlyReason = computed(() => { const readonlyReason = computed(() => {
if (impersonating.value) return '代看模式只读:可以查看作品与分集,但不能编辑、上传或提交'
switch (drama.value?.status) { switch (drama.value?.status) {
case 'published': case 'published':
return '已上线的作品只读,要改内容请先提交「申请下线」' return '已上线的作品只读,要改内容请先提交「申请下线」'
@@ -304,7 +309,10 @@ export function useDramaEdit() {
await loadAll() await loadAll()
} }
onMounted(loadAll) onMounted(async () => {
if (!(await ensureAccess())) return
await loadAll()
})
return { return {
dramaId, dramaId,
+2
View File
@@ -40,6 +40,8 @@ declare global {
method?: AxiosMethod method?: AxiosMethod
headers?: RawAxiosRequestHeaders headers?: RawAxiosRequestHeaders
responseType?: AxiosResponseType responseType?: AxiosResponseType
/** 静默请求:业务码非 0 时不弹全局提示,并把完整 Result 交给调用方判断 */
silent?: boolean
} }
declare interface IResponse<T = any> { declare interface IResponse<T = any> {