Files
mp-vue/src/views/ai/index.vue
赵忠林 d079a28ffc feat(ai): 添加AI模块文档和重构前端AI组件
- 新增 docs/ai/README.md 包含完整的AI模块配置、建表、API文档
- 重构 src/views/ai/index.vue 组件,移除硬编码BASE_URL和多余参数
- 添加 src/api/ai/backend.ts 统一的AI后端API接口实现
- 集成模型列表、流式对话、非流式对话等功能
- 实现SE流式响应处理和鉴权头自动携带
- 移除历史消息存储和温度参数等冗余功能
2026-02-28 11:02:40 +08:00

197 lines
5.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import { computed, onBeforeUnmount, ref } from 'vue';
import { message } from 'ant-design-vue';
import {
aiChat,
aiChatStream,
aiListModels,
normalizeModels
} from '@/api/ai/backend';
const modelLoading = ref(false);
const models = ref<Array<{ id: string; name?: string }>>([]);
const modelId = ref<string>('');
const userPrompt = ref<string>('你好,介绍一下你能做什么。');
const stream = ref<boolean>(true);
const sending = ref(false);
const assistantText = ref<string>('');
const errorText = ref<string>('');
const canSend = computed(() => {
return !!userPrompt.value.trim() && !sending.value;
});
const abortController = ref<AbortController | null>(null);
const stop = () => {
abortController.value?.abort();
abortController.value = null;
sending.value = false;
};
const loadModels = async () => {
modelLoading.value = true;
errorText.value = '';
try {
const res = await aiListModels();
const ms = normalizeModels(res);
models.value = ms
.map((m) => ({
id: String(m.name ?? m.id ?? ''),
name: String(m.name ?? m.id ?? '')
}))
.filter((m) => !!m.id);
if (!modelId.value && models.value.length) {
modelId.value = models.value[0].id;
}
} catch (e: any) {
errorText.value = e?.message ?? String(e);
message.error('加载模型列表失败');
} finally {
modelLoading.value = false;
}
};
const clearChat = () => {
assistantText.value = '';
errorText.value = '';
};
const send = async () => {
if (!canSend.value) return;
sending.value = true;
assistantText.value = '';
errorText.value = '';
const prompt = userPrompt.value.trim();
const controller = new AbortController();
abortController.value = controller;
try {
if (stream.value) {
await aiChatStream(
{ prompt },
{
signal: controller.signal,
onDelta: (t) => {
assistantText.value += t;
}
}
);
} else {
// axios 已在拦截器里自动带上 token/tenant
assistantText.value = await aiChat({ prompt });
}
userPrompt.value = '';
} catch (e: any) {
// Abort is expected when clicking "Stop".
if (e?.name !== 'AbortError') {
errorText.value = e?.message ?? String(e);
message.error('请求失败(可能是后端未启动 / 鉴权 / 租户头缺失)');
}
} finally {
sending.value = false;
abortController.value = null;
}
};
const modelOptions = computed(() => {
return models.value.map((m) => ({
label: m.name ?? m.id,
value: m.id
}));
});
// Load once for convenience; if the gateway blocks CORS you can still paste output from curl.
loadModels();
onBeforeUnmount(() => {
stop();
});
</script>
<template>
<div class="ele-body ele-body-card">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<a-space direction="vertical" style="width: 100%" :size="12">
<a-alert
type="info"
show-icon
message="AI 助手"
description="支持Qwen3.5、DeepSeek、Gemini3等主流的开源大模型免费使用"
/>
<a-row :gutter="12">
<a-col :xs="24" :md="12">
<a-select
v-model:value="modelId"
:options="modelOptions"
:loading="modelLoading"
placeholder="选择模型"
style="width: 100%"
/>
</a-col>
<a-col :xs="24" :md="12">
<a-space>
<a-button :loading="modelLoading" @click="loadModels"
>刷新模型</a-button
>
<span>流式</span>
<a-switch v-model:checked="stream" />
</a-space>
</a-col>
</a-row>
<!-- <a-textarea-->
<!-- v-model:value="systemPrompt"-->
<!-- :auto-size="{ minRows: 2, maxRows: 6 }"-->
<!-- placeholder="System Prompt可选"-->
<!-- />-->
<a-textarea
v-model:value="userPrompt"
:auto-size="{ minRows: 3, maxRows: 10 }"
placeholder="输入要问的问题..."
@pressEnter.exact.prevent="send"
/>
<a-space>
<a-button
type="primary"
:disabled="!canSend"
:loading="sending"
@click="send"
>发送</a-button
>
<a-button v-if="sending" danger @click="stop">停止</a-button>
<a-button @click="clearChat">清空</a-button>
</a-space>
<a-alert v-if="errorText" type="error" show-icon :message="errorText" />
<a-divider>输出</a-divider>
<a-card size="small" :bordered="true">
<pre class="output">{{ assistantText }}</pre>
</a-card>
<!-- <a-divider>历史最后一次请求</a-divider>-->
<!-- <a-card size="small" :bordered="true">-->
<!-- <pre class="output">{{ historyJson }}</pre>-->
<!-- </a-card>-->
</a-space>
</a-card>
</div>
</template>
<style scoped lang="less">
.output {
margin: 0;
white-space: pre-wrap;
word-break: break-word;
}
</style>