feat: Features架构迁移 - P0和P1部分完成

## 完成的工作

### P0: 基础设施准备
- 创建 features/ 和 shared/ 目录结构
- 迁移共享资源:API基础设施、工具函数、类型定义
- 迁移通用composables:useMoney, useSmsCountdown, usePricingCalculator
- 迁移全局样式文件
- 建立模块化导出系统

### P1.1: 钱包模块 (wallet)
- 迁移 API: wallet.ts
- 迁移 Views: WalletView.vue
- 新增 Composable: useWallet.ts (封装钱包状态管理)
- 更新导入路径到 shared/

### P1.2: 聊天模块 (chats)
- 迁移 API: chats.ts
- 迁移 Views: ChatView, MessagesView (桌面+移动)
- 迁移 Composables: useChatSSE.ts
- 迁移 Components: ChatAttachmentImage.vue
- 更新导入路径到 shared/

## 技术改进
- 修复 shared/composables 导出问题 (default → 命名导出)
- 修复 shared/api/client.ts 类型导入路径
- 建立清晰的模块边界和导出规范

## 文档
- 添加完整的迁移计划文档
- 添加进度跟踪文档

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
yml2213
2026-06-04 08:38:36 +08:00
co-authored by Claude Opus 4.7
parent 10acca637e
commit b5903a169f
42 changed files with 7957 additions and 0 deletions
@@ -0,0 +1,118 @@
import { onBeforeUnmount, ref, type Ref } from 'vue'
import { refreshAccessToken } from '@/api/client'
import { getAccessToken, type AuthScope } from '@/utils/authStorage'
export interface SSEMessage {
id: number
conversation_id: number
sender_type: string
sender_id: number
sender_role: string
sender_name: string
content_type: string
content: string
attachment_urls: string[]
created_at: string
}
export interface ChatEvent {
type: 'new_message' | 'conversation_updated'
conversation_id: number
message?: SSEMessage
}
type EventHandler = (event: ChatEvent) => void
const reconnectDelay = 3000
export function useChatSSE(scope: AuthScope, endpoint: string) {
const connected: Ref<boolean> = ref(false)
let source: EventSource | null = null
let reconnectTimer: ReturnType<typeof setTimeout> | null = null
let stopped = false
let refreshing = false
const handlers: EventHandler[] = []
function onEvent(handler: EventHandler) {
handlers.push(handler)
}
function connect() {
if (stopped || source) return
const token = getAccessToken(scope)
if (!token) return
const url = `${endpoint}?token=${encodeURIComponent(token)}`
source = new EventSource(url)
source.addEventListener('connected', () => {
connected.value = true
})
source.addEventListener('new_message', (e) => {
try {
const data = JSON.parse((e as MessageEvent).data) as ChatEvent
handlers.forEach(h => h(data))
} catch { /* ignore */ }
})
source.addEventListener('conversation_updated', (e) => {
try {
const data = JSON.parse((e as MessageEvent).data) as ChatEvent
handlers.forEach(h => h(data))
} catch { /* ignore */ }
})
source.onerror = async () => {
connected.value = false
source?.close()
source = null
if (stopped || refreshing) return
refreshing = true
try {
await refreshAccessToken(scope)
if (!stopped) {
reconnectTimer = setTimeout(connect, reconnectDelay)
}
} catch {
closeSource()
} finally {
refreshing = false
}
}
}
function closeSource() {
if (reconnectTimer) {
clearTimeout(reconnectTimer)
reconnectTimer = null
}
source?.close()
source = null
connected.value = false
}
function handleAuthStorageChanged(event: Event) {
const detail = (event as CustomEvent<{ scope?: AuthScope }>).detail
if (detail?.scope !== scope || stopped) return
closeSource()
connect()
}
function disconnect() {
stopped = true
closeSource()
window.removeEventListener('auth-storage-changed', handleAuthStorageChanged)
}
window.addEventListener('auth-storage-changed', handleAuthStorageChanged)
onBeforeUnmount(() => {
disconnect()
})
connect()
return { connected, onEvent, disconnect }
}