初始化
This commit is contained in:
285
easyflow-ui-admin/app/src/views/ai/plugin/AddPluginModal.vue
Normal file
285
easyflow-ui-admin/app/src/views/ai/plugin/AddPluginModal.vue
Normal file
@@ -0,0 +1,285 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from 'element-plus';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { Plus, Remove } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElButton,
|
||||
ElDialog,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElIcon,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
ElOption,
|
||||
ElRadio,
|
||||
ElRadioGroup,
|
||||
ElSelect,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import UploadAvatar from '#/components/upload/UploadAvatar.vue';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const emit = defineEmits(['reload']);
|
||||
const embeddingLlmList = ref<any>([]);
|
||||
const rerankerLlmList = ref<any>([]);
|
||||
interface headersType {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
const authTypeList = ref<headersType[]>([
|
||||
{
|
||||
label: 'None',
|
||||
value: 'none',
|
||||
},
|
||||
{
|
||||
label: 'Service token / ApiKey',
|
||||
value: 'apiKey',
|
||||
},
|
||||
]);
|
||||
onMounted(() => {
|
||||
api.get('/api/v1/model/list?supportEmbed=true').then((res) => {
|
||||
embeddingLlmList.value = res.data;
|
||||
});
|
||||
api.get('/api/v1/model/list?supportRerankerLlmList=true').then((res) => {
|
||||
rerankerLlmList.value = res.data;
|
||||
});
|
||||
});
|
||||
defineExpose({
|
||||
openDialog,
|
||||
});
|
||||
const saveForm = ref<FormInstance>();
|
||||
// variables
|
||||
const dialogVisible = ref(false);
|
||||
const isAdd = ref(true);
|
||||
const tempAddHeaders = ref<headersType[]>([]);
|
||||
const entity = ref<any>({
|
||||
alias: '',
|
||||
deptId: '',
|
||||
icon: '',
|
||||
title: '',
|
||||
authType: 'none',
|
||||
description: '',
|
||||
englishName: '',
|
||||
headers: '',
|
||||
position: '',
|
||||
});
|
||||
const btnLoading = ref(false);
|
||||
const rules = ref({
|
||||
name: [{ required: true, message: $t('message.required'), trigger: 'blur' }],
|
||||
description: [
|
||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
||||
],
|
||||
baseUrl: [
|
||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
||||
],
|
||||
authType: [
|
||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
||||
],
|
||||
tokenKey: [
|
||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
||||
],
|
||||
tokenValue: [
|
||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
||||
],
|
||||
position: [
|
||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
||||
],
|
||||
});
|
||||
|
||||
// functions
|
||||
function openDialog(row: any) {
|
||||
if (row.id) {
|
||||
isAdd.value = false;
|
||||
if (row.headers) {
|
||||
tempAddHeaders.value = JSON.parse(row.headers);
|
||||
}
|
||||
}
|
||||
entity.value = {
|
||||
...row,
|
||||
authType: row.authType || 'none',
|
||||
};
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
function save() {
|
||||
saveForm.value?.validate((valid) => {
|
||||
if (valid) {
|
||||
const plainEntity = { ...entity.value };
|
||||
const plainHeaders = [...tempAddHeaders.value];
|
||||
if (isAdd.value) {
|
||||
api
|
||||
.post('/api/v1/plugin/plugin/save', {
|
||||
...plainEntity,
|
||||
headers: plainHeaders,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
dialogVisible.value = false;
|
||||
ElMessage.success($t('message.saveOkMessage'));
|
||||
emit('reload');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
api
|
||||
.post('/api/v1/plugin/plugin/update', {
|
||||
...plainEntity,
|
||||
headers: plainHeaders,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
dialogVisible.value = false;
|
||||
ElMessage.success($t('message.updateOkMessage'));
|
||||
emit('reload');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
function closeDialog() {
|
||||
saveForm.value?.resetFields();
|
||||
isAdd.value = true;
|
||||
entity.value = {};
|
||||
dialogVisible.value = false;
|
||||
}
|
||||
function addHeader() {
|
||||
tempAddHeaders.value.push({
|
||||
label: '',
|
||||
value: '',
|
||||
});
|
||||
}
|
||||
function removeHeader(index: number) {
|
||||
tempAddHeaders.value.splice(index, 1);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
draggable
|
||||
:title="isAdd ? $t('button.add') : $t('button.edit')"
|
||||
:before-close="closeDialog"
|
||||
:close-on-click-modal="false"
|
||||
align-center
|
||||
>
|
||||
<ElForm
|
||||
label-width="150px"
|
||||
ref="saveForm"
|
||||
:model="entity"
|
||||
status-icon
|
||||
:rules="rules"
|
||||
>
|
||||
<ElFormItem
|
||||
prop="icon"
|
||||
:label="$t('plugin.icon')"
|
||||
style="display: flex; align-items: center"
|
||||
>
|
||||
<UploadAvatar v-model="entity.icon" />
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="name" :label="$t('plugin.name')">
|
||||
<ElInput
|
||||
v-model.trim="entity.name"
|
||||
:placeholder="$t('plugin.placeholder.name')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="baseUrl" :label="$t('plugin.baseUrl')">
|
||||
<ElInput v-model.trim="entity.baseUrl" />
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="description" :label="$t('plugin.description')">
|
||||
<ElInput
|
||||
v-model.trim="entity.description"
|
||||
:rows="4"
|
||||
type="textarea"
|
||||
:placeholder="$t('plugin.placeholder.description')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="Headers" label="Headers">
|
||||
<div
|
||||
class="headers-container-reduce flex flex-row gap-4"
|
||||
v-for="(item, index) in tempAddHeaders"
|
||||
:key="index"
|
||||
>
|
||||
<div class="head-con-content flex flex-row gap-4">
|
||||
<ElInput v-model.trim="item.label" placeholder="header name" />
|
||||
<ElInput v-model.trim="item.value" placeholder="header value" />
|
||||
<ElIcon size="20" @click="removeHeader" style="cursor: pointer">
|
||||
<Remove />
|
||||
</ElIcon>
|
||||
</div>
|
||||
</div>
|
||||
<ElButton @click="addHeader" class="addHeadersBtn">
|
||||
<ElIcon size="18" style="margin-right: 4px">
|
||||
<Plus />
|
||||
</ElIcon>
|
||||
{{ $t('button.add') }}headers
|
||||
</ElButton>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="authType" :label="$t('plugin.authType')">
|
||||
<ElSelect v-model="entity.authType">
|
||||
<ElOption
|
||||
v-for="item in authTypeList"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value || ''"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
prop="position"
|
||||
:label="$t('plugin.position')"
|
||||
v-if="entity.authType === 'apiKey'"
|
||||
>
|
||||
<ElRadioGroup v-model="entity.position">
|
||||
<ElRadio value="headers">headers</ElRadio>
|
||||
<ElRadio value="query">query</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
prop="tokenKey"
|
||||
:label="$t('plugin.tokenKey')"
|
||||
v-if="entity.authType === 'apiKey'"
|
||||
>
|
||||
<ElInput v-model.trim="entity.tokenKey" />
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
prop="tokenValue"
|
||||
:label="$t('plugin.tokenValue')"
|
||||
v-if="entity.authType === 'apiKey'"
|
||||
>
|
||||
<ElInput v-model.trim="entity.tokenValue" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<ElButton @click="closeDialog">
|
||||
{{ $t('button.cancel') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
@click="save"
|
||||
:loading="btnLoading"
|
||||
:disabled="btnLoading"
|
||||
>
|
||||
{{ $t('button.save') }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.headers-container-reduce {
|
||||
align-items: center;
|
||||
}
|
||||
.addHeadersBtn {
|
||||
width: 100%;
|
||||
border-style: dashed;
|
||||
border-color: var(--el-color-primary);
|
||||
border-radius: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.head-con-content {
|
||||
margin-bottom: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
196
easyflow-ui-admin/app/src/views/ai/plugin/AiPluginToolModal.vue
Normal file
196
easyflow-ui-admin/app/src/views/ai/plugin/AiPluginToolModal.vue
Normal file
@@ -0,0 +1,196 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from 'element-plus';
|
||||
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElDialog,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const props = defineProps({
|
||||
pluginId: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['reload']);
|
||||
|
||||
const entity = reactive({
|
||||
pluginId: '',
|
||||
name: '',
|
||||
englishName: '',
|
||||
description: '',
|
||||
});
|
||||
|
||||
const saveForm = ref<FormInstance>();
|
||||
const dialogVisible = ref(false);
|
||||
const isAdd = ref(true);
|
||||
const btnLoading = ref(false);
|
||||
const ENGLISH_NAME_REG = /^[\w-]{1,64}$/;
|
||||
const rules = ref({
|
||||
name: [{ required: true, message: $t('message.required'), trigger: 'blur' }],
|
||||
englishName: [
|
||||
{
|
||||
required: true,
|
||||
message: $t('message.englishNameRule'),
|
||||
trigger: 'blur',
|
||||
pattern: ENGLISH_NAME_REG,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// 安全地打开对话框
|
||||
function openDialog(row: any) {
|
||||
try {
|
||||
// 重置表单状态
|
||||
saveForm.value?.resetFields();
|
||||
|
||||
// 安全地处理传入的数据
|
||||
if (row && row.id) {
|
||||
isAdd.value = false;
|
||||
// 使用 Object.assign 避免直接 Proxy 赋值
|
||||
Object.assign(entity, {
|
||||
...row,
|
||||
pluginId: props.pluginId,
|
||||
});
|
||||
} else {
|
||||
isAdd.value = true;
|
||||
// 重置 entity 数据
|
||||
Object.assign(entity, {
|
||||
pluginId: props.pluginId,
|
||||
name: '',
|
||||
description: '',
|
||||
});
|
||||
}
|
||||
|
||||
dialogVisible.value = true;
|
||||
} catch (error) {
|
||||
console.error('打开对话框错误:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 保存数据
|
||||
function save() {
|
||||
if (!saveForm.value) return;
|
||||
|
||||
saveForm.value.validate((valid) => {
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
btnLoading.value = true;
|
||||
|
||||
const apiUrl = isAdd.value
|
||||
? 'api/v1/pluginItem/tool/save'
|
||||
: 'api/v1/pluginItem/tool/update';
|
||||
|
||||
// 创建纯对象提交,避免 Proxy
|
||||
const submitData = { ...entity };
|
||||
|
||||
api
|
||||
.post(apiUrl, submitData)
|
||||
.then((res) => {
|
||||
btnLoading.value = false;
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success($t('message.saveOkMessage'));
|
||||
closeDialog();
|
||||
emit('reload');
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('API请求错误:', error);
|
||||
btnLoading.value = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
function closeDialog() {
|
||||
try {
|
||||
if (saveForm.value) {
|
||||
saveForm.value.resetFields();
|
||||
}
|
||||
|
||||
// 重置数据
|
||||
Object.assign(entity, {
|
||||
pluginId: props.pluginId,
|
||||
name: '',
|
||||
description: '',
|
||||
});
|
||||
|
||||
isAdd.value = true;
|
||||
dialogVisible.value = false;
|
||||
} catch (error) {
|
||||
console.error('关闭对话框错误:', error);
|
||||
// 强制关闭
|
||||
dialogVisible.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
Object.assign(entity, {
|
||||
pluginId: props.pluginId,
|
||||
name: '',
|
||||
description: '',
|
||||
});
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
openDialog,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
draggable
|
||||
:title="isAdd ? $t('button.add') : $t('button.edit')"
|
||||
:before-close="closeDialog"
|
||||
:close-on-click-modal="false"
|
||||
width="600px"
|
||||
@closed="closeDialog"
|
||||
>
|
||||
<ElForm
|
||||
ref="saveForm"
|
||||
:model="entity"
|
||||
:rules="rules"
|
||||
label-width="80px"
|
||||
status-icon
|
||||
>
|
||||
<ElFormItem :label="$t('pluginItem.name')" prop="name">
|
||||
<ElInput v-model.trim="entity.name" />
|
||||
</ElFormItem>
|
||||
<ElFormItem :label="$t('pluginItem.englishName')" prop="englishName">
|
||||
<ElInput v-model.trim="entity.englishName" />
|
||||
</ElFormItem>
|
||||
<ElFormItem :label="$t('pluginItem.description')" prop="description">
|
||||
<ElInput v-model.trim="entity.description" type="textarea" :rows="4" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<ElButton @click="closeDialog" :disabled="btnLoading">
|
||||
{{ $t('button.cancel') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
:loading="btnLoading"
|
||||
:disabled="btnLoading"
|
||||
@click="save"
|
||||
>
|
||||
{{ $t('button.save') }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,133 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from 'element-plus';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElDialog,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElMessage,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const emit = defineEmits(['reload']);
|
||||
const categoryList = ref<any[]>([]);
|
||||
const getPluginCategoryList = async () => {
|
||||
return api.get('/api/v1/pluginCategory/list').then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
categoryList.value = res.data;
|
||||
}
|
||||
});
|
||||
};
|
||||
onMounted(() => {
|
||||
getPluginCategoryList();
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
openDialog,
|
||||
});
|
||||
const saveForm = ref<FormInstance>();
|
||||
const dialogVisible = ref(false);
|
||||
const isAdd = ref(true);
|
||||
const entity = ref<any>({
|
||||
id: '',
|
||||
categoryIds: [],
|
||||
});
|
||||
const btnLoading = ref(false);
|
||||
function getPluginCategories(id: string) {
|
||||
return api
|
||||
.get(`/api/v1/pluginCategoryMapping/getPluginCategories?pluginId=${id}`)
|
||||
.then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
entity.value.categoryIds = res.data;
|
||||
}
|
||||
});
|
||||
}
|
||||
function openDialog(row: any) {
|
||||
if (row.id) {
|
||||
isAdd.value = false;
|
||||
}
|
||||
getPluginCategories(row.id).then(() => {
|
||||
entity.value.categoryIds = row.categoryIds.map((item: any) => item.id);
|
||||
});
|
||||
entity.value = row;
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
function save() {
|
||||
saveForm.value?.validate((valid) => {
|
||||
if (valid) {
|
||||
const tempParams = {
|
||||
pluginId: entity.value.id,
|
||||
categoryIds: entity.value.categoryIds,
|
||||
};
|
||||
api
|
||||
.post('/api/v1/pluginCategoryMapping/updateRelation', tempParams)
|
||||
.then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success($t('message.updateOkMessage'));
|
||||
closeDialog();
|
||||
emit('reload');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
function closeDialog() {
|
||||
saveForm.value?.resetFields();
|
||||
isAdd.value = true;
|
||||
entity.value = {};
|
||||
dialogVisible.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
draggable
|
||||
width="500px"
|
||||
:title="$t('plugin.placeholder.categorize')"
|
||||
:before-close="closeDialog"
|
||||
:close-on-click-modal="false"
|
||||
align-center
|
||||
>
|
||||
<ElForm ref="saveForm" :model="entity" status-icon>
|
||||
<ElFormItem prop="authType" :label="$t('plugin.category')">
|
||||
<ElSelect
|
||||
v-model="entity.categoryIds"
|
||||
multiple
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
:max-collapse-tags="3"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in categoryList"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<ElButton @click="closeDialog">
|
||||
{{ $t('button.cancel') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
@click="save"
|
||||
:loading="btnLoading"
|
||||
:disabled="btnLoading"
|
||||
>
|
||||
{{ $t('button.save') }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
308
easyflow-ui-admin/app/src/views/ai/plugin/Plugin.vue
Normal file
308
easyflow-ui-admin/app/src/views/ai/plugin/Plugin.vue
Normal file
@@ -0,0 +1,308 @@
|
||||
<script setup lang="ts">
|
||||
import type { ActionButton } from '#/components/page/CardList.vue';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { $t } from '@easyflow/locales';
|
||||
|
||||
import { Delete, Edit, Plus } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElButton,
|
||||
ElDialog,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import defaultPluginIcon from '#/assets/ai/plugin/defaultPluginIcon.png';
|
||||
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
||||
import CategorizeIcon from '#/components/icons/CategorizeIcon.vue';
|
||||
import PluginToolIcon from '#/components/icons/PluginToolIcon.vue';
|
||||
import CardPage from '#/components/page/CardList.vue';
|
||||
import PageData from '#/components/page/PageData.vue';
|
||||
import PageSide from '#/components/page/PageSide.vue';
|
||||
import AddPluginModal from '#/views/ai/plugin/AddPluginModal.vue';
|
||||
import CategoryPluginModal from '#/views/ai/plugin/CategoryPluginModal.vue';
|
||||
|
||||
const router = useRouter();
|
||||
// 操作按钮配置
|
||||
const actions: ActionButton[] = [
|
||||
{
|
||||
icon: Edit,
|
||||
text: $t('button.edit'),
|
||||
className: '',
|
||||
permission: '/api/v1/plugin/save',
|
||||
onClick(item) {
|
||||
aiPluginModalRef.value.openDialog(item);
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: PluginToolIcon,
|
||||
text: $t('plugin.button.tools'),
|
||||
className: '',
|
||||
permission: '/api/v1/plugin/save',
|
||||
onClick(item) {
|
||||
router.push({
|
||||
path: '/ai/plugin/tools',
|
||||
query: {
|
||||
id: item.id,
|
||||
pageKey: '/ai/plugin',
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: CategorizeIcon,
|
||||
text: $t('plugin.button.categorize'),
|
||||
className: '',
|
||||
permission: '/api/v1/plugin/save',
|
||||
onClick(item) {
|
||||
categoryCategoryModal.value.openDialog(item);
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: Delete,
|
||||
text: $t('button.delete'),
|
||||
className: 'item-danger',
|
||||
permission: '/api/v1/plugin/remove',
|
||||
onClick(item) {
|
||||
handleDelete(item);
|
||||
},
|
||||
},
|
||||
];
|
||||
const categoryList = ref([]);
|
||||
const controlBtns = [
|
||||
{
|
||||
icon: Edit,
|
||||
label: $t('button.edit'),
|
||||
onClick(row) {
|
||||
formData.value.name = row.name;
|
||||
formData.value.id = row.id;
|
||||
isEdit.value = true;
|
||||
dialogVisible.value = true;
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'danger',
|
||||
icon: Delete,
|
||||
label: $t('button.delete'),
|
||||
onClick(row) {
|
||||
handleDeleteCategory(row);
|
||||
},
|
||||
},
|
||||
];
|
||||
const footerButton = {
|
||||
icon: Plus,
|
||||
label: $t('button.add'),
|
||||
onClick() {
|
||||
dialogVisible.value = true;
|
||||
isEdit.value = false;
|
||||
},
|
||||
};
|
||||
const getPluginCategoryList = async () => {
|
||||
return api.get('/api/v1/pluginCategory/list').then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
categoryList.value = [
|
||||
{ id: '0', name: $t('common.allCategories') },
|
||||
...res.data,
|
||||
];
|
||||
}
|
||||
});
|
||||
};
|
||||
onMounted(() => {
|
||||
getPluginCategoryList();
|
||||
});
|
||||
const handleDelete = (item) => {
|
||||
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
|
||||
confirmButtonText: $t('message.ok'),
|
||||
cancelButtonText: $t('message.cancel'),
|
||||
type: 'warning',
|
||||
})
|
||||
.then(() => {
|
||||
api.post('/api/v1/plugin/plugin/remove', { id: item.id }).then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success($t('message.deleteOkMessage'));
|
||||
pageDataRef.value.setQuery({});
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
const pageDataRef = ref();
|
||||
const aiPluginModalRef = ref();
|
||||
const categoryCategoryModal = ref();
|
||||
const headerButtons = [
|
||||
{
|
||||
key: 'add',
|
||||
text: $t('plugin.button.addPlugin'),
|
||||
icon: Plus,
|
||||
type: 'primary',
|
||||
data: { action: 'add' },
|
||||
},
|
||||
];
|
||||
const pluginCategoryId = ref('0');
|
||||
const dialogVisible = ref(false); // 弹窗显隐
|
||||
const isEdit = ref(false); // 是否为编辑模式
|
||||
const formData = ref({ name: '', id: '' });
|
||||
|
||||
const handleSubmit = () => {
|
||||
// 触发对应事件,传递表单数据
|
||||
if (isEdit.value) {
|
||||
handleEditCategory(formData.value);
|
||||
} else {
|
||||
handleAddCategory(formData.value);
|
||||
}
|
||||
// 提交后关闭弹窗
|
||||
dialogVisible.value = false;
|
||||
};
|
||||
const handleButtonClick = (event, _item) => {
|
||||
switch (event.key) {
|
||||
case 'add': {
|
||||
aiPluginModalRef.value.openDialog({});
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
const handleSearch = (params) => {
|
||||
pageDataRef.value.setQuery({ title: params, isQueryOr: true });
|
||||
};
|
||||
const handleEditCategory = (params) => {
|
||||
api
|
||||
.post('/api/v1/pluginCategory/update', {
|
||||
id: params.id,
|
||||
name: params.name,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
getPluginCategoryList();
|
||||
ElMessage.success($t('message.updateOkMessage'));
|
||||
}
|
||||
});
|
||||
};
|
||||
const handleAddCategory = (params) => {
|
||||
api.post('/api/v1/pluginCategory/save', { name: params.name }).then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
getPluginCategoryList();
|
||||
ElMessage.success($t('message.saveOkMessage'));
|
||||
}
|
||||
});
|
||||
};
|
||||
const handleDeleteCategory = (params) => {
|
||||
api
|
||||
.get(`/api/v1/pluginCategory/doRemoveCategory?id=${params.id}`)
|
||||
.then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
getPluginCategoryList();
|
||||
ElMessage.success($t('message.deleteOkMessage'));
|
||||
}
|
||||
});
|
||||
};
|
||||
const handleClickCategory = (item) => {
|
||||
pageDataRef.value.setQuery({ category: item.id });
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="knowledge-container">
|
||||
<div class="knowledge-header">
|
||||
<HeaderSearch
|
||||
:buttons="headerButtons"
|
||||
:search-placeholder="$t('plugin.searchUsers')"
|
||||
@search="handleSearch"
|
||||
@button-click="handleButtonClick"
|
||||
/>
|
||||
</div>
|
||||
<div class="plugin-content-container">
|
||||
<div class="category-panel-container">
|
||||
<PageSide
|
||||
label-key="name"
|
||||
value-key="id"
|
||||
:menus="categoryList"
|
||||
:control-btns="controlBtns"
|
||||
:footer-button="footerButton"
|
||||
default-selected="0"
|
||||
@change="handleClickCategory"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="plugin-content-data-container h-full overflow-auto">
|
||||
<PageData
|
||||
ref="pageDataRef"
|
||||
page-url="/api/v1/plugin/pageByCategory"
|
||||
:page-size="12"
|
||||
:page-sizes="[12, 24, 36, 48]"
|
||||
:extra-query-params="{ category: pluginCategoryId }"
|
||||
>
|
||||
<template #default="{ pageList }">
|
||||
<CardPage
|
||||
title-key="title"
|
||||
avatar-key="icon"
|
||||
description-key="description"
|
||||
:data="pageList"
|
||||
:actions="actions"
|
||||
:default-icon="defaultPluginIcon"
|
||||
/>
|
||||
</template>
|
||||
</PageData>
|
||||
</div>
|
||||
</div>
|
||||
<AddPluginModal ref="aiPluginModalRef" @reload="handleSearch" />
|
||||
<CategoryPluginModal ref="categoryCategoryModal" @reload="handleSearch" />
|
||||
<ElDialog
|
||||
:title="isEdit ? `${$t('button.edit')}` : `${$t('button.add')}`"
|
||||
v-model="dialogVisible"
|
||||
width="500px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<ElForm :model="formData" status-icon>
|
||||
<ElFormItem>
|
||||
<ElInput v-model.trim="formData.name" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<ElButton @click="dialogVisible = false">
|
||||
{{ $t('button.cancel') }}
|
||||
</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit">
|
||||
{{ $t('button.confirm') }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.knowledge-container {
|
||||
width: 100%;
|
||||
padding: 24px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-bottom: 30px;
|
||||
color: #303133;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.plugin-content-container {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
height: calc(100vh - 161px);
|
||||
padding-top: 24px;
|
||||
}
|
||||
|
||||
.plugin-content-data-container {
|
||||
/* padding: 20px; */
|
||||
|
||||
/* background-color: var(--el-bg-color); */
|
||||
width: 100%;
|
||||
border-top-right-radius: var(--el-border-radius-base);
|
||||
border-bottom-right-radius: var(--el-border-radius-base);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,703 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import { Delete, Plus } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElButton,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
ElSwitch,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
} from 'element-plus';
|
||||
|
||||
import { $t } from '#/locales';
|
||||
|
||||
export interface TreeTableNode {
|
||||
key: string;
|
||||
name: string;
|
||||
description: string;
|
||||
method?: 'Body' | 'Header' | 'Path' | 'Query';
|
||||
required?: boolean;
|
||||
defaultValue?: string;
|
||||
enabled?: boolean;
|
||||
type?: string;
|
||||
children?: TreeTableNode[];
|
||||
}
|
||||
|
||||
interface Props {
|
||||
modelValue?: TreeTableNode[];
|
||||
editable?: boolean;
|
||||
isEditOutput?: boolean;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: TreeTableNode[]): void;
|
||||
(e: 'submit', value: TreeTableNode[]): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: () => [],
|
||||
editable: false,
|
||||
isEditOutput: false,
|
||||
});
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const data = ref<TreeTableNode[]>([]);
|
||||
const expandedKeys = ref<string[]>(['1']);
|
||||
const errors = ref<
|
||||
Record<string, Partial<Record<keyof TreeTableNode, string>>>
|
||||
>({});
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
data.value = newVal;
|
||||
}
|
||||
},
|
||||
{ immediate: true, deep: true },
|
||||
);
|
||||
|
||||
// 计算缩进宽度
|
||||
const getIndentWidth = (record: TreeTableNode): number => {
|
||||
const level = String(record.key).split('-').length - 1;
|
||||
const indentSize = 20;
|
||||
return level > 0 ? level * indentSize : 0;
|
||||
};
|
||||
|
||||
// 获取类型选项
|
||||
const getTypeOptions = (record: TreeTableNode) => {
|
||||
if (record.name === 'arrayItem') {
|
||||
return [
|
||||
{ label: 'Array[String]', value: 'Array[String]' },
|
||||
{ label: 'Array[Number]', value: 'Array[Number]' },
|
||||
{ label: 'Array[Boolean]', value: 'Array[Boolean]' },
|
||||
{ label: 'Array[Object]', value: 'Array[Object]' },
|
||||
];
|
||||
}
|
||||
return [
|
||||
{ label: 'String', value: 'String' },
|
||||
{ label: 'Boolean', value: 'Boolean' },
|
||||
{ label: 'Number', value: 'Number' },
|
||||
{ label: 'Object', value: 'Object' },
|
||||
{ label: 'Array', value: 'Array' },
|
||||
{ label: 'File', value: 'File' },
|
||||
];
|
||||
};
|
||||
|
||||
// 数据变化处理
|
||||
const handleDataChange = () => {
|
||||
emit('update:modelValue', data.value);
|
||||
};
|
||||
|
||||
// 类型变化处理
|
||||
const handleTypeChange = (record: TreeTableNode, newType: string) => {
|
||||
const updateNode = (nodes: TreeTableNode[]): TreeTableNode[] => {
|
||||
return nodes.map((node) => {
|
||||
if (node.key === record.key) {
|
||||
// 如果是简单类型,移除 children
|
||||
if (
|
||||
[
|
||||
'Array[Boolean]',
|
||||
'Array[Integer]',
|
||||
'Array[Number]',
|
||||
'Array[Object]',
|
||||
'Array[String]',
|
||||
'Boolean',
|
||||
'Number',
|
||||
'String',
|
||||
].includes(newType)
|
||||
) {
|
||||
return {
|
||||
...node,
|
||||
type: newType,
|
||||
children: undefined,
|
||||
};
|
||||
}
|
||||
// 如果是 Object 或 Array,保留或初始化 children
|
||||
return {
|
||||
...node,
|
||||
type: newType,
|
||||
children: node.children || [],
|
||||
};
|
||||
}
|
||||
if (node.children) {
|
||||
return {
|
||||
...node,
|
||||
children: updateNode(node.children),
|
||||
};
|
||||
}
|
||||
return node;
|
||||
});
|
||||
};
|
||||
|
||||
data.value = updateNode(data.value);
|
||||
handleDataChange();
|
||||
|
||||
// 如果是 Object 或 Array,添加默认子节点并展开
|
||||
if (
|
||||
newType === 'Object' ||
|
||||
newType === 'Array' ||
|
||||
newType === 'Array[Object]'
|
||||
) {
|
||||
const newChild: TreeTableNode = {
|
||||
key: `${record.key}-${Date.now()}`,
|
||||
name: newType === 'Array' ? 'arrayItem' : '',
|
||||
description: '',
|
||||
enabled: true,
|
||||
...(props.isEditOutput
|
||||
? {}
|
||||
: { method: 'Query', defaultValue: '', required: false }),
|
||||
type: newType === 'Array' ? 'Array[String]' : 'String',
|
||||
};
|
||||
|
||||
const addChildToNode = (nodes: TreeTableNode[]): TreeTableNode[] => {
|
||||
return nodes.map((node) => {
|
||||
if (node.key === record.key) {
|
||||
return {
|
||||
...node,
|
||||
children: [newChild],
|
||||
};
|
||||
}
|
||||
if (node.children) {
|
||||
return {
|
||||
...node,
|
||||
children: addChildToNode(node.children),
|
||||
};
|
||||
}
|
||||
return node;
|
||||
});
|
||||
};
|
||||
|
||||
data.value = addChildToNode(data.value);
|
||||
handleDataChange();
|
||||
|
||||
// 自动展开父节点
|
||||
if (!expandedKeys.value.includes(record.key)) {
|
||||
expandedKeys.value = [...expandedKeys.value, record.key];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 展开/折叠处理
|
||||
const onExpand = (_row: TreeTableNode, expandedRows: TreeTableNode[]) => {
|
||||
expandedKeys.value = expandedRows.map((item) => item.key);
|
||||
};
|
||||
|
||||
// 添加根节点
|
||||
const addNewRootNode = () => {
|
||||
if (!props.editable) return;
|
||||
|
||||
const newKey = `${Date.now()}`;
|
||||
const newNode: TreeTableNode = {
|
||||
key: newKey,
|
||||
name: '',
|
||||
description: '',
|
||||
enabled: true,
|
||||
type: 'String',
|
||||
...(props.isEditOutput
|
||||
? {}
|
||||
: { method: 'Query', defaultValue: '', required: false }),
|
||||
};
|
||||
|
||||
data.value = [...data.value, newNode];
|
||||
handleDataChange();
|
||||
};
|
||||
|
||||
// 添加子节点
|
||||
const handleAddChild = (parentKey: string) => {
|
||||
if (!props.editable || !parentKey) return;
|
||||
|
||||
const newChild: TreeTableNode = {
|
||||
key: `${parentKey}-${Date.now()}`,
|
||||
name: '',
|
||||
description: '',
|
||||
required: false,
|
||||
enabled: true,
|
||||
type: 'String',
|
||||
...(props.isEditOutput ? {} : { method: 'Query', defaultValue: '' }),
|
||||
};
|
||||
|
||||
const addChildToNode = (nodes: TreeTableNode[]): TreeTableNode[] => {
|
||||
return nodes.map((node) => {
|
||||
if (node.key === parentKey) {
|
||||
return {
|
||||
...node,
|
||||
children: [...(node.children || []), newChild],
|
||||
};
|
||||
}
|
||||
if (node.children) {
|
||||
return {
|
||||
...node,
|
||||
children: addChildToNode(node.children),
|
||||
};
|
||||
}
|
||||
return node;
|
||||
});
|
||||
};
|
||||
|
||||
data.value = addChildToNode(data.value);
|
||||
handleDataChange();
|
||||
|
||||
if (!expandedKeys.value.includes(parentKey)) {
|
||||
expandedKeys.value = [...expandedKeys.value, parentKey];
|
||||
}
|
||||
};
|
||||
|
||||
// 删除节点
|
||||
const deleteNode = (key: string) => {
|
||||
if (!props.editable) return;
|
||||
|
||||
const removeNodeRecursively = (nodes: TreeTableNode[]): TreeTableNode[] => {
|
||||
return nodes.filter((node) => {
|
||||
if (node.key === key) return false;
|
||||
if (node.children) {
|
||||
node.children = removeNodeRecursively(node.children);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
data.value = removeNodeRecursively(data.value);
|
||||
handleDataChange();
|
||||
};
|
||||
|
||||
// 验证字段
|
||||
// 验证字段
|
||||
const validateFields = (): boolean => {
|
||||
const newErrors: Record<
|
||||
string,
|
||||
Partial<Record<keyof TreeTableNode, string>>
|
||||
> = {};
|
||||
let isValid = true;
|
||||
|
||||
// 递归校验节点(包括子节点)
|
||||
const checkNode = (node: TreeTableNode): boolean => {
|
||||
const { name, description, method, type } = node;
|
||||
const nodeErrors: Partial<Record<keyof TreeTableNode, string>> = {};
|
||||
let nodeIsValid = true;
|
||||
|
||||
// 校验参数名称
|
||||
if (!name?.trim()) {
|
||||
nodeErrors.name = $t('message.cannotBeEmpty.name');
|
||||
nodeIsValid = false;
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// 校验参数描述
|
||||
if (!description?.trim()) {
|
||||
nodeErrors.description = $t('message.cannotBeEmpty.description');
|
||||
nodeIsValid = false;
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// 校验传入方法(仅根节点+输入参数)
|
||||
if (isRootNode(node) && !method && !props.isEditOutput) {
|
||||
nodeErrors.method = $t('message.cannotBeEmpty.method');
|
||||
nodeIsValid = false;
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// 校验参数类型
|
||||
if (!type) {
|
||||
nodeErrors.type = $t('message.cannotBeEmpty.type');
|
||||
nodeIsValid = false;
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// 记录当前节点的错误
|
||||
if (Object.keys(nodeErrors).length > 0) {
|
||||
newErrors[node.key] = nodeErrors;
|
||||
}
|
||||
|
||||
// 递归校验子节点
|
||||
if (node.children) {
|
||||
node.children.forEach((child) => {
|
||||
if (!checkNode(child)) {
|
||||
nodeIsValid = false;
|
||||
isValid = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return nodeIsValid;
|
||||
};
|
||||
|
||||
// 校验所有根节点
|
||||
data.value.forEach((node) => {
|
||||
checkNode(node);
|
||||
});
|
||||
|
||||
// 更新错误信息
|
||||
errors.value = newErrors;
|
||||
return isValid;
|
||||
};
|
||||
|
||||
// 判断是否为根节点
|
||||
const isRootNode = (record: TreeTableNode): boolean => {
|
||||
return !record.key.includes('-');
|
||||
};
|
||||
|
||||
const handleSubmitParams = () => {
|
||||
// 全量校验所有字段
|
||||
const isFormValid = validateFields();
|
||||
|
||||
if (!isFormValid) {
|
||||
ElMessage.error($t('message.cannotBeEmpty.all'));
|
||||
|
||||
// 找到第一个错误的输入框/选择器
|
||||
const firstErrorInput = document.querySelector('.error-border');
|
||||
if (firstErrorInput) {
|
||||
// 滚动到错误元素位置
|
||||
firstErrorInput.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
// 给输入框添加焦点
|
||||
if ((firstErrorInput as HTMLInputElement).focus) {
|
||||
(firstErrorInput as HTMLInputElement).focus();
|
||||
} else {
|
||||
// 处理选择器的焦点
|
||||
const selectInput = firstErrorInput.querySelector('.el-input__inner');
|
||||
if (selectInput) (selectInput as HTMLInputElement).focus();
|
||||
}
|
||||
}
|
||||
throw new Error($t('message.cannotBeEmpty.error'));
|
||||
}
|
||||
|
||||
// 校验通过,提交数据
|
||||
emit('submit', data.value);
|
||||
};
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
handleSubmitParams,
|
||||
});
|
||||
// 输入框失焦时清除对应字段的错误提示
|
||||
const handleInputBlur = (row: TreeTableNode, field: keyof TreeTableNode) => {
|
||||
if (
|
||||
errors.value &&
|
||||
row &&
|
||||
field &&
|
||||
(errors.value[row.key] as Record<string, unknown>)
|
||||
) {
|
||||
delete (errors.value[row.key] as Record<string, unknown>)[field];
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tree-table-container">
|
||||
<ElTable
|
||||
:data="data"
|
||||
row-key="key"
|
||||
:border="true"
|
||||
size="default"
|
||||
:expand-row-keys="expandedKeys"
|
||||
@expand-change="onExpand"
|
||||
style="width: 100%; overflow-x: auto"
|
||||
>
|
||||
<!-- 参数名称列 -->
|
||||
<ElTableColumn prop="name" class-name="first-column">
|
||||
<template #header>
|
||||
<div class="header-with-asterisk">
|
||||
{{ $t('pluginItem.parameterName') }}
|
||||
<span class="required-asterisk">*</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<div class="name-cell">
|
||||
<div
|
||||
v-if="!props.editable"
|
||||
:style="{ paddingLeft: `${getIndentWidth(row)}px` }"
|
||||
>
|
||||
{{ row.name || '' }}
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="name-input-wrapper">
|
||||
<div :style="{ width: `${getIndentWidth(row)}px` }"></div>
|
||||
<ElInput
|
||||
v-model="row.name"
|
||||
:disabled="row.name === 'arrayItem'"
|
||||
@input="handleDataChange"
|
||||
@blur="handleInputBlur(row, 'name')"
|
||||
:class="{ 'error-border': errors[row.key]?.name }"
|
||||
/>
|
||||
<div v-if="errors[row.key]?.name" class="error-message">
|
||||
{{ errors[row.key]?.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
|
||||
<!-- 参数描述列 -->
|
||||
<ElTableColumn prop="description">
|
||||
<template #header>
|
||||
<div class="header-with-asterisk">
|
||||
{{ $t('pluginItem.parameterDescription') }}
|
||||
<span class="required-asterisk">*</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<div class="description-cell">
|
||||
<span v-if="!props.editable">{{ row.description || '' }}</span>
|
||||
<div v-else>
|
||||
<ElInput
|
||||
v-model="row.description"
|
||||
@input="handleDataChange"
|
||||
@blur="handleInputBlur(row, 'description')"
|
||||
:class="{ 'error-border': errors[row.key]?.description }"
|
||||
/>
|
||||
<div v-if="errors[row.key]?.description" class="error-message">
|
||||
{{ errors[row.key]?.description }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
|
||||
<!-- 参数类型列 -->
|
||||
<ElTableColumn
|
||||
prop="type"
|
||||
:label="$t('pluginItem.parameterType')"
|
||||
width="150px"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span v-if="!props.editable">{{ row.type || '' }}</span>
|
||||
<div v-else>
|
||||
<ElSelect
|
||||
v-model="row.type"
|
||||
@change="handleTypeChange(row, $event)"
|
||||
>
|
||||
<ElOption
|
||||
v-for="option in getTypeOptions(row)"
|
||||
:key="option.value"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
<div v-if="errors[row.key]?.type" class="error-message">
|
||||
{{ errors[row.key]?.type }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
|
||||
<!-- 传入方法列 (仅输入参数显示) -->
|
||||
<ElTableColumn
|
||||
v-if="!props.isEditOutput"
|
||||
prop="method"
|
||||
:label="$t('pluginItem.inputMethod')"
|
||||
width="120px"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.name === 'arrayItem'"></span>
|
||||
<span v-else-if="!props.editable">{{ row.method || '' }}</span>
|
||||
<div v-else>
|
||||
<ElSelect v-model="row.method" @change="handleDataChange">
|
||||
<ElOption label="Query" value="Query" />
|
||||
<ElOption label="Body" value="Body" />
|
||||
<ElOption label="Path" value="Path" />
|
||||
<ElOption label="Header" value="Header" />
|
||||
</ElSelect>
|
||||
<div v-if="errors[row.key]?.method" class="error-message">
|
||||
{{ errors[row.key]?.method }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
|
||||
<!-- 是否必填列 (仅输入参数显示) -->
|
||||
<ElTableColumn
|
||||
v-if="!props.isEditOutput"
|
||||
prop="required"
|
||||
:label="$t('pluginItem.required')"
|
||||
width="120px"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<ElSwitch
|
||||
v-model="row.required"
|
||||
@change="handleDataChange"
|
||||
:disabled="!props.editable"
|
||||
/>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
|
||||
<!-- 默认值列 (仅输入参数显示) -->
|
||||
<ElTableColumn
|
||||
v-if="!props.isEditOutput"
|
||||
prop="defaultValue"
|
||||
:label="$t('pluginItem.defaultValue')"
|
||||
width="150px"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.type === 'Object'"></span>
|
||||
<span v-else-if="!props.editable">{{ row.defaultValue || '' }}</span>
|
||||
<ElInput
|
||||
v-else
|
||||
v-model="row.defaultValue"
|
||||
@input="handleDataChange"
|
||||
:disabled="!props.editable"
|
||||
/>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
|
||||
<!-- 启用状态列 -->
|
||||
<ElTableColumn
|
||||
prop="enabled"
|
||||
:label="$t('pluginItem.enabledStatus')"
|
||||
width="120px"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<ElSwitch
|
||||
v-model="row.enabled"
|
||||
@change="handleDataChange"
|
||||
:disabled="!props.editable"
|
||||
/>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
|
||||
<!-- 操作列 (仅可编辑时显示) -->
|
||||
<ElTableColumn
|
||||
v-if="props.editable"
|
||||
:label="$t('common.handle')"
|
||||
width="130px"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="action-buttons">
|
||||
<ElButton
|
||||
v-if="row.type === 'Object' || row.type === 'Array[Object]'"
|
||||
type="primary"
|
||||
link
|
||||
:icon="Plus"
|
||||
@click="handleAddChild(row.key)"
|
||||
:title="$t('pluginItem.addChildNode')"
|
||||
/>
|
||||
<ElButton
|
||||
type="danger"
|
||||
link
|
||||
:icon="Delete"
|
||||
@click="deleteNode(row.key)"
|
||||
>
|
||||
{{ $t('button.delete') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
|
||||
<!-- 新增参数按钮 -->
|
||||
<div v-if="props.editable" class="add-button-container">
|
||||
<ElButton type="default" @click="addNewRootNode" :icon="Plus">
|
||||
{{ $t('pluginItem.addParameter') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.tree-table-container {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.name-cell {
|
||||
position: relative;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.editable-name {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.name-input-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
margin-top: 2px;
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.action-buttons .el-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.add-button-container {
|
||||
margin-top: 16px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.description-cell {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-table td.el-table__cell.first-column > div) {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.el-table__header-wrapper,
|
||||
.el-table__body-wrapper {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.header-with-asterisk {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.required-asterisk {
|
||||
position: absolute;
|
||||
right: -8px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
/* 输入框/选择器错误样式 */
|
||||
:deep(.el-input__inner.error-border),
|
||||
:deep(.el-select .el-input__inner.error-border) {
|
||||
border-color: #ff4d4f !important;
|
||||
box-shadow: 0 0 0 2px rgb(255 77 79 / 20%) !important;
|
||||
}
|
||||
|
||||
/* 下拉选择器的触发框错误样式 */
|
||||
:deep(.el-select__wrapper.error-border) {
|
||||
border-color: #ff4d4f !important;
|
||||
box-shadow: 0 0 0 2px rgb(255 77 79 / 20%) !important;
|
||||
}
|
||||
|
||||
.name-input-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
259
easyflow-ui-admin/app/src/views/ai/plugin/PluginRunParams.vue
Normal file
259
easyflow-ui-admin/app/src/views/ai/plugin/PluginRunParams.vue
Normal file
@@ -0,0 +1,259 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import { ElInput, ElMessage, ElTable, ElTableColumn } from 'element-plus';
|
||||
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: () => [],
|
||||
editable: false,
|
||||
isEditOutput: false,
|
||||
});
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
export interface TreeTableNode {
|
||||
key: string;
|
||||
name: string;
|
||||
description: string;
|
||||
method?: 'Body' | 'Header' | 'Path' | 'Query';
|
||||
required?: boolean;
|
||||
defaultValue?: string;
|
||||
enabled?: boolean;
|
||||
type?: string;
|
||||
children?: TreeTableNode[];
|
||||
}
|
||||
|
||||
interface Props {
|
||||
modelValue?: TreeTableNode[];
|
||||
editable?: boolean;
|
||||
isEditOutput?: boolean;
|
||||
}
|
||||
|
||||
const data = ref<TreeTableNode[]>([]);
|
||||
const expandedKeys = ref<string[]>(['1']);
|
||||
const errors = ref<
|
||||
Record<string, Partial<Record<keyof TreeTableNode, string>>>
|
||||
>({});
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
data.value = newVal;
|
||||
}
|
||||
},
|
||||
{ immediate: true, deep: true },
|
||||
);
|
||||
|
||||
// 计算缩进宽度
|
||||
const getIndentWidth = (record: TreeTableNode): number => {
|
||||
const level = String(record.key).split('-').length - 1;
|
||||
const indentSize = 20;
|
||||
return level > 0 ? level * indentSize : 0;
|
||||
};
|
||||
|
||||
// 数据变化处理
|
||||
const handleDataChange = () => {
|
||||
emit('update:modelValue', data.value);
|
||||
};
|
||||
|
||||
// 展开/折叠处理
|
||||
const onExpand = (_row: TreeTableNode, expandedRows: TreeTableNode[]) => {
|
||||
expandedKeys.value = expandedRows.map((item) => item.key);
|
||||
};
|
||||
|
||||
// 验证字段
|
||||
const validateFields = (): boolean => {
|
||||
const newErrors: Record<
|
||||
string,
|
||||
Partial<Record<keyof TreeTableNode, string>>
|
||||
> = {};
|
||||
let isValid = true;
|
||||
|
||||
const checkNode = (node: TreeTableNode): boolean => {
|
||||
const { name, description, method, type } = node;
|
||||
const nodeErrors: Partial<Record<keyof TreeTableNode, string>> = {};
|
||||
|
||||
if (!name?.trim()) {
|
||||
nodeErrors.name = $t('message.cannotBeEmpty.name');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (!description?.trim()) {
|
||||
nodeErrors.description = $t('message.cannotBeEmpty.description');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (isRootNode(node) && !method && !props.isEditOutput) {
|
||||
nodeErrors.method = $t('message.cannotBeEmpty.method');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (!type) {
|
||||
nodeErrors.type = $t('message.cannotBeEmpty.type');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (Object.keys(nodeErrors).length > 0) {
|
||||
newErrors[node.key] = nodeErrors;
|
||||
}
|
||||
|
||||
if (node.children) {
|
||||
node.children.forEach((child) => {
|
||||
if (!checkNode(child)) isValid = false;
|
||||
});
|
||||
}
|
||||
|
||||
return isValid;
|
||||
};
|
||||
|
||||
data.value.forEach((node) => {
|
||||
if (!checkNode(node)) isValid = false;
|
||||
});
|
||||
|
||||
errors.value = newErrors;
|
||||
return isValid;
|
||||
};
|
||||
|
||||
// 判断是否为根节点
|
||||
const isRootNode = (record: TreeTableNode): boolean => {
|
||||
return !record.key.includes('-');
|
||||
};
|
||||
|
||||
// 提交参数
|
||||
const handleSubmitParams = () => {
|
||||
if (!validateFields()) {
|
||||
ElMessage.error($t('message.completeForm'));
|
||||
return;
|
||||
}
|
||||
return data.value;
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
handleSubmitParams,
|
||||
});
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: TreeTableNode[]): void;
|
||||
(e: 'submit', value: TreeTableNode[]): void;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tree-table-container">
|
||||
<ElTable
|
||||
:data="data"
|
||||
row-key="key"
|
||||
:border="true"
|
||||
size="default"
|
||||
:expand-row-keys="expandedKeys"
|
||||
@expand-change="onExpand"
|
||||
style="width: 100%; overflow-x: auto"
|
||||
>
|
||||
<ElTableColumn
|
||||
prop="name"
|
||||
:label="$t('pluginItem.parameterName')"
|
||||
class-name="first-column"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="name-cell">
|
||||
<div
|
||||
v-if="!props.editable"
|
||||
:style="{ paddingLeft: `${getIndentWidth(row)}px` }"
|
||||
>
|
||||
{{ row.name || '' }}
|
||||
</div>
|
||||
<div v-else class="editable-name">
|
||||
<div class="name-input-wrapper">
|
||||
<div :style="{ width: `${getIndentWidth(row)}px` }"></div>
|
||||
<ElInput
|
||||
v-model="row.name"
|
||||
:disabled="row.name === 'arrayItem'"
|
||||
@input="handleDataChange"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="errors[row.key]?.name"
|
||||
class="error-message"
|
||||
:style="{ marginLeft: `${getIndentWidth(row)}px` }"
|
||||
>
|
||||
{{ errors[row.key]?.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
|
||||
<!-- 参数值-->
|
||||
<ElTableColumn
|
||||
prop="defaultValue"
|
||||
:label="$t('plugin.parameterValue')"
|
||||
width="150px"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.type === 'Object'"></span>
|
||||
<span v-else-if="!props.editable">{{ row.defaultValue || '' }}</span>
|
||||
<ElInput
|
||||
v-else
|
||||
v-model="row.defaultValue"
|
||||
@input="handleDataChange"
|
||||
:disabled="!props.editable"
|
||||
/>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.tree-table-container {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.name-cell {
|
||||
position: relative;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.editable-name {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.name-input-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.name-input-wrapper .el-input {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
margin-top: 2px;
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.action-buttons .el-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:deep(.el-table td.el-table__cell.first-column div) {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
215
easyflow-ui-admin/app/src/views/ai/plugin/PluginRunTestModal.vue
Normal file
215
easyflow-ui-admin/app/src/views/ai/plugin/PluginRunTestModal.vue
Normal file
@@ -0,0 +1,215 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import { $t } from '@easyflow/locales';
|
||||
import { preferences } from '@easyflow/preferences';
|
||||
|
||||
import { VideoPlay } from '@element-plus/icons-vue';
|
||||
import { ElButton, ElDialog, ElMenu, ElMenuItem } from 'element-plus';
|
||||
import { JsonViewer } from 'vue3-json-viewer';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import PluginRunParams from '#/views/ai/plugin/PluginRunParams.vue';
|
||||
|
||||
import 'vue3-json-viewer/dist/vue3-json-viewer.css';
|
||||
|
||||
const props = defineProps<{
|
||||
pluginToolId: string;
|
||||
}>();
|
||||
|
||||
const themeMode = ref(preferences.theme.mode);
|
||||
watch(
|
||||
() => preferences.theme.mode,
|
||||
(newVal) => {
|
||||
themeMode.value = newVal;
|
||||
},
|
||||
);
|
||||
const dialogVisible = ref(false);
|
||||
const openDialog = () => {
|
||||
getPluginToolInfo();
|
||||
runResultResponse.value = null;
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
const runTitle = ref('');
|
||||
const runResult = ref('');
|
||||
const inputDataParams = ref<any>(null);
|
||||
const runResultResponse = ref<any>(null);
|
||||
function getPluginToolInfo() {
|
||||
api
|
||||
.post('/api/v1/pluginItem/tool/search', {
|
||||
aiPluginToolId: props.pluginToolId,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
runTitle.value = `${res.data.aiPlugin.title} - ${res.data.data.name} ${$t(
|
||||
'pluginItem.inputData',
|
||||
)}`;
|
||||
runResult.value = `${$t('pluginItem.pluginToolEdit.runResult')}`;
|
||||
inputDataParams.value = JSON.parse(res.data.data.inputData || '[]');
|
||||
}
|
||||
});
|
||||
}
|
||||
const activeIndex = ref('1');
|
||||
defineExpose({
|
||||
openDialog,
|
||||
});
|
||||
function handleSelect(index: string) {
|
||||
activeIndex.value = index;
|
||||
}
|
||||
const runParamsRef = ref();
|
||||
const runLoading = ref(false);
|
||||
function handleSubmitRun() {
|
||||
runLoading.value = true;
|
||||
const runParams = runParamsRef.value.handleSubmitParams();
|
||||
api
|
||||
.post('/api/v1/pluginItem/test', {
|
||||
pluginToolId: props.pluginToolId,
|
||||
inputData: JSON.stringify(runParams),
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
runResultResponse.value = res.data;
|
||||
activeIndex.value = '2';
|
||||
}
|
||||
runLoading.value = false;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
draggable
|
||||
:close-on-click-modal="false"
|
||||
width="80%"
|
||||
align-center
|
||||
class="run-test-dialog"
|
||||
:title="$t('pluginItem.pluginToolEdit.trialRun')"
|
||||
>
|
||||
<div class="run-test-container">
|
||||
<div class="run-test-params">
|
||||
<div class="run-title-style">
|
||||
{{ runTitle }}
|
||||
</div>
|
||||
<div>
|
||||
<PluginRunParams
|
||||
v-model="inputDataParams"
|
||||
:editable="true"
|
||||
:is-edit-output="true"
|
||||
ref="runParamsRef"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="run-test-result">
|
||||
<div class="run-title-style">
|
||||
{{ runResult }}
|
||||
</div>
|
||||
<div>
|
||||
<ElMenu
|
||||
:default-active="activeIndex"
|
||||
class="el-menu-demo"
|
||||
mode="horizontal"
|
||||
:ellipsis="false"
|
||||
@select="handleSelect"
|
||||
>
|
||||
<ElMenuItem index="1">Request</ElMenuItem>
|
||||
<ElMenuItem index="2">Response</ElMenuItem>
|
||||
</ElMenu>
|
||||
</div>
|
||||
<div class="run-res-json">
|
||||
<JsonViewer
|
||||
v-if="activeIndex === '1'"
|
||||
:value="inputDataParams || {}"
|
||||
copyable
|
||||
:expand-depth="Infinity"
|
||||
:theme="themeMode"
|
||||
/>
|
||||
<JsonViewer
|
||||
v-if="activeIndex === '2'"
|
||||
:value="runResultResponse || {}"
|
||||
copyable
|
||||
:expand-depth="Infinity"
|
||||
:theme="themeMode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<ElButton @click="dialogVisible = false">
|
||||
{{ $t('button.cancel') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
:icon="VideoPlay"
|
||||
@click="handleSubmitRun"
|
||||
:loading="runLoading"
|
||||
>
|
||||
{{ $t('pluginItem.pluginToolEdit.run') }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.run-test-container {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
height: calc(100vh - 161px);
|
||||
}
|
||||
.run-test-dialog {
|
||||
}
|
||||
.run-test-params {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
}
|
||||
.run-res-json {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
.run-test-result {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.name-cell {
|
||||
position: relative;
|
||||
min-width: 100%;
|
||||
}
|
||||
.run-title-style {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.editable-name {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.name-input-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.name-input-wrapper .el-input {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #ff4d4f;
|
||||
font-size: 12px;
|
||||
margin-top: 2px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
:deep(.el-table td.el-table__cell.first-column div) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,5 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template></template>
|
||||
|
||||
<style scoped></style>
|
||||
763
easyflow-ui-admin/app/src/views/ai/plugin/PluginToolEdit.vue
Normal file
763
easyflow-ui-admin/app/src/views/ai/plugin/PluginToolEdit.vue
Normal file
@@ -0,0 +1,763 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { $t } from '@easyflow/locales';
|
||||
|
||||
import { ArrowDown, Back, VideoPlay } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElButton,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElIcon,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import PluginInputAndOutParams from '#/views/ai/plugin/PluginInputAndOutParams.vue';
|
||||
import PluginRunTestModal from '#/views/ai/plugin/PluginRunTestModal.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const toolId = ref<string>((route.query.id as string) || '');
|
||||
|
||||
onMounted(() => {
|
||||
if (!toolId.value) {
|
||||
return;
|
||||
}
|
||||
getPluginToolInfo();
|
||||
});
|
||||
const pluginToolInfo = ref<any>({
|
||||
name: '',
|
||||
englishName: '',
|
||||
description: '',
|
||||
basePath: '',
|
||||
requestMethod: '',
|
||||
});
|
||||
const pluginInfo = ref<any>({});
|
||||
const pluginInputData = ref<any[]>([]);
|
||||
const pluginOutputData = ref<any[]>([]);
|
||||
|
||||
function getPluginToolInfo() {
|
||||
api
|
||||
.post('/api/v1/pluginItem/tool/search', {
|
||||
aiPluginToolId: toolId.value,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
pluginToolInfo.value = res.data.data;
|
||||
pluginInfo.value = res.data.aiPlugin;
|
||||
pluginInputData.value = JSON.parse(res.data.data.inputData || '[]');
|
||||
pluginOutputData.value = JSON.parse(res.data.data.outputData || '[]');
|
||||
}
|
||||
});
|
||||
}
|
||||
const pluginInputParamsEditable = ref(false);
|
||||
const pluginOutputParamsEditable = ref(false);
|
||||
|
||||
const pluginBasicCollapse = ref({
|
||||
title: $t('pluginItem.pluginToolEdit.basicInfo'),
|
||||
isOpen: true,
|
||||
isEdit: false,
|
||||
});
|
||||
const pluginBasicCollapseInputParams = ref({
|
||||
title: $t('pluginItem.pluginToolEdit.configureInputParameters'),
|
||||
isOpen: false,
|
||||
isEdit: false,
|
||||
});
|
||||
const pluginBasicCollapseOutputParams = ref({
|
||||
title: $t('pluginItem.pluginToolEdit.configureOutputParameters'),
|
||||
isOpen: false,
|
||||
isEdit: false,
|
||||
});
|
||||
const pluginInputParamsRef = ref();
|
||||
const pluginOutputParamsRef = ref();
|
||||
const handleClickHeader = (index: number) => {
|
||||
switch (index) {
|
||||
case 1: {
|
||||
pluginBasicCollapse.value.isOpen = !pluginBasicCollapse.value.isOpen;
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
pluginBasicCollapseInputParams.value.isOpen =
|
||||
!pluginBasicCollapseInputParams.value.isOpen;
|
||||
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
pluginBasicCollapseOutputParams.value.isOpen =
|
||||
!pluginBasicCollapseOutputParams.value.isOpen;
|
||||
|
||||
break;
|
||||
}
|
||||
// No default
|
||||
}
|
||||
};
|
||||
|
||||
const back = () => {
|
||||
router.back();
|
||||
};
|
||||
const rules = reactive({
|
||||
name: [{ required: true, message: $t('message.required'), trigger: 'blur' }],
|
||||
requestMethod: [
|
||||
{
|
||||
required: true,
|
||||
message: $t('message.required'),
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
basePath: [
|
||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
||||
],
|
||||
englishName: [
|
||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
||||
],
|
||||
description: [
|
||||
{
|
||||
required: true,
|
||||
message: $t('message.required'),
|
||||
trigger: 'blur',
|
||||
whiteSpace: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
const saveForm = ref();
|
||||
const updatePluginTool = (index: number) => {
|
||||
if (index === 1) {
|
||||
if (!saveForm.value) return;
|
||||
saveForm.value.validate((valid: boolean) => {
|
||||
if (valid) {
|
||||
updatePluginToolInfo(index);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
updatePluginToolInfo(index);
|
||||
}
|
||||
};
|
||||
const updatePluginToolInfo = (index: number) => {
|
||||
api
|
||||
.post('/api/v1/pluginItem/tool/update', {
|
||||
id: toolId.value,
|
||||
name: pluginToolInfo.value.name,
|
||||
englishName: pluginToolInfo.value.englishName,
|
||||
description: pluginToolInfo.value.description,
|
||||
basePath: pluginToolInfo.value.basePath,
|
||||
requestMethod: pluginToolInfo.value.requestMethod,
|
||||
inputData: JSON.stringify(pluginInputData.value),
|
||||
outputData: JSON.stringify(pluginOutputData.value),
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success($t('message.updateOkMessage'));
|
||||
switch (index) {
|
||||
case 1: {
|
||||
pluginBasicCollapse.value.isEdit = false;
|
||||
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
pluginBasicCollapseInputParams.value.isEdit = false;
|
||||
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
pluginBasicCollapseOutputParams.value.isEdit = false;
|
||||
|
||||
break;
|
||||
}
|
||||
// No default
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
const handleEdit = (index: number) => {
|
||||
switch (index) {
|
||||
case 1: {
|
||||
pluginBasicCollapse.value.isEdit = true;
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
pluginBasicCollapseInputParams.value.isEdit = true;
|
||||
pluginBasicCollapseInputParams.value.isOpen = true;
|
||||
pluginInputParamsEditable.value = true;
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
pluginBasicCollapseOutputParams.value.isEdit = true;
|
||||
pluginBasicCollapseOutputParams.value.isOpen = true;
|
||||
pluginOutputParamsEditable.value = true;
|
||||
break;
|
||||
}
|
||||
// No default
|
||||
}
|
||||
};
|
||||
const handleSave = (index: number) => {
|
||||
if (index === 2) {
|
||||
try {
|
||||
// 调用校验方法,若抛异常则进入 catch
|
||||
pluginInputParamsRef.value.handleSubmitParams();
|
||||
} catch (error) {
|
||||
console.error('校验失败:', error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (index === 3) {
|
||||
try {
|
||||
pluginOutputParamsRef.value.handleSubmitParams();
|
||||
} catch (error) {
|
||||
console.error('校验失败:', error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
pluginInputParamsEditable.value = false;
|
||||
updatePluginTool(index);
|
||||
};
|
||||
const handleCancel = (index: number) => {
|
||||
getPluginToolInfo();
|
||||
switch (index) {
|
||||
case 1: {
|
||||
pluginBasicCollapse.value.isEdit = false;
|
||||
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
pluginBasicCollapseInputParams.value.isEdit = false;
|
||||
pluginInputParamsEditable.value = false;
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
pluginBasicCollapseOutputParams.value.isEdit = false;
|
||||
pluginOutputParamsEditable.value = false;
|
||||
break;
|
||||
}
|
||||
// No default
|
||||
}
|
||||
};
|
||||
const requestMethodOptions = [
|
||||
{
|
||||
label: 'POST',
|
||||
value: 'POST',
|
||||
},
|
||||
{
|
||||
label: 'GET',
|
||||
value: 'GET',
|
||||
},
|
||||
{
|
||||
label: 'PUT',
|
||||
value: 'PUT',
|
||||
},
|
||||
{
|
||||
label: 'DELETE',
|
||||
value: 'DELETE',
|
||||
},
|
||||
{
|
||||
label: 'PATCH',
|
||||
value: 'PATCH',
|
||||
},
|
||||
];
|
||||
const runTestRef = ref();
|
||||
const handleOpenRunModal = () => {
|
||||
runTestRef.value.openDialog();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="accordion-container">
|
||||
<div class="controls-header">
|
||||
<ElButton @click="back" :icon="Back">
|
||||
{{ $t('button.back') }}
|
||||
</ElButton>
|
||||
<ElButton type="primary" :icon="VideoPlay" @click="handleOpenRunModal">
|
||||
{{ $t('pluginItem.pluginToolEdit.trialRun') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
<!-- 折叠面板列表 -->
|
||||
<div class="accordion-list">
|
||||
<!-- 基本信息-->
|
||||
<div
|
||||
class="accordion-item"
|
||||
:class="{ 'accordion-item--active': pluginBasicCollapse.isOpen }"
|
||||
>
|
||||
<!-- 面板头部 -->
|
||||
<div class="accordion-header" @click="handleClickHeader(1)">
|
||||
<div class="column-header-container">
|
||||
<div
|
||||
class="accordion-icon"
|
||||
:class="{ 'accordion-icon--rotated': pluginBasicCollapse.isOpen }"
|
||||
>
|
||||
<ElIcon size="16">
|
||||
<ArrowDown />
|
||||
</ElIcon>
|
||||
</div>
|
||||
<h3 class="accordion-title">{{ pluginBasicCollapse.title }}</h3>
|
||||
</div>
|
||||
<div>
|
||||
<ElButton
|
||||
@click.stop="handleEdit(1)"
|
||||
type="primary"
|
||||
v-if="!pluginBasicCollapse.isEdit"
|
||||
>
|
||||
{{ $t('button.edit') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
@click.stop="handleCancel(1)"
|
||||
v-if="pluginBasicCollapse.isEdit"
|
||||
>
|
||||
{{ $t('button.cancel') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
@click.stop="handleSave(1)"
|
||||
type="primary"
|
||||
v-if="pluginBasicCollapse.isEdit"
|
||||
>
|
||||
{{ $t('button.save') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 面板内容 -->
|
||||
<div
|
||||
class="accordion-content"
|
||||
:class="{ 'accordion-content--open': pluginBasicCollapse.isOpen }"
|
||||
>
|
||||
<div class="accordion-content-inner">
|
||||
<!--编辑基本信息-->
|
||||
<div v-show="pluginBasicCollapse.isEdit">
|
||||
<div class="plugin-tool-info-edit-container">
|
||||
<ElForm
|
||||
ref="saveForm"
|
||||
:model="pluginToolInfo"
|
||||
label-width="80px"
|
||||
status-icon
|
||||
:rules="rules"
|
||||
>
|
||||
<ElFormItem :label="$t('pluginItem.name')" prop="name">
|
||||
<ElInput v-model.trim="pluginToolInfo.name" />
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
:label="$t('pluginItem.englishName')"
|
||||
prop="englishName"
|
||||
>
|
||||
<ElInput v-model.trim="pluginToolInfo.englishName" />
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
:label="$t('pluginItem.pluginToolEdit.toolPath')"
|
||||
prop="basePath"
|
||||
>
|
||||
<ElInput v-model.trim="pluginToolInfo.basePath">
|
||||
<template #prepend>{{ pluginInfo.baseUrl }}</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
:label="$t('pluginItem.description')"
|
||||
prop="description"
|
||||
>
|
||||
<ElInput
|
||||
v-model.trim="pluginToolInfo.description"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
:label="$t('pluginItem.pluginToolEdit.requestMethod')"
|
||||
prop="requestMethod"
|
||||
>
|
||||
<ElSelect
|
||||
v-model="pluginToolInfo.requestMethod"
|
||||
:placeholder="$t('ui.placeholder.select')"
|
||||
>
|
||||
<ElOption
|
||||
v-for="option in requestMethodOptions"
|
||||
:key="option.value"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</div>
|
||||
</div>
|
||||
<!--显示基本信息-->
|
||||
<div
|
||||
v-show="!pluginBasicCollapse.isEdit"
|
||||
class="plugin-tool-info-view-container"
|
||||
>
|
||||
<div class="plugin-tool-view-item">
|
||||
<div class="view-item-title">{{ $t('pluginItem.name') }}:</div>
|
||||
<div>{{ pluginToolInfo.name }}</div>
|
||||
</div>
|
||||
<div class="plugin-tool-view-item">
|
||||
<div class="view-item-title">
|
||||
{{ $t('pluginItem.englishName') }}:
|
||||
</div>
|
||||
<div>{{ pluginToolInfo.englishName }}</div>
|
||||
</div>
|
||||
<div class="plugin-tool-view-item">
|
||||
<div class="view-item-title">
|
||||
{{ $t('pluginItem.description') }}:
|
||||
</div>
|
||||
<div>{{ pluginToolInfo.description }}</div>
|
||||
</div>
|
||||
<div class="plugin-tool-view-item">
|
||||
<div class="view-item-title">
|
||||
{{ $t('pluginItem.pluginToolEdit.toolPath') }}:
|
||||
</div>
|
||||
<div>{{ pluginInfo.baseUrl }}{{ pluginToolInfo.basePath }}</div>
|
||||
</div>
|
||||
<div class="plugin-tool-view-item">
|
||||
<div class="view-item-title">
|
||||
{{ $t('pluginItem.pluginToolEdit.requestMethod') }}:
|
||||
</div>
|
||||
<div>
|
||||
{{ pluginToolInfo.requestMethod }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 输入参数-->
|
||||
<div
|
||||
class="accordion-item"
|
||||
:class="{
|
||||
'accordion-item--active': pluginBasicCollapseInputParams.isOpen,
|
||||
}"
|
||||
>
|
||||
<!-- 面板头部 -->
|
||||
<div class="accordion-header" @click="handleClickHeader(2)">
|
||||
<div class="column-header-container">
|
||||
<div
|
||||
class="accordion-icon"
|
||||
:class="{
|
||||
'accordion-icon--rotated':
|
||||
pluginBasicCollapseInputParams.isOpen,
|
||||
}"
|
||||
>
|
||||
<ElIcon size="16">
|
||||
<ArrowDown />
|
||||
</ElIcon>
|
||||
</div>
|
||||
<h3 class="accordion-title">
|
||||
{{ pluginBasicCollapseInputParams.title }}
|
||||
</h3>
|
||||
</div>
|
||||
<div>
|
||||
<ElButton
|
||||
@click.stop="handleEdit(2)"
|
||||
type="primary"
|
||||
v-if="!pluginBasicCollapseInputParams.isEdit"
|
||||
>
|
||||
{{ $t('button.edit') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
@click.stop="handleCancel(2)"
|
||||
v-if="pluginBasicCollapseInputParams.isEdit"
|
||||
>
|
||||
{{ $t('button.cancel') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
@click.stop="handleSave(2)"
|
||||
type="primary"
|
||||
v-if="pluginBasicCollapseInputParams.isEdit"
|
||||
>
|
||||
{{ $t('button.save') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--输入参数-->
|
||||
<div
|
||||
class="accordion-content"
|
||||
:class="{
|
||||
'accordion-content--open': pluginBasicCollapseInputParams.isOpen,
|
||||
}"
|
||||
>
|
||||
<div class="accordion-content-inner">
|
||||
<PluginInputAndOutParams
|
||||
ref="pluginInputParamsRef"
|
||||
v-model="pluginInputData"
|
||||
:editable="pluginInputParamsEditable"
|
||||
:is-edit-output="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 输出参数-->
|
||||
<div
|
||||
class="accordion-item"
|
||||
:class="{
|
||||
'accordion-item--active': pluginBasicCollapseOutputParams.isOpen,
|
||||
}"
|
||||
>
|
||||
<!-- 面板头部 -->
|
||||
<div class="accordion-header" @click="handleClickHeader(3)">
|
||||
<div class="column-header-container">
|
||||
<div
|
||||
class="accordion-icon"
|
||||
:class="{
|
||||
'accordion-icon--rotated':
|
||||
pluginBasicCollapseOutputParams.isOpen,
|
||||
}"
|
||||
>
|
||||
<ElIcon size="16">
|
||||
<ArrowDown />
|
||||
</ElIcon>
|
||||
</div>
|
||||
<h3 class="accordion-title">
|
||||
{{ pluginBasicCollapseOutputParams.title }}
|
||||
</h3>
|
||||
</div>
|
||||
<div>
|
||||
<ElButton
|
||||
@click.stop="handleEdit(3)"
|
||||
type="primary"
|
||||
v-if="!pluginBasicCollapseOutputParams.isEdit"
|
||||
>
|
||||
{{ $t('button.edit') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
@click.stop="handleCancel(3)"
|
||||
v-if="pluginBasicCollapseOutputParams.isEdit"
|
||||
>
|
||||
{{ $t('button.cancel') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
@click.stop="handleSave(3)"
|
||||
type="primary"
|
||||
v-if="pluginBasicCollapseOutputParams.isEdit"
|
||||
>
|
||||
{{ $t('button.save') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--输出参数-->
|
||||
<div
|
||||
class="accordion-content"
|
||||
:class="{
|
||||
'accordion-content--open': pluginBasicCollapseOutputParams.isOpen,
|
||||
}"
|
||||
>
|
||||
<div class="accordion-content-inner">
|
||||
<PluginInputAndOutParams
|
||||
v-model="pluginOutputData"
|
||||
ref="pluginOutputParamsRef"
|
||||
:editable="pluginOutputParamsEditable"
|
||||
:is-edit-output="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 试运行模态框-->
|
||||
<PluginRunTestModal ref="runTestRef" :plugin-tool-id="toolId" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.accordion-container {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.accordion-header {
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.accordion-title {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.accordion-container {
|
||||
max-width: 100%;
|
||||
padding: 20px;
|
||||
margin: 0 auto;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
.controls-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-bottom: 8px;
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin-bottom: 30px;
|
||||
font-size: 1.1rem;
|
||||
color: var(--el-text-color-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 控制面板样式 */
|
||||
.controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20px;
|
||||
margin-bottom: 30px;
|
||||
background: var(--el-bg-color);
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-secondary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-secondary);
|
||||
cursor: pointer;
|
||||
background: var(--el-bg-color);
|
||||
border-radius: 4px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.control-btn:hover {
|
||||
background: #3498db;
|
||||
background: var(--el-color-primary-light-9);
|
||||
}
|
||||
|
||||
/* 折叠面板列表 */
|
||||
.accordion-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.accordion-item {
|
||||
overflow: hidden;
|
||||
background: hsl(var(--background));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.accordion-item:hover {
|
||||
box-shadow: 0 4px 12px rgb(0 0 0 / 10%);
|
||||
}
|
||||
|
||||
.accordion-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
background: hsl(var(--background));
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.accordion-title {
|
||||
padding-left: 12px;
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.accordion-icon {
|
||||
font-size: 12px;
|
||||
color: #7f8c8d;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.accordion-icon--rotated {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.accordion-content {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
background: hsl(var(--background));
|
||||
transition: max-height 0.4s ease;
|
||||
}
|
||||
|
||||
.accordion-content--open {
|
||||
max-height: 2000px;
|
||||
}
|
||||
|
||||
.accordion-content-inner {
|
||||
padding: 20px;
|
||||
border-top: 1px solid hsl(var(--border));
|
||||
}
|
||||
|
||||
.accordion-content-inner p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.column-header-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.plugin-tool-info-view-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 25px;
|
||||
}
|
||||
|
||||
.plugin-tool-view-item {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.view-item-title {
|
||||
width: 70px;
|
||||
|
||||
/* text-align: right; */
|
||||
|
||||
/* margin-right: 12px; */
|
||||
}
|
||||
</style>
|
||||
135
easyflow-ui-admin/app/src/views/ai/plugin/PluginToolTable.vue
Normal file
135
easyflow-ui-admin/app/src/views/ai/plugin/PluginToolTable.vue
Normal file
@@ -0,0 +1,135 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { $t } from '@easyflow/locales';
|
||||
|
||||
import { Delete, MoreFilled } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElButton,
|
||||
ElDropdown,
|
||||
ElDropdownItem,
|
||||
ElDropdownMenu,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import PageData from '#/components/page/PageData.vue';
|
||||
import AiPluginToolModal from '#/views/ai/plugin/AiPluginToolModal.vue';
|
||||
|
||||
const props = defineProps({
|
||||
pluginId: {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
const router = useRouter();
|
||||
defineExpose({
|
||||
openPluginToolModal() {
|
||||
aiPluginToolRef.value.openDialog();
|
||||
},
|
||||
reload: () => {
|
||||
pageDataRef.value.setQuery({ pluginId: props.pluginId });
|
||||
},
|
||||
handleSearch: (params: string) => {
|
||||
pageDataRef.value.setQuery({
|
||||
pluginId: props.pluginId,
|
||||
isQueryOr: true,
|
||||
name: params,
|
||||
});
|
||||
},
|
||||
});
|
||||
const pageDataRef = ref();
|
||||
const handleEdit = (row: any) => {
|
||||
router.push({
|
||||
path: '/ai/plugin/tool/edit',
|
||||
query: {
|
||||
id: row.id,
|
||||
pageKey: '/ai/plugin',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = (row: any) => {
|
||||
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
|
||||
confirmButtonText: $t('button.confirm'),
|
||||
cancelButtonText: $t('button.cancel'),
|
||||
type: 'warning',
|
||||
}).then(() => {
|
||||
api.post('/api/v1/pluginItem/remove', { id: row.id }).then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success($t('message.deleteOkMessage'));
|
||||
pageDataRef.value.setQuery({ pluginId: props.pluginId });
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
const aiPluginToolRef = ref();
|
||||
const pluginToolReload = () => {
|
||||
pageDataRef.value.setQuery({ pluginId: props.pluginId });
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageData
|
||||
page-url="/api/v1/pluginItem/page"
|
||||
ref="pageDataRef"
|
||||
:page-size="10"
|
||||
:extra-query-params="{ pluginId: props.pluginId }"
|
||||
>
|
||||
<template #default="{ pageList }">
|
||||
<ElTable :data="pageList" style="width: 100%" size="large">
|
||||
<ElTableColumn prop="name" :label="$t('pluginItem.name')" />
|
||||
<ElTableColumn
|
||||
prop="description"
|
||||
:label="$t('pluginItem.description')"
|
||||
/>
|
||||
<ElTableColumn prop="created" :label="$t('pluginItem.created')" />
|
||||
<ElTableColumn
|
||||
fixed="right"
|
||||
:label="$t('common.handle')"
|
||||
width="100"
|
||||
align="right"
|
||||
>
|
||||
<template #default="scope">
|
||||
<div class="flex items-center gap-3">
|
||||
<ElButton link type="primary" @click="handleEdit(scope.row)">
|
||||
{{ $t('button.edit') }}
|
||||
</ElButton>
|
||||
|
||||
<ElDropdown>
|
||||
<ElButton link :icon="MoreFilled" />
|
||||
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownItem @click="handleDelete(scope.row)">
|
||||
<ElButton link :icon="Delete" type="danger">
|
||||
{{ $t('button.delete') }}
|
||||
</ElButton>
|
||||
</ElDropdownItem>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</template>
|
||||
</PageData>
|
||||
<AiPluginToolModal
|
||||
ref="aiPluginToolRef"
|
||||
:plugin-id="pluginId"
|
||||
@reload="pluginToolReload"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.time-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
||||
62
easyflow-ui-admin/app/src/views/ai/plugin/PluginTools.vue
Normal file
62
easyflow-ui-admin/app/src/views/ai/plugin/PluginTools.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
import { markRaw, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { $t } from '@easyflow/locales';
|
||||
|
||||
import { Back, Plus } from '@element-plus/icons-vue';
|
||||
|
||||
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
||||
import PluginToolTable from '#/views/ai/plugin/PluginToolTable.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const pluginId = ref<string>((route.query.id as string) || '');
|
||||
const headerButtons = [
|
||||
{
|
||||
key: 'back',
|
||||
text: $t('button.back'),
|
||||
icon: markRaw(Back),
|
||||
data: { action: 'back' },
|
||||
},
|
||||
{
|
||||
key: 'createTool',
|
||||
text: $t('pluginItem.createPluginTool'),
|
||||
icon: markRaw(Plus),
|
||||
type: 'primary',
|
||||
data: { action: 'createTool' },
|
||||
},
|
||||
];
|
||||
const handleSearch = (params: any) => {
|
||||
pluginToolRef.value.handleSearch(params);
|
||||
};
|
||||
const handleButtonClick = (event: any) => {
|
||||
// 根据按钮 key 执行不同操作
|
||||
switch (event.key) {
|
||||
case 'back': {
|
||||
router.push({ path: '/ai/plugin' });
|
||||
break;
|
||||
}
|
||||
case 'createTool': {
|
||||
pluginToolRef.value.openPluginToolModal({});
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
const pluginToolRef = ref();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full flex-col gap-6 p-6">
|
||||
<HeaderSearch
|
||||
:buttons="headerButtons"
|
||||
@search="handleSearch"
|
||||
@button-click="handleButtonClick"
|
||||
/>
|
||||
|
||||
<div class="bg-background border-border flex-1 rounded-lg border p-5">
|
||||
<PluginToolTable :plugin-id="pluginId" ref="pluginToolRef" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user