From 526052d633b2594125ca92022628bcd67d270a7f Mon Sep 17 00:00:00 2001 From: zhxiao1124 Date: Thu, 2 Jul 2026 11:50:07 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=E5=88=86?= =?UTF-8?q?=E9=A1=B5=E9=80=BB=E8=BE=91=E5=B9=B6=E7=BB=9F=E4=B8=80=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E9=A3=8E=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 新增分页数据处理hook和工具函数,实现分页状态统一管理 2. 替换所有页面的表格分页配置为标准current/page模式 3. 统一项目内字符串引号风格为双引号 4. 调整权限树面板样式和布局代码格式 --- .prettierrc | 9 + src/Data/index.tsx | 6 +- src/Drama/index.tsx | 810 ++++++++++++++---------------- src/Personnel/index.tsx | 4 +- src/Role/PermissionTreePicker.tsx | 223 ++++---- src/Role/index.css | 1 + src/Role/index.tsx | 244 +++++---- src/User/index.tsx | 4 +- src/api/client.ts | 21 +- src/hooks/pageData.ts | 20 + src/hooks/usePageData.ts | 70 ++- src/layout/index.tsx | 267 ++++++---- tests/pageData.test.ts | 14 + 13 files changed, 884 insertions(+), 809 deletions(-) create mode 100644 .prettierrc create mode 100644 src/hooks/pageData.ts create mode 100644 tests/pageData.test.ts diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..0f88dd5 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,9 @@ +{ + "tabWidth": 4, + "singleQuote": false, + "trailingComma": "all", + "printWidth": 390, + "eslintIntegration": true, + "bracketSpacing": true, + "stylelintIntegration": true +} diff --git a/src/Data/index.tsx b/src/Data/index.tsx index 3297ab8..98f392f 100644 --- a/src/Data/index.tsx +++ b/src/Data/index.tsx @@ -55,7 +55,7 @@ function DataPage() { - -
-
= { - not_submitted: "未提交", - pending: "待审核", - reviewing: "审核中", - approved: "已通过", - rejected: "已拒绝", + not_submitted: "未提交", + pending: "待审核", + reviewing: "审核中", + approved: "已通过", + rejected: "已拒绝", }; const PUBLISH_STATUS_LABEL: Record = { - draft: "草稿", - scheduled: "定时发布", - published: "已发布", - offline: "已下线", + draft: "草稿", + scheduled: "定时发布", + published: "已发布", + offline: "已下线", }; function DramasPage() { - const { id } = useParams(); + const { id } = useParams(); - if (id) { - return ; - } + if (id) { + return ; + } - return ; + return ; } function DramaListPage() { - const navigate = useNavigate(); - const { data, loading, reload } = usePageData("/api/admin/v1/dramas"); - const [visible, setVisible] = useState(false); - const [creating, setCreating] = useState(false); + const navigate = useNavigate(); + const { data, loading, reload, changePage } = usePageData("/api/admin/v1/dramas"); + const [visible, setVisible] = useState(false); + const [creating, setCreating] = useState(false); - const createDrama = async (values: Partial) => { - setCreating(true); - try { - await api.post("/api/admin/v1/dramas", values); - Message.success("短剧创建成功"); - setVisible(false); - await reload(); - } finally { - setCreating(false); - } - }; + const createDrama = async (values: Partial) => { + setCreating(true); + try { + await api.post("/api/admin/v1/dramas", values); + Message.success("短剧创建成功"); + setVisible(false); + await reload(); + } finally { + setCreating(false); + } + }; - return ( - } - onClick={() => setVisible(true)} + return ( + } onClick={() => setVisible(true)}> + 新增短剧 + + } > - 新增短剧 - - } - > -
( - - {PUBLISH_STATUS_LABEL[String(status)] || String(status)} - - ), - }, - { - title: "推荐", - dataIndex: "isRecommended", - width: 100, - render: (value) => - value ? : , - }, - { - title: "操作", - width: 120, - render: (_, record) => ( - - ), - }, - ]} - /> - setVisible(false)} - footer={null} - style={{ width: 640 }} - > -
- - - - -
- - - - - - - - - - - - - - - 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) => ( + {PUBLISH_STATUS_LABEL[String(status)] || String(status)} + ), + }, + { + title: "推荐", + dataIndex: "isRecommended", + width: 100, + render: (value) => (value ? : ), + }, + { + title: "操作", + width: 120, + render: (_, record) => ( + + ), + }, + ]} /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - ); + setVisible(false)} footer={null} style={{ width: 640 }}> +
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + ); } function DramaDetailPage({ dramaId }: { dramaId: number }) { - const navigate = useNavigate(); - const [drama, setDrama] = useState(null); - const [episodes, setEpisodes] = useState([]); - const [loading, setLoading] = useState(false); - const [episodeCount, setEpisodeCount] = useState(1); - const [uploadingEpisodeId, setUploadingEpisodeId] = useState( - null, - ); + const navigate = useNavigate(); + const [drama, setDrama] = useState(null); + const [episodes, setEpisodes] = useState([]); + const [loading, setLoading] = useState(false); + const [episodeCount, setEpisodeCount] = useState(1); + const [uploadingEpisodeId, setUploadingEpisodeId] = useState(null); - const loadDetail = async () => { - setLoading(true); - try { - const [dramaResponse, episodeResponse] = await Promise.all([ - api.get>(`/api/admin/v1/dramas/${dramaId}`), - api.get>( - `/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>( - "/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 ( -
- - - - + + + + } > - 上架短剧 - - - } - > - {drama && ( - - )} - + {drama && ( + + )} + - - setEpisodeCount(Number(value) || 1)} - /> - - - } - > -
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) => ( - - {REVIEW_STATUS_LABEL[String(status)] || String(status)} - - ), - }, - { - title: "发布状态", - dataIndex: "publishStatus", - width: 120, - render: (status) => ( - - {PUBLISH_STATUS_LABEL[String(status)] || String(status)} - - ), - }, - { - title: "操作", - width: 280, - render: (_, record) => ( - - - - - ), - }, - ]} - /> - - - ); + + setEpisodeCount(Number(value) || 1)} /> + + + } + > +
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) => ( + {REVIEW_STATUS_LABEL[String(status)] || String(status)} + ), + }, + { + title: "发布状态", + dataIndex: "publishStatus", + width: 120, + render: (status) => ( + {PUBLISH_STATUS_LABEL[String(status)] || String(status)} + ), + }, + { + title: "操作", + width: 280, + render: (_, record) => ( + + + + + ), + }, + ]} + /> + + + ); } export default DramasPage; diff --git a/src/Personnel/index.tsx b/src/Personnel/index.tsx index 57187a9..2c5a62e 100644 --- a/src/Personnel/index.tsx +++ b/src/Personnel/index.tsx @@ -6,7 +6,7 @@ import { api } from '../api/client'; import { usePageData } from '../hooks/usePageData'; function PersonnelPage() { - const { data, loading, reload } = usePageData('/api/admin/v1/admin-users'); + const { data, loading, reload, changePage } = usePageData('/api/admin/v1/admin-users'); const roles = usePageData('/api/admin/v1/roles'); const [createForm] = Form.useForm(); const [createVisible, setCreateVisible] = useState(false); @@ -77,7 +77,7 @@ function PersonnelPage() { return ( } onClick={openCreate}>新增人员}> -
= { - read: '查看', - create: '创建', - update: '更新', - delete: '删除', - manage: '管理', + read: "查看", + create: "创建", + update: "更新", + delete: "删除", + manage: "管理", }; function permissionKey(id: number) { - return `permission:${id}`; + return `permission:${id}`; } function permissionIdFromKey(key: string) { - const id = Number(key.replace('permission:', '')); - return Number.isInteger(id) ? id : null; + const id = Number(key.replace("permission:", "")); + return Number.isInteger(id) ? id : null; } function getPermissionPrefix(code: string) { - return code.split(':')[0]; + return code.split(":")[0]; } function getPermissionAction(code: string) { - return code.split(':')[1] || code; + return code.split(":")[1] || code; } function getPermissionTitle(permission: Permission, module?: PermissionModuleConfig) { - return permission.name || (module ? `${actionNames[getPermissionAction(permission.code)]}${module.subject}` : permission.code); + return permission.name || (module ? `${actionNames[getPermissionAction(permission.code)]}${module.subject}` : permission.code); } -function buildPermissionModuleNode( - permissions: Permission[], - module: PermissionModuleConfig, -): PermissionTreeNode | null { - const children = permissions - .filter((permission) => getPermissionPrefix(permission.code) === module.prefix) - .map((permission) => ({ - key: permissionKey(permission.id), - title: getPermissionTitle(permission, module), - })); +function buildPermissionModuleNode(permissions: Permission[], module: PermissionModuleConfig): PermissionTreeNode | null { + const children = permissions + .filter((permission) => getPermissionPrefix(permission.code) === module.prefix) + .map((permission) => ({ + key: permissionKey(permission.id), + title: getPermissionTitle(permission, module), + })); - if (!children.length) { - return null; - } + if (!children.length) { + return null; + } - return { - key: `module:${module.prefix}`, - title: module.title, - children, - }; + return { + key: `module:${module.prefix}`, + title: module.title, + children, + }; } function buildPermissionTree(permissions: Permission[]): PermissionTreeNode[] { - const knownPrefixes = new Set([ - ...systemPermissionModules.map((module) => module.prefix), - ...businessPermissionModules.map((module) => module.prefix), - ]); - const systemChildren = systemPermissionModules - .map((module) => buildPermissionModuleNode(permissions, module)) - .filter((node): node is PermissionTreeNode => Boolean(node)); - const businessChildren = businessPermissionModules - .map((module) => buildPermissionModuleNode(permissions, module)) - .filter((node): node is PermissionTreeNode => Boolean(node)); - const otherPermissions = permissions - .filter((permission) => !knownPrefixes.has(getPermissionPrefix(permission.code))) - .map((permission) => ({ - key: permissionKey(permission.id), - title: permission.name, - })); + const knownPrefixes = new Set([ + ...systemPermissionModules.map((module) => module.prefix), + ...businessPermissionModules.map((module) => module.prefix), + ]); + const systemChildren = systemPermissionModules + .map((module) => buildPermissionModuleNode(permissions, module)) + .filter((node): node is PermissionTreeNode => Boolean(node)); + const businessChildren = businessPermissionModules + .map((module) => buildPermissionModuleNode(permissions, module)) + .filter((node): node is PermissionTreeNode => Boolean(node)); + const otherPermissions = permissions + .filter((permission) => !knownPrefixes.has(getPermissionPrefix(permission.code))) + .map((permission) => ({ + key: permissionKey(permission.id), + title: permission.name, + })); - if (otherPermissions.length) { - businessChildren.push({ - key: 'module:other', - title: '其他权限', - children: otherPermissions, - }); - } + if (otherPermissions.length) { + businessChildren.push({ + key: "module:other", + title: "其他权限", + children: otherPermissions, + }); + } - return [ - { key: 'group:system', title: '系统权限', children: systemChildren }, - { key: 'group:business', title: '业务权限', children: businessChildren }, - ]; + return [ + { key: "group:system", title: "系统权限", children: systemChildren }, + { key: "group:business", title: "业务权限", children: businessChildren }, + ]; } function collectExpandedKeys(nodes: PermissionTreeNode[]) { - const keys: string[] = []; - const visit = (items: PermissionTreeNode[]) => { - items.forEach((item) => { - if (item.children?.length) { - keys.push(item.key); - visit(item.children); - } - }); - }; - visit(nodes); - return keys; + const keys: string[] = []; + const visit = (items: PermissionTreeNode[]) => { + items.forEach((item) => { + if (item.children?.length) { + keys.push(item.key); + visit(item.children); + } + }); + }; + visit(nodes); + return keys; } export function PermissionTreePicker({ - permissions, - value, - onChange, + permissions, + value, + onChange, }: { - permissions: Permission[]; - value: number[]; - onChange: (permissionIds: number[]) => void; + permissions: Permission[]; + value: number[]; + onChange: (permissionIds: number[]) => void; }) { - const treeData = useMemo(() => buildPermissionTree(permissions), [permissions]); - const expandedKeys = useMemo(() => collectExpandedKeys(treeData), [treeData]); - const checkedKeys = value.map((id) => permissionKey(id)); + const treeData = useMemo(() => buildPermissionTree(permissions), [permissions]); + const expandedKeys = useMemo(() => collectExpandedKeys(treeData), [treeData]); + const checkedKeys = value.map((id) => permissionKey(id)); - return ( -
- { - onChange( - keys - .map((key) => permissionIdFromKey(key)) - .filter((id): id is number => id !== null), - ); - }} - /> -
- ); + return ( +
+ { + onChange(keys.map((key) => permissionIdFromKey(key)).filter((id): id is number => id !== null)); + }} + /> +
+ ); } diff --git a/src/Role/index.css b/src/Role/index.css index 464fa45..4711fe7 100644 --- a/src/Role/index.css +++ b/src/Role/index.css @@ -15,6 +15,7 @@ .permission-tree-panel { height: 360px; overflow: auto; + margin-bottom: 15px; padding: 12px 16px; border: 1px solid #e5e6eb; border-radius: 6px; diff --git a/src/Role/index.tsx b/src/Role/index.tsx index a941bc2..576b978 100644 --- a/src/Role/index.tsx +++ b/src/Role/index.tsx @@ -1,117 +1,147 @@ -import { useState } from 'react'; -import { Button, Card, Form, Input, Message, Modal, Space, Table, Tag } from '@arco-design/web-react'; -import { IconPlus } from '@arco-design/web-react/icon'; -import type { Permission, Role } from '../api/types'; -import { api } from '../api/client'; -import { usePageData } from '../hooks/usePageData'; -import { PermissionTreePicker } from './PermissionTreePicker'; -import './index.css'; +import { useState } from "react"; +import { Button, Card, Form, Input, Message, Modal, Space, Table, Tag } from "@arco-design/web-react"; +import { IconPlus } from "@arco-design/web-react/icon"; +import type { Permission, Role } from "../api/types"; +import { api } from "../api/client"; +import { usePageData } from "../hooks/usePageData"; +import { PermissionTreePicker } from "./PermissionTreePicker"; +import "./index.css"; function RolesPage() { - const { data, loading, reload } = usePageData('/api/admin/v1/roles'); - const permissions = usePageData('/api/admin/v1/permissions'); - const [createForm] = Form.useForm(); - const [createVisible, setCreateVisible] = useState(false); - const [creatingRole, setCreatingRole] = useState(false); - const [createPermissionIds, setCreatePermissionIds] = useState([]); - const [assignmentVisible, setAssignmentVisible] = useState(false); - const [selectedRole, setSelectedRole] = useState(null); - const [selectedPermissionIds, setSelectedPermissionIds] = useState([]); - const [savingPermissions, setSavingPermissions] = useState(false); + const { data, loading, reload, changePage } = usePageData("/api/admin/v1/roles"); + const permissions = usePageData("/api/admin/v1/permissions"); + const [createForm] = Form.useForm(); + const [createVisible, setCreateVisible] = useState(false); + const [creatingRole, setCreatingRole] = useState(false); + const [createPermissionIds, setCreatePermissionIds] = useState([]); + const [assignmentVisible, setAssignmentVisible] = useState(false); + const [selectedRole, setSelectedRole] = useState(null); + const [selectedPermissionIds, setSelectedPermissionIds] = useState([]); + const [savingPermissions, setSavingPermissions] = useState(false); - const openCreateRole = () => { - createForm.resetFields(); - setCreatePermissionIds([]); - setCreateVisible(true); - }; + const openCreateRole = () => { + createForm.resetFields(); + setCreatePermissionIds([]); + setCreateVisible(true); + }; - const createRole = async (values: { code: string; name: string; description?: string }) => { - setCreatingRole(true); - try { - await api.post('/api/admin/v1/roles', { - ...values, - permissionIds: createPermissionIds, - }); - Message.success('角色创建成功'); - setCreateVisible(false); - await reload(); - } finally { - setCreatingRole(false); - } - }; + const createRole = async (values: { code: string; name: string; description?: string }) => { + setCreatingRole(true); + try { + await api.post("/api/admin/v1/roles", { + ...values, + permissionIds: createPermissionIds, + }); + Message.success("角色创建成功"); + setCreateVisible(false); + await reload(); + } finally { + setCreatingRole(false); + } + }; - const openAssignPermissions = (role: Role) => { - setSelectedRole(role); - setSelectedPermissionIds(role.permissions.map((permission) => permission.id)); - setAssignmentVisible(true); - }; + const openAssignPermissions = (role: Role) => { + setSelectedRole(role); + setSelectedPermissionIds(role.permissions.map((permission) => permission.id)); + setAssignmentVisible(true); + }; - const saveRolePermissions = async () => { - if (!selectedRole) { - return; - } - setSavingPermissions(true); - try { - await api.patch(`/api/admin/v1/roles/${selectedRole.id}/permissions`, { - permissionIds: selectedPermissionIds, - }); - Message.success('角色权限已更新'); - setAssignmentVisible(false); - await reload(); - } finally { - setSavingPermissions(false); - } - }; + const saveRolePermissions = async () => { + if (!selectedRole) { + return; + } + setSavingPermissions(true); + try { + await api.patch(`/api/admin/v1/roles/${selectedRole.id}/permissions`, { + permissionIds: selectedPermissionIds, + }); + Message.success("角色权限已更新"); + setAssignmentVisible(false); + await reload(); + } finally { + setSavingPermissions(false); + } + }; - return ( - } onClick={openCreateRole}>新增角色}> -
{record.permissions.map((item) => {item.name})} }, - { title: '操作', width: 140, render: (_, record: Role) => }, - ]} /> - setCreateVisible(false)} footer={null}> -
- - - -
角色的唯一标识,创建后不可修改
- - - - - - -
权限设置
- -
为角色分配系统权限,决定拥有此角色的用户可以进行的操作
-
- - -
- -
- setAssignmentVisible(false)} - > - - - - ); + return ( + } onClick={openCreateRole}> + 新增角色 + + } + > +
( + + {record.permissions.map((item) => ( + {item.name} + ))} + + ), + }, + { + title: "操作", + width: 140, + render: (_, record: Role) => ( + + ), + }, + ]} + /> + setCreateVisible(false)} footer={null}> +
+ + + +
角色的唯一标识,创建后不可修改
+ + + + + + +
权限设置
+ +
为角色分配系统权限,决定拥有此角色的用户可以进行的操作
+
+ + +
+ +
+ setAssignmentVisible(false)} + > + + + + ); } export default RolesPage; diff --git a/src/User/index.tsx b/src/User/index.tsx index f3a0340..363c072 100644 --- a/src/User/index.tsx +++ b/src/User/index.tsx @@ -4,7 +4,7 @@ import { api } from '../api/client'; import { usePageData } from '../hooks/usePageData'; function UsersPage() { - const { data, loading, reload } = usePageData('/api/admin/v1/users'); + const { data, loading, reload, changePage } = usePageData('/api/admin/v1/users'); const update状态 = async (id: number, status: 'active' | 'disabled') => { await api.patch(`/api/admin/v1/users/${id}/status`, { status }); Message.success('用户状态已更新'); @@ -13,7 +13,7 @@ function UsersPage() { return ( -
{ api.interceptors.response.use( (response) => response, (error: AxiosError>) => { - const message = error.response?.data?.message || error.message || '请求失败'; + const message = + error.response?.data?.message || error.message || "请求失败"; Message.error(message); return Promise.reject(error); }, ); -export async function fetchPage(url: string): Promise> { - const response = await api.get>>(url); +export async function fetchPage( + url: string, + params?: { page?: number; pageSize?: number }, +): Promise> { + const response = await api.get>>(url, { params }); return response.data.data; } diff --git a/src/hooks/pageData.ts b/src/hooks/pageData.ts new file mode 100644 index 0000000..2ea44c7 --- /dev/null +++ b/src/hooks/pageData.ts @@ -0,0 +1,20 @@ +import type { PageData } from '../api/types'; + +type PageQuery = { + page: number; + pageSize: number; +}; + +export function resolvePageData( + response: PageData, + query: PageQuery, +): PageData { + return { + list: response.list, + pagination: { + ...response.pagination, + page: query.page, + pageSize: query.pageSize, + }, + }; +} diff --git a/src/hooks/usePageData.ts b/src/hooks/usePageData.ts index 284893c..1c6cf21 100644 --- a/src/hooks/usePageData.ts +++ b/src/hooks/usePageData.ts @@ -1,31 +1,51 @@ -import { useEffect, useState } from 'react'; -import { fetchPage } from '../api/client'; -import type { PageData } from '../api/types'; +import { useEffect, useState } from "react"; +import { fetchPage } from "../api/client"; +import type { PageData } from "../api/types"; +import { resolvePageData } from "./pageData"; + +type PageQuery = { + page: number; + pageSize: number; +}; export function usePageData(url: string, enabled = true) { - const [loading, setLoading] = useState(false); - const [data, setData] = useState>({ - list: [], - pagination: { page: 1, pageSize: 20, total: 0 }, - }); + const [loading, setLoading] = useState(false); + const [pageQuery, setPageQuery] = useState({ + page: 1, + pageSize: 20, + }); + const [data, setData] = useState>({ + list: [], + pagination: { page: 1, pageSize: 20, total: 0 }, + }); - const load = async () => { - if (!enabled) { - return; - } - setLoading(true); - try { - setData(await fetchPage(url)); - } finally { - setLoading(false); - } - }; + const load = async (query = pageQuery) => { + if (!enabled) { + return; + } + setLoading(true); + try { + const response = await fetchPage(url, query); + setData(resolvePageData(response, query)); + } finally { + setLoading(false); + } + }; - useEffect(() => { - if (enabled) { - void load(); - } - }, [url, enabled]); + const changePage = (page: number, pageSize: number) => { + setPageQuery({ page, pageSize }); + }; - return { data, loading, reload: load }; + useEffect(() => { + if (enabled) { + void load(pageQuery); + } + }, [url, enabled, pageQuery.page, pageQuery.pageSize]); + + return { + data, + loading, + reload: () => load(pageQuery), + changePage, + }; } diff --git a/src/layout/index.tsx b/src/layout/index.tsx index 5155edc..116831a 100644 --- a/src/layout/index.tsx +++ b/src/layout/index.tsx @@ -1,117 +1,168 @@ -import { useEffect, useMemo, useState } from 'react'; -import { - Avatar, - Button, - Dropdown, - Layout, - Menu, - Space, - Typography, -} from '@arco-design/web-react'; -import { IconMenuFold, IconMenuUnfold } from '@arco-design/web-react/icon'; -import { Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-dom'; -import type { ApiResponse, AdminProfile } from '../api/types'; -import { API_BASE_URL, TOKEN_KEY, api } from '../api/client'; -import { filterMenuItems, findCurrentMenu, menuItems, renderMenuItem } from './menu'; -import { PermissionRoute } from './PermissionRoute'; -import DashboardPage from '../Dashboard'; -import DramasPage from '../Drama'; -import DataPage from '../Data'; -import UsersPage from '../User'; -import RolesPage from '../Role'; -import PersonnelPage from '../Personnel'; -import ProfilePage from '../Profile'; -import LogsPage from '../Log'; -import './layout.css'; +import { useEffect, useMemo, useState } from "react"; +import { Avatar, Button, Dropdown, Layout, Menu, Space, Typography } from "@arco-design/web-react"; +import { IconMenuFold, IconMenuUnfold } from "@arco-design/web-react/icon"; +import { Navigate, Route, Routes, useLocation, useNavigate } from "react-router-dom"; +import type { ApiResponse, AdminProfile } from "../api/types"; +import { TOKEN_KEY, api } from "../api/client"; +import { filterMenuItems, findCurrentMenu, menuItems, renderMenuItem } from "./menu"; +import { PermissionRoute } from "./PermissionRoute"; +import DashboardPage from "../Dashboard"; +import DramasPage from "../Drama"; +import DataPage from "../Data"; +import UsersPage from "../User"; +import RolesPage from "../Role"; +import PersonnelPage from "../Personnel"; +import ProfilePage from "../Profile"; +import LogsPage from "../Log"; +import "./layout.css"; const { Header, Sider, Content } = Layout; const { Text } = Typography; export function AdminLayout() { - const navigate = useNavigate(); - const location = useLocation(); - const [collapsed, setCollapsed] = useState(false); - const [profile, setProfile] = useState(null); - const current = useMemo(() => findCurrentMenu(location.pathname), [location.pathname]); - const visibleMenuItems = useMemo(() => filterMenuItems(menuItems, profile), [profile]); - const [openMenuKeys, setOpenMenuKeys] = useState(current?.parent ? [current.parent.key] : []); + const navigate = useNavigate(); + const location = useLocation(); + const [collapsed, setCollapsed] = useState(false); + const [profile, setProfile] = useState(null); + const current = useMemo(() => findCurrentMenu(location.pathname), [location.pathname]); + const visibleMenuItems = useMemo(() => filterMenuItems(menuItems, profile), [profile]); + const [openMenuKeys, setOpenMenuKeys] = useState(current?.parent ? [current.parent.key] : []); - useEffect(() => { - api.get>('/api/admin/v1/auth/profile') - .then((response) => setProfile(response.data.data)) - .catch(() => { + useEffect(() => { + api.get>("/api/admin/v1/auth/profile") + .then((response) => setProfile(response.data.data)) + .catch(() => { + localStorage.removeItem(TOKEN_KEY); + navigate("/login", { replace: true }); + }); + }, [navigate]); + + useEffect(() => { + if (current?.parent && !openMenuKeys.includes(current.parent.key)) { + setOpenMenuKeys((keys) => [...keys, current.parent!.key]); + } + }, [current, openMenuKeys]); + + const logout = () => { localStorage.removeItem(TOKEN_KEY); - navigate('/login', { replace: true }); - }); - }, [navigate]); + navigate("/login", { replace: true }); + }; - useEffect(() => { - if (current?.parent && !openMenuKeys.includes(current.parent.key)) { - setOpenMenuKeys((keys) => [...keys, current.parent!.key]); - } - }, [current, openMenuKeys]); - - const logout = () => { - localStorage.removeItem(TOKEN_KEY); - navigate('/login', { replace: true }); - }; - - return ( - - -
- TK - {!collapsed && TK短剧管理后台} -
- navigate(key)} - onClickSubMenu={(_, openKeys) => setOpenMenuKeys(openKeys)} - > - {visibleMenuItems.map((item) => renderMenuItem(item))} - -
- -
- - - -
- - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - -
-
- ); + return ( + + +
+ TK + {!collapsed && TK短剧管理后台} +
+ navigate(key)} + onClickSubMenu={(_, openKeys) => setOpenMenuKeys(openKeys)} + > + {visibleMenuItems.map((item) => renderMenuItem(item))} + +
+ +
+ + + +
+ + + } /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + } /> + + +
+
+ ); } diff --git a/tests/pageData.test.ts b/tests/pageData.test.ts new file mode 100644 index 0000000..6405360 --- /dev/null +++ b/tests/pageData.test.ts @@ -0,0 +1,14 @@ +import assert from 'node:assert/strict'; +import { resolvePageData } from '../src/hooks/pageData.js'; + +const response = { + list: [{ id: 21 }], + pagination: { page: 1, pageSize: 20, total: 30 }, +}; + +const resolved = resolvePageData(response, { page: 2, pageSize: 20 }); + +assert.deepEqual(resolved, { + list: [{ id: 21 }], + pagination: { page: 2, pageSize: 20, total: 30 }, +});