From f552ba061915449c22b7a3d06821460d223a0623 Mon Sep 17 00:00:00 2001 From: WangDL Date: Fri, 22 May 2026 00:38:56 +0800 Subject: [PATCH] feat: add TaskAssistant page with AI chat + admin layout updates + service layer --- package-lock.json | 2 +- src/App.tsx | 4 +- src/config/menu.tsx | 6 +- src/layouts/AdminLayout.tsx | 3 +- src/pages/Dashboard.tsx | 2 +- src/pages/TaskAssistant.tsx | 203 ++++++++++++++++++++++++++++++++++++ src/routes/index.tsx | 4 +- src/services/admin-api.ts | 10 +- src/services/ai-chat.ts | 16 +++ vite.config.ts | 5 +- 10 files changed, 238 insertions(+), 17 deletions(-) create mode 100644 src/pages/TaskAssistant.tsx create mode 100644 src/services/ai-chat.ts diff --git a/package-lock.json b/package-lock.json index 5c4515e..5a68d7f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2811,7 +2811,7 @@ }, "node_modules/echarts": { "version": "6.1.0", - "resolved": "https://registry.npmmirror.com/echarts/-/echarts-6.1.0.tgz", + "resolved": "https://registry.npmjs.org/echarts/-/echarts-6.1.0.tgz", "integrity": "sha512-q0yaFPggC9FUdsWH4blavRWFmxdrIodbkoKNAjJudAI6CA9gNPxHtV2RcZNEepZVlk4yvBYkOkbk6HIVpIyHZA==", "license": "Apache-2.0", "dependencies": { diff --git a/src/App.tsx b/src/App.tsx index 6d860ac..98e9702 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import { Suspense, lazy } from 'react' +import { Suspense, lazy } from 'react' import { BrowserRouter, Routes, Route } from 'react-router-dom' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { ConfigProvider } from 'antd' @@ -12,6 +12,7 @@ import AdminLayout from './layouts/AdminLayout' const Login = lazy(() => import('./pages/Login')) const Dashboard = lazy(() => import('./pages/Dashboard')) const UserManagement = lazy(() => import('./pages/UserManagement')) +const TaskAssistant = lazy(() => import('./pages/TaskAssistant')) const Placeholder = lazy(() => import('./pages/Placeholder')) const ForbiddenPage = lazy(() => import('./pages/403')) const NotFoundPage = lazy(() => import('./pages/404')) @@ -39,6 +40,7 @@ function App() { } > } /> + } /> }, + { path: '/assistant', name: '任务助理', icon: }, { path: '/users', name: '用户管理', diff --git a/src/layouts/AdminLayout.tsx b/src/layouts/AdminLayout.tsx index 25d37b7..9dd2c8e 100644 --- a/src/layouts/AdminLayout.tsx +++ b/src/layouts/AdminLayout.tsx @@ -1,4 +1,4 @@ -import { Outlet, useNavigate, useLocation } from 'react-router-dom' +import { Outlet, useNavigate, useLocation } from 'react-router-dom' import { ProLayout } from '@ant-design/pro-components' import { Dropdown, Avatar, Tag, Space, message } from 'antd' import { LogoutOutlined, UserOutlined } from '@ant-design/icons' @@ -9,6 +9,7 @@ import type { AdminRole } from '@/types/admin' const breadcrumbMap: Record = { '/': '总览', + '/assistant': '任务助理', '/users': '用户管理', '/users/admins': '管理员', '/users/members': '普通用户', diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index 85d3246..8228133 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -1,4 +1,4 @@ -import { useMemo } from 'react' +import { useMemo } from 'react' import { Row, Col, Typography } from 'antd' import { useQuery } from '@tanstack/react-query' import ReactEChartsCore from 'echarts-for-react/esm/core' diff --git a/src/pages/TaskAssistant.tsx b/src/pages/TaskAssistant.tsx new file mode 100644 index 0000000..8ebf7a8 --- /dev/null +++ b/src/pages/TaskAssistant.tsx @@ -0,0 +1,203 @@ +import { useState, useRef, useEffect } from 'react' +import { Typography, Input, Button, Space, Avatar, Spin, theme } from 'antd' +import { SendOutlined, RobotOutlined, UserOutlined } from '@ant-design/icons' +import { sendMessage } from '@/services/ai-chat' + +const { Title, Text } = Typography +const { TextArea } = Input + +interface Message { + id: string + role: 'user' | 'assistant' + content: string + timestamp: number +} + +export default function TaskAssistant() { + const [messages, setMessages] = useState([]) + const [input, setInput] = useState('') + const [loading, setLoading] = useState(false) + const messagesEndRef = useRef(null) + const { token } = theme.useToken() + + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }) + }, [messages]) + + const handleSend = async () => { + const text = input.trim() + if (!text || loading) return + + const userMsg: Message = { + id: Date.now().toString(), + role: 'user', + content: text, + timestamp: Date.now(), + } + setMessages(prev => [...prev, userMsg]) + setInput('') + setLoading(true) + + try { + const reply = await sendMessage([...messages, userMsg].map(m => ({ + role: m.role, + content: m.content, + }))) + + const assistantMsg: Message = { + id: (Date.now() + 1).toString(), + role: 'assistant', + content: reply, + timestamp: Date.now(), + } + setMessages(prev => [...prev, assistantMsg]) + } catch (err) { + const errorMsg: Message = { + id: (Date.now() + 1).toString(), + role: 'assistant', + content: '抱歉,请求失败:' + (err instanceof Error ? err.message : '未知错误'), + timestamp: Date.now(), + } + setMessages(prev => [...prev, errorMsg]) + } finally { + setLoading(false) + } + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault() + handleSend() + } + } + + return ( +
+
+ 任务助理 + AI 助手,随时为你提供帮助 +
+ +
+ {messages.length === 0 && ( +
+ + 有什么我可以帮助你的? + + 输入你的问题,AI 助手将为你解答 + +
+ )} + + {messages.map(msg => ( +
+ : } + style={{ + backgroundColor: msg.role === 'user' ? token.colorPrimary : token.colorSuccess, + flexShrink: 0, + }} + /> +
+ {msg.content} +
+
+ ))} + + {loading && ( +
+ } + style={{ backgroundColor: token.colorSuccess, flexShrink: 0 }} + /> +
+ 思考中... +
+
+ )} + +
+
+ +
+ +