重命名前端项目目录:web-flowith -> web
- 将前端项目目录从 web-flowith 重命名为 web,使目录结构更简洁 - 保持所有前端代码和配置文件不变 - 统一项目目录命名规范
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import 'dayjs/locale/zh-cn'
|
||||
|
||||
// 配置dayjs
|
||||
dayjs.extend(relativeTime)
|
||||
dayjs.locale('zh-cn')
|
||||
|
||||
// 时间格式化
|
||||
export const formatTime = {
|
||||
// 相对时间
|
||||
relative: (date: string | Date) => dayjs(date).fromNow(),
|
||||
|
||||
// 标准格式
|
||||
standard: (date: string | Date) => dayjs(date).format('YYYY-MM-DD HH:mm:ss'),
|
||||
|
||||
// 日期格式
|
||||
date: (date: string | Date) => dayjs(date).format('YYYY-MM-DD'),
|
||||
|
||||
// 时间格式
|
||||
time: (date: string | Date) => dayjs(date).format('HH:mm:ss'),
|
||||
|
||||
// 友好格式
|
||||
friendly: (date: string | Date) => {
|
||||
const now = dayjs()
|
||||
const target = dayjs(date)
|
||||
const diffDays = now.diff(target, 'day')
|
||||
|
||||
if (diffDays === 0) {
|
||||
return target.format('HH:mm')
|
||||
} else if (diffDays === 1) {
|
||||
return '昨天 ' + target.format('HH:mm')
|
||||
} else if (diffDays < 7) {
|
||||
return target.format('M月D日 HH:mm')
|
||||
} else {
|
||||
return target.format('YYYY年M月D日')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 防抖函数
|
||||
export const debounce = <T extends (...args: any[]) => any>(
|
||||
func: T,
|
||||
wait: number
|
||||
): ((...args: Parameters<T>) => void) => {
|
||||
let timeout: NodeJS.Timeout | null = null
|
||||
|
||||
return (...args: Parameters<T>) => {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
timeout = setTimeout(() => func(...args), wait)
|
||||
}
|
||||
}
|
||||
|
||||
// 节流函数
|
||||
export const throttle = <T extends (...args: any[]) => any>(
|
||||
func: T,
|
||||
wait: number
|
||||
): ((...args: Parameters<T>) => void) => {
|
||||
let lastTime = 0
|
||||
|
||||
return (...args: Parameters<T>) => {
|
||||
const now = Date.now()
|
||||
if (now - lastTime >= wait) {
|
||||
lastTime = now
|
||||
func(...args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 生成唯一ID
|
||||
export const generateId = () => {
|
||||
return Date.now().toString(36) + Math.random().toString(36).substr(2)
|
||||
}
|
||||
|
||||
// 深拷贝
|
||||
export const deepClone = <T>(obj: T): T => {
|
||||
if (obj === null || typeof obj !== 'object') {
|
||||
return obj
|
||||
}
|
||||
|
||||
if (obj instanceof Date) {
|
||||
return new Date(obj.getTime()) as T
|
||||
}
|
||||
|
||||
if (obj instanceof Array) {
|
||||
return obj.map(item => deepClone(item)) as T
|
||||
}
|
||||
|
||||
if (typeof obj === 'object') {
|
||||
const cloned = {} as T
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
cloned[key] = deepClone(obj[key])
|
||||
}
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
// 文件大小格式化
|
||||
export const formatFileSize = (bytes: number): string => {
|
||||
if (bytes === 0) return '0 B'
|
||||
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
// 数字格式化
|
||||
export const formatNumber = (num: number): string => {
|
||||
if (num < 1000) return num.toString()
|
||||
if (num < 10000) return (num / 1000).toFixed(1) + 'K'
|
||||
if (num < 100000000) return (num / 10000).toFixed(1) + '万'
|
||||
return (num / 100000000).toFixed(1) + '亿'
|
||||
}
|
||||
|
||||
// 颜色工具
|
||||
export const colorUtils = {
|
||||
// 十六进制转RGB
|
||||
hexToRgb: (hex: string) => {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
||||
return result ? {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16)
|
||||
} : null
|
||||
},
|
||||
|
||||
// RGB转十六进制
|
||||
rgbToHex: (r: number, g: number, b: number) => {
|
||||
return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)
|
||||
},
|
||||
|
||||
// 获取随机颜色
|
||||
random: () => {
|
||||
return '#' + Math.floor(Math.random() * 16777215).toString(16)
|
||||
}
|
||||
}
|
||||
|
||||
// 本地存储工具
|
||||
export const storage = {
|
||||
set: (key: string, value: any) => {
|
||||
try {
|
||||
localStorage.setItem(key, JSON.stringify(value))
|
||||
} catch (error) {
|
||||
console.error('Storage set error:', error)
|
||||
}
|
||||
},
|
||||
|
||||
get: <T = any>(key: string, defaultValue?: T): T | null => {
|
||||
try {
|
||||
const item = localStorage.getItem(key)
|
||||
return item ? JSON.parse(item) : defaultValue || null
|
||||
} catch (error) {
|
||||
console.error('Storage get error:', error)
|
||||
return defaultValue || null
|
||||
}
|
||||
},
|
||||
|
||||
remove: (key: string) => {
|
||||
try {
|
||||
localStorage.removeItem(key)
|
||||
} catch (error) {
|
||||
console.error('Storage remove error:', error)
|
||||
}
|
||||
},
|
||||
|
||||
clear: () => {
|
||||
try {
|
||||
localStorage.clear()
|
||||
} catch (error) {
|
||||
console.error('Storage clear error:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// URL工具
|
||||
export const urlUtils = {
|
||||
// 获取查询参数
|
||||
getQuery: (name: string): string | null => {
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
return urlParams.get(name)
|
||||
},
|
||||
|
||||
// 设置查询参数
|
||||
setQuery: (params: Record<string, string>) => {
|
||||
const url = new URL(window.location.href)
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
url.searchParams.set(key, value)
|
||||
})
|
||||
window.history.replaceState({}, '', url.toString())
|
||||
},
|
||||
|
||||
// 删除查询参数
|
||||
removeQuery: (name: string) => {
|
||||
const url = new URL(window.location.href)
|
||||
url.searchParams.delete(name)
|
||||
window.history.replaceState({}, '', url.toString())
|
||||
}
|
||||
}
|
||||
|
||||
// 设备检测
|
||||
export const deviceUtils = {
|
||||
isMobile: () => /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent),
|
||||
isIOS: () => /iPad|iPhone|iPod/.test(navigator.userAgent),
|
||||
isAndroid: () => /Android/.test(navigator.userAgent),
|
||||
isWechat: () => /MicroMessenger/i.test(navigator.userAgent)
|
||||
}
|
||||
|
||||
// 验证工具
|
||||
export const validators = {
|
||||
email: (email: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email),
|
||||
phone: (phone: string) => /^1[3-9]\d{9}$/.test(phone),
|
||||
password: (password: string) => password.length >= 6,
|
||||
url: (url: string) => {
|
||||
try {
|
||||
new URL(url)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import axios, { AxiosInstance, InternalAxiosRequestConfig, AxiosResponse } from 'axios'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import router from '@/router'
|
||||
|
||||
// 获取API基础URL
|
||||
const getApiBaseUrl = () => {
|
||||
// 开发环境使用代理
|
||||
if (import.meta.env.DEV) {
|
||||
return '/api'
|
||||
}
|
||||
// 生产环境使用环境变量
|
||||
return import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080/api'
|
||||
}
|
||||
|
||||
// 创建axios实例
|
||||
const request: AxiosInstance = axios.create({
|
||||
baseURL: getApiBaseUrl(),
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
console.log('API Base URL:', getApiBaseUrl())
|
||||
|
||||
// 请求拦截器
|
||||
request.interceptors.request.use(
|
||||
(config: InternalAxiosRequestConfig) => {
|
||||
// 从localStorage获取token
|
||||
const token = localStorage.getItem('token')
|
||||
|
||||
if (token && config.headers) {
|
||||
// 在请求头中添加Authorization
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
|
||||
console.log('发送请求:', {
|
||||
url: config.url,
|
||||
method: config.method,
|
||||
hasToken: !!token,
|
||||
headers: config.headers
|
||||
})
|
||||
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
console.error('请求拦截器错误:', error)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
request.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
const { data } = response
|
||||
// 标准后端格式: { code, message, data, timestamp }
|
||||
if (typeof data === 'object' && data !== null && 'code' in data) {
|
||||
if (data.code !== 200) {
|
||||
message.error(data.message || '请求失败')
|
||||
return Promise.reject(new Error(data.message || '请求失败'))
|
||||
}
|
||||
// 只返回data字段, 兼容验证码等所有接口
|
||||
return data.data
|
||||
}
|
||||
// 兼容极特殊情况(如验证码图片流等)
|
||||
return data
|
||||
},
|
||||
(error) => {
|
||||
console.error('响应拦截器错误:', error)
|
||||
|
||||
if (error.response) {
|
||||
const { status, data } = error.response
|
||||
|
||||
switch (status) {
|
||||
case 401:
|
||||
// token过期或无效
|
||||
message.error('登录已过期,请重新登录')
|
||||
|
||||
// 清除本地存储的用户信息
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('userInfo')
|
||||
|
||||
// 清除store中的用户信息
|
||||
const userStore = useUserStore()
|
||||
userStore.setToken('')
|
||||
userStore.setUserInfo(null)
|
||||
|
||||
// 跳转到登录页
|
||||
router.push('/login')
|
||||
break
|
||||
|
||||
case 403:
|
||||
message.error('没有权限访问该资源')
|
||||
break
|
||||
|
||||
case 404:
|
||||
message.error('请求的资源不存在')
|
||||
break
|
||||
|
||||
case 500:
|
||||
message.error('服务器内部错误')
|
||||
break
|
||||
|
||||
default:
|
||||
message.error(data?.message || '请求失败')
|
||||
}
|
||||
} else if (error.request) {
|
||||
message.error('网络连接失败,请检查网络')
|
||||
} else {
|
||||
message.error('请求配置错误')
|
||||
}
|
||||
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export default request
|
||||
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* WebSocket连接测试工具
|
||||
* 用于测试WebSocket连接和消息发送功能
|
||||
*/
|
||||
|
||||
import webSocketService from '@/services/websocket'
|
||||
|
||||
export class WebSocketTester {
|
||||
private isConnected = false
|
||||
private testResults: string[] = []
|
||||
|
||||
/**
|
||||
* 运行WebSocket连接测试
|
||||
*/
|
||||
async runConnectionTest(): Promise<boolean> {
|
||||
this.testResults = []
|
||||
this.log('开始WebSocket连接测试...')
|
||||
|
||||
try {
|
||||
// 测试连接
|
||||
await webSocketService.connect('test_user_' + Date.now(), {
|
||||
onConnect: () => {
|
||||
this.isConnected = true
|
||||
this.log('✅ WebSocket连接成功')
|
||||
},
|
||||
onDisconnect: () => {
|
||||
this.isConnected = false
|
||||
this.log('❌ WebSocket连接断开')
|
||||
},
|
||||
onError: (error) => {
|
||||
this.log(`❌ WebSocket错误: ${error.userMessage || error.message || '未知错误'}`)
|
||||
},
|
||||
onMessage: (message) => {
|
||||
this.log(`📨 收到消息: ${message.type} - ${message.content}`)
|
||||
}
|
||||
})
|
||||
|
||||
// 等待连接建立
|
||||
await this.waitForConnection(5000)
|
||||
|
||||
if (this.isConnected) {
|
||||
this.log('✅ 连接测试通过')
|
||||
return true
|
||||
} else {
|
||||
this.log('❌ 连接测试失败')
|
||||
return false
|
||||
}
|
||||
} catch (error) {
|
||||
this.log(`❌ 连接测试异常: ${error}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试消息发送
|
||||
*/
|
||||
async testMessageSending(): Promise<boolean> {
|
||||
if (!this.isConnected) {
|
||||
this.log('❌ 未连接,无法测试消息发送')
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
this.log('开始测试消息发送...')
|
||||
|
||||
// 设置测试会话ID
|
||||
webSocketService.setConversationId('test_conversation_' + Date.now())
|
||||
|
||||
// 发送测试消息
|
||||
webSocketService.sendChatMessage('这是一条测试消息')
|
||||
|
||||
this.log('✅ 消息发送成功')
|
||||
return true
|
||||
} catch (error) {
|
||||
this.log(`❌ 消息发送失败: ${error}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开连接测试
|
||||
*/
|
||||
testDisconnection(): void {
|
||||
this.log('开始测试断开连接...')
|
||||
webSocketService.disconnect()
|
||||
this.log('✅ 断开连接完成')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取测试结果
|
||||
*/
|
||||
getTestResults(): string[] {
|
||||
return [...this.testResults]
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空测试结果
|
||||
*/
|
||||
clearResults(): void {
|
||||
this.testResults = []
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录测试日志
|
||||
*/
|
||||
private log(message: string): void {
|
||||
const timestamp = new Date().toLocaleTimeString()
|
||||
const logMessage = `[${timestamp}] ${message}`
|
||||
this.testResults.push(logMessage)
|
||||
console.log(logMessage)
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待连接建立
|
||||
*/
|
||||
private waitForConnection(timeout: number): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const startTime = Date.now()
|
||||
|
||||
const checkConnection = () => {
|
||||
if (this.isConnected) {
|
||||
resolve()
|
||||
} else if (Date.now() - startTime > timeout) {
|
||||
reject(new Error('连接超时'))
|
||||
} else {
|
||||
setTimeout(checkConnection, 100)
|
||||
}
|
||||
}
|
||||
|
||||
checkConnection()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 导出测试实例
|
||||
export const wsTest = new WebSocketTester()
|
||||
|
||||
// 开发环境下添加到全局对象,方便调试
|
||||
if (import.meta.env.DEV) {
|
||||
(window as any).wsTest = wsTest
|
||||
}
|
||||
Reference in New Issue
Block a user