From 2dace1cf8d22116da0ce0201a9870d3e8a3fc467 Mon Sep 17 00:00:00 2001 From: zhxiao1124 Date: Thu, 2 Jul 2026 18:48:51 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=9F=AD=E5=89=A7?= =?UTF-8?q?=E7=AD=9B=E9=80=89=E5=8A=9F=E8=83=BD=E5=92=8C=E8=87=AA=E5=AE=9A?= =?UTF-8?q?=E4=B9=89Panel=E5=AE=B9=E5=99=A8=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 新增DramaQuery类型扩展分页查询参数,支持标题、状态、类型筛选 2. 重构usePageData hook,支持自定义查询类型和筛选状态管理 3. 新建Panel通用容器组件替代原生Card 4. 为短剧管理页面添加搜索筛选UI和逻辑 5. 统一所有页面的分页查询默认参数 6. 调整布局样式优化后台管理界面展示 --- .mimocode/plans/1782986311447-swift-planet.md | 59 +++++ src/Dashboard/index.tsx | 6 +- src/Data/index.tsx | 6 +- src/Drama/components/DramaList.tsx | 241 +++++++++++------- src/Log/index.tsx | 2 +- src/Personnel/index.tsx | 4 +- src/Role/index.tsx | 4 +- src/User/index.tsx | 27 +- src/api/dramas.ts | 4 +- src/api/interface.ts | 6 + src/components/Panel/index.tsx | 22 ++ src/hooks/interface.ts | 2 +- src/hooks/usePageData.ts | 34 ++- src/layout/index.tsx | 8 +- 14 files changed, 295 insertions(+), 130 deletions(-) create mode 100644 .mimocode/plans/1782986311447-swift-planet.md create mode 100644 src/components/Panel/index.tsx diff --git a/.mimocode/plans/1782986311447-swift-planet.md b/.mimocode/plans/1782986311447-swift-planet.md new file mode 100644 index 0000000..6014b2d --- /dev/null +++ b/.mimocode/plans/1782986311447-swift-planet.md @@ -0,0 +1,59 @@ +# DramaList 筛选功能 + 自定义容器组件 + +## 目标 +1. 为短剧管理页面添加搜索筛选功能(标题、状态、类型) +2. 将 Card 容器替换为自定义实现的容器组件 + +## 分析 + +### 现有架构 +- `PageQuery` 类型仅支持 `{ page, pageSize }`,无筛选字段 +- `fetchPage` 直接将 params 传给 axios,扩展字段会自动作为 query 参数发送 +- `usePageData` hook 固定使用 `PageQuery`,需扩展支持筛选 + +### 筛选字段(基于 Drama 类型) +- `title` - 短剧标题(模糊搜索) +- `status` - 状态(draft/scheduled/published/offline) +- `type` - 类型(如复仇、爱情、动作等) + +## 实现方案 + +### 1. 扩展 API 查询类型 +**文件**: `src/api/interface.ts` +- 新增 `DramaQuery` 类型,继承 `PageQuery`,添加 `title?`、`status?`、`type?` 字段 + +**文件**: `src/api/dramas.ts` +- `listDramas` 参数类型从 `PageQuery` 改为 `DramaQuery` + +### 2. 扩展 usePageData hook +**文件**: `src/hooks/interface.ts` +- `PageFetcher` 泛型扩展,支持自定义查询类型 + +**文件**: `src/hooks/usePageData.ts` +- 泛型参数增加 `Q extends PageQuery`,支持自定义查询类型 +- 新增 `setQuery` 方法用于更新筛选条件 +- 筛选条件变化时重置到第 1 页 + +### 3. 创建自定义容器组件 +**文件**: `src/components/Panel/index.tsx`(新建) +- 实现一个轻量级容器组件,替代 Card +- Props: `title`、`extra`(右侧操作区)、`children`、`className` +- 样式:圆角、边框、阴影,与现有 `.section-card` 风格一致 + +### 4. 改造 DramaList 页面 +**文件**: `src/Drama/components/DramaList.tsx` +- 使用自定义 Panel 组件替换 Card +- 添加筛选区域:标题输入框 + 状态选择器 + 类型输入框 + 查询/重置按钮 +- 管理筛选状态,调用 `setQuery` 更新查询 + +## 修改文件清单 +1. `src/api/interface.ts` - 新增 DramaQuery 类型 +2. `src/api/dramas.ts` - 更新 listDramas 参数类型 +3. `src/hooks/interface.ts` - 扩展 PageFetcher 泛型 +4. `src/hooks/usePageData.ts` - 支持自定义查询类型和 setQuery +5. `src/components/Panel/index.tsx` - 新建自定义容器组件 +6. `src/Drama/components/DramaList.tsx` - 添加筛选 UI 和 Panel + +## 验证 +- `npm run lint` TypeScript 检查通过 +- `npm run build` 构建成功 diff --git a/src/Dashboard/index.tsx b/src/Dashboard/index.tsx index 28c4d9e..9f35676 100644 --- a/src/Dashboard/index.tsx +++ b/src/Dashboard/index.tsx @@ -39,9 +39,9 @@ function DashboardPage({ profile }: DashboardPageProps) { const canReadDramas = hasPermissions(profile, ['drama:read']); const canReadUsers = hasPermissions(profile, ['user:read']); const canReadLogs = hasPermissions(profile, ['log:read']); - const dramas = usePageData(dramaApi.listDramas, canReadDramas); - const users = usePageData(userApi.listUsers, canReadUsers); - const logs = usePageData(logApi.listRequestLogs, canReadLogs); + const dramas = usePageData(dramaApi.listDramas, { page: 1, pageSize: 20 }, canReadDramas); + const users = usePageData(userApi.listUsers, { page: 1, pageSize: 20 }, canReadUsers); + const logs = usePageData(logApi.listRequestLogs, { page: 1, pageSize: 20 }, canReadLogs); const hasAnyDataModule = canReadDramas || canReadUsers || canReadLogs; return ( diff --git a/src/Data/index.tsx b/src/Data/index.tsx index 71138b0..f951ac4 100644 --- a/src/Data/index.tsx +++ b/src/Data/index.tsx @@ -17,9 +17,9 @@ function DataPage() { totalComments: 0, }); const [syncing, setSyncing] = useState(false); - const dramas = usePageData(dataApi.listDramaMetrics); - const episodes = usePageData(dataApi.listEpisodeMetrics); - const jobs = usePageData(dataApi.listSyncJobs); + const dramas = usePageData(dataApi.listDramaMetrics, { page: 1, pageSize: 20 }); + const episodes = usePageData(dataApi.listEpisodeMetrics, { page: 1, pageSize: 20 }); + const jobs = usePageData(dataApi.listSyncJobs, { page: 1, pageSize: 20 }); const loadOverview = async () => { setOverview(await dataApi.getDataOverview()); diff --git a/src/Drama/components/DramaList.tsx b/src/Drama/components/DramaList.tsx index d4efabf..bc76e58 100644 --- a/src/Drama/components/DramaList.tsx +++ b/src/Drama/components/DramaList.tsx @@ -1,9 +1,11 @@ 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 { 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 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"; @@ -23,11 +25,27 @@ const PUBLISH_STATUS_LABEL: Record = { offline: "已下线", }; +const INITIAL_QUERY: DramaQuery = { page: 1, pageSize: 20 }; + export function DramaList() { const navigate = useNavigate(); - const { data, loading, reload, changePage } = usePageData(dramaApi.listDramas); + const { data, loading, reload, changePage, setFilter } = usePageData(dramaApi.listDramas, INITIAL_QUERY); const [visible, setVisible] = useState(false); const [creating, setCreating] = useState(false); + const [filterTitle, setFilterTitle] = useState(""); + const [filterStatus, setFilterStatus] = useState(undefined); + const [filterType, setFilterType] = useState(""); + + const handleSearch = () => { + setFilter({ title: filterTitle || undefined, status: filterStatus, type: filterType || undefined }); + }; + + const handleReset = () => { + setFilterTitle(""); + setFilterStatus(undefined); + setFilterType(""); + setFilter({ title: undefined, status: undefined, type: undefined }); + }; const createDrama = async (values: Partial) => { setCreating(true); @@ -43,15 +61,59 @@ export function DramaList() { return (
- } onClick={() => setVisible(true)}> - 新增短剧 - - } - > + + + +
短剧标题
+ + + +
状态
+ + + +
类型
+ + + +
 
