From a393b5c0e3e516f859039606d817ccd516913a83 Mon Sep 17 00:00:00 2001 From: ouzhou Date: Wed, 17 Jun 2026 10:38:40 +0800 Subject: [PATCH] =?UTF-8?q?feat(config):=20=E9=85=8D=E7=BD=AE=E9=A1=B5=20b?= =?UTF-8?q?ool=20=E5=BC=80=E5=85=B3=E6=8E=A7=E4=BB=B6=20+=20=E5=85=A8?= =?UTF-8?q?=E7=AB=99=20antd=20App.useApp=20=E8=BF=81=E7=A7=BB=20(#12)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 系统配置页支持 bool 类型(Switch 控件),供「提现自动对账」「比价期广告」等运营开关使用 - message / Modal.confirm 静态调用迁移到 App.useApp() 的 message / modal,消除 React 19 "Static function can not consume context" 告警;providers 用 容器包裹全站 Co-Authored-By: Claude Opus 4.8 (1M context) --------- Co-authored-by: OuYingJun1024 <1034284404@qq.com> Reviewed-on: https://gitea.shaguabijia.com/WonderableAI/shaguabijia-admin-web/pulls/12 Co-authored-by: ouzhou Co-committed-by: ouzhou --- src/app/(main)/ad-revenue/page.tsx | 3 ++- src/app/(main)/admins/page.tsx | 7 ++++--- src/app/(main)/config/page.tsx | 13 ++++++++++--- src/app/(main)/dashboard/HomeMarqueeSeeds.tsx | 3 ++- src/app/(main)/dashboard/HomeStatsConfig.tsx | 6 +++--- src/app/(main)/devices/page.tsx | 5 +++-- src/app/(main)/feedbacks/page.tsx | 6 +++--- src/app/(main)/price-reports/page.tsx | 5 +++-- src/app/(main)/users/[id]/page.tsx | 6 +++--- src/app/(main)/users/page.tsx | 11 ++++++----- src/app/(main)/withdraws/page.tsx | 13 +++++++------ src/app/login/page.tsx | 3 ++- src/app/providers.tsx | 6 ++++-- src/components/AdjustBalanceModals.tsx | 4 +++- 14 files changed, 55 insertions(+), 36 deletions(-) diff --git a/src/app/(main)/ad-revenue/page.tsx b/src/app/(main)/ad-revenue/page.tsx index e5d2e04..8515de2 100644 --- a/src/app/(main)/ad-revenue/page.tsx +++ b/src/app/(main)/ad-revenue/page.tsx @@ -3,6 +3,7 @@ import { useCallback, useEffect, useState } from 'react'; import type { ColumnsType } from 'antd/es/table'; import { + App, Alert, Button, Card, @@ -19,7 +20,6 @@ import { Table, Tag, Typography, - message, } from 'antd'; import { FallOutlined, @@ -269,6 +269,7 @@ function TrendChart({ points }: { points: TrendPoint[] }) { // 广告数据页:单表 —— 按 用户×类型×应用×代码位 聚合,一行同时给出 // 展示条数 / 收益 / 应发金币 / 实发金币 / 是否一致(发奖对账下沉到聚合级)。 export default function AdRevenuePage() { + const { message } = App.useApp(); const [range, setRange] = useState<[Dayjs, Dayjs]>([dayjs(), dayjs()]); const [userId, setUserId] = useState(null); const [adType, setAdType] = useState(); diff --git a/src/app/(main)/admins/page.tsx b/src/app/(main)/admins/page.tsx index a8baa4e..4750ff3 100644 --- a/src/app/(main)/admins/page.tsx +++ b/src/app/(main)/admins/page.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'; import type { ColumnsType } from 'antd/es/table'; -import { Button, Form, Input, Modal, Select, Table, Tag, message } from 'antd'; +import { App, Button, Form, Input, Modal, Select, Table, Tag } from 'antd'; import { api, errMsg } from '@/lib/api'; import { formatUtcTime } from '@/lib/format'; import type { AdminInfo } from '@/lib/types'; @@ -16,6 +16,7 @@ const ROLES = [ const dt = (v: string | null) => (v ? formatUtcTime(v, 'YYYY-MM-DD HH:mm:ss') : '从未'); export default function AdminsPage() { + const { message, modal } = App.useApp(); const [admins, setAdmins] = useState([]); const [loading, setLoading] = useState(false); const [createOpen, setCreateOpen] = useState(false); @@ -49,7 +50,7 @@ export default function AdminsPage() { const changeRole = (a: AdminInfo, role: string) => { if (role === a.role) return; - Modal.confirm({ + modal.confirm({ title: `把 ${a.username} 的角色改为 ${role}?`, onOk: async () => { try { @@ -65,7 +66,7 @@ export default function AdminsPage() { const toggle = (a: AdminInfo) => { const next = a.status === 'active' ? 'disabled' : 'active'; - Modal.confirm({ + modal.confirm({ title: `${next === 'disabled' ? '禁用' : '启用'}管理员 ${a.username}?`, onOk: async () => { try { diff --git a/src/app/(main)/config/page.tsx b/src/app/(main)/config/page.tsx index 54069a5..4df3570 100644 --- a/src/app/(main)/config/page.tsx +++ b/src/app/(main)/config/page.tsx @@ -1,14 +1,14 @@ 'use client'; import { useEffect, useState } from 'react'; -import { Button, Card, Input, InputNumber, Space, Spin, Tag, Tooltip, message } from 'antd'; +import { App, Button, Card, Input, InputNumber, Space, Spin, Switch, Tag, Tooltip } from 'antd'; import { api, errMsg } from '@/lib/api'; interface ConfigItem { key: string; label: string; group: string; - type: string; // int / int_list / dict_str_int + type: string; // int / int_list / dict_str_int / bool help: string | null; // eslint-disable-next-line @typescript-eslint/no-explicit-any default: any; @@ -28,6 +28,7 @@ function toEdit(item: ConfigItem): any { // eslint-disable-next-line @typescript-eslint/no-explicit-any function fromEdit(type: string, raw: any): any { if (type === 'int') return Number(raw); + if (type === 'bool') return Boolean(raw); if (type === 'int_list') { return String(raw) .split(',') @@ -38,6 +39,7 @@ function fromEdit(type: string, raw: any): any { } export default function ConfigPage() { + const { message } = App.useApp(); const [items, setItems] = useState([]); const [loading, setLoading] = useState(true); // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -109,7 +111,12 @@ export default function ConfigPage() { )} - {item.type === 'int' ? ( + {item.type === 'bool' ? ( + setEdits({ ...edits, [item.key]: v })} + /> + ) : item.type === 'int' ? ( setEdits({ ...edits, [item.key]: v })} diff --git a/src/app/(main)/dashboard/HomeMarqueeSeeds.tsx b/src/app/(main)/dashboard/HomeMarqueeSeeds.tsx index 4fe0db7..ab915f8 100644 --- a/src/app/(main)/dashboard/HomeMarqueeSeeds.tsx +++ b/src/app/(main)/dashboard/HomeMarqueeSeeds.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'; import { + App, Button, Form, Input, @@ -12,7 +13,6 @@ import { Spin, Switch, Table, - message, } from 'antd'; import { api, errMsg } from '@/lib/api'; @@ -51,6 +51,7 @@ const yuan = (cents: number) => (cents / 100).toFixed(2); /** 首页轮播「种子」管理(真实记录不足时的兜底假数据,现为「生成规则」)。「数据大盘」页的一个区块。 */ export default function HomeMarqueeSeeds() { + const { message } = App.useApp(); const [seeds, setSeeds] = useState([]); const [loading, setLoading] = useState(true); const [selectedRowKeys, setSelectedRowKeys] = useState([]); // 批量操作选中行 diff --git a/src/app/(main)/dashboard/HomeStatsConfig.tsx b/src/app/(main)/dashboard/HomeStatsConfig.tsx index 9825843..326920a 100644 --- a/src/app/(main)/dashboard/HomeStatsConfig.tsx +++ b/src/app/(main)/dashboard/HomeStatsConfig.tsx @@ -2,12 +2,12 @@ import { useEffect, useState } from 'react'; import { + App, Button, Card, Checkbox, Col, InputNumber, - Modal, Popconfirm, Radio, Row, @@ -16,7 +16,6 @@ import { Spin, Tag, Tooltip, - message, } from 'antd'; import { api, errMsg } from '@/lib/api'; import { formatUtcTime } from '@/lib/format'; @@ -249,6 +248,7 @@ function buildBody(metric: string, e: Edit, immediate: boolean): Record([]); const [loading, setLoading] = useState(true); const [edits, setEdits] = useState>({}); @@ -321,7 +321,7 @@ export default function HomeStatsConfig() { // 智能校验:按下后三者大概会呈现的关系是否合理,不合理则弹窗劝阻、确认才提交。 const rel = checkRelations(items, edits); if (rel && rel.warnings.length > 0) { - Modal.confirm({ + modal.confirm({ title: '数据关系校验', width: 480, okText: '仍要这样设置', diff --git a/src/app/(main)/devices/page.tsx b/src/app/(main)/devices/page.tsx index f746de8..99c03b7 100644 --- a/src/app/(main)/devices/page.tsx +++ b/src/app/(main)/devices/page.tsx @@ -1,7 +1,7 @@ 'use client'; import type { ColumnsType } from 'antd/es/table'; -import { Button, Modal, Popconfirm, Space, Table, message } from 'antd'; +import { App, Button, Popconfirm, Space, Table } from 'antd'; import { api, errMsg } from '@/lib/api'; import { canDo } from '@/lib/auth'; import { formatUtcTime } from '@/lib/format'; @@ -13,6 +13,7 @@ const dt = (v: string) => formatUtcTime(v); const EMPTY: Record = {}; export default function DevicesPage() { + const { message, modal } = App.useApp(); // 后端按设备聚合、全量返回(next_cursor 恒 null),故无筛选/加载更多。 const { items, loading, reload } = useCursorList( '/admin/api/onboarding/devices', @@ -31,7 +32,7 @@ export default function DevicesPage() { }; const resetAll = () => { - Modal.confirm({ + modal.confirm({ title: '确认全部重设新手引导?', content: '清空所有设备的引导完成记录,库里所有用户下次打开 App 都会重走一遍新手引导。此操作不可逆。', diff --git a/src/app/(main)/feedbacks/page.tsx b/src/app/(main)/feedbacks/page.tsx index 4e0d253..82b2a5b 100644 --- a/src/app/(main)/feedbacks/page.tsx +++ b/src/app/(main)/feedbacks/page.tsx @@ -5,17 +5,16 @@ import type { Key } from 'react'; import type { ColumnsType } from 'antd/es/table'; import type { SorterResult } from 'antd/es/table/interface'; import { + App, Button, DatePicker, Image, Input, - Modal, Select, Space, Table, Tag, Typography, - message, } from 'antd'; import type { Dayjs } from 'dayjs'; import { api, errMsg } from '@/lib/api'; @@ -34,6 +33,7 @@ const mediaUrl = (p: string) => (p.startsWith('http') ? p : MEDIA_BASE + p); type SortField = 'id' | 'created_at'; export default function FeedbacksPage() { + const { message, modal } = App.useApp(); // 筛选草稿:点「查询」才应用(避免输入即刷新) const [status, setStatus] = useState(); const [userId, setUserId] = useState(''); @@ -114,7 +114,7 @@ export default function FeedbacksPage() { message.warning('没有可处理的选中项(仅待处理 new 态生效)'); return; } - Modal.confirm({ + modal.confirm({ title: `确认批量标记处理 ${selectedNew.length} 条反馈?`, content: '仅对选中的待处理(new)反馈生效', onOk: async () => { diff --git a/src/app/(main)/price-reports/page.tsx b/src/app/(main)/price-reports/page.tsx index db2d17d..1bfbfd2 100644 --- a/src/app/(main)/price-reports/page.tsx +++ b/src/app/(main)/price-reports/page.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'; import { + App, Button, Card, Image, @@ -13,7 +14,6 @@ import { Tabs, Tag, Typography, - message, } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import { CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons'; @@ -57,6 +57,7 @@ function statusTag(status: string) { } export default function PriceReportsPage() { + const { message, modal } = App.useApp(); const [activeStatus, setActiveStatus] = useState('pending'); const [summary, setSummary] = useState(null); const [rejecting, setRejecting] = useState(null); @@ -88,7 +89,7 @@ export default function PriceReportsPage() { }; const approve = (r: PriceReport) => { - Modal.confirm({ + modal.confirm({ title: '确认通过该上报?', icon: , content: ( diff --git a/src/app/(main)/users/[id]/page.tsx b/src/app/(main)/users/[id]/page.tsx index 1a49a32..2c1c1dc 100644 --- a/src/app/(main)/users/[id]/page.tsx +++ b/src/app/(main)/users/[id]/page.tsx @@ -4,11 +4,11 @@ import { useCallback, useEffect, useState } from 'react'; import { useParams } from 'next/navigation'; import type { ColumnsType } from 'antd/es/table'; import { + App, Button, Card, Col, Descriptions, - Modal, Result, Row, Space, @@ -17,7 +17,6 @@ import { Table, Tabs, Tag, - message, } from 'antd'; import { api, errMsg } from '@/lib/api'; import { canDo } from '@/lib/auth'; @@ -27,6 +26,7 @@ import { AdjustCashModal, AdjustCoinModal } from '@/components/AdjustBalanceModa import type { CashTxn, CoinTxn, UserOverview, WithdrawOrder } from '@/lib/types'; export default function UserDetailPage() { + const { message, modal } = App.useApp(); const params = useParams<{ id: string }>(); const uid = Number(params.id); const [data, setData] = useState(null); @@ -56,7 +56,7 @@ export default function UserDetailPage() { const toggleStatus = () => { if (!data) return; const next = data.user.status === 'active' ? 'disabled' : 'active'; - Modal.confirm({ + modal.confirm({ title: `确认${next === 'disabled' ? '封禁' : '解封'}用户 ${data.user.phone}?`, onOk: async () => { try { diff --git a/src/app/(main)/users/page.tsx b/src/app/(main)/users/page.tsx index 8e18458..5b0f537 100644 --- a/src/app/(main)/users/page.tsx +++ b/src/app/(main)/users/page.tsx @@ -5,7 +5,7 @@ import type { Key } from 'react'; import { useRouter } from 'next/navigation'; import type { ColumnsType } from 'antd/es/table'; import type { SorterResult } from 'antd/es/table/interface'; -import { Button, DatePicker, Input, Modal, Select, Space, Table, Tag, message } from 'antd'; +import { App, Button, DatePicker, Input, Select, Space, Table, Tag } from 'antd'; import dayjs, { type Dayjs } from 'dayjs'; import { api, errMsg } from '@/lib/api'; import { canDo } from '@/lib/auth'; @@ -22,6 +22,7 @@ type SortField = 'id' | 'created_at' | 'last_login_at'; export default function UsersPage() { const router = useRouter(); + const { message, modal } = App.useApp(); // 筛选草稿:点「查询」才应用(避免输入即刷新);状态/渠道/日期一并随查询提交 const [phone, setPhone] = useState(''); @@ -97,7 +98,7 @@ export default function UsersPage() { const toggleStatus = (u: UserListItem) => { const next = u.status === 'active' ? 'disabled' : 'active'; - Modal.confirm({ + modal.confirm({ title: `确认${next === 'disabled' ? '封禁' : '解封'}用户 ${u.phone}?`, onOk: async () => { try { @@ -113,7 +114,7 @@ export default function UsersPage() { const toggleDebugTrace = (u: UserListItem) => { const next = !u.debug_trace_enabled; - Modal.confirm({ + modal.confirm({ title: `确认${next ? '开启' : '关闭'}用户 ${u.phone} 的调试链接权限?`, content: next ? '开启后,该用户在 App 比价结果页/记录页可复制 trace 调试链接发给开发排障' @@ -156,7 +157,7 @@ export default function UsersPage() { message.warning(`没有可${target === 'disabled' ? '封禁(active)' : '解封(disabled)'}的选中用户`); return; } - Modal.confirm({ + modal.confirm({ title: `确认${label} ${targets.length} 个用户?`, content: target === 'disabled' ? '仅对选中的 active 用户生效' : '仅对选中的 disabled 用户生效', okButtonProps: { danger: target === 'disabled' }, @@ -177,7 +178,7 @@ export default function UsersPage() { message.warning('没有需要变更的选中用户'); return; } - Modal.confirm({ + modal.confirm({ title: `确认${label} ${targets.length} 个用户?`, onOk: () => runBulk(targets, label, (u) => diff --git a/src/app/(main)/withdraws/page.tsx b/src/app/(main)/withdraws/page.tsx index a33a067..afc9fdf 100644 --- a/src/app/(main)/withdraws/page.tsx +++ b/src/app/(main)/withdraws/page.tsx @@ -3,6 +3,7 @@ import { useCallback, useEffect, useState } from 'react'; import type { Key } from 'react'; import { + App, Alert, Button, Card, @@ -22,7 +23,6 @@ import { Tag, Timeline, Typography, - message, } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import { @@ -169,6 +169,7 @@ function bulkResultText(action: string, result: WithdrawBulkResult) { } export default function WithdrawsPage() { + const { message, modal } = App.useApp(); const [activeStatus, setActiveStatus] = useState('reviewing'); const [summary, setSummary] = useState(null); const [health, setHealth] = useState(null); @@ -305,7 +306,7 @@ export default function WithdrawsPage() { }; const approve = (o: WithdrawOrder) => { - Modal.confirm({ + modal.confirm({ title: `确认通过并打款 ${yuan(o.amount_cents)}?`, content: (
@@ -368,7 +369,7 @@ export default function WithdrawsPage() { } const amount = selectedReviewing.reduce((sum, item) => sum + item.amount_cents, 0); let confirmText = ''; - Modal.confirm({ + modal.confirm({ title: `确认批量通过 ${selectedReviewing.length} 笔提现?`, content: ( @@ -406,7 +407,7 @@ export default function WithdrawsPage() { message.warning('请选择打款中的提现单'); return; } - Modal.confirm({ + modal.confirm({ title: `确认批量刷新 ${selectedPending.length} 笔打款状态?`, content: '会逐笔向微信查单,并按最新状态归一化。', okText: '批量刷新查单', @@ -428,7 +429,7 @@ export default function WithdrawsPage() { }; const retry = (o: WithdrawOrder) => { - Modal.confirm({ + modal.confirm({ title: `重新查询 ${o.out_bill_no} 的微信状态?`, content: '会按微信最新状态归一化为成功、失败退款或撤销未确认。', okText: '刷新查单', @@ -450,7 +451,7 @@ export default function WithdrawsPage() { }; const reconcile = () => { - Modal.confirm({ + modal.confirm({ title: '批量对账?', content: '扫描超过 15 分钟仍处于打款中的提现单,逐单向微信查实并归一化。', okText: '开始对账', diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index c899571..d4bfd9d 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -2,13 +2,14 @@ import { useState } from 'react'; import { useRouter } from 'next/navigation'; -import { Button, Card, Form, Input, message } from 'antd'; +import { App, Button, Card, Form, Input } from 'antd'; import { api, errMsg } from '@/lib/api'; import { setAuth } from '@/lib/auth'; import type { LoginResponse } from '@/lib/types'; export default function LoginPage() { const router = useRouter(); + const { message } = App.useApp(); const [loading, setLoading] = useState(false); const onFinish = async (values: { username: string; password: string }) => { diff --git a/src/app/providers.tsx b/src/app/providers.tsx index 62e2100..e81d986 100644 --- a/src/app/providers.tsx +++ b/src/app/providers.tsx @@ -5,7 +5,7 @@ // 必须在引入 antd 之前 import。 import '@ant-design/v5-patch-for-react-19'; import { AntdRegistry } from '@ant-design/nextjs-registry'; -import { ConfigProvider } from 'antd'; +import { App, ConfigProvider } from 'antd'; import zhCN from 'antd/locale/zh_CN'; export function Providers({ children }: { children: React.ReactNode }) { @@ -23,7 +23,9 @@ export function Providers({ children }: { children: React.ReactNode }) { }, }} > - {children} + {/* antd 容器:让 message/notification/Modal 能通过 App.useApp() 读到 ConfigProvider + 的动态主题/locale,消除 "Static function can not consume context" 告警(全站生效) */} + {children} ); diff --git a/src/components/AdjustBalanceModals.tsx b/src/components/AdjustBalanceModals.tsx index 5192b1f..8c287b7 100644 --- a/src/components/AdjustBalanceModals.tsx +++ b/src/components/AdjustBalanceModals.tsx @@ -3,7 +3,7 @@ // 调金币 / 调现金弹窗(受控)。列表页与用户详情页共用,避免两处各写一套。 // 每个弹窗自带「增减 / 设为指定值」两种模式;打开时拉一次 360 概览展示当前余额。 import { useEffect, useState } from 'react'; -import { Form, Input, InputNumber, Modal, Segmented, message } from 'antd'; +import { App, Form, Input, InputNumber, Modal, Segmented } from 'antd'; import { api, errMsg } from '@/lib/api'; import { yuan } from '@/lib/format'; import type { UserOverview } from '@/lib/types'; @@ -47,6 +47,7 @@ function useBalances(userId: number | undefined, open: boolean) { } export function AdjustCoinModal({ user, open, onClose, onDone }: ModalProps) { + const { message } = App.useApp(); const [form] = Form.useForm(); const [mode, setMode] = useState<'delta' | 'set'>('delta'); const balances = useBalances(user?.id, open); @@ -110,6 +111,7 @@ export function AdjustCoinModal({ user, open, onClose, onDone }: ModalProps) { } export function AdjustCashModal({ user, open, onClose, onDone }: ModalProps) { + const { message } = App.useApp(); const [form] = Form.useForm(); const [mode, setMode] = useState<'delta' | 'set'>('delta'); const balances = useBalances(user?.id, open);