接入真实模型调用与参考文件上传

This commit is contained in:
zwt13703
2026-07-02 15:16:34 +08:00
parent b313083766
commit 5a43cc70d5
9 changed files with 420 additions and 53 deletions
+1
View File
@@ -2,6 +2,7 @@ import http from './index'
export const generateApi = {
test: (data: any) => http.post('/generate/test', data),
upload: (formData: FormData) => http.post('/generate/upload', formData, { headers: { 'Content-Type': 'multipart/form-data' } }),
full: (data: any) => http.post('/generate/full', data),
progress: (id: number) => `/api/v1/generate/progress/${id}`,
cancel: (id: number) => http.post(`/generate/cancel/${id}`),
+7 -4
View File
@@ -1,9 +1,10 @@
<template><div style="padding:24px;display:flex;gap:24px;height:calc(100vh - 112px)"><div style="width:340px;flex-shrink:0"><a-card title="选择模板"><a-select style="width:100%" v-model:value="selectedTplId" placeholder="请选择已编辑好的模板" @change="onTplChange"><a-select-option v-for="t in templates" :key="t.id" :value="t.id">{{t.name}}</a-select-option></a-select><a-divider /><a-statistic title="总段落" :value="tplInfo.paragraph_count||0" /><a-statistic title="需上传文件" :value="tplInfo.fileCount||0" suffix="/"+String(tplInfo.paragraph_count||0) /></a-card><div v-if="generating" style="margin-top:16px"><a-card title="生成进度"><a-progress :percent="progress" /><p>{{progressText}}</p></a-card></div></div><div style="flex:1;display:flex;flex-direction:column"><a-card title="段落文件配置" style="flex:1"><div v-for="p in paragraphs" :key="p.id" :class="['para-row',{needFile:p.need_file,noFile:!p.need_file}]"><div class="para-info"><span class="idx">{{p.sort_index}}</span><span>{{p.title}}</span><a-tag color="blue">{{p.modelName||"默认"}}</a-tag></div><div v-if="p.need_file" class="file-info"><a-upload :beforeUpload="(f: File)=>{return handleFileUpload(p.id,f)}" :showUploadList="false"><a-button size="small">{{uploadedFiles[p.id]?"已上传":"上传文件"}}</a-button></a-upload><span v-if="uploadedFiles[p.id]" style="color:green;margin-left:8px">{{uploadedFiles[p.id]}}</span></div><span v-else class="no-file-tag">无需上传</span></div></a-card><div style="margin-top:16px;display:flex;justify-content:space-between;align-items:center"><span>{{fileCount}}/{{needFileCount}} 个文件已上传</span><a-button type="primary" size="large" :loading="generating" @click="startGen">立即生成</a-button></div></div></div></template>
<template><div style="padding:24px;display:flex;gap:24px;height:calc(100vh - 112px)"><div style="width:340px;flex-shrink:0"><a-card title="选择模板"><a-select style="width:100%" v-model:value="selectedTplId" placeholder="请选择已编辑好的模板" @change="onTplChange"><a-select-option v-for="t in templates" :key="t.id" :value="t.id">{{t.name}}</a-select-option></a-select><a-divider /><a-statistic title="总段落" :value="tplInfo.paragraph_count||0" /><a-statistic title="需上传文件" :value="tplInfo.fileCount||0" suffix="/"+String(tplInfo.paragraph_count||0) /></a-card><div v-if="generating" style="margin-top:16px"><a-card title="生成进度"><a-progress :percent="progress" /><p>{{progressText}}</p></a-card></div></div><div style="flex:1;display:flex;flex-direction:column"><a-card title="段落文件配置" style="flex:1"><div v-for="p in paragraphs" :key="p.id" :class="['para-row',{needFile:p.need_file,noFile:!p.need_file}]"><div class="para-info"><span class="idx">{{p.sort_index}}</span><span>{{p.title}}</span><a-tag color="blue">{{p.modelName||"默认"}}</a-tag></div><div v-if="p.need_file" class="file-info"><a-upload :beforeUpload="(f: File)=>{return handleFileUpload(p.id,f)}" :showUploadList="false"><a-button size="small" :loading="uploadingMap[p.id]">{{uploadedFiles[p.id]?"已上传":"上传文件"}}</a-button></a-upload><span v-if="uploadedFiles[p.id]" style="color:green;margin-left:8px">{{uploadedFiles[p.id]}}</span></div><span v-else class="no-file-tag">无需上传</span></div></a-card><div style="margin-top:16px;display:flex;justify-content:space-between;align-items:center"><span>{{fileCount}}/{{needFileCount}} 个文件已上传</span><a-button type="primary" size="large" :loading="generating" @click="startGen">立即生成</a-button></div></div></div></template>
<script setup lang="ts">
import { ref, computed, onMounted } from "vue";
import { useRouter } from "vue-router";
import { useTemplateStore } from "@/stores/template";
import { useDocumentStore } from "@/stores/document";
import { generateApi } from "@/api/generate";
import { message } from "ant-design-vue";
const router = useRouter();
const tplStore = useTemplateStore();
@@ -12,6 +13,8 @@ const templates = ref<any[]>([]);
const paragraphs = ref<any[]>([]);
const selectedTplId = ref(undefined);
const uploadedFiles = ref<Record<number,string>>({});
const uploadedFilePaths = ref<Record<number,string>>({});
const uploadingMap = ref<Record<number,boolean>>({});
const generating = ref(false);
const progress = ref(0);
const progressText = ref("");
@@ -19,8 +22,8 @@ const tplInfo = ref<any>({});
const needFileCount = computed(()=>paragraphs.value.filter(p=>p.need_file).length);
const fileCount = computed(()=>Object.keys(uploadedFiles.value).length);
onMounted(async()=>{await tplStore.fetchList();templates.value=tplStore.templates as any});
async function onTplChange(id:number){const tpl=await tplStore.fetchOne(id);paragraphs.value=tplStore.paragraphs as any;tplInfo.value={paragraph_count:tpl.paragraph_count,fileCount:paragraphs.value.filter((p:any)=>p.need_file).length}}
function handleFileUpload(paraId:number,file:File){uploadedFiles.value[paraId]=file.name;return false}
async function startGen(){if(!selectedTplId.value){message.warning("请先选择模板");return}generating.value=true;progress.value=0;progressText.value="正在生成...";const data={template_id:selectedTplId.value,file_map:{}};try{const doc=await docStore.generateFull(data);message.success("生成完成");router.push(`/preview/${doc.id}`)}catch(e:any){message.error(e.message||"生成失败")}finally{generating.value=false}}
async function onTplChange(id:number){const tpl=await tplStore.fetchOne(id);paragraphs.value=tplStore.paragraphs as any;tplInfo.value={paragraph_count:tpl.paragraph_count,fileCount:paragraphs.value.filter((p:any)=>p.need_file).length};uploadedFiles.value={};uploadedFilePaths.value={};}
async function handleFileUpload(paraId:number,file:File){try{uploadingMap.value[paraId]=true;const fd=new FormData();fd.append("file",file);const res:any=await generateApi.upload(fd);uploadedFiles.value[paraId]=res.data.file_name;uploadedFilePaths.value[paraId]=res.data.file_path;message.success("文件上传成功")}catch(e:any){message.error(e.message||"文件上传失败")}finally{uploadingMap.value[paraId]=false}return false}
async function startGen(){if(!selectedTplId.value){message.warning("请先选择模板");return}const missing=paragraphs.value.filter((p:any)=>p.need_file&&!uploadedFilePaths.value[p.id]);if(missing.length){message.warning("还有必传文件未上传");return}generating.value=true;progress.value=30;progressText.value="正在生成...";const fileMap=Object.fromEntries(Object.entries(uploadedFilePaths.value).map(([k,v])=>[String(k),v]));const data={template_id:selectedTplId.value,file_map:fileMap};try{const doc=await docStore.generateFull(data);progress.value=100;progressText.value="生成完成";message.success("生成完成");router.push(`/preview/${doc.id}`)}catch(e:any){message.error(e.message||"生成失败")}finally{generating.value=false}}
</script>
<style scoped>.para-row{display:flex;align-items:center;justify-content:space-between;padding:12px;border:1px solid #f0f0f0;border-radius:8px;margin-bottom:8px}.para-row.needFile{background:#fff;border-color:#d9d9d9}.para-row.noFile{background:#fafafa;border-style:dashed;opacity:.7}.para-info{display:flex;align-items:center;gap:8px}.para-info .idx{width:22px;height:22px;border-radius:50%;background:#f0f0ff;color:#5b5bd6;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700}.no-file-tag{font-size:12px;color:#999}</style>
+126 -15
View File
@@ -1,17 +1,128 @@
<template><div style="padding:24px"><div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px"><h2>模型管理</h2><a-button type="primary" @click="openAdd">添加模型</a-button></div><a-row :gutter="[16,16]"><a-col :span="8" v-for="m in models" :key="m.id"><a-card :title="m.name"><template #extra><a-button type="link" size="small" @click="openEdit(m)">编辑</a-button></template><p>厂商:{{m.provider}}</p><p>格式:{{m.api_format}}</p><p>地址:{{m.api_endpoint}}</p><p>状态:<a-switch :checked="m.status==='enabled'" @change="toggleStatus(m)" /></p></a-card></a-col></a-row><a-modal v-model:open="modalOpen" :title="isEdit?'编辑模型':'添加模型'" @ok="saveModel"><a-form layout="vertical"><a-form-item label="模型名称"><a-input v-model:value="form.name" /></a-form-item><a-form-item label="供应厂商"><a-input v-model:value="form.provider" /></a-form-item><a-form-item label="API 格式"><a-select v-model:value="form.api_format"><a-select-option value="openai">OpenAI 格式</a-select-option><a-select-option value="anthropic">Anthropic 格式</a-select-option></a-select></a-form-item><a-form-item label="API 地址"><a-input v-model:value="form.api_endpoint" placeholder="https://api.xxx.com" /></a-form-item><a-form-item label="API Key"><a-input-password v-model:value="form.api_key" /></a-form-item></a-form></a-modal></div></template>
<template>
<div style="padding:24px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
<h2>模型管理</h2>
<a-button type="primary" @click="openAdd">添加模型</a-button>
</div>
<a-row :gutter="[16, 16]">
<a-col :span="8" v-for="item in models" :key="item.id">
<a-card :title="item.name">
<template #extra>
<a-button type="link" size="small" @click="openEdit(item)">编辑</a-button>
</template>
<p>厂商{{ item.provider }}</p>
<p>格式{{ item.api_format }}</p>
<p>地址{{ item.api_endpoint }}</p>
<p>密钥{{ item.api_key_preview || '未设置' }}</p>
<p>状态<a-switch :checked="item.status === 'enabled'" @change="toggleStatus(item)" /></p>
<div style="margin-top:12px;display:flex;gap:8px">
<a-button size="small" @click="runTest(item)">连接测试</a-button>
<a-button danger size="small" @click="removeModel(item.id)">删除</a-button>
</div>
</a-card>
</a-col>
</a-row>
<a-modal v-model:open="modalOpen" :title="isEdit ? '编辑模型' : '添加模型'" @ok="saveModel">
<a-form layout="vertical">
<a-form-item label="模型名称">
<a-input v-model:value="form.name" placeholder="如 gpt-4o-mini / deepseek-chat / claude-3-5-sonnet-latest" />
</a-form-item>
<a-form-item label="供应厂商">
<a-input v-model:value="form.provider" />
</a-form-item>
<a-form-item label="API 格式">
<a-select v-model:value="form.api_format">
<a-select-option value="openai">OpenAI 格式</a-select-option>
<a-select-option value="anthropic">Anthropic 格式</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="API 地址">
<a-input v-model:value="form.api_endpoint" placeholder="https://api.openai.com 或兼容网关地址" />
</a-form-item>
<a-form-item label="API Key">
<a-input-password v-model:value="form.api_key" placeholder="编辑时留空表示保持原密钥不变" />
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { useModelStore } from "@/stores/model";
import { message } from "ant-design-vue";
const store = useModelStore();
const models = ref<any[]>([]);
const modalOpen = ref(false);
const isEdit = ref(false);
const editId = ref(0);
const form = ref({name:"",provider:"",api_format:"openai",api_endpoint:"",api_key:""});
onMounted(async()=>{await store.fetchList();models.value=store.models as any});
function openAdd(){isEdit.value=false;form.value={name:"",provider:"",api_format:"openai",api_endpoint:"",api_key:""};modalOpen.value=true}
function openEdit(m:any){isEdit.value=true;editId.value=m.id;form.value={name:m.name,provider:m.provider,api_format:m.api_format,api_endpoint:m.api_endpoint,api_key:""};modalOpen.value=true}
async function saveModel(){if(isEdit.value){await store.update(editId.value,form.value)}else{await store.create(form.value)}modalOpen.value=false;models.value=store.models as any;message.success("保存成功")}
async function toggleStatus(m:any){m.status=m.status==="enabled"?"disabled":"enabled";await store.update(m.id,{status:m.status});message.success("状态已更新")}
import { ref, onMounted } from 'vue'
import { Modal, message } from 'ant-design-vue'
import { useModelStore } from '@/stores/model'
const store = useModelStore()
const models = ref<any[]>([])
const modalOpen = ref(false)
const isEdit = ref(false)
const editId = ref(0)
const form = ref({ name: '', provider: '', api_format: 'openai', api_endpoint: '', api_key: '' })
async function refreshList() {
await store.fetchList()
models.value = store.models as any
}
onMounted(async () => {
await refreshList()
})
function openAdd() {
isEdit.value = false
form.value = { name: '', provider: '', api_format: 'openai', api_endpoint: '', api_key: '' }
modalOpen.value = true
}
function openEdit(item: any) {
isEdit.value = true
editId.value = item.id
form.value = {
name: item.name,
provider: item.provider,
api_format: item.api_format,
api_endpoint: item.api_endpoint,
api_key: '',
}
modalOpen.value = true
}
async function saveModel() {
if (isEdit.value) {
await store.update(editId.value, form.value)
} else {
await store.create(form.value)
}
modalOpen.value = false
await refreshList()
message.success('保存成功')
}
async function toggleStatus(item: any) {
const nextStatus = item.status === 'enabled' ? 'disabled' : 'enabled'
await store.update(item.id, { status: nextStatus })
await refreshList()
message.success('状态已更新')
}
async function runTest(item: any) {
try {
const result: any = await store.test(item.id)
Modal.info({
title: '连接测试结果',
width: 640,
content: JSON.stringify(result.data, null, 2),
})
} catch (error: any) {
message.error(error.message || '连接测试失败')
}
}
async function removeModel(id: number) {
await store.remove(id)
await refreshList()
message.success('模型已删除')
}
</script>