feat: 新增数据管理页面与短剧详情页,完善接口类型与菜单配置
1. 新增数据管理页面,支持查看数据概览、短剧/剧集指标和同步记录 2. 新增短剧详情页,支持查看短剧信息、管理剧集和上传视频 3. 新增菜单配置项,添加数据管理入口 4. 扩展API类型定义,新增剧集、上传任务、数据统计等类型 5. 修复timestamp类型为数字类型,新增短剧额外字段 6. 为页面布局添加统一的page-stack样式类 7. 新增短剧详情路由,优化短剧列表页跳转逻辑
This commit is contained in:
96
src/Data/index.tsx
Normal file
96
src/Data/index.tsx
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Button, Card, Grid, Message, Statistic, Table } from '@arco-design/web-react';
|
||||||
|
import { IconRefresh } from '@arco-design/web-react/icon';
|
||||||
|
import type { ApiResponse, DataOverview, DramaMetric, EpisodeMetric, SyncJob } from '../api/types';
|
||||||
|
import { api } from '../api/client';
|
||||||
|
import { usePageData } from '../hooks/usePageData';
|
||||||
|
|
||||||
|
const { Row, Col } = Grid;
|
||||||
|
|
||||||
|
function DataPage() {
|
||||||
|
const [overview, setOverview] = useState<DataOverview>({
|
||||||
|
totalDramas: 0,
|
||||||
|
totalEpisodes: 0,
|
||||||
|
totalPlays: 0,
|
||||||
|
totalLikes: 0,
|
||||||
|
totalShares: 0,
|
||||||
|
totalComments: 0,
|
||||||
|
});
|
||||||
|
const [syncing, setSyncing] = useState(false);
|
||||||
|
const dramas = usePageData<DramaMetric>('/api/admin/v1/data/dramas');
|
||||||
|
const episodes = usePageData<EpisodeMetric>('/api/admin/v1/data/episodes');
|
||||||
|
const jobs = usePageData<SyncJob>('/api/admin/v1/data/sync-jobs');
|
||||||
|
|
||||||
|
const loadOverview = async () => {
|
||||||
|
const response = await api.get<ApiResponse<DataOverview>>('/api/admin/v1/data/overview');
|
||||||
|
setOverview(response.data.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void loadOverview();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const syncData = async () => {
|
||||||
|
setSyncing(true);
|
||||||
|
try {
|
||||||
|
await api.post('/api/admin/v1/data/sync', {});
|
||||||
|
Message.success('数据同步完成');
|
||||||
|
await Promise.all([loadOverview(), dramas.reload(), episodes.reload(), jobs.reload()]);
|
||||||
|
} finally {
|
||||||
|
setSyncing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="page-stack">
|
||||||
|
<Card className="section-card" title="数据管理" extra={<Button type="primary" icon={<IconRefresh />} loading={syncing} onClick={syncData}>同步 TikTok 数据</Button>}>
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={4}><Statistic title="短剧数" value={overview.totalDramas} /></Col>
|
||||||
|
<Col span={4}><Statistic title="剧集数" value={overview.totalEpisodes} /></Col>
|
||||||
|
<Col span={4}><Statistic title="播放量" value={overview.totalPlays} /></Col>
|
||||||
|
<Col span={4}><Statistic title="点赞" value={overview.totalLikes} /></Col>
|
||||||
|
<Col span={4}><Statistic title="分享" value={overview.totalShares} /></Col>
|
||||||
|
<Col span={4}><Statistic title="评论" value={overview.totalComments} /></Col>
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="section-card" title="短剧数据">
|
||||||
|
<Table rowKey="dramaId" loading={dramas.loading} data={dramas.data.list} pagination={{ total: dramas.data.pagination.total, pageSize: dramas.data.pagination.pageSize }} columns={[
|
||||||
|
{ title: '短剧ID', dataIndex: 'dramaId', width: 100 },
|
||||||
|
{ title: '短剧名称', dataIndex: 'title' },
|
||||||
|
{ title: '播放量', dataIndex: 'plays', width: 120 },
|
||||||
|
{ title: '点赞', dataIndex: 'likes', width: 120 },
|
||||||
|
{ title: '分享', dataIndex: 'shares', width: 120 },
|
||||||
|
{ title: '评论', dataIndex: 'comments', width: 120 },
|
||||||
|
{ title: '完播率', dataIndex: 'completionRate', width: 120, render: (value) => `${Math.round(Number(value) * 100)}%` },
|
||||||
|
{ title: '同步时间', dataIndex: 'syncedAt', width: 220 },
|
||||||
|
]} />
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="section-card" title="剧集数据">
|
||||||
|
<Table rowKey="episodeId" loading={episodes.loading} data={episodes.data.list} pagination={{ total: episodes.data.pagination.total, pageSize: episodes.data.pagination.pageSize }} columns={[
|
||||||
|
{ title: '剧集ID', dataIndex: 'episodeId', width: 100 },
|
||||||
|
{ title: '短剧ID', dataIndex: 'dramaId', width: 100 },
|
||||||
|
{ title: '集数', dataIndex: 'episodeNo', width: 90 },
|
||||||
|
{ title: '剧集名称', dataIndex: 'title' },
|
||||||
|
{ title: '播放量', dataIndex: 'plays', width: 120 },
|
||||||
|
{ title: '点赞', dataIndex: 'likes', width: 120 },
|
||||||
|
{ title: '平均观看秒数', dataIndex: 'averageWatchSeconds', width: 150 },
|
||||||
|
{ title: '同步时间', dataIndex: 'syncedAt', width: 220 },
|
||||||
|
]} />
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="section-card" title="同步记录">
|
||||||
|
<Table rowKey="id" loading={jobs.loading} data={jobs.data.list} pagination={{ total: jobs.data.pagination.total, pageSize: jobs.data.pagination.pageSize }} columns={[
|
||||||
|
{ title: '任务ID', dataIndex: 'jobId' },
|
||||||
|
{ title: '状态', dataIndex: 'status', width: 120 },
|
||||||
|
{ title: '短剧数', dataIndex: 'dramaCount', width: 120 },
|
||||||
|
{ title: '剧集数', dataIndex: 'episodeCount', width: 120 },
|
||||||
|
{ title: '创建时间', dataIndex: 'createdAt', width: 220 },
|
||||||
|
]} />
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DataPage;
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
|
Descriptions,
|
||||||
Form,
|
Form,
|
||||||
Grid,
|
Grid,
|
||||||
Input,
|
Input,
|
||||||
@@ -10,12 +12,13 @@ import {
|
|||||||
Message,
|
Message,
|
||||||
Modal,
|
Modal,
|
||||||
Select,
|
Select,
|
||||||
|
Space,
|
||||||
Switch,
|
Switch,
|
||||||
Table,
|
Table,
|
||||||
Tag,
|
Tag,
|
||||||
} from '@arco-design/web-react';
|
} from '@arco-design/web-react';
|
||||||
import { IconPlus } from '@arco-design/web-react/icon';
|
import { IconArrowLeft, IconPlus, IconRefresh } from '@arco-design/web-react/icon';
|
||||||
import type { Drama } from '../api/types';
|
import type { ApiResponse, Drama, Episode, UploadTask } from '../api/types';
|
||||||
import { api } from '../api/client';
|
import { api } from '../api/client';
|
||||||
import { usePageData } from '../hooks/usePageData';
|
import { usePageData } from '../hooks/usePageData';
|
||||||
|
|
||||||
@@ -30,7 +33,33 @@ const STATUS_OPTIONS: { value: string; label: string }[] = [
|
|||||||
{ value: 'offline', label: '已下线' },
|
{ value: 'offline', label: '已下线' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const REVIEW_STATUS_LABEL: Record<string, string> = {
|
||||||
|
not_submitted: '未提交',
|
||||||
|
pending: '待审核',
|
||||||
|
reviewing: '审核中',
|
||||||
|
approved: '已通过',
|
||||||
|
rejected: '已拒绝',
|
||||||
|
};
|
||||||
|
|
||||||
|
const PUBLISH_STATUS_LABEL: Record<string, string> = {
|
||||||
|
draft: '草稿',
|
||||||
|
scheduled: '定时发布',
|
||||||
|
published: '已发布',
|
||||||
|
offline: '已下线',
|
||||||
|
};
|
||||||
|
|
||||||
function DramasPage() {
|
function DramasPage() {
|
||||||
|
const { id } = useParams();
|
||||||
|
|
||||||
|
if (id) {
|
||||||
|
return <DramaDetailPage dramaId={Number(id)} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <DramaListPage />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DramaListPage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
const { data, loading, reload } = usePageData<Drama>('/api/admin/v1/dramas');
|
const { data, loading, reload } = usePageData<Drama>('/api/admin/v1/dramas');
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const [creating, setCreating] = useState(false);
|
const [creating, setCreating] = useState(false);
|
||||||
@@ -52,9 +81,13 @@ function DramasPage() {
|
|||||||
<Table rowKey="id" loading={loading} data={data.list} pagination={{ total: data.pagination.total, pageSize: data.pagination.pageSize }} columns={[
|
<Table rowKey="id" loading={loading} data={data.list} pagination={{ total: data.pagination.total, pageSize: data.pagination.pageSize }} columns={[
|
||||||
{ title: 'ID', dataIndex: 'id', width: 80 },
|
{ title: 'ID', dataIndex: 'id', width: 80 },
|
||||||
{ title: '标题', dataIndex: 'title' },
|
{ title: '标题', dataIndex: 'title' },
|
||||||
|
{ title: '地区', dataIndex: 'region', width: 120 },
|
||||||
{ title: '类型', dataIndex: 'type', width: 140 },
|
{ title: '类型', dataIndex: 'type', width: 140 },
|
||||||
{ title: '状态', dataIndex: 'status', width: 120, render: (status) => <Tag color={status === 'published' ? 'green' : 'gray'}>{status}</Tag> },
|
{ title: '总集数', dataIndex: 'totalEpisodes', width: 100 },
|
||||||
{ title: '推荐', dataIndex: 'isRecommended', width: 140, render: (value) => value ? <Tag color="arcoblue">是</Tag> : <Tag>否</Tag> },
|
{ title: '已发布', dataIndex: 'publishedEpisodes', width: 100 },
|
||||||
|
{ title: '状态', dataIndex: 'status', width: 120, render: (status) => <Tag color={status === 'published' ? 'green' : 'gray'}>{PUBLISH_STATUS_LABEL[String(status)] || String(status)}</Tag> },
|
||||||
|
{ title: '推荐', dataIndex: 'isRecommended', width: 100, render: (value) => value ? <Tag color="arcoblue">是</Tag> : <Tag>否</Tag> },
|
||||||
|
{ title: '操作', width: 120, render: (_, record) => <Button type="text" onClick={() => navigate(`/dramas/${record.id}`)}>详情</Button> },
|
||||||
]} />
|
]} />
|
||||||
<Modal title="创建短剧" visible={visible} onCancel={() => setVisible(false)} footer={null} style={{ width: 640 }}>
|
<Modal title="创建短剧" visible={visible} onCancel={() => setVisible(false)} footer={null} style={{ width: 640 }}>
|
||||||
<Form
|
<Form
|
||||||
@@ -129,4 +162,144 @@ function DramasPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function DramaDetailPage({ dramaId }: { dramaId: number }) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [drama, setDrama] = useState<Drama | null>(null);
|
||||||
|
const [episodes, setEpisodes] = useState<Episode[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [episodeCount, setEpisodeCount] = useState(1);
|
||||||
|
const [uploadingEpisodeId, setUploadingEpisodeId] = useState<number | null>(null);
|
||||||
|
|
||||||
|
const loadDetail = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const [dramaResponse, episodeResponse] = await Promise.all([
|
||||||
|
api.get<ApiResponse<Drama>>(`/api/admin/v1/dramas/${dramaId}`),
|
||||||
|
api.get<ApiResponse<{ list: Episode[] }>>(`/api/admin/v1/dramas/${dramaId}/episodes?pageSize=200`),
|
||||||
|
]);
|
||||||
|
setDrama(dramaResponse.data.data);
|
||||||
|
setEpisodeCount(dramaResponse.data.data.totalEpisodes || 1);
|
||||||
|
setEpisodes(episodeResponse.data.data.list);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void loadDetail();
|
||||||
|
}, [dramaId]);
|
||||||
|
|
||||||
|
const configureEpisodes = async () => {
|
||||||
|
await api.post(`/api/admin/v1/dramas/${dramaId}/episodes/batch`, { episodeCount });
|
||||||
|
Message.success('剧集数量已更新');
|
||||||
|
await loadDetail();
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateDramaStatus = async (status: string) => {
|
||||||
|
await api.patch(`/api/admin/v1/dramas/${dramaId}`, { status });
|
||||||
|
Message.success('短剧状态已更新');
|
||||||
|
await loadDetail();
|
||||||
|
};
|
||||||
|
|
||||||
|
const uploadEpisodeVideo = async (episode: Episode, file: File) => {
|
||||||
|
setUploadingEpisodeId(episode.id);
|
||||||
|
try {
|
||||||
|
const task = await api.post<ApiResponse<UploadTask>>('/api/admin/v1/media/upload-tasks', {
|
||||||
|
episodeId: episode.id,
|
||||||
|
fileName: file.name,
|
||||||
|
contentType: file.type || 'video/mp4',
|
||||||
|
fileSize: file.size,
|
||||||
|
});
|
||||||
|
await api.patch(`/api/admin/v1/media/upload-tasks/${task.data.data.jobId}/complete`, {});
|
||||||
|
await api.post('/api/admin/v1/media/review-status/sync', {});
|
||||||
|
Message.success('视频上传并审核通过');
|
||||||
|
await loadDetail();
|
||||||
|
} finally {
|
||||||
|
setUploadingEpisodeId(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const publishEpisode = async (episode: Episode) => {
|
||||||
|
await api.patch(`/api/admin/v1/episodes/${episode.id}`, {
|
||||||
|
publishStatus: 'published',
|
||||||
|
status: 'published',
|
||||||
|
});
|
||||||
|
Message.success('剧集已发布');
|
||||||
|
await loadDetail();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="page-stack">
|
||||||
|
<Card
|
||||||
|
className="section-card"
|
||||||
|
loading={loading && !drama}
|
||||||
|
title={<Space><Button icon={<IconArrowLeft />} onClick={() => navigate('/dramas')} />短剧详情</Space>}
|
||||||
|
extra={<Space>
|
||||||
|
<Button icon={<IconRefresh />} onClick={loadDetail}>刷新</Button>
|
||||||
|
<Button onClick={() => updateDramaStatus('draft')}>设为草稿</Button>
|
||||||
|
<Button type="primary" onClick={() => updateDramaStatus('published')}>上架短剧</Button>
|
||||||
|
</Space>}
|
||||||
|
>
|
||||||
|
{drama && (
|
||||||
|
<Descriptions
|
||||||
|
column={3}
|
||||||
|
data={[
|
||||||
|
{ label: '短剧ID', value: drama.id },
|
||||||
|
{ label: '名称', value: drama.title },
|
||||||
|
{ label: '地区', value: drama.region || '-' },
|
||||||
|
{ label: '类型', value: drama.type },
|
||||||
|
{ label: '总集数', value: drama.totalEpisodes || 0 },
|
||||||
|
{ label: '已发布集数', value: drama.publishedEpisodes || 0 },
|
||||||
|
{ label: '状态', value: PUBLISH_STATUS_LABEL[drama.status] || drama.status },
|
||||||
|
{ label: '封面', value: drama.coverUrl },
|
||||||
|
{ label: '简介', value: drama.description || '-' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="section-card" title="剧集配置" extra={<Space>
|
||||||
|
<InputNumber min={1} max={500} value={episodeCount} onChange={(value) => setEpisodeCount(Number(value) || 1)} />
|
||||||
|
<Button type="primary" onClick={configureEpisodes}>保存集数</Button>
|
||||||
|
</Space>}>
|
||||||
|
<Table rowKey="id" loading={loading} data={episodes} pagination={false} columns={[
|
||||||
|
{ title: '集数', dataIndex: 'episodeNo', width: 80 },
|
||||||
|
{ title: '标题', dataIndex: 'title' },
|
||||||
|
{ title: 'BytePlus VID', dataIndex: 'byteplusVid', width: 180, render: (value) => value || '-' },
|
||||||
|
{ title: '尺寸', width: 120, render: (_, record) => record.width && record.height ? `${record.width}x${record.height}` : '-' },
|
||||||
|
{ title: '时长', dataIndex: 'durationSeconds', width: 100, render: (value) => value ? `${value}s` : '-' },
|
||||||
|
{ title: '审核状态', dataIndex: 'reviewStatus', width: 120, render: (status) => <Tag color={status === 'approved' ? 'green' : 'orange'}>{REVIEW_STATUS_LABEL[String(status)] || String(status)}</Tag> },
|
||||||
|
{ title: '发布状态', dataIndex: 'publishStatus', width: 120, render: (status) => <Tag color={status === 'published' ? 'green' : 'gray'}>{PUBLISH_STATUS_LABEL[String(status)] || String(status)}</Tag> },
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 280,
|
||||||
|
render: (_, record) => (
|
||||||
|
<Space>
|
||||||
|
<Button loading={uploadingEpisodeId === record.id}>
|
||||||
|
<label>
|
||||||
|
上传视频
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="video/*"
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
onChange={(event) => {
|
||||||
|
const file = event.target.files?.[0];
|
||||||
|
if (file) {
|
||||||
|
void uploadEpisodeVideo(record, file);
|
||||||
|
}
|
||||||
|
event.target.value = '';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</Button>
|
||||||
|
<Button type="text" disabled={record.reviewStatus !== 'approved'} onClick={() => publishEpisode(record)}>发布</Button>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]} />
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default DramasPage;
|
export default DramasPage;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ export type ApiResponse<T> = {
|
|||||||
code: number;
|
code: number;
|
||||||
data: T;
|
data: T;
|
||||||
message: string;
|
message: string;
|
||||||
timestamp: string;
|
timestamp: number;
|
||||||
requestId: string;
|
requestId: string;
|
||||||
cost: string;
|
cost: string;
|
||||||
};
|
};
|
||||||
@@ -35,6 +35,8 @@ export type Drama = {
|
|||||||
status: string;
|
status: string;
|
||||||
coverUrl: string;
|
coverUrl: string;
|
||||||
isRecommended: boolean;
|
isRecommended: boolean;
|
||||||
|
totalEpisodes?: number;
|
||||||
|
publishedEpisodes?: number;
|
||||||
description?: string;
|
description?: string;
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
region?: string;
|
region?: string;
|
||||||
@@ -44,6 +46,90 @@ export type Drama = {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type Episode = {
|
||||||
|
id: number;
|
||||||
|
dramaId: number;
|
||||||
|
albumId: number;
|
||||||
|
episodeNo: number;
|
||||||
|
seq: number;
|
||||||
|
title: string;
|
||||||
|
coverUrl: string;
|
||||||
|
videoUrl?: string;
|
||||||
|
byteplusVid?: string;
|
||||||
|
freeType: 'free' | 'paid';
|
||||||
|
price: number;
|
||||||
|
durationSeconds: number;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
reviewStatus: string;
|
||||||
|
reviewMessage?: string;
|
||||||
|
publishStatus: string;
|
||||||
|
nextReleaseAt?: string;
|
||||||
|
createdAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UploadTask = {
|
||||||
|
id: number;
|
||||||
|
jobId: string;
|
||||||
|
episodeId?: number;
|
||||||
|
status: string;
|
||||||
|
uploadUrl?: string;
|
||||||
|
bucket?: string;
|
||||||
|
objectKey?: string;
|
||||||
|
byteplusVid?: string;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
durationSeconds?: number;
|
||||||
|
reviewStatus?: string;
|
||||||
|
reviewMessage?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DataOverview = {
|
||||||
|
totalDramas: number;
|
||||||
|
totalEpisodes: number;
|
||||||
|
totalPlays: number;
|
||||||
|
totalLikes: number;
|
||||||
|
totalShares: number;
|
||||||
|
totalComments: number;
|
||||||
|
latestSyncedAt?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DramaMetric = {
|
||||||
|
dramaId: number;
|
||||||
|
title: string;
|
||||||
|
plays: number;
|
||||||
|
likes: number;
|
||||||
|
shares: number;
|
||||||
|
comments: number;
|
||||||
|
completionRate: number;
|
||||||
|
averageWatchSeconds: number;
|
||||||
|
syncedAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EpisodeMetric = {
|
||||||
|
dramaId: number;
|
||||||
|
episodeId: number;
|
||||||
|
title: string;
|
||||||
|
episodeNo: number;
|
||||||
|
plays: number;
|
||||||
|
likes: number;
|
||||||
|
shares: number;
|
||||||
|
comments: number;
|
||||||
|
completionRate: number;
|
||||||
|
averageWatchSeconds: number;
|
||||||
|
syncedAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SyncJob = {
|
||||||
|
id: number;
|
||||||
|
jobId: string;
|
||||||
|
status: string;
|
||||||
|
dramaId?: number;
|
||||||
|
dramaCount: number;
|
||||||
|
episodeCount: number;
|
||||||
|
createdAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type AppUser = {
|
export type AppUser = {
|
||||||
id: number;
|
id: number;
|
||||||
tiktokOpenId: string;
|
tiktokOpenId: string;
|
||||||
|
|||||||
@@ -28,6 +28,12 @@ select {
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page-stack {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.modal-actions {
|
.modal-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { filterMenuItems, findCurrentMenu, menuItems, renderMenuItem } from './m
|
|||||||
import { PermissionRoute } from './PermissionRoute';
|
import { PermissionRoute } from './PermissionRoute';
|
||||||
import DashboardPage from '../Dashboard';
|
import DashboardPage from '../Dashboard';
|
||||||
import DramasPage from '../Drama';
|
import DramasPage from '../Drama';
|
||||||
|
import DataPage from '../Data';
|
||||||
import UsersPage from '../User';
|
import UsersPage from '../User';
|
||||||
import RolesPage from '../Role';
|
import RolesPage from '../Role';
|
||||||
import PersonnelPage from '../Personnel';
|
import PersonnelPage from '../Personnel';
|
||||||
@@ -100,6 +101,8 @@ export function AdminLayout() {
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/dashboard" element={<DashboardPage profile={profile} />} />
|
<Route path="/dashboard" element={<DashboardPage profile={profile} />} />
|
||||||
<Route path="/dramas" element={<PermissionRoute profile={profile} requiredPermissions={['drama:read']}><DramasPage /></PermissionRoute>} />
|
<Route path="/dramas" element={<PermissionRoute profile={profile} requiredPermissions={['drama:read']}><DramasPage /></PermissionRoute>} />
|
||||||
|
<Route path="/dramas/:id" element={<PermissionRoute profile={profile} requiredPermissions={['drama:read']}><DramasPage /></PermissionRoute>} />
|
||||||
|
<Route path="/data" element={<PermissionRoute profile={profile} requiredPermissions={['data:read']}><DataPage /></PermissionRoute>} />
|
||||||
<Route path="/users" element={<PermissionRoute profile={profile} requiredPermissions={['user:read']}><UsersPage /></PermissionRoute>} />
|
<Route path="/users" element={<PermissionRoute profile={profile} requiredPermissions={['user:read']}><UsersPage /></PermissionRoute>} />
|
||||||
<Route path="/roles" element={<PermissionRoute profile={profile} requiredPermissions={['role:read']}><RolesPage /></PermissionRoute>} />
|
<Route path="/roles" element={<PermissionRoute profile={profile} requiredPermissions={['role:read']}><RolesPage /></PermissionRoute>} />
|
||||||
<Route path="/personnel" element={<PermissionRoute profile={profile} requiredPermissions={['admin-user:read']}><PersonnelPage /></PermissionRoute>} />
|
<Route path="/personnel" element={<PermissionRoute profile={profile} requiredPermissions={['admin-user:read']}><PersonnelPage /></PermissionRoute>} />
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export const SYSTEM_MENU_KEY = '/system';
|
|||||||
export const menuItems: MenuConfig[] = [
|
export const menuItems: MenuConfig[] = [
|
||||||
{ key: '/dashboard', label: '工作台', icon: <IconDashboard /> },
|
{ key: '/dashboard', label: '工作台', icon: <IconDashboard /> },
|
||||||
{ key: '/dramas', label: '短剧管理', icon: <IconVideoCamera />, requiredPermissions: ['drama:read'] },
|
{ key: '/dramas', label: '短剧管理', icon: <IconVideoCamera />, requiredPermissions: ['drama:read'] },
|
||||||
|
{ key: '/data', label: '数据管理', icon: <IconApps />, requiredPermissions: ['data:read'] },
|
||||||
{ key: '/users', label: '用户管理', icon: <IconUser />, requiredPermissions: ['user:read'] },
|
{ key: '/users', label: '用户管理', icon: <IconUser />, requiredPermissions: ['user:read'] },
|
||||||
{
|
{
|
||||||
key: SYSTEM_MENU_KEY,
|
key: SYSTEM_MENU_KEY,
|
||||||
|
|||||||
Reference in New Issue
Block a user