提交 d238f10c authored 作者: 王鹏飞's avatar 王鹏飞

refactor(admin): migrate members list module

上级 08581ef2
import { message } from "antd";
import request from "@/utils/request";
import type { MemberQuery } from "./query";
async function post(path: string, data: Record<string, unknown>, notify = false) {
try {
const response = (await request.post(
path,
new URLSearchParams(data as Record<string, string>).toString(),
)) as {
data?: { code?: number; data?: any; message?: string };
};
if (response.data?.code === 200) {
if (notify) message.success(response.data.message);
return response.data.data;
}
message.error(response.data?.message || "数据获取失败!");
} catch {
return false;
}
return false;
}
export const getMemberList = (query: MemberQuery) => post("/members/members/getList", query);
export const getMemberInfo = (id: string | number) => post("/members/members/getInfoById", { id });
export const addMember = (data: Record<string, unknown>) =>
post("/members/members/add", data, true);
export const editMember = (data: Record<string, unknown>) =>
post("/members/members/edit", data, true);
export const deleteMember = (id: string | number) => post("/members/members/del", { id }, true);
export const getOrders = (id: string | number) => post("/members/members/getOrdersList", { id });
export const getIntegralRecords = (id: string | number) =>
post("/members/members/getIntegralRecordList", { id });
export const getCouponRecords = (id: string | number) =>
post("/members/members/getCouponList", { id });
import { Segmented, Table } from "antd";
import { useEffect, useState } from "react";
import { useLocation } from "react-router";
import PaginationCom from "@/common/Pagination";
import { getCouponRecords, getIntegralRecords, getOrders } from "../api";
const records = {
1: {
load: getOrders,
columns: [
{ title: "订单编号", dataIndex: "id" },
{ title: "书籍名称", dataIndex: "book_name" },
{ title: "总金额", dataIndex: "total_price" },
{ title: "实际付款金额", dataIndex: "final_total_price" },
{ title: "下单时间", dataIndex: "create_time" },
{ title: "支付时间", dataIndex: "pay_time" },
{
title: "支付方式",
dataIndex: "pay_type",
render: (value: number) => ({ 1: "微信", 2: "支付宝", 3: "紫荆币" })[value] || "-",
},
{
title: "结算状态",
dataIndex: "status",
render: (_: unknown, row: any) =>
({ 1: "待支付", 2: "取消支付", 3: "已支付" })[row.pay_type] || "-",
},
],
},
2: {
load: getIntegralRecords,
columns: [
{
title: "积分变更",
dataIndex: "options",
render: (value: number, row: any) => (
<div>
{value === 1 ? "加" : value === 2 ? "减" : ""}
{row.numbers}
</div>
),
},
{ title: "最终积分", dataIndex: "result" },
{ title: "备注", dataIndex: "remarks" },
{ title: "创建时间", dataIndex: "create_time" },
],
},
3: {
load: getCouponRecords,
columns: [
{ title: "优惠券名称", dataIndex: "coupon_name" },
{ title: "领取时间", dataIndex: "create_time" },
{ title: "有效期", dataIndex: "end_time" },
{ title: "使用状态", dataIndex: "use_status_name" },
],
},
} as const;
function RecordTable({ type, id }: { type: 1 | 2 | 3; id: string | number }) {
const [rows, setRows] = useState<any[]>([]);
const [total, setTotal] = useState(type === 2 ? 4 : 0);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(type === 2 ? 2 : 10);
const [loading, setLoading] = useState(true);
const init = async () => {
setLoading(true);
const result: any = await records[type].load(id);
if (result) {
setRows(result.list || []);
setTotal(result.total || 0);
}
setLoading(false);
};
useEffect(() => {
void init();
}, [id, type]);
return (
<div>
<Table
columns={records[type].columns as any}
rowKey={(row: any) => row.ID || row.id}
dataSource={rows}
loading={loading}
pagination={false}
scroll={type === 3 ? { y: 600, x: true } : undefined}
/>
<br />
<PaginationCom
total={total}
page_size={pageSize}
setpage_size={setPageSize}
page={page}
setPage={setPage}
init={init}
/>
</div>
);
}
export default function MemberDetailView() {
const location = useLocation();
const [current, setCurrent] = useState<1 | 2 | 3>(1);
const id = (location.state as { id?: string | number } | null)?.id;
return (
<div>
<Segmented
options={[
{ label: "订单记录", value: 1 },
{ label: "积分记录", value: 2 },
{ label: "优惠券记录", value: 3 },
]}
value={current}
onChange={(value) => setCurrent(value as 1 | 2 | 3)}
style={{ marginBottom: 20 }}
/>
{id ? <RecordTable type={current} id={id} /> : null}
</div>
);
}
import { Button, DatePicker, Drawer, Form, Input, Radio, Space } from "antd";
import dayjs from "dayjs";
import { useEffect } from "react";
import { addMember, editMember, getMemberInfo } from "../api";
export default function MemberDrawer({ open, id, onClose, onComplete }: any) {
const [form] = Form.useForm();
useEffect(() => {
if (!open) {
form.setFieldsValue({ name: "", phone: "", sex: "1", birthday_time: "" });
return;
}
if (id)
void getMemberInfo(id).then(
(data: any) =>
data &&
form.setFieldsValue({
...data,
birthday_time: dayjs(data.birthday_time),
sex: String(data.sex),
}),
);
}, [form, id, open]);
const submit = async (values: any) => {
const result = id
? await editMember({ ...values, id, birthday_time: dayjs(values.birthday_time).unix() })
: await addMember({ ...values, birthday_time: dayjs(values.birthday_time).unix() });
if (result) {
onClose();
onComplete();
}
};
return (
<Drawer open={open} mask={false} centered footer={null} onClose={onClose}>
<Form form={form} onFinish={submit} labelCol={{ span: 7 }}>
<Form.Item
name="name"
label="用户名称"
rules={[
{ required: true, message: "请输入用户名称" },
{ max: 30, message: "最大30字符" },
]}
>
<Input autoComplete="off" placeholder="请输入用户名称" />
</Form.Item>
<Form.Item
name="phone"
label="手机号名称"
rules={[
{ required: true, message: "请输入手机号名称" },
{
pattern: /^(?:(?:\+|00)86)?1[3-9]\d{9}$/,
message: "请输入正确格式的手机号",
trigger: "blur",
},
]}
>
<Input autoComplete="off" placeholder="请输入手机号名称" />
</Form.Item>
<Form.Item name="sex" label="性别" rules={[{ required: true, message: "请选择性别" }]}>
<Radio.Group>
<Radio value="1">男</Radio>
<Radio value="2">女</Radio>
<Radio value="3">保密</Radio>
</Radio.Group>
</Form.Item>
<Form.Item
name="birthday_time"
label="出生日期"
rules={[{ required: true, message: "请选择出生日期" }]}
>
<DatePicker />
</Form.Item>
<Form.Item wrapperCol={{ offset: 9, span: 16 }}>
<Space size={20}>
<Button onClick={onClose}>取消</Button>
<Button type="primary" htmlType="submit">
提交
</Button>
</Space>
</Form.Item>
</Form>
</Drawer>
);
}
import type { Dayjs } from "dayjs";
export interface MemberFilters {
name?: string;
phone?: string;
time?: [Dayjs | null, Dayjs | null] | null;
}
export interface MemberQuery {
page: number;
page_size: number;
name?: string;
phone?: string;
start_time?: number;
end_time?: number;
}
export function buildMemberQuery(
page: number,
pageSize: number,
filters: MemberFilters,
): MemberQuery {
const query: MemberQuery = { page, page_size: pageSize };
if (filters.name) query.name = filters.name;
if (filters.phone) query.phone = filters.phone;
if (filters.time?.[0] && filters.time[1]) {
query.start_time = filters.time[0].startOf("day").unix();
query.end_time = filters.time[1].endOf("day").unix();
}
return query;
}
import { Navigate, useRoutes } from "react-router";
import MemberDetailView from "./components/MemberDetailView";
import MemberListView from "./views/MemberListView";
export default function MemberListRoutes() {
return useRoutes([
{ index: true, Component: MemberListView },
{ path: "query", Component: MemberListView },
{ path: "detail", Component: MemberDetailView },
{ path: "*", element: <Navigate to="/member/list" /> },
]);
}
import { useCallback, useEffect, useRef, useState } from "react";
import { getMemberList } from "./api";
import { buildMemberQuery, type MemberFilters, type MemberQuery } from "./query";
export function useMemberList() {
const [filters, setFilters] = useState<MemberFilters>({});
const [applied, setApplied] = useState<MemberFilters>({});
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [rows, setRows] = useState<any[]>([]);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const last = useRef<MemberQuery>(buildMemberQuery(1, 10, {}));
const requestId = useRef(0);
const load = useCallback(async (query: MemberQuery) => {
const id = ++requestId.current;
last.current = query;
setLoading(true);
setError(null);
try {
const result = await getMemberList(query);
if (!result) throw new Error("请求失败");
if (id === requestId.current) {
setRows(result.list || []);
setTotal(result.total || 0);
}
} catch (reason) {
if (id === requestId.current)
setError(reason instanceof Error ? reason : new Error("请求失败"));
} finally {
if (id === requestId.current) setLoading(false);
}
}, []);
useEffect(() => {
void load(last.current);
}, [load]);
const run = useCallback(
(nextPage: number, nextSize: number, nextFilters: MemberFilters) =>
void load(buildMemberQuery(nextPage, nextSize, nextFilters)),
[load],
);
const search = useCallback(() => {
setApplied(filters);
setPage(1);
run(1, pageSize, filters);
}, [filters, pageSize, run]);
const reset = useCallback(() => {
setFilters({});
setApplied({});
setPage(1);
run(1, pageSize, {});
}, [pageSize, run]);
const refresh = useCallback(() => {
setApplied(filters);
run(page, pageSize, filters);
}, [filters, page, pageSize, run]);
return {
filters,
setFilters,
page,
pageSize,
rows,
total,
loading,
error,
search,
reset,
refresh,
reloadFirst: search,
retry: () => void load(last.current),
changePage: (next: number) => {
setPage(next);
run(next, pageSize, applied);
},
changePageSize: (next: number) => {
setPage(1);
setPageSize(next);
run(1, next, applied);
},
};
}
import { App, Button, DatePicker, Input, Space } from "antd";
import type { ColumnsType } from "antd/es/table";
import { useState } from "react";
import { useNavigate } from "react-router";
import { useSelector } from "react-redux";
import { AppList, type AppListFilterItem } from "@/shared/components/AppList";
import { deleteMember } from "../api";
import MemberDrawer from "../components/MemberDrawer";
import type { MemberFilters } from "../query";
import { useMemberList } from "../useMemberList";
const { RangePicker } = DatePicker;
const items: AppListFilterItem<MemberFilters>[] = [
{
name: "name",
label: "用户名称",
render: (field) => (
<Input
allowClear
placeholder="请输入用户名称"
value={field.value || ""}
onChange={(e) => field.onChange(e.target.value)}
/>
),
},
{
name: "phone",
label: "手机号",
render: (field) => (
<Input
allowClear
placeholder="请输入手机号"
value={field.value || ""}
onChange={(e) => field.onChange(e.target.value)}
/>
),
},
{
name: "time",
label: "创建时间",
render: (field) => (
<RangePicker value={field.value} onChange={(value) => field.onChange(value)} />
),
},
];
export default function MemberListView() {
const list = useMemberList();
const navigate = useNavigate();
const { modal } = App.useApp();
const { operationPermissionsList = [] } = useSelector((state: any) => state.user);
const [id, setId] = useState<any>("");
const [open, setOpen] = useState(false);
const columns: ColumnsType<any> = [
"ID:id",
"用户名:name",
"用户手机号:phone",
"创建时间:create_time",
"最后登录时间:last_login_time",
"最后登录IP:last_ip",
].map((item) => {
const [title, dataIndex] = item.split(":");
return { title, key: dataIndex, dataIndex, align: "center" };
});
columns.push({
title: "操作",
key: "handle",
render: (_, row) => (
<Space>
{operationPermissionsList.includes("/member/list/getInfoById") && (
<Button
onClick={() => {
setId(row.id);
setOpen(true);
}}
>
编辑
</Button>
)}
{operationPermissionsList.includes("/member/list/del") && (
<Button
onClick={() =>
modal.confirm({
title: "提示",
content: "用户将永久删除,是否继续?",
onOk: async () => {
if (await deleteMember(row.id)) list.reloadFirst();
},
})
}
>
删除
</Button>
)}
{operationPermissionsList.includes("/member/list/detail") && (
<Button onClick={() => navigate("/member/list/detail", { state: { id: row.id } })}>
详情
</Button>
)}
</Space>
),
});
return (
<div className="classify">
<AppList
rowKey="id"
columns={columns}
data={{ rows: list.rows, total: list.total, loading: list.loading, error: list.error }}
filters={{
value: list.filters,
items,
onChange: list.setFilters,
onSearch: list.search,
onReset: list.reset,
onRefresh: list.refresh,
}}
pagination={{
page: list.page,
pageSize: list.pageSize,
pageSizeOptions: [10, 20, 30, 40],
onChange: list.changePage,
onPageSizeChange: list.changePageSize,
}}
toolbar={
operationPermissionsList.includes("/member/list/add") ? (
<Button
type="primary"
ghost
onClick={() => {
setId("");
setOpen(true);
}}
>
添加
</Button>
) : null
}
onRetry={list.retry}
/>
<MemberDrawer
open={open}
id={id}
onClose={() => setOpen(false)}
onComplete={list.reloadFirst}
/>
</div>
);
}
...@@ -3,9 +3,7 @@ import { Navigate, useRoutes } from "react-router"; ...@@ -3,9 +3,7 @@ import { Navigate, useRoutes } from "react-router";
import ModuleBoundary from "@/app/ModuleBoundary"; import ModuleBoundary from "@/app/ModuleBoundary";
const MembersLayout = lazy(() => import("@/pages/member/index")); const MembersLayout = lazy(() => import("@/pages/member/index"));
const MemberListLayout = lazy(() => import("@/pages/member/list/index")); const MemberListRoutes = lazy(() => import("./list/routes"));
const MemberList = lazy(() => import("@/pages/member/list/list"));
const MemberDetail = lazy(() => import("@/pages/member/detail/index"));
const UserLevel = lazy(() => import("@/pages/member/userlevel/index")); const UserLevel = lazy(() => import("@/pages/member/userlevel/index"));
export default function MembersRoutes() { export default function MembersRoutes() {
...@@ -14,15 +12,7 @@ export default function MembersRoutes() { ...@@ -14,15 +12,7 @@ export default function MembersRoutes() {
Component: MembersLayout, Component: MembersLayout,
children: [ children: [
{ index: true, element: <Navigate to="/member/list" /> }, { index: true, element: <Navigate to="/member/list" /> },
{ { path: "list/*", Component: MemberListRoutes },
path: "list",
Component: MemberListLayout,
children: [
{ index: true, Component: MemberList },
{ path: "query", Component: MemberList },
{ path: "detail", Component: MemberDetail },
],
},
{ path: "userlevel", Component: UserLevel }, { path: "userlevel", Component: UserLevel },
], ],
}, },
......
Markdown 格式
0% 或
您添加了 0 人 到此讨论。请谨慎行事。
请先完成此评论的编辑!
请 注册 或者 后发表评论