Files
claude-relay-service/web/admin-spa/src/utils/useTestState.js
SunSeekerX a119cb1744 1
2026-02-09 18:13:45 +08:00

209 lines
6.1 KiB
JavaScript

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
}
}