feat(admin): 用户体验体系化提升与高危缺陷修复

UX 缺陷修复:
- 校验失败不再卡死弹窗按钮(Users/Roles/Bills)
- 押金收取/批量收取/添加分期防重复提交;切换房型重置勾选
- AI 表单/批量确认不再出现"假成功"
- Dashboard 各数据模块独立加载,单接口失败不再整页清零
- 房间可视化加载失败显示错误态而非永久转圈
- 学生编辑表单回填前重置,避免字段残留污染
- 覆盖式导入增加二次确认;恢复默认考勤时段确认并同步表单
- 金数据匹配关闭前确认,同步中禁止误关

体验提升:
- 新增统一 QueryErrorState/QueryEmpty,20+ 页面加载失败显示错误态与重试
- 全局 ErrorBoundary + RouteKeeper 逐页兜底
- 新增 usePageVisible/useVisibleRefetch,保活页面切回自动刷新数据
- 新增首次登录角色引导 RoleTour 与业务闭环 NextStepHint 引导卡
- 重构 A2UI:useSubmissionState/useXCardSurface 收敛状态与命令生命周期,
  ArtifactErrorBoundary 渲染降级,图表空数据占位
- AI 助手欢迎语与建议话术按角色定制,会话列表空态引导
- 更新 a2ui-contract.md 契约文档说明实现现状
This commit is contained in:
2026-08-07 17:23:23 +08:00
parent db0d972633
commit 67435e46ca
54 changed files with 2298 additions and 1177 deletions

View File

