重命名前端项目目录:web-flowith -> web

- 将前端项目目录从 web-flowith 重命名为 web,使目录结构更简洁
- 保持所有前端代码和配置文件不变
- 统一项目目录命名规范
This commit is contained in:
2025-07-24 22:20:19 +08:00
parent ca42a7d9a4
commit bbe8fcd776
57 changed files with 0 additions and 0 deletions
+116
View File
@@ -0,0 +1,116 @@
import axios from 'axios'
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import type { ApiResponse } from '@/types'
// 创建axios实例
const api: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
})
// 请求拦截器
api.interceptors.request.use(
(config) => {
// 添加认证token
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
// 添加请求时间戳
config.headers['X-Request-Time'] = Date.now().toString()
return config
},
(error) => {
console.error('Request error:', error)
return Promise.reject(error)
}
)
// 响应拦截器
api.interceptors.response.use(
(response: AxiosResponse<ApiResponse>) => {
const { data } = response
// 检查业务状态码
if (data.code !== 200) {
console.error('API Error:', data.message)
return Promise.reject(new Error(data.message))
}
return response
},
(error) => {
// 处理HTTP错误
if (error.response) {
const { status, data } = error.response
switch (status) {
case 401:
// 未授权,清除token并跳转到登录页
localStorage.removeItem('token')
window.location.href = '/login'
break
case 403:
console.error('Access forbidden')
break
case 404:
console.error('Resource not found')
break
case 500:
console.error('Server error')
break
default:
console.error('HTTP Error:', status, data?.message || error.message)
}
} else if (error.request) {
console.error('Network error:', error.message)
} else {
console.error('Request setup error:', error.message)
}
return Promise.reject(error)
}
)
// 通用请求方法
export const request = {
get: <T = any>(url: string, config?: AxiosRequestConfig): Promise<T> =>
api.get(url, config).then(res => res.data.data),
post: <T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> =>
api.post(url, data, config).then(res => res.data.data),
put: <T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> =>
api.put(url, data, config).then(res => res.data.data),
delete: <T = any>(url: string, config?: AxiosRequestConfig): Promise<T> =>
api.delete(url, config).then(res => res.data.data),
patch: <T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> =>
api.patch(url, data, config).then(res => res.data.data),
}
// 文件上传
export const uploadFile = (file: File, onProgress?: (progress: number) => void): Promise<string> => {
const formData = new FormData()
formData.append('file', file)
return api.post('/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
onUploadProgress: (progressEvent) => {
if (onProgress && progressEvent.total) {
const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total)
onProgress(progress)
}
},
}).then(res => res.data.data.url)
}
export default api
+114
View File
@@ -0,0 +1,114 @@
import request from '@/utils/request'
import type {
LoginRequest,
LoginResponse,
RegisterRequest,
CaptchaResponse,
ApiResponse,
RefreshTokenRequest,
ChangePasswordRequest,
ForgotPasswordRequest,
ResetPasswordRequest,
UserInfo
} from '@/types/auth'
export const authService = {
// 获取验证码
async getCaptcha(): Promise<CaptchaResponse> {
return await request.get('/auth/captcha')
},
// 用户登录
async login(data: LoginRequest): Promise<LoginResponse> {
return await request.post('/auth/login', data)
},
// 用户注册
async register(data: RegisterRequest): Promise<LoginResponse> {
return await request.post('/auth/register', data)
},
// 刷新token
async refreshToken(data: RefreshTokenRequest): Promise<ApiResponse<LoginResponse>> {
const response = await request.post('/auth/refresh-token', data)
return response.data
},
// 用户登出
async logout(): Promise<ApiResponse<void>> {
const response = await request.post('/auth/logout')
return response.data
},
// 获取用户信息
async getUserInfo(): Promise<ApiResponse<UserInfo>> {
const response = await request.get('/auth/user-info')
return response.data
},
// 修改密码
async changePassword(data: ChangePasswordRequest): Promise<ApiResponse<void>> {
const response = await request.post('/auth/change-password', data)
return response.data
},
// 忘记密码
async forgotPassword(data: ForgotPasswordRequest): Promise<ApiResponse<void>> {
return await authApi.post('/forgot-password', data)
},
// 重置密码
async resetPassword(data: ResetPasswordRequest): Promise<ApiResponse<void>> {
return await authApi.post('/reset-password', data)
},
// 验证token有效性
async validateToken(): Promise<ApiResponse<boolean>> {
return await authApi.get('/validate-token')
},
// 检查账号是否存在
async checkAccount(account: string): Promise<ApiResponse<boolean>> {
return await authApi.get(`/check-account?account=${account}`)
}
}
// 工具函数
export const authUtils = {
// 获取token
getToken(): string | null {
return localStorage.getItem('token')
},
// 设置token
setToken(token: string): void {
localStorage.setItem('token', token)
},
// 移除token
removeToken(): void {
localStorage.removeItem('token')
localStorage.removeItem('userInfo')
},
// 获取用户信息
getUserInfo(): UserInfo | null {
const userInfo = localStorage.getItem('userInfo')
return userInfo ? JSON.parse(userInfo) : null
},
// 设置用户信息
setUserInfo(userInfo: UserInfo): void {
localStorage.setItem('userInfo', JSON.stringify(userInfo))
},
// 检查是否已登录
isLoggedIn(): boolean {
return !!this.getToken()
},
// 清除所有认证信息
clearAuth(): void {
this.removeToken()
}
}
+51
View File
@@ -0,0 +1,51 @@
import { request } from './api'
import type { ChatMessage, ChatSession, PaginatedResponse } from '@/types'
export const chatApi = {
// 发送AI聊天消息(REST备用,主用WebSocket
sendAiMessage: (conversationId: string, message: string, userId: string): Promise<any> =>
request.post('/ai/chat', { conversationId, message, userId }),
// 创建会话
createSession: (userId: string, title: string): Promise<ChatSession> =>
request.post('/conversation', { userId, title }),
// 获取会话分页
getSessions: (params: { page: number, size: number, userId?: string }): Promise<PaginatedResponse<ChatSession>> =>
request.get('/conversation/page', { params }),
// 获取用户所有会话
getUserSessions: (userId: string): Promise<ChatSession[]> =>
request.get(`/conversation/user/${userId}`),
// 删除会话
deleteSession: (id: string): Promise<void> =>
request.delete(`/conversation/${id}`),
// 更新会话标题
updateSessionTitle: (id: string, title: string): Promise<ChatSession> =>
request.put(`/conversation/${id}`, { title }),
// 获取会话消息分页
getSessionMessages: (conversationId: string, params: { page: number, size: number }): Promise<PaginatedResponse<ChatMessage>> =>
request.get(`/message/conversation/${conversationId}/page`, { params }),
// 获取会话所有消息
getAllSessionMessages: (conversationId: string): Promise<ChatMessage[]> =>
request.get(`/message/conversation/${conversationId}`),
// 创建消息(保存到数据库)
createMessage: (data: {
conversationId: string,
userId: string,
content: string,
contentType?: string,
senderType?: string,
senderId?: string
}): Promise<ChatMessage> =>
request.post('/message', data),
// 聊天统计
getChatStats: (userId?: string, conversationId?: string): Promise<any> =>
request.get('/ai/stats', { params: { userId, conversationId } }),
}
+374
View File
@@ -0,0 +1,374 @@
import SockJS from 'sockjs-client'
import * as Stomp from 'stompjs'
import type { ChatMessage } from '@/types'
// WebSocket消息类型 - 与后端保持一致
export interface WebSocketMessage {
messageId: string
conversationId?: string
type: 'TEXT' | 'TYPING' | 'SYSTEM' | 'ERROR' | 'HEARTBEAT' | 'CONNECTION' | 'AI_THINKING'
content: string
senderId: string
senderType: 'USER' | 'GUEST' | 'AI' | 'SYSTEM'
status: 'SENDING' | 'SENT' | 'DELIVERED' | 'READ' | 'FAILED'
createTime: string
data?: any
}
// 聊天请求类型 - 与后端ChatRequest保持一致
export interface ChatRequest {
content: string
senderId: string
senderType: 'USER' | 'GUEST' | 'AI' | 'SYSTEM'
messageType: 'TEXT' | 'IMAGE' | 'FILE' | 'SYSTEM' | 'HEARTBEAT'
conversationId?: string
timestamp?: number
}
// 连接请求类型
export interface ConnectRequest {
userId?: string
username?: string
clientType?: string
clientVersion?: string
timestamp?: number
}
// WebSocket连接状态
export type ConnectionStatus = 'CONNECTING' | 'CONNECTED' | 'DISCONNECTED' | 'ERROR'
// 事件回调类型
export interface WebSocketCallbacks {
onMessage?: (message: WebSocketMessage) => void
onConnect?: () => void
onDisconnect?: () => void
onError?: (error: any) => void
onStatusChange?: (status: ConnectionStatus) => void
}
export class WebSocketService {
private client: Stomp.Client | null = null
private callbacks: WebSocketCallbacks = {}
private status: ConnectionStatus = 'DISCONNECTED'
private reconnectAttempts = 0
private maxReconnectAttempts = 5
private reconnectInterval = 3000
private heartbeatTimer: number | null = null
private userId: string | null = null
private conversationId: string | null = null
constructor(private wsUrl: string) {}
/**
* 连接WebSocket
*/
connect(userId?: string, callbacks?: WebSocketCallbacks): Promise<void> {
return new Promise((resolve, reject) => {
try {
this.callbacks = { ...callbacks }
this.userId = userId || `guest_${Date.now()}`
this.setStatus('CONNECTING')
// 创建SockJS连接
const socket = new SockJS(this.wsUrl, null, {
transports: ['websocket', 'xhr-streaming', 'xhr-polling']
})
this.client = Stomp.over(socket)
// 禁用调试日志
this.client.debug = () => {}
// 设置心跳
this.client.heartbeat.outgoing = 20000
this.client.heartbeat.incoming = 20000
// 连接配置 - 添加token支持
const connectHeaders: any = {
'X-User-Id': this.userId
}
// 如果有token,添加到连接头中
const token = localStorage.getItem('token')
if (token) {
connectHeaders['Authorization'] = `Bearer ${token}`
}
this.client.connect(
connectHeaders,
(frame) => {
console.log('WebSocket连接成功:', frame)
this.setStatus('CONNECTED')
this.reconnectAttempts = 0
// 订阅用户消息
this.subscribeToMessages()
// 发送连接消息
this.sendConnectMessage()
// 启动心跳
this.startHeartbeat()
this.callbacks.onConnect?.()
resolve()
},
(error) => {
console.error('WebSocket连接失败:', error)
this.setStatus('ERROR')
// 详细的错误处理
let errorMessage = '连接失败'
if (error) {
if (error.type === 'close') {
switch (error.code) {
case 1006:
errorMessage = '连接异常断开,正在重连...'
break
case 1000:
errorMessage = '连接正常关闭'
break
case 1001:
errorMessage = '服务器正在重启,请稍后重试'
break
case 1002:
errorMessage = '协议错误'
break
case 1003:
errorMessage = '数据格式错误'
break
default:
errorMessage = `连接关闭 (代码: ${error.code})`
}
} else if (error.message) {
errorMessage = error.message
}
}
this.callbacks.onError?.({ ...error, userMessage: errorMessage })
// 尝试重连
this.scheduleReconnect()
reject(error)
}
)
} catch (error) {
console.error('WebSocket初始化失败:', error)
this.setStatus('ERROR')
reject(error)
}
})
}
/**
* 断开连接
*/
disconnect(): void {
if (this.client?.connected) {
this.sendDisconnectMessage()
this.client.disconnect(() => {
console.log('WebSocket已断开连接')
})
}
this.stopHeartbeat()
this.setStatus('DISCONNECTED')
this.callbacks.onDisconnect?.()
}
/**
* 发送聊天消息
*/
sendChatMessage(content: string, conversationId?: string): void {
if (!this.client?.connected) {
const error = new Error('WebSocket连接已断开,无法发送消息')
console.error('WebSocket未连接')
this.callbacks.onError?.({ userMessage: '连接已断开,请等待重连后再试', originalError: error })
return
}
if (!content.trim()) {
const error = new Error('消息内容不能为空')
this.callbacks.onError?.({ userMessage: '消息内容不能为空', originalError: error })
return
}
// 使用新的后端接口格式
const chatRequest: ChatRequest = {
content: content.trim(),
senderId: this.userId!,
senderType: this.userId?.startsWith('guest_') ? 'GUEST' : 'USER',
messageType: 'TEXT',
conversationId: conversationId || this.conversationId || undefined,
timestamp: Date.now()
}
try {
this.client.send('/app/chat.send', {}, JSON.stringify(chatRequest))
console.log('发送聊天消息:', chatRequest)
} catch (error) {
console.error('发送消息失败:', error)
this.callbacks.onError?.({
userMessage: '消息发送失败,请重试',
originalError: error
})
}
}
/**
* 设置会话ID
*/
setConversationId(conversationId: string): void {
this.conversationId = conversationId
console.log('WebSocket会话ID已更新:', conversationId)
}
/**
* 获取当前会话ID
*/
getConversationId(): string | null {
return this.conversationId
}
/**
* 获取连接状态
*/
getStatus(): ConnectionStatus {
return this.status
}
/**
* 检查是否已连接
*/
isConnected(): boolean {
return this.status === 'CONNECTED' && this.client?.connected === true
}
/**
* 订阅消息
*/
private subscribeToMessages(): void {
if (!this.client?.connected) return
// 订阅用户私有消息
this.client.subscribe('/user/queue/messages', (message) => {
try {
const wsMessage: WebSocketMessage = JSON.parse(message.body)
console.log('收到WebSocket消息:', wsMessage)
this.callbacks.onMessage?.(wsMessage)
} catch (error) {
console.error('解析WebSocket消息失败:', error)
}
})
// 订阅广播消息
this.client.subscribe('/topic/broadcast', (message) => {
try {
const wsMessage: WebSocketMessage = JSON.parse(message.body)
console.log('收到广播消息:', wsMessage)
this.callbacks.onMessage?.(wsMessage)
} catch (error) {
console.error('解析广播消息失败:', error)
}
})
}
/**
* 发送连接消息
*/
private sendConnectMessage(): void {
if (!this.client?.connected) return
const connectRequest: ConnectRequest = {
userId: this.userId!,
username: this.userId!,
clientType: 'web',
clientVersion: '1.0.0',
timestamp: Date.now()
}
try {
this.client.send('/app/chat.connect', {}, JSON.stringify(connectRequest))
console.log('发送连接消息:', connectRequest)
} catch (error) {
console.error('发送连接消息失败:', error)
}
}
/**
* 发送断开连接消息
*/
private sendDisconnectMessage(): void {
if (!this.client?.connected) return
try {
this.client.send('/app/chat.disconnect', {}, JSON.stringify({}))
} catch (error) {
console.error('发送断开连接消息失败:', error)
}
}
/**
* 启动心跳
*/
private startHeartbeat(): void {
this.stopHeartbeat()
this.heartbeatTimer = window.setInterval(() => {
if (this.client?.connected) {
try {
this.client.send('/app/chat.heartbeat', {}, JSON.stringify({}))
} catch (error) {
console.error('心跳发送失败:', error)
}
}
}, 30000) // 30秒心跳间隔
}
/**
* 停止心跳
*/
private stopHeartbeat(): void {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer)
this.heartbeatTimer = null
}
}
/**
* 设置连接状态
*/
private setStatus(status: ConnectionStatus): void {
this.status = status
this.callbacks.onStatusChange?.(status)
}
/**
* 安排重连
*/
private scheduleReconnect(): void {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('达到最大重连次数,停止重连')
return
}
this.reconnectAttempts++
console.log(`${this.reconnectInterval}ms后尝试第${this.reconnectAttempts}次重连`)
setTimeout(() => {
if (this.status !== 'CONNECTED') {
this.connect(this.userId!, this.callbacks).catch(() => {
// 重连失败会自动安排下次重连
})
}
}, this.reconnectInterval)
// 递增重连间隔
this.reconnectInterval = Math.min(this.reconnectInterval * 1.5, 30000)
}
}
// 创建WebSocket服务实例
const wsUrl = import.meta.env.VITE_WS_URL || 'http://localhost:19089/ws/chat'
export const webSocketService = new WebSocketService(wsUrl)
export default webSocketService