初始化

This commit is contained in:
2026-02-22 18:56:10 +08:00
commit 26677972a6
3112 changed files with 255972 additions and 0 deletions

View 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>

View 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>

View File

@@ -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>

View 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>

View File

@@ -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>

View 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>

View 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>

View File

@@ -0,0 +1,5 @@
<script setup lang="ts"></script>
<template></template>
<style scoped></style>

View 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>

View 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>

View 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>