refactor: 重构项目接口与API调用体系,统一类型管理

1.  合并拆分的API类型定义到统一的api/interface.ts文件
2.  删除旧的api/client.ts与api/types.ts文件
3.  重构所有页面与组件的API调用方式,统一使用api/index.ts导出的接口
4.  调整组件文件结构,将分散的组件按模块整理
5.  统一类型导入路径,移除冗余的类型重复定义
This commit is contained in:
2026-07-02 14:57:06 +08:00
parent e160476ecf
commit 3451d373bc
54 changed files with 1106 additions and 930 deletions

View File

@@ -0,0 +1,142 @@
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { Button, Card, Descriptions, InputNumber, Message, Space } from "@arco-design/web-react";
import { IconArrowLeft, IconRefresh } from "@arco-design/web-react/icon";
import { dramaApi, mediaApi } from "../../api";
import type { Drama, DramaDetailPageProps, Episode } from "../interface";
import { EpisodeTable } from "./EpisodeTable";
const PUBLISH_STATUS_LABEL: Record<string, string> = {
draft: "草稿",
scheduled: "定时发布",
published: "已发布",
offline: "已下线",
};
export function DramaDetail({ dramaId }: DramaDetailPageProps) {
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 [dramaData, episodeList] = await Promise.all([dramaApi.getDrama(dramaId), dramaApi.getDramaEpisodes(dramaId)]);
setDrama(dramaData);
setEpisodeCount(dramaData.totalEpisodes || 1);
setEpisodes(episodeList);
} finally {
setLoading(false);
}
};
useEffect(() => {
void loadDetail();
}, [dramaId]);
const configureEpisodes = async () => {
await dramaApi.configureDramaEpisodes(dramaId, episodeCount);
Message.success("剧集数量已更新");
await loadDetail();
};
const updateDramaStatus = async (status: string) => {
await dramaApi.updateDrama(dramaId, { status });
Message.success("短剧状态已更新");
await loadDetail();
};
const uploadEpisodeVideo = async (episode: Episode, file: File) => {
setUploadingEpisodeId(episode.id);
try {
const task = await mediaApi.createUploadTask({
episodeId: episode.id,
fileName: file.name,
contentType: file.type || "video/mp4",
fileSize: file.size,
});
await mediaApi.completeUploadTask(task.jobId);
await mediaApi.syncReviewStatus();
Message.success("视频上传并审核通过");
await loadDetail();
} finally {
setUploadingEpisodeId(null);
}
};
const publishEpisode = async (episode: Episode) => {
await dramaApi.updateEpisode(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>
}
>
<EpisodeTable
episodes={episodes}
loading={loading}
uploadingEpisodeId={uploadingEpisodeId}
onUploadVideo={uploadEpisodeVideo}
onPublishEpisode={publishEpisode}
/>
</Card>
</div>
);
}

View File

