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

refactor(admin): migrate point management settings

上级 cdfadb54
# Settings Point Management Migration Report
## Status
- Migrated the active `/setting/point-management` route to `src/modules/settings/point-management`.
- Preserved the page as a dedicated settings form with a private recharge drawer; no `AppList`, query, or list Hook was introduced.
- Kept the legacy page and request module because they still have references outside this task's allowed scope.
## Preserved requests and feedback
- `POST /system/config/getIntegral`: loads the integral settings with an empty form-encoded body; non-200 responses show the server message or `数据获取失败!`.
- `POST /system/config/integralSet`: sends string `integral_switch` values `"1"`/`"0"`; success remains silent and reloads settings.
- `POST /system/config/integralEdit`: saves the three settings fields; success shows the server message and reloads settings.
- `POST /members/integral/add`: retains the legacy result handling:
- code `3000` warns `优惠券已过期` and closes the drawer;
- successful phone groups show `<手机号结果>充值成功` for 10 seconds;
- failed phone groups show `<手机号结果>充值失败` for 10 seconds;
- the special blank-phone failure text remains suppressed when a non-zero success group exists.
- Transport failures remain silent and do not close or refresh the current form.
## Preserved form behavior
- Integral switch remains controlled from `integral_switch`; disabling it closes the recharge drawer.
- The recharge button remains hidden without permission and disabled/invisible while the switch is off.
- Settings fields, dimensions, validation expressions, validation copy, reset-to-empty action, and responsive label layout are retained:
- `reward_points_ratio`
- `one_point_deduction_amount`
- `integral_use_limit`
- The recharge drawer retains `phone` and `num`, the comma-separated-phone copy, 100-phone note, positive-integer validation, and cancel/submit actions.
- Recharge form state remains mounted when the drawer closes, matching the legacy Drawer behavior.
## Permissions
- `/setting/point-management/integralSet`: enable the integral switch.
- `/setting/point-management/add`: show the recharge action.
- `/setting/point-management/integralEdit`: show reset and save actions.
## Structure
- `api.ts`: form-encoded settings/switch/recharge requests and notifications.
- `routes.tsx`: private route boundary preserving `/setting/point-management`.
- `views/PointManagementView.tsx`: loading, switch, save, and recharge coordination.
- `components/PointSettingsForm.tsx`: private integral settings form.
- `components/RechargeDrawer.tsx`: private recharge form drawer.
## Verification
- `pnpm exec oxlint src/modules/settings/point-management src/modules/settings/routes.tsx --deny-warnings`: passed.
- `pnpm exec oxfmt --check src/modules/settings/point-management src/modules/settings/routes.tsx`: passed.
- `pnpm test`: passed, 11 tests.
- `pnpm build`: passed after the unrelated concurrent syntax error was resolved; only the existing formula-editor and large-chunk warnings were emitted.
- `pnpm exec tsc --noEmit`: no diagnostics under `src/modules/settings/point-management` or `src/modules/settings/routes.tsx`; the workspace still has unrelated existing diagnostics.
## Concerns
- Per task direction, no API/query/Hook feature tests were added; this page does not require query or list Hook files.
- `src/routes/routes.jsx` and `src/config/component.js` still reference the legacy point-management page. In addition, the retained legacy personalized-setting page imports `point-management/request.js`. These files are outside this task's allowed scope, so `src/pages/setting/point-management/index.jsx` and `request.js` were intentionally retained.
import { message } from "antd";
// @ts-expect-error The legacy workspace does not install the qs declaration package.
import qs from "qs";
import request from "@/utils/request.js";
export interface PointSettings {
integral_switch?: string | number;
reward_points_ratio?: string | number;
one_point_deduction_amount?: string | number;
integral_use_limit?: string | number;
}
export interface PointSettingsFormValues {
reward_points_ratio: string | number;
one_point_deduction_amount: string | number;
integral_use_limit: string | number;
}
export interface RechargeFormValues {
phone: string;
num: string;
}
interface RechargeMessageGroup {
title?: string;
msg?: string;
}
interface RechargeResult {
suc?: RechargeMessageGroup;
err?: RechargeMessageGroup;
}
type ApiBody<T> = {
code?: number;
data?: T;
message?: string;
};
export async function getPointSettings(): Promise<PointSettings | false> {
try {
const response = await request.post("/system/config/getIntegral", qs.stringify(undefined));
const body = response.data as ApiBody<PointSettings>;
const text = body?.message ?? "数据获取失败!";
if (body?.code === 200) return body.data ?? {};
message.error(text);
return false;
} catch {
return false;
}
}
export async function setPointSwitch(integralSwitch: "0" | "1") {
try {
const response = await request.post(
"/system/config/integralSet",
qs.stringify({ integral_switch: integralSwitch }),
);
const body = response.data as ApiBody<unknown>;
if (body?.code === 200) return true;
message.error(body?.message ?? "数据获取失败!");
return false;
} catch {
return false;
}
}
export async function savePointSettings(values: PointSettingsFormValues) {
try {
const response = await request.post("/system/config/integralEdit", qs.stringify(values));
const body = response.data as ApiBody<unknown>;
const text = body?.message ?? "数据获取失败!";
if (body?.code === 200) {
message.success(text);
return true;
}
message.error(text);
return false;
} catch {
return false;
}
}
export async function rechargePoints(values: RechargeFormValues) {
try {
const response = await request.post("/members/integral/add", qs.stringify(values));
const body = response.data as ApiBody<RechargeResult>;
if (body?.code === 3000) {
message.warning("优惠券已过期");
return true;
}
const success = body?.data?.suc ?? {};
const failure = body?.data?.err ?? {};
if (failure.msg === " 手机号:" && success.title && !success.title.includes("充值成功(0)")) {
if (success.msg !== "") message.success(`${success.msg}充值成功`, 10);
return true;
}
if (success.title && !success.title.includes("充值成功(0)") && success.msg !== "") {
message.success(`${success.msg}充值成功`, 10);
}
if (failure.title && !failure.title.includes("充值失败(0)") && failure.msg !== "") {
message.error(`${failure.msg}充值失败`, 10);
}
return true;
} catch {
return false;
}
}
import { Button, Form, Input, Space, Spin, Switch } from "antd";
import { useEffect } from "react";
import type { PointSettings, PointSettingsFormValues } from "../api";
interface PointSettingsFormProps {
loading: boolean;
values: PointSettings;
integralEnabled: boolean;
canToggle: boolean;
canRecharge: boolean;
canEdit: boolean;
onToggle: (enabled: boolean) => void | Promise<void>;
onOpenRecharge: () => void;
onSubmit: (values: PointSettingsFormValues) => void | Promise<void>;
}
const EMPTY_VALUES: PointSettingsFormValues = {
reward_points_ratio: "",
one_point_deduction_amount: "",
integral_use_limit: "",
};
const responsiveFormItemLayout = {
labelCol: { xxl: 2, xl: 3, lg: 4, md: 5, sm: 6, xs: 8 },
wrapperCol: { xs: 24, sm: 18 },
};
export default function PointSettingsForm({
loading,
values,
integralEnabled,
canToggle,
canRecharge,
canEdit,
onToggle,
onOpenRecharge,
onSubmit,
}: PointSettingsFormProps) {
const [form] = Form.useForm<PointSettingsFormValues>();
useEffect(() => {
form.setFieldsValue(values);
}, [form, values]);
return (
<Spin spinning={loading}>
<Form
form={form}
{...responsiveFormItemLayout}
initialValues={EMPTY_VALUES}
onFinish={onSubmit}
>
<Form.Item label="积分开关">
<div>
<Switch
checked={integralEnabled}
onChange={(checked) => void onToggle(checked)}
disabled={!canToggle}
/>
{canRecharge ? (
<Button
disabled={!integralEnabled}
style={{ marginLeft: 30, opacity: integralEnabled ? "1" : "0" }}
onClick={onOpenRecharge}
type="primary"
ghost
>
积分充值
</Button>
) : null}
</div>
</Form.Item>
<Form.Item
label="赠送积分比例(%)"
name="reward_points_ratio"
rules={[
{ required: true, message: "请输入赠送积分比例" },
{
validator: (_, value) =>
/^(?:100|[1-9]?[0-9])$/.test(value) || !value
? Promise.resolve()
: Promise.reject("请输入区间0-100的正整数"),
},
]}
>
<Input autoComplete="off" style={{ width: 340 }} placeholder="请输入赠送积分比例" />
</Form.Item>
<Form.Item
label="1积分抵扣金额(元)"
name="one_point_deduction_amount"
rules={[
{ required: true, message: "请输入1积分抵扣金额" },
{
validator: (_, value) =>
/^(?:100(?:\.0{1,2})?|\d{1,2}(?:\.\d{1,2})?)$/.test(value) || !value
? Promise.resolve()
: Promise.reject("请输入区间0-100的数字,可以是小数,支持俩位小数"),
},
]}
>
<Input autoComplete="off" style={{ width: 340 }} placeholder="请输入1积分抵扣金额" />
</Form.Item>
<Form.Item
label="积分使用上限(%)"
name="integral_use_limit"
rules={[
{ required: true, message: "请输入积分使用上限" },
{
validator: (_, value) =>
/^(?:100|[1-9]?[0-9])$/.test(value) || !value
? Promise.resolve()
: Promise.reject("请输入区间0-100的正整数"),
},
]}
>
<Input autoComplete="off" style={{ width: 340 }} placeholder="请输入积分使用上限" />
</Form.Item>
<Form.Item wrapperCol={{ offset: 3, span: 16 }}>
{canEdit ? (
<Space size={20}>
<Button onClick={() => form.setFieldsValue(EMPTY_VALUES)}>重置</Button>
<Button type="primary" htmlType="submit">
确定
</Button>
</Space>
) : null}
</Form.Item>
</Form>
</Spin>
);
}
import { Button, Drawer, Form, Input, Space } from "antd";
import type { RechargeFormValues } from "../api";
const { TextArea } = Input;
interface RechargeDrawerProps {
open: boolean;
onClose: () => void;
onSubmit: (values: RechargeFormValues) => void | Promise<void>;
}
export default function RechargeDrawer({ open, onClose, onSubmit }: RechargeDrawerProps) {
const [form] = Form.useForm<RechargeFormValues>();
return (
<Drawer placement="right" onClose={onClose} open={open} mask={false}>
<Form form={form} onFinish={onSubmit} labelCol={{ span: 6 }}>
<Form.Item
name="phone"
label="用户手机号"
rules={[{ required: true, message: "请输入用户手机号" }]}
>
<TextArea
placeholder="请输入用户手机号,用逗号隔开,每次最多100个"
autoSize={{ minRows: 5, maxRows: 8 }}
/>
</Form.Item>
<Form.Item
name="num"
label="充值积分"
rules={[
{ required: true, message: "请输入充值积分数量" },
{ pattern: /^[1-9]\d*$/, message: "请输入正整数" },
]}
>
<Input autoComplete="off" placeholder="请输入充值积分数量" />
</Form.Item>
<Form.Item wrapperCol={{ offset: 6, span: 16 }} style={{ textAlign: "center" }}>
<Space size={20}>
<Button onClick={onClose}>取消</Button>
<Button type="primary" htmlType="submit">
提交
</Button>
</Space>
</Form.Item>
</Form>
</Drawer>
);
}
import { lazy } from "react";
import { Navigate, useRoutes } from "react-router";
const PointManagementView = lazy(() => import("./views/PointManagementView"));
export default function PointManagementRoutes() {
return useRoutes([
{ index: true, Component: PointManagementView },
{ path: "*", element: <Navigate to="/setting/point-management" /> },
]);
}
import { useCallback, useEffect, useState } from "react";
import { useSelector } from "react-redux";
import {
getPointSettings,
rechargePoints,
savePointSettings,
setPointSwitch,
type PointSettings,
type PointSettingsFormValues,
type RechargeFormValues,
} from "../api";
import PointSettingsForm from "../components/PointSettingsForm";
import RechargeDrawer from "../components/RechargeDrawer";
interface RootState {
user: {
operationPermissionsList?: string[];
};
}
export default function PointManagementView() {
const permissions = useSelector((state: RootState) => state.user.operationPermissionsList ?? []);
const [settings, setSettings] = useState<PointSettings>({});
const [integralEnabled, setIntegralEnabled] = useState(false);
const [rechargeOpen, setRechargeOpen] = useState(false);
const [loading, setLoading] = useState(false);
const load = useCallback(async () => {
setLoading(true);
const result = await getPointSettings();
if (result !== false) {
setSettings(result);
setIntegralEnabled(String(result.integral_switch) !== "0");
}
setLoading(false);
}, []);
useEffect(() => {
void load();
}, [load]);
const handleSubmit = async (values: PointSettingsFormValues) => {
const success = await savePointSettings(values);
if (success) await load();
};
const handleToggle = async (enabled: boolean) => {
const success = await setPointSwitch(enabled ? "1" : "0");
if (success) await load();
if (!enabled) setRechargeOpen(false);
};
const handleRecharge = async (values: RechargeFormValues) => {
const success = await rechargePoints(values);
if (success) setRechargeOpen(false);
};
return (
<div style={{ padding: 20 }}>
<PointSettingsForm
loading={loading}
values={settings}
integralEnabled={integralEnabled}
canToggle={permissions.includes("/setting/point-management/integralSet")}
canRecharge={permissions.includes("/setting/point-management/add")}
canEdit={permissions.includes("/setting/point-management/integralEdit")}
onToggle={handleToggle}
onOpenRecharge={() => setRechargeOpen(true)}
onSubmit={handleSubmit}
/>
<RechargeDrawer
open={rechargeOpen}
onClose={() => setRechargeOpen(false)}
onSubmit={handleRecharge}
/>
</div>
);
}
...@@ -6,7 +6,7 @@ const SettingsLayout = lazy(() => import("@/pages/setting/index")); ...@@ -6,7 +6,7 @@ const SettingsLayout = lazy(() => import("@/pages/setting/index"));
const HelpRoutes = lazy(() => import("./help/routes")); const HelpRoutes = lazy(() => import("./help/routes"));
const AppliedRoutes = lazy(() => import("./applied/routes")); const AppliedRoutes = lazy(() => import("./applied/routes"));
const PersonalizedRoutes = lazy(() => import("./personalized/routes")); const PersonalizedRoutes = lazy(() => import("./personalized/routes"));
const PointManagement = lazy(() => import("@/pages/setting/point-management/index")); const PointManagementRoutes = lazy(() => import("./point-management/routes"));
const AppVersionRoutes = lazy(() => import("./app-version/routes")); const AppVersionRoutes = lazy(() => import("./app-version/routes"));
const CouponRoutes = lazy(() => import("./coupon/routes")); const CouponRoutes = lazy(() => import("./coupon/routes"));
const SensitiveRoutes = lazy(() => import("./sensitive/routes")); const SensitiveRoutes = lazy(() => import("./sensitive/routes"));
...@@ -21,7 +21,7 @@ export default function SettingsRoutes() { ...@@ -21,7 +21,7 @@ export default function SettingsRoutes() {
{ path: "help/*", Component: HelpRoutes }, { path: "help/*", Component: HelpRoutes },
{ path: "applied/*", Component: AppliedRoutes }, { path: "applied/*", Component: AppliedRoutes },
{ path: "personalized-setting/*", Component: PersonalizedRoutes }, { path: "personalized-setting/*", Component: PersonalizedRoutes },
{ path: "point-management", Component: PointManagement }, { path: "point-management/*", Component: PointManagementRoutes },
{ path: "app-version/*", Component: AppVersionRoutes }, { path: "app-version/*", Component: AppVersionRoutes },
{ path: "coupon/*", Component: CouponRoutes }, { path: "coupon/*", Component: CouponRoutes },
{ path: "sensitive/*", Component: SensitiveRoutes }, { path: "sensitive/*", Component: SensitiveRoutes },
......
Markdown 格式
0% 或
您添加了 0 人 到此讨论。请谨慎行事。
请先完成此评论的编辑!
请 注册 或者 后发表评论