init
This commit is contained in:
549
src/views/knowledge/aiDataset/form.vue
Normal file
549
src/views/knowledge/aiDataset/form.vue
Normal file
@@ -0,0 +1,549 @@
|
||||
<template>
|
||||
<el-drawer v-model="visible" :title="form.id ? '编辑' : '新增'" size="50%">
|
||||
<el-form ref="dataFormRef" :model="form" :rules="dataRules" label-width="auto" v-loading="loading">
|
||||
<!-- Tab buttons -->
|
||||
<div role="tablist" class="mb-5 tabs tabs-boxed abs-bordered">
|
||||
<a role="tab" class="tab" :class="{ 'tab-active': activeTab === 'basic' }" @click="activeTab = 'basic'"
|
||||
><span class="inline-flex justify-center items-center mr-2 w-5 h-5 text-white rounded-full bg-primary">1</span>基础配置</a
|
||||
>
|
||||
<a role="tab" class="tab" :class="{ 'tab-active': activeTab === 'advanced' }" @click="activeTab = 'advanced'"
|
||||
><span class="inline-flex justify-center items-center mr-2 w-5 h-5 text-white rounded-full bg-primary">2</span>高级配置</a
|
||||
>
|
||||
<a role="tab" class="tab" :class="{ 'tab-active': activeTab === 'security' }" @click="activeTab = 'security'"
|
||||
><span class="inline-flex justify-center items-center mr-2 w-5 h-5 text-white rounded-full bg-primary">3</span>安全配置</a
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Basic Configuration Tab -->
|
||||
<div v-show="activeTab === 'basic'" class="px-2">
|
||||
<el-row :gutter="30">
|
||||
<el-col :span="12" class="mb-6">
|
||||
<el-form-item label="头像" prop="avatarUrl">
|
||||
<upload-img v-model:image-url="form.avatarUrl" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12" class="mb-6">
|
||||
<!-- Placeholder for alignment -->
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="30">
|
||||
<el-col :span="12" class="mb-6">
|
||||
<el-form-item label="名称" prop="name">
|
||||
<el-input v-model="form.name" maxlength="20" placeholder="请输入名称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12" class="mb-6">
|
||||
<el-form-item prop="sortOrder">
|
||||
<template #label>
|
||||
排序值
|
||||
<tip content="越大展示越靠前" />
|
||||
</template>
|
||||
<el-input-number v-model="form.sortOrder" :min="1" :max="9999" :step="1" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="30">
|
||||
<el-col :span="24" class="mb-6">
|
||||
<el-form-item label="欢迎语" prop="welcomeMsg">
|
||||
<el-input
|
||||
v-model="form.welcomeMsg"
|
||||
type="textarea"
|
||||
placeholder="描述知识库的内容,详尽的描述将帮助AI能深入理解该知识库的内容,能更准确的检索到内容,提高该知识库的命中率。"
|
||||
maxlength="1024"
|
||||
show-word-limit
|
||||
rows="10"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- Advanced Configuration Tab -->
|
||||
<div v-show="activeTab === 'advanced'" class="px-2">
|
||||
<el-divider content-position="left">模型配置</el-divider>
|
||||
|
||||
<el-row :gutter="30" class="mb-6">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="向量库" prop="storeId">
|
||||
<el-select v-model="form.storeId" placeholder="请选择向量库" clearable filterable :disabled="form.id !== ''">
|
||||
<el-option v-for="item in storeList" :key="item.storeId" :label="item.name" :value="item.storeId" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="向量模型" prop="embeddingModel">
|
||||
<el-select v-model="form.embeddingModel" placeholder="请选择向量模型" clearable filterable :disabled="form.id !== ''">
|
||||
<el-option v-for="item in embeddingModelList" :key="item.id" :label="item.name" :value="item.name" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="30" class="mb-6">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="总结模型" prop="summaryModel">
|
||||
<el-select v-model="form.summaryModel" placeholder="请选择总结模型" clearable filterable>
|
||||
<el-option v-for="item in chatModelList" :key="item.id" :label="item.name" :value="item.name" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="重排模型" prop="rerankerModel">
|
||||
<template #label>
|
||||
重排模型
|
||||
<tip content="重排模型,如没有重排模型,则后台使用默认重排算法" />
|
||||
</template>
|
||||
<el-select v-model="form.rerankerModel" placeholder="请选择重排模型" clearable filterable>
|
||||
<el-option v-for="item in rerankerModelList" :key="item.id" :label="item.name" :value="item.name" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-divider content-position="left">检索配置</el-divider>
|
||||
|
||||
<el-row :gutter="30" class="mb-6">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="会话轮数" prop="multiRound">
|
||||
<template #label>
|
||||
会话轮数
|
||||
<tip content="会话轮数,0代表不记忆上文会话" />
|
||||
</template>
|
||||
<el-input-number v-model="form.multiRound" :min="0" :max="5" :step="1" class="w-full" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="topK">
|
||||
<template #label>
|
||||
匹配条数
|
||||
<tip content="向量数据库匹配最多几条结果" />
|
||||
</template>
|
||||
<el-input-number v-model="form.topK" :min="1" :max="5" :step="1" class="w-full" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="30" class="mb-6">
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="fragmentSize">
|
||||
<template #label>
|
||||
分片值
|
||||
<tip content="分片值取决于模型自身能力,理论上分片值越大越准确" />
|
||||
</template>
|
||||
<el-input-number v-model="form.fragmentSize" :min="500" :max="9999" :step="1" class="w-full" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="score">
|
||||
<template #label>
|
||||
匹配率
|
||||
<tip content="向量数据库匹配率,建议不低于 50%" />
|
||||
</template>
|
||||
<el-slider v-model="form.score" :step="10" :min="10" :max="90" :format-tooltip="(value: number) => value + '%'" show-stops />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="30" class="mb-6">
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="emptyLlmFlag">
|
||||
<template #label>
|
||||
空查询
|
||||
<tip content="查询不到结果时,是否调用大模型查询" />
|
||||
</template>
|
||||
<el-switch v-model="form.emptyLlmFlag" :active-value="'1'" :inactive-value="'0'" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12" v-if="form.emptyLlmFlag !== '1'">
|
||||
<el-form-item prop="emptyDesc">
|
||||
<template #label>
|
||||
空提示
|
||||
<tip content="未匹配的时候,返回的文案" />
|
||||
</template>
|
||||
<el-input v-model="form.emptyDesc" placeholder="请输入描述" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-divider content-position="left">处理选项</el-divider>
|
||||
|
||||
<el-row :gutter="30" class="mb-6">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="文档总结" prop="preSummary">
|
||||
<el-switch v-model="form.preSummary" :active-value="'1'" :inactive-value="'0'" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="aiOcrFlag">
|
||||
<template #label>
|
||||
AI OCR
|
||||
<tip content="PDF、图片等文件,自动进行 AI OCR 识别" />
|
||||
</template>
|
||||
<el-switch v-model="form.aiOcrFlag" :active-value="'1'" :inactive-value="'0'" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="30" class="mb-6">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="会话压缩" prop="preCompress">
|
||||
<el-switch v-model="form.preCompress" :active-value="'1'" :inactive-value="'0'" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="standardFlag">
|
||||
<template #label>
|
||||
标注数据
|
||||
<tip content="使用已经标注修正后的答案,直接返回" />
|
||||
</template>
|
||||
<el-switch v-model="form.standardFlag" :active-value="'1'" :inactive-value="'0'" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- Security Configuration Tab -->
|
||||
<div v-show="activeTab === 'security'" class="px-2">
|
||||
<el-divider content-position="left">访问控制</el-divider>
|
||||
|
||||
<el-row :gutter="30" class="mb-6">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="是否对外" prop="publicFlag">
|
||||
<el-switch v-model="form.publicFlag" :active-value="'1'" :inactive-value="'0'" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="publicPassword">
|
||||
<template #label>
|
||||
安全密钥
|
||||
<tip content="对外服务,需要用户输入的密码" />
|
||||
</template>
|
||||
<el-input v-model="form.publicPassword" placeholder="请输入密码" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="30" class="mb-6">
|
||||
<el-col :span="24">
|
||||
<el-form-item prop="visibleUsers">
|
||||
<template #label>
|
||||
可见范围
|
||||
<tip content="选择可以访问此知识库的用户,如果不选择则全部的用户可访问" />
|
||||
</template>
|
||||
<org-selector
|
||||
v-model="form.visibleUsers"
|
||||
:type="'user'"
|
||||
:multiple="true"
|
||||
:selectSelf="true"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-divider content-position="left">内容过滤</el-divider>
|
||||
|
||||
<el-row :gutter="30" class="mb-6">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="敏感词过滤" prop="sensitiveFlag">
|
||||
<el-switch v-model="form.sensitiveFlag" :active-value="'1'" :inactive-value="'0'" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="sensitiveMsg">
|
||||
<template #label>
|
||||
提示
|
||||
<tip content="命中敏感词,返回的文案" />
|
||||
</template>
|
||||
<el-input v-model="form.sensitiveMsg" placeholder="请输入描述" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-divider content-position="left">附加信息</el-divider>
|
||||
|
||||
<el-row :gutter="30" class="mb-6">
|
||||
<el-col :span="24">
|
||||
<el-form-item label="底部信息" prop="footer">
|
||||
<el-input type="textarea" maxlength="255" :rows="5" v-model="form.footer" placeholder="聊天框底部的信息,支持 HTML 语法" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" @click="onSubmit" :disabled="loading">确认</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="AiDatasetDialog">
|
||||
// import { useDict } from '/@/hooks/dict'; // Removed as it's unused
|
||||
import { useMessage } from '/@/hooks/message';
|
||||
import { getDetails, addObj, putObj, validateName } from '/@/api/knowledge/aiDataset';
|
||||
import { list as aiModelList } from '/@/api/knowledge/aiModel';
|
||||
import { list } from '/@/api/knowledge/aiEmbedStore';
|
||||
import { rule } from '/@/utils/validate';
|
||||
import UploadImg from '/@/components/Upload/Image.vue';
|
||||
import OrgSelector from '/@/components/OrgSelector/index.vue';
|
||||
|
||||
const emit = defineEmits(['refresh']);
|
||||
|
||||
// 定义变量内容
|
||||
const dataFormRef = ref();
|
||||
const visible = ref(false);
|
||||
const loading = ref(false);
|
||||
const activeTab = ref('basic'); // Added for tab management
|
||||
|
||||
// 提交表单数据
|
||||
const form = reactive({
|
||||
id: '',
|
||||
name: '',
|
||||
avatarUrl: '',
|
||||
description: '',
|
||||
units: '0',
|
||||
fileSize: '0',
|
||||
multiRound: 3,
|
||||
topK: 2,
|
||||
score: 40,
|
||||
fragmentSize: 1000,
|
||||
sortOrder: 1,
|
||||
emptyLlmFlag: '0',
|
||||
emptyDesc: '知识库未匹配相关问题,请重新提问',
|
||||
sensitiveFlag: '1',
|
||||
preSummary: '0',
|
||||
preCompress: '0',
|
||||
sensitiveMsg: '您输入内容包含敏感词,请重新输入',
|
||||
publicFlag: '1',
|
||||
publicPassword: '' as string | undefined, // Allow undefined for publicPassword
|
||||
standardFlag: '0',
|
||||
aiOcrFlag: '1',
|
||||
welcomeMsg: '',
|
||||
footer: '',
|
||||
embeddingModel: '',
|
||||
summaryModel: '',
|
||||
storeId: '',
|
||||
rerankerModel: '',
|
||||
visibleUsers: [] as Array<any>, // 可见范围用户列表
|
||||
});
|
||||
|
||||
// 定义校验规则
|
||||
const dataRules = ref({
|
||||
name: [
|
||||
{ required: true, message: '知识库名称不能为空', trigger: 'blur' },
|
||||
{
|
||||
validator: (rule: any, value: any, callback: any) => {
|
||||
validateName(rule, value, callback, form.id !== '');
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
welcomeMsg: [{ required: true, message: '欢迎语不能为空', trigger: 'blur' }],
|
||||
multiRound: [{ required: true, message: '多轮会话不能为空', trigger: 'blur' }],
|
||||
topK: [{ required: true, message: '多轮会话不能为空', trigger: 'blur' }],
|
||||
avatarUrl: [{ required: true, message: '封面不能为空', trigger: 'blur' }],
|
||||
emptyDesc: [
|
||||
{ validator: rule.overLength, trigger: 'blur' },
|
||||
{
|
||||
required: true,
|
||||
message: '提示不能为空',
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
sensitiveMsg: [
|
||||
{ validator: rule.overLength, trigger: 'blur' },
|
||||
{
|
||||
required: true,
|
||||
message: '提示不能为空',
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
storeId: [{ required: true, message: '请选择向量库', trigger: 'change' }],
|
||||
embeddingModel: [{ required: true, message: '请选择向量模型', trigger: 'change' }],
|
||||
summaryModel: [{ required: true, message: '请选择总结模型', trigger: 'change' }],
|
||||
rerankerModel: [
|
||||
{
|
||||
validator: rule.overLength,
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const embeddingModelList = ref<Array<{ id: string; name: string }>>([]);
|
||||
const chatModelList = ref<Array<{ id: string; name: string }>>([]);
|
||||
const storeList = ref<Array<{ storeId: string; name: string }>>([]);
|
||||
const rerankerModelList = ref<Array<{ id: string; name: string }>>([]);
|
||||
|
||||
// Modify the function to fetch AI models based on modelType
|
||||
async function loadAiModelList() {
|
||||
try {
|
||||
const [embeddingResponse, chatResponse, rerankerResponse] = await Promise.all([
|
||||
aiModelList({ modelType: 'Embedding' }),
|
||||
aiModelList({ modelType: ['Chat', 'Reason'] }),
|
||||
aiModelList({ modelType: 'Reranker' }),
|
||||
]);
|
||||
|
||||
embeddingModelList.value = embeddingResponse.data.map((item: any) => ({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
}));
|
||||
|
||||
chatModelList.value = chatResponse.data.map((item: any) => ({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
}));
|
||||
|
||||
rerankerModelList.value = rerankerResponse.data.map((item: any) => ({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
}));
|
||||
|
||||
// Set default values if lists are not empty
|
||||
if (embeddingModelList.value.length > 0 && !form.embeddingModel) {
|
||||
form.embeddingModel = embeddingModelList.value[0].name;
|
||||
}
|
||||
if (chatModelList.value.length > 0 && !form.summaryModel) {
|
||||
form.summaryModel = chatModelList.value[0].name;
|
||||
}
|
||||
if (rerankerModelList.value.length > 0 && !form.rerankerModel) {
|
||||
form.rerankerModel = rerankerModelList.value[0].name;
|
||||
}
|
||||
} catch (error) {
|
||||
useMessage().error('加载AI模型列表失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化表单数据
|
||||
const getAiDatasetData = (id: string) => {
|
||||
// 获取数据
|
||||
loading.value = true;
|
||||
getDetails({ id })
|
||||
.then((res: any) => {
|
||||
Object.assign(form, res.data);
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
// Modify the openDialog function
|
||||
const openDialog = async (id: string) => {
|
||||
visible.value = true;
|
||||
form.id = '';
|
||||
|
||||
// Reset form data
|
||||
nextTick(() => {
|
||||
dataFormRef.value?.resetFields();
|
||||
});
|
||||
|
||||
// Load the collection list and AI model list
|
||||
await loadAiModelList();
|
||||
|
||||
// 初始化向量库列表
|
||||
const { data } = await list();
|
||||
storeList.value = data;
|
||||
|
||||
// Set default value for storeId if list is not empty
|
||||
if (storeList.value.length > 0 && !form.storeId) {
|
||||
form.storeId = storeList.value[0].storeId;
|
||||
}
|
||||
|
||||
// Get aiDataset information
|
||||
if (id) {
|
||||
form.id = id;
|
||||
getAiDatasetData(id);
|
||||
} else {
|
||||
// Set default values for new records
|
||||
if (embeddingModelList.value.length > 0) {
|
||||
form.embeddingModel = embeddingModelList.value[0].name;
|
||||
}
|
||||
if (chatModelList.value.length > 0) {
|
||||
form.summaryModel = chatModelList.value[0].name;
|
||||
}
|
||||
if (rerankerModelList.value.length > 0) {
|
||||
form.rerankerModel = rerankerModelList.value[0].name;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 提交
|
||||
const onSubmit = async () => {
|
||||
try {
|
||||
// 步骤 1: 异步验证整个表单
|
||||
// dataFormRef.value.validate() 会返回一个 Promise
|
||||
// 如果验证失败,它会抛出错误,该错误将被下面的 catch 块捕获
|
||||
await dataFormRef.value.validate();
|
||||
|
||||
// 步骤 2: 如果验证通过,则继续执行提交逻辑
|
||||
// 特殊处理:如果存在 ID (即为编辑模式) 且密码字段包含 '**' (表示未修改),
|
||||
// 则将密码设置为空,避免将占位符提交到后端
|
||||
if (form.id && form.publicPassword?.includes('**')) {
|
||||
form.publicPassword = undefined;
|
||||
}
|
||||
|
||||
loading.value = true; // 开始加载状态,禁用提交按钮等
|
||||
// 根据是否存在 form.id 来判断是更新 (putObj) 还是新增 (addObj)
|
||||
form.id ? await putObj(form) : await addObj(form);
|
||||
|
||||
// 操作成功提示
|
||||
useMessage().success(form.id ? '修改成功' : '添加成功');
|
||||
visible.value = false; // 关闭抽屉(或弹窗)
|
||||
emit('refresh'); // 触发 'refresh' 事件,通知父组件刷新列表数据
|
||||
} catch (invalidFields: any) {
|
||||
// 步骤 3: 处理表单验证失败的情况
|
||||
// invalidFields 对象包含了所有未通过验证的字段及其错误信息
|
||||
// Element Plus 的 validate 方法在验证失败时会 reject 一个包含错误字段的对象
|
||||
if (invalidFields && Object.keys(invalidFields).length > 0) {
|
||||
const errorFields: string[] = Object.keys(invalidFields); // 获取所有验证失败的字段名
|
||||
|
||||
// 定义各选项卡包含的字段,用于定位错误字段所在的选项卡
|
||||
const tabFields: Record<string, string[]> = {
|
||||
basic: ['name', 'avatarUrl', 'welcomeMsg', 'sortOrder'], // 基础配置选项卡下的字段
|
||||
advanced: [
|
||||
'storeId',
|
||||
'embeddingModel',
|
||||
'summaryModel',
|
||||
'multiRound',
|
||||
'topK',
|
||||
'fragmentSize',
|
||||
'score',
|
||||
'emptyLlmFlag',
|
||||
'emptyDesc',
|
||||
'preSummary',
|
||||
'preCompress',
|
||||
'aiOcrFlag',
|
||||
'standardFlag',
|
||||
'rerankerModel',
|
||||
], // 高级配置选项卡下的字段
|
||||
security: ['publicFlag', 'publicPassword', 'visibleUsers', 'sensitiveFlag', 'sensitiveMsg', 'footer'], // 安全配置选项卡下的字段
|
||||
};
|
||||
|
||||
// 遍历 tabFields 来确定哪个选项卡包含第一个出错的字段
|
||||
for (const [tab, fieldsInTab] of Object.entries(tabFields)) {
|
||||
// .some() 方法检查 errorFields 数组中是否有任何一个字段存在于当前 fieldsInTab 数组中
|
||||
if (errorFields.some((errorField: string) => fieldsInTab.includes(errorField))) {
|
||||
activeTab.value = tab; // 如果找到,则切换到该选项卡
|
||||
break; // 找到第一个错误字段所在的选项卡后,停止遍历
|
||||
}
|
||||
}
|
||||
}
|
||||
// 如果验证失败,错误被捕获,提交过程在此处停止。
|
||||
// 函数将继续执行 finally 块。
|
||||
} finally {
|
||||
// 步骤 4: 无论提交成功还是失败,最后都会执行 finally 块
|
||||
loading.value = false; // 结束加载状态
|
||||
}
|
||||
};
|
||||
|
||||
// 暴露变量
|
||||
defineExpose({
|
||||
openDialog,
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user