feat: 新增频道管理功能,优化整体UI与交互体验
本次提交完成了以下核心变更: 1. 新增完整的频道管理模块,包括频道增删改查API、页面组件与权限配置 2. 为短剧功能添加频道关联字段,支持将短剧绑定到指定频道 3. 全局UI优化:统一使用Panel组件重构页面布局,统一样式规范与交互细节 4. 表格与日志组件优化:新增请求方法颜色标记、耗时高亮、状态码分级展示 5. 登录页、个人中心、布局等页面的视觉升级与交互优化 6. 完善全局TailwindCSS样式与组件样式自定义
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Button, Card, Descriptions, Form, Grid, Input, InputNumber, InputTag, Message, Modal, Select, Space, Switch } from "@arco-design/web-react";
|
||||
import { Button, Descriptions, Form, Grid, Input, InputNumber, InputTag, Message, Modal, Select, Space, Switch } from "@arco-design/web-react";
|
||||
import { IconArrowLeft, IconRefresh } from "@arco-design/web-react/icon";
|
||||
import { dramaApi, mediaApi } from "../../api";
|
||||
import { channelApi, dramaApi, mediaApi } from "../../api";
|
||||
import { Panel } from "../../components/Panel";
|
||||
import { toDramaPayload, toEpisodePayload } from "../formPayload";
|
||||
import type { Drama, DramaDetailPageProps, Episode } from "../interface";
|
||||
import type { Channel, Drama, DramaDetailPageProps, Episode } from "../interface";
|
||||
import { EpisodeTable } from "./EpisodeTable";
|
||||
|
||||
const { Row, Col } = Grid;
|
||||
@@ -32,14 +33,20 @@ export function DramaDetail({ dramaId }: DramaDetailPageProps) {
|
||||
const [uploadingEpisodeId, setUploadingEpisodeId] = useState<number | null>(null);
|
||||
const [editVisible, setEditVisible] = useState(false);
|
||||
const [updatingDrama, setUpdatingDrama] = useState(false);
|
||||
const [channels, setChannels] = useState<Channel[]>([]);
|
||||
|
||||
const loadDetail = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [dramaData, episodeList] = await Promise.all([dramaApi.getDrama(dramaId), dramaApi.getDramaEpisodes(dramaId)]);
|
||||
const [dramaData, episodeList, channelPage] = await Promise.all([
|
||||
dramaApi.getDrama(dramaId),
|
||||
dramaApi.getDramaEpisodes(dramaId),
|
||||
channelApi.listChannels({ page: 1, pageSize: 100 }).catch(() => ({ list: [] })),
|
||||
]);
|
||||
setDrama(dramaData);
|
||||
setEpisodeCount(dramaData.totalEpisodes || 1);
|
||||
setEpisodes(episodeList);
|
||||
setChannels(channelPage.list);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -108,9 +115,7 @@ export function DramaDetail({ dramaId }: DramaDetailPageProps) {
|
||||
|
||||
return (
|
||||
<div className="page-stack">
|
||||
<Card
|
||||
className="section-card"
|
||||
loading={loading && !drama}
|
||||
<Panel
|
||||
title={
|
||||
<Space>
|
||||
<Button icon={<IconArrowLeft />} onClick={() => navigate("/dramas")} />
|
||||
@@ -132,12 +137,15 @@ export function DramaDetail({ dramaId }: DramaDetailPageProps) {
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
{drama && (
|
||||
{loading && !drama ? (
|
||||
<div className="py-12 text-center text-muted">加载中...</div>
|
||||
) : drama ? (
|
||||
<Descriptions
|
||||
column={3}
|
||||
data={[
|
||||
{ label: "短剧ID", value: drama.id },
|
||||
{ label: "名称", value: drama.title },
|
||||
{ label: "频道", value: drama.channelName || "-" },
|
||||
{ label: "地区", value: drama.region || "-" },
|
||||
{ label: "类型", value: drama.type },
|
||||
{ label: "总集数", value: drama.totalEpisodes || 0 },
|
||||
@@ -147,11 +155,10 @@ export function DramaDetail({ dramaId }: DramaDetailPageProps) {
|
||||
{ label: "简介", value: drama.description || "-" },
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
) : null}
|
||||
</Panel>
|
||||
|
||||
<Card
|
||||
className="section-card"
|
||||
<Panel
|
||||
title="剧集配置"
|
||||
extra={
|
||||
<Space>
|
||||
@@ -161,6 +168,7 @@ export function DramaDetail({ dramaId }: DramaDetailPageProps) {
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
noPadding
|
||||
>
|
||||
<EpisodeTable
|
||||
episodes={episodes}
|
||||
@@ -170,7 +178,7 @@ export function DramaDetail({ dramaId }: DramaDetailPageProps) {
|
||||
onPublishEpisode={publishEpisode}
|
||||
onUpdateEpisode={updateEpisode}
|
||||
/>
|
||||
</Card>
|
||||
</Panel>
|
||||
|
||||
{drama && (
|
||||
<Modal title="编辑短剧" visible={editVisible} onCancel={() => setEditVisible(false)} footer={null} style={{ width: 720 }}>
|
||||
@@ -190,19 +198,25 @@ export function DramaDetail({ dramaId }: DramaDetailPageProps) {
|
||||
year: drama.year,
|
||||
sortOrder: drama.sortOrder,
|
||||
isRecommended: drama.isRecommended,
|
||||
totalEpisodes: drama.totalEpisodes || 1,
|
||||
channelId: drama.channelId,
|
||||
}}
|
||||
>
|
||||
<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="请输入类型" />
|
||||
<Col span={8}>
|
||||
<Form.Item label="频道" field="channelId">
|
||||
<Select placeholder="默认频道" allowClear>
|
||||
{channels.map((channel) => (
|
||||
<Select.Option key={channel.id} value={channel.id}>
|
||||
{channel.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Col span={8}>
|
||||
<Form.Item label="状态" field="status">
|
||||
<Select>
|
||||
{STATUS_OPTIONS.map((option) => (
|
||||
@@ -213,6 +227,11 @@ export function DramaDetail({ dramaId }: DramaDetailPageProps) {
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item label="类型" field="type">
|
||||
<Input placeholder="请输入类型" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item label="封面地址" field="coverUrl" rules={[{ required: true }]}>
|
||||
<Input placeholder="请输入封面图片 URL" />
|
||||
@@ -241,17 +260,12 @@ export function DramaDetail({ dramaId }: DramaDetailPageProps) {
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={16}>
|
||||
<Col span={8}>
|
||||
<Col span={12}>
|
||||
<Form.Item label="排序权重" field="sortOrder">
|
||||
<InputNumber style={{ width: "100%" }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item label="剧集总数" field="totalEpisodes" rules={[{ required: true }]}>
|
||||
<InputNumber min={1} max={500} style={{ width: "100%" }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Col span={12}>
|
||||
<Form.Item label="推荐" field="isRecommended" triggerPropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Button, Form, Grid, Input, InputNumber, InputTag, Message, Modal, Select, Space, Switch, Table, Tag } from "@arco-design/web-react";
|
||||
import { IconPlus, IconSearch, IconRefresh } from "@arco-design/web-react/icon";
|
||||
import { dramaApi } from "../../api";
|
||||
import { IconDelete, IconPlus, IconSearch, IconRefresh } from "@arco-design/web-react/icon";
|
||||
import { channelApi, dramaApi } from "../../api";
|
||||
import type { DramaQuery } from "../../api/interface";
|
||||
import { usePageData } from "../../hooks/usePageData";
|
||||
import { Panel } from "../../components/Panel";
|
||||
import { toDramaPayload } from "../formPayload";
|
||||
import type { Drama, StatusOption } from "../interface";
|
||||
import type { Channel, Drama, StatusOption } from "../interface";
|
||||
|
||||
const { Row, Col } = Grid;
|
||||
|
||||
@@ -35,6 +35,12 @@ export function DramaList() {
|
||||
const [filterTitle, setFilterTitle] = useState("");
|
||||
const [filterStatus, setFilterStatus] = useState<string | undefined>(undefined);
|
||||
const [filterType, setFilterType] = useState("");
|
||||
const [channels, setChannels] = useState<Channel[]>([]);
|
||||
const defaultChannel = useMemo(() => channels.find((channel) => channel.isDefault) ?? channels[0], [channels]);
|
||||
|
||||
useEffect(() => {
|
||||
channelApi.listChannels({ page: 1, pageSize: 100 }).then((page) => setChannels(page.list)).catch(() => setChannels([]));
|
||||
}, []);
|
||||
|
||||
const handleSearch = () => {
|
||||
setFilter({ title: filterTitle || undefined, status: filterStatus, type: filterType || undefined });
|
||||
@@ -59,27 +65,30 @@ export function DramaList() {
|
||||
}
|
||||
};
|
||||
|
||||
const deleteDrama = (drama: Drama) => {
|
||||
Modal.confirm({
|
||||
title: "删除短剧",
|
||||
content: `确认删除短剧「${drama.title}」?删除后该短剧下的剧集也会同步删除。`,
|
||||
okButtonProps: { status: "danger" },
|
||||
onOk: async () => {
|
||||
await dramaApi.deleteDrama(drama.id);
|
||||
Message.success("短剧已删除");
|
||||
await reload();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="page-stack">
|
||||
<Panel title="短剧管理">
|
||||
<Row gutter={16} className="mb-0">
|
||||
<Panel title="短剧管理" extra={<Button type="primary" status="success" icon={<IconPlus />} onClick={() => setVisible(true)}>新增短剧</Button>}>
|
||||
<Row gutter={16} align="center">
|
||||
<Col span={6}>
|
||||
<div className="text-[13px] text-muted mb-1.5">短剧标题</div>
|
||||
<Input
|
||||
placeholder="搜索短剧标题"
|
||||
value={filterTitle}
|
||||
onChange={setFilterTitle}
|
||||
allowClear
|
||||
/>
|
||||
<div className="form-label">短剧标题</div>
|
||||
<Input placeholder="搜索短剧标题" value={filterTitle} onChange={setFilterTitle} allowClear />
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<div className="text-[13px] text-muted mb-1.5">状态</div>
|
||||
<Select
|
||||
placeholder="全部状态"
|
||||
value={filterStatus}
|
||||
onChange={setFilterStatus}
|
||||
allowClear
|
||||
>
|
||||
<div className="form-label">状态</div>
|
||||
<Select placeholder="全部状态" value={filterStatus} onChange={setFilterStatus} allowClear>
|
||||
{STATUS_OPTIONS.map((option) => (
|
||||
<Select.Option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
@@ -88,16 +97,11 @@ export function DramaList() {
|
||||
</Select>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<div className="text-[13px] text-muted mb-1.5">类型</div>
|
||||
<Input
|
||||
placeholder="搜索类型"
|
||||
value={filterType}
|
||||
onChange={setFilterType}
|
||||
allowClear
|
||||
/>
|
||||
<div className="form-label">类型</div>
|
||||
<Input placeholder="搜索类型" value={filterType} onChange={setFilterType} allowClear />
|
||||
</Col>
|
||||
<Col span={10}>
|
||||
<div className="text-[13px] mb-1.5"> </div>
|
||||
<div className="form-label invisible">操作</div>
|
||||
<Space>
|
||||
<Button type="primary" icon={<IconSearch />} onClick={handleSearch}>
|
||||
查询
|
||||
@@ -105,21 +109,20 @@ export function DramaList() {
|
||||
<Button icon={<IconRefresh />} onClick={handleReset}>
|
||||
重置
|
||||
</Button>
|
||||
<Button type="primary" status="success" icon={<IconPlus />} onClick={() => setVisible(true)}>
|
||||
新增短剧
|
||||
</Button>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
</Panel>
|
||||
|
||||
<Panel>
|
||||
<Panel noPadding>
|
||||
<Table
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
data={data.list}
|
||||
pagination={{
|
||||
current: data.pagination.page,
|
||||
sizeCanChange: true,
|
||||
showTotal: true,
|
||||
total: data.pagination.total,
|
||||
pageSize: data.pagination.pageSize,
|
||||
onChange: (page, pageSize) => changePage(page, pageSize),
|
||||
@@ -145,11 +148,16 @@ export function DramaList() {
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
width: 120,
|
||||
width: 180,
|
||||
render: (_, record) => (
|
||||
<Button type="text" onClick={() => navigate(`/dramas/${record.id}`)}>
|
||||
详情
|
||||
</Button>
|
||||
<Space>
|
||||
<Button type="text" onClick={() => navigate(`/dramas/${record.id}`)}>
|
||||
详情
|
||||
</Button>
|
||||
<Button type="text" status="danger" icon={<IconDelete />} onClick={() => deleteDrama(record)}>
|
||||
删除
|
||||
</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]}
|
||||
@@ -157,17 +165,23 @@ export function DramaList() {
|
||||
</Panel>
|
||||
|
||||
<Modal title="创建短剧" visible={visible} onCancel={() => setVisible(false)} footer={null} style={{ width: 640 }}>
|
||||
<Form layout="vertical" onSubmit={createDrama} initialValues={{ status: "draft", isRecommended: false, sortOrder: 0, totalEpisodes: 1 }}>
|
||||
<Form layout="vertical" onSubmit={createDrama} initialValues={{ status: "draft", isRecommended: false, sortOrder: 0, totalEpisodes: 1, channelId: defaultChannel?.id }}>
|
||||
<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="请输入类型,如复仇、爱情、动作等" />
|
||||
<Col span={8}>
|
||||
<Form.Item label="频道" field="channelId">
|
||||
<Select placeholder="默认频道" allowClear>
|
||||
{channels.map((channel) => (
|
||||
<Select.Option key={channel.id} value={channel.id}>
|
||||
{channel.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Col span={8}>
|
||||
<Form.Item label="状态" field="status">
|
||||
<Select placeholder="请选择状态">
|
||||
{STATUS_OPTIONS.map((option) => (
|
||||
@@ -178,6 +192,11 @@ export function DramaList() {
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item label="类型" field="type">
|
||||
<Input placeholder="请输入类型,如复仇、爱情、动作等" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item label="封面地址" field="coverUrl" rules={[{ required: true }]}>
|
||||
<Input placeholder="请输入封面图片 URL" />
|
||||
|
||||
@@ -7,6 +7,7 @@ type DramaFormValues = Partial<
|
||||
| "type"
|
||||
| "status"
|
||||
| "coverUrl"
|
||||
| "channelId"
|
||||
| "description"
|
||||
| "tags"
|
||||
| "region"
|
||||
@@ -55,6 +56,7 @@ export function toDramaPayload(values: DramaFormValues): DramaFormValues {
|
||||
type: normalizeOptionalString(values.type),
|
||||
status: values.status,
|
||||
coverUrl: normalizeOptionalString(values.coverUrl),
|
||||
channelId: normalizePositiveInteger(values.channelId),
|
||||
description: normalizeOptionalString(values.description),
|
||||
tags: values.tags?.filter(Boolean),
|
||||
region: normalizeOptionalString(values.region),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Drama, Episode } from "../api";
|
||||
import type { Channel, Drama, Episode } from "../api";
|
||||
|
||||
export type { Drama, Episode };
|
||||
export type { Channel, Drama, Episode };
|
||||
|
||||
export type StatusOption = {
|
||||
value: string;
|
||||
|
||||
Reference in New Issue
Block a user