@@ -0,0 +1,164 @@
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { Button, Card, Form, Grid, Input, InputNumber, InputTag, Message, Modal, Select, Switch, Table, Tag } from "@arco-design/web-react";
import { IconPlus } from "@arco-design/web-react/icon";
import { dramaApi } from "../../api";
import { usePageData } from "../../hooks/usePageData";
import type { Drama, StatusOption } from "../interface";
const { Row, Col } = Grid;
const STATUS_OPTIONS: StatusOption[] = [
{ value: "draft", label: "草稿" },
{ value: "scheduled", label: "定时发布" },
{ value: "published", label: "已发布" },
{ value: "offline", label: "已下线" },
];
const PUBLISH_STATUS_LABEL: Record<string, string> = {
draft: "草稿",
scheduled: "定时发布",
published: "已发布",
offline: "已下线",
};
export function DramaList() {
const navigate = useNavigate();
const { data, loading, reload, changePage } = usePageData<Drama>(dramaApi.listDramas);
const [visible, setVisible] = useState(false);
const [creating, setCreating] = useState(false);
const createDrama = async (values: Partial<Drama>) => {
setCreating(true);
try {
await dramaApi.createDrama(values);
Message.success("短剧创建成功");
setVisible(false);
await reload();
} finally {
setCreating(false);
}
};
return (
<Card
className="section-card"
title="短剧管理"
extra={
<Button type="primary" icon={<IconPlus />} onClick={() => setVisible(true)}>
</Button>
}
>
<Table
rowKey="id"
loading={loading}
data={data.list}
pagination={{
current: data.pagination.page,
total: data.pagination.total,
pageSize: data.pagination.pageSize,
onChange: (page, pageSize) => changePage(page, pageSize),
}}
columns={[
{ title: "ID", dataIndex: "id", width: 80 },
{ title: "标题", dataIndex: "title" },
{ title: "地区", dataIndex: "region", width: 120 },
{ title: "类型", dataIndex: "type", width: 140 },
{ title: "总集数", dataIndex: "totalEpisodes", width: 100 },
{ 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 }}>
<Form layout="vertical" onSubmit={createDrama} initialValues={{ status: "draft", isRecommended: false, sortOrder: 0 }}>
<Form.Item label="标题" field="title" rules={[{ required: true }]}>
<Input placeholder="请输入短剧标题" />
</Form.Item>
<Row gutter={16}>
<Col span={12}>
<Form.Item label="类型" field="type" rules={[{ required: true }]}>
<Input placeholder="请输入类型,如复仇、爱情、动作等" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="状态" field="status">
<Select placeholder="请选择状态">
{STATUS_OPTIONS.map((option) => (
<Select.Option key={option.value} value={option.value}>
{option.label}
</Select.Option>
))}
</Select>
</Form.Item>
</Col>
</Row>
<Form.Item label="封面地址" field="coverUrl" rules={[{ required: true }]}>
<Input placeholder="请输入封面图片 URL" />
</Form.Item>
<Form.Item label="简介" field="description">
<Input.TextArea placeholder="请输入短剧简介" autoSize={{ minRows: 2, maxRows: 4 }} />
</Form.Item>
<Form.Item label="标签" field="tags">
<InputTag placeholder="输入后回车添加标签" allowClear />
</Form.Item>
<Row gutter={16}>
<Col span={8}>
<Form.Item label="地区" field="region">
<Input placeholder="如 中国" />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item label="语言" field="language">
<Input placeholder="如 zh-CN" />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item label="年份" field="year">
<InputNumber placeholder="如 2026" min={1900} style={{ width: "100%" }} />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item label="排序权重" field="sortOrder">
<InputNumber placeholder="数值越大越靠前" style={{ width: "100%" }} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="推荐" field="isRecommended" triggerPropName="checked">
<Switch />
</Form.Item>
</Col>
</Row>
<div className="modal-actions">
<Button onClick={() => setVisible(false)}></Button>
<Button type="primary" htmlType="submit" loading={creating}>
</Button>
</div>
</Form>
</Modal>
</Card>
);
}

View File

@@ -0,0 +1,89 @@
import { Button, Space, Table, Tag } from "@arco-design/web-react";
import type { EpisodeTableProps } from "../interface";
const REVIEW_STATUS_LABEL: Record<string, string> = {
not_submitted: "未提交",
pending: "待审核",
reviewing: "审核中",
approved: "已通过",
rejected: "已拒绝",
};
const PUBLISH_STATUS_LABEL: Record<string, string> = {
draft: "草稿",
scheduled: "定时发布",
published: "已发布",
offline: "已下线",
};
export function EpisodeTable({ episodes, loading, uploadingEpisodeId, onUploadVideo, onPublishEpisode }: EpisodeTableProps) {
return (
<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 onUploadVideo(record, file);
}
event.target.value = "";
}}
/>
</label>
</Button>
<Button type="text" disabled={record.reviewStatus !== "approved"} onClick={() => onPublishEpisode(record)}>
</Button>
</Space>
),
},
]}
/>
);
}

View File

