fix(admin): 通知分页、登录过期提示、附件打开反馈与导出统一 loading

- 通知中心改为游标分页 + 加载更多,历史通知不再被 50 条上限截断;
  加载失败显示错误态与重试
- AI 流式请求 401 被登出时,登录页提示"登录已过期",不再无声踢出
- AI 附件/引用来源打开失败时给出明确错误提示,不再"点了没反应"
- 新增 useDownload hook:统一导出/下载的防重复、loading 与成功/失败反馈;
  接入学生/账单/房间/入住/费用五个页面的模板下载与导出按钮
- 学生页移除不检查响应状态的私有下载实现,统一走 downloadBlob
This commit is contained in:
2026-08-07 17:33:58 +08:00
parent 67435e46ca
commit 00e2bc5acf
14 changed files with 320 additions and 153 deletions

View File

@@ -13,6 +13,7 @@ import type { ThoughtChainItemType } from '@ant-design/x';
import XMarkdown, { type ComponentProps } from '@ant-design/x-markdown';
import { Alert, Button, Flex, Input, Space, Typography } from 'antd';
import { useUserStore } from '../../store/user/userStore';
import { message } from '../../ui/app-message';
import { DynamicChart } from './DynamicChart';
import { DynamicForm } from './DynamicForm';
import { DynamicReview } from './DynamicReview';
@@ -104,6 +105,24 @@ async function openSourceUrl(item: { url?: string }): Promise<void> {
window.setTimeout(() => URL.revokeObjectURL(objectUrl), 60_000);
}
/** 打开附件,失败时给出明确提示(避免「点了没反应」) */
async function handleOpenAttachment(attachment: AiAttachment): Promise<void> {
try {
await openAttachment(attachment);
} catch (error: unknown) {
message.error(error instanceof Error ? error.message : '附件打开失败,请重试');
}
}
/** 打出来源链接,失败时给出明确提示 */
async function handleOpenSource(item: { url?: string }): Promise<void> {
try {
await openSourceUrl(item);
} catch (error: unknown) {
message.error(error instanceof Error ? error.message : '来源打开失败,请重试');
}
}
function ToolChain({ tools }: { tools: AiToolRun[] }) {
const items = useMemo<ThoughtChainItemType[]>(
() =>
@@ -228,7 +247,7 @@ export const AiMessageContent: React.FC<AiMessageContentProps> = ({
byte={attachment.size}
size="small"
icon={attachmentIcon(attachment)}
onClick={() => void openAttachment(attachment)}
onClick={() => void handleOpenAttachment(attachment)}
/>
));
@@ -352,7 +371,7 @@ export const AiMessageContent: React.FC<AiMessageContentProps> = ({
<Sources
items={sourceItems}
title="引用来源"
onClick={(item) => void openSourceUrl(item as { url?: string })}
onClick={(item) => void handleOpenSource(item as { url?: string })}
/>
)}
{(message.forms ?? []).map((form) => (

View File

@@ -80,6 +80,8 @@ export async function authenticatedFetch(
}
const response = await fetch(requestInput, { ...requestInit, headers });
if (response.status === 401) {
// 提示由登录页读取展示:直接弹 toast 会被跳转销毁
sessionStorage.setItem('login_expired_hint', '1');
useUserStore.getState().logout();
usePermissionStore.getState().clearPermissions();
window.location.href = '/login';