新增 auth/paths 与登录页;HTTP 与流式聊天在 401 时续签,失败则清理会话并跳转登录。 侧栏用户信息区布局修复;用户菜单项间距与验证码发送冷却、antd 发送按钮。 Made-with: Cursor
This commit is contained in:
@@ -1,17 +1,36 @@
|
||||
import type { CSSProperties } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
ArrowUpOutlined,
|
||||
LogoutOutlined,
|
||||
MenuFoldOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
MessageOutlined,
|
||||
MobileOutlined,
|
||||
MoreOutlined,
|
||||
PaperClipOutlined,
|
||||
PlusOutlined,
|
||||
QuestionCircleOutlined,
|
||||
SearchOutlined,
|
||||
SettingOutlined,
|
||||
ThunderboltOutlined,
|
||||
UserOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { Button, Collapse, Drawer, Input, Layout, Select, Typography } from "antd";
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
Collapse,
|
||||
Drawer,
|
||||
Dropdown,
|
||||
Input,
|
||||
Layout,
|
||||
Select,
|
||||
Typography,
|
||||
message,
|
||||
} from "antd";
|
||||
import type { MenuProps } from "antd";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { ACCESS_TOKEN_KEY, SessionExpiredError, USER_KEY, logout } from "../api/auth";
|
||||
import { isAbortLikeError, streamQwenChat, type ChatMessagePayload } from "../api/qwenChat";
|
||||
import StreamMessage from "../components/StreamMessage";
|
||||
|
||||
@@ -25,6 +44,27 @@ type UiMessage = {
|
||||
thinking?: string;
|
||||
};
|
||||
|
||||
function readStoredUserProfile(): { displayName: string; avatar?: string } {
|
||||
try {
|
||||
const raw = window.localStorage.getItem(USER_KEY);
|
||||
if (!raw) return { displayName: "用户" };
|
||||
const u = JSON.parse(raw) as Record<string, unknown>;
|
||||
const displayName =
|
||||
(typeof u.nickname === "string" && u.nickname) ||
|
||||
(typeof u.name === "string" && u.name) ||
|
||||
(typeof u.username === "string" && u.username) ||
|
||||
(typeof u.phone === "string" && u.phone) ||
|
||||
"用户";
|
||||
const avatar =
|
||||
(typeof u.avatar === "string" && u.avatar) ||
|
||||
(typeof u.avatarUrl === "string" && u.avatarUrl) ||
|
||||
undefined;
|
||||
return { displayName: String(displayName), avatar };
|
||||
} catch {
|
||||
return { displayName: "用户" };
|
||||
}
|
||||
}
|
||||
|
||||
const INITIAL_MESSAGES: UiMessage[] = [
|
||||
{
|
||||
id: "init-assistant",
|
||||
@@ -46,6 +86,7 @@ const QWEN_MODEL_OPTIONS: { value: string; label: string }[] = [
|
||||
];
|
||||
|
||||
export default function HomePage() {
|
||||
const navigate = useNavigate();
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
|
||||
const [viewportWidth, setViewportWidth] = useState(0);
|
||||
@@ -67,6 +108,57 @@ export default function HomePage() {
|
||||
[],
|
||||
);
|
||||
|
||||
const userProfile = useMemo(() => readStoredUserProfile(), []);
|
||||
|
||||
/** 用户信息下拉:略增大行高与项间距 */
|
||||
const userMenuItemStyle: CSSProperties = {
|
||||
height: "auto",
|
||||
lineHeight: 1.45,
|
||||
paddingBlock: 10,
|
||||
marginBlock: 3,
|
||||
};
|
||||
const userMenuDividerStyle: CSSProperties = { margin: "10px 0" };
|
||||
|
||||
const userMenuItems: MenuProps["items"] = [
|
||||
{ key: "app", icon: <MobileOutlined />, label: "下载手机应用", style: userMenuItemStyle },
|
||||
{ key: "settings", icon: <SettingOutlined />, label: "系统设置", style: userMenuItemStyle },
|
||||
{
|
||||
key: "help",
|
||||
icon: <QuestionCircleOutlined />,
|
||||
label: "帮助与反馈",
|
||||
style: userMenuItemStyle,
|
||||
},
|
||||
{ type: "divider", style: userMenuDividerStyle },
|
||||
{
|
||||
key: "logout",
|
||||
icon: <LogoutOutlined />,
|
||||
label: "退出登录",
|
||||
danger: true,
|
||||
style: userMenuItemStyle,
|
||||
},
|
||||
];
|
||||
|
||||
const onUserMenuClick: MenuProps["onClick"] = async ({ key }) => {
|
||||
if (key === "logout") {
|
||||
abortRef.current?.abort();
|
||||
setIsSending(false);
|
||||
await logout();
|
||||
message.success("已退出登录");
|
||||
navigate("/login", { replace: true });
|
||||
return;
|
||||
}
|
||||
if (key === "app" || key === "settings" || key === "help") {
|
||||
message.info("功能开发中");
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const accessToken = window.localStorage.getItem(ACCESS_TOKEN_KEY);
|
||||
if (!accessToken) {
|
||||
navigate("/login", { replace: true });
|
||||
}
|
||||
}, [navigate]);
|
||||
|
||||
useEffect(() => {
|
||||
const updateIsMobile = () => {
|
||||
const w = Math.min(
|
||||
@@ -163,6 +255,10 @@ export default function HomePage() {
|
||||
lastError = null;
|
||||
break;
|
||||
} catch (error) {
|
||||
if (error instanceof SessionExpiredError) {
|
||||
lastError = null;
|
||||
break;
|
||||
}
|
||||
lastError = error;
|
||||
const isAbortLike = isAbortLikeError(error);
|
||||
const canRetry = attempt < MAX_STREAM_RETRY;
|
||||
@@ -202,7 +298,7 @@ export default function HomePage() {
|
||||
};
|
||||
|
||||
const sidebarContent = (
|
||||
<div className="flex h-full flex-col bg-[var(--ds-bg-sider)]">
|
||||
<div className="flex min-h-0 w-full min-w-0 flex-1 flex-col bg-[var(--ds-bg-sider)]">
|
||||
<div className="shrink-0 px-3 pt-3 pb-2">
|
||||
<Button
|
||||
block
|
||||
@@ -241,21 +337,33 @@ export default function HomePage() {
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 border-t border-[var(--ds-border)] p-3">
|
||||
<Button
|
||||
type="text"
|
||||
className="h-10 w-full justify-start! rounded-xl! px-2 text-neutral-600 hover:bg-black/5!"
|
||||
icon={<SettingOutlined />}
|
||||
<Dropdown
|
||||
menu={{ items: userMenuItems, onClick: onUserMenuClick }}
|
||||
placement="topLeft"
|
||||
trigger={["click"]}
|
||||
>
|
||||
{(!collapsed || isMobile) && "设置"}
|
||||
</Button>
|
||||
<div className="mt-2 flex items-center gap-2 rounded-xl px-2 py-1.5">
|
||||
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-neutral-200 text-neutral-500">
|
||||
<UserOutlined />
|
||||
</div>
|
||||
{(!collapsed || isMobile) && (
|
||||
<Typography.Text className="truncate text-[13px]">用户</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className={`flex w-full items-center gap-2 rounded-xl bg-neutral-100 px-2 py-2 text-left transition-colors hover:bg-neutral-200/90 ${
|
||||
collapsed && !isMobile ? "justify-center py-2.5" : ""
|
||||
}`}
|
||||
>
|
||||
<Avatar
|
||||
size={36}
|
||||
src={userProfile.avatar}
|
||||
icon={!userProfile.avatar ? <UserOutlined /> : undefined}
|
||||
className="shrink-0 bg-neutral-300"
|
||||
/>
|
||||
{(!collapsed || isMobile) && (
|
||||
<>
|
||||
<span className="min-w-0 flex-1 truncate text-[13px] font-medium text-neutral-800">
|
||||
{userProfile.displayName}
|
||||
</span>
|
||||
<MoreOutlined className="shrink-0 text-neutral-500" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -273,24 +381,26 @@ export default function HomePage() {
|
||||
width={260}
|
||||
collapsedWidth={72}
|
||||
theme="light"
|
||||
className="!min-h-0 !overflow-hidden !bg-[var(--ds-bg-sider)] !border-r !border-[var(--ds-border)]"
|
||||
className="!min-h-0 !h-full !overflow-hidden !bg-[var(--ds-bg-sider)] !border-r !border-[var(--ds-border)]"
|
||||
>
|
||||
<div className="flex h-14 items-center gap-2 border-b border-[var(--ds-border)] px-3">
|
||||
<img
|
||||
src="/logo.png"
|
||||
alt=""
|
||||
width={32}
|
||||
height={32}
|
||||
className="h-8 w-8 shrink-0 object-contain"
|
||||
decoding="async"
|
||||
/>
|
||||
{!collapsed && (
|
||||
<Typography.Title level={5} className="!m-0 !truncate !text-[15px] !font-semibold">
|
||||
ChatOne
|
||||
</Typography.Title>
|
||||
)}
|
||||
<div className="flex h-full min-h-0 min-w-0 flex-col">
|
||||
<div className="flex h-14 shrink-0 items-center gap-2 border-b border-[var(--ds-border)] px-3">
|
||||
<img
|
||||
src="/logo.png"
|
||||
alt=""
|
||||
width={32}
|
||||
height={32}
|
||||
className="h-8 w-8 shrink-0 object-contain"
|
||||
decoding="async"
|
||||
/>
|
||||
{!collapsed && (
|
||||
<Typography.Title level={5} className="!m-0 !truncate !text-[15px] !font-semibold">
|
||||
ChatOne
|
||||
</Typography.Title>
|
||||
)}
|
||||
</div>
|
||||
{sidebarContent}
|
||||
</div>
|
||||
{sidebarContent}
|
||||
</Sider>
|
||||
)}
|
||||
|
||||
@@ -500,7 +610,16 @@ export default function HomePage() {
|
||||
onClose={() => setMobileSidebarOpen(false)}
|
||||
open={isMobile && mobileSidebarOpen}
|
||||
width={280}
|
||||
styles={{ body: { padding: 0, background: "var(--ds-bg-sider)" } }}
|
||||
styles={{
|
||||
body: {
|
||||
padding: 0,
|
||||
background: "var(--ds-bg-sider)",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: "100%",
|
||||
minHeight: 0,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{sidebarContent}
|
||||
</Drawer>
|
||||
|
||||
Reference in New Issue
Block a user