feat: 新增频道管理功能,优化整体UI与交互体验

本次提交完成了以下核心变更:
1.  新增完整的频道管理模块,包括频道增删改查API、页面组件与权限配置
2.  为短剧功能添加频道关联字段,支持将短剧绑定到指定频道
3.  全局UI优化:统一使用Panel组件重构页面布局,统一样式规范与交互细节
4.  表格与日志组件优化:新增请求方法颜色标记、耗时高亮、状态码分级展示
5.  登录页、个人中心、布局等页面的视觉升级与交互优化
6.  完善全局TailwindCSS样式与组件样式自定义
This commit is contained in:
2026-07-03 15:01:27 +08:00
parent 2dace1cf8d
commit e830f5a733
29 changed files with 667 additions and 170 deletions

View File

@@ -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">&nbsp;</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" />