mirror of
https://github.com/foxhui/WebAI2API.git
synced 2026-06-16 21:03:59 +08:00
624 lines
22 KiB
Vue
624 lines
22 KiB
Vue
<script setup>
|
||
import { ref, onMounted, onUnmounted } from 'vue';
|
||
import { useRouter } from 'vue-router';
|
||
import { Modal, message } from 'ant-design-vue';
|
||
import {
|
||
DashboardOutlined,
|
||
SettingOutlined,
|
||
ToolOutlined,
|
||
PoweroffOutlined,
|
||
GithubOutlined,
|
||
ApiOutlined,
|
||
CheckCircleOutlined,
|
||
CloseCircleOutlined,
|
||
LoadingOutlined,
|
||
InboxOutlined,
|
||
PictureOutlined
|
||
} from '@ant-design/icons-vue';
|
||
import { useSettingsStore } from '@/stores/settings';
|
||
import LoginModal from '@/components/auth/LoginModal.vue';
|
||
|
||
const router = useRouter();
|
||
const settingsStore = useSettingsStore();
|
||
|
||
const selectedKeys = ref(['dash']);
|
||
const collapsed = ref(false);
|
||
const loginVisible = ref(false);
|
||
|
||
const iconLoading = ref(false);
|
||
const enterIconLoading = () => {
|
||
iconLoading.value = true;
|
||
settingsStore.setToken('');
|
||
setTimeout(() => {
|
||
iconLoading.value = false;
|
||
loginVisible.value = true;
|
||
}, 500);
|
||
};
|
||
|
||
// 接口测试抽屉
|
||
const apiTestDrawer = ref(false);
|
||
const apiTestResults = ref({
|
||
models: { status: 'pending', data: null, error: null },
|
||
cookies: { status: 'pending', data: null, error: null },
|
||
chat: { status: 'pending', data: null, error: null }
|
||
});
|
||
const chatTestPrompt = ref('Say hello in one word');
|
||
const chatTestModel = ref('');
|
||
const chatModelList = ref([]);
|
||
const chatImageList = ref([]);
|
||
const chatStreamMode = ref(false);
|
||
const chatStreamContent = ref('');
|
||
|
||
// 获取模型列表
|
||
const fetchModelList = async () => {
|
||
try {
|
||
const res = await fetch('/v1/models', { headers: settingsStore.getHeaders() });
|
||
if (res.ok) {
|
||
const data = await res.json();
|
||
chatModelList.value = data.data || [];
|
||
if (chatModelList.value.length > 0 && !chatTestModel.value) {
|
||
chatTestModel.value = chatModelList.value[0].id;
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.error('获取模型列表失败', e);
|
||
}
|
||
};
|
||
|
||
// 图片转 base64
|
||
const fileToBase64 = (file) => {
|
||
return new Promise((resolve, reject) => {
|
||
const reader = new FileReader();
|
||
reader.readAsDataURL(file);
|
||
reader.onload = () => resolve(reader.result);
|
||
reader.onerror = reject;
|
||
});
|
||
};
|
||
|
||
// 图片上传前检查
|
||
const beforeUpload = (file) => {
|
||
const allowedTypes = ['image/png', 'image/jpeg', 'image/gif', 'image/webp'];
|
||
if (!allowedTypes.includes(file.type)) {
|
||
message.error('仅支持 PNG, JPEG, GIF, WebP 格式');
|
||
return false;
|
||
}
|
||
if (chatImageList.value.length >= 10) {
|
||
message.error('最多上传 10 张图片');
|
||
return false;
|
||
}
|
||
return false; // 阻止自动上传,手动处理
|
||
};
|
||
|
||
// 处理图片选择
|
||
const handleImageChange = async (info) => {
|
||
const file = info.file;
|
||
if (file.status === 'removed') {
|
||
chatImageList.value = chatImageList.value.filter(f => f.uid !== file.uid);
|
||
return;
|
||
}
|
||
try {
|
||
const base64 = await fileToBase64(file.originFileObj || file);
|
||
chatImageList.value.push({
|
||
uid: file.uid,
|
||
name: file.name,
|
||
base64
|
||
});
|
||
} catch (e) {
|
||
message.error('图片读取失败');
|
||
}
|
||
};
|
||
|
||
// 解析 Markdown 图片
|
||
const parseMarkdownImages = (content) => {
|
||
if (!content) return { text: '', images: [] };
|
||
|
||
const imageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g;
|
||
const images = [];
|
||
let match;
|
||
let lastIndex = 0;
|
||
let textParts = [];
|
||
|
||
while ((match = imageRegex.exec(content)) !== null) {
|
||
// 添加图片之前的文本
|
||
if (match.index > lastIndex) {
|
||
textParts.push(content.substring(lastIndex, match.index));
|
||
}
|
||
|
||
// 添加图片
|
||
images.push({
|
||
alt: match[1] || '图片',
|
||
src: match[2]
|
||
});
|
||
|
||
// 不添加占位符,直接跳过图片的 markdown 语法
|
||
|
||
lastIndex = imageRegex.lastIndex;
|
||
}
|
||
|
||
// 添加剩余文本
|
||
if (lastIndex < content.length) {
|
||
textParts.push(content.substring(lastIndex));
|
||
}
|
||
|
||
return {
|
||
text: textParts.join('').trim(),
|
||
images
|
||
};
|
||
};
|
||
|
||
const testApi = async (type) => {
|
||
apiTestResults.value[type].status = 'loading';
|
||
apiTestResults.value[type].error = null;
|
||
apiTestResults.value[type].data = null;
|
||
chatStreamContent.value = '';
|
||
|
||
try {
|
||
let url, options;
|
||
if (type === 'models') {
|
||
url = '/v1/models';
|
||
options = { headers: settingsStore.getHeaders() };
|
||
} else if (type === 'cookies') {
|
||
url = '/v1/cookies';
|
||
options = { headers: settingsStore.getHeaders() };
|
||
} else if (type === 'chat') {
|
||
url = '/v1/chat/completions';
|
||
|
||
// 构建消息内容
|
||
let content;
|
||
if (chatImageList.value.length > 0) {
|
||
// 多模态请求
|
||
content = [
|
||
{ type: 'text', text: chatTestPrompt.value }
|
||
];
|
||
for (const img of chatImageList.value) {
|
||
content.push({
|
||
type: 'image_url',
|
||
image_url: { url: img.base64 }
|
||
});
|
||
}
|
||
} else {
|
||
content = chatTestPrompt.value;
|
||
}
|
||
|
||
options = {
|
||
method: 'POST',
|
||
headers: { ...settingsStore.getHeaders(), 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
model: chatTestModel.value,
|
||
messages: [{ role: 'user', content }],
|
||
stream: chatStreamMode.value
|
||
})
|
||
};
|
||
|
||
// 流式请求处理
|
||
if (chatStreamMode.value) {
|
||
const res = await fetch(url, options);
|
||
if (!res.ok) {
|
||
const errData = await res.json();
|
||
throw new Error(errData.error?.message || `HTTP ${res.status}`);
|
||
}
|
||
|
||
const reader = res.body.getReader();
|
||
const decoder = new TextDecoder();
|
||
let buffer = '';
|
||
|
||
while (true) {
|
||
const { done, value } = await reader.read();
|
||
if (done) break;
|
||
|
||
buffer += decoder.decode(value, { stream: true });
|
||
const lines = buffer.split('\n');
|
||
buffer = lines.pop() || '';
|
||
|
||
for (const line of lines) {
|
||
if (line.startsWith('data: ')) {
|
||
const data = line.slice(6).trim();
|
||
if (data === '[DONE]') continue;
|
||
try {
|
||
const json = JSON.parse(data);
|
||
const delta = json.choices?.[0]?.delta?.content || '';
|
||
chatStreamContent.value += delta;
|
||
} catch { /* 忽略解析错误 */ }
|
||
}
|
||
}
|
||
}
|
||
|
||
apiTestResults.value[type].status = 'success';
|
||
apiTestResults.value[type].data = { content: chatStreamContent.value };
|
||
return;
|
||
}
|
||
}
|
||
|
||
const res = await fetch(url, options);
|
||
const data = await res.json();
|
||
|
||
if (res.ok) {
|
||
apiTestResults.value[type].status = 'success';
|
||
// Chat 接口:提取 content
|
||
if (type === 'chat' && data.choices?.[0]?.message?.content) {
|
||
apiTestResults.value[type].data = { content: data.choices[0].message.content };
|
||
} else {
|
||
apiTestResults.value[type].data = data;
|
||
}
|
||
} else {
|
||
apiTestResults.value[type].status = 'error';
|
||
apiTestResults.value[type].error = data.error?.message || `HTTP ${res.status}`;
|
||
}
|
||
} catch (e) {
|
||
apiTestResults.value[type].status = 'error';
|
||
apiTestResults.value[type].error = e.message;
|
||
}
|
||
};
|
||
|
||
const openApiTestDrawer = () => {
|
||
apiTestDrawer.value = true;
|
||
// 重置状态
|
||
Object.keys(apiTestResults.value).forEach(key => {
|
||
apiTestResults.value[key] = { status: 'pending', data: null, error: null };
|
||
});
|
||
chatImageList.value = [];
|
||
// 获取模型列表
|
||
fetchModelList();
|
||
};
|
||
|
||
// 菜单 key 到路由路径的映射
|
||
const menuRoutes = {
|
||
'dash': '/',
|
||
'settings-server': '/settings/server',
|
||
'settings-workers': '/settings/workers',
|
||
'settings-browser': '/settings/browser',
|
||
'settings-adapters': '/settings/adapters',
|
||
'tools-display': '/tools/display',
|
||
'tools-cache': '/tools/cache',
|
||
'tools-logs': '/tools/logs'
|
||
};
|
||
|
||
// 处理菜单点击
|
||
const handleMenuClick = ({ key }) => {
|
||
const route = menuRoutes[key];
|
||
if (route) {
|
||
router.push(route);
|
||
}
|
||
};
|
||
|
||
const isInitializing = ref(true);
|
||
|
||
// 后端连接检测
|
||
let connectionCheckInterval = null;
|
||
let disconnectModalShown = false;
|
||
|
||
async function checkConnection() {
|
||
try {
|
||
const res = await fetch('/admin/status', {
|
||
headers: settingsStore.getHeaders(),
|
||
signal: AbortSignal.timeout(5000)
|
||
});
|
||
if (res.ok && disconnectModalShown) {
|
||
// 连接恢复,刷新页面
|
||
disconnectModalShown = false;
|
||
Modal.destroyAll();
|
||
window.location.reload();
|
||
}
|
||
} catch (e) {
|
||
if (!disconnectModalShown && !isInitializing.value) {
|
||
disconnectModalShown = true;
|
||
Modal.warning({
|
||
title: '后端连接断开',
|
||
content: '无法连接到后端服务,请检查服务是否正在运行。连接恢复后页面将自动刷新。',
|
||
okText: '我知道了',
|
||
centered: true
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
// 挂载时检查身份验证
|
||
onMounted(async () => {
|
||
// 响应式侧边栏
|
||
const checkScreenSize = () => {
|
||
if (window.innerWidth <= 768) {
|
||
collapsed.value = true;
|
||
}
|
||
};
|
||
checkScreenSize();
|
||
window.addEventListener('resize', checkScreenSize);
|
||
|
||
// 身份验证
|
||
try {
|
||
if (!settingsStore.token) {
|
||
loginVisible.value = true;
|
||
} else {
|
||
// 使用真实API验证
|
||
const isValid = await settingsStore.checkAuth();
|
||
if (!isValid) {
|
||
settingsStore.setToken(''); // 清除无效token
|
||
loginVisible.value = true;
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.error('Auth check failed', e);
|
||
loginVisible.value = true;
|
||
} finally {
|
||
// 隐藏加载状态
|
||
isInitializing.value = false;
|
||
}
|
||
|
||
// 启动后端连接检测(每 5 秒检测一次)
|
||
connectionCheckInterval = setInterval(checkConnection, 5000);
|
||
|
||
// 清理监听器
|
||
onUnmounted(() => {
|
||
window.removeEventListener('resize', checkScreenSize);
|
||
if (connectionCheckInterval) {
|
||
clearInterval(connectionCheckInterval);
|
||
}
|
||
});
|
||
});
|
||
</script>
|
||
|
||
<template>
|
||
<a-spin :spinning="isInitializing" tip="正在验证身份..." size="large"
|
||
style="height: 100vh; display: flex; align-items: center; justify-content: center;" v-if="isInitializing" />
|
||
<div v-else>
|
||
<LoginModal v-model:visible="loginVisible" />
|
||
<a-layout style="min-height: 100vh" theme="light">
|
||
<a-layout-header class="header"
|
||
style="background: rgba(255, 255, 255, 0.7); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); border-bottom: 1.5px solid rgba(0, 0, 0, 0.05); display: flex; align-items: center; padding: 0 24px; position: fixed; width: 100%; top: 0; z-index: 1000;">
|
||
<div class="logo" style="font-size: 1.25rem; font-weight: bold; color: #1890ff; margin-right: 24px;">
|
||
WebAI2API
|
||
</div>
|
||
<a-flex justify="end" align="center" style="flex: 1;" :gap="12">
|
||
<a-button @click="openApiTestDrawer">
|
||
<template #icon>
|
||
<ApiOutlined />
|
||
</template>
|
||
接口测试
|
||
</a-button>
|
||
<a-button danger :loading="iconLoading" @click="enterIconLoading">
|
||
<template #icon>
|
||
<PoweroffOutlined />
|
||
</template>
|
||
退出登录
|
||
</a-button>
|
||
</a-flex>
|
||
</a-layout-header>
|
||
<a-layout style="margin-top: 64px;">
|
||
<a-layout-sider v-model:collapsed="collapsed" collapsible theme="light"
|
||
style="position: fixed; left: 0; top: 64px; height: calc(100vh - 64px); overflow-y: auto; z-index: 100;">
|
||
<a-menu v-model:selectedKeys="selectedKeys" mode="inline" @click="handleMenuClick">
|
||
<a-menu-item key="dash">
|
||
<DashboardOutlined />
|
||
<span>状态概览</span>
|
||
</a-menu-item>
|
||
<a-sub-menu key="settings">
|
||
<template #title>
|
||
<span>
|
||
<SettingOutlined />
|
||
<span>系统设置</span>
|
||
</span>
|
||
</template>
|
||
<a-menu-item key="settings-server">服务器</a-menu-item>
|
||
<a-menu-item key="settings-workers">工作池</a-menu-item>
|
||
<a-menu-item key="settings-browser">浏览器</a-menu-item>
|
||
<a-menu-item key="settings-adapters">适配器</a-menu-item>
|
||
</a-sub-menu>
|
||
<a-sub-menu key="tools">
|
||
<template #title>
|
||
<span>
|
||
<ToolOutlined />
|
||
<span>系统管理</span>
|
||
</span>
|
||
</template>
|
||
<a-menu-item key="tools-display">虚拟显示器</a-menu-item>
|
||
<a-menu-item key="tools-cache">缓存与重启</a-menu-item>
|
||
<a-menu-item key="tools-logs">日志查看器</a-menu-item>
|
||
</a-sub-menu>
|
||
</a-menu>
|
||
</a-layout-sider>
|
||
<a-layout
|
||
:style="{ marginLeft: collapsed ? '80px' : '200px', padding: '16px', transition: 'margin-left 0.2s' }">
|
||
<a-layout-content style="min-height: 280px">
|
||
<router-view />
|
||
</a-layout-content>
|
||
<a-layout-footer class="footer" style="padding: 0px; margin-top: 10px;">
|
||
<a-card :bordered="false"
|
||
:bodyStyle="{ padding: '16px 24px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }">
|
||
<div>
|
||
<a href="https://github.com/foxhui/WebAI2API" target="_blank" style="color: #8c8c8c; font-size: 20px;">
|
||
<GithubOutlined />
|
||
</a>
|
||
</div>
|
||
</a-card>
|
||
</a-layout-footer>
|
||
</a-layout>
|
||
</a-layout>
|
||
</a-layout>
|
||
|
||
<!-- 接口测试抽屉 -->
|
||
<a-drawer v-model:open="apiTestDrawer" title="接口测试" placement="right" :width="500">
|
||
<a-space direction="vertical" style="width: 100%" size="large">
|
||
<!-- Models 接口 -->
|
||
<a-card title="GET /v1/models" size="small">
|
||
<template #extra>
|
||
<a-button size="small" type="primary" @click="testApi('models')"
|
||
:loading="apiTestResults.models.status === 'loading'">
|
||
测试
|
||
</a-button>
|
||
</template>
|
||
<div v-if="apiTestResults.models.status === 'success'">
|
||
<a-tag color="success">
|
||
<CheckCircleOutlined /> 成功
|
||
</a-tag>
|
||
<div style="margin-top: 8px; font-size: 12px; color: #8c8c8c;">
|
||
返回 {{ apiTestResults.models.data?.data?.length || 0 }} 个模型
|
||
</div>
|
||
</div>
|
||
<div v-else-if="apiTestResults.models.status === 'error'">
|
||
<a-tag color="error">
|
||
<CloseCircleOutlined /> 失败
|
||
</a-tag>
|
||
<div style="margin-top: 8px; font-size: 12px; color: #ff4d4f;">
|
||
{{ apiTestResults.models.error }}
|
||
</div>
|
||
</div>
|
||
<div v-else style="color: #8c8c8c; font-size: 12px;">点击测试按钮开始</div>
|
||
</a-card>
|
||
|
||
<!-- Cookies 接口 -->
|
||
<a-card title="GET /v1/cookies" size="small">
|
||
<template #extra>
|
||
<a-button size="small" type="primary" @click="testApi('cookies')"
|
||
:loading="apiTestResults.cookies.status === 'loading'">
|
||
测试
|
||
</a-button>
|
||
</template>
|
||
<div v-if="apiTestResults.cookies.status === 'success'">
|
||
<a-tag color="success">
|
||
<CheckCircleOutlined /> 成功
|
||
</a-tag>
|
||
<div style="margin-top: 8px; font-size: 12px; color: #8c8c8c;">
|
||
返回 {{ apiTestResults.cookies.data?.cookies?.length || 0 }} 个 Cookie
|
||
</div>
|
||
</div>
|
||
<div v-else-if="apiTestResults.cookies.status === 'error'">
|
||
<a-tag color="error">
|
||
<CloseCircleOutlined /> 失败
|
||
</a-tag>
|
||
<div style="margin-top: 8px; font-size: 12px; color: #ff4d4f;">
|
||
{{ apiTestResults.cookies.error }}
|
||
</div>
|
||
</div>
|
||
<div v-else style="color: #8c8c8c; font-size: 12px;">点击测试按钮开始</div>
|
||
</a-card>
|
||
|
||
<!-- Chat 接口 -->
|
||
<a-card title="POST /v1/chat/completions" size="small">
|
||
<template #extra>
|
||
<a-button size="small" type="primary" @click="testApi('chat')"
|
||
:loading="apiTestResults.chat.status === 'loading'" :disabled="!chatTestModel">
|
||
测试
|
||
</a-button>
|
||
</template>
|
||
|
||
<!-- 模型选择 -->
|
||
<div style="margin-bottom: 12px;">
|
||
<div style="font-size: 12px; color: #8c8c8c; margin-bottom: 4px;">模型</div>
|
||
<a-select v-model:value="chatTestModel" style="width: 100%" size="small" placeholder="选择模型" show-search>
|
||
<a-select-option v-for="model in chatModelList" :key="model.id" :value="model.id">
|
||
{{ model.id }}
|
||
</a-select-option>
|
||
</a-select>
|
||
</div>
|
||
|
||
<!-- 提示词 -->
|
||
<div style="margin-bottom: 12px;">
|
||
<div style="font-size: 12px; color: #8c8c8c; margin-bottom: 4px;">提示词</div>
|
||
<a-textarea v-model:value="chatTestPrompt" placeholder="输入提示词" :rows="2" size="small" />
|
||
</div>
|
||
|
||
<!-- 图片上传 -->
|
||
<div style="margin-bottom: 12px;">
|
||
<div style="font-size: 12px; color: #8c8c8c; margin-bottom: 4px;">
|
||
附加图片 ({{ chatImageList.length }}/10)
|
||
</div>
|
||
<a-upload-dragger :file-list="[]" :multiple="true" :before-upload="beforeUpload" @change="handleImageChange"
|
||
accept=".png,.jpg,.jpeg,.gif,.webp" :show-upload-list="false" style="padding: 8px;">
|
||
<p style="margin: 0;">
|
||
<InboxOutlined style="font-size: 24px; color: #1890ff;" />
|
||
</p>
|
||
<p style="font-size: 12px; margin: 4px 0 0 0; color: #8c8c8c;">
|
||
点击或拖拽上传图片 (PNG/JPEG/GIF/WebP)
|
||
</p>
|
||
</a-upload-dragger>
|
||
<div v-if="chatImageList.length > 0" style="margin-top: 8px; display: flex; flex-wrap: wrap; gap: 4px;">
|
||
<a-tag v-for="img in chatImageList" :key="img.uid" closable
|
||
@close="chatImageList = chatImageList.filter(i => i.uid !== img.uid)">
|
||
<PictureOutlined /> {{ img.name.slice(0, 15) }}{{ img.name.length > 15 ? '...' : '' }}
|
||
</a-tag>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 流式选项 -->
|
||
<div style="margin-bottom: 12px;">
|
||
<a-checkbox v-model:checked="chatStreamMode">流式响应</a-checkbox>
|
||
</div>
|
||
|
||
<!-- 测试结果 -->
|
||
<!-- 流式模式:实时显示内容 -->
|
||
<div v-if="chatStreamMode && apiTestResults.chat.status === 'loading'"
|
||
style="background: #fafafa; padding: 12px; border-radius: 4px; font-size: 12px; max-height: 400px; overflow-y: auto;">
|
||
<div style="color: #1890ff; margin-bottom: 8px;">
|
||
<LoadingOutlined /> 正在接收流式响应...
|
||
</div>
|
||
<!-- 文本内容 -->
|
||
<pre v-if="parseMarkdownImages(chatStreamContent).text"
|
||
style="white-space: pre-wrap; word-break: break-all; margin: 0 0 8px 0; min-height: 50px;">{{
|
||
parseMarkdownImages(chatStreamContent).text || '等待内容...' }}</pre>
|
||
<!-- 图片展示 -->
|
||
<div v-if="parseMarkdownImages(chatStreamContent).images.length > 0"
|
||
style="display: flex; flex-direction: column; gap: 8px;">
|
||
<div v-for="(img, index) in parseMarkdownImages(chatStreamContent).images" :key="index"
|
||
style="border: 1px solid #d9d9d9; border-radius: 4px; padding: 4px; background: white;">
|
||
<div style="font-size: 11px; color: #8c8c8c; margin-bottom: 4px;">{{ img.alt }}</div>
|
||
<img :src="img.src" :alt="img.alt"
|
||
style="max-width: 100%; height: auto; display: block; border-radius: 2px;" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div v-else-if="apiTestResults.chat.status === 'success'">
|
||
<a-tag color="success">
|
||
<CheckCircleOutlined /> 成功
|
||
</a-tag>
|
||
<div
|
||
style="margin-top: 8px; font-size: 12px; max-height: 400px; overflow-y: auto; background: #fafafa; padding: 8px; border-radius: 4px;">
|
||
<!-- 文本内容 -->
|
||
<pre v-if="parseMarkdownImages(apiTestResults.chat.data?.content).text"
|
||
style="white-space: pre-wrap; word-break: break-all; margin: 0 0 8px 0;">{{
|
||
parseMarkdownImages(apiTestResults.chat.data?.content).text }}</pre>
|
||
<!-- 图片展示 -->
|
||
<div v-if="parseMarkdownImages(apiTestResults.chat.data?.content).images.length > 0"
|
||
style="display: flex; flex-direction: column; gap: 8px;">
|
||
<div v-for="(img, index) in parseMarkdownImages(apiTestResults.chat.data?.content).images" :key="index"
|
||
style="border: 1px solid #d9d9d9; border-radius: 4px; padding: 4px; background: white;">
|
||
<div style="font-size: 11px; color: #8c8c8c; margin-bottom: 4px;">{{ img.alt }}</div>
|
||
<img :src="img.src" :alt="img.alt"
|
||
style="max-width: 100%; height: auto; display: block; border-radius: 2px;" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div v-else-if="apiTestResults.chat.status === 'error'">
|
||
<a-tag color="error">
|
||
<CloseCircleOutlined /> 失败
|
||
</a-tag>
|
||
<div style="margin-top: 8px; font-size: 12px; color: #ff4d4f;">
|
||
{{ apiTestResults.chat.error }}
|
||
</div>
|
||
</div>
|
||
<div v-else-if="apiTestResults.chat.status === 'loading' && !chatStreamMode"
|
||
style="color: #1890ff; font-size: 12px;">
|
||
<LoadingOutlined /> 请求中,可能需要较长时间...
|
||
</div>
|
||
</a-card>
|
||
</a-space>
|
||
</a-drawer>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
/* 滚动条美化 */
|
||
::-webkit-scrollbar {
|
||
width: 6px;
|
||
height: 6px;
|
||
}
|
||
|
||
::-webkit-scrollbar-thumb {
|
||
background: #ccc;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
::-webkit-scrollbar-track {
|
||
background: #f1f1f1;
|
||
}
|
||
</style>
|