Files
qhmes/jeecgboot-vue3/src/views/super/airag/aimodel/components/AiModelModal.vue

626 lines
21 KiB
Vue
Raw Normal View History

2026-04-03 09:56:14 +08:00
<template>
<BasicModal destroyOnClose @register="registerModal" :canFullscreen="false" width="600px" wrapClassName="ai-model-modal">
<div class="modal">
<div class="header">
<span class="header-title">
<span v-if="dataIndex ==='list' || dataIndex ==='add'" :class="dataIndex === 'list' ? '' : 'add-header-title pointer'" @click="goToList">
选择供应商
<a-tooltip title="供应商文档" v-if="dataIndex ==='list'">
<a style="color: #333333" href="https://help.jeecg.com/aigc/guide/model/#2-%E4%BE%9B%E5%BA%94%E5%95%86%E9%80%89%E6%8B%A9" target="_blank">
<Icon style="position:relative;left: -2px;top:1px" icon="ant-design:question-circle-outlined"></Icon>
</a>
</a-tooltip>
</span>
<span v-if="dataIndex === 'add'" class="add-header-title"> > </span>
<span v-if="dataIndex === 'add'" style="color: #1f2329">添加 {{ providerName }}</span>
</span>
<a-select v-if="dataIndex === 'list'" :bordered="false" class="header-select" size="small" v-model:value="modelType" @change="handleChange">
<a-select-option v-for="item in modelTypeOption" :value="item.value">{{ item.text }}</a-select-option>
</a-select>
</div>
<div class="model-content" v-if="dataIndex === 'list'">
<a-row :span="24">
<a-col :xxl="12" :xl="12" :lg="12" :md="12" :sm="12" :xs="24" v-for="item in modelTypeList">
<a-card class="model-card" @click="handleClick(item)">
<div class="model-header">
<div class="flex">
<img :src="getImage(item.value)" :class="['header-img', item.value === 'VLLM' ? 'header-img-lg' : '']" />
2026-04-03 09:56:14 +08:00
<div class="header-text">{{ item.title }}</div>
</div>
</div>
</a-card>
</a-col>
</a-row>
</div>
<a-tabs v-model:activeKey="activeKey" v-if="dataIndex === 'add' || dataIndex === 'edit'">
<a-tab-pane :key="1">
<template #tab>
<span style="display: flex">
基础信息
<a-tooltip title="基础信息文档">
<a @click.stop style="color: unset" href="https://help.jeecg.com/aigc/guide/model/#31-%E5%A1%AB%E5%86%99%E5%9F%BA%E7%A1%80%E4%BF%A1%E6%81%AF" target="_blank">
<Icon style="position:relative;left:2px;top:1px" icon="ant-design:question-circle-outlined"></Icon>
</a>
</a-tooltip>
</span>
</template>
<div class="model-content">
<BasicForm @register="registerForm">
<template #modelType="{ model, field }">
<a-select v-model:value="model[field]" @change="handleModelTypeChange" :disabled="modelTypeDisabled">
<a-select-option v-for="item in modelTypeAddOption" :value="item">
<span v-if="item === 'LLM'">语言模型</span>
<span v-else-if="item === 'EMBED'">向量模型</span>
<span v-else-if="item === 'IMAGE'">图像模型</span>
</a-select-option>
</a-select>
</template>
<template #extraParams="{ model, field }">
<a-input v-model:value="model[field]" readonly placeholder="点击右侧按钮编辑JSON参数">
<template #suffix>
<FullscreenOutlined style="cursor: pointer;" @click="openExtraParamsModal" />
</template>
</a-input>
<div style="margin-top: 4px; color: #999; font-size: 12px; line-height: 1.5;">
目前只支持图片传递固定格式为 "image_url":"图片地址"适用于qwen-vl-ocr等视觉模型<br/>
在qwen3.5-plus最新视觉模型中需要额外传递 "incremental_output": true
</div>
<a-modal v-model:open="extraParamsVisible" title="编辑额外参数" width="600px" @ok="saveExtraParams" destroyOnClose>
<JCodeEditor v-model:value="extraParamsTemp" language="javascript" fullScreen height="500px" />
</a-modal>
</template>
2026-04-03 09:56:14 +08:00
<template #modelName="{ model, field }">
<AutoComplete v-model:value="model[field]" :options="modelNameAddOption" :filter-option="filterOption">
<template #option="{ value, label, descr, type }">
<a-tooltip placement="right" color="#ffffff" :overlayInnerStyle="{ color:'#646a73' }">
<template #title>
<div v-html="getTitle(descr)"></div>
</template>
<div style="display: flex;justify-content: space-between;">
<span>{{label}}</span>
<div>
<a-tag v-if="type && type.split(',').includes('text')" color="#E8D7C3">文本</a-tag>
<a-tag v-if="type && type.split(',').includes('imageGen')" color="#FFEBD3">图像生成</a-tag>
<a-tag v-if="type && type.split(',').includes('image')" color="#C3D9DC">图像分析</a-tag>
<a-tag v-if="type && type.split(',').includes('vector')" color="#D4E0D8">向量</a-tag>
<a-tag v-if="type && type.split(',').includes('embeddings')" color="#FFEBD3">文本嵌入</a-tag>
</div>
</div>
</a-tooltip>
</template>
</AutoComplete>
</template>
</BasicForm>
<a-alert v-if="!modelActivate" message="模型未激活,请通过下方「保存并激活」按钮激活当前模型" type="warning" show-icon />
</div>
</a-tab-pane>
<a-tab-pane :key="2" v-if="modelParamsShow">
<template #tab>
<span style="display: flex">
高级配置
<a-tooltip title="高级配置文档">
<a @click.stop style="color: unset" href="https://help.jeecg.com/aigc/guide/model/#32-%E9%85%8D%E7%BD%AE%E9%AB%98%E7%BA%A7%E5%8F%82%E6%95%B0" target="_blank">
<Icon style="position:relative;left:2px;top:1px" icon="ant-design:question-circle-outlined"></Icon>
</a>
</a-tooltip>
</span>
</template>
<AiModelSeniorForm ref="modelParamsRef" :modelParams="modelParams"></AiModelSeniorForm>
</a-tab-pane>
</a-tabs>
</div>
<template v-if="dataIndex === 'add' || dataIndex === 'edit'" #footer>
<a-button @click="cancel">关闭</a-button>
<a-button @click="test" v-if="modelActivate" :loading="testLoading" type="default">测试连接</a-button>
<a-button @click="save" type="primary" ghost="true">保存</a-button>
<a-button @click="test(false)" v-if="!modelActivate" :loading="testLoading" type="primary" >保存并激活</a-button>
</template>
<template v-else #footer> </template>
</BasicModal>
</template>
<script lang="ts">
import { ref, reactive } from 'vue';
import BasicModal from '@/components/Modal/src/BasicModal.vue';
import { useModal, useModalInner } from '@/components/Modal';
import { initDictOptions } from '@/utils/dict';
import model from './model.json';
import { AutoComplete } from 'ant-design-vue';
import BasicForm from '@/components/Form/src/BasicForm.vue';
import { useForm } from '@/components/Form';
import { formSchema, imageList } from '../model.data';
import { editModel, queryById, saveModel, testConn } from '../model.api';
import { useMessage } from '/@/hooks/web/useMessage';
const {createMessage: $message, createConfirm} = useMessage();
import { FullscreenOutlined } from '@ant-design/icons-vue';
2026-04-03 09:56:14 +08:00
import AiModelSeniorForm from './AiModelSeniorForm.vue';
import JCodeEditor from '/@/components/Form/src/jeecg/components/JCodeEditor.vue';
2026-04-03 09:56:14 +08:00
import { cloneDeep } from "lodash-es";
export default {
name: 'AddModelModal',
components: {
BasicForm,
BasicModal,
AiModelSeniorForm,
AutoComplete,
JCodeEditor,
FullscreenOutlined,
2026-04-03 09:56:14 +08:00
},
emits: ['success', 'register'],
setup(props, { emit }) {
//ai类型数据
const modelTypeData = ref<any>([]);
//模型类型下拉框
const modelTypeOption = ref<any>([]);
//模型类型禁用状态
const modelTypeDisabled = ref<boolean>(false);
//模型类型
const modelType = ref<string>('all');
//模型供应商
const modelTypeList = ref<any>([]);
//list:供应商选择页面add 添加编辑
const dataIndex = ref<string>('list');
//供应商名称
const providerName = ref<string>('');
//添加模型类型的option
const modelTypeAddOption = ref<any>([]);
//添加模型名称的option
const modelNameAddOption = ref<any>([]);
//模型数据
const modelData = ref<any>({});
//tab切换对应的key
const activeKey = ref<number>(1);
//模型参数
const modelParams = ref<any>({});
//是否显示模型参数
const modelParamsShow = ref<boolean>(false);
//模型参数ref
const modelParamsRef = ref();
//测试按钮loading状态
const testLoading = ref<boolean>(false);
//模型是否已激活
const modelActivate = ref<boolean>(false);
//特殊参数
const extraParamsVisible = ref<boolean>(false);
const extraParamsTemp = ref<string>('');
/**
* 打开特殊参数编辑弹窗
*/
function openExtraParamsModal() {
const formVal = getFieldsValue();
let val = formVal.extraParams || '';
if (val) {
try {
val = JSON.stringify(JSON.parse(val), null, 2);
} catch (e) {}
}
extraParamsTemp.value = val;
extraParamsVisible.value = true;
}
/**
* 保存特殊参数
*/
function saveExtraParams() {
const val = extraParamsTemp.value;
if (val) {
try {
JSON.parse(val);
} catch (e) {
$message.error('JSON格式不正确请检查');
return;
}
}
setFieldsValue({ extraParams: val });
extraParamsVisible.value = false;
}
2026-04-03 09:56:14 +08:00
const getImage = (name) => {
return imageList.value[name];
};
//自动填充文本搜索事件
const filterOption = (input: string, option: any)=>{
return option.value.toUpperCase().indexOf(input.toUpperCase()) >= 0;
}
//表单配置
const [registerForm, { resetFields, setFieldsValue, getFieldsValue, validate, clearValidate }] = useForm({
2026-04-03 09:56:14 +08:00
schemas: formSchema,
showActionButtonGroup: false,
layout: 'vertical',
wrapperCol: { span: 24 },
labelCol: { span: 24 },
2026-04-03 09:56:14 +08:00
});
//注册modal
const [registerModal, { closeModal, setModalProps }] = useModalInner(async (data) => {
activeKey.value = 1;
modelParamsShow.value = false;
if(dataIndex.value !== 'list') {
//重置表单
await resetFields();
}
setModalProps({ minHeight: 500 });
if (data.id) {
dataIndex.value = 'edit';
let values = await queryById({ id: data.id });
if (values) {
if(values.result.credential){
let credential = JSON.parse(values.result.credential);
if(credential.secretKey){
values.result.secretKey = credential.secretKey;
}
if(credential.apiKey){
values.result.apiKey = credential.apiKey;
}
if (credential.httpVersionOne) {
values.result.httpVersionOne = credential.httpVersionOne;
}
2026-04-03 09:56:14 +08:00
}
let provider = values.result.provider;
let data = model.data.filter((item) => {
return item.value.includes(provider);
});
if (data && data.length > 0) {
modelTypeAddOption.value = data[0].type;
modelNameAddOption.value = data[0][values.result.modelType];
}
if(values.result.modelType && values.result.modelType === 'LLM'){
modelParamsShow.value = true;
}
if (values.result.activateFlag) {
modelActivate.value = true;
}else{
modelActivate.value = false;
}
if(values.result.modelParams){
let allParams = JSON.parse(values.result.modelParams);
if (allParams.extraParams) {
values.result.extraParams = JSON.stringify(allParams.extraParams, null, 2);
delete allParams.extraParams;
}
modelParams.value = allParams;
2026-04-03 09:56:14 +08:00
}
modelTypeDisabled.value = true;
//表单赋值
await setFieldsValue({
...values.result,
});
//初始化模型提供者
initModelProvider();
}
} else {
modelTypeDisabled.value = false;
//初始化模型提供者
initModelProvider();
dataIndex.value = 'list';
modelNameAddOption.value = [];
modelActivate.value = false;
}
});
//初始化模型类型
initModelTypeOption();
/**
* 初始化 模型类型字典
*/
function initModelTypeOption() {
initDictOptions('model_type').then((data) => {
modelTypeOption.value = cloneDeep(data);
//update-begin---author:wangshuai---date:2025-03-04---for: 解决页面tab刷新一次就多一个全部类型的选项---
if(data[0].value != 'all'){
modelTypeOption.value.unshift({
text: '全部类型',
value: 'all',
});
}
//update-end---author:wangshuai---date:2025-03-04---for: 解决页面tab刷新一次就多一个全部类型的选项---
});
}
/**
* 下拉框值选中事件
* @param value
*/
function handleChange(value) {
if ('all' == value) {
modelTypeList.value = model.data;
return;
}
let data = model.data.filter((item) => {
return item.type.includes(value);
});
modelTypeList.value = data;
}
/**
* 初始化模型提供者
*/
function initModelProvider() {
modelTypeList.value = model.data;
}
/**
* 供应商点击事件
*
* @param item
*/
function handleClick(item) {
dataIndex.value = 'add';
modelData.value = item;
providerName.value = item.title;
modelTypeAddOption.value = item.type;
setTimeout(()=>{
setFieldsValue({ 'provider': item.value, 'baseUrl': item.baseUrl })
},100)
}
/**
* 保存
*/
async function save() {
try {
setModalProps({ confirmLoading: true });
let values = await validate();
let credential = {
apiKey: values.apiKey,
secretKey: values.secretKey,
httpVersionOne: values.httpVersionOne,
2026-04-03 09:56:14 +08:00
}
let params = {};
2026-04-03 09:56:14 +08:00
if(modelParamsRef.value){
let seniorParams = modelParamsRef.value.emitChange();
if(seniorParams){
params = { ...seniorParams };
2026-04-03 09:56:14 +08:00
}
}
if (values.extraParams) {
try {
params.extraParams = JSON.parse(values.extraParams);
} catch(e) {}
}
if (Object.keys(params).length > 0) {
values.modelParams = JSON.stringify(params);
}
delete values.extraParams;
2026-04-03 09:56:14 +08:00
if(modelActivate.value){
values.activateFlag = 1
}else{
values.activateFlag = 0;
}
values.credential = JSON.stringify(credential);
//新增
if (!values.id) {
values.provider = modelData.value.value;
await saveModel(values);
closeModal();
emit('success');
} else {
await editModel(values);
closeModal();
emit('success');
}
}catch(e){
if(e.hasOwnProperty('errorFields')){
activeKey.value = 1;
}
} finally {
setModalProps({ confirmLoading: false });
}
}
/**
* 取消
*/
function cancel() {
dataIndex.value = 'list';
closeModal();
emit('success');
}
/**
* 测试连接
*/
async function test(onlyTest = false) {
try {
testLoading.value = true;
let values = await validate();
let credential = {
apiKey: values.apiKey,
secretKey: values.secretKey,
httpVersionOne: values.httpVersionOne,
2026-04-03 09:56:14 +08:00
};
let params = {};
2026-04-03 09:56:14 +08:00
if (modelParamsRef.value) {
//update-begin---author:wangshuai---date:2026-03-20---for:【issues/8】保存激活qwen-vl-ocr模型报错---
let seniorParams = modelParamsRef.value.emitChange();
if (seniorParams) {
params = { ...seniorParams };
//update-end---author:wangshuai---date:2026-03-20---for:【issues/8】保存激活qwen-vl-ocr模型报错---
2026-04-03 09:56:14 +08:00
}
}
if (values.extraParams) {
try {
params.extraParams = JSON.parse(values.extraParams);
} catch(e) {}
}
if (Object.keys(params).length > 0) {
values.modelParams = JSON.stringify(params);
}
delete values.extraParams;
2026-04-03 09:56:14 +08:00
values.credential = JSON.stringify(credential);
if (!values.provider) {
values.provider = modelData.value.value;
}
//测试
await testConn(values).then(async (result) => {
if(onlyTest){
$message.success('测试连接成功');
return true;
}
modelActivate.value = true;
await save();
});
} catch (e) {
if (e.hasOwnProperty('errorFields')) {
activeKey.value = 1;
}
} finally {
testLoading.value = false;
}
}
/**
* 模型类型选择事件
* @param value
*/
async function handleModelTypeChange(value) {
await setFieldsValue({ modelName: '' });
await clearValidate('modelName');
await setFieldsValue({
modelName: modelData.value[value+'DefaultValue']
})
modelNameAddOption.value = modelData.value[value];
if(value === 'LLM'){
modelParamsShow.value = true;
}else{
modelParamsShow.value = false;
}
if(value === "IMAGE" && modelData.value.baseImageUrl){
setFieldsValue({ 'baseUrl': modelData.value.baseImageUrl })
} else if(modelData.value.baseUrl) {
setFieldsValue({ 'baseUrl': modelData.value.baseUrl })
}
}
/**
* 选择供应商
*/
function goToList() {
if (dataIndex.value === 'add') {
dataIndex.value = 'list';
}
}
/**
* 获取标题
* @param title
*/
function getTitle(title) {
if(!title){
return "暂无描述内容";
}
return title.replaceAll("\n","<br>")
}
return {
registerModal,
modelTypeData,
modelTypeOption,
modelType,
handleChange,
modelTypeList,
getImage,
handleClick,
dataIndex,
providerName,
save,
cancel,
registerForm,
handleModelTypeChange,
modelTypeAddOption,
modelNameAddOption,
goToList,
modelTypeDisabled,
activeKey,
modelParams,
modelParamsShow,
modelActivate,
modelParamsRef,
filterOption,
getTitle,
test,
testLoading,
extraParamsVisible,
extraParamsTemp,
openExtraParamsModal,
saveExtraParams,
2026-04-03 09:56:14 +08:00
};
},
};
</script>
<style scoped lang="less">
.modal {
padding: 12px 20px 20px 20px;
.header {
padding: 0 24px 24px 0;
display: flex;
justify-content: space-between;
.header-title {
font-size: 16px;
font-weight: bold;
}
.header-select {
margin-right: 10px;
}
.add-header-title {
color: #646a73;
}
}
.model-content {
.model-header {
position: relative;
font-size: 14px;
.header-img {
width: 32px;
height: 32px;
margin-right: 12px;
object-fit: contain;
}
.header-img-lg {
width: 48px;
height: 48px;
2026-04-03 09:56:14 +08:00
}
.header-text {
width: calc(100% - 80px);
overflow: hidden;
align-content: center;
}
}
}
.model-card {
margin-right: 10px;
margin-bottom: 10px;
cursor: pointer;
}
}
:deep(.ant-card .ant-card-body) {
padding: 12px;
}
.pointer {
cursor: pointer;
}
:deep(.jeecg-basic-modal-close){
span{
margin-left: 0 !important;
}
}
</style>
<style lang="less">
.ai-model-modal{
.jeecg-basic-modal-close > span{
margin-left: 0 !important;
}
}
</style>