feat: 添加短剧筛选功能和自定义Panel容器组件
1. 新增DramaQuery类型扩展分页查询参数,支持标题、状态、类型筛选 2. 重构usePageData hook,支持自定义查询类型和筛选状态管理 3. 新建Panel通用容器组件替代原生Card 4. 为短剧管理页面添加搜索筛选UI和逻辑 5. 统一所有页面的分页查询默认参数 6. 调整布局样式优化后台管理界面展示
This commit is contained in:
59
.mimocode/plans/1782986311447-swift-planet.md
Normal file
59
.mimocode/plans/1782986311447-swift-planet.md
Normal file
@@ -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<T>` 泛型扩展,支持自定义查询类型
|
||||||
|
|
||||||
|
**文件**: `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` 构建成功
|
||||||
@@ -39,9 +39,9 @@ function DashboardPage({ profile }: DashboardPageProps) {
|
|||||||
const canReadDramas = hasPermissions(profile, ['drama:read']);
|
const canReadDramas = hasPermissions(profile, ['drama:read']);
|
||||||
const canReadUsers = hasPermissions(profile, ['user:read']);
|
const canReadUsers = hasPermissions(profile, ['user:read']);
|
||||||
const canReadLogs = hasPermissions(profile, ['log:read']);
|
const canReadLogs = hasPermissions(profile, ['log:read']);
|
||||||
const dramas = usePageData<Drama>(dramaApi.listDramas, canReadDramas);
|
const dramas = usePageData<Drama>(dramaApi.listDramas, { page: 1, pageSize: 20 }, canReadDramas);
|
||||||
const users = usePageData<AppUser>(userApi.listUsers, canReadUsers);
|
const users = usePageData<AppUser>(userApi.listUsers, { page: 1, pageSize: 20 }, canReadUsers);
|
||||||
const logs = usePageData<RequestLog>(logApi.listRequestLogs, canReadLogs);
|
const logs = usePageData<RequestLog>(logApi.listRequestLogs, { page: 1, pageSize: 20 }, canReadLogs);
|
||||||
const hasAnyDataModule = canReadDramas || canReadUsers || canReadLogs;
|
const hasAnyDataModule = canReadDramas || canReadUsers || canReadLogs;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ function DataPage() {
|
|||||||
totalComments: 0,
|
totalComments: 0,
|
||||||
});
|
});
|
||||||
const [syncing, setSyncing] = useState(false);
|
const [syncing, setSyncing] = useState(false);
|
||||||
const dramas = usePageData<DramaMetric>(dataApi.listDramaMetrics);
|
const dramas = usePageData<DramaMetric>(dataApi.listDramaMetrics, { page: 1, pageSize: 20 });
|
||||||
const episodes = usePageData<EpisodeMetric>(dataApi.listEpisodeMetrics);
|
const episodes = usePageData<EpisodeMetric>(dataApi.listEpisodeMetrics, { page: 1, pageSize: 20 });
|
||||||
const jobs = usePageData<SyncJob>(dataApi.listSyncJobs);
|
const jobs = usePageData<SyncJob>(dataApi.listSyncJobs, { page: 1, pageSize: 20 });
|
||||||
|
|
||||||
const loadOverview = async () => {
|
const loadOverview = async () => {
|
||||||
setOverview(await dataApi.getDataOverview());
|
setOverview(await dataApi.getDataOverview());
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
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 { Button, Form, Grid, Input, InputNumber, InputTag, Message, Modal, Select, Space, Switch, Table, Tag } from "@arco-design/web-react";
|
||||||
import { IconPlus } from "@arco-design/web-react/icon";
|
import { IconPlus, IconSearch, IconRefresh } from "@arco-design/web-react/icon";
|
||||||
import { dramaApi } from "../../api";
|
import { dramaApi } from "../../api";
|
||||||
|
import type { DramaQuery } from "../../api/interface";
|
||||||
import { usePageData } from "../../hooks/usePageData";
|
import { usePageData } from "../../hooks/usePageData";
|
||||||
|
import { Panel } from "../../components/Panel";
|
||||||
import { toDramaPayload } from "../formPayload";
|
import { toDramaPayload } from "../formPayload";
|
||||||
import type { Drama, StatusOption } from "../interface";
|
import type { Drama, StatusOption } from "../interface";
|
||||||
|
|
||||||
@@ -23,11 +25,27 @@ const PUBLISH_STATUS_LABEL: Record<string, string> = {
|
|||||||
offline: "已下线",
|
offline: "已下线",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const INITIAL_QUERY: DramaQuery = { page: 1, pageSize: 20 };
|
||||||
|
|
||||||
export function DramaList() {
|
export function DramaList() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { data, loading, reload, changePage } = usePageData<Drama>(dramaApi.listDramas);
|
const { data, loading, reload, changePage, setFilter } = usePageData<Drama, DramaQuery>(dramaApi.listDramas, INITIAL_QUERY);
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const [creating, setCreating] = useState(false);
|
const [creating, setCreating] = useState(false);
|
||||||
|
const [filterTitle, setFilterTitle] = useState("");
|
||||||
|
const [filterStatus, setFilterStatus] = useState<string | undefined>(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<Drama>) => {
|
const createDrama = async (values: Partial<Drama>) => {
|
||||||
setCreating(true);
|
setCreating(true);
|
||||||
@@ -43,15 +61,59 @@ export function DramaList() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page-stack">
|
<div className="page-stack">
|
||||||
<Card
|
<Panel title="短剧管理">
|
||||||
className="section-card"
|
<Row gutter={16} className="mb-0">
|
||||||
title="短剧管理"
|
<Col span={6}>
|
||||||
extra={
|
<div className="text-[13px] text-muted mb-1.5">短剧标题</div>
|
||||||
<Button type="primary" icon={<IconPlus />} onClick={() => setVisible(true)}>
|
<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
|
||||||
|
>
|
||||||
|
{STATUS_OPTIONS.map((option) => (
|
||||||
|
<Select.Option key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<div className="text-[13px] text-muted mb-1.5">类型</div>
|
||||||
|
<Input
|
||||||
|
placeholder="搜索类型"
|
||||||
|
value={filterType}
|
||||||
|
onChange={setFilterType}
|
||||||
|
allowClear
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={10}>
|
||||||
|
<div className="text-[13px] mb-1.5"> </div>
|
||||||
|
<Space>
|
||||||
|
<Button type="primary" icon={<IconSearch />} onClick={handleSearch}>
|
||||||
|
查询
|
||||||
|
</Button>
|
||||||
|
<Button icon={<IconRefresh />} onClick={handleReset}>
|
||||||
|
重置
|
||||||
|
</Button>
|
||||||
|
<Button type="primary" status="success" icon={<IconPlus />} onClick={() => setVisible(true)}>
|
||||||
新增短剧
|
新增短剧
|
||||||
</Button>
|
</Button>
|
||||||
}
|
</Space>
|
||||||
>
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
|
<Panel>
|
||||||
<Table
|
<Table
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
@@ -64,11 +126,11 @@ export function DramaList() {
|
|||||||
}}
|
}}
|
||||||
columns={[
|
columns={[
|
||||||
{ title: "ID", dataIndex: "id", width: 80 },
|
{ title: "ID", dataIndex: "id", width: 80 },
|
||||||
{ title: "封面图", dataIndex: "coverUrl", width: 120, render: (value) => (value ? <img src={value} alt="封面图" style={{ width: 100, height: 100, objectFit: "cover" }} /> : null) },
|
{ title: "封面图", dataIndex: "coverUrl", width: 120, render: (value) => (value ? <img src={value} alt="封面图" className="rounded-lg object-cover" style={{ width: 80, height: 80 }} /> : null) },
|
||||||
{ title: "标题", dataIndex: "title" },
|
{ title: "标题", dataIndex: "title" },
|
||||||
{ title: "地区", dataIndex: "region", width: 120 },
|
{ title: "地区", dataIndex: "region", width: 120 },
|
||||||
{ title: "类型", dataIndex: "type", width: 140 },
|
{ 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: "状态",
|
title: "状态",
|
||||||
dataIndex: "status",
|
dataIndex: "status",
|
||||||
@@ -92,6 +154,8 @@ export function DramaList() {
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
<Modal title="创建短剧" visible={visible} onCancel={() => setVisible(false)} footer={null} style={{ width: 640 }}>
|
<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 }}>
|
||||||
<Form.Item label="标题" field="title" rules={[{ required: true }]}>
|
<Form.Item label="标题" field="title" rules={[{ required: true }]}>
|
||||||
@@ -166,7 +230,6 @@ export function DramaList() {
|
|||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { RequestLogTable } from '../components/RequestLogTable';
|
|||||||
import type { RequestLog } from './interface';
|
import type { RequestLog } from './interface';
|
||||||
|
|
||||||
function LogsPage() {
|
function LogsPage() {
|
||||||
const { data, loading } = usePageData<RequestLog>(logApi.listRequestLogs);
|
const { data, loading } = usePageData<RequestLog>(logApi.listRequestLogs, { page: 1, pageSize: 20 });
|
||||||
return <Card className="section-card" title="请求日志"><RequestLogTable data={data.list} loading={loading} /></Card>;
|
return <Card className="section-card" title="请求日志"><RequestLogTable data={data.list} loading={loading} /></Card>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import { usePageData } from '../hooks/usePageData';
|
|||||||
import type { AdminUser, CreatePersonnelFormValues, Role } from './interface';
|
import type { AdminUser, CreatePersonnelFormValues, Role } from './interface';
|
||||||
|
|
||||||
function PersonnelPage() {
|
function PersonnelPage() {
|
||||||
const { data, loading, reload, changePage } = usePageData<AdminUser>(personnelApi.listPersonnel);
|
const { data, loading, reload, changePage } = usePageData<AdminUser>(personnelApi.listPersonnel, { page: 1, pageSize: 20 });
|
||||||
const roles = usePageData<Role>(roleApi.listRoles);
|
const roles = usePageData<Role>(roleApi.listRoles, { page: 1, pageSize: 20 });
|
||||||
const [createForm] = Form.useForm();
|
const [createForm] = Form.useForm();
|
||||||
const [createVisible, setCreateVisible] = useState(false);
|
const [createVisible, setCreateVisible] = useState(false);
|
||||||
const [creating, setCreating] = useState(false);
|
const [creating, setCreating] = useState(false);
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import { PermissionTreePicker } from "./components/PermissionTreePicker";
|
|||||||
import type { CreateRoleFormValues, Permission, Role } from "./interface";
|
import type { CreateRoleFormValues, Permission, Role } from "./interface";
|
||||||
|
|
||||||
function RolesPage() {
|
function RolesPage() {
|
||||||
const { data, loading, reload, changePage } = usePageData<Role>(roleApi.listRoles);
|
const { data, loading, reload, changePage } = usePageData<Role>(roleApi.listRoles, { page: 1, pageSize: 20 });
|
||||||
const permissions = usePageData<Permission>(roleApi.listPermissions);
|
const permissions = usePageData<Permission>(roleApi.listPermissions, { page: 1, pageSize: 20 });
|
||||||
const [createForm] = Form.useForm();
|
const [createForm] = Form.useForm();
|
||||||
const [createVisible, setCreateVisible] = useState(false);
|
const [createVisible, setCreateVisible] = useState(false);
|
||||||
const [creatingRole, setCreatingRole] = useState(false);
|
const [creatingRole, setCreatingRole] = useState(false);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { usePageData } from '../hooks/usePageData';
|
|||||||
import type { AppUser } from './interface';
|
import type { AppUser } from './interface';
|
||||||
|
|
||||||
function UsersPage() {
|
function UsersPage() {
|
||||||
const { data, loading, reload, changePage } = usePageData<AppUser>(userApi.listUsers);
|
const { data, loading, reload, changePage } = usePageData<AppUser>(userApi.listUsers, { page: 1, pageSize: 20 });
|
||||||
const update状态 = async (id: number, status: 'active' | 'disabled') => {
|
const update状态 = async (id: number, status: 'active' | 'disabled') => {
|
||||||
await userApi.updateUserStatus(id, status);
|
await userApi.updateUserStatus(id, status);
|
||||||
Message.success('用户状态已更新');
|
Message.success('用户状态已更新');
|
||||||
@@ -12,7 +12,13 @@ function UsersPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="section-card" title="小程序用户管理">
|
<div className="page-stack">
|
||||||
|
<Card className="section-card">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="m-0 text-[16px] font-semibold text-ink">小程序用户管理</h3>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<Card className="section-card">
|
||||||
<Table rowKey="id" loading={loading} data={data.list} pagination={{ current: data.pagination.page, total: data.pagination.total, pageSize: data.pagination.pageSize, onChange: changePage }} columns={[
|
<Table rowKey="id" loading={loading} data={data.list} pagination={{ current: data.pagination.page, total: data.pagination.total, pageSize: data.pagination.pageSize, onChange: changePage }} columns={[
|
||||||
{ title: 'ID', dataIndex: 'id', width: 80 },
|
{ title: 'ID', dataIndex: 'id', width: 80 },
|
||||||
{ title: 'TikTok Open ID', dataIndex: 'tiktokOpenId' },
|
{ title: 'TikTok Open ID', dataIndex: 'tiktokOpenId' },
|
||||||
@@ -21,6 +27,7 @@ function UsersPage() {
|
|||||||
{ title: '操作', width: 160, render: (_, record: AppUser) => <Button size="small" onClick={() => update状态(record.id, record.status === 'active' ? 'disabled' : 'active')}>{record.status === 'active' ? '禁用' : '启用'}</Button> },
|
{ title: '操作', width: 160, render: (_, record: AppUser) => <Button size="small" onClick={() => update状态(record.id, record.status === 'active' ? 'disabled' : 'active')}>{record.status === 'active' ? '禁用' : '启用'}</Button> },
|
||||||
]} />
|
]} />
|
||||||
</Card>
|
</Card>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { fetchPage, request } from "../plugins/axios";
|
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<Drama>('/api/admin/v1/dramas', query);
|
return fetchPage<Drama>('/api/admin/v1/dramas', query);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ export type PageQuery = {
|
|||||||
pageSize: number;
|
pageSize: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DramaQuery = PageQuery & {
|
||||||
|
title?: string;
|
||||||
|
status?: string;
|
||||||
|
type?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type PageData<T> = {
|
export type PageData<T> = {
|
||||||
list: T[];
|
list: T[];
|
||||||
pagination: {
|
pagination: {
|
||||||
|
|||||||
22
src/components/Panel/index.tsx
Normal file
22
src/components/Panel/index.tsx
Normal file
@@ -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 (
|
||||||
|
<div className={`rounded-xl border border-line/60 bg-white shadow-sm ${className}`}>
|
||||||
|
{(title || extra) && (
|
||||||
|
<div className="flex items-center justify-between border-b border-line/60 px-5 py-4">
|
||||||
|
{title && <h3 className="m-0 text-[15px] font-semibold text-ink">{title}</h3>}
|
||||||
|
{extra && <div>{extra}</div>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="p-5">{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,4 +2,4 @@ import type { PageData, PageQuery } from "../api";
|
|||||||
|
|
||||||
export type { PageData, PageQuery };
|
export type { PageData, PageQuery };
|
||||||
|
|
||||||
export type PageFetcher<T> = (query: PageQuery) => Promise<PageData<T>>;
|
export type PageFetcher<T, Q extends PageQuery = PageQuery> = (query: Q) => Promise<PageData<T>>;
|
||||||
|
|||||||
@@ -1,45 +1,53 @@
|
|||||||
import { useEffect, useState } from "react";
|
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";
|
import { resolvePageData } from "./pageData";
|
||||||
|
|
||||||
export function usePageData<T>(fetcher: PageFetcher<T>, enabled = true) {
|
export function usePageData<T, Q extends PageQuery = PageQuery>(
|
||||||
|
fetcher: PageFetcher<T, Q>,
|
||||||
|
initialQuery: Q,
|
||||||
|
enabled = true,
|
||||||
|
) {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [pageQuery, setPageQuery] = useState<PageQuery>({
|
const [query, setQuery] = useState<Q>(initialQuery);
|
||||||
page: 1,
|
|
||||||
pageSize: 20,
|
|
||||||
});
|
|
||||||
const [data, setData] = useState<PageData<T>>({
|
const [data, setData] = useState<PageData<T>>({
|
||||||
list: [],
|
list: [],
|
||||||
pagination: { page: 1, pageSize: 20, total: 0 },
|
pagination: { page: 1, pageSize: 20, total: 0 },
|
||||||
});
|
});
|
||||||
|
|
||||||
const load = async (query = pageQuery) => {
|
const load = async (q = query) => {
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await fetcher(query);
|
const response = await fetcher(q);
|
||||||
setData(resolvePageData(response, query));
|
setData(resolvePageData(response, q));
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const changePage = (page: number, pageSize: number) => {
|
const changePage = (page: number, pageSize: number) => {
|
||||||
setPageQuery({ page, pageSize });
|
setQuery((prev) => ({ ...prev, page, pageSize } as Q));
|
||||||
|
};
|
||||||
|
|
||||||
|
const setFilter = (patch: Partial<Q>) => {
|
||||||
|
setQuery((prev) => ({ ...prev, ...patch, page: 1 } as Q));
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
void load(pageQuery);
|
void load(query);
|
||||||
}
|
}
|
||||||
}, [fetcher, enabled, pageQuery.page, pageQuery.pageSize]);
|
}, [fetcher, enabled, query]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data,
|
data,
|
||||||
loading,
|
loading,
|
||||||
reload: () => load(pageQuery),
|
query,
|
||||||
|
reload: () => load(query),
|
||||||
changePage,
|
changePage,
|
||||||
|
setFilter,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export function AdminLayout() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout className="min-h-screen bg-canvas">
|
<Layout className="h-screen bg-canvas">
|
||||||
<Sider className="shadow-[1px_0_0_#e5e6eb] bg-white" width={220} collapsed={collapsed} collapsible trigger={null}>
|
<Sider className="shadow-[1px_0_0_#e5e6eb] bg-white" width={220} collapsed={collapsed} collapsible trigger={null}>
|
||||||
<div className="flex h-14 items-center gap-2.5 border-b border-line/60 px-[18px]">
|
<div className="flex h-14 items-center gap-2.5 border-b border-line/60 px-[18px]">
|
||||||
<span className="grid h-8 w-8 place-items-center rounded-lg bg-brand text-sm font-bold text-white shadow-sm">TK</span>
|
<span className="grid h-8 w-8 place-items-center rounded-lg bg-brand text-sm font-bold text-white shadow-sm">TK</span>
|
||||||
@@ -64,8 +64,8 @@ export function AdminLayout() {
|
|||||||
{visibleMenuItems.map((item) => renderMenuItem(item))}
|
{visibleMenuItems.map((item) => renderMenuItem(item))}
|
||||||
</Menu>
|
</Menu>
|
||||||
</Sider>
|
</Sider>
|
||||||
<Layout>
|
<Layout className="flex flex-col overflow-hidden">
|
||||||
<Header className="flex h-14 items-center justify-between border-b border-line/60 bg-white/80 px-5 backdrop-blur-md">
|
<Header className="flex h-14 shrink-0 items-center justify-between border-b border-line/60 bg-white/80 px-5 backdrop-blur-md">
|
||||||
<Space>
|
<Space>
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
@@ -97,7 +97,7 @@ export function AdminLayout() {
|
|||||||
</button>
|
</button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</Header>
|
</Header>
|
||||||
<Content className="p-4 sm:p-6">
|
<Content className="flex-1 overflow-y-auto p-4 sm:p-6">
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/dashboard" element={<DashboardPage profile={profile} />} />
|
<Route path="/dashboard" element={<DashboardPage profile={profile} />} />
|
||||||
<Route
|
<Route
|
||||||
|
|||||||
Reference in New Issue
Block a user