Compare commits

...

1 Commits

Author SHA1 Message Date
OuYingJun1024 6333d3d6ce feat(config): 配置页 bool 开关控件 + 全站 antd App.useApp 迁移
- 系统配置页支持 bool 类型(Switch 控件),供「提现自动对账」「比价期广告」等运营开关使用
- message / Modal.confirm 静态调用迁移到 App.useApp() 的 message / modal,消除 React 19
  "Static function can not consume context" 告警;providers 用 <App> 容器包裹全站

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 20:28:26 +08:00
14 changed files with 55 additions and 36 deletions
+2 -1
View File
@@ -3,6 +3,7 @@
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import type { ColumnsType } from 'antd/es/table'; import type { ColumnsType } from 'antd/es/table';
import { import {
App,
Alert, Alert,
Button, Button,
Card, Card,
@@ -19,7 +20,6 @@ import {
Table, Table,
Tag, Tag,
Typography, Typography,
message,
} from 'antd'; } from 'antd';
import { import {
FallOutlined, FallOutlined,
@@ -269,6 +269,7 @@ function TrendChart({ points }: { points: TrendPoint[] }) {
// 广告数据页:单表 —— 按 用户×类型×应用×代码位 聚合,一行同时给出 // 广告数据页:单表 —— 按 用户×类型×应用×代码位 聚合,一行同时给出
// 展示条数 / 收益 / 应发金币 / 实发金币 / 是否一致(发奖对账下沉到聚合级)。 // 展示条数 / 收益 / 应发金币 / 实发金币 / 是否一致(发奖对账下沉到聚合级)。
export default function AdRevenuePage() { export default function AdRevenuePage() {
const { message } = App.useApp();
const [range, setRange] = useState<[Dayjs, Dayjs]>([dayjs(), dayjs()]); const [range, setRange] = useState<[Dayjs, Dayjs]>([dayjs(), dayjs()]);
const [userId, setUserId] = useState<number | null>(null); const [userId, setUserId] = useState<number | null>(null);
const [adType, setAdType] = useState<string | undefined>(); const [adType, setAdType] = useState<string | undefined>();
+4 -3
View File
@@ -2,7 +2,7 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import type { ColumnsType } from 'antd/es/table'; 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 { api, errMsg } from '@/lib/api';
import { formatUtcTime } from '@/lib/format'; import { formatUtcTime } from '@/lib/format';
import type { AdminInfo } from '@/lib/types'; 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') : '从未'); const dt = (v: string | null) => (v ? formatUtcTime(v, 'YYYY-MM-DD HH:mm:ss') : '从未');
export default function AdminsPage() { export default function AdminsPage() {
const { message, modal } = App.useApp();
const [admins, setAdmins] = useState<AdminInfo[]>([]); const [admins, setAdmins] = useState<AdminInfo[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [createOpen, setCreateOpen] = useState(false); const [createOpen, setCreateOpen] = useState(false);
@@ -49,7 +50,7 @@ export default function AdminsPage() {
const changeRole = (a: AdminInfo, role: string) => { const changeRole = (a: AdminInfo, role: string) => {
if (role === a.role) return; if (role === a.role) return;
Modal.confirm({ modal.confirm({
title: `${a.username} 的角色改为 ${role}?`, title: `${a.username} 的角色改为 ${role}?`,
onOk: async () => { onOk: async () => {
try { try {
@@ -65,7 +66,7 @@ export default function AdminsPage() {
const toggle = (a: AdminInfo) => { const toggle = (a: AdminInfo) => {
const next = a.status === 'active' ? 'disabled' : 'active'; const next = a.status === 'active' ? 'disabled' : 'active';
Modal.confirm({ modal.confirm({
title: `${next === 'disabled' ? '禁用' : '启用'}管理员 ${a.username}?`, title: `${next === 'disabled' ? '禁用' : '启用'}管理员 ${a.username}?`,
onOk: async () => { onOk: async () => {
try { try {
+10 -3
View File
@@ -1,14 +1,14 @@
'use client'; 'use client';
import { useEffect, useState } from 'react'; 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'; import { api, errMsg } from '@/lib/api';
interface ConfigItem { interface ConfigItem {
key: string; key: string;
label: string; label: string;
group: string; group: string;
type: string; // int / int_list / dict_str_int type: string; // int / int_list / dict_str_int / bool
help: string | null; help: string | null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
default: any; default: any;
@@ -28,6 +28,7 @@ function toEdit(item: ConfigItem): any {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
function fromEdit(type: string, raw: any): any { function fromEdit(type: string, raw: any): any {
if (type === 'int') return Number(raw); if (type === 'int') return Number(raw);
if (type === 'bool') return Boolean(raw);
if (type === 'int_list') { if (type === 'int_list') {
return String(raw) return String(raw)
.split(',') .split(',')
@@ -38,6 +39,7 @@ function fromEdit(type: string, raw: any): any {
} }
export default function ConfigPage() { export default function ConfigPage() {
const { message } = App.useApp();
const [items, setItems] = useState<ConfigItem[]>([]); const [items, setItems] = useState<ConfigItem[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -109,7 +111,12 @@ export default function ConfigPage() {
)} )}
</div> </div>
<Space wrap> <Space wrap>
{item.type === 'int' ? ( {item.type === 'bool' ? (
<Switch
checked={!!edits[item.key]}
onChange={(v) => setEdits({ ...edits, [item.key]: v })}
/>
) : item.type === 'int' ? (
<InputNumber <InputNumber
value={edits[item.key]} value={edits[item.key]}
onChange={(v) => setEdits({ ...edits, [item.key]: v })} onChange={(v) => setEdits({ ...edits, [item.key]: v })}
@@ -2,6 +2,7 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { import {
App,
Button, Button,
Form, Form,
Input, Input,
@@ -12,7 +13,6 @@ import {
Spin, Spin,
Switch, Switch,
Table, Table,
message,
} from 'antd'; } from 'antd';
import { api, errMsg } from '@/lib/api'; import { api, errMsg } from '@/lib/api';
@@ -51,6 +51,7 @@ const yuan = (cents: number) => (cents / 100).toFixed(2);
/** 首页轮播「种子」管理(真实记录不足时的兜底假数据,现为「生成规则」)。「数据大盘」页的一个区块。 */ /** 首页轮播「种子」管理(真实记录不足时的兜底假数据,现为「生成规则」)。「数据大盘」页的一个区块。 */
export default function HomeMarqueeSeeds() { export default function HomeMarqueeSeeds() {
const { message } = App.useApp();
const [seeds, setSeeds] = useState<Seed[]>([]); const [seeds, setSeeds] = useState<Seed[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [selectedRowKeys, setSelectedRowKeys] = useState<number[]>([]); // 批量操作选中行 const [selectedRowKeys, setSelectedRowKeys] = useState<number[]>([]); // 批量操作选中行
+3 -3
View File
@@ -2,12 +2,12 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { import {
App,
Button, Button,
Card, Card,
Checkbox, Checkbox,
Col, Col,
InputNumber, InputNumber,
Modal,
Popconfirm, Popconfirm,
Radio, Radio,
Row, Row,
@@ -16,7 +16,6 @@ import {
Spin, Spin,
Tag, Tag,
Tooltip, Tooltip,
message,
} from 'antd'; } from 'antd';
import { api, errMsg } from '@/lib/api'; import { api, errMsg } from '@/lib/api';
import { formatUtcTime } from '@/lib/format'; import { formatUtcTime } from '@/lib/format';
@@ -249,6 +248,7 @@ function buildBody(metric: string, e: Edit, immediate: boolean): Record<string,
/** 首页三门面数字(帮助用户/完成比价/累计节省)的展示模式配置。作为「数据大盘」页的一个区块。 */ /** 首页三门面数字(帮助用户/完成比价/累计节省)的展示模式配置。作为「数据大盘」页的一个区块。 */
export default function HomeStatsConfig() { export default function HomeStatsConfig() {
const { message, modal } = App.useApp();
const [items, setItems] = useState<StatItem[]>([]); const [items, setItems] = useState<StatItem[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [edits, setEdits] = useState<Record<string, Edit>>({}); const [edits, setEdits] = useState<Record<string, Edit>>({});
@@ -321,7 +321,7 @@ export default function HomeStatsConfig() {
// 智能校验:按下后三者大概会呈现的关系是否合理,不合理则弹窗劝阻、确认才提交。 // 智能校验:按下后三者大概会呈现的关系是否合理,不合理则弹窗劝阻、确认才提交。
const rel = checkRelations(items, edits); const rel = checkRelations(items, edits);
if (rel && rel.warnings.length > 0) { if (rel && rel.warnings.length > 0) {
Modal.confirm({ modal.confirm({
title: '数据关系校验', title: '数据关系校验',
width: 480, width: 480,
okText: '仍要这样设置', okText: '仍要这样设置',
+3 -2
View File
@@ -1,7 +1,7 @@
'use client'; 'use client';
import type { ColumnsType } from 'antd/es/table'; 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 { api, errMsg } from '@/lib/api';
import { canDo } from '@/lib/auth'; import { canDo } from '@/lib/auth';
import { formatUtcTime } from '@/lib/format'; import { formatUtcTime } from '@/lib/format';
@@ -13,6 +13,7 @@ const dt = (v: string) => formatUtcTime(v);
const EMPTY: Record<string, unknown> = {}; const EMPTY: Record<string, unknown> = {};
export default function DevicesPage() { export default function DevicesPage() {
const { message, modal } = App.useApp();
// 后端按设备聚合、全量返回(next_cursor 恒 null),故无筛选/加载更多。 // 后端按设备聚合、全量返回(next_cursor 恒 null),故无筛选/加载更多。
const { items, loading, reload } = useCursorList<DeviceOnboardingItem>( const { items, loading, reload } = useCursorList<DeviceOnboardingItem>(
'/admin/api/onboarding/devices', '/admin/api/onboarding/devices',
@@ -31,7 +32,7 @@ export default function DevicesPage() {
}; };
const resetAll = () => { const resetAll = () => {
Modal.confirm({ modal.confirm({
title: '确认全部重设新手引导?', title: '确认全部重设新手引导?',
content: content:
'清空所有设备的引导完成记录,库里所有用户下次打开 App 都会重走一遍新手引导。此操作不可逆。', '清空所有设备的引导完成记录,库里所有用户下次打开 App 都会重走一遍新手引导。此操作不可逆。',
+3 -3
View File
@@ -5,17 +5,16 @@ import type { Key } from 'react';
import type { ColumnsType } from 'antd/es/table'; import type { ColumnsType } from 'antd/es/table';
import type { SorterResult } from 'antd/es/table/interface'; import type { SorterResult } from 'antd/es/table/interface';
import { import {
App,
Button, Button,
DatePicker, DatePicker,
Image, Image,
Input, Input,
Modal,
Select, Select,
Space, Space,
Table, Table,
Tag, Tag,
Typography, Typography,
message,
} from 'antd'; } from 'antd';
import type { Dayjs } from 'dayjs'; import type { Dayjs } from 'dayjs';
import { api, errMsg } from '@/lib/api'; 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'; type SortField = 'id' | 'created_at';
export default function FeedbacksPage() { export default function FeedbacksPage() {
const { message, modal } = App.useApp();
// 筛选草稿:点「查询」才应用(避免输入即刷新) // 筛选草稿:点「查询」才应用(避免输入即刷新)
const [status, setStatus] = useState<string | undefined>(); const [status, setStatus] = useState<string | undefined>();
const [userId, setUserId] = useState(''); const [userId, setUserId] = useState('');
@@ -114,7 +114,7 @@ export default function FeedbacksPage() {
message.warning('没有可处理的选中项(仅待处理 new 态生效)'); message.warning('没有可处理的选中项(仅待处理 new 态生效)');
return; return;
} }
Modal.confirm({ modal.confirm({
title: `确认批量标记处理 ${selectedNew.length} 条反馈?`, title: `确认批量标记处理 ${selectedNew.length} 条反馈?`,
content: '仅对选中的待处理(new)反馈生效', content: '仅对选中的待处理(new)反馈生效',
onOk: async () => { onOk: async () => {
+3 -2
View File
@@ -2,6 +2,7 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { import {
App,
Button, Button,
Card, Card,
Image, Image,
@@ -13,7 +14,6 @@ import {
Tabs, Tabs,
Tag, Tag,
Typography, Typography,
message,
} from 'antd'; } from 'antd';
import type { ColumnsType } from 'antd/es/table'; import type { ColumnsType } from 'antd/es/table';
import { CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons'; import { CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons';
@@ -57,6 +57,7 @@ function statusTag(status: string) {
} }
export default function PriceReportsPage() { export default function PriceReportsPage() {
const { message, modal } = App.useApp();
const [activeStatus, setActiveStatus] = useState('pending'); const [activeStatus, setActiveStatus] = useState('pending');
const [summary, setSummary] = useState<PriceReportSummary | null>(null); const [summary, setSummary] = useState<PriceReportSummary | null>(null);
const [rejecting, setRejecting] = useState<PriceReport | null>(null); const [rejecting, setRejecting] = useState<PriceReport | null>(null);
@@ -88,7 +89,7 @@ export default function PriceReportsPage() {
}; };
const approve = (r: PriceReport) => { const approve = (r: PriceReport) => {
Modal.confirm({ modal.confirm({
title: '确认通过该上报?', title: '确认通过该上报?',
icon: <CheckCircleOutlined style={{ color: '#52c41a' }} />, icon: <CheckCircleOutlined style={{ color: '#52c41a' }} />,
content: ( content: (
+3 -3
View File
@@ -4,11 +4,11 @@ import { useCallback, useEffect, useState } from 'react';
import { useParams } from 'next/navigation'; import { useParams } from 'next/navigation';
import type { ColumnsType } from 'antd/es/table'; import type { ColumnsType } from 'antd/es/table';
import { import {
App,
Button, Button,
Card, Card,
Col, Col,
Descriptions, Descriptions,
Modal,
Result, Result,
Row, Row,
Space, Space,
@@ -17,7 +17,6 @@ import {
Table, Table,
Tabs, Tabs,
Tag, Tag,
message,
} from 'antd'; } from 'antd';
import { api, errMsg } from '@/lib/api'; import { api, errMsg } from '@/lib/api';
import { canDo } from '@/lib/auth'; import { canDo } from '@/lib/auth';
@@ -27,6 +26,7 @@ import { AdjustCashModal, AdjustCoinModal } from '@/components/AdjustBalanceModa
import type { CashTxn, CoinTxn, UserOverview, WithdrawOrder } from '@/lib/types'; import type { CashTxn, CoinTxn, UserOverview, WithdrawOrder } from '@/lib/types';
export default function UserDetailPage() { export default function UserDetailPage() {
const { message, modal } = App.useApp();
const params = useParams<{ id: string }>(); const params = useParams<{ id: string }>();
const uid = Number(params.id); const uid = Number(params.id);
const [data, setData] = useState<UserOverview | null>(null); const [data, setData] = useState<UserOverview | null>(null);
@@ -56,7 +56,7 @@ export default function UserDetailPage() {
const toggleStatus = () => { const toggleStatus = () => {
if (!data) return; if (!data) return;
const next = data.user.status === 'active' ? 'disabled' : 'active'; const next = data.user.status === 'active' ? 'disabled' : 'active';
Modal.confirm({ modal.confirm({
title: `确认${next === 'disabled' ? '封禁' : '解封'}用户 ${data.user.phone}?`, title: `确认${next === 'disabled' ? '封禁' : '解封'}用户 ${data.user.phone}?`,
onOk: async () => { onOk: async () => {
try { try {
+6 -5
View File
@@ -5,7 +5,7 @@ import type { Key } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import type { ColumnsType } from 'antd/es/table'; import type { ColumnsType } from 'antd/es/table';
import type { SorterResult } from 'antd/es/table/interface'; 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 dayjs, { type Dayjs } from 'dayjs';
import { api, errMsg } from '@/lib/api'; import { api, errMsg } from '@/lib/api';
import { canDo } from '@/lib/auth'; import { canDo } from '@/lib/auth';
@@ -22,6 +22,7 @@ type SortField = 'id' | 'created_at' | 'last_login_at';
export default function UsersPage() { export default function UsersPage() {
const router = useRouter(); const router = useRouter();
const { message, modal } = App.useApp();
// 筛选草稿:点「查询」才应用(避免输入即刷新);状态/渠道/日期一并随查询提交 // 筛选草稿:点「查询」才应用(避免输入即刷新);状态/渠道/日期一并随查询提交
const [phone, setPhone] = useState(''); const [phone, setPhone] = useState('');
@@ -97,7 +98,7 @@ export default function UsersPage() {
const toggleStatus = (u: UserListItem) => { const toggleStatus = (u: UserListItem) => {
const next = u.status === 'active' ? 'disabled' : 'active'; const next = u.status === 'active' ? 'disabled' : 'active';
Modal.confirm({ modal.confirm({
title: `确认${next === 'disabled' ? '封禁' : '解封'}用户 ${u.phone}?`, title: `确认${next === 'disabled' ? '封禁' : '解封'}用户 ${u.phone}?`,
onOk: async () => { onOk: async () => {
try { try {
@@ -113,7 +114,7 @@ export default function UsersPage() {
const toggleDebugTrace = (u: UserListItem) => { const toggleDebugTrace = (u: UserListItem) => {
const next = !u.debug_trace_enabled; const next = !u.debug_trace_enabled;
Modal.confirm({ modal.confirm({
title: `确认${next ? '开启' : '关闭'}用户 ${u.phone} 的调试链接权限?`, title: `确认${next ? '开启' : '关闭'}用户 ${u.phone} 的调试链接权限?`,
content: next content: next
? '开启后,该用户在 App 比价结果页/记录页可复制 trace 调试链接发给开发排障' ? '开启后,该用户在 App 比价结果页/记录页可复制 trace 调试链接发给开发排障'
@@ -156,7 +157,7 @@ export default function UsersPage() {
message.warning(`没有可${target === 'disabled' ? '封禁(active)' : '解封(disabled)'}的选中用户`); message.warning(`没有可${target === 'disabled' ? '封禁(active)' : '解封(disabled)'}的选中用户`);
return; return;
} }
Modal.confirm({ modal.confirm({
title: `确认${label} ${targets.length} 个用户?`, title: `确认${label} ${targets.length} 个用户?`,
content: target === 'disabled' ? '仅对选中的 active 用户生效' : '仅对选中的 disabled 用户生效', content: target === 'disabled' ? '仅对选中的 active 用户生效' : '仅对选中的 disabled 用户生效',
okButtonProps: { danger: target === 'disabled' }, okButtonProps: { danger: target === 'disabled' },
@@ -177,7 +178,7 @@ export default function UsersPage() {
message.warning('没有需要变更的选中用户'); message.warning('没有需要变更的选中用户');
return; return;
} }
Modal.confirm({ modal.confirm({
title: `确认${label} ${targets.length} 个用户?`, title: `确认${label} ${targets.length} 个用户?`,
onOk: () => onOk: () =>
runBulk(targets, label, (u) => runBulk(targets, label, (u) =>
+7 -6
View File
@@ -3,6 +3,7 @@
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import type { Key } from 'react'; import type { Key } from 'react';
import { import {
App,
Alert, Alert,
Button, Button,
Card, Card,
@@ -22,7 +23,6 @@ import {
Tag, Tag,
Timeline, Timeline,
Typography, Typography,
message,
} from 'antd'; } from 'antd';
import type { ColumnsType } from 'antd/es/table'; import type { ColumnsType } from 'antd/es/table';
import { import {
@@ -169,6 +169,7 @@ function bulkResultText(action: string, result: WithdrawBulkResult) {
} }
export default function WithdrawsPage() { export default function WithdrawsPage() {
const { message, modal } = App.useApp();
const [activeStatus, setActiveStatus] = useState('reviewing'); const [activeStatus, setActiveStatus] = useState('reviewing');
const [summary, setSummary] = useState<WithdrawSummary | null>(null); const [summary, setSummary] = useState<WithdrawSummary | null>(null);
const [health, setHealth] = useState<WxpayHealthCheck | null>(null); const [health, setHealth] = useState<WxpayHealthCheck | null>(null);
@@ -305,7 +306,7 @@ export default function WithdrawsPage() {
}; };
const approve = (o: WithdrawOrder) => { const approve = (o: WithdrawOrder) => {
Modal.confirm({ modal.confirm({
title: `确认通过并打款 ${yuan(o.amount_cents)}?`, title: `确认通过并打款 ${yuan(o.amount_cents)}?`,
content: ( content: (
<div> <div>
@@ -368,7 +369,7 @@ export default function WithdrawsPage() {
} }
const amount = selectedReviewing.reduce((sum, item) => sum + item.amount_cents, 0); const amount = selectedReviewing.reduce((sum, item) => sum + item.amount_cents, 0);
let confirmText = ''; let confirmText = '';
Modal.confirm({ modal.confirm({
title: `确认批量通过 ${selectedReviewing.length} 笔提现?`, title: `确认批量通过 ${selectedReviewing.length} 笔提现?`,
content: ( content: (
<Space direction="vertical" style={{ width: '100%' }}> <Space direction="vertical" style={{ width: '100%' }}>
@@ -406,7 +407,7 @@ export default function WithdrawsPage() {
message.warning('请选择打款中的提现单'); message.warning('请选择打款中的提现单');
return; return;
} }
Modal.confirm({ modal.confirm({
title: `确认批量刷新 ${selectedPending.length} 笔打款状态?`, title: `确认批量刷新 ${selectedPending.length} 笔打款状态?`,
content: '会逐笔向微信查单,并按最新状态归一化。', content: '会逐笔向微信查单,并按最新状态归一化。',
okText: '批量刷新查单', okText: '批量刷新查单',
@@ -428,7 +429,7 @@ export default function WithdrawsPage() {
}; };
const retry = (o: WithdrawOrder) => { const retry = (o: WithdrawOrder) => {
Modal.confirm({ modal.confirm({
title: `重新查询 ${o.out_bill_no} 的微信状态?`, title: `重新查询 ${o.out_bill_no} 的微信状态?`,
content: '会按微信最新状态归一化为成功、失败退款或撤销未确认。', content: '会按微信最新状态归一化为成功、失败退款或撤销未确认。',
okText: '刷新查单', okText: '刷新查单',
@@ -450,7 +451,7 @@ export default function WithdrawsPage() {
}; };
const reconcile = () => { const reconcile = () => {
Modal.confirm({ modal.confirm({
title: '批量对账?', title: '批量对账?',
content: '扫描超过 15 分钟仍处于打款中的提现单,逐单向微信查实并归一化。', content: '扫描超过 15 分钟仍处于打款中的提现单,逐单向微信查实并归一化。',
okText: '开始对账', okText: '开始对账',
+2 -1
View File
@@ -2,13 +2,14 @@
import { useState } from 'react'; import { useState } from 'react';
import { useRouter } from 'next/navigation'; 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 { api, errMsg } from '@/lib/api';
import { setAuth } from '@/lib/auth'; import { setAuth } from '@/lib/auth';
import type { LoginResponse } from '@/lib/types'; import type { LoginResponse } from '@/lib/types';
export default function LoginPage() { export default function LoginPage() {
const router = useRouter(); const router = useRouter();
const { message } = App.useApp();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const onFinish = async (values: { username: string; password: string }) => { const onFinish = async (values: { username: string; password: string }) => {
+4 -2
View File
@@ -5,7 +5,7 @@
// 必须在引入 antd 之前 import。 // 必须在引入 antd 之前 import。
import '@ant-design/v5-patch-for-react-19'; import '@ant-design/v5-patch-for-react-19';
import { AntdRegistry } from '@ant-design/nextjs-registry'; import { AntdRegistry } from '@ant-design/nextjs-registry';
import { ConfigProvider } from 'antd'; import { App, ConfigProvider } from 'antd';
import zhCN from 'antd/locale/zh_CN'; import zhCN from 'antd/locale/zh_CN';
export function Providers({ children }: { children: React.ReactNode }) { export function Providers({ children }: { children: React.ReactNode }) {
@@ -23,7 +23,9 @@ export function Providers({ children }: { children: React.ReactNode }) {
}, },
}} }}
> >
{children} {/* antd <App> : message/notification/Modal App.useApp() ConfigProvider
/locale, "Static function can not consume context" () */}
<App>{children}</App>
</ConfigProvider> </ConfigProvider>
</AntdRegistry> </AntdRegistry>
); );
+3 -1
View File
@@ -3,7 +3,7 @@
// 调金币 / 调现金弹窗(受控)。列表页与用户详情页共用,避免两处各写一套。 // 调金币 / 调现金弹窗(受控)。列表页与用户详情页共用,避免两处各写一套。
// 每个弹窗自带「增减 / 设为指定值」两种模式;打开时拉一次 360 概览展示当前余额。 // 每个弹窗自带「增减 / 设为指定值」两种模式;打开时拉一次 360 概览展示当前余额。
import { useEffect, useState } from 'react'; 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 { api, errMsg } from '@/lib/api';
import { yuan } from '@/lib/format'; import { yuan } from '@/lib/format';
import type { UserOverview } from '@/lib/types'; 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) { export function AdjustCoinModal({ user, open, onClose, onDone }: ModalProps) {
const { message } = App.useApp();
const [form] = Form.useForm(); const [form] = Form.useForm();
const [mode, setMode] = useState<'delta' | 'set'>('delta'); const [mode, setMode] = useState<'delta' | 'set'>('delta');
const balances = useBalances(user?.id, open); 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) { export function AdjustCashModal({ user, open, onClose, onDone }: ModalProps) {
const { message } = App.useApp();
const [form] = Form.useForm(); const [form] = Form.useForm();
const [mode, setMode] = useState<'delta' | 'set'>('delta'); const [mode, setMode] = useState<'delta' | 'set'>('delta');
const balances = useBalances(user?.id, open); const balances = useBalances(user?.id, open);