🎨 优化扩展模块,完成ai接入和对话功能

This commit is contained in:
2026-02-12 23:12:28 +08:00
parent 4e611d3a5e
commit 572f3aa15b
779 changed files with 194400 additions and 3136 deletions

View File

@@ -0,0 +1,253 @@
<template>
<el-dialog
:model-value="modelValue"
title="发送测试消息"
width="560px"
:close-on-click-modal="false"
@update:model-value="$emit('update:modelValue', $event)"
@open="onOpen"
@closed="onClosed"
>
<div class="test-message-content">
<!-- 提供商信息 -->
<div class="provider-info">
<span class="provider-label">{{ provider?.providerName }}</span>
<el-tag size="small">{{ getProviderTypeLabel(provider?.providerType || '') }}</el-tag>
</div>
<!-- 模型选择 -->
<el-form label-position="top">
<el-form-item label="选择模型">
<el-select
v-model="selectedModel"
placeholder="请选择要测试的模型"
style="width: 100%"
>
<el-option
v-for="model in chatModels"
:key="model.id"
:label="model.displayName || model.modelName"
:value="model.modelName"
/>
</el-select>
</el-form-item>
<!-- 消息内容 -->
<el-form-item label="测试消息">
<el-input
v-model="message"
type="textarea"
:rows="2"
placeholder="留空使用默认消息:你好,请用一句话介绍你自己。"
/>
</el-form-item>
</el-form>
<!-- 发送按钮 -->
<div class="send-section">
<el-button
type="primary"
:loading="sending"
:disabled="!selectedModel"
@click="handleSend"
>
<el-icon class="mr-1"><Promotion /></el-icon>
{{ sending ? '等待回复...' : '发送测试消息' }}
</el-button>
</div>
<!-- 测试结果 -->
<div v-if="testResult" class="result-section">
<div class="result-header" :class="{ success: testResult.success, error: !testResult.success }">
<el-icon v-if="testResult.success"><CircleCheckFilled /></el-icon>
<el-icon v-else><CircleCloseFilled /></el-icon>
<span>{{ testResult.message }}</span>
<span v-if="testResult.latency" class="result-meta">
{{ testResult.latency }}ms
</span>
<span v-if="testResult.tokens" class="result-meta">
{{ testResult.tokens }} tokens
</span>
<span v-if="testResult.model" class="result-meta">
模型: {{ testResult.model }}
</span>
</div>
<!-- AI 回复内容 -->
<div v-if="testResult.reply" class="result-reply">
<div class="reply-label">AI 回复</div>
<div class="reply-content">{{ testResult.reply }}</div>
</div>
</div>
</div>
<template #footer>
<el-button @click="$emit('update:modelValue', false)">关闭</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { Promotion, CircleCheckFilled, CircleCloseFilled } from '@element-plus/icons-vue'
import { useProviderStore } from '@/stores/provider'
const props = defineProps<{
modelValue: boolean
provider?: AIProvider | null
}>()
defineEmits<{
'update:modelValue': [value: boolean]
}>()
const store = useProviderStore()
const selectedModel = ref('')
const message = ref('')
const sending = ref(false)
const testResult = ref<SendTestMessageResponse | null>(null)
/** 只显示对话类型模型 */
const chatModels = computed(() => {
if (!props.provider) return []
return props.provider.models.filter(m => m.modelType === 'chat' && m.isEnabled)
})
function getProviderTypeLabel(type: string) {
const labels: Record<string, string> = {
openai: 'OpenAI / 兼容接口',
claude: 'Anthropic Claude',
gemini: 'Google Gemini',
custom: '自定义接口',
}
return labels[type] || type
}
/** 发送测试消息 */
async function handleSend() {
if (!props.provider || !selectedModel.value) return
sending.value = true
testResult.value = null
const result = await store.sendTestMessageExisting(
props.provider.id,
selectedModel.value,
message.value || undefined,
)
sending.value = false
if (result) {
testResult.value = result
} else {
testResult.value = {
success: false,
message: '请求失败,请检查网络',
reply: '',
model: '',
latency: 0,
tokens: 0,
}
}
}
/** 对话框打开 */
function onOpen() {
testResult.value = null
message.value = ''
// 默认选中第一个对话模型
if (chatModels.value.length > 0) {
selectedModel.value = chatModels.value[0].modelName
} else {
selectedModel.value = ''
}
}
/** 对话框关闭 */
function onClosed() {
testResult.value = null
sending.value = false
}
</script>
<style scoped lang="scss">
.test-message-content {
.provider-info {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
padding: 10px 14px;
background: var(--el-bg-color-page);
border-radius: 8px;
.provider-label {
font-weight: 500;
font-size: 14px;
}
}
.send-section {
margin-bottom: 16px;
}
.result-section {
border: 1px solid var(--el-border-color-lighter);
border-radius: 8px;
overflow: hidden;
.result-header {
display: flex;
align-items: center;
gap: 6px;
padding: 10px 14px;
font-size: 13px;
flex-wrap: wrap;
&.success {
background: var(--el-color-success-light-9);
color: var(--el-color-success);
}
&.error {
background: var(--el-color-danger-light-9);
color: var(--el-color-danger);
}
.result-meta {
font-size: 12px;
opacity: 0.8;
margin-left: 4px;
&::before {
content: '·';
margin-right: 4px;
}
}
}
.result-reply {
padding: 14px;
.reply-label {
font-size: 12px;
color: var(--el-text-color-secondary);
margin-bottom: 6px;
}
.reply-content {
font-size: 14px;
line-height: 1.7;
color: var(--el-text-color-primary);
background: var(--el-bg-color-page);
padding: 12px 14px;
border-radius: 8px;
white-space: pre-wrap;
word-break: break-word;
}
}
}
}
.mr-1 { margin-right: 4px; }
</style>