@@ -23,6 +23,8 @@ import api from '../../api';
import PermissionButton from '../../components/PermissionButton';
import { message } from '../../ui/app-message';
import { newOperationId } from '../../utils/operation-id';
import { QueryErrorState } from '../../components/QueryState';
import { useVisibleRefetch } from '../../hooks/usePageVisible';
interface WalletRow {
studentId: number;
@@ -54,7 +56,6 @@ const WalletsPage: React.FC = () => {
const [debtOnly, setDebtOnly] = useState(false);
const [roomType, setRoomType] = useState<string | undefined>();
const [selected, setSelected] = useState<WalletRow | null>(null);
const [transactions, setTransactions] = useState<WalletTransaction[]>([]);
const [drawerOpen, setDrawerOpen] = useState(false);
const [form] = Form.useForm();
const [batchForm] = Form.useForm();
@@ -66,21 +67,17 @@ const WalletsPage: React.FC = () => {
data: rows = [],
isLoading,
isFetching,
isError,
refetch,
} = useQuery<WalletRow[]>({
queryKey: ['wallets', keyword, debtOnly, roomType],
queryFn: async () => {
try {
return validateResponse<WalletRow[]>(
walletsSchema,
await api.get('/wallets', {
params: { keyword: keyword || undefined, debtOnly, roomType },
}),
);
} catch (error: any) {
message.error(error?.message || '加载学生余额失败');
return [];
}
return validateResponse<WalletRow[]>(
walletsSchema,
await api.get('/wallets', {
params: { keyword: keyword || undefined, debtOnly, roomType },
}),
);
},
});
const { data: roomTypes = [] } = useQuery<string[]>({
@@ -98,6 +95,27 @@ const WalletsPage: React.FC = () => {
},
});
const loading = isLoading || isFetching;
// RouteKeeper 保活页面切回时刷新余额,避免费用/账单操作后数据陈旧
useVisibleRefetch(['wallets']);
const selectedStudentId = selected?.studentId;
const {
data: transactions = [],
isLoading: txLoading,
isFetching: txFetching,
isError: txError,
refetch: refetchTransactions,
} = useQuery<WalletTransaction[]>({
queryKey: ['wallets', 'transactions', selectedStudentId],
enabled: drawerOpen && !!selectedStudentId,
queryFn: async () => {
return (
(await api.get<WalletTransaction[]>('/wallets/transactions', {
params: { studentId: selectedStudentId },
})) || []
);
},
});
const fetchRows = useCallback(() => refetch(), [refetch]);
const changeMutation = useApiMutation(
@@ -193,21 +211,11 @@ const WalletsPage: React.FC = () => {
};
const showTransactions = useCallback(
async (row: WalletRow) => {
(row: WalletRow) => {
setSelected(row);
setDrawerOpen(true);
try {
setTransactions(
await api.get<WalletTransaction[]>('/wallets/transactions', {
params: { studentId: row.studentId },
}),
);
} catch (error: unknown) {
console.error('加载余额流水失败', error);
message.error('加载流水失败');
}
},
[setSelected, setDrawerOpen, setTransactions],
[setSelected, setDrawerOpen],
);
const columns = useMemo(
@@ -310,19 +318,27 @@ const WalletsPage: React.FC = () => {
</Button>
</Space>
</div>
<Table
rowKey="studentId"
loading={loading}
dataSource={rows}
columns={columns}
rowSelection={{ selectedRowKeys, onChange: setSelectedRowKeys }}
pagination={{
defaultPageSize: 15,
showSizeChanger: true,
pageSizeOptions: [15, 30, 50, 100],
showTotal: (total) => `${total}`,
}}
/>
{isError ? (
<QueryErrorState
title="钱包数据加载失败"
description="请检查网络后重试。"
onRetry={() => void refetch()}
/>
) : (
<Table
rowKey="studentId"
loading={loading}
dataSource={rows}
columns={columns}
rowSelection={{ selectedRowKeys, onChange: setSelectedRowKeys }}
pagination={{
defaultPageSize: 15,
showSizeChanger: true,
pageSizeOptions: [15, 30, 50, 100],
showTotal: (total) => `${total}`,
}}
/>
)}
<Modal
title={`${selected?.studentName || ''} - 余额操作`}
open={!!selected && !drawerOpen}
@@ -400,43 +416,53 @@ const WalletsPage: React.FC = () => {
setSelected(null);
}}
>
<Table
rowKey="id"
dataSource={transactions}
pagination={{ pageSize: 10 }}
columns={[
{
title: '时间',
dataIndex: 'createdAt',
render: (value: string) => dayjs(value).format('YYYY-MM-DD HH:mm'),
},
{
title: '类型',
dataIndex: 'type',
render: (value: string) => transactionNames[value] || value,
},
{
title: '金额',
dataIndex: 'amount',
render: (value: number) => (
<span style={{ color: value >= 0 ? '#389e0d' : '#cf1322' }}>
{value >= 0 ? '+' : ''}¥{value.toFixed(2)}
</span>
),
},
{
title: '变动后余额',
dataIndex: 'balanceAfter',
render: (value: number) => `¥${value.toFixed(2)}`,
},
{
title: '关联账单',
dataIndex: 'billId',
render: (value: number) => (value ? `#${value}` : '-'),
},
{ title: '说明', dataIndex: 'description' },
]}
/>
{txError ? (
<QueryErrorState
compact
title="流水加载失败"
description="请检查网络后重试。"
onRetry={() => void refetchTransactions()}
/>
) : (
<Table
rowKey="id"
dataSource={transactions}
loading={txLoading || txFetching}
pagination={{ pageSize: 10 }}
columns={[
{
title: '时间',
dataIndex: 'createdAt',
render: (value: string) => dayjs(value).format('YYYY-MM-DD HH:mm'),
},
{
title: '类型',
dataIndex: 'type',
render: (value: string) => transactionNames[value] || value,
},
{
title: '额',
dataIndex: 'amount',
render: (value: number) => (
<span style={{ color: value >= 0 ? '#389e0d' : '#cf1322' }}>
{value >= 0 ? '+' : ''}¥{value.toFixed(2)}
</span>
),
},
{
title: '变动后余额',
dataIndex: 'balanceAfter',
render: (value: number) => `¥${value.toFixed(2)}`,
},
{
title: '关联账单',
dataIndex: 'billId',
render: (value: number) => (value ? `#${value}` : '-'),
},
{ title: '说明', dataIndex: 'description' },
]}
/>
)}
</Drawer>
</div>
);