feat: 实现设置页面 + 路由系统 + JWT认证体系 + 公告优化 + 错误类型系统
## 新增功能 ### 设置页面(Router + Modal 叠加) - HashRouter 路由系统,Electron file:// 协议兼容 - 设置页以居中 Modal 叠加首页,仅 X 按钮关闭 - 主题切换 Segmented 控件(与首页 Switch 共享 Context) - 大模型产物存储仓库路径配置(原生文件夹选择对话框) - 企业版额外:团队创作存储仓库路径 - 系统信息展示(平台/版本/版本类型/环境) - 版本更新检查(状态机:idle→checking→no-update/available→downloading→downloaded→error) ### JWT 认证体系 - 双 Token 机制:access_token + refresh_token - Axios 拦截器 401 自动刷新 + 并发请求去重队列 - 启动时 refresh_token 静默续期(自动登录) - 记住密码:Base64 编码存储,表单自动回填 - setTokenRefreshHandler 解耦模式:request.ts 不知晓 auth ### 自定义错误类型系统 - RefreshError 继承 RequestError,type = AUTH_EXPIRED - 全局拦截器捕获 RefreshError → 自动 clearToken + AUTH_REQUIRED - 任何位置 throw new RefreshError() 即可触发登录 Modal ### 公告卡片优化 - 长文本 Tooltip 悬浮预览 - 点击卡片弹出详情 Modal(全文保留换行) ### SettingsContext(类似 Vue Pinia) - 集中式设置状态管理 - localStorage 持久化:ele-heixiu-output-path / ele-heixiu-team-repo-path - useSettings() 全局消费 ## 架构变更 - App.tsx → HashRouter + AppRoutes + LoginPage 弹层 - Layout.tsx = NavBar + <Outlet /> 页面壳 - HomePage.tsx 从 App.tsx 提取 - NavBar 使用 useNavigate() 真实路由导航 - useUpdater 改为模块级单例 IPC 监听器(修复 MaxListenersExceededWarning) - electron/main.ts 新增 FILE_DIALOG IPC handler ## 依赖 - 新增 react-router-dom
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
// ============================================================
|
||||
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { Drawer, List, Typography, Empty, Tag, Spin } from 'antd';
|
||||
import { Drawer, Card, Typography, Empty, Tag, Spin, Modal, Tooltip } from 'antd';
|
||||
import { NotificationOutlined } from '@ant-design/icons';
|
||||
|
||||
import { fetchAnnouncements, type Announcement } from '@/services/modules';
|
||||
@@ -29,13 +29,15 @@ const typeLabelMap: Record<string, string> = {
|
||||
maintenance: '维护',
|
||||
};
|
||||
|
||||
/** 格式化日期(YYYY-MM-DD) */
|
||||
/** 格式化日期(YYYY-MM-DD HH:mm) */
|
||||
function formatDate(iso: string): string {
|
||||
try {
|
||||
return new Date(iso).toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
} catch {
|
||||
return iso;
|
||||
@@ -54,6 +56,9 @@ export function AnnouncementDrawer({ open, onClose }: AnnouncementDrawerProps) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// 详情 Modal
|
||||
const [detail, setDetail] = useState<Announcement | null>(null);
|
||||
|
||||
// 打开抽屉时请求公告列表
|
||||
const loadAnnouncements = useCallback(async () => {
|
||||
setLoading(true);
|
||||
@@ -79,69 +84,121 @@ export function AnnouncementDrawer({ open, onClose }: AnnouncementDrawerProps) {
|
||||
// ---- 渲染 ----
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
title={
|
||||
<span className="flex items-center gap-2">
|
||||
<NotificationOutlined />
|
||||
公告
|
||||
</span>
|
||||
}
|
||||
placement="right"
|
||||
size={380}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
styles={{ body: { padding: 0 } }}
|
||||
>
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<Spin description="加载中..." />
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<Empty description={error} />
|
||||
</div>
|
||||
) : list.length === 0 ? (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<Empty description="暂无公告" />
|
||||
</div>
|
||||
) : (
|
||||
<List
|
||||
dataSource={list}
|
||||
renderItem={(item) => (
|
||||
<List.Item className="px-6! py-4!">
|
||||
<div className="w-full space-y-2">
|
||||
{/* 标题 + 类型标签 */}
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<Text strong className="text-sm flex-1" ellipsis>
|
||||
{item.pinned && '📌 '}
|
||||
{item.title}
|
||||
</Text>
|
||||
<Tag
|
||||
color={typeColorMap[item.type] || 'default'}
|
||||
className="m-0! text-xs shrink-0"
|
||||
>
|
||||
{typeLabelMap[item.type] || item.type}
|
||||
</Tag>
|
||||
</div>
|
||||
|
||||
{/* 内容 */}
|
||||
<Paragraph
|
||||
className="mb-0! text-xs"
|
||||
type="secondary"
|
||||
ellipsis={{ rows: 2 }}
|
||||
<>
|
||||
<Drawer
|
||||
title={
|
||||
<span className="flex items-center gap-2">
|
||||
<NotificationOutlined />
|
||||
公告
|
||||
</span>
|
||||
}
|
||||
placement="right"
|
||||
size={380}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
styles={{ body: { padding: 0 } }}
|
||||
>
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<Spin description="加载中..." />
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<Empty description={error} />
|
||||
</div>
|
||||
) : list.length === 0 ? (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<Empty description="暂无公告" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-3 p-3">
|
||||
{list.map((item) => (
|
||||
<Tooltip
|
||||
key={item.id}
|
||||
title={
|
||||
item.content.length > 100
|
||||
? item.content.slice(0, 200) + '…'
|
||||
: undefined
|
||||
}
|
||||
placement="left"
|
||||
>
|
||||
<Card
|
||||
size="small"
|
||||
className="rounded-md! border-0! shadow-sm! cursor-pointer hover:shadow-md! transition-shadow"
|
||||
title={
|
||||
<Text strong className="text-sm" ellipsis>
|
||||
{item.pinned && '📌 '}
|
||||
{item.title}
|
||||
</Text>
|
||||
}
|
||||
extra={
|
||||
<Tag
|
||||
color={typeColorMap[item.type] || 'default'}
|
||||
className="m-0! text-xs"
|
||||
>
|
||||
{typeLabelMap[item.type] || item.type}
|
||||
</Tag>
|
||||
}
|
||||
onClick={() => setDetail(item)}
|
||||
>
|
||||
{item.content}
|
||||
</Paragraph>
|
||||
<Paragraph
|
||||
className="mb-2! text-xs"
|
||||
type="secondary"
|
||||
ellipsis={{ rows: 2 }}
|
||||
>
|
||||
{item.content}
|
||||
</Paragraph>
|
||||
<Text type="secondary" className="text-xs">
|
||||
{formatDate(item.created_at)}
|
||||
</Text>
|
||||
</Card>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Drawer>
|
||||
|
||||
{/* 日期 */}
|
||||
<Text type="secondary" className="text-xs">
|
||||
{formatDate(item.created_at)}
|
||||
</Text>
|
||||
</div>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</Drawer>
|
||||
{/* ======== 公告详情 Modal ======== */}
|
||||
<Modal
|
||||
open={!!detail}
|
||||
onCancel={() => setDetail(null)}
|
||||
footer={null}
|
||||
width={480}
|
||||
centered
|
||||
title={
|
||||
detail && (
|
||||
<span className="flex items-center gap-2">
|
||||
{detail.pinned && '📌 '}
|
||||
{detail.title}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
>
|
||||
{detail && (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Tag
|
||||
color={typeColorMap[detail.type] || 'default'}
|
||||
className="m-0! text-xs"
|
||||
>
|
||||
{typeLabelMap[detail.type] || detail.type}
|
||||
</Tag>
|
||||
<Text type="secondary" className="text-xs">
|
||||
{formatDate(detail.created_at)}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="px-1 py-2 rounded-md"
|
||||
style={{
|
||||
background: 'var(--color-fill-tertiary, rgba(0,0,0,0.04))',
|
||||
}}
|
||||
>
|
||||
<Paragraph className="mb-0! text-sm leading-relaxed whitespace-pre-wrap">
|
||||
{detail.content}
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user