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

refactor(admin): migrate coupon to settings module

上级 48bd74bd
import { message } from "antd";
import axios from "@/utils/axios";
import legacyRequest from "@/utils/request";
import type { CouponQuery } from "./query";
export interface CouponRow {
id: string | number;
name?: string;
norm_price?: string | number;
reduced_price?: string | number;
start_time?: string;
end_time?: string;
receive_num?: string | number;
use_num?: string | number;
}
export interface CouponListResult {
rows: CouponRow[];
total: number;
}
interface CouponListEnvelope {
data?: {
list?: CouponRow[];
total?: number;
};
}
export type CouponListRequest = (params: CouponQuery) => Promise<CouponListResult>;
export async function getCouponList(params: CouponQuery): Promise<CouponListResult> {
const response = (await axios.post(
"/api/system/coupon/getList",
params,
)) as unknown as CouponListEnvelope;
const { list = [], total = 0 } = response.data ?? {};
return { rows: list, total };
}
export function addCoupon(data: object) {
return axios.post("/api/system/coupon/add", data);
}
export function editCoupon(data: object) {
return axios.post("/api/system/coupon/edit", data);
}
export function deleteCoupon(data: object) {
return axios.post("/api/system/coupon/del", data);
}
export function getCouponDetail(data: object) {
return axios.post("/api/system/coupon/getInfoById", data);
}
export function addCouponRecord(data: object) {
return axios.post("/api/system/coupon/record/add", data);
}
async function legacyPost(path: string, data: object) {
try {
const response = (await legacyRequest.post(
path,
new URLSearchParams(data as Record<string, string>).toString(),
)) as { data?: { code?: number; data?: unknown; message?: string } };
const code = response.data?.code ?? 0;
const payload = response.data?.data ?? [];
const responseMessage = response.data?.message ?? "数据获取失败!";
if (code === 200) {
return { payload, message: responseMessage };
}
message.error(responseMessage);
return false;
} catch {
return false;
}
}
export async function getCouponConfig() {
const result = await legacyPost("/system/config/getCoupon", {});
return result ? (result.payload as { coupon_switch?: string }) : false;
}
export async function setCouponConfig(data: { coupon_switch: "0" | "1" }) {
const result = await legacyPost("/system/config/couponEdit", data);
if (!result) {
return false;
}
message.success(result.message);
return true;
}
import { Button, DatePicker, Drawer, Form, Input, Space, message } from "antd";
import dayjs from "dayjs";
import { useEffect } from "react";
import { addCoupon, editCoupon, getCouponDetail } from "../api";
const { RangePicker } = DatePicker;
interface FormDrawerProps {
open: boolean;
id: string | number | "";
onClose: () => void;
onComplete: () => void;
}
export default function FormDrawer({
id,
onClose: closeDrawer,
onComplete,
...rest
}: FormDrawerProps) {
const [form] = Form.useForm();
useEffect(() => {
async function fetchInfo() {
if (!id) return;
const response = (await getCouponDetail({ id })) as {
data: { start_time: string; end_time: string };
};
const { data } = response;
const time = [dayjs(data.start_time), dayjs(data.end_time)];
form.setFieldsValue({ ...data, time });
}
void fetchInfo();
}, [form, id]);
const onClose = () => {
form.resetFields();
closeDrawer();
};
const onFinish = async (values: Record<string, unknown>) => {
const { time, ...params } = values;
if (Array.isArray(time)) {
params.start_time = new Date(time[0] as Date).getTime() / 1000;
params.end_time = new Date(time[1] as Date).getTime() / 1000;
}
const response = (id
? await editCoupon({ ...params, id })
: await addCoupon(params)) as unknown as {
message: string;
};
message.success(response.message);
onClose();
onComplete();
};
return (
<Drawer placement="right" title={id ? "编辑优惠券" : "添加优惠券"} {...rest} onClose={onClose}>
<Form form={form} onFinish={onFinish} labelCol={{ span: 6 }} autoComplete="off">
<Form.Item
name="name"
label="优惠券名称"
rules={[
{ required: true, message: "请输入优惠券名称" },
{ max: 30, message: "最大30字符" },
]}
>
<Input placeholder="请输入优惠券名称" />
</Form.Item>
<Form.Item
name="norm_price"
label="达标金额"
rules={[
{ required: true, message: "请输入达标金额" },
{
validator: (_, value) =>
!value || /^[1-9]\d*$/.test(value)
? Promise.resolve()
: Promise.reject(new Error("必须是正整数")),
},
]}
>
<Input placeholder="请输入达标金额" />
</Form.Item>
<Form.Item
name="reduced_price"
label="满减金额"
rules={[
{ required: true, message: "请输入满减金额" },
{
validator: (_, value) => {
if (!value || /^[1-9]\d*$/.test(value)) {
return Promise.resolve();
}
if (!(Number(value) <= Number(form.getFieldValue("norm_price")))) {
return Promise.reject("小于等于达标金额");
}
return undefined;
},
},
]}
>
<Input placeholder="请输入满减金额" />
</Form.Item>
<Form.Item name="time" label="有效期" rules={[{ required: true, message: "请选择有效期" }]}>
<RangePicker />
</Form.Item>
<Form.Item style={{ textAlign: "center" }}>
<Space size={20}>
<Button type="default" onClick={onClose}>
取消
</Button>
<Button type="primary" htmlType="submit">
提交
</Button>
</Space>
</Form.Item>
</Form>
</Drawer>
);
}
import { App, Button, Drawer, Form, Input, Select, Space } from "antd";
import { addCouponRecord, type CouponRow } from "../api";
const { TextArea } = Input;
interface SendCouponDrawerProps {
open: boolean;
couponList: CouponRow[];
onClose: () => void;
}
export default function SendCouponDrawer({
couponList,
onClose: closeDrawer,
...rest
}: SendCouponDrawerProps) {
const { message } = App.useApp();
const [form] = Form.useForm();
const onClose = () => {
form.resetFields();
closeDrawer();
};
const onFinish = async (values: Record<string, unknown>) => {
const response = (await addCouponRecord(values)) as unknown as {
data: { err: { msg?: string; title?: string } };
message: string;
};
if (response.data.err.msg) {
message.error(response.data.err.title);
} else {
message.success(response.message);
onClose();
}
};
return (
<Drawer title="优惠券发放" placement="right" {...rest} onClose={onClose}>
<Form form={form} onFinish={onFinish} labelCol={{ span: 6 }} autoComplete="off">
<Form.Item
name="phone"
label="用户手机号"
rules={[{ required: true, message: "请输入用户手机号" }]}
>
<TextArea
placeholder="请输入用户手机号,用英文逗号隔开,每次最多100个"
autoSize={{ minRows: 5, maxRows: 8 }}
/>
</Form.Item>
<Form.Item
name="id"
label="发放优惠券"
rules={[{ required: true, message: "请选择优惠券" }]}
>
<Select placeholder="请选择优惠券">
{couponList.map((item) => (
<Select.Option value={item.id} key={item.id}>
{item.name}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item wrapperCol={{ offset: 5, span: 16 }} style={{ textAlign: "center" }}>
<Space size={20}>
<Button onClick={onClose}>取消</Button>
<Button type="primary" htmlType="submit">
提交
</Button>
</Space>
</Form.Item>
</Form>
</Drawer>
);
}
export interface CouponFilters {
name?: string;
}
export interface CouponQuery {
page: number;
page_size: number;
name?: string;
}
interface BuildCouponQueryInput {
page: number;
pageSize: number;
filters: CouponFilters;
}
export function buildCouponQuery({ page, pageSize, filters }: BuildCouponQueryInput): CouponQuery {
const query: CouponQuery = {
page,
page_size: pageSize,
};
if (filters.name !== undefined) {
query.name = filters.name;
}
return query;
}
import { Navigate, useRoutes } from "react-router";
import CouponListView from "./views/CouponListView";
export default function CouponRoutes() {
return useRoutes([
{ index: true, Component: CouponListView },
{ path: "*", element: <Navigate to="/setting/coupon" /> },
]);
}
import { useCallback, useEffect, useRef, useState } from "react";
import { getCouponList, type CouponListRequest, type CouponListResult } from "./api";
import { buildCouponQuery, type CouponFilters, type CouponQuery } from "./query";
const INITIAL_PAGE = 1;
const INITIAL_PAGE_SIZE = 10;
function toError(reason: unknown) {
return reason instanceof Error ? reason : new Error("请求失败");
}
export function useCouponList({ request = getCouponList }: { request?: CouponListRequest } = {}) {
const [filters, setFilterState] = useState<CouponFilters>({});
const [appliedFilters, setAppliedFilters] = useState<CouponFilters>({});
const [page, setPage] = useState(INITIAL_PAGE);
const [pageSize, setPageSize] = useState(INITIAL_PAGE_SIZE);
const [rows, setRows] = useState<CouponListResult["rows"]>([]);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const requestRef = useRef(request);
const requestIdRef = useRef(0);
const lastQueryRef = useRef<CouponQuery>(
buildCouponQuery({ page: INITIAL_PAGE, pageSize: INITIAL_PAGE_SIZE, filters: {} }),
);
useEffect(() => {
requestRef.current = request;
}, [request]);
const load = useCallback(async (query: CouponQuery) => {
const requestId = ++requestIdRef.current;
lastQueryRef.current = query;
setLoading(true);
setError(null);
try {
const result = await requestRef.current(query);
if (requestId === requestIdRef.current) {
setRows(result.rows);
setTotal(result.total);
}
} catch (reason) {
if (requestId === requestIdRef.current) {
setError(toError(reason));
}
} finally {
if (requestId === requestIdRef.current) {
setLoading(false);
}
}
}, []);
useEffect(() => {
void load(lastQueryRef.current);
return () => {
requestIdRef.current += 1;
};
}, [load]);
const loadFor = useCallback(
(nextPage: number, nextPageSize: number, nextFilters: CouponFilters) =>
load(buildCouponQuery({ page: nextPage, pageSize: nextPageSize, filters: nextFilters })),
[load],
);
const setFilters = useCallback((nextFilters: CouponFilters) => {
setFilterState(nextFilters);
}, []);
const reloadFirstPage = useCallback(() => {
setAppliedFilters(filters);
setPage(INITIAL_PAGE);
void loadFor(INITIAL_PAGE, pageSize, filters);
}, [filters, loadFor, pageSize]);
const reset = useCallback(() => {
const emptyFilters: CouponFilters = {};
setFilterState(emptyFilters);
setAppliedFilters(emptyFilters);
setPage(INITIAL_PAGE);
void loadFor(INITIAL_PAGE, pageSize, emptyFilters);
}, [loadFor, pageSize]);
const refresh = useCallback(() => {
setAppliedFilters(filters);
void loadFor(page, pageSize, filters);
}, [filters, loadFor, page, pageSize]);
const changePage = useCallback(
(nextPage: number) => {
setPage(nextPage);
void loadFor(nextPage, pageSize, appliedFilters);
},
[appliedFilters, loadFor, pageSize],
);
const changePageSize = useCallback(
(nextPageSize: number) => {
setPage(INITIAL_PAGE);
setPageSize(nextPageSize);
void loadFor(INITIAL_PAGE, nextPageSize, appliedFilters);
},
[appliedFilters, loadFor],
);
const retry = useCallback(() => {
void load(lastQueryRef.current);
}, [load]);
return {
filters,
setFilters,
page,
pageSize,
rows,
total,
loading,
error,
search: reloadFirstPage,
reset,
refresh,
reloadFirstPage,
changePage,
changePageSize,
retry,
};
}
import { App, Button, Image, Input, Popover, Space, Switch } from "antd";
import type { ColumnsType } from "antd/es/table";
import { useCallback, useEffect, useState } from "react";
import { useSelector } from "react-redux";
import Add from "@/assets/images/icon/add.png";
import coupon from "@/assets/images/icon/coupon.png";
import coupon2 from "@/assets/images/icon/coupon2.png";
import { AppList, type AppListFilterItem } from "@/shared/components/AppList";
import { deleteCoupon, getCouponConfig, setCouponConfig, type CouponRow } from "../api";
import FormDrawer from "../components/FormDrawer";
import SendCouponDrawer from "../components/SendCouponDrawer";
import type { CouponFilters } from "../query";
import { useCouponList } from "../useCouponList";
const filterItems: AppListFilterItem<CouponFilters>[] = [
{
label: "优惠券名称",
name: "name",
render: (field) => (
<Input
placeholder="请输入优惠券名称"
allowClear
value={field.value ?? ""}
onChange={(event) => field.onChange(event.target.value)}
/>
),
},
];
export default function CouponListView() {
const { modal } = App.useApp();
const { operationPermissionsList = [] } = useSelector(
(state: { user: { operationPermissionsList?: string[] } }) => state.user,
);
const list = useCouponList();
const [drawerOpen, setDrawerOpen] = useState(false);
const [id, setId] = useState<string | number | "">("");
const [switchFlag, setSwitchFlag] = useState(false);
const [switchLoad, setSwitchLoad] = useState(false);
const [sendCouponDrawerOpen, setSendCouponDrawerOpen] = useState(false);
const loadCouponConfig = useCallback(async () => {
const result = await getCouponConfig();
if (result) {
setSwitchFlag(result.coupon_switch === "1");
}
}, []);
useEffect(() => {
void loadCouponConfig();
}, [loadCouponConfig]);
const handleAdd = () => {
setId("");
setDrawerOpen(true);
};
const handleEdit = (couponId: string | number) => {
setId(couponId);
setDrawerOpen(true);
};
const handleDelete = (couponId: string | number) => {
modal.confirm({
title: "提示",
content: "优惠券将永久删除,是否继续?",
onOk: async () => {
await deleteCoupon({ id: couponId });
list.reloadFirstPage();
},
});
};
const changeCoupon = async (value: boolean) => {
setSwitchLoad(true);
await setCouponConfig({ coupon_switch: value ? "1" : "0" });
window.setTimeout(() => {
setSwitchLoad(false);
}, 500);
await loadCouponConfig();
};
const columns: ColumnsType<CouponRow> = [
{ title: "ID", key: "id", align: "center", dataIndex: "id" },
{
title: "优惠券名称",
key: "name",
align: "center",
dataIndex: "name",
width: 200,
render: (text: string) => (
<Popover content={text} overlayStyle={{ maxWidth: "400px" }}>
<div
style={{
display: "-webkit-box",
WebkitLineClamp: "2",
WebkitBoxOrient: "vertical",
overflow: "hidden",
textOverflow: "ellipsis",
textAlign: "center",
}}
>
{text}
</div>
</Popover>
),
},
{ title: "达标金额", key: "norm_price", align: "center", dataIndex: "norm_price" },
{ title: "满减金额", key: "reduced_price", align: "center", dataIndex: "reduced_price" },
{ title: "有效期开始时间", key: "start_time", align: "center", dataIndex: "start_time" },
{ title: "有效期结束时间", key: "end_time", align: "center", dataIndex: "end_time" },
{ title: "领取人数", key: "receive_num", align: "center", dataIndex: "receive_num" },
{ title: "使用人数", key: "use_num", align: "center", dataIndex: "use_num" },
{
title: "操作",
key: "handle",
align: "center",
render: (_, row) => (
<Space>
{operationPermissionsList.includes("/setting/coupon/getInfoById") && (
<Button onClick={() => handleEdit(row.id)}>编辑</Button>
)}
{operationPermissionsList.includes("/setting/coupon/del") && (
<Button onClick={() => handleDelete(row.id)}>删除</Button>
)}
</Space>
),
},
];
const toolbar = (
<Space>
<label htmlFor="coupon">优惠券开关</label>
<Switch
checked={switchFlag}
loading={switchLoad}
onChange={changeCoupon}
disabled={!operationPermissionsList.includes("/setting/coupon/getCoupon")}
/>
{operationPermissionsList.includes("/setting/coupon/add") && (
<Button
type="primary"
disabled={!switchFlag}
ghost
icon={
<img
style={{
background: String(!switchFlag),
display: "inline-block",
width: "13px",
height: "11px",
pointerEvents: "none",
}}
src={!switchFlag ? coupon2 : coupon}
/>
}
onClick={() => setSendCouponDrawerOpen(true)}
>
优惠券发放
</Button>
)}
{operationPermissionsList.includes("/setting/coupon/give") && (
<Button
type="primary"
ghost
icon={
<span
style={{
display: "inline-block",
width: "13px",
height: "12px",
pointerEvents: "none",
}}
>
<Image src={Add} />
</span>
}
onClick={handleAdd}
>
添加
</Button>
)}
</Space>
);
return (
<div className="classify">
<AppList<CouponRow, CouponFilters>
rowKey="id"
columns={columns}
data={{
rows: list.rows,
total: list.total,
loading: list.loading,
error: list.error,
}}
filters={{
value: list.filters,
items: filterItems,
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={toolbar}
onRetry={list.retry}
/>
<FormDrawer
open={drawerOpen}
id={id}
onClose={() => setDrawerOpen(false)}
onComplete={list.reloadFirstPage}
/>
<SendCouponDrawer
open={sendCouponDrawerOpen}
couponList={list.rows}
onClose={() => setSendCouponDrawerOpen(false)}
/>
</div>
);
}
......@@ -7,12 +7,10 @@ const HelpLayout = lazy(() => import("@/pages/setting/help/index"));
const HelpList = lazy(() => import("@/pages/setting/help/table"));
const HelpAddEdit = lazy(() => import("@/pages/setting/help/addedit/index"));
const Applied = lazy(() => import("@/pages/setting/applied/index"));
const PersonalizedSetting = lazy(
() => import("@/pages/setting/personalized-setting/index"),
);
const PersonalizedSetting = lazy(() => import("@/pages/setting/personalized-setting/index"));
const PointManagement = lazy(() => import("@/pages/setting/point-management/index"));
const AppVersion = lazy(() => import("@/pages/setting/app-version/index"));
const Coupon = lazy(() => import("@/pages/setting/coupon/index"));
const CouponRoutes = lazy(() => import("./coupon/routes"));
const Sensitive = lazy(() => import("@/pages/setting/sensitive/index"));
const FeedbackRoutes = lazy(() => import("./feedback/routes"));
......@@ -35,7 +33,7 @@ export default function SettingsRoutes() {
{ path: "personalized-setting", Component: PersonalizedSetting },
{ path: "point-management", Component: PointManagement },
{ path: "app-version", Component: AppVersion },
{ path: "coupon", Component: Coupon },
{ path: "coupon/*", Component: CouponRoutes },
{ path: "sensitive", Component: Sensitive },
{ path: "feedback/*", Component: FeedbackRoutes },
],
......
Markdown 格式
0% 或
您添加了 0 人 到此讨论。请谨慎行事。
请先完成此评论的编辑!
请 注册 或者 后发表评论