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:
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user