style: [FL-166] 优化AI问答界面样式,与系统风格保持一致

主要改动:
- 使用标准的 Card 组件替代自定义布局
- 采用 Row/Col 响应式布局,适配移动端
- 使用系统统一的 Space、Typography 组件
- 添加 useToastFeedback 统一错误提示
- 使用 useMobileDetection 实现响应式设计
- 优化消息显示,使用系统配色和边距
- 添加 Popconfirm 二次确认删除操作
- 调整页面布局,移除全屏高度容器
- 统一按钮、图标和文字样式

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
chengkai3
2026-06-20 23:51:54 +08:00
parent 483fdb982b
commit ccb796bc02
+265 -140
View File
@@ -4,25 +4,33 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
Button,
Card,
Col,
Empty,
Form,
Input,
List,
Modal,
Popconfirm,
Row,
Space,
Spin,
Tag,
Typography,
message,
type CardProps,
} from "antd";
import {
PlusOutlined,
SendOutlined,
DeleteOutlined,
EditOutlined,
RobotOutlined,
MessageOutlined,
} from "@ant-design/icons";
import { useCallback, useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useRef, useState, type ComponentType, type RefAttributes } from "react";
import { useAuth } from "@/components/auth-provider";
import { useToastFeedback } from "@/hooks/use-toast-feedback";
import { useMobileDetection } from "@/hooks/use-mobile-detection";
import { readApiError, API_BASE_URL } from "@/lib/api";
import type {
AiChatConversation,
@@ -32,18 +40,30 @@ import type {
} from "@/types/ai-chat";
const { TextArea } = Input;
const { Title, Text } = Typography;
const { Title, Text, Paragraph } = Typography;
const AntCard = Card as unknown as ComponentType<CardProps & RefAttributes<HTMLDivElement>>;
export default function AiChatPage() {
const { fetchWithAuth } = useAuth();
const { user, initializing, fetchWithAuth } = useAuth();
const queryClient = useQueryClient();
const isMobile = useMobileDetection();
const [selectedConvId, setSelectedConvId] = useState<number | null>(null);
const [messageInput, setMessageInput] = useState("");
const [createModalOpen, setCreateModalOpen] = useState(false);
const [newConvTitle, setNewConvTitle] = useState("新对话");
const [error, setError] = useState("");
const [success, setSuccess] = useState("");
const messagesEndRef = useRef<HTMLDivElement>(null);
useToastFeedback({
errorMessage: error,
successMessage: success,
errorTitle: "操作失败",
clearError: () => setError(""),
clearSuccess: () => setSuccess(""),
});
const { data: conversations, isLoading: convLoading } = useQuery({
queryKey: ["ai-chat-conversations"],
queryFn: async () => {
@@ -55,6 +75,7 @@ export default function AiChatPage() {
}
return (await response.json()) as AiChatConversationListResponse;
},
enabled: !!user,
});
const { data: currentConv, isLoading: convDetailLoading } = useQuery({
@@ -92,10 +113,10 @@ export default function AiChatPage() {
setSelectedConvId(data.id);
setCreateModalOpen(false);
setNewConvTitle("新对话");
message.success("创建对话成功");
setSuccess("创建对话成功");
},
onError: (error: Error) => {
message.error(`创建对话失败: ${error.message}`);
onError: (err: Error) => {
setError(`创建对话失败: ${err.message}`);
},
});
@@ -112,10 +133,10 @@ export default function AiChatPage() {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["ai-chat-conversations"] });
setSelectedConvId(null);
message.success("删除对话成功");
setSuccess("删除对话成功");
},
onError: (error: Error) => {
message.error(`删除对话失败: ${error.message}`);
onError: (err: Error) => {
setError(`删除对话失败: ${err.message}`);
},
});
@@ -146,8 +167,8 @@ export default function AiChatPage() {
});
setMessageInput("");
},
onError: (error: Error) => {
message.error(`发送消息失败: ${error.message}`);
onError: (err: Error) => {
setError(`发送消息失败: ${err.message}`);
},
});
@@ -163,37 +184,58 @@ export default function AiChatPage() {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [currentConv?.messages]);
if (initializing) {
return (
<Space direction="vertical" size="large" style={{ width: "100%", padding: 24 }}>
<Spin size="large" />
</Space>
);
}
return (
<div style={{ height: "100vh", display: "flex", flexDirection: "column" }}>
<div style={{ padding: "16px", borderBottom: "1px solid #f0f0f0" }}>
<Title level={3} style={{ margin: 0 }}>
<RobotOutlined /> AI
</Title>
</div>
<Space direction="vertical" size="large" style={{ width: "100%", padding: isMobile ? 16 : 24 }}>
<Row gutter={16}>
<Col span={24}>
<Title level={3}>
<RobotOutlined /> AI
</Title>
<Paragraph type="secondary">
ChatGPT
</Paragraph>
</Col>
</Row>
<div style={{ flex: 1, display: "flex", overflow: "hidden" }}>
<div
style={{
width: "280px",
borderRight: "1px solid #f0f0f0",
display: "flex",
flexDirection: "column",
}}
>
<div style={{ padding: "16px" }}>
<Button
type="primary"
icon={<PlusOutlined />}
block
onClick={() => setCreateModalOpen(true)}
>
</Button>
</div>
<div style={{ flex: 1, overflow: "auto", padding: "0 16px" }}>
<Row gutter={[16, 16]}>
<Col xs={24} md={8}>
<AntCard
title={
<Space>
<MessageOutlined />
<span></span>
</Space>
}
extra={
<Button
type="primary"
size="small"
icon={<PlusOutlined />}
onClick={() => setCreateModalOpen(true)}
>
</Button>
}
styles={{ body: { padding: 0, maxHeight: 600, overflowY: "auto" } }}
>
{convLoading ? (
<Spin />
<div style={{ textAlign: "center", padding: 40 }}>
<Spin />
</div>
) : conversations?.items.length === 0 ? (
<Empty
description="暂无对话"
style={{ padding: 40 }}
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
) : (
<List
dataSource={conversations?.items || []}
@@ -204,115 +246,194 @@ export default function AiChatPage() {
style={{
cursor: "pointer",
background:
selectedConvId === conv.id ? "#e6f7ff" : "transparent",
padding: "8px",
borderRadius: "4px",
marginBottom: "8px",
selectedConvId === conv.id
? "var(--ant-color-primary-bg)"
: "transparent",
padding: "12px 16px",
}}
actions={[
<Popconfirm
key="delete"
title="确认删除"
description="删除后将无法恢复,确定删除此对话吗?"
onConfirm={(e) => {
e?.stopPropagation();
deleteConvMutation.mutate(conv.id);
}}
okText="确定"
cancelText="取消"
>
<Button
danger
size="small"
type="text"
icon={<DeleteOutlined />}
onClick={(e) => e.stopPropagation()}
/>
</Popconfirm>,
]}
>
<List.Item.Meta
title={conv.title}
description={`${conv.message_count || 0} 条消息`}
/>
<Button
danger
size="small"
icon={<DeleteOutlined />}
onClick={(e) => {
e.stopPropagation();
deleteConvMutation.mutate(conv.id);
}}
description={
<Space size={4}>
<Tag color="blue">{conv.message_count || 0} </Tag>
<Text type="secondary" style={{ fontSize: 12 }}>
{new Date(conv.updated_at).toLocaleString("zh-CN", {
month: "numeric",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
})}
</Text>
</Space>
}
/>
</List.Item>
)}
/>
)}
</div>
</div>
</AntCard>
</Col>
<div style={{ flex: 1, display: "flex", flexDirection: "column" }}>
{!selectedConvId ? (
<Empty
description="请选择或创建一个对话"
style={{ marginTop: "100px" }}
/>
) : (
<>
<div
style={{
flex: 1,
overflow: "auto",
padding: "24px",
background: "#fafafa",
}}
>
{convDetailLoading ? (
<Spin />
) : (
<Space direction="vertical" style={{ width: "100%" }} size="large">
{currentConv?.messages?.map((msg) => (
<Card
key={msg.id}
style={{
maxWidth: "80%",
marginLeft: msg.role === "user" ? "auto" : 0,
background: msg.role === "user" ? "#1890ff" : "#fff",
color: msg.role === "user" ? "#fff" : "#000",
}}
>
<Text
strong
<Col xs={24} md={16}>
<AntCard
title={currentConv ? currentConv.title : "AI 对话"}
styles={{
body: {
padding: 0,
display: "flex",
flexDirection: "column",
height: 600,
},
}}
>
{!selectedConvId ? (
<Empty
description="请选择或创建一个对话开始聊天"
style={{ margin: "auto" }}
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
) : (
<>
<div
style={{
flex: 1,
overflowY: "auto",
padding: 16,
background: "var(--ant-color-bg-layout)",
}}
>
{convDetailLoading ? (
<div style={{ textAlign: "center", padding: 40 }}>
<Spin />
</div>
) : (
<Space direction="vertical" style={{ width: "100%" }} size={12}>
{currentConv?.messages?.map((msg) => (
<div
key={msg.id}
style={{
color: msg.role === "user" ? "#fff" : "#000",
display: "flex",
justifyContent: msg.role === "user" ? "flex-end" : "flex-start",
}}
>
{msg.role === "user" ? "我" : "AI"}
</Text>
<div style={{ marginTop: "8px", whiteSpace: "pre-wrap" }}>
{msg.content}
<Card
size="small"
style={{
maxWidth: "80%",
background:
msg.role === "user"
? "var(--ant-color-primary)"
: "var(--ant-color-bg-container)",
border:
msg.role === "user"
? "1px solid var(--ant-color-primary)"
: undefined,
}}
>
<Space direction="vertical" size={4} style={{ width: "100%" }}>
<Text
strong
style={{
color:
msg.role === "user"
? "var(--ant-color-white)"
: "var(--ant-color-text)",
}}
>
{msg.role === "user" ? "我" : "AI 助手"}
</Text>
<div
style={{
whiteSpace: "pre-wrap",
wordBreak: "break-word",
color:
msg.role === "user"
? "var(--ant-color-white)"
: "var(--ant-color-text)",
}}
>
{msg.content}
</div>
<Text
type="secondary"
style={{
fontSize: 12,
color:
msg.role === "user"
? "rgba(255, 255, 255, 0.65)"
: undefined,
}}
>
{new Date(msg.created_at).toLocaleTimeString("zh-CN")}
</Text>
</Space>
</Card>
</div>
</Card>
))}
<div ref={messagesEndRef} />
</Space>
)}
</div>
))}
<div ref={messagesEndRef} />
</Space>
)}
</div>
<div
style={{
padding: "16px",
borderTop: "1px solid #f0f0f0",
background: "#fff",
}}
>
<Space.Compact style={{ width: "100%" }}>
<TextArea
value={messageInput}
onChange={(e) => setMessageInput(e.target.value)}
placeholder="输入消息..."
autoSize={{ minRows: 1, maxRows: 4 }}
onPressEnter={(e) => {
if (!e.shiftKey) {
e.preventDefault();
handleSendMessage();
}
}}
/>
<Button
type="primary"
icon={<SendOutlined />}
loading={sendMessageMutation.isPending}
onClick={handleSendMessage}
disabled={!messageInput.trim()}
>
</Button>
</Space.Compact>
</div>
</>
)}
</div>
</div>
<div
style={{
padding: 16,
borderTop: "1px solid var(--ant-color-border)",
background: "var(--ant-color-bg-container)",
}}
>
<Space.Compact style={{ width: "100%" }}>
<TextArea
value={messageInput}
onChange={(e) => setMessageInput(e.target.value)}
placeholder="输入消息...Shift + Enter 换行,Enter 发送)"
autoSize={{ minRows: 1, maxRows: 4 }}
onPressEnter={(e) => {
if (!e.shiftKey) {
e.preventDefault();
handleSendMessage();
}
}}
disabled={sendMessageMutation.isPending}
/>
<Button
type="primary"
icon={<SendOutlined />}
loading={sendMessageMutation.isPending}
onClick={handleSendMessage}
disabled={!messageInput.trim()}
>
</Button>
</Space.Compact>
</div>
</>
)}
</AntCard>
</Col>
</Row>
<Modal
title="新建对话"
@@ -324,12 +445,16 @@ export default function AiChatPage() {
}}
confirmLoading={createConvMutation.isPending}
>
<Input
placeholder="对话标题"
value={newConvTitle}
onChange={(e) => setNewConvTitle(e.target.value)}
/>
<Form layout="vertical">
<Form.Item label="对话标题">
<Input
placeholder="请输入对话标题"
value={newConvTitle}
onChange={(e) => setNewConvTitle(e.target.value)}
/>
</Form.Item>
</Form>
</Modal>
</div>
</Space>
);
}