+ + + + + + +
+
+ + (value ? 封面图 : null) }, + { title: "封面图", dataIndex: "coverUrl", width: 120, render: (value) => (value ? 封面图 : null) }, { title: "标题", dataIndex: "title" }, { title: "地区", dataIndex: "region", width: 120 }, { title: "类型", dataIndex: "type", width: 140 }, - { title: "已发布 / 总集数", dataIndex: "totalEpisodes", width: 180, render: (_, record) => `${record.publishedEpisodes}(已发布)/共${record.totalEpisodes}集` }, + { title: "已发布 / 总集数", dataIndex: "totalEpisodes", width: 180, render: (_, record) => `${record.publishedEpisodes || 0}(已发布)/共${record.totalEpisodes || 0}集` }, { title: "状态", dataIndex: "status", @@ -92,81 +154,82 @@ export function DramaList() { }, ]} /> - setVisible(false)} footer={null} style={{ width: 640 }}> -
- - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - + + + setVisible(false)} footer={null} style={{ width: 640 }}> +
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + ); } diff --git a/src/Log/index.tsx b/src/Log/index.tsx index dbdb362..29aee8b 100644 --- a/src/Log/index.tsx +++ b/src/Log/index.tsx @@ -5,7 +5,7 @@ import { RequestLogTable } from '../components/RequestLogTable'; import type { RequestLog } from './interface'; function LogsPage() { - const { data, loading } = usePageData(logApi.listRequestLogs); + const { data, loading } = usePageData(logApi.listRequestLogs, { page: 1, pageSize: 20 }); return ; } diff --git a/src/Personnel/index.tsx b/src/Personnel/index.tsx index ebf5a0c..05b3db2 100644 --- a/src/Personnel/index.tsx +++ b/src/Personnel/index.tsx @@ -6,8 +6,8 @@ import { usePageData } from '../hooks/usePageData'; import type { AdminUser, CreatePersonnelFormValues, Role } from './interface'; function PersonnelPage() { - const { data, loading, reload, changePage } = usePageData(personnelApi.listPersonnel); - const roles = usePageData(roleApi.listRoles); + const { data, loading, reload, changePage } = usePageData(personnelApi.listPersonnel, { page: 1, pageSize: 20 }); + const roles = usePageData(roleApi.listRoles, { page: 1, pageSize: 20 }); const [createForm] = Form.useForm(); const [createVisible, setCreateVisible] = useState(false); const [creating, setCreating] = useState(false); diff --git a/src/Role/index.tsx b/src/Role/index.tsx index 663f714..98396f7 100644 --- a/src/Role/index.tsx +++ b/src/Role/index.tsx @@ -7,8 +7,8 @@ import { PermissionTreePicker } from "./components/PermissionTreePicker"; import type { CreateRoleFormValues, Permission, Role } from "./interface"; function RolesPage() { - const { data, loading, reload, changePage } = usePageData(roleApi.listRoles); - const permissions = usePageData(roleApi.listPermissions); + const { data, loading, reload, changePage } = usePageData(roleApi.listRoles, { page: 1, pageSize: 20 }); + const permissions = usePageData(roleApi.listPermissions, { page: 1, pageSize: 20 }); const [createForm] = Form.useForm(); const [createVisible, setCreateVisible] = useState(false); const [creatingRole, setCreatingRole] = useState(false); diff --git a/src/User/index.tsx b/src/User/index.tsx index ed1b163..9150ee6 100644 --- a/src/User/index.tsx +++ b/src/User/index.tsx @@ -4,7 +4,7 @@ import { usePageData } from '../hooks/usePageData'; import type { AppUser } from './interface'; function UsersPage() { - const { data, loading, reload, changePage } = usePageData(userApi.listUsers); + const { data, loading, reload, changePage } = usePageData(userApi.listUsers, { page: 1, pageSize: 20 }); const update状态 = async (id: number, status: 'active' | 'disabled') => { await userApi.updateUserStatus(id, status); Message.success('用户状态已更新'); @@ -12,15 +12,22 @@ function UsersPage() { }; return ( - -
{status} }, - { title: '操作', width: 160, render: (_, record: AppUser) => }, - ]} /> - +
+ +
+

小程序用户管理

+
+
+ +
{status} }, + { title: '操作', width: 160, render: (_, record: AppUser) => }, + ]} /> + + ); } diff --git a/src/api/dramas.ts b/src/api/dramas.ts index f7e35d6..1ae2fd8 100644 --- a/src/api/dramas.ts +++ b/src/api/dramas.ts @@ -1,7 +1,7 @@ import { fetchPage, request } from "../plugins/axios"; -import type { ApiResponse, CreateDramaPayload, Drama, Episode, PageQuery, UpdateDramaPayload, UpdateEpisodePayload } from "./interface"; +import type { ApiResponse, CreateDramaPayload, Drama, DramaQuery, Episode, UpdateDramaPayload, UpdateEpisodePayload } from "./interface"; -export function listDramas(query: PageQuery) { +export function listDramas(query: DramaQuery) { return fetchPage('/api/admin/v1/dramas', query); } diff --git a/src/api/interface.ts b/src/api/interface.ts index 7090ee5..64d0588 100644 --- a/src/api/interface.ts +++ b/src/api/interface.ts @@ -12,6 +12,12 @@ export type PageQuery = { pageSize: number; }; +export type DramaQuery = PageQuery & { + title?: string; + status?: string; + type?: string; +}; + export type PageData = { list: T[]; pagination: { diff --git a/src/components/Panel/index.tsx b/src/components/Panel/index.tsx new file mode 100644 index 0000000..5e80434 --- /dev/null +++ b/src/components/Panel/index.tsx @@ -0,0 +1,22 @@ +import type { ReactNode } from "react"; + +type PanelProps = { + title?: ReactNode; + extra?: ReactNode; + children: ReactNode; + className?: string; +}; + +export function Panel({ title, extra, children, className = "" }: PanelProps) { + return ( +
+ {(title || extra) && ( +
+ {title &&

{title}

} + {extra &&
{extra}
} +
+ )} +
{children}
+
+ ); +} diff --git a/src/hooks/interface.ts b/src/hooks/interface.ts index d2ef804..baa3a82 100644 --- a/src/hooks/interface.ts +++ b/src/hooks/interface.ts @@ -2,4 +2,4 @@ import type { PageData, PageQuery } from "../api"; export type { PageData, PageQuery }; -export type PageFetcher = (query: PageQuery) => Promise>; +export type PageFetcher = (query: Q) => Promise>; diff --git a/src/hooks/usePageData.ts b/src/hooks/usePageData.ts index 9078fd1..1026fb9 100644 --- a/src/hooks/usePageData.ts +++ b/src/hooks/usePageData.ts @@ -1,45 +1,53 @@ import { useEffect, useState } from "react"; -import type { PageData, PageFetcher, PageQuery } from "./interface"; +import type { PageData, PageQuery } from "../api"; +import type { PageFetcher } from "./interface"; import { resolvePageData } from "./pageData"; -export function usePageData(fetcher: PageFetcher, enabled = true) { +export function usePageData( + fetcher: PageFetcher, + initialQuery: Q, + enabled = true, +) { const [loading, setLoading] = useState(false); - const [pageQuery, setPageQuery] = useState({ - page: 1, - pageSize: 20, - }); + const [query, setQuery] = useState(initialQuery); const [data, setData] = useState>({ list: [], pagination: { page: 1, pageSize: 20, total: 0 }, }); - const load = async (query = pageQuery) => { + const load = async (q = query) => { if (!enabled) { return; } setLoading(true); try { - const response = await fetcher(query); - setData(resolvePageData(response, query)); + const response = await fetcher(q); + setData(resolvePageData(response, q)); } finally { setLoading(false); } }; const changePage = (page: number, pageSize: number) => { - setPageQuery({ page, pageSize }); + setQuery((prev) => ({ ...prev, page, pageSize } as Q)); + }; + + const setFilter = (patch: Partial) => { + setQuery((prev) => ({ ...prev, ...patch, page: 1 } as Q)); }; useEffect(() => { if (enabled) { - void load(pageQuery); + void load(query); } - }, [fetcher, enabled, pageQuery.page, pageQuery.pageSize]); + }, [fetcher, enabled, query]); return { data, loading, - reload: () => load(pageQuery), + query, + reload: () => load(query), changePage, + setFilter, }; } diff --git a/src/layout/index.tsx b/src/layout/index.tsx index 8c0bc11..be2343b 100644 --- a/src/layout/index.tsx +++ b/src/layout/index.tsx @@ -48,7 +48,7 @@ export function AdminLayout() { }; return ( - +
TK @@ -64,8 +64,8 @@ export function AdminLayout() { {visibleMenuItems.map((item) => renderMenuItem(item))} - -
+ +
- + } />