重命名前端项目目录: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
+229
View File
@@ -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
}
}
}
+118
View File
@@ -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
+141
View File
@@ -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
}