mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-04-19 19:08:38 +00:00
1
This commit is contained in:
208
web/admin-spa/src/utils/useTestState.js
Normal file
208
web/admin-spa/src/utils/useTestState.js
Normal file
@@ -0,0 +1,208 @@
|
||||
import { ref, computed, onUnmounted } from 'vue'
|
||||
|
||||
export const useTestState = () => {
|
||||
// ========== 状态 ==========
|
||||
const testStatus = ref('idle') // idle, testing, success, error
|
||||
const responseText = ref('')
|
||||
const errorMessage = ref('')
|
||||
const testDuration = ref(0)
|
||||
const testStartTime = ref(null)
|
||||
const abortController = ref(null)
|
||||
|
||||
// ========== 状态样式计算属性 ==========
|
||||
const statusStyleMap = {
|
||||
idle: {
|
||||
title: '准备就绪',
|
||||
card: 'border-gray-200 bg-gray-50 dark:border-gray-700 dark:bg-gray-800/50',
|
||||
iconBg: 'bg-gray-200 dark:bg-gray-700',
|
||||
icon: 'fa-hourglass-start',
|
||||
iconColor: 'text-gray-500 dark:text-gray-400',
|
||||
text: 'text-gray-700 dark:text-gray-300'
|
||||
},
|
||||
testing: {
|
||||
title: '正在测试...',
|
||||
card: 'border-blue-200 bg-blue-50 dark:border-blue-500/30 dark:bg-blue-900/20',
|
||||
iconBg: 'bg-blue-100 dark:bg-blue-500/30',
|
||||
icon: 'fa-spinner fa-spin',
|
||||
iconColor: 'text-blue-500 dark:text-blue-400',
|
||||
text: 'text-blue-700 dark:text-blue-300'
|
||||
},
|
||||
success: {
|
||||
title: '测试成功',
|
||||
card: 'border-green-200 bg-green-50 dark:border-green-500/30 dark:bg-green-900/20',
|
||||
iconBg: 'bg-green-100 dark:bg-green-500/30',
|
||||
icon: 'fa-check-circle',
|
||||
iconColor: 'text-green-500 dark:text-green-400',
|
||||
text: 'text-green-700 dark:text-green-300'
|
||||
},
|
||||
error: {
|
||||
title: '测试失败',
|
||||
card: 'border-red-200 bg-red-50 dark:border-red-500/30 dark:bg-red-900/20',
|
||||
iconBg: 'bg-red-100 dark:bg-red-500/30',
|
||||
icon: 'fa-exclamation-circle',
|
||||
iconColor: 'text-red-500 dark:text-red-400',
|
||||
text: 'text-red-700 dark:text-red-300'
|
||||
}
|
||||
}
|
||||
|
||||
const currentStyle = computed(() => statusStyleMap[testStatus.value] || statusStyleMap.idle)
|
||||
const statusTitle = computed(() => currentStyle.value.title)
|
||||
const statusCardClass = computed(() => currentStyle.value.card)
|
||||
const statusIconBgClass = computed(() => currentStyle.value.iconBg)
|
||||
const statusIcon = computed(() => currentStyle.value.icon)
|
||||
const statusIconClass = computed(() => currentStyle.value.iconColor)
|
||||
const statusTextClass = computed(() => currentStyle.value.text)
|
||||
|
||||
// ========== SSE 事件处理 ==========
|
||||
const handleSSEEvent = (data) => {
|
||||
switch (data.type) {
|
||||
case 'test_start':
|
||||
break
|
||||
case 'content':
|
||||
responseText.value += data.text
|
||||
break
|
||||
case 'message_stop':
|
||||
break
|
||||
case 'test_complete':
|
||||
testDuration.value = Date.now() - testStartTime.value
|
||||
if (data.success) {
|
||||
testStatus.value = 'success'
|
||||
} else {
|
||||
testStatus.value = 'error'
|
||||
errorMessage.value = data.error || '测试失败'
|
||||
}
|
||||
break
|
||||
case 'error':
|
||||
testStatus.value = 'error'
|
||||
errorMessage.value = data.error || '未知错误'
|
||||
testDuration.value = Date.now() - testStartTime.value
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// ========== SSE 流读取 ==========
|
||||
const readSSEStream = async (response) => {
|
||||
const reader = response.body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let streamDone = false
|
||||
let buffer = ''
|
||||
|
||||
while (!streamDone) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) {
|
||||
streamDone = true
|
||||
// 处理缓冲区中剩余的数据
|
||||
if (buffer.trim()) {
|
||||
processSSELine(buffer)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
buffer += decoder.decode(value, { stream: true })
|
||||
const lines = buffer.split('\n')
|
||||
// 最后一行可能不完整,保留在缓冲区
|
||||
buffer = lines.pop() || ''
|
||||
|
||||
for (const line of lines) {
|
||||
processSSELine(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const processSSELine = (line) => {
|
||||
if (line.startsWith('data: ')) {
|
||||
try {
|
||||
const data = JSON.parse(line.substring(6))
|
||||
handleSSEEvent(data)
|
||||
} catch {
|
||||
// 忽略解析错误
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 通用测试请求 ==========
|
||||
const sendTestRequest = async (endpoint, payload, options = {}) => {
|
||||
const { useSSE = true, headers = {} } = options
|
||||
|
||||
// 重置状态
|
||||
testStatus.value = 'testing'
|
||||
responseText.value = ''
|
||||
errorMessage.value = ''
|
||||
testDuration.value = 0
|
||||
testStartTime.value = Date.now()
|
||||
|
||||
// 取消之前的请求
|
||||
if (abortController.value) {
|
||||
abortController.value.abort()
|
||||
}
|
||||
abortController.value = new AbortController()
|
||||
|
||||
try {
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', ...headers },
|
||||
body: JSON.stringify(payload),
|
||||
signal: abortController.value.signal
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
throw new Error(errorData.message || errorData.error || `HTTP ${response.status}`)
|
||||
}
|
||||
|
||||
if (useSSE) {
|
||||
await readSSEStream(response)
|
||||
} else {
|
||||
// JSON 响应
|
||||
const data = await response.json()
|
||||
testDuration.value = Date.now() - testStartTime.value
|
||||
if (data.success) {
|
||||
testStatus.value = 'success'
|
||||
responseText.value = data.data?.responseText || 'Test passed'
|
||||
} else {
|
||||
testStatus.value = 'error'
|
||||
errorMessage.value = data.message || 'Test failed'
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.name === 'AbortError') return
|
||||
testStatus.value = 'error'
|
||||
errorMessage.value = err.message || '连接失败'
|
||||
testDuration.value = Date.now() - testStartTime.value
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 重置 + 清理 ==========
|
||||
const resetState = () => {
|
||||
testStatus.value = 'idle'
|
||||
responseText.value = ''
|
||||
errorMessage.value = ''
|
||||
testDuration.value = 0
|
||||
testStartTime.value = null
|
||||
}
|
||||
|
||||
const cleanup = () => {
|
||||
if (abortController.value) {
|
||||
abortController.value.abort()
|
||||
abortController.value = null
|
||||
}
|
||||
}
|
||||
|
||||
onUnmounted(cleanup)
|
||||
|
||||
return {
|
||||
testStatus,
|
||||
responseText,
|
||||
errorMessage,
|
||||
testDuration,
|
||||
statusTitle,
|
||||
statusCardClass,
|
||||
statusIconBgClass,
|
||||
statusIcon,
|
||||
statusIconClass,
|
||||
statusTextClass,
|
||||
sendTestRequest,
|
||||
resetState,
|
||||
cleanup
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user