@@ -1,413 +1,15 @@
import { useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import {
Button,
Card,
Descriptions,
Form,
Grid,
Input,
InputNumber,
InputTag,
Message,
Modal,
Select,
Space,
Switch,
Table,
Tag,
} from "@arco-design/web-react";
import { IconArrowLeft, IconPlus, IconRefresh } from "@arco-design/web-react/icon";
import type { Drama, Episode } from "../api/types";
import {
configureDramaEpisodes,
createDrama as createDramaApi,
getDrama,
getDramaEpisodes,
updateDrama,
updateEpisode,
} from "../api/dramas";
import { completeUploadTask, createUploadTask, syncReviewStatus } from "../api/media";
import { usePageData } from "../hooks/usePageData";
const { Row, Col } = Grid;
const STATUS_OPTIONS: { value: string; label: string }[] = [
{ value: "draft", label: "草稿" },
{ value: "scheduled", label: "定时发布" },
{ value: "published", 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: "已下线",
};
import { useParams } from "react-router-dom";
import { DramaDetail } from "./components/DramaDetail";
import { DramaList } from "./components/DramaList";
function DramasPage() {
const { id } = useParams();
if (id) {
return <DramaDetailPage dramaId={Number(id)} />;
return <DramaDetail dramaId={Number(id)} />;
}
return <DramaListPage />;
}
function DramaListPage() {
const navigate = useNavigate();
const { data, loading, reload, changePage } = usePageData<Drama>("/api/admin/v1/dramas");
const [visible, setVisible] = useState(false);
const [creating, setCreating] = useState(false);
const createDrama = async (values: Partial<Drama>) => {
setCreating(true);
try {
await createDramaApi(values);
Message.success("短剧创建成功");
setVisible(false);
await reload();
} finally {
setCreating(false);
}
};
return (
<Card
className="section-card"
title="短剧管理"
extra={
<Button type="primary" icon={<IconPlus />} onClick={() => setVisible(true)}>
</Button>
}
>
<Table
rowKey="id"
loading={loading}
data={data.list}
pagination={{
current: data.pagination.page,
total: data.pagination.total,
pageSize: data.pagination.pageSize,
onChange: (page, pageSize) => changePage(page, pageSize),
}}
columns={[
{ title: "ID", dataIndex: "id", width: 80 },
{ title: "标题", dataIndex: "title" },
{ title: "地区", dataIndex: "region", width: 120 },
{ title: "类型", dataIndex: "type", width: 140 },
{ title: "总集数", dataIndex: "totalEpisodes", width: 100 },
{ 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 }}>
<Form
layout="vertical"
onSubmit={createDrama}
initialValues={{
status: "draft",
isRecommended: false,
sortOrder: 0,
}}
>
<Form.Item label="标题" field="title" rules={[{ required: true }]}>
<Input placeholder="请输入短剧标题" />
</Form.Item>
<Row gutter={16}>
<Col span={12}>
<Form.Item label="类型" field="type" rules={[{ required: true }]}>
<Input placeholder="请输入类型,如复仇、爱情、动作等" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="状态" field="status">
<Select placeholder="请选择状态">
{STATUS_OPTIONS.map((option) => (
<Select.Option key={option.value} value={option.value}>
{option.label}
</Select.Option>
))}
</Select>
</Form.Item>
</Col>
</Row>
<Form.Item label="封面地址" field="coverUrl" rules={[{ required: true }]}>
<Input placeholder="请输入封面图片 URL" />
</Form.Item>
<Form.Item label="简介" field="description">
<Input.TextArea placeholder="请输入短剧简介" autoSize={{ minRows: 2, maxRows: 4 }} />
</Form.Item>
<Form.Item label="标签" field="tags">
<InputTag placeholder="输入后回车添加标签" allowClear />
</Form.Item>
<Row gutter={16}>
<Col span={8}>
<Form.Item label="地区" field="region">
<Input placeholder="如 中国" />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item label="语言" field="language">
<Input placeholder="如 zh-CN" />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item label="年份" field="year">
<InputNumber placeholder="如 2026" min={1900} style={{ width: "100%" }} />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item label="排序权重" field="sortOrder">
<InputNumber placeholder="数值越大越靠前" style={{ width: "100%" }} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="推荐" field="isRecommended" triggerPropName="checked">
<Switch />
</Form.Item>
</Col>
</Row>
<div className="modal-actions">
<Button onClick={() => setVisible(false)}></Button>
<Button type="primary" htmlType="submit" loading={creating}>
</Button>
</div>
</Form>
</Modal>
</Card>
);
}
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 [dramaData, episodeList] = await Promise.all([getDrama(dramaId), getDramaEpisodes(dramaId)]);
setDrama(dramaData);
setEpisodeCount(dramaData.totalEpisodes || 1);
setEpisodes(episodeList);
} finally {
setLoading(false);
}
};
useEffect(() => {
void loadDetail();
}, [dramaId]);
const configureEpisodes = async () => {
await configureDramaEpisodes(dramaId, episodeCount);
Message.success("剧集数量已更新");
await loadDetail();
};
const updateDramaStatus = async (status: string) => {
await updateDrama(dramaId, { status });
Message.success("短剧状态已更新");
await loadDetail();
};
const uploadEpisodeVideo = async (episode: Episode, file: File) => {
setUploadingEpisodeId(episode.id);
try {
const task = await createUploadTask({
episodeId: episode.id,
fileName: file.name,
contentType: file.type || "video/mp4",
fileSize: file.size,
});
await completeUploadTask(task.jobId);
await syncReviewStatus();
Message.success("视频上传并审核通过");
await loadDetail();
} finally {
setUploadingEpisodeId(null);
}
};
const publishEpisode = async (episode: Episode) => {
await updateEpisode(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>
);
return <DramaList />;
}
export default DramasPage;

20
src/Drama/interface.ts Normal file
View File

@@ -0,0 +1,20 @@
import type { Drama, Episode } from "../api";
export type { Drama, Episode };
export type StatusOption = {
value: string;
label: string;
};
export type DramaDetailPageProps = {
dramaId: number;
};
export type EpisodeTableProps = {
episodes: Episode[];
loading: boolean;
uploadingEpisodeId: number | null;
onUploadVideo: (episode: Episode, file: File) => void | Promise<void>;
onPublishEpisode: (episode: Episode) => void | Promise<void>;
};