feat(dashboard): 首页三数共享保存+智能校验+保底;轮播种子批量操作 (#7)

- HomeStatsConfig: 三指标共享保存/立即更新;智能关系校验弹窗;真实值保底;初始基数护栏提示;saveAll 半失败合并
- HomeMarqueeSeeds: 勾选 + 批量启用/停用/删除
- 附带并行会话 WIP:users 页

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: OuYingJun1024 <1034284404@qq.com>
Reviewed-on: #7
Co-authored-by: ouzhou <ouzhou@wonderable.ai>
Co-committed-by: ouzhou <ouzhou@wonderable.ai>
This commit was merged in pull request #7.
This commit is contained in:
ouzhou
2026-06-10 22:26:36 +08:00
committed by marco
parent 8c7e2b0220
commit 1717ee8b6a
3 changed files with 353 additions and 90 deletions
+67 -3
View File
@@ -18,9 +18,10 @@ import {
import { api, errMsg } from '@/lib/api';
import { canDo } from '@/lib/auth';
import { useCursorList } from '@/lib/useCursorList';
import type { UserListItem } from '@/lib/types';
import type { UserListItem, UserOverview } from '@/lib/types';
const STATUS_COLOR: Record<string, string> = { active: 'green', disabled: 'red', deleted: 'default' };
const yuan = (c: number) => `¥${(c / 100).toFixed(2)}`;
export default function UsersPage() {
const router = useRouter();
@@ -37,6 +38,20 @@ export default function UsersPage() {
const [coinUser, setCoinUser] = useState<UserListItem | null>(null);
const [coinForm] = Form.useForm();
const [cashUser, setCashUser] = useState<UserListItem | null>(null);
const [cashForm] = Form.useForm();
// 弹窗里展示该用户当前余额:列表行本身不带余额,开弹窗时拉一次 360 概览
const [balances, setBalances] = useState<{ coin: number; cashCents: number } | null>(null);
const openAdjust = (u: UserListItem, kind: 'coin' | 'cash') => {
setBalances(null);
if (kind === 'coin') setCoinUser(u);
else setCashUser(u);
api
.get<UserOverview>(`/admin/api/users/${u.id}`)
.then((r) => setBalances({ coin: r.data.coin_balance, cashCents: r.data.cash_balance_cents }))
.catch(() => {});
};
const search = () => setFilters({ phone: phone || undefined, status });
@@ -52,6 +67,27 @@ export default function UsersPage() {
}
};
// 发/扣现金:输入元,×100 转分后调后端(主要给无现金用户发钱直接测试提现)
const submitCash = async () => {
const v = await cashForm.validateFields();
const amountCents = Math.round(Number(v.amount_yuan) * 100);
if (amountCents === 0) {
message.warning('金额不能为 0(且不小于 1 分)');
return;
}
try {
await api.post(`/admin/api/users/${cashUser!.id}/cash`, {
amount_cents: amountCents,
reason: v.reason,
});
message.success(`${amountCents > 0 ? '发放' : '扣减'} ¥${(Math.abs(amountCents) / 100).toFixed(2)} 现金`);
setCashUser(null);
cashForm.resetFields();
} catch (e) {
message.error(errMsg(e));
}
};
const toggleStatus = (u: UserListItem) => {
const next = u.status === 'active' ? 'disabled' : 'active';
Modal.confirm({
@@ -109,7 +145,8 @@ export default function UsersPage() {
render: (_: unknown, u: UserListItem) => (
<Space>
<a onClick={() => router.push(`/users/${u.id}`)}></a>
{canCoins && <a onClick={() => setCoinUser(u)}></a>}
{canCoins && <a onClick={() => openAdjust(u, 'coin')}></a>}
{canCoins && <a onClick={() => openAdjust(u, 'cash')}></a>}
{canStatus && u.status !== 'deleted' && (
<a onClick={() => toggleStatus(u)}>{u.status === 'active' ? '封禁' : '解封'}</a>
)}
@@ -163,8 +200,11 @@ export default function UsersPage() {
open={!!coinUser}
onOk={submitCoins}
onCancel={() => setCoinUser(null)}
destroyOnClose
destroyOnHidden
>
<p style={{ color: '#888', marginBottom: 12 }}>
当前余额:金币 {balances ? balances.coin : '…'} {balances ? yuan(balances.cashCents) : '…'}
</p>
<Form form={coinForm} layout="vertical">
<Form.Item name="amount" label="金币变动(正=增加,负=扣减)" rules={[{ required: true }]}>
<InputNumber style={{ width: '100%' }} />
@@ -174,6 +214,30 @@ export default function UsersPage() {
</Form.Item>
</Form>
</Modal>
<Modal
title={`调现金 - ${cashUser?.phone ?? ''}`}
open={!!cashUser}
onOk={submitCash}
onCancel={() => setCashUser(null)}
destroyOnHidden
>
<p style={{ color: '#888', marginBottom: 12 }}>
当前余额:金币 {balances ? balances.coin : '…'} {balances ? yuan(balances.cashCents) : '…'}
</p>
<Form form={cashForm} layout="vertical">
<Form.Item
name="amount_yuan"
label="现金变动(元,正=发放,负=扣减;不可扣成负)"
rules={[{ required: true }]}
>
<InputNumber style={{ width: '100%' }} step={0.01} precision={2} addonAfter="元" />
</Form.Item>
<Form.Item name="reason" label="原因(入审计)" rules={[{ required: true }]}>
<Input.TextArea rows={2} />
</Form.Item>
</Form>
</Modal>
</div>
);
}