初始化
This commit is contained in:
3
easyflow-ui-admin/app/src/views/_core/README.md
Normal file
3
easyflow-ui-admin/app/src/views/_core/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# \_core
|
||||
|
||||
此目录包含应用程序正常运行所需的基本视图。这些视图是应用程序布局中使用的视图。
|
||||
39
easyflow-ui-admin/app/src/views/_core/about/index.vue
Normal file
39
easyflow-ui-admin/app/src/views/_core/about/index.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { About } from '@easyflow/common-ui';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import { useDictStore } from '#/store';
|
||||
|
||||
defineOptions({ name: 'About' });
|
||||
const dictStore = useDictStore();
|
||||
onMounted(() => {
|
||||
test();
|
||||
dictStore.fetchDictionary('accountType');
|
||||
});
|
||||
const accountInfo = ref<any>();
|
||||
function test() {
|
||||
api
|
||||
.get('/api/v1/sysAccount/myProfile')
|
||||
.then((res) => {
|
||||
accountInfo.value = res.data;
|
||||
// console.log('res', res);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('error', error);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div>{{ accountInfo?.loginName }}</div>
|
||||
<div v-for="(item, index) in [0, 1, 99]" :key="index">
|
||||
<div>
|
||||
{{ dictStore.getDictLabel('accountType', item) }}
|
||||
</div>
|
||||
</div>
|
||||
<About />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,69 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EasyFlowFormSchema } from '@easyflow/common-ui';
|
||||
import type { Recordable } from '@easyflow/types';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { AuthenticationCodeLogin, z } from '@easyflow/common-ui';
|
||||
import { $t } from '@easyflow/locales';
|
||||
|
||||
defineOptions({ name: 'CodeLogin' });
|
||||
|
||||
const loading = ref(false);
|
||||
const CODE_LENGTH = 6;
|
||||
|
||||
const formSchema = computed((): EasyFlowFormSchema[] => {
|
||||
return [
|
||||
{
|
||||
component: 'EasyFlowInput',
|
||||
componentProps: {
|
||||
placeholder: $t('authentication.mobile'),
|
||||
},
|
||||
fieldName: 'phoneNumber',
|
||||
label: $t('authentication.mobile'),
|
||||
rules: z
|
||||
.string()
|
||||
.min(1, { message: $t('authentication.mobileTip') })
|
||||
.refine((v) => /^\d{11}$/.test(v), {
|
||||
message: $t('authentication.mobileErrortip'),
|
||||
}),
|
||||
},
|
||||
{
|
||||
component: 'EasyFlowPinInput',
|
||||
componentProps: {
|
||||
codeLength: CODE_LENGTH,
|
||||
createText: (countdown: number) => {
|
||||
const text =
|
||||
countdown > 0
|
||||
? $t('authentication.sendText', [countdown])
|
||||
: $t('authentication.sendCode');
|
||||
return text;
|
||||
},
|
||||
placeholder: $t('authentication.code'),
|
||||
},
|
||||
fieldName: 'code',
|
||||
label: $t('authentication.code'),
|
||||
rules: z.string().length(CODE_LENGTH, {
|
||||
message: $t('authentication.codeTip', [CODE_LENGTH]),
|
||||
}),
|
||||
},
|
||||
];
|
||||
});
|
||||
/**
|
||||
* 异步处理登录操作
|
||||
* Asynchronously handle the login process
|
||||
* @param values 登录表单数据
|
||||
*/
|
||||
async function handleLogin(values: Recordable<any>) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(values);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthenticationCodeLogin
|
||||
:form-schema="formSchema"
|
||||
:loading="loading"
|
||||
@submit="handleLogin"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,43 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EasyFlowFormSchema } from '@easyflow/common-ui';
|
||||
import type { Recordable } from '@easyflow/types';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { AuthenticationForgetPassword, z } from '@easyflow/common-ui';
|
||||
import { $t } from '@easyflow/locales';
|
||||
|
||||
defineOptions({ name: 'ForgetPassword' });
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
const formSchema = computed((): EasyFlowFormSchema[] => {
|
||||
return [
|
||||
{
|
||||
component: 'EasyFlowInput',
|
||||
componentProps: {
|
||||
placeholder: 'example@example.com',
|
||||
},
|
||||
fieldName: 'email',
|
||||
label: $t('authentication.email'),
|
||||
rules: z
|
||||
.string()
|
||||
.min(1, { message: $t('authentication.emailTip') })
|
||||
.email($t('authentication.emailValidErrorTip')),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
function handleSubmit(value: Recordable<any>) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('reset email:', value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthenticationForgetPassword
|
||||
:form-schema="formSchema"
|
||||
:loading="loading"
|
||||
@submit="handleSubmit"
|
||||
/>
|
||||
</template>
|
||||
129
easyflow-ui-admin/app/src/views/_core/authentication/login.vue
Normal file
129
easyflow-ui-admin/app/src/views/_core/authentication/login.vue
Normal file
@@ -0,0 +1,129 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EasyFlowFormSchema } from '@easyflow/common-ui';
|
||||
|
||||
import { computed, onMounted } from 'vue';
|
||||
|
||||
import { AuthenticationLogin, z } from '@easyflow/common-ui';
|
||||
import { useAppConfig } from '@easyflow/hooks';
|
||||
import { $t } from '@easyflow/locales';
|
||||
import { preferences } from '@easyflow/preferences';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import { useAuthStore } from '#/store';
|
||||
|
||||
defineOptions({ name: 'Login' });
|
||||
onMounted(() => {});
|
||||
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
||||
|
||||
type PlatformType = 'ding_talk' | 'wx_web';
|
||||
|
||||
const title = computed(() => preferences.auth.welcomeBack);
|
||||
const subTitle = computed(() => preferences.auth.loginSubtitle);
|
||||
const formSchema = computed((): EasyFlowFormSchema[] => {
|
||||
return [
|
||||
{
|
||||
component: 'EasyFlowInput',
|
||||
componentProps: {
|
||||
placeholder: $t('authentication.usernameTip'),
|
||||
},
|
||||
fieldName: 'account',
|
||||
label: $t('authentication.username'),
|
||||
rules: z.string().min(1, { message: $t('authentication.usernameTip') }),
|
||||
},
|
||||
{
|
||||
component: 'EasyFlowInputPassword',
|
||||
componentProps: {
|
||||
placeholder: $t('authentication.password'),
|
||||
},
|
||||
fieldName: 'password',
|
||||
label: $t('authentication.password'),
|
||||
rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
function onSubmit(values: any) {
|
||||
// config 对象为TAC验证码的一些配置和验证的回调
|
||||
const config = {
|
||||
// 生成接口 (必选项,必须配置, 要符合tianai-captcha默认验证码生成接口规范)
|
||||
requestCaptchaDataUrl: `${apiURL}/api/v1/public/getCaptcha`,
|
||||
// 验证接口 (必选项,必须配置, 要符合tianai-captcha默认验证码校验接口规范)
|
||||
validCaptchaUrl: `${apiURL}/api/v1/public/check`,
|
||||
// 验证码绑定的div块 (必选项,必须配置)
|
||||
bindEl: '#captcha-box',
|
||||
// 验证成功回调函数(必选项,必须配置)
|
||||
validSuccess: (res: any, _: any, tac: any) => {
|
||||
// 销毁验证码服务
|
||||
tac.destroyWindow();
|
||||
// 调用具体的login方法
|
||||
values.validToken = res.data;
|
||||
authStore.authLogin(values);
|
||||
},
|
||||
// 验证失败的回调函数(可忽略,如果不自定义 validFail 方法时,会使用默认的)
|
||||
validFail: (_: any, __: any, tac: any) => {
|
||||
// 验证失败后重新拉取验证码
|
||||
tac.reloadCaptcha();
|
||||
},
|
||||
// 刷新按钮回调事件
|
||||
btnRefreshFun: (_: any, tac: any) => {
|
||||
tac.reloadCaptcha();
|
||||
},
|
||||
// 关闭按钮回调事件
|
||||
btnCloseFun: (_: any, tac: any) => {
|
||||
tac.destroyWindow();
|
||||
},
|
||||
};
|
||||
const style = {
|
||||
logoUrl: null, // 去除logo
|
||||
// logoUrl: "/xx/xx/xxx.png" // 替换成自定义的logo
|
||||
btnUrl: '/tac-btn.png',
|
||||
};
|
||||
window
|
||||
// @ts-ignore
|
||||
.initTAC('/tac', config, style)
|
||||
.then((tac: any) => {
|
||||
tac.init(); // 调用init则显示验证码
|
||||
})
|
||||
.catch((error: any) => {
|
||||
console.error('初始化tac失败', error);
|
||||
});
|
||||
}
|
||||
|
||||
function getAuthUrl(platform: PlatformType) {
|
||||
return api.get('/thirdAuth/getAuthUrl', {
|
||||
params: {
|
||||
platform,
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<AuthenticationLogin
|
||||
:form-schema="formSchema"
|
||||
:loading="authStore.loginLoading"
|
||||
:title="title"
|
||||
:sub-title="subTitle"
|
||||
@submit="onSubmit"
|
||||
/>
|
||||
<div id="captcha-box" class="captcha-div"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.captcha-div {
|
||||
position: absolute;
|
||||
top: 30vh;
|
||||
left: 21vh;
|
||||
}
|
||||
|
||||
.platform-icon {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { useAccessStore, useUserStore } from '@easyflow/stores';
|
||||
|
||||
import { getAccessCodesApi, getUserInfoApi } from '#/api';
|
||||
|
||||
const accessStore = useAccessStore();
|
||||
const userStore = useUserStore();
|
||||
const route = useRoute();
|
||||
const token: any = route.query.token;
|
||||
onMounted(() => {
|
||||
redirect();
|
||||
});
|
||||
async function redirect() {
|
||||
accessStore.setAccessToken(token);
|
||||
const [fetchUserInfoResult, accessCodes] = await Promise.all([
|
||||
getUserInfoApi(),
|
||||
getAccessCodesApi(),
|
||||
]);
|
||||
userStore.setUserInfo(fetchUserInfoResult);
|
||||
accessStore.setAccessCodes(accessCodes);
|
||||
window.location.href = '/';
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>redirecting...</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
import { AuthenticationQrCodeLogin } from '@easyflow/common-ui';
|
||||
import { LOGIN_PATH } from '@easyflow/constants';
|
||||
|
||||
defineOptions({ name: 'QrCodeLogin' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthenticationQrCodeLogin :login-path="LOGIN_PATH" />
|
||||
</template>
|
||||
@@ -0,0 +1,96 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EasyFlowFormSchema } from '@easyflow/common-ui';
|
||||
import type { Recordable } from '@easyflow/types';
|
||||
|
||||
import { computed, h, ref } from 'vue';
|
||||
|
||||
import { AuthenticationRegister, z } from '@easyflow/common-ui';
|
||||
import { $t } from '@easyflow/locales';
|
||||
|
||||
defineOptions({ name: 'Register' });
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
const formSchema = computed((): EasyFlowFormSchema[] => {
|
||||
return [
|
||||
{
|
||||
component: 'EasyFlowInput',
|
||||
componentProps: {
|
||||
placeholder: $t('authentication.usernameTip'),
|
||||
},
|
||||
fieldName: 'username',
|
||||
label: $t('authentication.username'),
|
||||
rules: z.string().min(1, { message: $t('authentication.usernameTip') }),
|
||||
},
|
||||
{
|
||||
component: 'EasyFlowInputPassword',
|
||||
componentProps: {
|
||||
passwordStrength: true,
|
||||
placeholder: $t('authentication.password'),
|
||||
},
|
||||
fieldName: 'password',
|
||||
label: $t('authentication.password'),
|
||||
renderComponentContent() {
|
||||
return {
|
||||
strengthText: () => $t('authentication.passwordStrength'),
|
||||
};
|
||||
},
|
||||
rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
|
||||
},
|
||||
{
|
||||
component: 'EasyFlowInputPassword',
|
||||
componentProps: {
|
||||
placeholder: $t('authentication.confirmPassword'),
|
||||
},
|
||||
dependencies: {
|
||||
rules(values) {
|
||||
const { password } = values;
|
||||
return z
|
||||
.string({ required_error: $t('authentication.passwordTip') })
|
||||
.min(1, { message: $t('authentication.passwordTip') })
|
||||
.refine((value) => value === password, {
|
||||
message: $t('authentication.confirmPasswordTip'),
|
||||
});
|
||||
},
|
||||
triggerFields: ['password'],
|
||||
},
|
||||
fieldName: 'confirmPassword',
|
||||
label: $t('authentication.confirmPassword'),
|
||||
},
|
||||
{
|
||||
component: 'EasyFlowCheckbox',
|
||||
fieldName: 'agreePolicy',
|
||||
renderComponentContent: () => ({
|
||||
default: () =>
|
||||
h('span', [
|
||||
$t('authentication.agree'),
|
||||
h(
|
||||
'a',
|
||||
{
|
||||
class: 'easyflow-link ml-1 ',
|
||||
href: '',
|
||||
},
|
||||
`${$t('authentication.privacyPolicy')} & ${$t('authentication.terms')}`,
|
||||
),
|
||||
]),
|
||||
}),
|
||||
rules: z.boolean().refine((value) => !!value, {
|
||||
message: $t('authentication.agreeTip'),
|
||||
}),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
function handleSubmit(value: Recordable<any>) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('register submit:', value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthenticationRegister
|
||||
:form-schema="formSchema"
|
||||
:loading="loading"
|
||||
@submit="handleSubmit"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@easyflow/common-ui';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback status="coming-soon" />
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@easyflow/common-ui';
|
||||
|
||||
defineOptions({ name: 'Fallback403Demo' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback status="403" />
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@easyflow/common-ui';
|
||||
|
||||
defineOptions({ name: 'Fallback500Demo' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback status="500" />
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@easyflow/common-ui';
|
||||
|
||||
defineOptions({ name: 'Fallback404Demo' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback status="404" />
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@easyflow/common-ui';
|
||||
|
||||
defineOptions({ name: 'FallbackOfflineDemo' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback status="offline" />
|
||||
</template>
|
||||
@@ -0,0 +1,77 @@
|
||||
<script setup lang="ts">
|
||||
import type { EasyFlowFormSchema } from '#/adapter/form';
|
||||
|
||||
import { computed, markRaw, onMounted, ref } from 'vue';
|
||||
|
||||
import { ProfileBaseSetting } from '@easyflow/common-ui';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import Cropper from '#/components/upload/Cropper.vue';
|
||||
import { $t } from '#/locales';
|
||||
import { useAuthStore } from '#/store';
|
||||
|
||||
const { fetchUserInfo } = useAuthStore();
|
||||
const profileBaseSettingRef = ref();
|
||||
|
||||
const formSchema = computed((): EasyFlowFormSchema[] => {
|
||||
return [
|
||||
{
|
||||
fieldName: 'avatar',
|
||||
component: markRaw(Cropper),
|
||||
componentProps: {
|
||||
crop: true,
|
||||
},
|
||||
label: $t('sysAccount.avatar'),
|
||||
},
|
||||
{
|
||||
fieldName: 'nickname',
|
||||
component: 'Input',
|
||||
label: $t('sysAccount.nickname'),
|
||||
},
|
||||
{
|
||||
fieldName: 'mobile',
|
||||
component: 'Input',
|
||||
label: $t('sysAccount.mobile'),
|
||||
},
|
||||
{
|
||||
fieldName: 'email',
|
||||
component: 'Input',
|
||||
label: $t('sysAccount.email'),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
await getInfo();
|
||||
});
|
||||
async function getInfo() {
|
||||
loading.value = true;
|
||||
const data = await fetchUserInfo();
|
||||
await profileBaseSettingRef.value.getFormApi().setValues(data);
|
||||
loading.value = false;
|
||||
}
|
||||
const loading = ref(false);
|
||||
const updateLoading = ref(false);
|
||||
function handleSubmit(values: any) {
|
||||
updateLoading.value = true;
|
||||
api.post('/api/v1/sysAccount/updateProfile', values).then((res) => {
|
||||
updateLoading.value = false;
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success($t('message.success'));
|
||||
getInfo();
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<ProfileBaseSetting
|
||||
v-loading="loading"
|
||||
:button-loading="updateLoading"
|
||||
ref="profileBaseSettingRef"
|
||||
:form-schema="formSchema"
|
||||
:button-text="$t('button.update')"
|
||||
@submit="handleSubmit"
|
||||
/>
|
||||
</template>
|
||||
51
easyflow-ui-admin/app/src/views/_core/profile/index.vue
Normal file
51
easyflow-ui-admin/app/src/views/_core/profile/index.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { Profile } from '@easyflow/common-ui';
|
||||
import { useUserStore } from '@easyflow/stores';
|
||||
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import ProfileBase from './base-setting.vue';
|
||||
import ProfileNotificationSetting from './notification-setting.vue';
|
||||
import ProfilePasswordSetting from './password-setting.vue';
|
||||
import ProfileSecuritySetting from './security-setting.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const userStore = useUserStore();
|
||||
|
||||
const tabsValue = ref<string>('basic');
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
label: $t('settingsConfig.basic'),
|
||||
value: 'basic',
|
||||
},
|
||||
{
|
||||
label: $t('settingsConfig.updatePwd'),
|
||||
value: 'password',
|
||||
},
|
||||
];
|
||||
|
||||
onMounted(() => {
|
||||
if (route.query.tab) {
|
||||
tabsValue.value = route.query.tab as string;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Profile
|
||||
v-model:model-value="tabsValue"
|
||||
:title="$t('page.auth.profile')"
|
||||
:user-info="userStore.userInfo"
|
||||
:tabs="tabs"
|
||||
>
|
||||
<template #content>
|
||||
<ProfileBase v-if="tabsValue === 'basic'" />
|
||||
<ProfileSecuritySetting v-if="tabsValue === 'security'" />
|
||||
<ProfilePasswordSetting v-if="tabsValue === 'password'" />
|
||||
<ProfileNotificationSetting v-if="tabsValue === 'notice'" />
|
||||
</template>
|
||||
</Profile>
|
||||
</template>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { ProfileNotificationSetting } from '@easyflow/common-ui';
|
||||
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const formSchema = computed(() => {
|
||||
return [
|
||||
{
|
||||
value: true,
|
||||
fieldName: 'accountPassword',
|
||||
label: $t('page.auth.accountPassword'),
|
||||
description: $t('page.description.accountPassword'),
|
||||
},
|
||||
{
|
||||
value: true,
|
||||
fieldName: 'systemMessage',
|
||||
label: $t('page.auth.systemMessage'),
|
||||
description: $t('page.description.systemMessage'),
|
||||
},
|
||||
{
|
||||
value: true,
|
||||
fieldName: 'todoTask',
|
||||
label: $t('page.auth.todoTasks'),
|
||||
description: $t('page.description.todoTasks'),
|
||||
},
|
||||
];
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<ProfileNotificationSetting :form-schema="formSchema" />
|
||||
</template>
|
||||
@@ -0,0 +1,78 @@
|
||||
<script setup lang="ts">
|
||||
import type { EasyFlowFormSchema } from '#/adapter/form';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { ProfilePasswordSetting, z } from '@easyflow/common-ui';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const profilePasswordSettingRef = ref();
|
||||
|
||||
const formSchema = computed((): EasyFlowFormSchema[] => {
|
||||
return [
|
||||
{
|
||||
fieldName: 'password',
|
||||
label: $t('sysAccount.oldPwd'),
|
||||
component: 'EasyFlowInputPassword',
|
||||
componentProps: {
|
||||
placeholder: $t('sysAccount.oldPwd') + $t('common.isRequired'),
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'newPassword',
|
||||
label: $t('sysAccount.newPwd'),
|
||||
component: 'EasyFlowInputPassword',
|
||||
componentProps: {
|
||||
passwordStrength: true,
|
||||
placeholder: $t('sysAccount.newPwd') + $t('common.isRequired'),
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'confirmPassword',
|
||||
label: $t('sysAccount.confirmPwd'),
|
||||
component: 'EasyFlowInputPassword',
|
||||
componentProps: {
|
||||
passwordStrength: true,
|
||||
placeholder: $t('sysAccount.repeatPwd'),
|
||||
},
|
||||
dependencies: {
|
||||
rules(values) {
|
||||
const { newPassword } = values;
|
||||
return z
|
||||
.string({ required_error: $t('sysAccount.repeatPwd') })
|
||||
.min(1, { message: $t('sysAccount.repeatPwd') })
|
||||
.refine((value) => value === newPassword, {
|
||||
message: $t('sysAccount.notSamePwd'),
|
||||
});
|
||||
},
|
||||
triggerFields: ['newPassword'],
|
||||
},
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const updateLoading = ref(false);
|
||||
function handleSubmit(values: any) {
|
||||
updateLoading.value = true;
|
||||
api.post('/api/v1/sysAccount/updatePassword', values).then((res) => {
|
||||
updateLoading.value = false;
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success($t('message.success'));
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<ProfilePasswordSetting
|
||||
:button-loading="updateLoading"
|
||||
:button-text="$t('button.update')"
|
||||
ref="profilePasswordSettingRef"
|
||||
class="w-1/3"
|
||||
:form-schema="formSchema"
|
||||
@submit="handleSubmit"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { ProfileSecuritySetting } from '@easyflow/common-ui';
|
||||
|
||||
const formSchema = computed(() => {
|
||||
return [
|
||||
{
|
||||
value: true,
|
||||
fieldName: 'accountPassword',
|
||||
label: '账户密码',
|
||||
description: '当前密码强度:强',
|
||||
},
|
||||
{
|
||||
value: true,
|
||||
fieldName: 'securityPhone',
|
||||
label: '密保手机',
|
||||
description: '已绑定手机:138****8293',
|
||||
},
|
||||
{
|
||||
value: true,
|
||||
fieldName: 'securityQuestion',
|
||||
label: '密保问题',
|
||||
description: '未设置密保问题,密保问题可有效保护账户安全',
|
||||
},
|
||||
{
|
||||
value: true,
|
||||
fieldName: 'securityEmail',
|
||||
label: '备用邮箱',
|
||||
description: '已绑定邮箱:ant***sign.com',
|
||||
},
|
||||
{
|
||||
value: false,
|
||||
fieldName: 'securityMfa',
|
||||
label: 'MFA 设备',
|
||||
description: '未绑定 MFA 设备,绑定后,可以进行二次确认',
|
||||
},
|
||||
];
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<ProfileSecuritySetting :form-schema="formSchema" />
|
||||
</template>
|
||||
344
easyflow-ui-admin/app/src/views/ai/bots/index.vue
Normal file
344
easyflow-ui-admin/app/src/views/ai/bots/index.vue
Normal file
@@ -0,0 +1,344 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from 'element-plus';
|
||||
|
||||
import type { BotInfo } from '@easyflow/types';
|
||||
|
||||
import type { ActionButton } from '#/components/page/CardList.vue';
|
||||
|
||||
import { computed, markRaw, onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { $t } from '@easyflow/locales';
|
||||
|
||||
import { Delete, Edit, Plus, Setting } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElButton,
|
||||
ElDialog,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElInputNumber,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
} from 'element-plus';
|
||||
import { tryit } from 'radash';
|
||||
|
||||
import { removeBotFromId } from '#/api';
|
||||
import { api } from '#/api/request';
|
||||
import defaultAvatar from '#/assets/ai/bot/defaultBotAvatar.png';
|
||||
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
||||
import CardList from '#/components/page/CardList.vue';
|
||||
import PageData from '#/components/page/PageData.vue';
|
||||
import PageSide from '#/components/page/PageSide.vue';
|
||||
import { useDictStore } from '#/store';
|
||||
|
||||
import Modal from './modal.vue';
|
||||
|
||||
interface FieldDefinition {
|
||||
// 字段名称
|
||||
prop: string;
|
||||
// 字段标签
|
||||
label: string;
|
||||
// 字段类型:input, number, select, radio, checkbox, switch, date, datetime
|
||||
type?: 'input' | 'number';
|
||||
// 是否必填
|
||||
required?: boolean;
|
||||
// 占位符
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initDict();
|
||||
getSideList();
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const pageDataRef = ref();
|
||||
const modalRef = ref<InstanceType<typeof Modal>>();
|
||||
const dictStore = useDictStore();
|
||||
|
||||
// 操作按钮配置
|
||||
const headerButtons = [
|
||||
{
|
||||
key: 'create',
|
||||
text: `${$t('button.create')}${$t('bot.chatAssistant')}`,
|
||||
icon: markRaw(Plus),
|
||||
type: 'primary',
|
||||
data: { action: 'create' },
|
||||
permission: '/api/v1/documentCollection/save',
|
||||
},
|
||||
];
|
||||
const actions: ActionButton[] = [
|
||||
{
|
||||
icon: Edit,
|
||||
text: $t('button.edit'),
|
||||
className: '',
|
||||
permission: '',
|
||||
onClick(row: BotInfo) {
|
||||
modalRef.value?.open('edit', row);
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: Setting,
|
||||
text: $t('button.setting'),
|
||||
className: '',
|
||||
permission: '',
|
||||
onClick(row: BotInfo) {
|
||||
router.push({ path: `/ai/bots/setting/${row.id}` });
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: Delete,
|
||||
text: $t('button.delete'),
|
||||
className: 'item-danger',
|
||||
permission: '/api/v1/bot/remove',
|
||||
onClick(row: BotInfo) {
|
||||
removeBot(row);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const removeBot = async (bot: BotInfo) => {
|
||||
const [action] = await tryit(ElMessageBox.confirm)(
|
||||
$t('message.deleteAlert'),
|
||||
$t('message.noticeTitle'),
|
||||
{
|
||||
confirmButtonText: $t('message.ok'),
|
||||
cancelButtonText: $t('message.cancel'),
|
||||
type: 'warning',
|
||||
},
|
||||
);
|
||||
|
||||
if (!action) {
|
||||
const [err, res] = await tryit(removeBotFromId)(bot.id);
|
||||
|
||||
if (!err && res.errorCode === 0) {
|
||||
ElMessage.success($t('message.deleteOkMessage'));
|
||||
pageDataRef.value.setQuery({});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = (params: string) => {
|
||||
pageDataRef.value.setQuery({ title: params, isQueryOr: true });
|
||||
};
|
||||
const handleButtonClick = () => {
|
||||
modalRef.value?.open('create');
|
||||
};
|
||||
|
||||
const fieldDefinitions = ref<FieldDefinition[]>([
|
||||
{
|
||||
prop: 'categoryName',
|
||||
label: $t('aiWorkflowCategory.categoryName'),
|
||||
type: 'input',
|
||||
required: true,
|
||||
placeholder: $t('aiWorkflowCategory.categoryName'),
|
||||
},
|
||||
{
|
||||
prop: 'sortNo',
|
||||
label: $t('aiWorkflowCategory.sortNo'),
|
||||
type: 'number',
|
||||
required: false,
|
||||
placeholder: $t('aiWorkflowCategory.sortNo'),
|
||||
},
|
||||
]);
|
||||
|
||||
const formData = ref<any>({});
|
||||
const dialogVisible = ref(false);
|
||||
const formRef = ref<FormInstance>();
|
||||
const saveLoading = ref(false);
|
||||
const sideList = ref<any[]>([]);
|
||||
const controlBtns = [
|
||||
{
|
||||
icon: Edit,
|
||||
label: $t('button.edit'),
|
||||
onClick(row: any) {
|
||||
showControlDialog(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'danger',
|
||||
icon: Delete,
|
||||
label: $t('button.delete'),
|
||||
onClick(row: any) {
|
||||
removeCategory(row);
|
||||
},
|
||||
},
|
||||
];
|
||||
const footerButton = {
|
||||
icon: Plus,
|
||||
label: $t('button.add'),
|
||||
onClick() {
|
||||
showControlDialog({});
|
||||
},
|
||||
};
|
||||
|
||||
const formRules = computed(() => {
|
||||
const rules: Record<string, any[]> = {};
|
||||
fieldDefinitions.value.forEach((field) => {
|
||||
const fieldRules = [];
|
||||
if (field.required) {
|
||||
fieldRules.push({
|
||||
required: true,
|
||||
message: `${$t('message.required')}`,
|
||||
trigger: 'blur',
|
||||
});
|
||||
}
|
||||
if (fieldRules.length > 0) {
|
||||
rules[field.prop] = fieldRules;
|
||||
}
|
||||
});
|
||||
return rules;
|
||||
});
|
||||
function initDict() {
|
||||
dictStore.fetchDictionary('dataStatus');
|
||||
}
|
||||
function changeCategory(category: any) {
|
||||
pageDataRef.value.setQuery({ categoryId: category.id });
|
||||
}
|
||||
function showControlDialog(item: any) {
|
||||
formRef.value?.resetFields();
|
||||
formData.value = { ...item };
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
function removeCategory(row: any) {
|
||||
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
|
||||
confirmButtonText: $t('message.ok'),
|
||||
cancelButtonText: $t('message.cancel'),
|
||||
type: 'warning',
|
||||
beforeClose: (action, instance, done) => {
|
||||
if (action === 'confirm') {
|
||||
instance.confirmButtonLoading = true;
|
||||
api
|
||||
.post('/api/v1/botCategory/remove', { id: row.id })
|
||||
.then((res) => {
|
||||
instance.confirmButtonLoading = false;
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message);
|
||||
done();
|
||||
getSideList();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
instance.confirmButtonLoading = false;
|
||||
});
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
},
|
||||
}).catch(() => {});
|
||||
}
|
||||
function handleSubmit() {
|
||||
formRef.value?.validate((valid) => {
|
||||
if (valid) {
|
||||
saveLoading.value = true;
|
||||
const url = formData.value.id
|
||||
? '/api/v1/botCategory/update'
|
||||
: '/api/v1/botCategory/save';
|
||||
api.post(url, formData.value).then((res) => {
|
||||
saveLoading.value = false;
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message);
|
||||
dialogVisible.value = false;
|
||||
getSideList();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
const getSideList = async () => {
|
||||
const [, res] = await tryit(api.get)('/api/v1/botCategory/list', {
|
||||
params: { sortKey: 'sortNo', sortType: 'asc' },
|
||||
});
|
||||
|
||||
if (res && res.errorCode === 0) {
|
||||
sideList.value = [
|
||||
{
|
||||
id: '',
|
||||
categoryName: $t('common.allCategories'),
|
||||
},
|
||||
...res.data,
|
||||
];
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full flex-col gap-6 p-6">
|
||||
<HeaderSearch
|
||||
:buttons="headerButtons"
|
||||
@search="handleSearch"
|
||||
@button-click="handleButtonClick"
|
||||
/>
|
||||
<div class="flex flex-1 gap-6">
|
||||
<PageSide
|
||||
label-key="categoryName"
|
||||
value-key="id"
|
||||
:menus="sideList"
|
||||
:control-btns="controlBtns"
|
||||
:footer-button="footerButton"
|
||||
@change="changeCategory"
|
||||
/>
|
||||
<div class="h-[calc(100vh-192px)] flex-1 overflow-auto">
|
||||
<PageData
|
||||
ref="pageDataRef"
|
||||
page-url="/api/v1/bot/page"
|
||||
:page-sizes="[12, 18, 24]"
|
||||
:page-size="12"
|
||||
>
|
||||
<template #default="{ pageList }">
|
||||
<CardList
|
||||
:default-icon="defaultAvatar"
|
||||
:data="pageList"
|
||||
:actions="actions"
|
||||
/>
|
||||
</template>
|
||||
</PageData>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 创建&编辑Bot弹窗 -->
|
||||
<Modal ref="modalRef" @success="pageDataRef.setQuery({})" />
|
||||
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="formData.id ? `${$t('button.edit')}` : `${$t('button.add')}`"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<ElForm
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<!-- 动态生成表单项 -->
|
||||
<ElFormItem
|
||||
v-for="field in fieldDefinitions"
|
||||
:key="field.prop"
|
||||
:label="field.label"
|
||||
:prop="field.prop"
|
||||
>
|
||||
<ElInput
|
||||
v-if="!field.type || field.type === 'input'"
|
||||
v-model="formData[field.prop]"
|
||||
:placeholder="field.placeholder"
|
||||
/>
|
||||
<ElInputNumber
|
||||
v-else-if="field.type === 'number'"
|
||||
v-model="formData[field.prop]"
|
||||
:placeholder="field.placeholder"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<ElButton @click="dialogVisible = false">
|
||||
{{ $t('button.cancel') }}
|
||||
</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit" :loading="saveLoading">
|
||||
{{ $t('button.confirm') }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
120
easyflow-ui-admin/app/src/views/ai/bots/modal.vue
Normal file
120
easyflow-ui-admin/app/src/views/ai/bots/modal.vue
Normal file
@@ -0,0 +1,120 @@
|
||||
<script setup lang="ts">
|
||||
import type { BotInfo } from '@easyflow/types';
|
||||
|
||||
import type { SaveBotParams, UpdateBotParams } from '#/api/ai/bot';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { $t } from '@easyflow/locales';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElDialog,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
} from 'element-plus';
|
||||
import { tryit } from 'radash';
|
||||
|
||||
import { saveBot, updateBotApi } from '#/api/ai/bot';
|
||||
import DictSelect from '#/components/dict/DictSelect.vue';
|
||||
import UploadAvatar from '#/components/upload/UploadAvatar.vue';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
const initialFormData = {
|
||||
icon: '',
|
||||
title: '',
|
||||
alias: '',
|
||||
description: '',
|
||||
categoryId: '',
|
||||
status: 1,
|
||||
};
|
||||
const dialogVisible = ref(false);
|
||||
const dialogType = ref<'create' | 'edit'>('create');
|
||||
const formRef = ref<InstanceType<typeof ElForm>>();
|
||||
const formData = ref<SaveBotParams | UpdateBotParams>(initialFormData);
|
||||
const rules = {
|
||||
title: [{ required: true, message: $t('message.required'), trigger: 'blur' }],
|
||||
alias: [{ required: true, message: $t('message.required'), trigger: 'blur' }],
|
||||
};
|
||||
const loading = ref(false);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
loading.value = true;
|
||||
|
||||
const [err, res] = await tryit(
|
||||
dialogType.value === 'create' ? saveBot : updateBotApi,
|
||||
)(formData.value as any);
|
||||
|
||||
if (!err && res.errorCode === 0) {
|
||||
emit('success');
|
||||
ElMessage.success($t('message.saveOkMessage'));
|
||||
dialogVisible.value = false;
|
||||
}
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
open(type: typeof dialogType.value, bot?: BotInfo) {
|
||||
formData.value = bot
|
||||
? {
|
||||
id: bot.id,
|
||||
icon: bot.icon,
|
||||
title: bot.title,
|
||||
alias: bot.alias,
|
||||
description: bot.description,
|
||||
categoryId: bot.categoryId,
|
||||
status: bot.status,
|
||||
}
|
||||
: initialFormData;
|
||||
dialogType.value = type;
|
||||
dialogVisible.value = true;
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="`${$t(`button.${dialogType}`)}${$t('bot.chatAssistant')}`"
|
||||
draggable
|
||||
align-center
|
||||
>
|
||||
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="150px">
|
||||
<ElFormItem :label="$t('common.avatar')" prop="icon">
|
||||
<UploadAvatar v-model="formData.icon" />
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="categoryId" :label="$t('aiWorkflow.categoryId')">
|
||||
<DictSelect v-model="formData.categoryId" dict-code="aiBotCategory" />
|
||||
</ElFormItem>
|
||||
<ElFormItem :label="$t('aiWorkflow.title')" prop="title">
|
||||
<ElInput v-model="formData.title" />
|
||||
</ElFormItem>
|
||||
<ElFormItem :label="$t('plugin.alias')" prop="alias">
|
||||
<ElInput v-model="formData.alias" />
|
||||
</ElFormItem>
|
||||
<ElFormItem :label="$t('plugin.description')" prop="description">
|
||||
<ElInput type="textarea" :rows="3" v-model="formData.description" />
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="status" :label="$t('aiWorkflow.status')">
|
||||
<DictSelect v-model="formData.status" dict-code="showOrNot" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<ElButton @click="dialogVisible = false">
|
||||
{{ $t('button.cancel') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
:loading="loading"
|
||||
:disabled="loading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ $t('button.save') }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
||||
159
easyflow-ui-admin/app/src/views/ai/bots/pages/Run.vue
Normal file
159
easyflow-ui-admin/app/src/views/ai/bots/pages/Run.vue
Normal file
@@ -0,0 +1,159 @@
|
||||
<script setup lang="ts">
|
||||
import type { BotInfo, Session } from '@easyflow/types';
|
||||
|
||||
import { onMounted, ref, watchEffect } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { IconifyIcon } from '@easyflow/icons';
|
||||
import { preferences } from '@easyflow/preferences';
|
||||
import { uuid } from '@easyflow/utils';
|
||||
|
||||
import {
|
||||
ElAside,
|
||||
ElButton,
|
||||
ElContainer,
|
||||
ElEmpty,
|
||||
ElMain,
|
||||
ElSpace,
|
||||
} from 'element-plus';
|
||||
import { tryit } from 'radash';
|
||||
|
||||
import { getBotDetails, getSessionList } from '#/api';
|
||||
import BotAvatar from '#/components/botAvatar/botAvatar.vue';
|
||||
import Chat from '#/components/chat/chat.vue';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const bot = ref<BotInfo>();
|
||||
const sessionList = ref<Session[]>([]);
|
||||
const sessionId = ref<string>(route.params.sessionId as string);
|
||||
|
||||
watchEffect(() => {
|
||||
sessionId.value = route.params.sessionId as string;
|
||||
});
|
||||
|
||||
// 内置菜单点击方法
|
||||
// function handleMenuCommand(
|
||||
// command: ConversationMenuCommand,
|
||||
// item: ConversationItem,
|
||||
// ) {
|
||||
// console.warn('内置菜单点击事件:', command, item);
|
||||
// // 直接修改 item 是否生效
|
||||
// if (command === 'delete') {
|
||||
// const index = menuTestItems.value.findIndex(
|
||||
// (itemSlef) => itemSlef.key === item.key,
|
||||
// );
|
||||
|
||||
// if (index !== -1) {
|
||||
// menuTestItems.value.splice(index, 1);
|
||||
// console.warn('删除成功');
|
||||
// ElMessage.success('删除成功');
|
||||
// }
|
||||
// }
|
||||
// if (command === 'rename') {
|
||||
// item.label = '已修改';
|
||||
// console.warn('重命名成功');
|
||||
// ElMessage.success('重命名成功');
|
||||
// }
|
||||
// }
|
||||
|
||||
onMounted(() => {
|
||||
if (route.params.botId) {
|
||||
fetchBotDetail(route.params.botId as string);
|
||||
fetchSessionList(route.params.botId as string);
|
||||
}
|
||||
});
|
||||
|
||||
const fetchBotDetail = async (id: string) => {
|
||||
const [, res] = await tryit(getBotDetails)(id);
|
||||
|
||||
if (res?.errorCode === 0) {
|
||||
bot.value = res.data;
|
||||
}
|
||||
};
|
||||
const fetchSessionList = async (id: string) => {
|
||||
const [, res] = await tryit(getSessionList)({
|
||||
botId: id,
|
||||
tempUserId: uuid().toString() + id,
|
||||
});
|
||||
|
||||
if (res?.errorCode === 0) {
|
||||
sessionList.value = res.data.cons;
|
||||
}
|
||||
};
|
||||
|
||||
const updateActive = (_sessionId?: number | string) => {
|
||||
sessionId.value = `${_sessionId ?? ''}`;
|
||||
router.push(
|
||||
`/ai/bots/run/${bot.value?.id}${_sessionId ? `/${_sessionId}` : ''}`,
|
||||
);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElContainer class="h-screen" v-if="bot">
|
||||
<ElAside width="240px" class="flex flex-col items-center bg-[#f5f5f580]">
|
||||
<ElSpace class="py-7">
|
||||
<BotAvatar :src="bot.icon" :size="40" />
|
||||
<span class="text-base font-medium text-black/85">{{ bot.title }}</span>
|
||||
</ElSpace>
|
||||
<ElButton
|
||||
type="primary"
|
||||
class="!h-10 w-full max-w-[208px]"
|
||||
plain
|
||||
@click="updateActive()"
|
||||
>
|
||||
<template #icon>
|
||||
<IconifyIcon icon="mdi:chat-outline" />
|
||||
</template>
|
||||
{{ $t('button.newConversation') }}
|
||||
</ElButton>
|
||||
<span class="self-start p-6 pb-2 text-sm text-[#969799]">{{
|
||||
$t('common.history')
|
||||
}}</span>
|
||||
<div class="w-full max-w-[208px] flex-1 overflow-hidden">
|
||||
<ElConversations
|
||||
v-show="sessionList.length > 0"
|
||||
class="!w-full !shadow-none"
|
||||
v-model:active="sessionId"
|
||||
:items="sessionList"
|
||||
:label-max-width="120"
|
||||
:show-tooltip="true"
|
||||
row-key="sessionId"
|
||||
label-key="title"
|
||||
tooltip-placement="right"
|
||||
:tooltip-offset="35"
|
||||
show-to-top-btn
|
||||
show-built-in-menu
|
||||
show-built-in-menu-type="hover"
|
||||
@update:active="updateActive"
|
||||
/>
|
||||
<ElEmpty
|
||||
:image="`/empty${preferences.theme.mode === 'dark' ? '-dark' : ''}.png`"
|
||||
v-show="sessionList.length === 0"
|
||||
/>
|
||||
</div>
|
||||
</ElAside>
|
||||
<ElMain>
|
||||
<Chat :session-id="sessionId" :bot="bot" />
|
||||
</ElMain>
|
||||
</ElContainer>
|
||||
</template>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.conversations-container :deep(.conversations-list) {
|
||||
width: 100% !important;
|
||||
padding: 0 !important;
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
.conversations-container :deep(.conversation-item) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.conversations-container :deep(.conversation-label) {
|
||||
color: #1a1a1a;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,91 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { $t } from '@easyflow/locales';
|
||||
|
||||
import { ElButton, ElDialog, ElForm, ElFormItem, ElInput } from 'element-plus';
|
||||
|
||||
import { sseClient } from '#/api/request';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
const dialogVisible = ref(false);
|
||||
const formRef = ref<InstanceType<typeof ElForm>>();
|
||||
const formData = ref();
|
||||
const rules = {
|
||||
title: [{ required: true, message: $t('message.required'), trigger: 'blur' }],
|
||||
};
|
||||
const loading = ref(false);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
loading.value = true;
|
||||
const data = {
|
||||
botId: formData.value.botId,
|
||||
prompt: formData.value.prompt,
|
||||
};
|
||||
formData.value.prompt = '';
|
||||
sseClient.post('/api/v1/bot/prompt/chore/chat', data, {
|
||||
onMessage(message) {
|
||||
const event = message.event;
|
||||
// done
|
||||
if (event === 'done') {
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
if (!message.data) {
|
||||
return;
|
||||
}
|
||||
const sseData = JSON.parse(message.data);
|
||||
const delta = sseData.payload?.delta;
|
||||
formData.value.prompt += delta;
|
||||
},
|
||||
});
|
||||
};
|
||||
const handleReplace = () => {
|
||||
emit('success', formData.value.prompt);
|
||||
dialogVisible.value = false;
|
||||
};
|
||||
defineExpose({
|
||||
open(botId: string, systemPrompt: string) {
|
||||
formData.value = {
|
||||
botId,
|
||||
prompt: systemPrompt,
|
||||
};
|
||||
dialogVisible.value = true;
|
||||
handleSubmit();
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="$t('bot.aiOptimizedPrompts')"
|
||||
draggable
|
||||
align-center
|
||||
width="550px"
|
||||
>
|
||||
<ElForm ref="formRef" :model="formData" :rules="rules">
|
||||
<ElFormItem prop="prompt">
|
||||
<ElInput type="textarea" :rows="20" v-model="formData.prompt" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<ElButton @click="dialogVisible = false">
|
||||
{{ $t('button.cancel') }}
|
||||
</ElButton>
|
||||
<ElButton type="primary" @click="handleReplace" v-if="!loading">
|
||||
{{ $t('button.replace') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
:loading="loading"
|
||||
:disabled="loading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ loading ? $t('button.optimizing') : $t('button.regenerate') }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
||||
1056
easyflow-ui-admin/app/src/views/ai/bots/pages/setting/config.vue
Normal file
1056
easyflow-ui-admin/app/src/views/ai/bots/pages/setting/config.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,66 @@
|
||||
<script setup lang="ts">
|
||||
import type { BotInfo } from '@easyflow/types';
|
||||
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { tryit } from 'radash';
|
||||
|
||||
import { getBotDetails } from '#/api';
|
||||
import { hasPermission } from '#/api/common/hasPermission';
|
||||
|
||||
import Config from './config.vue';
|
||||
import Preview from './preview.vue';
|
||||
import Prompt from './prompt.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const hasSavePermission = computed(() =>
|
||||
hasPermission(['/api/v1/bot/save', '/api/v1/bot/updateLlmId']),
|
||||
);
|
||||
const bot = ref<BotInfo>();
|
||||
|
||||
onMounted(() => {
|
||||
if (route.params.id) {
|
||||
fetchBotDetail(route.params.id as string);
|
||||
}
|
||||
});
|
||||
|
||||
const fetchBotDetail = async (id: string) => {
|
||||
const [, res] = await tryit(getBotDetails)(id);
|
||||
|
||||
if (res?.errorCode === 0) {
|
||||
bot.value = res.data;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="settings-container">
|
||||
<div class="row-container">
|
||||
<div class="row-item">
|
||||
<Prompt :bot="bot" :has-save-permission="hasSavePermission" />
|
||||
</div>
|
||||
<div class="row-item">
|
||||
<Config :bot="bot" :has-save-permission="hasSavePermission" />
|
||||
</div>
|
||||
<div class="row-item">
|
||||
<Preview :bot="bot" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped>
|
||||
.settings-container {
|
||||
height: calc(100vh - 90px);
|
||||
padding: 20px;
|
||||
}
|
||||
.row-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
.row-item {
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Brush } from '@element-plus/icons-vue';
|
||||
import { ElButton, ElIcon } from 'element-plus';
|
||||
|
||||
import Chat from '#/components/chat/chat.vue';
|
||||
|
||||
const chatRef = ref();
|
||||
|
||||
const handleClear = () => {
|
||||
chatRef.value.clear();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="bg-background dark:border-border flex h-full flex-col gap-3 rounded-lg p-3 dark:border"
|
||||
>
|
||||
<div class="flex justify-between">
|
||||
<h1 class="text-base font-medium">
|
||||
{{ $t('button.preview') }}
|
||||
</h1>
|
||||
<ElButton text @click="handleClear">
|
||||
<ElIcon class="rotate-180"><Brush /></ElIcon>
|
||||
</ElButton>
|
||||
</div>
|
||||
<div class="relative flex-1">
|
||||
<Chat
|
||||
ref="chatRef"
|
||||
class="absolute inset-0"
|
||||
:ishow-chat-conversations="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
102
easyflow-ui-admin/app/src/views/ai/bots/pages/setting/prompt.vue
Normal file
102
easyflow-ui-admin/app/src/views/ai/bots/pages/setting/prompt.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<script setup lang="ts">
|
||||
import type { BotInfo } from '@easyflow/types';
|
||||
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import { useDebounceFn } from '@vueuse/core';
|
||||
import { ElIcon, ElInput } from 'element-plus';
|
||||
|
||||
import { updateLlmOptions } from '#/api';
|
||||
import MagicStaffIcon from '#/components/icons/MagicStaffIcon.vue';
|
||||
import { $t } from '#/locales';
|
||||
import PromptChoreChatModal from '#/views/ai/bots/pages/setting/PromptChoreChatModal.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
bot?: BotInfo;
|
||||
hasSavePermission?: boolean;
|
||||
}>();
|
||||
const systemPrompt = ref($t('bot.placeholder.prompt'));
|
||||
const promptChoreChatModalRef = ref();
|
||||
watch(
|
||||
() => props.bot?.modelOptions.systemPrompt,
|
||||
(newPrompt) => {
|
||||
if (newPrompt) {
|
||||
systemPrompt.value = newPrompt;
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const handleInput = useDebounceFn((value: string) => {
|
||||
updateLlmOptions({
|
||||
id: props.bot?.id || '',
|
||||
llmOptions: {
|
||||
systemPrompt: value,
|
||||
},
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
const handelReplacePrompt = (value: string) => {
|
||||
systemPrompt.value = value;
|
||||
handleInput(value);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="bg-background dark:border-border flex h-full flex-col gap-2 rounded-lg p-3 dark:border"
|
||||
>
|
||||
<div class="flex justify-between">
|
||||
<h1 class="text-base font-medium">
|
||||
{{ $t('bot.systemPrompt') }}
|
||||
</h1>
|
||||
<button
|
||||
@click="promptChoreChatModalRef.open(props.bot?.id, systemPrompt)"
|
||||
type="button"
|
||||
class="flex items-center gap-0.5 rounded-lg bg-[#f7f7f7] px-3 py-1"
|
||||
>
|
||||
<ElIcon size="16"><MagicStaffIcon /></ElIcon>
|
||||
<span
|
||||
class="bg-[linear-gradient(106.75666073298856deg,#F17E47,#D85ABF,#717AFF)] bg-clip-text text-sm text-transparent"
|
||||
>
|
||||
{{ $t('bot.aiOptimization') }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<ElInput
|
||||
class="flex-1"
|
||||
type="textarea"
|
||||
resize="none"
|
||||
v-model="systemPrompt"
|
||||
:title="!hasSavePermission ? $t('bot.placeholder.permission') : ''"
|
||||
:disabled="!hasSavePermission"
|
||||
@input="handleInput"
|
||||
/>
|
||||
|
||||
<!--系统提示词优化模态框-->
|
||||
<PromptChoreChatModal
|
||||
ref="promptChoreChatModalRef"
|
||||
@success="handelReplacePrompt"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.el-textarea :deep(.el-textarea__inner) {
|
||||
--el-input-bg-color: #f7f7f7;
|
||||
|
||||
height: 100%;
|
||||
padding: 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 1.25;
|
||||
border-radius: 8px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.dark .el-textarea :deep(.el-textarea__inner) {
|
||||
--el-input-bg-color: hsl(var(--background-deep));
|
||||
|
||||
border: 1px solid hsl(var(--border));
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,159 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { $t } from '@easyflow/locales';
|
||||
|
||||
import { Delete, MoreFilled } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElButton,
|
||||
ElDialog,
|
||||
ElDropdown,
|
||||
ElDropdownItem,
|
||||
ElDropdownMenu,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import PageData from '#/components/page/PageData.vue';
|
||||
|
||||
const props = defineProps({
|
||||
documentId: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
const dialogVisible = ref(false);
|
||||
const pageDataRef = ref();
|
||||
const handleEdit = (row: any) => {
|
||||
form.value = { id: row.id, content: row.content };
|
||||
openDialog();
|
||||
};
|
||||
const handleDelete = (row: any) => {
|
||||
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
|
||||
confirmButtonText: $t('message.ok'),
|
||||
cancelButtonText: $t('message.cancel'),
|
||||
type: 'warning',
|
||||
})
|
||||
.then(() => {
|
||||
btnLoading.value = true;
|
||||
api
|
||||
.post('/api/v1/documentChunk/removeChunk', { id: row.id })
|
||||
.then((res: any) => {
|
||||
btnLoading.value = false;
|
||||
if (res.errorCode !== 0) {
|
||||
ElMessage.error(res.message);
|
||||
return;
|
||||
}
|
||||
ElMessage.success($t('message.deleteOkMessage'));
|
||||
pageDataRef.value.setQuery(queryParams);
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
const openDialog = () => {
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
const closeDialog = () => {
|
||||
dialogVisible.value = false;
|
||||
};
|
||||
const queryParams = ref({
|
||||
documentId: props.documentId,
|
||||
sortKey: 'sorting',
|
||||
sortType: 'asc',
|
||||
});
|
||||
const save = () => {
|
||||
btnLoading.value = true;
|
||||
api.post('/api/v1/documentChunk/update', form.value).then((res: any) => {
|
||||
btnLoading.value = false;
|
||||
if (res.errorCode !== 0) {
|
||||
ElMessage.error(res.message);
|
||||
return;
|
||||
}
|
||||
ElMessage.success($t('message.updateOkMessage'));
|
||||
pageDataRef.value.setQuery(queryParams);
|
||||
closeDialog();
|
||||
});
|
||||
};
|
||||
const btnLoading = ref(false);
|
||||
const basicFormRef = ref();
|
||||
const form = ref({
|
||||
id: '',
|
||||
content: '',
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<PageData
|
||||
page-url="/api/v1/documentChunk/page"
|
||||
ref="pageDataRef"
|
||||
:page-size="10"
|
||||
:extra-query-params="queryParams"
|
||||
>
|
||||
<template #default="{ pageList }">
|
||||
<ElTable :data="pageList" style="width: 100%" size="large">
|
||||
<ElTableColumn
|
||||
prop="content"
|
||||
:label="$t('documentCollection.content')"
|
||||
min-width="240"
|
||||
/>
|
||||
<ElTableColumn :label="$t('common.handle')" width="100" align="right">
|
||||
<template #default="{ row }">
|
||||
<div class="flex items-center gap-3">
|
||||
<ElButton link type="primary" @click="handleEdit(row)">
|
||||
{{ $t('button.edit') }}
|
||||
</ElButton>
|
||||
|
||||
<ElDropdown>
|
||||
<ElButton link :icon="MoreFilled" />
|
||||
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownItem @click="handleDelete(row)">
|
||||
<ElButton link type="danger" :icon="Delete">
|
||||
{{ $t('button.delete') }}
|
||||
</ElButton>
|
||||
</ElDropdownItem>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</template>
|
||||
</PageData>
|
||||
<ElDialog v-model="dialogVisible" :title="$t('button.edit')" width="50%">
|
||||
<ElForm
|
||||
ref="basicFormRef"
|
||||
style="width: 100%; margin-top: 20px"
|
||||
:model="form"
|
||||
>
|
||||
<ElFormItem>
|
||||
<ElInput v-model="form.content" :rows="20" type="textarea" />
|
||||
</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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,99 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { $t } from '@easyflow/locales';
|
||||
|
||||
import { ElTable, ElTableColumn, ElTag } from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
|
||||
const props = defineProps({
|
||||
filesList: {
|
||||
default: () => [],
|
||||
type: Array<any>,
|
||||
},
|
||||
splitterParams: {
|
||||
default: () => {},
|
||||
type: Object,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['loadingFinish']);
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const knowledgeIdRef = ref<string>((route.query.id as string) || '');
|
||||
const localFilesList = ref<any[]>([]);
|
||||
watch(
|
||||
() => props.filesList,
|
||||
(newVal) => {
|
||||
localFilesList.value = [...newVal];
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
defineExpose({
|
||||
handleSave() {
|
||||
localFilesList.value.forEach((file, index) => {
|
||||
localFilesList.value[index].progressUpload = 'loading';
|
||||
saveDoc(file.filePath, 'saveText', file.fileName, index);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
function saveDoc(
|
||||
filePath: string,
|
||||
operation: string,
|
||||
fileOriginName: string,
|
||||
index: number,
|
||||
) {
|
||||
api
|
||||
.post('/api/v1/document/saveText', {
|
||||
filePath,
|
||||
operation,
|
||||
knowledgeId: knowledgeIdRef.value,
|
||||
fileOriginName,
|
||||
...props.splitterParams,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
localFilesList.value[index].progressUpload = 'success';
|
||||
emit('loadingFinish');
|
||||
}
|
||||
/* if (index === localFilesList.value.length - 1) {
|
||||
emit('loadingFinish');
|
||||
}*/
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="import-doc-file-list">
|
||||
<ElTable :data="localFilesList" size="large" style="width: 100%">
|
||||
<ElTableColumn
|
||||
prop="fileName"
|
||||
:label="$t('documentCollection.importDoc.fileName')"
|
||||
width="250"
|
||||
/>
|
||||
<ElTableColumn
|
||||
prop="progressUpload"
|
||||
:label="$t('documentCollection.splitterDoc.uploadStatus')"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<ElTag type="success" v-if="row.progressUpload === 'success'">
|
||||
{{ $t('documentCollection.splitterDoc.completed') }}
|
||||
</ElTag>
|
||||
<ElTag type="primary" v-else>
|
||||
{{ $t('documentCollection.splitterDoc.pendingUpload') }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.import-doc-file-list {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,258 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { $t } from '@easyflow/locales';
|
||||
|
||||
import { ArrowLeft, Plus } from '@element-plus/icons-vue';
|
||||
import { ElIcon, ElImage } from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import bookIcon from '#/assets/ai/knowledge/book.svg';
|
||||
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
||||
import PageSide from '#/components/page/PageSide.vue';
|
||||
import ChunkDocumentTable from '#/views/ai/documentCollection/ChunkDocumentTable.vue';
|
||||
import DocumentCollectionDataConfig from '#/views/ai/documentCollection/DocumentCollectionDataConfig.vue';
|
||||
import DocumentTable from '#/views/ai/documentCollection/DocumentTable.vue';
|
||||
import ImportKnowledgeDocFile from '#/views/ai/documentCollection/ImportKnowledgeDocFile.vue';
|
||||
import KnowledgeSearch from '#/views/ai/documentCollection/KnowledgeSearch.vue';
|
||||
import KnowledgeSearchConfig from '#/views/ai/documentCollection/KnowledgeSearchConfig.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const knowledgeId = ref<string>((route.query.id as string) || '');
|
||||
const activeMenu = ref<string>((route.query.activeMenu as string) || '');
|
||||
const knowledgeInfo = ref<any>({});
|
||||
const getKnowledge = () => {
|
||||
api
|
||||
.get('/api/v1/documentCollection/detail', {
|
||||
params: { id: knowledgeId.value },
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
knowledgeInfo.value = res.data;
|
||||
}
|
||||
});
|
||||
};
|
||||
onMounted(() => {
|
||||
if (activeMenu.value) {
|
||||
defaultSelectedMenu.value = activeMenu.value;
|
||||
}
|
||||
getKnowledge();
|
||||
});
|
||||
const back = () => {
|
||||
router.push({ path: '/ai/documentCollection' });
|
||||
};
|
||||
const categoryData = [
|
||||
{ key: 'documentList', name: $t('documentCollection.documentList') },
|
||||
{ key: 'knowledgeSearch', name: $t('documentCollection.knowledgeRetrieval') },
|
||||
{ key: 'config', name: $t('documentCollection.config') },
|
||||
];
|
||||
const headerButtons = [
|
||||
{
|
||||
key: 'importFile',
|
||||
text: $t('button.importFile'),
|
||||
icon: Plus,
|
||||
type: 'primary',
|
||||
data: { action: 'importFile' },
|
||||
},
|
||||
];
|
||||
const isImportFileVisible = ref(false);
|
||||
const selectedCategory = ref('documentList');
|
||||
const documentTableRef = ref();
|
||||
const handleSearch = (searchParams: string) => {
|
||||
documentTableRef.value.search(searchParams);
|
||||
};
|
||||
const handleButtonClick = (event: any) => {
|
||||
// 根据按钮 key 执行不同操作
|
||||
switch (event.key) {
|
||||
case 'back': {
|
||||
router.push({ path: '/ai/knowledge' });
|
||||
break;
|
||||
}
|
||||
case 'importFile': {
|
||||
isImportFileVisible.value = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
const handleCategoryClick = (category: any) => {
|
||||
selectedCategory.value = category.key;
|
||||
viewDocVisible.value = false;
|
||||
};
|
||||
const viewDocVisible = ref(false);
|
||||
const documentId = ref('');
|
||||
// 子组件传递事件,显示查看文档详情
|
||||
const viewDoc = (docId: string) => {
|
||||
viewDocVisible.value = true;
|
||||
documentId.value = docId;
|
||||
};
|
||||
const backDoc = () => {
|
||||
isImportFileVisible.value = false;
|
||||
};
|
||||
const defaultSelectedMenu = ref('documentList');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="document-container">
|
||||
<div v-if="!isImportFileVisible" class="doc-header-container">
|
||||
<div class="doc-knowledge-container">
|
||||
<div @click="back()" style="cursor: pointer">
|
||||
<ElIcon><ArrowLeft /></ElIcon>
|
||||
</div>
|
||||
<div>
|
||||
<ElImage :src="bookIcon" style="width: 36px; height: 36px" />
|
||||
</div>
|
||||
<div class="knowledge-info-container">
|
||||
<div class="title">{{ knowledgeInfo.title || '' }}</div>
|
||||
<div class="description">
|
||||
{{ knowledgeInfo.description || '' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="doc-content">
|
||||
<div>
|
||||
<PageSide
|
||||
label-key="name"
|
||||
value-key="key"
|
||||
:menus="categoryData"
|
||||
:default-selected="defaultSelectedMenu"
|
||||
@change="handleCategoryClick"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="doc-table-content menu-container border border-[var(--el-border-color)]"
|
||||
>
|
||||
<div v-if="selectedCategory === 'documentList'" class="doc-table">
|
||||
<div class="doc-header" v-if="!viewDocVisible">
|
||||
<HeaderSearch
|
||||
:buttons="headerButtons"
|
||||
@search="handleSearch"
|
||||
@button-click="handleButtonClick"
|
||||
/>
|
||||
</div>
|
||||
<DocumentTable
|
||||
ref="documentTableRef"
|
||||
:knowledge-id="knowledgeId"
|
||||
@view-doc="viewDoc"
|
||||
v-if="!viewDocVisible"
|
||||
/>
|
||||
|
||||
<ChunkDocumentTable
|
||||
v-else
|
||||
:document-id="documentId"
|
||||
:default-summary-prompt="knowledgeInfo.summaryPrompt"
|
||||
/>
|
||||
</div>
|
||||
<!--知识检索-->
|
||||
<div
|
||||
v-if="selectedCategory === 'knowledgeSearch'"
|
||||
class="doc-search-container"
|
||||
>
|
||||
<KnowledgeSearchConfig :document-collection-id="knowledgeId" />
|
||||
<KnowledgeSearch :knowledge-id="knowledgeId" />
|
||||
</div>
|
||||
<!--配置-->
|
||||
<div v-if="selectedCategory === 'config'">
|
||||
<DocumentCollectionDataConfig
|
||||
:detail-data="knowledgeInfo"
|
||||
@reload="getKnowledge"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="doc-imp-container">
|
||||
<ImportKnowledgeDocFile @import-back="backDoc" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.document-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
padding: 24px 24px 30px 24px;
|
||||
}
|
||||
.doc-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.doc-table-content {
|
||||
border-radius: 8px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 20px 14px 0 14px;
|
||||
background-color: var(--el-bg-color);
|
||||
flex: 1;
|
||||
}
|
||||
.doc-header {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
padding-bottom: 21px;
|
||||
}
|
||||
.doc-content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.doc-table {
|
||||
background-color: var(--el-bg-color);
|
||||
}
|
||||
.doc-imp-container {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.doc-header-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
.doc-knowledge-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
gap: 8px;
|
||||
}
|
||||
.knowledge-info-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.title {
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
color: #75808d;
|
||||
line-height: 22px;
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
text-transform: none;
|
||||
}
|
||||
.doc-search-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
}
|
||||
.menu-container {
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,371 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from 'element-plus';
|
||||
|
||||
import type { ActionButton } from '#/components/page/CardList.vue';
|
||||
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { $t } from '@easyflow/locales';
|
||||
|
||||
import { Delete, Edit, Notebook, Plus, Search } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElButton,
|
||||
ElDialog,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElInputNumber,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
} from 'element-plus';
|
||||
import { tryit } from 'radash';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import defaultIcon from '#/assets/ai/knowledge/book.svg';
|
||||
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
||||
import CardPage from '#/components/page/CardList.vue';
|
||||
import PageData from '#/components/page/PageData.vue';
|
||||
import PageSide from '#/components/page/PageSide.vue';
|
||||
import DocumentCollectionModal from '#/views/ai/documentCollection/DocumentCollectionModal.vue';
|
||||
|
||||
const router = useRouter();
|
||||
interface FieldDefinition {
|
||||
// 字段名称
|
||||
prop: string;
|
||||
// 字段标签
|
||||
label: string;
|
||||
// 字段类型:input, number, select, radio, checkbox, switch, date, datetime
|
||||
type?: 'input' | 'number';
|
||||
// 是否必填
|
||||
required?: boolean;
|
||||
// 占位符
|
||||
placeholder?: string;
|
||||
}
|
||||
// 操作按钮配置
|
||||
const actions: ActionButton[] = [
|
||||
{
|
||||
icon: Edit,
|
||||
text: $t('button.edit'),
|
||||
className: '',
|
||||
permission: '/api/v1/documentCollection/save',
|
||||
onClick(row) {
|
||||
aiKnowledgeModalRef.value.openDialog(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: Notebook,
|
||||
text: $t('documentCollection.actions.knowledge'),
|
||||
className: '',
|
||||
permission: '/api/v1/documentCollection/save',
|
||||
onClick(row) {
|
||||
router.push({
|
||||
path: '/ai/documentCollection/document',
|
||||
query: {
|
||||
id: row.id,
|
||||
pageKey: '/ai/documentCollection',
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: Search,
|
||||
text: $t('documentCollection.actions.retrieve'),
|
||||
className: '',
|
||||
permission: '',
|
||||
onClick(row) {
|
||||
router.push({
|
||||
path: '/ai/documentCollection/document',
|
||||
query: {
|
||||
id: row.id,
|
||||
pageKey: '/ai/documentCollection',
|
||||
activeMenu: 'knowledgeSearch',
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
text: $t('button.delete'),
|
||||
icon: Delete,
|
||||
className: 'item-danger',
|
||||
permission: '/api/v1/documentCollection/remove',
|
||||
onClick(row) {
|
||||
handleDelete(row);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
onMounted(() => {
|
||||
getCategoryList();
|
||||
});
|
||||
const handleDelete = (item: any) => {
|
||||
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
|
||||
confirmButtonText: $t('message.ok'),
|
||||
cancelButtonText: $t('message.cancel'),
|
||||
type: 'warning',
|
||||
})
|
||||
.then(() => {
|
||||
api
|
||||
.post('/api/v1/documentCollection/remove', { id: item.id })
|
||||
.then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success($t('message.deleteOkMessage'));
|
||||
pageDataRef.value.setQuery({});
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
const pageDataRef = ref();
|
||||
const aiKnowledgeModalRef = ref();
|
||||
const headerButtons = [
|
||||
{
|
||||
key: 'add',
|
||||
text: $t('documentCollection.actions.addKnowledge'),
|
||||
icon: Plus,
|
||||
type: 'primary',
|
||||
data: { action: 'add' },
|
||||
permission: '/api/v1/documentCollection/save',
|
||||
},
|
||||
];
|
||||
const handleButtonClick = (event: any, _item: any) => {
|
||||
switch (event.key) {
|
||||
case 'add': {
|
||||
aiKnowledgeModalRef.value.openDialog({});
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
const fieldDefinitions = ref<FieldDefinition[]>([
|
||||
{
|
||||
prop: 'categoryName',
|
||||
label: $t('aiWorkflowCategory.categoryName'),
|
||||
type: 'input',
|
||||
required: true,
|
||||
placeholder: $t('aiWorkflowCategory.categoryName'),
|
||||
},
|
||||
{
|
||||
prop: 'sortNo',
|
||||
label: $t('aiWorkflowCategory.sortNo'),
|
||||
type: 'number',
|
||||
required: false,
|
||||
placeholder: $t('aiWorkflowCategory.sortNo'),
|
||||
},
|
||||
]);
|
||||
const formRules = computed(() => {
|
||||
const rules: Record<string, any[]> = {};
|
||||
fieldDefinitions.value.forEach((field) => {
|
||||
const fieldRules = [];
|
||||
if (field.required) {
|
||||
fieldRules.push({
|
||||
required: true,
|
||||
message: `${$t('message.required')}`,
|
||||
trigger: 'blur',
|
||||
});
|
||||
}
|
||||
if (fieldRules.length > 0) {
|
||||
rules[field.prop] = fieldRules;
|
||||
}
|
||||
});
|
||||
return rules;
|
||||
});
|
||||
const handleSearch = (params: any) => {
|
||||
pageDataRef.value.setQuery({ title: params, isQueryOr: true });
|
||||
};
|
||||
const formData = ref<any>({});
|
||||
const dialogVisible = ref(false);
|
||||
const formRef = ref<FormInstance>();
|
||||
function showControlDialog(item: any) {
|
||||
formRef.value?.resetFields();
|
||||
formData.value = { ...item };
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
const categoryList = ref<any[]>([]);
|
||||
const getCategoryList = async () => {
|
||||
const [, res] = await tryit(api.get)(
|
||||
'/api/v1/documentCollectionCategory/list',
|
||||
{
|
||||
params: { sortKey: 'sortNo', sortType: 'asc' },
|
||||
},
|
||||
);
|
||||
|
||||
if (res && res.errorCode === 0) {
|
||||
categoryList.value = [
|
||||
{
|
||||
id: '',
|
||||
categoryName: $t('common.allCategories'),
|
||||
},
|
||||
...res.data,
|
||||
];
|
||||
}
|
||||
};
|
||||
function removeCategory(row: any) {
|
||||
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
|
||||
confirmButtonText: $t('message.ok'),
|
||||
cancelButtonText: $t('message.cancel'),
|
||||
type: 'warning',
|
||||
beforeClose: (action, instance, done) => {
|
||||
if (action === 'confirm') {
|
||||
instance.confirmButtonLoading = true;
|
||||
api
|
||||
.post('/api/v1/documentCollectionCategory/remove', { id: row.id })
|
||||
.then((res) => {
|
||||
instance.confirmButtonLoading = false;
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message);
|
||||
done();
|
||||
getCategoryList();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
instance.confirmButtonLoading = false;
|
||||
});
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
},
|
||||
}).catch(() => {});
|
||||
}
|
||||
const controlBtns = [
|
||||
{
|
||||
icon: Edit,
|
||||
label: $t('button.edit'),
|
||||
onClick(row: any) {
|
||||
showControlDialog(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'danger',
|
||||
icon: Delete,
|
||||
label: $t('button.delete'),
|
||||
onClick(row: any) {
|
||||
removeCategory(row);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const footerButton = {
|
||||
icon: Plus,
|
||||
label: $t('button.add'),
|
||||
onClick() {
|
||||
showControlDialog({});
|
||||
},
|
||||
};
|
||||
const saveLoading = ref(false);
|
||||
function handleSubmit() {
|
||||
formRef.value?.validate((valid) => {
|
||||
if (valid) {
|
||||
saveLoading.value = true;
|
||||
const url = formData.value.id
|
||||
? '/api/v1/documentCollectionCategory/update'
|
||||
: '/api/v1/documentCollectionCategory/save';
|
||||
api.post(url, formData.value).then((res) => {
|
||||
saveLoading.value = false;
|
||||
if (res.errorCode === 0) {
|
||||
getCategoryList();
|
||||
ElMessage.success(res.message);
|
||||
dialogVisible.value = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
function changeCategory(category: any) {
|
||||
pageDataRef.value.setQuery({ categoryId: category.id });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full flex-col gap-6 p-6">
|
||||
<div class="knowledge-header">
|
||||
<HeaderSearch
|
||||
:buttons="headerButtons"
|
||||
@search="handleSearch"
|
||||
@button-click="handleButtonClick"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex max-h-[calc(100vh-191px)] flex-1 gap-6">
|
||||
<PageSide
|
||||
label-key="categoryName"
|
||||
value-key="id"
|
||||
:menus="categoryList"
|
||||
:control-btns="controlBtns"
|
||||
:footer-button="footerButton"
|
||||
@change="changeCategory"
|
||||
/>
|
||||
<div class="h-full flex-1 overflow-auto">
|
||||
<PageData
|
||||
ref="pageDataRef"
|
||||
page-url="/api/v1/documentCollection/page"
|
||||
:page-size="12"
|
||||
:page-sizes="[12, 24, 36, 48]"
|
||||
:init-query-params="{ status: 1 }"
|
||||
>
|
||||
<template #default="{ pageList }">
|
||||
<CardPage
|
||||
:default-icon="defaultIcon"
|
||||
title-key="title"
|
||||
avatar-key="icon"
|
||||
description-key="description"
|
||||
:data="pageList"
|
||||
:actions="actions"
|
||||
/>
|
||||
</template>
|
||||
</PageData>
|
||||
</div>
|
||||
</div>
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="formData.id ? `${$t('button.edit')}` : `${$t('button.add')}`"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<ElForm
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<!-- 动态生成表单项 -->
|
||||
<ElFormItem
|
||||
v-for="field in fieldDefinitions"
|
||||
:key="field.prop"
|
||||
:label="field.label"
|
||||
:prop="field.prop"
|
||||
>
|
||||
<ElInput
|
||||
v-if="!field.type || field.type === 'input'"
|
||||
v-model="formData[field.prop]"
|
||||
:placeholder="field.placeholder"
|
||||
/>
|
||||
<ElInputNumber
|
||||
v-else-if="field.type === 'number'"
|
||||
v-model="formData[field.prop]"
|
||||
:placeholder="field.placeholder"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<ElButton @click="dialogVisible = false">
|
||||
{{ $t('button.cancel') }}
|
||||
</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit" :loading="saveLoading">
|
||||
{{ $t('button.confirm') }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 新增知识库模态框-->
|
||||
<DocumentCollectionModal ref="aiKnowledgeModalRef" @reload="handleSearch" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
color: #303133;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,346 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from 'element-plus';
|
||||
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { InfoFilled } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElButton,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElIcon,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
ElSwitch,
|
||||
ElTooltip,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import UploadAvatar from '#/components/upload/UploadAvatar.vue';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const props = defineProps({
|
||||
detailData: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
id: '',
|
||||
alias: '',
|
||||
deptId: '',
|
||||
icon: '',
|
||||
title: '',
|
||||
description: '',
|
||||
slug: '',
|
||||
vectorStoreEnable: false,
|
||||
vectorStoreType: '',
|
||||
vectorStoreCollection: '',
|
||||
vectorStoreConfig: '',
|
||||
vectorEmbedModelId: '',
|
||||
options: {
|
||||
canUpdateEmbeddingModel: true,
|
||||
},
|
||||
rerankModelId: '',
|
||||
searchEngineEnable: false,
|
||||
englishName: '',
|
||||
}),
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['reload']);
|
||||
|
||||
const entity = ref<any>({ ...props.detailData });
|
||||
|
||||
watch(
|
||||
() => props.detailData,
|
||||
(newVal) => {
|
||||
entity.value = { ...newVal };
|
||||
},
|
||||
{ immediate: true, deep: true },
|
||||
);
|
||||
|
||||
const embeddingLlmList = ref<any>([]);
|
||||
const rerankerLlmList = ref<any>([]);
|
||||
const vecotrDatabaseList = ref<any>([
|
||||
// { value: 'milvus', label: 'Milvus' },
|
||||
{ value: 'redis', label: 'Redis' },
|
||||
{ value: 'opensearch', label: 'OpenSearch' },
|
||||
{ value: 'elasticsearch', label: 'ElasticSearch' },
|
||||
{ value: 'aliyun', label: $t('documentCollection.alibabaCloud') },
|
||||
{ value: 'qcloud', label: $t('documentCollection.tencentCloud') },
|
||||
]);
|
||||
|
||||
const getEmbeddingLlmListData = async () => {
|
||||
try {
|
||||
const url = `/api/v1/model/list?modelType=embeddingModel`;
|
||||
const res = await api.get(url, {});
|
||||
if (res.errorCode === 0) {
|
||||
embeddingLlmList.value = res.data;
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error($t('message.apiError'));
|
||||
console.error('获取嵌入模型列表失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const getRerankerLlmListData = async () => {
|
||||
try {
|
||||
const res = await api.get('/api/v1/model/list?modelType=rerankModel');
|
||||
rerankerLlmList.value = res.data;
|
||||
} catch (error) {
|
||||
ElMessage.error($t('message.apiError'));
|
||||
console.error('获取重排模型列表失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await getEmbeddingLlmListData();
|
||||
await getRerankerLlmListData();
|
||||
});
|
||||
|
||||
const saveForm = ref<FormInstance>();
|
||||
const btnLoading = ref(false);
|
||||
const rules = ref({
|
||||
deptId: [
|
||||
{ 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' },
|
||||
],
|
||||
title: [{ required: true, message: $t('message.required'), trigger: 'blur' }],
|
||||
vectorStoreType: [
|
||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
||||
],
|
||||
vectorStoreCollection: [
|
||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
||||
],
|
||||
vectorStoreConfig: [
|
||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
||||
],
|
||||
vectorEmbedModelId: [
|
||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
||||
],
|
||||
});
|
||||
|
||||
async function save() {
|
||||
try {
|
||||
const valid = await saveForm.value?.validate();
|
||||
if (!valid) return;
|
||||
|
||||
btnLoading.value = true;
|
||||
const res = await api.post(
|
||||
'/api/v1/documentCollection/update',
|
||||
entity.value,
|
||||
);
|
||||
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success($t('message.saveOkMessage'));
|
||||
emit('reload');
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error($t('message.saveFail'));
|
||||
console.error('保存失败:', error);
|
||||
} finally {
|
||||
btnLoading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="document-config-container">
|
||||
<ElForm
|
||||
label-width="150px"
|
||||
ref="saveForm"
|
||||
:model="entity"
|
||||
status-icon
|
||||
:rules="rules"
|
||||
>
|
||||
<ElFormItem
|
||||
prop="icon"
|
||||
:label="$t('documentCollection.icon')"
|
||||
style="display: flex; align-items: center"
|
||||
>
|
||||
<UploadAvatar v-model="entity.icon" />
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="title" :label="$t('documentCollection.title')">
|
||||
<ElInput
|
||||
v-model.trim="entity.title"
|
||||
:placeholder="$t('documentCollection.placeholder.title')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="alias" :label="$t('documentCollection.alias')">
|
||||
<ElInput v-model.trim="entity.alias" />
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
prop="englishName"
|
||||
:label="$t('documentCollection.englishName')"
|
||||
>
|
||||
<ElInput v-model.trim="entity.englishName" />
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
prop="description"
|
||||
:label="$t('documentCollection.description')"
|
||||
>
|
||||
<ElInput
|
||||
v-model.trim="entity.description"
|
||||
:rows="4"
|
||||
type="textarea"
|
||||
:placeholder="$t('documentCollection.placeholder.description')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<!-- <ElFormItem
|
||||
prop="vectorStoreEnable"
|
||||
:label="$t('documentCollection.vectorStoreEnable')"
|
||||
>
|
||||
<ElSwitch v-model="entity.vectorStoreEnable" />
|
||||
</ElFormItem>-->
|
||||
<ElFormItem
|
||||
prop="vectorStoreType"
|
||||
:label="$t('documentCollection.vectorStoreType')"
|
||||
>
|
||||
<ElSelect
|
||||
v-model="entity.vectorStoreType"
|
||||
:placeholder="$t('documentCollection.placeholder.vectorStoreType')"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in vecotrDatabaseList"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value || ''"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
prop="vectorStoreCollection"
|
||||
:label="$t('documentCollection.vectorStoreCollection')"
|
||||
>
|
||||
<ElInput
|
||||
v-model.trim="entity.vectorStoreCollection"
|
||||
:placeholder="
|
||||
$t('documentCollection.placeholder.vectorStoreCollection')
|
||||
"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
prop="vectorStoreConfig"
|
||||
:label="$t('documentCollection.vectorStoreConfig')"
|
||||
>
|
||||
<ElInput
|
||||
v-model.trim="entity.vectorStoreConfig"
|
||||
:rows="4"
|
||||
type="textarea"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="vectorEmbedModelId">
|
||||
<template #label>
|
||||
<span style="display: flex; align-items: center">
|
||||
{{ $t('documentCollection.vectorEmbedLlmId') }}
|
||||
<ElTooltip
|
||||
:content="$t('documentCollection.vectorEmbedModelTips')"
|
||||
placement="top"
|
||||
effect="light"
|
||||
>
|
||||
<ElIcon
|
||||
style="
|
||||
margin-left: 4px;
|
||||
color: #909399;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
"
|
||||
>
|
||||
<InfoFilled />
|
||||
</ElIcon>
|
||||
</ElTooltip>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<ElSelect
|
||||
v-model="entity.vectorEmbedModelId"
|
||||
:disabled="!entity?.options?.canUpdateEmbeddingModel"
|
||||
:placeholder="$t('documentCollection.placeholder.embedLlm')"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in embeddingLlmList"
|
||||
:key="item.id"
|
||||
:label="item.title"
|
||||
:value="item.id || ''"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
prop="dimensionOfVectorModel"
|
||||
:label="$t('documentCollection.dimensionOfVectorModel')"
|
||||
>
|
||||
<template #label>
|
||||
<span style="display: flex; align-items: center">
|
||||
{{ $t('documentCollection.dimensionOfVectorModel') }}
|
||||
<ElTooltip
|
||||
:content="$t('documentCollection.dimensionOfVectorModelTips')"
|
||||
placement="top"
|
||||
effect="light"
|
||||
>
|
||||
<ElIcon
|
||||
style="
|
||||
margin-left: 4px;
|
||||
color: #909399;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
"
|
||||
>
|
||||
<InfoFilled />
|
||||
</ElIcon>
|
||||
</ElTooltip>
|
||||
</span>
|
||||
</template>
|
||||
<ElInput
|
||||
:disabled="!entity?.options?.canUpdateEmbeddingModel"
|
||||
v-model.trim="entity.dimensionOfVectorModel"
|
||||
type="number"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
prop="rerankModelId"
|
||||
:label="$t('documentCollection.rerankLlmId')"
|
||||
>
|
||||
<ElSelect
|
||||
v-model="entity.rerankModelId"
|
||||
:placeholder="$t('documentCollection.placeholder.rerankLlm')"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in rerankerLlmList"
|
||||
:key="item.id"
|
||||
:label="item.title"
|
||||
:value="item.id || ''"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
prop="searchEngineEnable"
|
||||
:label="$t('documentCollection.searchEngineEnable')"
|
||||
>
|
||||
<ElSwitch v-model="entity.searchEngineEnable" />
|
||||
</ElFormItem>
|
||||
<ElFormItem style="margin-top: 20px; text-align: right">
|
||||
<ElButton
|
||||
type="primary"
|
||||
@click="save"
|
||||
:loading="btnLoading"
|
||||
:disabled="btnLoading"
|
||||
>
|
||||
{{ $t('button.save') }}
|
||||
</ElButton>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.document-config-container {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,381 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from 'element-plus';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { InfoFilled } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElButton,
|
||||
ElDialog,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElIcon,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
ElSwitch,
|
||||
ElTooltip,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import DictSelect from '#/components/dict/DictSelect.vue';
|
||||
import UploadAvatar from '#/components/upload/UploadAvatar.vue';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const emit = defineEmits(['reload']);
|
||||
const embeddingLlmList = ref<any>([]);
|
||||
const rerankerLlmList = ref<any>([]);
|
||||
|
||||
const getEmbeddingLlmListData = async () => {
|
||||
try {
|
||||
const url = `/api/v1/model/list?modelType=embeddingModel`;
|
||||
const res = await api.get(url, {});
|
||||
if (res.errorCode === 0) {
|
||||
embeddingLlmList.value = res.data;
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error($t('message.apiError'));
|
||||
console.error('获取嵌入模型列表失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const getRerankerLlmListData = async () => {
|
||||
try {
|
||||
const res = await api.get('/api/v1/model/list?modelType=rerankModel');
|
||||
rerankerLlmList.value = res.data;
|
||||
} catch (error) {
|
||||
ElMessage.error($t('message.apiError'));
|
||||
console.error('获取重排模型列表失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await getEmbeddingLlmListData();
|
||||
await getRerankerLlmListData();
|
||||
});
|
||||
|
||||
const saveForm = ref<FormInstance>();
|
||||
const dialogVisible = ref(false);
|
||||
const isAdd = ref(true);
|
||||
const vecotrDatabaseList = ref<any>([
|
||||
{ value: 'milvus', label: 'Milvus' },
|
||||
{ value: 'redis', label: 'Redis' },
|
||||
{ value: 'opensearch', label: 'OpenSearch' },
|
||||
{ value: 'elasticsearch', label: 'ElasticSearch' },
|
||||
{ value: 'aliyun', label: $t('documentCollection.alibabaCloud') },
|
||||
{ value: 'qcloud', label: $t('documentCollection.tencentCloud') },
|
||||
]);
|
||||
|
||||
const defaultEntity = {
|
||||
alias: '',
|
||||
deptId: '',
|
||||
icon: '',
|
||||
title: '',
|
||||
categoryId: '',
|
||||
description: '',
|
||||
slug: '',
|
||||
vectorStoreEnable: false,
|
||||
vectorStoreType: '',
|
||||
vectorStoreCollection: '',
|
||||
vectorStoreConfig: '',
|
||||
vectorEmbedModelId: '',
|
||||
dimensionOfVectorModel: undefined,
|
||||
options: {
|
||||
canUpdateEmbeddingModel: true,
|
||||
},
|
||||
rerankModelId: '',
|
||||
searchEngineEnable: '',
|
||||
englishName: '',
|
||||
};
|
||||
const entity = ref<any>({ ...defaultEntity });
|
||||
|
||||
const btnLoading = ref(false);
|
||||
const rules = ref({
|
||||
deptId: [
|
||||
{ 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' },
|
||||
],
|
||||
title: [{ required: true, message: $t('message.required'), trigger: 'blur' }],
|
||||
vectorStoreType: [
|
||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
||||
],
|
||||
vectorStoreCollection: [
|
||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
||||
],
|
||||
vectorStoreConfig: [
|
||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
||||
],
|
||||
vectorEmbedModelId: [
|
||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
||||
],
|
||||
});
|
||||
|
||||
function openDialog(row: any = {}) {
|
||||
if (row.id) {
|
||||
isAdd.value = false;
|
||||
entity.value = {
|
||||
...defaultEntity,
|
||||
...row,
|
||||
options: { ...defaultEntity.options, ...row.options },
|
||||
};
|
||||
} else {
|
||||
isAdd.value = true;
|
||||
entity.value = { ...defaultEntity };
|
||||
}
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function save() {
|
||||
try {
|
||||
const valid = await saveForm.value?.validate();
|
||||
if (!valid) return;
|
||||
|
||||
btnLoading.value = true;
|
||||
const res = await api.post(
|
||||
isAdd.value
|
||||
? '/api/v1/documentCollection/save'
|
||||
: '/api/v1/documentCollection/update',
|
||||
entity.value,
|
||||
);
|
||||
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message || $t('message.saveSuccess'));
|
||||
emit('reload');
|
||||
closeDialog();
|
||||
} else {
|
||||
ElMessage.error(res.message || $t('message.saveFail'));
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error($t('message.saveFail'));
|
||||
console.error('保存失败:', error);
|
||||
} finally {
|
||||
btnLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
saveForm.value?.resetFields();
|
||||
isAdd.value = true;
|
||||
entity.value = { ...defaultEntity };
|
||||
dialogVisible.value = false;
|
||||
}
|
||||
|
||||
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"
|
||||
align-center
|
||||
>
|
||||
<ElForm
|
||||
label-width="150px"
|
||||
ref="saveForm"
|
||||
:model="entity"
|
||||
status-icon
|
||||
:rules="rules"
|
||||
>
|
||||
<ElFormItem
|
||||
prop="icon"
|
||||
:label="$t('documentCollection.icon')"
|
||||
style="display: flex; align-items: center"
|
||||
>
|
||||
<UploadAvatar v-model="entity.icon" />
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="title" :label="$t('documentCollection.title')">
|
||||
<ElInput
|
||||
v-model.trim="entity.title"
|
||||
:placeholder="$t('documentCollection.placeholder.title')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
prop="categoryId"
|
||||
:label="$t('documentCollection.categoryId')"
|
||||
>
|
||||
<DictSelect
|
||||
v-model="entity.categoryId"
|
||||
dict-code="aiDocumentCollectionCategory"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="alias" :label="$t('documentCollection.alias')">
|
||||
<ElInput v-model.trim="entity.alias" />
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
prop="englishName"
|
||||
:label="$t('documentCollection.englishName')"
|
||||
>
|
||||
<ElInput v-model.trim="entity.englishName" />
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
prop="description"
|
||||
:label="$t('documentCollection.description')"
|
||||
>
|
||||
<ElInput
|
||||
v-model.trim="entity.description"
|
||||
:rows="4"
|
||||
type="textarea"
|
||||
:placeholder="$t('documentCollection.placeholder.description')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<!-- <ElFormItem
|
||||
prop="vectorStoreEnable"
|
||||
:label="$t('documentCollection.vectorStoreEnable')"
|
||||
>
|
||||
<ElSwitch v-model="entity.vectorStoreEnable" />
|
||||
</ElFormItem>-->
|
||||
<ElFormItem
|
||||
prop="vectorStoreType"
|
||||
:label="$t('documentCollection.vectorStoreType')"
|
||||
>
|
||||
<ElSelect
|
||||
v-model="entity.vectorStoreType"
|
||||
:placeholder="$t('documentCollection.placeholder.vectorStoreType')"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in vecotrDatabaseList"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value || ''"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
prop="vectorStoreCollection"
|
||||
:label="$t('documentCollection.vectorStoreCollection')"
|
||||
>
|
||||
<ElInput
|
||||
v-model.trim="entity.vectorStoreCollection"
|
||||
:placeholder="
|
||||
$t('documentCollection.placeholder.vectorStoreCollection')
|
||||
"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
prop="vectorStoreConfig"
|
||||
:label="$t('documentCollection.vectorStoreConfig')"
|
||||
>
|
||||
<ElInput
|
||||
v-model.trim="entity.vectorStoreConfig"
|
||||
:rows="4"
|
||||
type="textarea"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="vectorEmbedModelId">
|
||||
<template #label>
|
||||
<span style="display: flex; align-items: center">
|
||||
{{ $t('documentCollection.vectorEmbedLlmId') }}
|
||||
<ElTooltip
|
||||
:content="$t('documentCollection.vectorEmbedModelTips')"
|
||||
placement="top"
|
||||
effect="light"
|
||||
>
|
||||
<ElIcon
|
||||
style="
|
||||
margin-left: 4px;
|
||||
color: #909399;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
"
|
||||
>
|
||||
<InfoFilled />
|
||||
</ElIcon>
|
||||
</ElTooltip>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<ElSelect
|
||||
v-model="entity.vectorEmbedModelId"
|
||||
:disabled="!entity?.options?.canUpdateEmbeddingModel"
|
||||
:placeholder="$t('documentCollection.placeholder.embedLlm')"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in embeddingLlmList"
|
||||
:key="item.id"
|
||||
:label="item.title"
|
||||
:value="item.id || ''"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
prop="dimensionOfVectorModel"
|
||||
:label="$t('documentCollection.dimensionOfVectorModel')"
|
||||
>
|
||||
<template #label>
|
||||
<span style="display: flex; align-items: center">
|
||||
{{ $t('documentCollection.dimensionOfVectorModel') }}
|
||||
<ElTooltip
|
||||
:content="$t('documentCollection.dimensionOfVectorModelTips')"
|
||||
placement="top"
|
||||
effect="light"
|
||||
>
|
||||
<ElIcon
|
||||
style="
|
||||
margin-left: 4px;
|
||||
color: #909399;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
"
|
||||
>
|
||||
<InfoFilled />
|
||||
</ElIcon>
|
||||
</ElTooltip>
|
||||
</span>
|
||||
</template>
|
||||
<ElInput
|
||||
:disabled="!entity?.options?.canUpdateEmbeddingModel"
|
||||
v-model.trim="entity.dimensionOfVectorModel"
|
||||
type="number"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
prop="rerankModelId"
|
||||
:label="$t('documentCollection.rerankLlmId')"
|
||||
>
|
||||
<ElSelect
|
||||
v-model="entity.rerankModelId"
|
||||
:placeholder="$t('documentCollection.placeholder.rerankLlm')"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in rerankerLlmList"
|
||||
:key="item.id"
|
||||
:label="item.title"
|
||||
:value="item.id || ''"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
prop="searchEngineEnable"
|
||||
:label="$t('documentCollection.searchEngineEnable')"
|
||||
>
|
||||
<ElSwitch v-model="entity.searchEngineEnable" />
|
||||
</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>
|
||||
@@ -0,0 +1,160 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { $t } from '@easyflow/locales';
|
||||
|
||||
import { Delete, Download, MoreFilled } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElButton,
|
||||
ElDropdown,
|
||||
ElDropdownItem,
|
||||
ElDropdownMenu,
|
||||
ElImage,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import documentIcon from '#/assets/ai/knowledge/document.svg';
|
||||
import PageData from '#/components/page/PageData.vue';
|
||||
|
||||
const props = defineProps({
|
||||
knowledgeId: {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
const emits = defineEmits(['viewDoc']);
|
||||
defineExpose({
|
||||
search(searchText: string) {
|
||||
pageDataRef.value.setQuery({
|
||||
title: searchText,
|
||||
});
|
||||
},
|
||||
});
|
||||
const pageDataRef = ref();
|
||||
const handleView = (row: any) => {
|
||||
emits('viewDoc', row.id);
|
||||
};
|
||||
const handleDownload = (row: any) => {
|
||||
window.open(row.documentPath, '_blank');
|
||||
};
|
||||
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/document/removeDoc', { id: row.id }).then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success($t('message.deleteOkMessage'));
|
||||
pageDataRef.value.setQuery({ id: props.knowledgeId });
|
||||
}
|
||||
});
|
||||
// 删除逻辑
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageData
|
||||
page-url="/api/v1/document/documentList"
|
||||
ref="pageDataRef"
|
||||
:page-size="10"
|
||||
:extra-query-params="{
|
||||
id: props.knowledgeId,
|
||||
sort: 'desc',
|
||||
sortKey: 'created',
|
||||
}"
|
||||
>
|
||||
<template #default="{ pageList }">
|
||||
<ElTable :data="pageList" style="width: 100%" size="large">
|
||||
<ElTableColumn
|
||||
prop="fileName"
|
||||
:label="$t('documentCollection.fileName')"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span class="file-name-container">
|
||||
<ElImage :src="documentIcon" class="mr-1" />
|
||||
<span class="title">
|
||||
{{ row.title }}
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
prop="documentType"
|
||||
:label="$t('documentCollection.documentType')"
|
||||
width="180"
|
||||
/>
|
||||
<ElTableColumn
|
||||
prop="chunkCount"
|
||||
:label="$t('documentCollection.knowledgeCount')"
|
||||
width="180"
|
||||
/>
|
||||
<ElTableColumn
|
||||
:label="$t('documentCollection.createdModifyTime')"
|
||||
width="200"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="time-container">
|
||||
<span>{{ row.created }}</span>
|
||||
<span>{{ row.modified }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn :label="$t('common.handle')" width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
<div class="flex items-center gap-3">
|
||||
<ElButton link type="primary" @click="handleView(row)">
|
||||
{{ $t('button.viewSegmentation') }}
|
||||
</ElButton>
|
||||
|
||||
<ElDropdown>
|
||||
<ElButton link :icon="MoreFilled" />
|
||||
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownItem @click="handleDownload(row)">
|
||||
<ElButton link :icon="Download">
|
||||
{{ $t('button.download') }}
|
||||
</ElButton>
|
||||
</ElDropdownItem>
|
||||
<ElDropdownItem @click="handleDelete(row)">
|
||||
<ElButton link :icon="Delete" type="danger">
|
||||
{{ $t('button.delete') }}
|
||||
</ElButton>
|
||||
</ElDropdownItem>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</template>
|
||||
</PageData>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.time-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.file-name-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.title {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
color: #1a1a1a;
|
||||
line-height: 20px;
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
text-transform: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,253 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { $t } from '@easyflow/locales';
|
||||
|
||||
import { Back } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElButton,
|
||||
ElMessage,
|
||||
ElPagination,
|
||||
ElStep,
|
||||
ElSteps,
|
||||
} from 'element-plus';
|
||||
|
||||
import ComfirmImportDocument from '#/views/ai/documentCollection/ComfirmImportDocument.vue';
|
||||
import ImportKnowledgeFileContainer from '#/views/ai/documentCollection/ImportKnowledgeFileContainer.vue';
|
||||
import SegmenterDoc from '#/views/ai/documentCollection/SegmenterDoc.vue';
|
||||
import SplitterDocPreview from '#/views/ai/documentCollection/SplitterDocPreview.vue';
|
||||
|
||||
const emits = defineEmits(['importBack']);
|
||||
const back = () => {
|
||||
emits('importBack');
|
||||
};
|
||||
const files = ref([]);
|
||||
const splitterParams = ref({});
|
||||
const activeStep = ref(0);
|
||||
const fileUploadRef = ref();
|
||||
const confirmImportRef = ref();
|
||||
const segmenterDocRef = ref();
|
||||
const pagination = ref({
|
||||
pageSize: 10,
|
||||
currentPage: 1,
|
||||
total: 0,
|
||||
});
|
||||
const goToNextStep = () => {
|
||||
if (activeStep.value === 0) {
|
||||
if (fileUploadRef.value.getFilesData().length === 0) {
|
||||
ElMessage.error($t('message.uploadFileFirst'));
|
||||
return;
|
||||
}
|
||||
files.value = fileUploadRef.value.getFilesData();
|
||||
}
|
||||
if (activeStep.value === 1 && segmenterDocRef.value) {
|
||||
splitterParams.value = segmenterDocRef.value.getSplitterFormValues();
|
||||
}
|
||||
activeStep.value += 1;
|
||||
};
|
||||
const goToPreviousStep = () => {
|
||||
activeStep.value -= 1;
|
||||
};
|
||||
const handleSizeChange = (val: number) => {
|
||||
pagination.value.pageSize = val;
|
||||
};
|
||||
const handleCurrentChange = (val: number) => {
|
||||
pagination.value.currentPage = val;
|
||||
};
|
||||
const handleTotalUpdate = (newTotal: number) => {
|
||||
pagination.value.total = newTotal; // 同步到父组件的 pagination.total
|
||||
};
|
||||
const loadingSave = ref(false);
|
||||
const confirmImport = () => {
|
||||
loadingSave.value = true;
|
||||
// 确认导入
|
||||
confirmImportRef.value.handleSave();
|
||||
};
|
||||
const finishImport = () => {
|
||||
loadingSave.value = false;
|
||||
ElMessage.success($t('documentCollection.splitterDoc.importSuccess'));
|
||||
emits('importBack');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="imp-doc-kno-container">
|
||||
<div class="imp-doc-header">
|
||||
<ElButton @click="back" :icon="Back">
|
||||
{{ $t('button.back') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
<div class="imp-doc-kno-content">
|
||||
<div class="rounded-lg bg-[var(--table-header-bg-color)] py-5">
|
||||
<ElSteps :active="activeStep" align-center>
|
||||
<ElStep>
|
||||
<template #icon>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="h-8 w-8 rounded-full bg-[var(--step-item-bg)]">
|
||||
<span class="text-accent-foreground text-sm/8">1</span>
|
||||
</div>
|
||||
<span class="text-base">{{
|
||||
$t('documentCollection.importDoc.fileUpload')
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
</ElStep>
|
||||
<ElStep>
|
||||
<template #icon>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="h-8 w-8 rounded-full bg-[var(--step-item-bg)]">
|
||||
<span class="text-accent-foreground text-sm/8">2</span>
|
||||
</div>
|
||||
<span class="text-base">{{
|
||||
$t('documentCollection.importDoc.parameterSettings')
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
</ElStep>
|
||||
<ElStep>
|
||||
<template #icon>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="h-8 w-8 rounded-full bg-[var(--step-item-bg)]">
|
||||
<span class="text-accent-foreground text-sm/8">3</span>
|
||||
</div>
|
||||
<span class="text-base">{{
|
||||
$t('documentCollection.importDoc.segmentedPreview')
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
</ElStep>
|
||||
<ElStep>
|
||||
<template #icon>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="h-8 w-8 rounded-full bg-[var(--step-item-bg)]">
|
||||
<span class="text-accent-foreground text-sm/8">4</span>
|
||||
</div>
|
||||
<span class="text-base">{{
|
||||
$t('documentCollection.importDoc.confirmImport')
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
</ElStep>
|
||||
</ElSteps>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 20px">
|
||||
<!-- 文件上传导入-->
|
||||
<div class="knw-file-upload" v-if="activeStep === 0">
|
||||
<ImportKnowledgeFileContainer ref="fileUploadRef" />
|
||||
</div>
|
||||
<!-- 分割参数设置-->
|
||||
<div class="knw-file-splitter" v-if="activeStep === 1">
|
||||
<SegmenterDoc ref="segmenterDocRef" />
|
||||
</div>
|
||||
<!-- 分割预览-->
|
||||
<div class="knw-file-preview" v-if="activeStep === 2">
|
||||
<SplitterDocPreview
|
||||
:flies-list="files"
|
||||
:splitter-params="splitterParams"
|
||||
:page-number="pagination.currentPage"
|
||||
:page-size="pagination.pageSize"
|
||||
@update-total="handleTotalUpdate"
|
||||
/>
|
||||
</div>
|
||||
<!-- 确认导入-->
|
||||
<div class="knw-file-confirm" v-if="activeStep === 3">
|
||||
<ComfirmImportDocument
|
||||
:splitter-params="splitterParams"
|
||||
:files-list="files"
|
||||
ref="confirmImportRef"
|
||||
@loading-finish="finishImport"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="height: 40px"></div>
|
||||
<div class="imp-doc-footer">
|
||||
<div v-if="activeStep === 2" class="imp-doc-page-container">
|
||||
<ElPagination
|
||||
:page-sizes="[10, 20]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="pagination.total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
<ElButton @click="goToPreviousStep" type="primary" v-if="activeStep >= 1">
|
||||
{{ $t('button.previousStep') }}
|
||||
</ElButton>
|
||||
<ElButton @click="goToNextStep" type="primary" v-if="activeStep < 3">
|
||||
{{ $t('button.nextStep') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
@click="confirmImport"
|
||||
type="primary"
|
||||
v-if="activeStep === 3"
|
||||
:loading="loadingSave"
|
||||
:disabled="loadingSave"
|
||||
>
|
||||
{{ $t('button.startImport') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.imp-doc-kno-container {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
background-color: var(--el-bg-color);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.imp-doc-kno-content {
|
||||
flex: 1;
|
||||
padding-top: 20px;
|
||||
overflow: auto;
|
||||
}
|
||||
.imp-doc-footer {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
display: flex;
|
||||
height: 40px;
|
||||
background-color: var(--el-bg-color);
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.knw-file-preview {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
.imp-doc-page-container {
|
||||
margin-right: 12px;
|
||||
}
|
||||
.knw-file-confirm {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-step__head) {
|
||||
--step-item-bg: rgba(0, 0, 0, 0.06);
|
||||
--step-item-solid-bg: rgba(0, 0, 0, 0.15);
|
||||
--accent-foreground: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
:deep(.el-step__head:where(.dark, .dark *)) {
|
||||
--step-item-bg: var(--el-text-color-placeholder);
|
||||
--step-item-solid-bg: var(--el-text-color-placeholder);
|
||||
--accent-foreground: var(--primary-foreground);
|
||||
}
|
||||
:deep(.el-step__head.is-finish) {
|
||||
--step-item-bg: hsl(var(--primary));
|
||||
--step-item-solid-bg: hsl(var(--primary));
|
||||
--accent-foreground: var(--primary-foreground);
|
||||
}
|
||||
:deep(.el-step__icon.is-icon) {
|
||||
width: 120px;
|
||||
background-color: var(--table-header-bg-color);
|
||||
}
|
||||
:deep(.el-step__line) {
|
||||
background-color: var(--step-item-solid-bg);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,107 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { $t } from '@easyflow/locales';
|
||||
|
||||
import { ElButton, ElProgress, ElTable, ElTableColumn } from 'element-plus';
|
||||
|
||||
import { formatFileSize } from '#/api/common/file';
|
||||
import DragFileUpload from '#/components/upload/DragFileUpload.vue';
|
||||
|
||||
interface FileInfo {
|
||||
uid: string;
|
||||
fileName: string;
|
||||
progressUpload: number;
|
||||
fileSize: number;
|
||||
status: string;
|
||||
filePath: string;
|
||||
}
|
||||
const fileData = ref<FileInfo[]>([]);
|
||||
const filesPath = ref([]);
|
||||
defineExpose({
|
||||
getFilesData() {
|
||||
return fileData.value;
|
||||
},
|
||||
});
|
||||
function handleSuccess(response: any) {
|
||||
filesPath.value = response.data;
|
||||
}
|
||||
function handleChange(file: any) {
|
||||
const existingFile = fileData.value.find((item) => item.uid === file.uid);
|
||||
if (existingFile) {
|
||||
fileData.value = fileData.value.map((item) => {
|
||||
if (item.uid === file.uid) {
|
||||
return {
|
||||
...item,
|
||||
fileSize: file.size,
|
||||
progressUpload: file.percentage,
|
||||
status: file.status,
|
||||
filePath: file?.response?.data?.path,
|
||||
};
|
||||
}
|
||||
return item;
|
||||
});
|
||||
} else {
|
||||
fileData.value.push({
|
||||
uid: file.uid,
|
||||
fileName: file.name,
|
||||
progressUpload: file.percentage,
|
||||
fileSize: file.size,
|
||||
status: file.status,
|
||||
filePath: file?.response?.data?.path,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleRemove(row: any) {
|
||||
fileData.value = fileData.value.filter((item) => item.uid !== row.uid);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div>
|
||||
<DragFileUpload @success="handleSuccess" @on-change="handleChange" />
|
||||
</div>
|
||||
<div>
|
||||
<ElTable :data="fileData" style="width: 100%" size="large">
|
||||
<ElTableColumn
|
||||
prop="fileName"
|
||||
:label="$t('documentCollection.importDoc.fileName')"
|
||||
width="250"
|
||||
/>
|
||||
<ElTableColumn
|
||||
prop="progressUpload"
|
||||
:label="$t('documentCollection.importDoc.progressUpload')"
|
||||
width="180"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<ElProgress
|
||||
:percentage="row.progressUpload"
|
||||
v-if="row.status === 'success'"
|
||||
status="success"
|
||||
/>
|
||||
<ElProgress v-else :percentage="row.progressUpload" />
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
prop="fileSize"
|
||||
:label="$t('documentCollection.importDoc.fileSize')"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span>{{ formatFileSize(row.fileSize) }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn :label="$t('common.handle')">
|
||||
<template #default="{ row }">
|
||||
<ElButton type="danger" size="small" @click="handleRemove(row)">
|
||||
{{ $t('button.delete') }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,75 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { $t } from '@easyflow/locales';
|
||||
|
||||
import { ElButton, ElInput, ElMessage } from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import PreviewSearchKnowledge from '#/views/ai/documentCollection/PreviewSearchKnowledge.vue';
|
||||
|
||||
const props = defineProps({
|
||||
knowledgeId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
const searchDataList = ref([]);
|
||||
const keyword = ref('');
|
||||
const previewSearchKnowledgeRef = ref();
|
||||
const handleSearch = () => {
|
||||
if (!keyword.value) {
|
||||
ElMessage.error($t('message.pleaseInputContent'));
|
||||
return;
|
||||
}
|
||||
previewSearchKnowledgeRef.value.loadingContent(true);
|
||||
api
|
||||
.get(
|
||||
`/api/v1/documentCollection/search?knowledgeId=${props.knowledgeId}&keyword=${keyword.value}`,
|
||||
)
|
||||
.then((res) => {
|
||||
previewSearchKnowledgeRef.value.loadingContent(false);
|
||||
searchDataList.value = res.data;
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="search-container">
|
||||
<div class="search-input">
|
||||
<ElInput
|
||||
v-model="keyword"
|
||||
:placeholder="$t('common.searchPlaceholder')"
|
||||
/>
|
||||
<ElButton type="primary" @click="handleSearch">
|
||||
{{ $t('button.query') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
<div class="search-result">
|
||||
<PreviewSearchKnowledge
|
||||
:data="searchDataList"
|
||||
ref="previewSearchKnowledgeRef"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.search-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0 0 20px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.search-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
.search-result {
|
||||
padding-top: 20px;
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,342 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
|
||||
import { $t } from '@easyflow/locales';
|
||||
|
||||
import { InfoFilled } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElButton,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInputNumber,
|
||||
ElMessage,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
ElSwitch,
|
||||
ElTooltip,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
|
||||
const props = defineProps({
|
||||
documentCollectionId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
getDocumentCollectionConfig();
|
||||
});
|
||||
const searchEngineEnable = ref(false);
|
||||
const getDocumentCollectionConfig = () => {
|
||||
api
|
||||
.get(`/api/v1/documentCollection/detail?id=${props.documentCollectionId}`)
|
||||
.then((res) => {
|
||||
const { data } = res;
|
||||
searchConfig.docRecallMaxNum = data.options.docRecallMaxNum
|
||||
? Number(data.options.docRecallMaxNum)
|
||||
: 5;
|
||||
searchConfig.simThreshold = data.options.simThreshold
|
||||
? Number(data.options.simThreshold)
|
||||
: 0.5;
|
||||
searchConfig.searchEngineType = data.options.searchEngineType || 'lucene';
|
||||
searchEngineEnable.value = !!data.searchEngineEnable;
|
||||
});
|
||||
};
|
||||
|
||||
const searchConfig = reactive({
|
||||
docRecallMaxNum: 5,
|
||||
simThreshold: 0.5,
|
||||
searchEngineType: 'lucene',
|
||||
});
|
||||
|
||||
const submitConfig = () => {
|
||||
const submitData = {
|
||||
id: props.documentCollectionId,
|
||||
options: {
|
||||
docRecallMaxNum: searchConfig.docRecallMaxNum,
|
||||
simThreshold: searchConfig.simThreshold,
|
||||
searchEngineType: searchConfig.searchEngineType,
|
||||
},
|
||||
searchEngineEnable: searchEngineEnable.value,
|
||||
};
|
||||
|
||||
api
|
||||
.post('/api/v1/documentCollection/update', submitData)
|
||||
.then(() => {
|
||||
ElMessage.success($t('documentCollectionSearch.message.saveSuccess'));
|
||||
})
|
||||
.catch((error) => {
|
||||
ElMessage.error($t('documentCollectionSearch.message.saveFailed'));
|
||||
console.error('保存配置失败:', error);
|
||||
});
|
||||
};
|
||||
|
||||
const searchEngineOptions = [
|
||||
{
|
||||
label: 'Lucene',
|
||||
value: 'lucene',
|
||||
},
|
||||
{
|
||||
label: 'ElasticSearch',
|
||||
value: 'elasticSearch',
|
||||
},
|
||||
];
|
||||
const handleSearchEngineEnableChange = () => {
|
||||
api.post('/api/v1/documentCollection/update', {
|
||||
id: props.documentCollectionId,
|
||||
searchEngineEnable: searchEngineEnable.value,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="search-config-sidebar">
|
||||
<div class="config-header">
|
||||
<h3>{{ $t('documentCollectionSearch.title') }}</h3>
|
||||
</div>
|
||||
|
||||
<ElForm
|
||||
class="config-form"
|
||||
:model="searchConfig"
|
||||
label-width="100%"
|
||||
size="small"
|
||||
>
|
||||
<ElFormItem prop="docRecallMaxNum" class="form-item">
|
||||
<div class="form-item-label">
|
||||
<span>{{
|
||||
$t('documentCollectionSearch.docRecallMaxNum.label')
|
||||
}}</span>
|
||||
<ElTooltip
|
||||
:content="$t('documentCollectionSearch.docRecallMaxNum.tooltip')"
|
||||
placement="top"
|
||||
effect="dark"
|
||||
class="label-tooltip"
|
||||
>
|
||||
<InfoFilled class="info-icon" />
|
||||
</ElTooltip>
|
||||
</div>
|
||||
<div class="form-item-content">
|
||||
<ElInputNumber
|
||||
v-model="searchConfig.docRecallMaxNum"
|
||||
:min="1"
|
||||
:max="50"
|
||||
:step="1"
|
||||
:placeholder="$t('documentCollectionSearch.placeholder.count')"
|
||||
class="form-control"
|
||||
>
|
||||
<template #append>
|
||||
{{ $t('documentCollectionSearch.unit.count') }}
|
||||
</template>
|
||||
</ElInputNumber>
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem prop="simThreshold" class="form-item">
|
||||
<div class="form-item-label">
|
||||
<span>{{ $t('documentCollectionSearch.simThreshold.label') }}</span>
|
||||
<ElTooltip
|
||||
:content="$t('documentCollectionSearch.simThreshold.tooltip')"
|
||||
placement="top"
|
||||
effect="dark"
|
||||
class="label-tooltip"
|
||||
>
|
||||
<InfoFilled class="info-icon" />
|
||||
</ElTooltip>
|
||||
</div>
|
||||
<div class="form-item-content">
|
||||
<ElInputNumber
|
||||
v-model="searchConfig.simThreshold"
|
||||
:min="0"
|
||||
:max="1"
|
||||
:step="0.01"
|
||||
show-input
|
||||
class="form-control"
|
||||
/>
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<!-- 搜索引擎启用开关 -->
|
||||
<ElFormItem class="form-item">
|
||||
<div class="form-item-label">
|
||||
<span>{{
|
||||
$t('documentCollectionSearch.searchEngineEnable.label')
|
||||
}}</span>
|
||||
<ElTooltip
|
||||
:content="$t('documentCollectionSearch.searchEngineEnable.tooltip')"
|
||||
placement="top"
|
||||
effect="dark"
|
||||
class="label-tooltip"
|
||||
>
|
||||
<InfoFilled class="info-icon" />
|
||||
</ElTooltip>
|
||||
</div>
|
||||
<div class="form-item-content">
|
||||
<ElSwitch
|
||||
v-model="searchEngineEnable"
|
||||
@change="handleSearchEngineEnableChange"
|
||||
:active-text="$t('documentCollectionSearch.switch.on')"
|
||||
:inactive-text="$t('documentCollectionSearch.switch.off')"
|
||||
class="form-control switch-control"
|
||||
/>
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<!-- 通过 searchEngineEnable 控制显示/隐藏 -->
|
||||
<ElFormItem
|
||||
v-if="searchEngineEnable"
|
||||
prop="searchEngineType"
|
||||
class="form-item"
|
||||
>
|
||||
<div class="form-item-label">
|
||||
<span>{{
|
||||
$t('documentCollectionSearch.searchEngineType.label')
|
||||
}}</span>
|
||||
<ElTooltip
|
||||
:content="$t('documentCollectionSearch.searchEngineType.tooltip')"
|
||||
placement="top"
|
||||
effect="dark"
|
||||
class="label-tooltip"
|
||||
>
|
||||
<InfoFilled class="info-icon" />
|
||||
</ElTooltip>
|
||||
</div>
|
||||
<div class="form-item-content">
|
||||
<ElSelect
|
||||
v-model="searchConfig.searchEngineType"
|
||||
:placeholder="
|
||||
$t('documentCollectionSearch.searchEngineType.placeholder')
|
||||
"
|
||||
class="form-control"
|
||||
>
|
||||
<ElOption
|
||||
v-for="option in searchEngineOptions"
|
||||
:key="option.value"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</div>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<div class="config-footer">
|
||||
<ElButton type="primary" @click="submitConfig" class="submit-btn">
|
||||
{{ $t('documentCollectionSearch.button.save') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.search-config-sidebar {
|
||||
width: 60%;
|
||||
height: 100%;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.config-header {
|
||||
margin-bottom: 16px;
|
||||
border-bottom: 1px solid #e6e6e6;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.config-header h3 {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.config-form {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.form-item-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.label-tooltip {
|
||||
display: inline-block;
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
:deep(.form-item .el-form-item__content) {
|
||||
width: 100%;
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
.form-item-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.switch-control {
|
||||
width: auto;
|
||||
flex: none;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.info-icon {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
cursor: help;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.info-icon:hover {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.config-footer {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
:deep(.el-form-item__content) {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:deep(.el-slider) {
|
||||
--el-slider-input-width: 60px;
|
||||
}
|
||||
|
||||
:deep(.el-input-number),
|
||||
:deep(.el-select) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,251 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Document } from '@element-plus/icons-vue';
|
||||
import { ElButton, ElIcon } from 'element-plus';
|
||||
// 定义类型接口(与 React 版本一致)
|
||||
interface PreviewItem {
|
||||
sorting: string;
|
||||
content: string;
|
||||
score: string;
|
||||
}
|
||||
const props = defineProps({
|
||||
hideScore: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
data: {
|
||||
type: Array as () => PreviewItem[],
|
||||
default: () => [],
|
||||
},
|
||||
total: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
confirmImport: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
disabledConfirm: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
onCancel: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
onConfirm: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
isSearching: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
const loadingStatus = ref(false);
|
||||
defineExpose({
|
||||
loadingContent: (state: boolean) => {
|
||||
loadingStatus.value = state;
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="preview-container" v-loading="loadingStatus">
|
||||
<!-- 头部区域:标题 + 统计信息 -->
|
||||
<div class="preview-header">
|
||||
<h3>
|
||||
<ElIcon class="header-icon" size="20">
|
||||
<Document />
|
||||
</ElIcon>
|
||||
{{
|
||||
isSearching
|
||||
? $t('documentCollection.searchResults')
|
||||
: $t('documentCollection.documentPreview')
|
||||
}}
|
||||
</h3>
|
||||
<span class="preview-stats" v-if="props.data.length > 0">
|
||||
{{ $t('documentCollection.total') }}
|
||||
{{ total > 0 ? total : data.length }}
|
||||
{{ $t('documentCollection.segments') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 内容区域:列表预览 -->
|
||||
<div class="preview-content">
|
||||
<div class="preview-list">
|
||||
<div
|
||||
v-for="(item, index) in data"
|
||||
:key="index"
|
||||
class="el-list-item-container"
|
||||
>
|
||||
<div class="el-list-item">
|
||||
<div class="segment-badge">
|
||||
{{ item.sorting ?? index + 1 }}
|
||||
</div>
|
||||
<div class="el-list-item-meta">
|
||||
<div v-if="!hideScore">
|
||||
{{ $t('documentCollection.similarityScore') }}: {{ item.score }}
|
||||
</div>
|
||||
<div class="content-desc">{{ item.content }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮区域(仅导入确认模式显示) -->
|
||||
<div class="preview-actions" v-if="confirmImport">
|
||||
<div class="action-buttons">
|
||||
<ElButton
|
||||
:style="{ minWidth: '100px', height: '36px' }"
|
||||
click="onCancel"
|
||||
>
|
||||
{{ $t('documentCollection.actions.confirmImport') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
:style="{ minWidth: '100px', height: '36px' }"
|
||||
:loading="disabledConfirm"
|
||||
click="onConfirm"
|
||||
>
|
||||
{{ $t('documentCollection.actions.cancelImport') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.preview-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background-color: var(--el-bg-color);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px 0 rgb(0 0 0 / 8%);
|
||||
|
||||
.preview-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
|
||||
h3 {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-primary);
|
||||
|
||||
.header-icon {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.preview-stats {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
|
||||
.preview-list {
|
||||
.segment-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--el-color-primary);
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.similarity-score {
|
||||
font-size: 14px;
|
||||
color: var(--el-color-primary);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.content-desc {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
background-color: var(--el-bg-color);
|
||||
border-left: 3px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #4361ee;
|
||||
box-shadow: 0 4px 12px rgb(67 97 238 / 8%);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
.el-list-item {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
padding: 18px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.el-list-item-meta {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.preview-actions {
|
||||
padding: 16px 20px;
|
||||
background-color: var(--el-bg-color-page);
|
||||
border-top: 1px solid var(--el-border-color);
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 适配 Element Plus 加载状态样式 */
|
||||
.el-list--loading .el-list-loading {
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
.el-list-item {
|
||||
width: 100%;
|
||||
margin-top: 12px;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,189 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue';
|
||||
|
||||
import { $t } from '@easyflow/locales';
|
||||
|
||||
import {
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
ElSlider,
|
||||
} from 'element-plus';
|
||||
|
||||
const formRef = ref();
|
||||
const form = reactive({
|
||||
fileType: 'doc',
|
||||
splitterName: 'SimpleDocumentSplitter',
|
||||
chunkSize: 512,
|
||||
overlapSize: 128,
|
||||
regex: '',
|
||||
rowsPerChunk: 0,
|
||||
mdSplitterLevel: 1,
|
||||
});
|
||||
const fileTypes = [
|
||||
{
|
||||
label: $t('documentCollection.splitterDoc.document'),
|
||||
value: 'doc',
|
||||
},
|
||||
];
|
||||
const splitterNames = [
|
||||
{
|
||||
label: $t('documentCollection.splitterDoc.simpleDocumentSplitter'),
|
||||
value: 'SimpleDocumentSplitter',
|
||||
},
|
||||
{
|
||||
label: $t('documentCollection.splitterDoc.simpleTokenizeSplitter'),
|
||||
value: 'SimpleTokenizeSplitter',
|
||||
},
|
||||
{
|
||||
label: $t('documentCollection.splitterDoc.regexDocumentSplitter'),
|
||||
value: 'RegexDocumentSplitter',
|
||||
},
|
||||
{
|
||||
label: $t('documentCollection.splitterDoc.markdownHeaderSplitter'),
|
||||
value: 'MarkdownHeaderSplitter',
|
||||
},
|
||||
];
|
||||
const mdSplitterLevel = [
|
||||
{
|
||||
label: '#',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: '##',
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
label: '###',
|
||||
value: 3,
|
||||
},
|
||||
{
|
||||
label: '####',
|
||||
value: 4,
|
||||
},
|
||||
{
|
||||
label: '#####',
|
||||
value: 5,
|
||||
},
|
||||
{
|
||||
label: '######',
|
||||
value: 6,
|
||||
},
|
||||
];
|
||||
const rules = {
|
||||
name: [
|
||||
{ required: true, message: 'Please input Activity name', trigger: 'blur' },
|
||||
],
|
||||
region: [
|
||||
{
|
||||
required: true,
|
||||
message: 'Please select Activity zone',
|
||||
trigger: 'change',
|
||||
},
|
||||
],
|
||||
};
|
||||
defineExpose({
|
||||
getSplitterFormValues() {
|
||||
return form;
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="splitter-doc-container">
|
||||
<ElForm
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-width="auto"
|
||||
class="custom-form"
|
||||
>
|
||||
<ElFormItem
|
||||
:label="$t('documentCollection.splitterDoc.fileType')"
|
||||
prop="fileType"
|
||||
>
|
||||
<ElSelect v-model="form.fileType">
|
||||
<ElOption
|
||||
v-for="item in fileTypes"
|
||||
:key="item.value"
|
||||
v-bind="item"
|
||||
:label="item.label"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
:label="$t('documentCollection.splitterDoc.splitterName')"
|
||||
prop="splitterName"
|
||||
>
|
||||
<ElSelect v-model="form.splitterName">
|
||||
<ElOption
|
||||
v-for="item in splitterNames"
|
||||
:key="item.value"
|
||||
v-bind="item"
|
||||
:label="item.label"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
:label="$t('documentCollection.splitterDoc.chunkSize')"
|
||||
v-if="
|
||||
form.splitterName === 'SimpleDocumentSplitter' ||
|
||||
form.splitterName === 'SimpleTokenizeSplitter'
|
||||
"
|
||||
prop="chunkSize"
|
||||
>
|
||||
<ElSlider v-model="form.chunkSize" show-input :max="2048" />
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
:label="$t('documentCollection.splitterDoc.overlapSize')"
|
||||
v-if="
|
||||
form.splitterName === 'SimpleDocumentSplitter' ||
|
||||
form.splitterName === 'SimpleTokenizeSplitter'
|
||||
"
|
||||
prop="overlapSize"
|
||||
>
|
||||
<ElSlider v-model="form.overlapSize" show-input :max="2048" />
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
:label="$t('documentCollection.splitterDoc.regex')"
|
||||
prop="regex"
|
||||
v-if="form.splitterName === 'RegexDocumentSplitter'"
|
||||
>
|
||||
<ElInput v-model="form.regex" />
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
v-if="form.splitterName === 'MarkdownHeaderSplitter'"
|
||||
:label="$t('documentCollection.splitterDoc.mdSplitterLevel')"
|
||||
prop="splitterName"
|
||||
>
|
||||
<ElSelect v-model="form.mdSplitterLevel">
|
||||
<ElOption
|
||||
v-for="item in mdSplitterLevel"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.splitter-doc-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.custom-form {
|
||||
width: 500px;
|
||||
}
|
||||
.custom-form :deep(.el-input),
|
||||
.custom-form :deep(.ElSelect) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,168 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import CategoryPanel from '#/components/categoryPanel/CategoryPanel.vue';
|
||||
import PreviewSearchKnowledge from '#/views/ai/documentCollection/PreviewSearchKnowledge.vue';
|
||||
|
||||
export interface FileInfo {
|
||||
filePath: string;
|
||||
fileName: string;
|
||||
}
|
||||
const props = defineProps({
|
||||
pageNumber: {
|
||||
default: 1,
|
||||
type: Number,
|
||||
},
|
||||
pageSize: {
|
||||
default: 10,
|
||||
type: Number,
|
||||
},
|
||||
knowledgeId: {
|
||||
default: '',
|
||||
type: String,
|
||||
},
|
||||
fliesList: {
|
||||
default: () => [],
|
||||
type: Array<FileInfo>,
|
||||
},
|
||||
splitterParams: {
|
||||
default: () => {},
|
||||
type: Object,
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['updateTotal']);
|
||||
const documentList = ref<any[]>([]);
|
||||
const route = useRoute();
|
||||
defineExpose({
|
||||
getFilesData() {
|
||||
return documentList.value.length;
|
||||
},
|
||||
});
|
||||
const knowledgeIdRef = ref<string>((route.query.id as string) || '');
|
||||
const selectedCategory = ref<any>();
|
||||
|
||||
watch(
|
||||
() => props.pageNumber,
|
||||
(newVal) => {
|
||||
if (selectedCategory.value) {
|
||||
splitterDocPreview(
|
||||
newVal,
|
||||
props.pageSize,
|
||||
selectedCategory.value.value,
|
||||
'textSplit',
|
||||
selectedCategory.value.label,
|
||||
);
|
||||
} else {
|
||||
splitterDocPreview(
|
||||
newVal,
|
||||
props.pageSize,
|
||||
props.fliesList[0]!.filePath,
|
||||
'textSplit',
|
||||
props.fliesList[0]!.fileName,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.pageSize,
|
||||
(newVal) => {
|
||||
if (selectedCategory.value) {
|
||||
splitterDocPreview(
|
||||
props.pageNumber,
|
||||
newVal,
|
||||
selectedCategory.value.value,
|
||||
'textSplit',
|
||||
selectedCategory.value.label,
|
||||
);
|
||||
} else {
|
||||
splitterDocPreview(
|
||||
props.pageNumber,
|
||||
newVal,
|
||||
props.fliesList[0]!.filePath,
|
||||
'textSplit',
|
||||
props.fliesList[0]!.fileName,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
function splitterDocPreview(
|
||||
pageNumber: number,
|
||||
pageSize: number,
|
||||
filePath: string,
|
||||
operation: string,
|
||||
fileOriginName: string,
|
||||
) {
|
||||
api
|
||||
.post('/api/v1/document/textSplit', {
|
||||
pageNumber,
|
||||
pageSize,
|
||||
filePath,
|
||||
operation,
|
||||
knowledgeId: knowledgeIdRef.value,
|
||||
fileOriginName,
|
||||
...props.splitterParams,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
documentList.value = res.data.previewData;
|
||||
emit('updateTotal', res.data.total);
|
||||
}
|
||||
});
|
||||
}
|
||||
onMounted(() => {
|
||||
if (props.fliesList.length === 0) {
|
||||
return;
|
||||
}
|
||||
splitterDocPreview(
|
||||
props.pageNumber,
|
||||
props.pageSize,
|
||||
props.fliesList[0]!.filePath,
|
||||
'textSplit',
|
||||
props.fliesList[0]!.fileName,
|
||||
);
|
||||
});
|
||||
const changeCategory = (category: any) => {
|
||||
selectedCategory.value = category;
|
||||
splitterDocPreview(
|
||||
props.pageNumber,
|
||||
props.pageSize,
|
||||
category.value,
|
||||
'textSplit',
|
||||
category.label,
|
||||
);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="splitter-doc-container">
|
||||
<div>
|
||||
<CategoryPanel
|
||||
:categories="fliesList"
|
||||
title-key="fileName"
|
||||
:need-hide-collapse="true"
|
||||
:expand-width="200"
|
||||
value-key="filePath"
|
||||
:default-selected-category="fliesList[0]!.filePath"
|
||||
@click="changeCategory"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="preview-container">
|
||||
<PreviewSearchKnowledge :data="documentList" :hide-score="true" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.splitter-doc-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
}
|
||||
.preview-container {
|
||||
flex: 1;
|
||||
overflow: scroll;
|
||||
}
|
||||
</style>
|
||||
205
easyflow-ui-admin/app/src/views/ai/mcp/Mcp.vue
Normal file
205
easyflow-ui-admin/app/src/views/ai/mcp/Mcp.vue
Normal file
@@ -0,0 +1,205 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from 'element-plus';
|
||||
|
||||
import { markRaw, ref } from 'vue';
|
||||
|
||||
import { Delete, MoreFilled, Plus, Refresh } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElButton,
|
||||
ElDropdown,
|
||||
ElDropdownItem,
|
||||
ElDropdownMenu,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElSwitch,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
ElTooltip,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
||||
import PageData from '#/components/page/PageData.vue';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import McpModal from './McpModal.vue';
|
||||
|
||||
const formRef = ref<FormInstance>();
|
||||
const pageDataRef = ref();
|
||||
const saveDialog = ref();
|
||||
function reset(formEl: FormInstance | undefined) {
|
||||
formEl?.resetFields();
|
||||
pageDataRef.value.setQuery({});
|
||||
}
|
||||
function showDialog(row: any) {
|
||||
saveDialog.value.openDialog({ ...row });
|
||||
}
|
||||
function remove(row: any) {
|
||||
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
|
||||
confirmButtonText: $t('message.ok'),
|
||||
cancelButtonText: $t('message.cancel'),
|
||||
type: 'warning',
|
||||
beforeClose: (action, instance, done) => {
|
||||
if (action === 'confirm') {
|
||||
instance.confirmButtonLoading = true;
|
||||
api
|
||||
.post('/api/v1/mcp/remove', { id: row.id })
|
||||
.then((res) => {
|
||||
instance.confirmButtonLoading = false;
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message);
|
||||
reset(formRef.value);
|
||||
done();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
instance.confirmButtonLoading = false;
|
||||
});
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
},
|
||||
}).catch(() => {});
|
||||
}
|
||||
const handleUpdate = (row: any, isRefresh: boolean) => {
|
||||
if (isRefresh) {
|
||||
refreshLoadingMap.value[row.id] = true;
|
||||
} else {
|
||||
loadingMap.value[row.id] = true;
|
||||
}
|
||||
api.post('/api/v1/mcp/update', { ...row }).then((res) => {
|
||||
if (isRefresh) {
|
||||
refreshLoadingMap.value[row.id] = false;
|
||||
} else {
|
||||
loadingMap.value[row.id] = false;
|
||||
}
|
||||
if (res.errorCode === 0) {
|
||||
if (row.status) {
|
||||
ElMessage.success($t('mcp.message.startupSuccessful'));
|
||||
} else {
|
||||
ElMessage.success($t('mcp.message.stopSuccessful'));
|
||||
}
|
||||
}
|
||||
pageDataRef.value.setQuery({});
|
||||
});
|
||||
};
|
||||
const headerButtons = [
|
||||
{
|
||||
key: 'create',
|
||||
type: 'primary',
|
||||
text: $t('button.add'),
|
||||
icon: markRaw(Plus),
|
||||
data: { action: 'create' },
|
||||
},
|
||||
];
|
||||
const handleSearch = (params: string) => {
|
||||
pageDataRef.value.setQuery({ packageName: params, isQueryOr: true });
|
||||
};
|
||||
const handleHeaderButtonClick = (button: any) => {
|
||||
if (button.key === 'create') {
|
||||
showDialog({});
|
||||
}
|
||||
};
|
||||
const loadingMap = ref<Record<number | string, boolean>>({});
|
||||
const refreshLoadingMap = ref<Record<number | string, boolean>>({});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full flex-col gap-6 p-6">
|
||||
<McpModal ref="saveDialog" @reload="reset" />
|
||||
<HeaderSearch
|
||||
:buttons="headerButtons"
|
||||
@search="handleSearch"
|
||||
@button-click="handleHeaderButtonClick"
|
||||
/>
|
||||
<div class="bg-background border-border flex-1 rounded-lg border p-5">
|
||||
<PageData ref="pageDataRef" page-url="/api/v1/mcp/page" :page-size="10">
|
||||
<template #default="{ pageList }">
|
||||
<ElTable :data="pageList" border>
|
||||
<ElTableColumn prop="title" :label="$t('mcp.title')">
|
||||
<template #default="{ row }">
|
||||
<ElTooltip
|
||||
:content="
|
||||
row.clientOnline
|
||||
? $t('mcp.labels.clientOnline')
|
||||
: $t('mcp.labels.clientOffline')
|
||||
"
|
||||
placement="top"
|
||||
>
|
||||
<span
|
||||
class="mr-2 inline-block h-2 w-2 rounded-full"
|
||||
:class="row.clientOnline ? 'bg-green-500' : 'bg-red-500'"
|
||||
></span>
|
||||
</ElTooltip>
|
||||
{{ row.title }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="description" :label="$t('mcp.description')">
|
||||
<template #default="{ row }">
|
||||
{{ row.description }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="created" :label="$t('mcp.created')">
|
||||
<template #default="{ row }">
|
||||
{{ row.created }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="status" :label="$t('mcp.status')">
|
||||
<template #default="{ row }">
|
||||
<ElSwitch
|
||||
v-model="row.status"
|
||||
@change="() => handleUpdate(row, false)"
|
||||
:loading="loadingMap[row.id]"
|
||||
:disabled="loadingMap[row.id]"
|
||||
/>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
:label="$t('common.handle')"
|
||||
width="150"
|
||||
align="right"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="flex items-center gap-3">
|
||||
<div v-access:code="'/api/v1/mcp/save'">
|
||||
<ElButton
|
||||
@click="handleUpdate({ ...row, status: true }, true)"
|
||||
type="primary"
|
||||
link
|
||||
:icon="Refresh"
|
||||
:loading="refreshLoadingMap[row.id]"
|
||||
>
|
||||
{{ $t('重启') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
<div v-access:code="'/api/v1/mcp/save'">
|
||||
<ElButton type="primary" link @click="showDialog(row)">
|
||||
{{ $t('button.edit') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<ElDropdown>
|
||||
<ElButton link :icon="MoreFilled" />
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<div v-access:code="'/api/v1/mcp/remove'">
|
||||
<ElDropdownItem @click="remove(row)">
|
||||
<ElButton type="danger" :icon="Delete" link>
|
||||
{{ $t('button.delete') }}
|
||||
</ElButton>
|
||||
</ElDropdownItem>
|
||||
</div>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</template>
|
||||
</PageData>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
338
easyflow-ui-admin/app/src/views/ai/mcp/McpModal.vue
Normal file
338
easyflow-ui-admin/app/src/views/ai/mcp/McpModal.vue
Normal file
@@ -0,0 +1,338 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from 'element-plus';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElDialog,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
ElSwitch,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
ElTabPane,
|
||||
ElTabs,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
interface PropValue {
|
||||
type?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface McpTool {
|
||||
name: string;
|
||||
description: string;
|
||||
status: boolean;
|
||||
inputSchema?: {
|
||||
properties: Record<string, PropValue>;
|
||||
required?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
interface McpEntity {
|
||||
id?: string;
|
||||
title: string;
|
||||
description: string;
|
||||
configJson: string;
|
||||
deptId: string;
|
||||
status: boolean;
|
||||
tools: McpTool[];
|
||||
}
|
||||
|
||||
const emit = defineEmits(['reload']);
|
||||
|
||||
onMounted(() => {});
|
||||
defineExpose({
|
||||
openDialog,
|
||||
});
|
||||
|
||||
const saveForm = ref<FormInstance>();
|
||||
const dialogVisible = ref(false);
|
||||
const isAdd = ref(true);
|
||||
const btnLoading = ref(false);
|
||||
|
||||
const defaultEntity: McpEntity = {
|
||||
title: '',
|
||||
description: '',
|
||||
configJson: '',
|
||||
deptId: '',
|
||||
status: false,
|
||||
tools: [],
|
||||
};
|
||||
const entity = ref<McpEntity>({ ...defaultEntity });
|
||||
|
||||
const rules = ref({
|
||||
title: [
|
||||
{
|
||||
required: true,
|
||||
message: $t('message.required'),
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
configJson: [
|
||||
{
|
||||
required: true,
|
||||
message: $t('message.required'),
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
function openDialog(row: Partial<McpEntity> = {}) {
|
||||
isAdd.value = !row.id;
|
||||
entity.value = { ...defaultEntity, ...row };
|
||||
if (!isAdd.value) {
|
||||
getMcpTools(row);
|
||||
}
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function getMcpTools(row: Partial<McpEntity>) {
|
||||
api.post('api/v1/mcp/getMcpTools', { id: row.id }).then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
entity.value.tools = res.data.tools;
|
||||
}
|
||||
});
|
||||
}
|
||||
function save() {
|
||||
saveForm.value?.validate((valid) => {
|
||||
if (valid) {
|
||||
btnLoading.value = true;
|
||||
api
|
||||
.post(
|
||||
isAdd.value ? 'api/v1/mcp/save' : 'api/v1/mcp/update',
|
||||
entity.value,
|
||||
)
|
||||
.then((res) => {
|
||||
btnLoading.value = false;
|
||||
if (res.errorCode === 0) {
|
||||
if (isAdd.value) {
|
||||
ElMessage.success($t('message.saveOkMessage'));
|
||||
} else {
|
||||
ElMessage.success($t('message.updateOkMessage'));
|
||||
}
|
||||
emit('reload');
|
||||
closeDialog();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
btnLoading.value = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
saveForm.value?.resetFields();
|
||||
isAdd.value = true;
|
||||
entity.value = { ...defaultEntity };
|
||||
dialogVisible.value = false;
|
||||
}
|
||||
const jsonPlaceholder = ref(`{
|
||||
"mcpServers": {
|
||||
"12306-mcp": {
|
||||
"command": "npx.cmd",
|
||||
"args": [
|
||||
"-y",
|
||||
"12306-mcp"
|
||||
]
|
||||
}
|
||||
}
|
||||
}`);
|
||||
const activeName = ref('config');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
draggable
|
||||
:title="isAdd ? $t('button.add') : $t('button.edit')"
|
||||
:before-close="closeDialog"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<ElTabs v-model="activeName" class="demo-tabs">
|
||||
<ElTabPane :label="$t('mcp.modal.config')" name="config">
|
||||
<ElForm
|
||||
label-width="120px"
|
||||
ref="saveForm"
|
||||
:model="entity"
|
||||
status-icon
|
||||
:rules="rules"
|
||||
>
|
||||
<ElFormItem prop="title" :label="$t('mcp.title')">
|
||||
<ElInput v-model.trim="entity.title" />
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="description" :label="$t('mcp.description')">
|
||||
<ElInput v-model.trim="entity.description" />
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="configJson" :label="$t('mcp.configJson')">
|
||||
<ElInput
|
||||
type="textarea"
|
||||
:rows="15"
|
||||
v-model.trim="entity.configJson"
|
||||
:placeholder="$t('mcp.example') + jsonPlaceholder" />
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="status" :label="$t('mcp.status')">
|
||||
<ElSwitch v-model="entity.status" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</ElTabPane>
|
||||
<div v-if="!isAdd">
|
||||
<ElTabPane :label="$t('mcp.modal.tool')" name="tool">
|
||||
<ElTable
|
||||
:data="entity.tools"
|
||||
border
|
||||
:preserve-expanded-content="true"
|
||||
>
|
||||
<ElTableColumn type="expand">
|
||||
<template #default="scope">
|
||||
<!-- 解构获取properties和required,同时做空值保护 -->
|
||||
<div
|
||||
v-if="scope.row?.inputSchema?.properties"
|
||||
class="params-list"
|
||||
>
|
||||
<div
|
||||
v-for="([propKey, propValue], index) in Object.entries(
|
||||
scope.row.inputSchema.properties,
|
||||
)"
|
||||
:key="index"
|
||||
class="params-content-container"
|
||||
>
|
||||
<div class="params-left-title-container">
|
||||
<div class="content-title">
|
||||
{{ propKey }}
|
||||
<span
|
||||
v-if="
|
||||
scope.row.inputSchema.required &&
|
||||
scope.row.inputSchema.required.includes(propKey)
|
||||
"
|
||||
class="required-mark"
|
||||
>
|
||||
*
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="params-desc-container">
|
||||
<div class="content-title">
|
||||
{{ (propValue as PropValue).type || '未知类型' }}
|
||||
</div>
|
||||
<div class="content-desc">
|
||||
{{ (propValue as PropValue).description || '无描述' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="params-name">暂无属性配置</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
|
||||
<ElTableColumn :label="$t('mcp.modal.table.availableTools')">
|
||||
<template #default="{ row }">
|
||||
<div class="content-left">
|
||||
<span class="content-title">{{ row.name }}</span>
|
||||
<span class="content-desc">{{ row.description }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<!-- <ElTableColumn :label="$t('mcp.status')">
|
||||
<template #default="{ row }">
|
||||
<ElSwitch v-model="row.status" />
|
||||
</template>
|
||||
</ElTableColumn>-->
|
||||
</ElTable>
|
||||
</ElTabPane>
|
||||
</div>
|
||||
</ElTabs>
|
||||
<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>
|
||||
.content-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.content-title {
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
line-height: 24px;
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
text-transform: none;
|
||||
}
|
||||
.content-desc {
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
line-height: 22px;
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
text-transform: none;
|
||||
}
|
||||
.params-name {
|
||||
flex: 1;
|
||||
background-color: #fafafa;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid #e6e9ee;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
}
|
||||
.params-content-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
}
|
||||
.params-desc-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
flex: 1;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e6e9ee;
|
||||
padding: 8px;
|
||||
}
|
||||
.params-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.params-left-title-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
background-color: #fafafa;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
border: 1px solid #e6e9ee;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
.required-mark {
|
||||
color: #f56c6c;
|
||||
margin-left: 2px;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
377
easyflow-ui-admin/app/src/views/ai/model/AddModelModal.vue
Normal file
377
easyflow-ui-admin/app/src/views/ai/model/AddModelModal.vue
Normal file
@@ -0,0 +1,377 @@
|
||||
<script setup lang="ts">
|
||||
import type { ModelAbilityItem } from '#/views/ai/model/modelUtils/model-ability';
|
||||
|
||||
import { reactive, ref, watch } from 'vue';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElDialog,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
ElTag,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import { $t } from '#/locales';
|
||||
import {
|
||||
getDefaultModelAbility,
|
||||
handleTagClick as handleTagClickUtil,
|
||||
syncTagSelectedStatus as syncTagSelectedStatusUtil,
|
||||
} from '#/views/ai/model/modelUtils/model-ability';
|
||||
import {
|
||||
generateFeaturesFromModelAbility,
|
||||
resetModelAbility,
|
||||
} from '#/views/ai/model/modelUtils/model-ability-utils';
|
||||
|
||||
interface FormData {
|
||||
modelType: string;
|
||||
title: string;
|
||||
modelName: string;
|
||||
groupName: string;
|
||||
providerId: string;
|
||||
provider: string;
|
||||
apiKey: string;
|
||||
endpoint: string;
|
||||
requestPath: string;
|
||||
supportThinking: boolean;
|
||||
supportTool: boolean;
|
||||
supportImage: boolean;
|
||||
supportAudio: boolean;
|
||||
supportFree: boolean;
|
||||
supportVideo: boolean;
|
||||
supportImageB64Only: boolean;
|
||||
supportToolMessage: boolean;
|
||||
options: {
|
||||
chatPath: string;
|
||||
embedPath: string;
|
||||
llmEndpoint: string;
|
||||
rerankPath: string;
|
||||
};
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
providerId: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['reload']);
|
||||
const selectedProviderId = ref<string>(props.providerId ?? '');
|
||||
|
||||
// 监听 providerId 的变化
|
||||
watch(
|
||||
() => props.providerId,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
selectedProviderId.value = newVal;
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const formDataRef = ref();
|
||||
const isAdd = ref(true);
|
||||
const dialogVisible = ref(false);
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive<FormData>({
|
||||
modelType: '',
|
||||
title: '',
|
||||
modelName: '',
|
||||
groupName: '',
|
||||
providerId: '',
|
||||
provider: '',
|
||||
apiKey: '',
|
||||
endpoint: '',
|
||||
requestPath: '',
|
||||
supportThinking: false,
|
||||
supportTool: false,
|
||||
supportImage: false,
|
||||
supportAudio: false,
|
||||
supportFree: false,
|
||||
supportVideo: false,
|
||||
supportImageB64Only: false,
|
||||
supportToolMessage: false,
|
||||
options: {
|
||||
llmEndpoint: '',
|
||||
chatPath: '',
|
||||
embedPath: '',
|
||||
rerankPath: '',
|
||||
},
|
||||
});
|
||||
|
||||
// 使用抽取的函数获取模型能力配置
|
||||
const modelAbility = ref<ModelAbilityItem[]>(getDefaultModelAbility());
|
||||
|
||||
/**
|
||||
* 同步标签选中状态与formData中的布尔字段
|
||||
*/
|
||||
const syncTagSelectedStatus = () => {
|
||||
syncTagSelectedStatusUtil(modelAbility.value, formData);
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理标签点击事件
|
||||
*/
|
||||
const handleTagClick = (item: ModelAbilityItem) => {
|
||||
// handleTagClickUtil(modelAbility.value, item, formData);
|
||||
handleTagClickUtil(item, formData);
|
||||
};
|
||||
|
||||
// 打开新增弹窗
|
||||
defineExpose({
|
||||
openAddDialog(modelType: string) {
|
||||
isAdd.value = true;
|
||||
if (formDataRef.value) {
|
||||
formDataRef.value.resetFields();
|
||||
}
|
||||
|
||||
// 重置表单数据
|
||||
Object.assign(formData, {
|
||||
id: '',
|
||||
modelType,
|
||||
title: '',
|
||||
modelName: '',
|
||||
groupName: '',
|
||||
provider: '',
|
||||
endPoint: '',
|
||||
providerId: '',
|
||||
supportThinking: false,
|
||||
supportTool: false,
|
||||
supportAudio: false,
|
||||
supportVideo: false,
|
||||
supportImage: false,
|
||||
supportImageB64Only: false,
|
||||
supportFree: false,
|
||||
supportToolMessage: true,
|
||||
options: {
|
||||
llmEndpoint: '',
|
||||
chatPath: '',
|
||||
embedPath: '',
|
||||
rerankPath: '',
|
||||
},
|
||||
});
|
||||
showMoreFields.value = false;
|
||||
// 重置标签状态
|
||||
resetModelAbility(modelAbility.value);
|
||||
syncTagSelectedStatus();
|
||||
dialogVisible.value = true;
|
||||
},
|
||||
|
||||
openEditDialog(item: any) {
|
||||
dialogVisible.value = true;
|
||||
isAdd.value = false;
|
||||
|
||||
// 填充表单数据
|
||||
Object.assign(formData, {
|
||||
id: item.id,
|
||||
modelType: item.modelType || '',
|
||||
title: item.title || '',
|
||||
modelName: item.modelName || '',
|
||||
groupName: item.groupName || '',
|
||||
provider: item.provider || '',
|
||||
endpoint: item.endpoint || '',
|
||||
requestPath: item.requestPath || '',
|
||||
supportThinking: item.supportThinking || false,
|
||||
supportAudio: item.supportAudio || false,
|
||||
supportImage: item.supportImage || false,
|
||||
supportImageB64Only: item.supportImageB64Only || false,
|
||||
supportVideo: item.supportVideo || false,
|
||||
supportTool: item.supportTool || false,
|
||||
supportFree: item.supportFree || false,
|
||||
supportToolMessage: item.supportToolMessage || false,
|
||||
options: {
|
||||
llmEndpoint: item.options?.llmEndpoint || '',
|
||||
chatPath: item.options?.chatPath || '',
|
||||
embedPath: item.options?.embedPath || '',
|
||||
rerankPath: item.options?.rerankPath || '',
|
||||
},
|
||||
});
|
||||
showMoreFields.value = false;
|
||||
// 同步标签状态
|
||||
syncTagSelectedStatus();
|
||||
},
|
||||
});
|
||||
|
||||
const closeDialog = () => {
|
||||
dialogVisible.value = false;
|
||||
};
|
||||
|
||||
const rules = {
|
||||
title: [
|
||||
{
|
||||
required: true,
|
||||
message: $t('message.required'),
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
modelName: [
|
||||
{
|
||||
required: true,
|
||||
message: $t('message.required'),
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
groupName: [
|
||||
{
|
||||
required: true,
|
||||
message: $t('message.required'),
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
provider: [
|
||||
{
|
||||
required: true,
|
||||
message: $t('message.required'),
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const btnLoading = ref(false);
|
||||
|
||||
const save = async () => {
|
||||
btnLoading.value = true;
|
||||
|
||||
// 使用工具函数从模型能力生成features
|
||||
const features = generateFeaturesFromModelAbility(modelAbility.value);
|
||||
|
||||
try {
|
||||
await formDataRef.value.validate();
|
||||
const submitData = { ...formData, ...features };
|
||||
|
||||
if (isAdd.value) {
|
||||
submitData.providerId = selectedProviderId.value;
|
||||
const res = await api.post('/api/v1/model/save', submitData);
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message);
|
||||
emit('reload');
|
||||
closeDialog();
|
||||
} else {
|
||||
ElMessage.error(res.message || $t('ui.actionMessage.operationFailed'));
|
||||
}
|
||||
} else {
|
||||
const res = await api.post('/api/v1/model/update', submitData);
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message);
|
||||
emit('reload');
|
||||
closeDialog();
|
||||
} else {
|
||||
ElMessage.error(res.message || $t('ui.actionMessage.operationFailed'));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Save model error:', error);
|
||||
ElMessage.error($t('ui.actionMessage.operationFailed'));
|
||||
} finally {
|
||||
btnLoading.value = false;
|
||||
}
|
||||
};
|
||||
const showMoreFields = ref(false);
|
||||
</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
|
||||
width="482"
|
||||
>
|
||||
<ElForm
|
||||
label-width="100px"
|
||||
ref="formDataRef"
|
||||
:model="formData"
|
||||
status-icon
|
||||
:rules="rules"
|
||||
>
|
||||
<ElFormItem prop="title" :label="$t('llm.title')">
|
||||
<ElInput v-model.trim="formData.title" />
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="modelName" :label="$t('llm.llmModel')">
|
||||
<ElInput v-model.trim="formData.modelName" />
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="groupName" :label="$t('llm.groupName')">
|
||||
<ElInput v-model.trim="formData.groupName" />
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="ability" :label="$t('llm.ability')">
|
||||
<div class="model-ability">
|
||||
<ElTag
|
||||
class="model-ability-tag"
|
||||
v-for="item in modelAbility"
|
||||
:key="item.value"
|
||||
:type="item.selected ? item.activeType : item.defaultType"
|
||||
@click="handleTagClick(item)"
|
||||
:class="{ 'tag-selected': item.selected }"
|
||||
>
|
||||
{{ item.label }}
|
||||
</ElTag>
|
||||
</div>
|
||||
</ElFormItem>
|
||||
<ElFormItem label=" " v-if="!showMoreFields">
|
||||
<ElButton @click="showMoreFields = !showMoreFields" type="primary">
|
||||
{{ showMoreFields ? $t('button.hide') : $t('button.more') }}
|
||||
</ElButton>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
prop="apiKey"
|
||||
:label="$t('llmProvider.apiKey')"
|
||||
v-show="showMoreFields"
|
||||
>
|
||||
<ElInput v-model.trim="formData.apiKey" />
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
prop="endpoint"
|
||||
:label="$t('llmProvider.endpoint')"
|
||||
v-show="showMoreFields"
|
||||
>
|
||||
<ElInput v-model.trim="formData.endpoint" />
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
prop="requestPath"
|
||||
:label="$t('llm.requestPath')"
|
||||
v-show="showMoreFields"
|
||||
>
|
||||
<ElInput v-model.trim="formData.requestPath" />
|
||||
</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>
|
||||
.model-ability {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.model-ability-tag {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.tag-selected {
|
||||
font-weight: bold;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,197 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElDialog,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import UploadAvatar from '#/components/upload/UploadAvatar.vue';
|
||||
import { $t } from '#/locales';
|
||||
import providerList from '#/views/ai/model/modelUtils/providerList.json';
|
||||
|
||||
const emit = defineEmits(['reload']);
|
||||
|
||||
const formDataRef = ref();
|
||||
|
||||
defineExpose({
|
||||
openAddDialog() {
|
||||
formDataRef.value?.resetFields();
|
||||
dialogVisible.value = true;
|
||||
},
|
||||
openEditDialog(item: any) {
|
||||
dialogVisible.value = true;
|
||||
isAdd.value = false;
|
||||
Object.assign(formData, item);
|
||||
},
|
||||
});
|
||||
const providerOptions =
|
||||
ref<Array<{ label: string; options: any; value: string }>>(providerList);
|
||||
const isAdd = ref(true);
|
||||
const dialogVisible = ref(false);
|
||||
const formData = reactive({
|
||||
id: '',
|
||||
icon: '',
|
||||
providerName: '',
|
||||
providerType: '',
|
||||
apiKey: '',
|
||||
endpoint: '',
|
||||
chatPath: '',
|
||||
embedPath: '',
|
||||
rerankPath: '',
|
||||
});
|
||||
const closeDialog = () => {
|
||||
dialogVisible.value = false;
|
||||
};
|
||||
const rules = {
|
||||
providerName: [
|
||||
{
|
||||
required: true,
|
||||
message: $t('message.required'),
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
providerType: [
|
||||
{
|
||||
required: true,
|
||||
message: $t('message.required'),
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
};
|
||||
const btnLoading = ref(false);
|
||||
const save = async () => {
|
||||
btnLoading.value = true;
|
||||
try {
|
||||
if (!isAdd.value) {
|
||||
api.post('/api/v1/modelProvider/update', formData).then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message);
|
||||
emit('reload');
|
||||
closeDialog();
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
await formDataRef.value.validate();
|
||||
api.post('/api/v1/modelProvider/save', formData).then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message);
|
||||
emit('reload');
|
||||
closeDialog();
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
btnLoading.value = false;
|
||||
}
|
||||
};
|
||||
const handleChangeProvider = (val: string) => {
|
||||
const tempProvider = providerList.find((item) => item.value === val);
|
||||
if (!tempProvider) {
|
||||
return;
|
||||
}
|
||||
formData.providerName = tempProvider.label;
|
||||
formData.endpoint = providerOptions.value.find(
|
||||
(item) => item.value === val,
|
||||
)?.options.llmEndpoint;
|
||||
formData.chatPath = providerOptions.value.find(
|
||||
(item) => item.value === val,
|
||||
)?.options.chatPath;
|
||||
formData.embedPath = providerOptions.value.find(
|
||||
(item) => item.value === val,
|
||||
)?.options.embedPath;
|
||||
};
|
||||
</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
|
||||
width="482"
|
||||
>
|
||||
<ElForm
|
||||
label-width="100px"
|
||||
ref="formDataRef"
|
||||
:model="formData"
|
||||
status-icon
|
||||
:rules="rules"
|
||||
>
|
||||
<ElFormItem
|
||||
prop="icon"
|
||||
style="display: flex; align-items: center"
|
||||
:label="$t('llmProvider.icon')"
|
||||
>
|
||||
<UploadAvatar v-model="formData.icon" />
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="providerName" :label="$t('llmProvider.providerName')">
|
||||
<ElInput v-model.trim="formData.providerName" />
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="provider" :label="$t('llmProvider.apiType')">
|
||||
<ElSelect v-model="formData.providerType" @change="handleChangeProvider">
|
||||
<ElOption
|
||||
v-for="item in providerOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value || ''"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="apiKey" :label="$t('llmProvider.apiKey')">
|
||||
<ElInput v-model.trim="formData.apiKey" />
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="endpoint" :label="$t('llmProvider.endpoint')">
|
||||
<ElInput v-model.trim="formData.endpoint" />
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="chatPath" :label="$t('llmProvider.chatPath')">
|
||||
<ElInput v-model.trim="formData.chatPath" />
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="rerankPath" :label="$t('llmProvider.rerankPath')">
|
||||
<ElInput v-model.trim="formData.rerankPath" />
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="embedPath" :label="$t('llmProvider.embedPath')">
|
||||
<ElInput v-model.trim="formData.embedPath" />
|
||||
</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>
|
||||
350
easyflow-ui-admin/app/src/views/ai/model/ManageModelModal.vue
Normal file
350
easyflow-ui-admin/app/src/views/ai/model/ManageModelModal.vue
Normal file
@@ -0,0 +1,350 @@
|
||||
<script setup lang="ts">
|
||||
import { nextTick, reactive, ref } from 'vue';
|
||||
|
||||
import {
|
||||
CirclePlus,
|
||||
Loading,
|
||||
Minus,
|
||||
RefreshRight,
|
||||
} from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElCollapse,
|
||||
ElCollapseItem,
|
||||
ElDialog,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElIcon,
|
||||
ElInput,
|
||||
ElMessageBox,
|
||||
ElTabPane,
|
||||
ElTabs,
|
||||
ElTooltip,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import { $t } from '#/locales';
|
||||
import ModelViewItemOperation from '#/views/ai/model/ModelViewItemOperation.vue';
|
||||
|
||||
const emit = defineEmits(['reload']);
|
||||
const tabList = ref<any>([]);
|
||||
const isLoading = ref(false);
|
||||
const chatModelTabList = [
|
||||
// {
|
||||
// label: $t('llm.all'),
|
||||
// name: 'all',
|
||||
// },
|
||||
{
|
||||
label: $t('llmProvider.chatModel'),
|
||||
name: 'chatModel',
|
||||
},
|
||||
// {
|
||||
// label: $t('llm.modelAbility.free'),
|
||||
// name: 'supportFree',
|
||||
// },
|
||||
];
|
||||
const embeddingModelTabList = [
|
||||
{
|
||||
label: $t('llmProvider.embeddingModel'),
|
||||
name: 'embeddingModel',
|
||||
},
|
||||
];
|
||||
|
||||
const rerankModelTabList = [
|
||||
{
|
||||
label: $t('llmProvider.rerankModel'),
|
||||
name: 'rerankModel',
|
||||
},
|
||||
];
|
||||
const formDataRef = ref();
|
||||
const providerInfo = ref<any>();
|
||||
const getProviderInfo = (id: string) => {
|
||||
api.get(`/api/v1/modelProvider/detail?id=${id}`).then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
providerInfo.value = res.data;
|
||||
}
|
||||
});
|
||||
};
|
||||
const modelList = ref<any>([]);
|
||||
const getLlmList = (providerId: string, modelType: string) => {
|
||||
isLoading.value = true;
|
||||
const url =
|
||||
modelType === ''
|
||||
? `/api/v1/model/selectLlmByProviderAndModelType?providerId=${providerId}&modelType=${modelType}&supportFree=true`
|
||||
: `/api/v1/model/selectLlmByProviderAndModelType?providerId=${providerId}&modelType=${modelType}&selectText=${searchFormDada.searchText}`;
|
||||
api.get(url).then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
const chatModelMap = res.data || {};
|
||||
modelList.value = Object.entries(chatModelMap).map(
|
||||
([groupName, llmList]) => ({
|
||||
groupName,
|
||||
llmList,
|
||||
}),
|
||||
);
|
||||
}
|
||||
isLoading.value = false;
|
||||
});
|
||||
};
|
||||
const selectedProviderId = ref('');
|
||||
defineExpose({
|
||||
// providerId: 供应商id, clickModelType 父组件点击的是什么类型的模型 可以是chatModel or embeddingModel
|
||||
openDialog(providerId: string, clickModelType: string) {
|
||||
switch (clickModelType) {
|
||||
case 'chatModel': {
|
||||
tabList.value = [...chatModelTabList];
|
||||
break;
|
||||
}
|
||||
case 'embeddingModel': {
|
||||
tabList.value = [...embeddingModelTabList];
|
||||
break;
|
||||
}
|
||||
case 'rerankModel': {
|
||||
tabList.value = [...rerankModelTabList];
|
||||
break;
|
||||
}
|
||||
// No default
|
||||
}
|
||||
selectedProviderId.value = providerId;
|
||||
formDataRef.value?.resetFields();
|
||||
modelList.value = [];
|
||||
activeName.value = tabList.value[0]?.name;
|
||||
getProviderInfo(providerId);
|
||||
getLlmList(providerId, clickModelType);
|
||||
dialogVisible.value = true;
|
||||
},
|
||||
openEditDialog(item: any) {
|
||||
dialogVisible.value = true;
|
||||
isAdd.value = false;
|
||||
formData.icon = item.icon;
|
||||
formData.providerName = item.providerName;
|
||||
formData.provider = item.provider;
|
||||
},
|
||||
});
|
||||
const isAdd = ref(true);
|
||||
const dialogVisible = ref(false);
|
||||
const formData = reactive({
|
||||
icon: '',
|
||||
providerName: '',
|
||||
provider: '',
|
||||
apiKey: '',
|
||||
endPoint: '',
|
||||
chatPath: '',
|
||||
embedPath: '',
|
||||
});
|
||||
const closeDialog = () => {
|
||||
dialogVisible.value = false;
|
||||
};
|
||||
const handleTabClick = async () => {
|
||||
await nextTick();
|
||||
getLlmList(providerInfo.value.id, activeName.value);
|
||||
};
|
||||
const activeName = ref('all');
|
||||
const handleGroupNameDelete = (groupName: string) => {
|
||||
ElMessageBox.confirm(
|
||||
$t('message.deleteModelGroupAlert'),
|
||||
$t('message.noticeTitle'),
|
||||
{
|
||||
confirmButtonText: $t('message.ok'),
|
||||
cancelButtonText: $t('message.cancel'),
|
||||
type: 'warning',
|
||||
},
|
||||
).then(() => {
|
||||
api
|
||||
.post(`/api/v1/model/removeByEntity`, {
|
||||
groupName,
|
||||
providerId: selectedProviderId.value,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
getLlmList(providerInfo.value.id, activeName.value);
|
||||
emit('reload');
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
const handleDeleteLlm = (id: any) => {
|
||||
ElMessageBox.confirm(
|
||||
$t('message.deleteModelAlert'),
|
||||
$t('message.noticeTitle'),
|
||||
{
|
||||
confirmButtonText: $t('message.ok'),
|
||||
cancelButtonText: $t('message.cancel'),
|
||||
type: 'warning',
|
||||
},
|
||||
).then(() => {
|
||||
api.post(`/api/v1/model/removeLlmByIds`, { id }).then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
getLlmList(providerInfo.value.id, activeName.value);
|
||||
emit('reload');
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
const handleAddLlm = (id: string) => {
|
||||
api
|
||||
.post(`/api/v1/model/update`, {
|
||||
id,
|
||||
withUsed: true,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
getLlmList(providerInfo.value.id, activeName.value);
|
||||
emit('reload');
|
||||
}
|
||||
});
|
||||
};
|
||||
const searchFormDada = reactive({
|
||||
searchText: '',
|
||||
});
|
||||
const handleAddAllLlm = () => {
|
||||
api
|
||||
.post(`/api/v1/model/addAllLlm`, {
|
||||
providerId: selectedProviderId.value,
|
||||
withUsed: true,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
getLlmList(providerInfo.value.id, activeName.value);
|
||||
emit('reload');
|
||||
}
|
||||
});
|
||||
};
|
||||
const handleRefresh = () => {
|
||||
if (isLoading.value) return;
|
||||
getLlmList(providerInfo.value.id, activeName.value);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
draggable
|
||||
:title="`${providerInfo?.providerName}${$t('llmProvider.model')}`"
|
||||
:before-close="closeDialog"
|
||||
:close-on-click-modal="false"
|
||||
align-center
|
||||
width="762"
|
||||
>
|
||||
<div class="manage-llm-container">
|
||||
<div class="form-container">
|
||||
<ElForm ref="formDataRef" :model="searchFormDada" status-icon>
|
||||
<ElFormItem prop="searchText">
|
||||
<div class="search-container">
|
||||
<ElInput
|
||||
v-model.trim="searchFormDada.searchText"
|
||||
@input="handleRefresh"
|
||||
:placeholder="$t('llm.searchTextPlaceholder')"
|
||||
/>
|
||||
<ElTooltip
|
||||
:content="$t('llm.button.addAllLlm')"
|
||||
placement="top"
|
||||
effect="dark"
|
||||
>
|
||||
<ElIcon
|
||||
size="20"
|
||||
@click="handleAddAllLlm"
|
||||
class="cursor-pointer"
|
||||
>
|
||||
<CirclePlus />
|
||||
</ElIcon>
|
||||
</ElTooltip>
|
||||
<ElTooltip
|
||||
:content="$t('llm.button.RetrieveAgain')"
|
||||
placement="top"
|
||||
effect="dark"
|
||||
>
|
||||
<ElIcon size="20" @click="handleRefresh" class="cursor-pointer">
|
||||
<RefreshRight />
|
||||
</ElIcon>
|
||||
</ElTooltip>
|
||||
</div>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</div>
|
||||
<div class="llm-table-container">
|
||||
<ElTabs v-model="activeName" @tab-click="handleTabClick">
|
||||
<ElTabPane
|
||||
:label="item.label"
|
||||
:name="item.name"
|
||||
v-for="item in tabList"
|
||||
default-active="all"
|
||||
:key="item.name"
|
||||
>
|
||||
<div v-if="isLoading" class="collapse-loading">
|
||||
<ElIcon class="is-loading" size="24">
|
||||
<Loading />
|
||||
</ElIcon>
|
||||
</div>
|
||||
<div v-else>
|
||||
<ElCollapse
|
||||
expand-icon-position="left"
|
||||
v-if="modelList.length > 0"
|
||||
>
|
||||
<ElCollapseItem
|
||||
v-for="group in modelList"
|
||||
:key="group.groupName"
|
||||
:title="group.groupName"
|
||||
:name="group.groupName"
|
||||
>
|
||||
<template #title>
|
||||
<div class="flex items-center justify-between pr-2">
|
||||
<span>{{ group.groupName }}</span>
|
||||
<span>
|
||||
<ElIcon
|
||||
@click.stop="handleGroupNameDelete(group.groupName)"
|
||||
>
|
||||
<Minus />
|
||||
</ElIcon>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<ModelViewItemOperation
|
||||
:need-hidden-setting-icon="true"
|
||||
:llm-list="group.llmList"
|
||||
@delete-llm="handleDeleteLlm"
|
||||
@add-llm="handleAddLlm"
|
||||
:is-management="true"
|
||||
/>
|
||||
</ElCollapseItem>
|
||||
</ElCollapse>
|
||||
</div>
|
||||
</ElTabPane>
|
||||
</ElTabs>
|
||||
</div>
|
||||
</div>
|
||||
</ElDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.manage-llm-container {
|
||||
height: 540px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
.form-container {
|
||||
height: 30px;
|
||||
}
|
||||
.search-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.llm-table-container {
|
||||
flex: 1;
|
||||
}
|
||||
.collapse-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 300px;
|
||||
gap: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
:deep(.el-tabs__nav-wrap::after) {
|
||||
height: 1px !important;
|
||||
background-color: #e4e7ed !important;
|
||||
}
|
||||
</style>
|
||||
622
easyflow-ui-admin/app/src/views/ai/model/Model.vue
Normal file
622
easyflow-ui-admin/app/src/views/ai/model/Model.vue
Normal file
@@ -0,0 +1,622 @@
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { $t } from '@easyflow/locales';
|
||||
|
||||
import { Delete, Edit, Minus, Plus } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElButton,
|
||||
ElCollapse,
|
||||
ElCollapseItem,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElIcon,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
} from 'element-plus';
|
||||
|
||||
import { getLlmProviderList } from '#/api/ai/llm.js';
|
||||
import { api } from '#/api/request.js';
|
||||
import ManageIcon from '#/components/icons/ManageIcon.vue';
|
||||
import PageSide from '#/components/page/PageSide.vue';
|
||||
import AddModelModal from '#/views/ai/model/AddModelModal.vue';
|
||||
import AddModelProviderModal from '#/views/ai/model/AddModelProviderModal.vue';
|
||||
import ManageModelModal from '#/views/ai/model/ManageModelModal.vue';
|
||||
import {
|
||||
getIconByValue,
|
||||
isSvgString,
|
||||
} from '#/views/ai/model/modelUtils/defaultIcon.ts';
|
||||
import { modelTypes } from '#/views/ai/model/modelUtils/modelTypes.ts';
|
||||
import ModelVerifyConfig from '#/views/ai/model/ModelVerifyConfig.vue';
|
||||
import ModelViewItemOperation from '#/views/ai/model/ModelViewItemOperation.vue';
|
||||
|
||||
const brandListData = ref([]);
|
||||
const defaultSelectProviderId = ref('');
|
||||
const defaultIcon = ref('');
|
||||
const modelListData = ref([]);
|
||||
onMounted(() => {
|
||||
getLlmProviderListData();
|
||||
});
|
||||
|
||||
const checkAndFillDefaultIcon = (list) => {
|
||||
if (!list || list.length === 0) return;
|
||||
list.forEach((item) => {
|
||||
if (!item.icon) {
|
||||
item.icon = getIconByValue(item.providerType);
|
||||
}
|
||||
});
|
||||
};
|
||||
const chatModelListData = ref([]);
|
||||
const embeddingModelListData = ref([]);
|
||||
const rerankModelListData = ref([]);
|
||||
|
||||
const getLlmDetailList = (providerId) => {
|
||||
api
|
||||
.get(`/api/v1/model/getList?providerId=${providerId}&withUsed=true`, {})
|
||||
.then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
modelListData.value = res.data;
|
||||
// 初始化模型分组数据(按modelType分类,存储groupName和对应的llm列表)
|
||||
chatModelListData.value = [];
|
||||
embeddingModelListData.value = [];
|
||||
|
||||
// 处理chatModel数据
|
||||
const chatModelMap = res.data.chatModel || {};
|
||||
// 将chatModel的key-value(groupName-llmList)转为数组,方便v-for遍历
|
||||
chatModelListData.value = Object.entries(chatModelMap).map(
|
||||
([groupName, llmList]) => ({
|
||||
groupName,
|
||||
llmList,
|
||||
}),
|
||||
);
|
||||
// 处理embeddingModel数据
|
||||
const embeddingModelMap = res.data.embeddingModel || {};
|
||||
embeddingModelListData.value = Object.entries(embeddingModelMap).map(
|
||||
([groupName, llmList]) => ({
|
||||
groupName,
|
||||
llmList,
|
||||
}),
|
||||
);
|
||||
|
||||
// 处理rerankModel数据
|
||||
const rerankModelMap = res.data.rerankModel || {};
|
||||
rerankModelListData.value = Object.entries(rerankModelMap).map(
|
||||
([groupName, llmList]) => ({
|
||||
groupName,
|
||||
llmList,
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const getLlmProviderListData = () => {
|
||||
getLlmProviderList().then((res) => {
|
||||
brandListData.value = res.data;
|
||||
checkAndFillDefaultIcon(brandListData.value);
|
||||
if (!defaultSelectProviderId.value) {
|
||||
defaultSelectProviderId.value = res.data[0].id;
|
||||
defaultIcon.value = res.data[0].icon;
|
||||
}
|
||||
llmProviderForm.value = {
|
||||
...res.data[0],
|
||||
};
|
||||
getLlmDetailList(defaultSelectProviderId.value);
|
||||
});
|
||||
};
|
||||
const selectCategory = ref({
|
||||
providerName: '',
|
||||
provider: '',
|
||||
});
|
||||
const handleCategoryClick = (category) => {
|
||||
selectCategory.value.providerName = category.providerName;
|
||||
selectCategory.value.provider = category.provider;
|
||||
defaultSelectProviderId.value = category.id;
|
||||
defaultIcon.value = category.icon;
|
||||
llmProviderForm.value = {
|
||||
...category,
|
||||
};
|
||||
getLlmDetailList(category.id);
|
||||
};
|
||||
|
||||
// 添加模型供应商
|
||||
const addLlmProviderRef = ref();
|
||||
// 模型管理ref
|
||||
const manageLlmRef = ref();
|
||||
// 模型验证配置ref
|
||||
const llmVerifyConfigRef = ref();
|
||||
// 添加模型
|
||||
const addLlmRef = ref();
|
||||
const handleDeleteProvider = (row) => {
|
||||
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
|
||||
confirmButtonText: $t('message.ok'),
|
||||
cancelButtonText: $t('message.cancel'),
|
||||
type: 'warning',
|
||||
}).then(() => {
|
||||
api
|
||||
.post('/api/v1/modelProvider/remove', {
|
||||
id: row.id,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message);
|
||||
getLlmProviderListData();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
const llmProviderForm = ref({});
|
||||
const llmProviderFormRef = ref();
|
||||
const isEdit = ref(false);
|
||||
const dialogAddProviderVisible = ref(false);
|
||||
const controlBtns = [
|
||||
{
|
||||
icon: Edit,
|
||||
label: $t('button.edit'),
|
||||
onClick(row) {
|
||||
isEdit.value = true;
|
||||
dialogAddProviderVisible.value = true;
|
||||
|
||||
const tempRow = {
|
||||
...row,
|
||||
};
|
||||
if (isSvgString(tempRow.icon)) {
|
||||
tempRow.icon = '';
|
||||
}
|
||||
addLlmProviderRef.value.openEditDialog(tempRow);
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'danger',
|
||||
icon: Delete,
|
||||
label: $t('button.delete'),
|
||||
onClick(row) {
|
||||
handleDeleteProvider(row);
|
||||
},
|
||||
},
|
||||
];
|
||||
const footerButton = {
|
||||
icon: Plus,
|
||||
label: $t('button.add'),
|
||||
onClick() {
|
||||
dialogAddProviderVisible.value = true;
|
||||
addLlmProviderRef.value.openAddDialog();
|
||||
isEdit.value = false;
|
||||
},
|
||||
};
|
||||
const handleAddLlm = (modelType) => {
|
||||
addLlmRef.value.openAddDialog(modelType);
|
||||
};
|
||||
const handleManageLlm = (clickModelType) => {
|
||||
manageLlmRef.value.openDialog(defaultSelectProviderId.value, clickModelType);
|
||||
};
|
||||
const handleDeleteLlm = (id) => {
|
||||
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
|
||||
confirmButtonText: $t('message.ok'),
|
||||
cancelButtonText: $t('message.cancel'),
|
||||
type: 'warning',
|
||||
}).then(() => {
|
||||
api.post('/api/v1/model/remove', { id }).then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success($t('message.deleteOkMessage'));
|
||||
getLlmDetailList(defaultSelectProviderId.value);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleEditLlm = (id) => {
|
||||
api.get(`/api/v1/model/detail?id=${id}`).then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
addLlmRef.value.openEditDialog(res.data);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleGroupNameUpdateModel = (groupName) => {
|
||||
api
|
||||
.post('/api/v1/model/updateByEntity', {
|
||||
providerId: defaultSelectProviderId.value,
|
||||
groupName,
|
||||
withUsed: false,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
getLlmDetailList(defaultSelectProviderId.value);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 输入框失去焦点时更新配置
|
||||
const handleFormBlur = async () => {
|
||||
if (!defaultSelectProviderId.value) return;
|
||||
|
||||
try {
|
||||
const res = await api.post('/api/v1/modelProvider/update', {
|
||||
id: defaultSelectProviderId.value,
|
||||
apiKey: llmProviderForm.value.apiKey,
|
||||
endpoint: llmProviderForm.value.endpoint,
|
||||
chatPath: llmProviderForm.value.chatPath,
|
||||
embedPath: llmProviderForm.value.embedPath,
|
||||
rerankPath: llmProviderForm.value.rerankPath,
|
||||
});
|
||||
|
||||
if (res.errorCode === 0) {
|
||||
getLlmProviderList().then((res) => {
|
||||
brandListData.value = res.data;
|
||||
checkAndFillDefaultIcon(res.data);
|
||||
brandListData.value.forEach((item) => {
|
||||
if (item.id === defaultSelectProviderId.value) {
|
||||
llmProviderForm.value = { ...item };
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
ElMessage.error(res.message || $t('message.updateFail'));
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error($t('message.networkError'));
|
||||
console.error('更新失败:', error);
|
||||
}
|
||||
};
|
||||
const handleTest = () => {
|
||||
llmVerifyConfigRef.value.openDialog(defaultSelectProviderId.value);
|
||||
};
|
||||
|
||||
const handleUpdateLlm = (id) => {
|
||||
api.post('/api/v1/model/update', { id, withUsed: false }).then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
getLlmDetailList(defaultSelectProviderId.value);
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="llm-container">
|
||||
<div>
|
||||
<PageSide
|
||||
:title="$t('llm.addProvider')"
|
||||
label-key="providerName"
|
||||
value-key="id"
|
||||
:menus="brandListData"
|
||||
:control-btns="controlBtns"
|
||||
:footer-button="footerButton"
|
||||
@change="handleCategoryClick"
|
||||
:default-selected="defaultSelectProviderId"
|
||||
:icon-size="21"
|
||||
/>
|
||||
</div>
|
||||
<div class="llm-table-container">
|
||||
<div class="llm-form-container">
|
||||
<div class="title">{{ selectCategory.providerName }}</div>
|
||||
<ElForm
|
||||
ref="llmProviderFormRef"
|
||||
:model="llmProviderForm"
|
||||
status-icon
|
||||
label-position="top"
|
||||
>
|
||||
<ElFormItem prop="apiKey" :label="$t('llmProvider.apiKey')">
|
||||
<ElInput
|
||||
v-model="llmProviderForm.apiKey"
|
||||
@blur="handleFormBlur"
|
||||
type="password"
|
||||
show-password
|
||||
>
|
||||
<template #append>
|
||||
<ElButton
|
||||
@click="handleTest"
|
||||
style="
|
||||
background-color: var(--el-bg-color);
|
||||
width: 80px;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 0 8px 8px 0;
|
||||
"
|
||||
>
|
||||
{{ $t('llm.button.test') }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="endpoint" :label="$t('llmProvider.endpoint')">
|
||||
<ElInput
|
||||
v-model.trim="llmProviderForm.endpoint"
|
||||
@blur="handleFormBlur"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="chatPath" :label="$t('llmProvider.chatPath')">
|
||||
<ElInput
|
||||
v-model.trim="llmProviderForm.chatPath"
|
||||
@blur="handleFormBlur"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="embedPath" :label="$t('llmProvider.embedPath')">
|
||||
<ElInput
|
||||
v-model.trim="llmProviderForm.embedPath"
|
||||
@blur="handleFormBlur"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="rerankPath" :label="$t('llmProvider.rerankPath')">
|
||||
<ElInput
|
||||
v-model.trim="llmProviderForm.rerankPath"
|
||||
@blur="handleFormBlur"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<div class="llm-manage-container">
|
||||
<div
|
||||
v-for="(model, index) in modelTypes"
|
||||
:key="model.value"
|
||||
class="model-container"
|
||||
>
|
||||
<div
|
||||
class="model-common-title"
|
||||
:class="[index === 0 ? 'first-model-title' : '']"
|
||||
>
|
||||
{{ model.label }}
|
||||
</div>
|
||||
|
||||
<!-- 对话模型(chatModel)遍历 -->
|
||||
<div
|
||||
v-if="model.value === 'chatModel' && chatModelListData.length > 0"
|
||||
>
|
||||
<ElCollapse expand-icon-position="left">
|
||||
<ElCollapseItem
|
||||
v-for="group in chatModelListData"
|
||||
:key="group.groupName"
|
||||
:title="group.groupName"
|
||||
:name="group.groupName"
|
||||
>
|
||||
<template #title>
|
||||
<div class="flex items-center justify-between pr-2">
|
||||
<span>{{ group.groupName }}</span>
|
||||
<span>
|
||||
<ElIcon
|
||||
@click.stop="
|
||||
handleGroupNameUpdateModel(group.groupName)
|
||||
"
|
||||
>
|
||||
<Minus />
|
||||
</ElIcon>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<ModelViewItemOperation
|
||||
:llm-list="group.llmList"
|
||||
:icon="defaultIcon"
|
||||
@delete-llm="handleDeleteLlm"
|
||||
@edit-llm="handleEditLlm"
|
||||
@update-with-used="handleUpdateLlm"
|
||||
/>
|
||||
</ElCollapseItem>
|
||||
</ElCollapse>
|
||||
</div>
|
||||
|
||||
<!-- 嵌入模型(embeddingModel)遍历-->
|
||||
<div
|
||||
v-if="
|
||||
model.value === 'embeddingModel' &&
|
||||
embeddingModelListData.length > 0
|
||||
"
|
||||
>
|
||||
<ElCollapse expand-icon-position="left">
|
||||
<ElCollapseItem
|
||||
v-for="group in embeddingModelListData"
|
||||
:key="group.groupName"
|
||||
:title="group.groupName"
|
||||
:name="group.groupName"
|
||||
>
|
||||
<template #title>
|
||||
<div class="flex items-center justify-between pr-2">
|
||||
<span>{{ group.groupName }}</span>
|
||||
<span
|
||||
@click.stop="
|
||||
handleGroupNameUpdateModel(group.groupName)
|
||||
"
|
||||
>
|
||||
<ElIcon>
|
||||
<Minus />
|
||||
</ElIcon>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<ModelViewItemOperation
|
||||
:llm-list="group.llmList"
|
||||
:icon="defaultIcon"
|
||||
@delete-llm="handleDeleteLlm"
|
||||
@edit-llm="handleEditLlm"
|
||||
@update-with-used="handleUpdateLlm"
|
||||
/>
|
||||
</ElCollapseItem>
|
||||
</ElCollapse>
|
||||
</div>
|
||||
|
||||
<!-- 重排模型(rerankModel)遍历-->
|
||||
<div
|
||||
v-if="
|
||||
model.value === 'rerankModel' &&
|
||||
embeddingModelListData.length > 0
|
||||
"
|
||||
>
|
||||
<ElCollapse expand-icon-position="left">
|
||||
<ElCollapseItem
|
||||
v-for="group in rerankModelListData"
|
||||
:key="group.groupName"
|
||||
:title="group.groupName"
|
||||
:name="group.groupName"
|
||||
>
|
||||
<template #title>
|
||||
<div class="flex items-center justify-between pr-2">
|
||||
<span>{{ group.groupName }}</span>
|
||||
<span
|
||||
@click.stop="
|
||||
handleGroupNameUpdateModel(group.groupName)
|
||||
"
|
||||
>
|
||||
<ElIcon>
|
||||
<Minus />
|
||||
</ElIcon>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<ModelViewItemOperation
|
||||
:llm-list="group.llmList"
|
||||
:icon="defaultIcon"
|
||||
@delete-llm="handleDeleteLlm"
|
||||
@edit-llm="handleEditLlm"
|
||||
@update-with-used="handleUpdateLlm"
|
||||
/>
|
||||
</ElCollapseItem>
|
||||
</ElCollapse>
|
||||
</div>
|
||||
|
||||
<div class="model-operation-container">
|
||||
<ElButton
|
||||
type="primary"
|
||||
@click="handleManageLlm(model.value)"
|
||||
:icon="ManageIcon"
|
||||
>
|
||||
{{ $t('llm.button.management') }}
|
||||
</ElButton>
|
||||
<ElButton :icon="Plus" @click="handleAddLlm(model.value)">
|
||||
{{ $t('button.add') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--添加模型供应商模态框-->
|
||||
<AddModelProviderModal
|
||||
ref="addLlmProviderRef"
|
||||
@reload="getLlmProviderListData()"
|
||||
/>
|
||||
<!--添加模型模态框-->
|
||||
<AddModelModal
|
||||
ref="addLlmRef"
|
||||
@reload="getLlmProviderListData()"
|
||||
:provider-id="defaultSelectProviderId"
|
||||
/>
|
||||
<!--模型管理模态框-->
|
||||
<ManageModelModal ref="manageLlmRef" @reload="getLlmProviderListData()" />
|
||||
|
||||
<!--模型检测配置模态框-->
|
||||
<ModelVerifyConfig ref="llmVerifyConfigRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.llm-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 20px;
|
||||
height: calc(100vh - 90px);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
color: #333333;
|
||||
line-height: 22px;
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.llm-table-container {
|
||||
flex: 1;
|
||||
padding: 24px;
|
||||
background-color: var(--el-bg-color);
|
||||
border-radius: 8px;
|
||||
overflow: auto;
|
||||
border: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.llm-form-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.model-common-title {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
line-height: 20px;
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
margin: 24px 0 12px 0;
|
||||
}
|
||||
|
||||
.first-model-title {
|
||||
margin: 0 0 12px 0;
|
||||
}
|
||||
|
||||
/* 折叠面板容器 */
|
||||
:deep(.el-collapse) {
|
||||
border: none;
|
||||
border-radius: 8px !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
:deep(.el-collapse-item) {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
:deep(.el-collapse-item__header) {
|
||||
background-color: #f9fafc;
|
||||
padding: 0 9px 0 17px;
|
||||
border-radius: 8px 8px 0 0;
|
||||
border: 1px solid #f0f0f0;
|
||||
height: 20px !important;
|
||||
line-height: 20px !important;
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
:deep(.el-collapse-item__arrow) {
|
||||
line-height: 38px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
:deep(.el-collapse-item__wrap) {
|
||||
border: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
:deep(.el-collapse-item__content) {
|
||||
border: 1px solid #f0f0f0;
|
||||
background: #ffffff;
|
||||
border-radius: 0 0 8px 8px;
|
||||
padding: 12px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
box-sizing: border-box;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
:deep(.el-collapse-item:last-child) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.model-operation-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.flex.items-center.justify-between.pr-2 {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
139
easyflow-ui-admin/app/src/views/ai/model/ModelVerifyConfig.vue
Normal file
139
easyflow-ui-admin/app/src/views/ai/model/ModelVerifyConfig.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElDialog,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElMessage,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const options = ref<any[]>([]);
|
||||
const getLlmList = (providerId: string) => {
|
||||
api.get(`/api/v1/model/list?providerId=${providerId}`, {}).then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
options.value = res.data;
|
||||
}
|
||||
});
|
||||
};
|
||||
const modelType = ref('');
|
||||
const vectorDimension = ref('');
|
||||
const formDataRef = ref();
|
||||
const dialogVisible = ref(false);
|
||||
defineExpose({
|
||||
openDialog(providerId: string) {
|
||||
formDataRef.value?.resetFields();
|
||||
modelType.value = '';
|
||||
vectorDimension.value = '';
|
||||
getLlmList(providerId);
|
||||
dialogVisible.value = true;
|
||||
},
|
||||
});
|
||||
const formData = reactive({
|
||||
llmId: '',
|
||||
});
|
||||
|
||||
const rules = {
|
||||
llmId: [
|
||||
{
|
||||
required: true,
|
||||
message: $t('message.required'),
|
||||
trigger: 'change',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const save = async () => {
|
||||
btnLoading.value = true;
|
||||
await formDataRef.value.validate();
|
||||
api
|
||||
.get(`/api/v1/model/verifyLlmConfig?id=${formData.llmId}`, {})
|
||||
.then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success($t('llm.testSuccess'));
|
||||
if (modelType.value === 'embeddingModel' && res?.data?.dimension) {
|
||||
vectorDimension.value = res?.data?.dimension;
|
||||
}
|
||||
}
|
||||
btnLoading.value = false;
|
||||
});
|
||||
};
|
||||
const btnLoading = ref(false);
|
||||
const closeDialog = () => {
|
||||
dialogVisible.value = false;
|
||||
};
|
||||
const getModelInfo = (id: string) => {
|
||||
options.value.forEach((item: any) => {
|
||||
if (item.id === id) {
|
||||
modelType.value = item.modelType;
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
draggable
|
||||
:title="$t('llm.verifyLlmTitle')"
|
||||
:close-on-click-modal="false"
|
||||
align-center
|
||||
width="482"
|
||||
>
|
||||
<ElForm ref="formDataRef" :model="formData" status-icon :rules="rules">
|
||||
<ElFormItem prop="llmId" :label="$t('llm.modelToBeTested')">
|
||||
<ElSelect v-model="formData.llmId" @change="getModelInfo">
|
||||
<ElOption
|
||||
v-for="item in options"
|
||||
:key="item.id"
|
||||
:label="item.title"
|
||||
:value="item.id || ''"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
v-if="modelType === 'embeddingModel' && vectorDimension"
|
||||
:label="$t('documentCollection.dimensionOfVectorModel')"
|
||||
label-width="100px"
|
||||
>
|
||||
{{ vectorDimension }}
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<ElButton @click="closeDialog">
|
||||
{{ $t('button.cancel') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
@click="save"
|
||||
:loading="btnLoading"
|
||||
:disabled="btnLoading"
|
||||
>
|
||||
{{ $t('button.confirm') }}
|
||||
</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>
|
||||
@@ -0,0 +1,201 @@
|
||||
<script setup lang="ts">
|
||||
import type { PropType } from 'vue';
|
||||
|
||||
import type { llmType } from '#/api';
|
||||
import type { ModelAbilityItem } from '#/views/ai/model/modelUtils/model-ability';
|
||||
|
||||
import { Minus, Plus, Setting } from '@element-plus/icons-vue';
|
||||
import { ElIcon, ElImage, ElTag } from 'element-plus';
|
||||
|
||||
import { getIconByValue } from '#/views/ai/model/modelUtils/defaultIcon';
|
||||
import { getDefaultModelAbility } from '#/views/ai/model/modelUtils/model-ability';
|
||||
import { mapLlmToModelAbility } from '#/views/ai/model/modelUtils/model-ability-utils';
|
||||
|
||||
defineProps({
|
||||
llmList: {
|
||||
type: Array as PropType<llmType[]>,
|
||||
default: () => [],
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
needHiddenSettingIcon: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isManagement: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['deleteLlm', 'editLlm', 'addLlm', 'updateWithUsed']);
|
||||
|
||||
const handleDeleteLlm = (id: string) => {
|
||||
emit('deleteLlm', id);
|
||||
};
|
||||
|
||||
const handleAddLlm = (id: string) => {
|
||||
emit('addLlm', id);
|
||||
};
|
||||
|
||||
const handleEditLlm = (id: string) => {
|
||||
emit('editLlm', id);
|
||||
};
|
||||
|
||||
// 修改该模型为未使用状态,修改数据库的with_used字段为false
|
||||
const handleUpdateWithUsedLlm = (id: string) => {
|
||||
emit('updateWithUsed', id);
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取LLM支持的选中的能力标签
|
||||
* 只返回 selected 为 true 的标签
|
||||
*/
|
||||
const getSelectedAbilityTagsForLlm = (llm: llmType): ModelAbilityItem[] => {
|
||||
const defaultAbility = getDefaultModelAbility();
|
||||
const allTags = mapLlmToModelAbility(llm, defaultAbility);
|
||||
return allTags.filter((tag) => tag.selected);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-for="llm in llmList" :key="llm.id" class="container">
|
||||
<div class="llm-item">
|
||||
<div class="start">
|
||||
<ElImage
|
||||
v-if="llm.modelProvider.icon"
|
||||
:src="llm.modelProvider.icon"
|
||||
style="width: 21px; height: 21px"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-else
|
||||
v-html="getIconByValue(llm.modelProvider.providerType)"
|
||||
:style="{
|
||||
width: '21px',
|
||||
height: '21px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
}"
|
||||
class="svg-container"
|
||||
></div>
|
||||
|
||||
<div>{{ llm?.modelProvider?.providerName }}/{{ llm.title }}</div>
|
||||
|
||||
<!-- 模型能力 -->
|
||||
<div
|
||||
v-if="getSelectedAbilityTagsForLlm(llm).length > 0"
|
||||
class="ability-tags"
|
||||
>
|
||||
<ElTag
|
||||
v-for="tag in getSelectedAbilityTagsForLlm(llm)"
|
||||
:key="tag.value"
|
||||
class="ability-tag"
|
||||
:type="tag.activeType"
|
||||
size="small"
|
||||
>
|
||||
{{ tag.label }}
|
||||
</ElTag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="end">
|
||||
<ElIcon
|
||||
v-if="!needHiddenSettingIcon"
|
||||
size="16"
|
||||
@click="handleEditLlm(llm.id)"
|
||||
style="cursor: pointer"
|
||||
>
|
||||
<Setting />
|
||||
</ElIcon>
|
||||
<template v-if="!isManagement">
|
||||
<ElIcon
|
||||
size="16"
|
||||
@click="handleUpdateWithUsedLlm(llm.id)"
|
||||
style="cursor: pointer"
|
||||
>
|
||||
<Minus />
|
||||
</ElIcon>
|
||||
</template>
|
||||
|
||||
<template v-if="isManagement">
|
||||
<ElIcon
|
||||
v-if="llm.withUsed"
|
||||
size="16"
|
||||
@click="handleDeleteLlm(llm.id)"
|
||||
style="cursor: pointer"
|
||||
>
|
||||
<Minus />
|
||||
</ElIcon>
|
||||
<ElIcon
|
||||
v-else
|
||||
size="16"
|
||||
@click="handleAddLlm(llm.id)"
|
||||
style="cursor: pointer"
|
||||
>
|
||||
<Plus />
|
||||
</ElIcon>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.llm-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 12px 18px;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
}
|
||||
|
||||
.container:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.start {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.end {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.ability-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.ability-tag {
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.svg-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.svg-container :deep(svg) {
|
||||
width: 21px;
|
||||
height: 21px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,27 @@
|
||||
import { ref } from 'vue';
|
||||
|
||||
import providerList from './providerList.json';
|
||||
|
||||
const providerOptions =
|
||||
ref<Array<{ icon: string; label: string; options: any; value: string }>>(
|
||||
providerList,
|
||||
);
|
||||
|
||||
/**
|
||||
* 根据传入的value,返回对应的icon属性
|
||||
* @param targetValue 要匹配的value值
|
||||
* @returns 匹配到的icon字符串,未匹配到返回空字符串
|
||||
*/
|
||||
export const getIconByValue = (targetValue: string): string => {
|
||||
const matchItem = providerOptions.value.find(
|
||||
(item) => item.value === targetValue,
|
||||
);
|
||||
|
||||
return matchItem?.icon || '';
|
||||
};
|
||||
|
||||
export const isSvgString = (icon: any) => {
|
||||
if (typeof icon !== 'string') return false;
|
||||
// 简单判断:是否包含 SVG 根标签
|
||||
return icon.trim().startsWith('<svg') && icon.trim().endsWith('</svg>');
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
import type { BooleanField, ModelAbilityItem } from './model-ability';
|
||||
|
||||
import type { llmType } from '#/api';
|
||||
|
||||
/**
|
||||
* 将 llm 数据转换为标签选中状态
|
||||
* @param llm LLM数据对象
|
||||
* @param modelAbility 模型能力数组
|
||||
* @returns 更新后的模型能力数组
|
||||
*/
|
||||
export const mapLlmToModelAbility = (
|
||||
llm: llmType,
|
||||
modelAbility: ModelAbilityItem[],
|
||||
): ModelAbilityItem[] => {
|
||||
return modelAbility.map((tag) => ({
|
||||
...tag,
|
||||
selected: Boolean(llm[tag.field as keyof llmType]),
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* 从标签选中状态生成 features 对象
|
||||
* @param modelAbility 模型能力数组
|
||||
* @returns 包含所有字段的features对象
|
||||
*/
|
||||
export const generateFeaturesFromModelAbility = (
|
||||
modelAbility: ModelAbilityItem[],
|
||||
): Record<BooleanField, boolean> => {
|
||||
const features: Partial<Record<BooleanField, boolean>> = {};
|
||||
|
||||
modelAbility.forEach((tag) => {
|
||||
features[tag.field] = tag.selected;
|
||||
});
|
||||
|
||||
return features as Record<BooleanField, boolean>;
|
||||
};
|
||||
|
||||
/**
|
||||
* 过滤显示选中的标签
|
||||
* @param modelAbility 模型能力数组
|
||||
* @returns 选中的标签数组
|
||||
*/
|
||||
export const getSelectedModelAbility = (
|
||||
modelAbility: ModelAbilityItem[],
|
||||
): ModelAbilityItem[] => {
|
||||
return modelAbility.filter((tag) => tag.selected);
|
||||
};
|
||||
|
||||
/**
|
||||
* 重置所有标签为未选中状态
|
||||
* @param modelAbility 模型能力数组
|
||||
*/
|
||||
export const resetModelAbility = (modelAbility: ModelAbilityItem[]): void => {
|
||||
modelAbility.forEach((tag) => {
|
||||
tag.selected = false;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据标签选中状态更新表单数据
|
||||
* @param modelAbility 模型能力数组
|
||||
* @param formData 表单数据对象
|
||||
*/
|
||||
export const updateFormDataFromModelAbility = (
|
||||
modelAbility: ModelAbilityItem[],
|
||||
formData: Record<BooleanField, boolean>,
|
||||
): void => {
|
||||
modelAbility.forEach((tag) => {
|
||||
formData[tag.field] = tag.selected;
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,169 @@
|
||||
import { $t } from '#/locales';
|
||||
|
||||
export type BooleanField =
|
||||
| 'supportAudio'
|
||||
| 'supportFree'
|
||||
| 'supportImage'
|
||||
| 'supportImageB64Only'
|
||||
| 'supportThinking'
|
||||
| 'supportTool'
|
||||
| 'supportToolMessage'
|
||||
| 'supportVideo';
|
||||
|
||||
export interface ModelAbilityItem {
|
||||
activeType: 'danger' | 'info' | 'primary' | 'success' | 'warning';
|
||||
defaultType: 'info';
|
||||
field: BooleanField;
|
||||
label: string;
|
||||
selected: boolean;
|
||||
value: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模型能力标签的默认配置
|
||||
* @returns ModelAbilityItem[] 模型能力配置数组
|
||||
*/
|
||||
export const getDefaultModelAbility = (): ModelAbilityItem[] => [
|
||||
{
|
||||
label: $t('llm.modelAbility.supportThinking'),
|
||||
value: 'thinking',
|
||||
defaultType: 'info',
|
||||
activeType: 'success',
|
||||
selected: false,
|
||||
field: 'supportThinking',
|
||||
},
|
||||
{
|
||||
label: $t('llm.modelAbility.supportTool'),
|
||||
value: 'tool',
|
||||
defaultType: 'info',
|
||||
activeType: 'success',
|
||||
selected: false,
|
||||
field: 'supportTool',
|
||||
},
|
||||
{
|
||||
label: $t('llm.modelAbility.supportVideo'),
|
||||
value: 'video',
|
||||
defaultType: 'info',
|
||||
activeType: 'success',
|
||||
selected: false,
|
||||
field: 'supportVideo',
|
||||
},
|
||||
{
|
||||
label: $t('llm.modelAbility.supportImage'),
|
||||
value: 'image',
|
||||
defaultType: 'info',
|
||||
activeType: 'success',
|
||||
selected: false,
|
||||
field: 'supportImage',
|
||||
},
|
||||
{
|
||||
label: $t('llm.modelAbility.supportFree'),
|
||||
value: 'free',
|
||||
defaultType: 'info',
|
||||
activeType: 'success',
|
||||
selected: false,
|
||||
field: 'supportFree',
|
||||
},
|
||||
{
|
||||
label: $t('llm.modelAbility.supportAudio'),
|
||||
value: 'audio',
|
||||
defaultType: 'info',
|
||||
activeType: 'success',
|
||||
selected: false,
|
||||
field: 'supportAudio',
|
||||
},
|
||||
{
|
||||
label: $t('llm.modelAbility.supportImageB64Only'),
|
||||
value: 'imageB64',
|
||||
defaultType: 'info',
|
||||
activeType: 'success',
|
||||
selected: false,
|
||||
field: 'supportImageB64Only',
|
||||
},
|
||||
{
|
||||
label: $t('llm.modelAbility.supportToolMessage'),
|
||||
value: 'toolMessage',
|
||||
defaultType: 'info',
|
||||
activeType: 'success',
|
||||
selected: true,
|
||||
field: 'supportToolMessage',
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* 根据字段数组获取对应的标签选中状态
|
||||
* @param modelAbility 模型能力数组
|
||||
* @param fields 需要获取的字段数组
|
||||
* @returns 以字段名为键、选中状态为值的对象
|
||||
*/
|
||||
export const getTagsSelectedStatus = (
|
||||
modelAbility: ModelAbilityItem[],
|
||||
fields: BooleanField[],
|
||||
): Record<BooleanField, boolean> => {
|
||||
const result: Partial<Record<BooleanField, boolean>> = {};
|
||||
|
||||
fields.forEach((field) => {
|
||||
const tagItem = modelAbility.find((tag) => tag.field === field);
|
||||
result[field] = tagItem?.selected ?? false;
|
||||
});
|
||||
|
||||
return result as Record<BooleanField, boolean>;
|
||||
};
|
||||
|
||||
/**
|
||||
* 同步标签选中状态与formData中的布尔字段
|
||||
* @param modelAbility 模型能力数组
|
||||
* @param formData 表单数据对象
|
||||
*/
|
||||
export const syncTagSelectedStatus = (
|
||||
modelAbility: ModelAbilityItem[],
|
||||
formData: Record<BooleanField, boolean>,
|
||||
): void => {
|
||||
modelAbility.forEach((tag) => {
|
||||
tag.selected = formData[tag.field] ?? false;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理标签点击事件
|
||||
* @param modelAbility 模型能力数组
|
||||
* @param item 被点击的标签项
|
||||
* @param formData 表单数据对象
|
||||
*/
|
||||
export const handleTagClick = (
|
||||
// modelAbility: ModelAbilityItem[],
|
||||
item: ModelAbilityItem,
|
||||
formData: Record<BooleanField, boolean>,
|
||||
): void => {
|
||||
// 切换标签选中状态
|
||||
item.selected = !item.selected;
|
||||
|
||||
// 同步更新formData中的布尔字段
|
||||
formData[item.field] = item.selected;
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据字段获取对应的标签项
|
||||
* @param modelAbility 模型能力数组
|
||||
* @param field 布尔字段名
|
||||
* @returns 标签项 | undefined
|
||||
*/
|
||||
export const getTagByField = (
|
||||
modelAbility: ModelAbilityItem[],
|
||||
field: BooleanField,
|
||||
): ModelAbilityItem | undefined => {
|
||||
return modelAbility.find((tag) => tag.field === field);
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取所有支持的BooleanField数组
|
||||
*/
|
||||
export const getAllBooleanFields = (): BooleanField[] => [
|
||||
'supportThinking',
|
||||
'supportTool',
|
||||
'supportImage',
|
||||
'supportImageB64Only',
|
||||
'supportVideo',
|
||||
'supportAudio',
|
||||
'supportFree',
|
||||
];
|
||||
@@ -0,0 +1,16 @@
|
||||
import { $t } from '@easyflow/locales';
|
||||
|
||||
export const modelTypes = [
|
||||
{
|
||||
label: $t('llmProvider.chatModel'),
|
||||
value: 'chatModel',
|
||||
},
|
||||
{
|
||||
label: $t('llmProvider.embeddingModel'),
|
||||
value: 'embeddingModel',
|
||||
},
|
||||
{
|
||||
label: $t('llmProvider.rerankModel'),
|
||||
value: 'rerankModel',
|
||||
},
|
||||
];
|
||||
File diff suppressed because one or more lines are too long
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>
|
||||
@@ -0,0 +1,77 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import { ElButton, ElDialog } from 'element-plus';
|
||||
|
||||
import PageData from '#/components/page/PageData.vue';
|
||||
import { $t } from '#/locales';
|
||||
import ResourceCardList from '#/views/ai/resource/ResourceCardList.vue';
|
||||
|
||||
const props = defineProps({
|
||||
attrName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['choose']);
|
||||
|
||||
const pageDataRef = ref();
|
||||
const dialogVisible = ref(false);
|
||||
const chooseResources = ref([]);
|
||||
const currentChoose = ref<any>({});
|
||||
function openDialog() {
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
function closeDialog() {
|
||||
dialogVisible.value = false;
|
||||
}
|
||||
function confirm() {
|
||||
emit('choose', currentChoose.value, props.attrName);
|
||||
closeDialog();
|
||||
}
|
||||
watch(
|
||||
() => chooseResources.value,
|
||||
(newValue) => {
|
||||
currentChoose.value = newValue.length > 0 ? newValue[0] : {};
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
draggable
|
||||
:title="$t('aiResource.choose')"
|
||||
:before-close="closeDialog"
|
||||
:close-on-click-modal="false"
|
||||
width="80%"
|
||||
destroy-on-close
|
||||
>
|
||||
<PageData
|
||||
ref="pageDataRef"
|
||||
page-url="/api/v1/resource/page"
|
||||
:page-size="8"
|
||||
:page-sizes="[8, 12, 16, 20]"
|
||||
>
|
||||
<template #default="{ pageList }">
|
||||
<ResourceCardList v-model="chooseResources" :data="pageList" />
|
||||
</template>
|
||||
</PageData>
|
||||
<template #footer>
|
||||
<ElButton @click="closeDialog">
|
||||
{{ $t('button.cancel') }}
|
||||
</ElButton>
|
||||
<ElButton type="primary" @click="confirm">
|
||||
{{ $t('button.confirm') }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
<ElButton @click="openDialog()">
|
||||
{{ $t('button.choose') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
47
easyflow-ui-admin/app/src/views/ai/resource/PreviewModal.vue
Normal file
47
easyflow-ui-admin/app/src/views/ai/resource/PreviewModal.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { ElDialog, ElImage } from 'element-plus';
|
||||
|
||||
defineExpose({
|
||||
openDialog,
|
||||
});
|
||||
const dialogVisible = ref(false);
|
||||
const data = ref<any>();
|
||||
function openDialog(row: any) {
|
||||
data.value = row;
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
function closeDialog() {
|
||||
dialogVisible.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
draggable
|
||||
:title="$t('message.preview')"
|
||||
:before-close="closeDialog"
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
>
|
||||
<div class="flex justify-center">
|
||||
<ElImage
|
||||
v-if="data.resourceType === 0"
|
||||
style="width: 200px"
|
||||
:preview-src-list="[data.resourceUrl]"
|
||||
:src="data.resourceUrl"
|
||||
/>
|
||||
<video v-if="data.resourceType === 1" controls width="640" height="360">
|
||||
<source :src="data.resourceUrl" type="video/mp4" />
|
||||
{{ $t('message.notVideo') }}
|
||||
</video>
|
||||
<audio v-if="data.resourceType === 2" controls :src="data.resourceUrl">
|
||||
{{ $t('message.notAudio') }}
|
||||
</audio>
|
||||
</div>
|
||||
</ElDialog>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
155
easyflow-ui-admin/app/src/views/ai/resource/ResourceCardList.vue
Normal file
155
easyflow-ui-admin/app/src/views/ai/resource/ResourceCardList.vue
Normal file
@@ -0,0 +1,155 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
|
||||
import {
|
||||
ElCard,
|
||||
ElCheckbox,
|
||||
ElCol,
|
||||
ElImage,
|
||||
ElRadio,
|
||||
ElRow,
|
||||
ElText,
|
||||
ElTooltip,
|
||||
} from 'element-plus';
|
||||
|
||||
import Tag from '#/components/tag/Tag.vue';
|
||||
import { $t } from '#/locales';
|
||||
import { useDictStore } from '#/store';
|
||||
import {
|
||||
getResourceOriginColor,
|
||||
getResourceTypeColor,
|
||||
getSrc,
|
||||
} from '#/utils/resource';
|
||||
import PreviewModal from '#/views/ai/resource/PreviewModal.vue';
|
||||
|
||||
export interface ResourceCardProps {
|
||||
data: any[];
|
||||
multiple?: boolean;
|
||||
valueProp?: string;
|
||||
}
|
||||
const props = withDefaults(defineProps<ResourceCardProps>(), {
|
||||
multiple: false,
|
||||
valueProp: 'id',
|
||||
});
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
onMounted(() => {
|
||||
initDict();
|
||||
});
|
||||
const dictStore = useDictStore();
|
||||
function initDict() {
|
||||
dictStore.fetchDictionary('resourceType');
|
||||
dictStore.fetchDictionary('resourceOriginType');
|
||||
}
|
||||
const previewDialog = ref();
|
||||
const radioValue = ref('');
|
||||
const checkAll = ref(false);
|
||||
function choose() {
|
||||
const arr = [];
|
||||
if (props.multiple) {
|
||||
for (const data of props.data) {
|
||||
if (data.checkboxValue) {
|
||||
arr.push(data);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (radioValue.value) {
|
||||
for (const data of props.data) {
|
||||
if (data[props.valueProp] === radioValue.value) {
|
||||
arr.push(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
emit('update:modelValue', arr);
|
||||
}
|
||||
function handleCheckAllChange(val: any) {
|
||||
if (val) {
|
||||
for (const data of props.data) {
|
||||
data.checkboxValue = data[props.valueProp];
|
||||
}
|
||||
} else {
|
||||
for (const data of props.data) {
|
||||
data.checkboxValue = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
function preview(row: any) {
|
||||
previewDialog.value.openDialog({ ...row });
|
||||
}
|
||||
watch(
|
||||
[() => radioValue.value, () => props.data],
|
||||
() => {
|
||||
choose();
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<PreviewModal ref="previewDialog" />
|
||||
<ElCheckbox
|
||||
v-if="multiple"
|
||||
:label="$t('button.selectAll')"
|
||||
v-model="checkAll"
|
||||
@change="handleCheckAllChange"
|
||||
/>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="6" v-for="item in data" :key="item.id" class="mb-5">
|
||||
<ElCard
|
||||
:body-style="{ padding: '12px', height: '285px' }"
|
||||
shadow="hover"
|
||||
>
|
||||
<div>
|
||||
<div>
|
||||
<ElCheckbox
|
||||
v-if="multiple"
|
||||
v-model="item.checkboxValue"
|
||||
:true-value="item[valueProp]"
|
||||
false-value=""
|
||||
/>
|
||||
<ElRadio v-else v-model="radioValue" :value="item[valueProp]" />
|
||||
</div>
|
||||
<div>
|
||||
<ElImage
|
||||
@click="preview(item)"
|
||||
:src="getSrc(item)"
|
||||
style="width: 100%; height: 150px; cursor: pointer"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<ElTooltip
|
||||
:content="`${item.resourceName}.${item.suffix}`"
|
||||
placement="top"
|
||||
>
|
||||
<ElText truncated>
|
||||
{{ item.resourceName }}.{{ item.suffix }}
|
||||
</ElText>
|
||||
</ElTooltip>
|
||||
</div>
|
||||
<div class="flex gap-1.5">
|
||||
<Tag
|
||||
size="small"
|
||||
:background-color="`${getResourceOriginColor(item)}15`"
|
||||
:text-color="getResourceOriginColor(item)"
|
||||
:text="
|
||||
dictStore.getDictLabel('resourceOriginType', item.origin)
|
||||
"
|
||||
/>
|
||||
<Tag
|
||||
size="small"
|
||||
:background-color="`${getResourceTypeColor(item)}15`"
|
||||
:text-color="getResourceTypeColor(item)"
|
||||
:text="
|
||||
dictStore.getDictLabel('resourceType', item.resourceType)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
467
easyflow-ui-admin/app/src/views/ai/resource/ResourceList.vue
Normal file
467
easyflow-ui-admin/app/src/views/ai/resource/ResourceList.vue
Normal file
@@ -0,0 +1,467 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from 'element-plus';
|
||||
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
|
||||
import { formatBytes } from '@easyflow/utils';
|
||||
|
||||
import {
|
||||
Delete,
|
||||
Download,
|
||||
Edit,
|
||||
MoreFilled,
|
||||
Plus,
|
||||
} from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElAvatar,
|
||||
ElButton,
|
||||
ElDialog,
|
||||
ElDropdown,
|
||||
ElDropdownItem,
|
||||
ElDropdownMenu,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElIcon,
|
||||
ElInput,
|
||||
ElInputNumber,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
ElText,
|
||||
} from 'element-plus';
|
||||
import { tryit } from 'radash';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import DictSelect from '#/components/dict/DictSelect.vue';
|
||||
import PageData from '#/components/page/PageData.vue';
|
||||
import PageSide from '#/components/page/PageSide.vue';
|
||||
import Tag from '#/components/tag/Tag.vue';
|
||||
import { $t } from '#/locales';
|
||||
import { useDictStore } from '#/store';
|
||||
import {
|
||||
getResourceOriginColor,
|
||||
getResourceTypeColor,
|
||||
getSrc,
|
||||
} from '#/utils/resource';
|
||||
import PreviewModal from '#/views/ai/resource/PreviewModal.vue';
|
||||
|
||||
import ResourceModal from './ResourceModal.vue';
|
||||
|
||||
onMounted(() => {
|
||||
initDict();
|
||||
getSideList();
|
||||
});
|
||||
const formRef = ref<FormInstance>();
|
||||
const pageDataRef = ref();
|
||||
const saveDialog = ref();
|
||||
const previewDialog = ref();
|
||||
const formInline = ref({
|
||||
resourceName: '',
|
||||
resourceType: '',
|
||||
});
|
||||
const dictStore = useDictStore();
|
||||
function initDict() {
|
||||
dictStore.fetchDictionary('resourceType');
|
||||
dictStore.fetchDictionary('resourceOriginType');
|
||||
}
|
||||
function search(formEl: FormInstance | undefined) {
|
||||
formEl?.validate((valid) => {
|
||||
if (valid) {
|
||||
pageDataRef.value.setQuery(formInline.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
function reset(formEl: FormInstance | undefined) {
|
||||
formEl?.resetFields();
|
||||
pageDataRef.value.setQuery({});
|
||||
}
|
||||
function showDialog(row: any) {
|
||||
saveDialog.value.openDialog({ ...row });
|
||||
}
|
||||
function remove(row: any) {
|
||||
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
|
||||
confirmButtonText: $t('message.ok'),
|
||||
cancelButtonText: $t('message.cancel'),
|
||||
type: 'warning',
|
||||
beforeClose: (action, instance, done) => {
|
||||
if (action === 'confirm') {
|
||||
instance.confirmButtonLoading = true;
|
||||
api
|
||||
.post('/api/v1/resource/remove', { id: row.id })
|
||||
.then((res) => {
|
||||
instance.confirmButtonLoading = false;
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message);
|
||||
reset(formRef.value);
|
||||
done();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
instance.confirmButtonLoading = false;
|
||||
});
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
},
|
||||
}).catch(() => {});
|
||||
}
|
||||
function preview(row: any) {
|
||||
previewDialog.value.openDialog({ ...row });
|
||||
}
|
||||
function download(row: any) {
|
||||
window.open(row.resourceUrl, '_blank');
|
||||
}
|
||||
|
||||
const fieldDefinitions = ref<any[]>([
|
||||
{
|
||||
prop: 'categoryName',
|
||||
label: $t('aiWorkflowCategory.categoryName'),
|
||||
type: 'input',
|
||||
required: true,
|
||||
placeholder: $t('aiWorkflowCategory.categoryName'),
|
||||
},
|
||||
{
|
||||
prop: 'sortNo',
|
||||
label: $t('aiWorkflowCategory.sortNo'),
|
||||
type: 'number',
|
||||
required: false,
|
||||
placeholder: $t('aiWorkflowCategory.sortNo'),
|
||||
},
|
||||
]);
|
||||
const sideList = ref<any>([]);
|
||||
const controlBtns = [
|
||||
{
|
||||
icon: Edit,
|
||||
label: $t('button.edit'),
|
||||
onClick(row: any) {
|
||||
showControlDialog(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'danger',
|
||||
icon: Delete,
|
||||
label: $t('button.delete'),
|
||||
onClick(row: any) {
|
||||
removeCategory(row);
|
||||
},
|
||||
},
|
||||
];
|
||||
const footerButton = {
|
||||
icon: Plus,
|
||||
label: $t('button.add'),
|
||||
onClick() {
|
||||
showControlDialog({});
|
||||
},
|
||||
};
|
||||
const sideDialogVisible = ref(false);
|
||||
const sideFormData = ref<any>({});
|
||||
const sideFormRef = ref<FormInstance>();
|
||||
const sideFormRules = computed(() => {
|
||||
const rules: Record<string, any[]> = {};
|
||||
fieldDefinitions.value.forEach((field) => {
|
||||
const fieldRules = [];
|
||||
if (field.required) {
|
||||
fieldRules.push({
|
||||
required: true,
|
||||
message: `${$t('message.required')}`,
|
||||
trigger: 'blur',
|
||||
});
|
||||
}
|
||||
if (fieldRules.length > 0) {
|
||||
rules[field.prop] = fieldRules;
|
||||
}
|
||||
});
|
||||
return rules;
|
||||
});
|
||||
const sideSaveLoading = ref(false);
|
||||
|
||||
function changeCategory(category: any) {
|
||||
pageDataRef.value.setQuery({ categoryId: category.id });
|
||||
}
|
||||
function showControlDialog(item: any) {
|
||||
sideFormRef.value?.resetFields();
|
||||
sideFormData.value = { ...item };
|
||||
sideDialogVisible.value = true;
|
||||
}
|
||||
function removeCategory(row: any) {
|
||||
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
|
||||
confirmButtonText: $t('message.ok'),
|
||||
cancelButtonText: $t('message.cancel'),
|
||||
type: 'warning',
|
||||
beforeClose: (action, instance, done) => {
|
||||
if (action === 'confirm') {
|
||||
instance.confirmButtonLoading = true;
|
||||
api
|
||||
.post('/api/v1/resourceCategory/remove', { id: row.id })
|
||||
.then((res) => {
|
||||
instance.confirmButtonLoading = false;
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message);
|
||||
done();
|
||||
getSideList();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
instance.confirmButtonLoading = false;
|
||||
});
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
},
|
||||
}).catch(() => {});
|
||||
}
|
||||
function handleSideSubmit() {
|
||||
formRef.value?.validate((valid) => {
|
||||
if (valid) {
|
||||
sideSaveLoading.value = true;
|
||||
const url = sideFormData.value.id
|
||||
? '/api/v1/resourceCategory/update'
|
||||
: '/api/v1/resourceCategory/save';
|
||||
api.post(url, sideFormData.value).then((res) => {
|
||||
sideSaveLoading.value = false;
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message);
|
||||
sideDialogVisible.value = false;
|
||||
getSideList();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
const getSideList = async () => {
|
||||
const [, res] = await tryit(api.get)('/api/v1/resourceCategory/list', {
|
||||
params: { sortKey: 'sortNo', sortType: 'asc' },
|
||||
});
|
||||
|
||||
if (res && res.errorCode === 0) {
|
||||
sideList.value = [
|
||||
{
|
||||
id: '',
|
||||
categoryName: $t('common.allCategories'),
|
||||
},
|
||||
...res.data,
|
||||
];
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full flex-col gap-1.5 p-6">
|
||||
<PreviewModal ref="previewDialog" />
|
||||
<ResourceModal ref="saveDialog" @reload="reset" />
|
||||
<div class="flex items-center justify-between">
|
||||
<ElForm ref="formRef" inline :model="formInline">
|
||||
<ElFormItem prop="resourceType" class="!mr-3">
|
||||
<DictSelect
|
||||
v-model="formInline.resourceType"
|
||||
dict-code="resourceType"
|
||||
:placeholder="$t('aiResource.resourceType')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="resourceName" class="!mr-3">
|
||||
<ElInput
|
||||
v-model="formInline.resourceName"
|
||||
:placeholder="$t('aiResource.resourceName')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem>
|
||||
<ElButton @click="search(formRef)" type="primary">
|
||||
{{ $t('button.query') }}
|
||||
</ElButton>
|
||||
<ElButton @click="reset(formRef)">
|
||||
{{ $t('button.reset') }}
|
||||
</ElButton>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<div class="handle-div">
|
||||
<ElButton
|
||||
v-access:code="'/api/v1/resource/save'"
|
||||
@click="showDialog({})"
|
||||
type="primary"
|
||||
>
|
||||
<ElIcon class="mr-1">
|
||||
<Plus />
|
||||
</ElIcon>
|
||||
{{ $t('button.add') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex max-h-[calc(100vh-191px)] flex-1 gap-6">
|
||||
<PageSide
|
||||
label-key="categoryName"
|
||||
value-key="id"
|
||||
:menus="sideList"
|
||||
:control-btns="controlBtns"
|
||||
:footer-button="footerButton"
|
||||
@change="changeCategory"
|
||||
/>
|
||||
<div class="bg-background h-full flex-1 overflow-auto rounded-lg p-5">
|
||||
<PageData
|
||||
ref="pageDataRef"
|
||||
page-url="/api/v1/resource/page"
|
||||
:page-size="10"
|
||||
>
|
||||
<template #default="{ pageList }">
|
||||
<ElTable :data="pageList" border>
|
||||
<ElTableColumn
|
||||
prop="resourceName"
|
||||
:label="$t('aiResource.resourceName')"
|
||||
width="300"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<ElAvatar :src="getSrc(row)" shape="square" :size="32" />
|
||||
<ElText truncated>
|
||||
{{ row.resourceName }}
|
||||
</ElText>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
align="center"
|
||||
prop="suffix"
|
||||
:label="$t('aiResource.suffix')"
|
||||
width="60"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ row.suffix }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
align="center"
|
||||
prop="fileSize"
|
||||
:label="$t('aiResource.fileSize')"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ formatBytes(row.fileSize) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
align="center"
|
||||
prop="origin"
|
||||
:label="$t('aiResource.origin')"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<Tag
|
||||
size="small"
|
||||
:background-color="`${getResourceOriginColor(row)}15`"
|
||||
:text-color="getResourceOriginColor(row)"
|
||||
:text="
|
||||
dictStore.getDictLabel('resourceOriginType', row.origin)
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
align="center"
|
||||
prop="resourceType"
|
||||
:label="$t('aiResource.resourceType')"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<Tag
|
||||
size="small"
|
||||
:background-color="`${getResourceTypeColor(row)}15`"
|
||||
:text-color="getResourceTypeColor(row)"
|
||||
:text="
|
||||
dictStore.getDictLabel('resourceType', row.resourceType)
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="created" :label="$t('aiResource.created')">
|
||||
<template #default="{ row }">
|
||||
{{ row.created }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
:label="$t('common.handle')"
|
||||
width="140"
|
||||
align="right"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center">
|
||||
<ElButton link type="primary" @click="preview(row)">
|
||||
{{ $t('button.view') }}
|
||||
</ElButton>
|
||||
<ElButton link type="primary" @click="showDialog(row)">
|
||||
{{ $t('button.edit') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
<ElDropdown>
|
||||
<ElButton link :icon="MoreFilled" />
|
||||
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownItem @click="download(row)">
|
||||
<ElButton :icon="Download" link>
|
||||
{{ $t('button.download') }}
|
||||
</ElButton>
|
||||
</ElDropdownItem>
|
||||
<div v-access:code="'/api/v1/resource/remove'">
|
||||
<ElDropdownItem @click="remove(row)">
|
||||
<ElButton type="danger" :icon="Delete" link>
|
||||
{{ $t('button.delete') }}
|
||||
</ElButton>
|
||||
</ElDropdownItem>
|
||||
</div>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</template>
|
||||
</PageData>
|
||||
</div>
|
||||
</div>
|
||||
<ElDialog
|
||||
v-model="sideDialogVisible"
|
||||
:title="sideFormData.id ? `${$t('button.edit')}` : `${$t('button.add')}`"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<ElForm
|
||||
ref="sideFormRef"
|
||||
:model="sideFormData"
|
||||
:rules="sideFormRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<!-- 动态生成表单项 -->
|
||||
<ElFormItem
|
||||
v-for="field in fieldDefinitions"
|
||||
:key="field.prop"
|
||||
:label="field.label"
|
||||
:prop="field.prop"
|
||||
>
|
||||
<ElInput
|
||||
v-if="!field.type || field.type === 'input'"
|
||||
v-model="sideFormData[field.prop]"
|
||||
:placeholder="field.placeholder"
|
||||
/>
|
||||
<ElInputNumber
|
||||
v-else-if="field.type === 'number'"
|
||||
v-model="sideFormData[field.prop]"
|
||||
:placeholder="field.placeholder"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<ElButton @click="sideDialogVisible = false">
|
||||
{{ $t('button.cancel') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
@click="handleSideSubmit"
|
||||
:loading="sideSaveLoading"
|
||||
>
|
||||
{{ $t('button.confirm') }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
168
easyflow-ui-admin/app/src/views/ai/resource/ResourceModal.vue
Normal file
168
easyflow-ui-admin/app/src/views/ai/resource/ResourceModal.vue
Normal file
@@ -0,0 +1,168 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from 'element-plus';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { getResourceType } from '@easyflow/utils';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElDialog,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import DictSelect from '#/components/dict/DictSelect.vue';
|
||||
import Upload from '#/components/upload/Upload.vue';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const emit = defineEmits(['reload']);
|
||||
// vue
|
||||
onMounted(() => {});
|
||||
defineExpose({
|
||||
openDialog,
|
||||
});
|
||||
const saveForm = ref<FormInstance>();
|
||||
// variables
|
||||
const dialogVisible = ref(false);
|
||||
const isAdd = ref(true);
|
||||
const entity = ref<any>({
|
||||
deptId: '',
|
||||
resourceType: '',
|
||||
resourceName: '',
|
||||
suffix: '',
|
||||
resourceUrl: '',
|
||||
origin: '',
|
||||
status: '',
|
||||
options: '',
|
||||
fileSize: '',
|
||||
});
|
||||
const btnLoading = ref(false);
|
||||
const rules = ref({
|
||||
deptId: [
|
||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
||||
],
|
||||
resourceType: [
|
||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
||||
],
|
||||
resourceName: [
|
||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
||||
],
|
||||
suffix: [
|
||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
||||
],
|
||||
resourceUrl: [
|
||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
||||
],
|
||||
origin: [
|
||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
||||
],
|
||||
status: [
|
||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
||||
],
|
||||
});
|
||||
// functions
|
||||
function openDialog(row: any) {
|
||||
if (row.id) {
|
||||
isAdd.value = false;
|
||||
}
|
||||
entity.value = row;
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
function save() {
|
||||
saveForm.value?.validate((valid) => {
|
||||
if (valid) {
|
||||
btnLoading.value = true;
|
||||
api
|
||||
.post(
|
||||
isAdd.value ? 'api/v1/resource/save' : 'api/v1/resource/update',
|
||||
entity.value,
|
||||
)
|
||||
.then((res) => {
|
||||
btnLoading.value = false;
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message);
|
||||
emit('reload');
|
||||
closeDialog();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
btnLoading.value = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
function closeDialog() {
|
||||
saveForm.value?.resetFields();
|
||||
isAdd.value = true;
|
||||
entity.value = {};
|
||||
dialogVisible.value = false;
|
||||
}
|
||||
function beforeUpload(f: any) {
|
||||
const fName = f?.name?.split('.')[0];
|
||||
const fExt = f?.name?.split('.')[1];
|
||||
entity.value.resourceType = getResourceType(fExt);
|
||||
entity.value.resourceName = fName;
|
||||
entity.value.suffix = fExt;
|
||||
entity.value.fileSize = f.size;
|
||||
entity.value.origin = 0;
|
||||
}
|
||||
function uploadSuccess(res: any) {
|
||||
entity.value.resourceUrl = res;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
draggable
|
||||
:title="isAdd ? $t('button.add') : $t('button.edit')"
|
||||
:before-close="closeDialog"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<ElForm
|
||||
label-width="120px"
|
||||
ref="saveForm"
|
||||
:model="entity"
|
||||
status-icon
|
||||
:rules="rules"
|
||||
>
|
||||
<ElFormItem prop="resourceUrl" :label="$t('aiResource.resourceUrl')">
|
||||
<Upload @before-upload="beforeUpload" @success="uploadSuccess" />
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="origin" :label="$t('aiResource.origin')">
|
||||
<DictSelect v-model="entity.origin" dict-code="resourceOriginType" />
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="resourceType" :label="$t('aiResource.resourceType')">
|
||||
<DictSelect v-model="entity.resourceType" dict-code="resourceType" />
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="resourceName" :label="$t('aiResource.resourceName')">
|
||||
<ElInput v-model.trim="entity.resourceName" />
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="categoryId" :label="$t('aiResource.categoryId')">
|
||||
<DictSelect
|
||||
v-model="entity.categoryId"
|
||||
dict-code="aiResourceCategory"
|
||||
/>
|
||||
</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>
|
||||
131
easyflow-ui-admin/app/src/views/ai/workflow/RunPage.vue
Normal file
131
easyflow-ui-admin/app/src/views/ai/workflow/RunPage.vue
Normal file
@@ -0,0 +1,131 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { sortNodes } from '@easyflow/utils';
|
||||
|
||||
import { ArrowLeft } from '@element-plus/icons-vue';
|
||||
import { ElAvatar, ElButton, ElCard, ElCol, ElRow } from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import workflowIcon from '#/assets/ai/workflow/workflowIcon.png';
|
||||
import { $t } from '#/locales';
|
||||
import { router } from '#/router';
|
||||
import ExecResult from '#/views/ai/workflow/components/ExecResult.vue';
|
||||
import WorkflowForm from '#/views/ai/workflow/components/WorkflowForm.vue';
|
||||
import WorkflowSteps from '#/views/ai/workflow/components/WorkflowSteps.vue';
|
||||
|
||||
onMounted(async () => {
|
||||
pageLoading.value = true;
|
||||
await Promise.all([getWorkflowInfo(workflowId.value), getRunningParams()]);
|
||||
pageLoading.value = false;
|
||||
});
|
||||
const pageLoading = ref(false);
|
||||
const route = useRoute();
|
||||
const workflowId = ref(route.query.id);
|
||||
const workflowInfo = ref<any>({});
|
||||
const runParams = ref<any>(null);
|
||||
const initState = ref(false);
|
||||
const tinyFlowData = ref<any>(null);
|
||||
const workflowForm = ref();
|
||||
async function getWorkflowInfo(workflowId: any) {
|
||||
api.get(`/api/v1/workflow/detail?id=${workflowId}`).then((res) => {
|
||||
workflowInfo.value = res.data;
|
||||
tinyFlowData.value = workflowInfo.value.content
|
||||
? JSON.parse(workflowInfo.value.content)
|
||||
: {};
|
||||
});
|
||||
}
|
||||
async function getRunningParams() {
|
||||
api
|
||||
.get(`/api/v1/workflow/getRunningParameters?id=${workflowId.value}`)
|
||||
.then((res) => {
|
||||
runParams.value = res.data;
|
||||
});
|
||||
}
|
||||
function onSubmit() {
|
||||
initState.value = !initState.value;
|
||||
}
|
||||
function resumeChain(data: any) {
|
||||
workflowForm.value?.resume(data);
|
||||
}
|
||||
const chainInfo = ref<any>(null);
|
||||
function onAsyncExecute(info: any) {
|
||||
chainInfo.value = info;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-loading="pageLoading"
|
||||
class="bg-background-deep flex h-full max-h-[calc(100vh-90px)] w-full flex-col gap-6 overflow-hidden p-6"
|
||||
>
|
||||
<div>
|
||||
<ElButton :icon="ArrowLeft" @click="router.back()">
|
||||
{{ $t('button.back') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
<div
|
||||
class="flex h-[150px] shrink-0 items-center gap-6 rounded-lg border border-[var(--el-border-color)] bg-[var(--el-bg-color)] pl-11"
|
||||
>
|
||||
<ElAvatar
|
||||
class="shrink-0"
|
||||
:src="workflowInfo.icon ?? workflowIcon"
|
||||
:size="72"
|
||||
/>
|
||||
<div class="flex flex-col gap-5">
|
||||
<span class="text-2xl font-medium">{{ workflowInfo.title }}</span>
|
||||
<span class="text-base text-[#75808d]">{{
|
||||
workflowInfo.description
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<ElRow class="h-full overflow-hidden" :gutter="10">
|
||||
<ElCol :span="10" class="h-full overflow-hidden">
|
||||
<div class="grid h-full grid-rows-2 gap-2.5">
|
||||
<ElCard shadow="never" style="height: 100%; overflow: auto">
|
||||
<div class="mb-2.5 font-semibold">
|
||||
{{ $t('aiWorkflow.params') }}:
|
||||
</div>
|
||||
<WorkflowForm
|
||||
v-if="runParams && tinyFlowData"
|
||||
ref="workflowForm"
|
||||
:workflow-id="workflowId"
|
||||
:workflow-params="runParams"
|
||||
:on-submit="onSubmit"
|
||||
:on-async-execute="onAsyncExecute"
|
||||
:tiny-flow-data="tinyFlowData"
|
||||
/>
|
||||
</ElCard>
|
||||
<ElCard shadow="never" style="height: 100%; overflow: auto">
|
||||
<div class="mb-2.5 font-semibold">
|
||||
{{ $t('aiWorkflow.steps') }}:
|
||||
</div>
|
||||
<WorkflowSteps
|
||||
v-if="tinyFlowData"
|
||||
:workflow-id="workflowId"
|
||||
:node-json="sortNodes(tinyFlowData)"
|
||||
:init-signal="initState"
|
||||
:polling-data="chainInfo"
|
||||
@resume="resumeChain"
|
||||
/>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ElCol>
|
||||
<ElCol :span="14">
|
||||
<ElCard shadow="never" style="height: 100%; overflow: auto">
|
||||
<div class="mb-2.5 mt-2.5 font-semibold">
|
||||
{{ $t('aiWorkflow.result') }}:
|
||||
</div>
|
||||
<ExecResult
|
||||
v-if="tinyFlowData"
|
||||
:workflow-id="workflowId"
|
||||
:node-json="sortNodes(tinyFlowData)"
|
||||
:init-signal="initState"
|
||||
:polling-data="chainInfo"
|
||||
/>
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</div>
|
||||
</template>
|
||||
317
easyflow-ui-admin/app/src/views/ai/workflow/WorkflowDesign.vue
Normal file
317
easyflow-ui-admin/app/src/views/ai/workflow/WorkflowDesign.vue
Normal file
@@ -0,0 +1,317 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { getOptions, sortNodes } from '@easyflow/utils';
|
||||
|
||||
import { ArrowLeft, Position } from '@element-plus/icons-vue';
|
||||
import { Tinyflow } from '@tinyflow-ai/vue';
|
||||
import { ElButton, ElDrawer, ElMessage, ElSkeleton } from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import CommonSelectDataModal from '#/components/commonSelectModal/CommonSelectDataModal.vue';
|
||||
import { $t } from '#/locales';
|
||||
import { router } from '#/router';
|
||||
import ExecResult from '#/views/ai/workflow/components/ExecResult.vue';
|
||||
import SingleRun from '#/views/ai/workflow/components/SingleRun.vue';
|
||||
import WorkflowForm from '#/views/ai/workflow/components/WorkflowForm.vue';
|
||||
import WorkflowSteps from '#/views/ai/workflow/components/WorkflowSteps.vue';
|
||||
|
||||
import { getCustomNode } from './customNode/index';
|
||||
import nodeNames from './customNode/nodeNames';
|
||||
|
||||
import '@tinyflow-ai/vue/dist/index.css';
|
||||
|
||||
const route = useRoute();
|
||||
// vue
|
||||
onMounted(async () => {
|
||||
document.addEventListener('keydown', handleKeydown);
|
||||
await Promise.all([
|
||||
loadCustomNode(),
|
||||
getLlmList(),
|
||||
getKnowledgeList(),
|
||||
getWorkflowInfo(workflowId.value),
|
||||
]);
|
||||
showTinyFlow.value = true;
|
||||
});
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('keydown', handleKeydown);
|
||||
});
|
||||
// variables
|
||||
const tinyflowRef = ref<InstanceType<typeof Tinyflow> | null>(null);
|
||||
const workflowId = ref(route.query.id);
|
||||
const workflowInfo = ref<any>({});
|
||||
const runParams = ref<any>(null);
|
||||
const tinyFlowData = ref<any>(null);
|
||||
const llmList = ref<any>([]);
|
||||
const knowledgeList = ref<any>([]);
|
||||
const provider = computed(() => ({
|
||||
llm: () => getOptions('title', 'id', llmList.value),
|
||||
knowledge: () => getOptions('title', 'id', knowledgeList.value),
|
||||
searchEngine: (): any => [
|
||||
{
|
||||
value: 'bocha-search',
|
||||
label: $t('aiWorkflow.bochaSearch'),
|
||||
},
|
||||
],
|
||||
}));
|
||||
const customNode = ref();
|
||||
const showTinyFlow = ref(false);
|
||||
const saveLoading = ref(false);
|
||||
const handleKeydown = (event: KeyboardEvent) => {
|
||||
// 检查是否是 Ctrl+S
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === 's') {
|
||||
event.preventDefault(); // 阻止浏览器默认保存行为
|
||||
if (!saveLoading.value) {
|
||||
handleSave(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
const drawerVisible = ref(false);
|
||||
const initState = ref(false);
|
||||
const singleNode = ref<any>();
|
||||
const singleRunVisible = ref(false);
|
||||
const workflowForm = ref();
|
||||
const workflowSelectRef = ref();
|
||||
const updateWorkflowNode = ref<any>(null);
|
||||
const pluginSelectRef = ref();
|
||||
const updatePluginNode = ref<any>(null);
|
||||
const pageLoading = ref(false);
|
||||
const chainInfo = ref<any>(null);
|
||||
// functions
|
||||
async function loadCustomNode() {
|
||||
customNode.value = await getCustomNode({
|
||||
handleChosen: (nodeName: string, updateNodeData: any, value: string) => {
|
||||
const v = [];
|
||||
if (value) {
|
||||
v.push(value);
|
||||
}
|
||||
if (nodeName === nodeNames.workflowNode) {
|
||||
workflowSelectRef.value.openDialog(v);
|
||||
updateWorkflowNode.value = updateNodeData;
|
||||
}
|
||||
if (nodeName === nodeNames.pluginNode) {
|
||||
pluginSelectRef.value.openDialog(v);
|
||||
updatePluginNode.value = updateNodeData;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
async function runWorkflow() {
|
||||
if (!saveLoading.value) {
|
||||
await handleSave().then(() => {
|
||||
getWorkflowInfo(workflowId.value);
|
||||
getRunningParams();
|
||||
});
|
||||
}
|
||||
}
|
||||
async function handleSave(showMsg: boolean = false) {
|
||||
saveLoading.value = true;
|
||||
await api
|
||||
.post('/api/v1/workflow/update', {
|
||||
id: workflowId.value,
|
||||
content: tinyflowRef.value?.getData(),
|
||||
})
|
||||
.then((res) => {
|
||||
saveLoading.value = false;
|
||||
if (res.errorCode === 0 && showMsg) {
|
||||
ElMessage.success(res.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
async function getWorkflowInfo(workflowId: any) {
|
||||
api.get(`/api/v1/workflow/detail?id=${workflowId}`).then((res) => {
|
||||
workflowInfo.value = res.data;
|
||||
tinyFlowData.value = workflowInfo.value.content
|
||||
? JSON.parse(workflowInfo.value.content)
|
||||
: {};
|
||||
});
|
||||
}
|
||||
async function getLlmList() {
|
||||
api.get('/api/v1/model/list').then((res) => {
|
||||
llmList.value = res.data;
|
||||
});
|
||||
}
|
||||
async function getKnowledgeList() {
|
||||
api.get('/api/v1/documentCollection/list').then((res) => {
|
||||
knowledgeList.value = res.data;
|
||||
});
|
||||
}
|
||||
function getRunningParams() {
|
||||
api
|
||||
.get(`/api/v1/workflow/getRunningParameters?id=${workflowId.value}`)
|
||||
.then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
runParams.value = res.data;
|
||||
drawerVisible.value = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
function onSubmit() {
|
||||
initState.value = !initState.value;
|
||||
}
|
||||
async function runIndependently(node: any) {
|
||||
if (node.type === 'loopNode') {
|
||||
ElMessage.warning($t('message.notSupported'));
|
||||
return;
|
||||
}
|
||||
await handleSave();
|
||||
singleNode.value = node;
|
||||
singleRunVisible.value = true;
|
||||
}
|
||||
function resumeChain(data: any) {
|
||||
workflowForm.value?.resume(data);
|
||||
}
|
||||
function handleChoose(nodeName: string, value: any) {
|
||||
if (nodeName === nodeNames.workflowNode) {
|
||||
handleWorkflowNodeUpdate(value[0]);
|
||||
}
|
||||
if (nodeName === nodeNames.pluginNode) {
|
||||
handlePluginNodeUpdate(value[0]);
|
||||
}
|
||||
}
|
||||
function handleWorkflowNodeUpdate(chooseId: any) {
|
||||
pageLoading.value = true;
|
||||
api
|
||||
.get('/api/v1/workflowNode/getChainParams', {
|
||||
params: {
|
||||
currentId: workflowId.value,
|
||||
workflowId: chooseId,
|
||||
},
|
||||
})
|
||||
.then((res) => {
|
||||
pageLoading.value = false;
|
||||
updateWorkflowNode.value(res.data);
|
||||
});
|
||||
}
|
||||
function handlePluginNodeUpdate(chooseId: any) {
|
||||
pageLoading.value = true;
|
||||
api
|
||||
.get('/api/v1/pluginItem/getTinyFlowData', {
|
||||
params: {
|
||||
id: chooseId,
|
||||
},
|
||||
})
|
||||
.then((res) => {
|
||||
pageLoading.value = false;
|
||||
updatePluginNode.value(res.data);
|
||||
});
|
||||
}
|
||||
function onAsyncExecute(info: any) {
|
||||
chainInfo.value = info;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="head-div h-full w-full" v-loading="pageLoading">
|
||||
<CommonSelectDataModal
|
||||
ref="workflowSelectRef"
|
||||
page-url="/api/v1/workflow/page"
|
||||
@get-data="(v) => handleChoose(nodeNames.workflowNode, v)"
|
||||
/>
|
||||
<CommonSelectDataModal
|
||||
:title="$t('menus.ai.plugin')"
|
||||
width="730"
|
||||
ref="pluginSelectRef"
|
||||
page-url="/api/v1/plugin/page"
|
||||
:has-parent="true"
|
||||
single-select
|
||||
@get-data="(v) => handleChoose(nodeNames.pluginNode, v)"
|
||||
/>
|
||||
<ElDrawer
|
||||
v-model="singleRunVisible"
|
||||
:title="singleNode?.data?.title"
|
||||
destroy-on-close
|
||||
size="600px"
|
||||
>
|
||||
<SingleRun :node="singleNode" :workflow-id="workflowId" />
|
||||
</ElDrawer>
|
||||
<ElDrawer v-model="drawerVisible" :title="$t('button.run')" size="600px">
|
||||
<div class="mb-2.5 font-semibold">{{ $t('aiWorkflow.params') }}:</div>
|
||||
<WorkflowForm
|
||||
ref="workflowForm"
|
||||
:workflow-id="workflowId"
|
||||
:workflow-params="runParams"
|
||||
:on-submit="onSubmit"
|
||||
:on-async-execute="onAsyncExecute"
|
||||
:tiny-flow-data="tinyFlowData"
|
||||
/>
|
||||
<div class="mb-2.5 font-semibold">{{ $t('aiWorkflow.steps') }}:</div>
|
||||
<WorkflowSteps
|
||||
:workflow-id="workflowId"
|
||||
:node-json="sortNodes(tinyFlowData)"
|
||||
:init-signal="initState"
|
||||
:polling-data="chainInfo"
|
||||
@resume="resumeChain"
|
||||
/>
|
||||
<div class="mb-2.5 mt-2.5 font-semibold">
|
||||
{{ $t('aiWorkflow.result') }}:
|
||||
</div>
|
||||
<ExecResult
|
||||
:workflow-id="workflowId"
|
||||
:node-json="sortNodes(tinyFlowData)"
|
||||
:init-signal="initState"
|
||||
:polling-data="chainInfo"
|
||||
/>
|
||||
</ElDrawer>
|
||||
<div class="flex items-center justify-between border-b p-2.5">
|
||||
<div>
|
||||
<ElButton :icon="ArrowLeft" link @click="router.back()">
|
||||
<span
|
||||
class="max-w-[500px] overflow-hidden text-ellipsis text-nowrap text-base"
|
||||
style="font-size: 14px"
|
||||
:title="workflowInfo.title"
|
||||
>
|
||||
{{ workflowInfo.title }}
|
||||
</span>
|
||||
</ElButton>
|
||||
</div>
|
||||
<div>
|
||||
<ElButton :disabled="saveLoading" :icon="Position" @click="runWorkflow">
|
||||
{{ $t('button.runTest') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
:disabled="saveLoading"
|
||||
@click="handleSave(true)"
|
||||
>
|
||||
{{ $t('button.save') }}(ctrl+s)
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
<Tinyflow
|
||||
ref="tinyflowRef"
|
||||
v-if="showTinyFlow"
|
||||
class="tiny-flow-container"
|
||||
:data="JSON.parse(JSON.stringify(tinyFlowData))"
|
||||
:provider="provider"
|
||||
:custom-nodes="customNode"
|
||||
:on-node-execute="runIndependently"
|
||||
/>
|
||||
<ElSkeleton class="load-div" v-else :rows="5" animated />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.tf-toolbar-container-body) {
|
||||
height: calc(100vh - 365px) !important;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
:deep(.agentsflow) {
|
||||
height: calc(100vh - 130px) !important;
|
||||
}
|
||||
|
||||
.head-div {
|
||||
background-color: var(--el-bg-color);
|
||||
}
|
||||
|
||||
.tiny-flow-container {
|
||||
width: 100%;
|
||||
height: calc(100vh - 150px);
|
||||
}
|
||||
|
||||
.load-div {
|
||||
margin: 20px;
|
||||
}
|
||||
</style>
|
||||
451
easyflow-ui-admin/app/src/views/ai/workflow/WorkflowList.vue
Normal file
451
easyflow-ui-admin/app/src/views/ai/workflow/WorkflowList.vue
Normal file
@@ -0,0 +1,451 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from 'element-plus';
|
||||
|
||||
import type { ActionButton } from '#/components/page/CardList.vue';
|
||||
|
||||
import { computed, markRaw, onMounted, ref } from 'vue';
|
||||
|
||||
import {
|
||||
CopyDocument,
|
||||
Delete,
|
||||
Download,
|
||||
Edit,
|
||||
Plus,
|
||||
Tickets,
|
||||
Upload,
|
||||
VideoPlay,
|
||||
} from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElButton,
|
||||
ElDialog,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElInputNumber,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
} from 'element-plus';
|
||||
import { tryit } from 'radash';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import workflowIcon from '#/assets/ai/workflow/workflowIcon.png';
|
||||
// import workflowSvg from '#/assets/workflow.svg';
|
||||
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
||||
import DesignIcon from '#/components/icons/DesignIcon.vue';
|
||||
import CardList from '#/components/page/CardList.vue';
|
||||
import PageData from '#/components/page/PageData.vue';
|
||||
import PageSide from '#/components/page/PageSide.vue';
|
||||
import { $t } from '#/locales';
|
||||
import { router } from '#/router';
|
||||
import { useDictStore } from '#/store';
|
||||
|
||||
import WorkflowModal from './WorkflowModal.vue';
|
||||
|
||||
interface FieldDefinition {
|
||||
// 字段名称
|
||||
prop: string;
|
||||
// 字段标签
|
||||
label: string;
|
||||
// 字段类型:input, number, select, radio, checkbox, switch, date, datetime
|
||||
type?: 'input' | 'number';
|
||||
// 是否必填
|
||||
required?: boolean;
|
||||
// 占位符
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
const actions: ActionButton[] = [
|
||||
{
|
||||
icon: Edit,
|
||||
text: $t('button.edit'),
|
||||
className: '',
|
||||
permission: '/api/v1/workflow/save',
|
||||
onClick: (row: any) => {
|
||||
showDialog(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: DesignIcon,
|
||||
text: $t('button.design'),
|
||||
className: '',
|
||||
permission: '/api/v1/workflow/save',
|
||||
onClick: (row: any) => {
|
||||
toDesignPage(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: VideoPlay,
|
||||
text: $t('button.run'),
|
||||
className: '',
|
||||
permission: '',
|
||||
onClick: (row: any) => {
|
||||
router.push({
|
||||
name: 'RunPage',
|
||||
query: {
|
||||
id: row.id,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: Tickets,
|
||||
text: $t('aiWorkflowExecRecord.moduleName'),
|
||||
className: '',
|
||||
permission: '/api/v1/workflow/save',
|
||||
onClick: (row: any) => {
|
||||
router.push({
|
||||
name: 'ExecRecord',
|
||||
query: {
|
||||
workflowId: row.id,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: Download,
|
||||
text: $t('button.export'),
|
||||
className: '',
|
||||
permission: '',
|
||||
onClick: (row: any) => {
|
||||
exportJson(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: CopyDocument,
|
||||
text: $t('button.copy'),
|
||||
className: '',
|
||||
permission: '',
|
||||
onClick: (row: any) => {
|
||||
showDialog({
|
||||
title: `${row.title}Copy`,
|
||||
content: row.content,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: Delete,
|
||||
text: $t('button.delete'),
|
||||
className: 'item-danger',
|
||||
permission: '',
|
||||
onClick: (row: any) => {
|
||||
remove(row);
|
||||
},
|
||||
},
|
||||
];
|
||||
onMounted(() => {
|
||||
initDict();
|
||||
getSideList();
|
||||
});
|
||||
const pageDataRef = ref();
|
||||
const saveDialog = ref();
|
||||
const dictStore = useDictStore();
|
||||
const headerButtons = [
|
||||
{
|
||||
key: 'create',
|
||||
text: $t('button.add'),
|
||||
icon: markRaw(Plus),
|
||||
type: 'primary',
|
||||
data: { action: 'create' },
|
||||
permission: '/api/v1/workflow/save',
|
||||
},
|
||||
{
|
||||
key: 'import',
|
||||
text: $t('button.import'),
|
||||
icon: markRaw(Upload),
|
||||
type: 'default',
|
||||
data: { action: 'import' },
|
||||
permission: '/api/v1/workflow/save',
|
||||
},
|
||||
];
|
||||
|
||||
function initDict() {
|
||||
dictStore.fetchDictionary('dataStatus');
|
||||
}
|
||||
const handleSearch = (params: string) => {
|
||||
pageDataRef.value.setQuery({ title: params, isQueryOr: true });
|
||||
};
|
||||
function reset() {
|
||||
pageDataRef.value.setQuery({});
|
||||
}
|
||||
function showDialog(row: any, importMode = false) {
|
||||
saveDialog.value.openDialog({ ...row }, importMode);
|
||||
}
|
||||
function remove(row: any) {
|
||||
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
|
||||
confirmButtonText: $t('message.ok'),
|
||||
cancelButtonText: $t('message.cancel'),
|
||||
type: 'warning',
|
||||
beforeClose: (action, instance, done) => {
|
||||
if (action === 'confirm') {
|
||||
instance.confirmButtonLoading = true;
|
||||
api
|
||||
.post('/api/v1/workflow/remove', { id: row.id })
|
||||
.then((res) => {
|
||||
instance.confirmButtonLoading = false;
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message);
|
||||
reset();
|
||||
done();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
instance.confirmButtonLoading = false;
|
||||
});
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
},
|
||||
}).catch(() => {});
|
||||
}
|
||||
function toDesignPage(row: any) {
|
||||
router.push({
|
||||
name: 'WorkflowDesign',
|
||||
query: {
|
||||
id: row.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
function exportJson(row: any) {
|
||||
api
|
||||
.get('/api/v1/workflow/exportWorkFlow', {
|
||||
params: {
|
||||
id: row.id,
|
||||
},
|
||||
})
|
||||
.then((res) => {
|
||||
const text = res.data;
|
||||
const element = document.createElement('a');
|
||||
element.setAttribute(
|
||||
'href',
|
||||
`data:text/plain;charset=utf-8,${encodeURIComponent(text)}`,
|
||||
);
|
||||
element.setAttribute('download', `${row.title}.json`);
|
||||
element.style.display = 'none';
|
||||
document.body.append(element);
|
||||
element.click();
|
||||
element.remove();
|
||||
ElMessage.success($t('message.downloadSuccess'));
|
||||
});
|
||||
}
|
||||
const fieldDefinitions = ref<FieldDefinition[]>([
|
||||
{
|
||||
prop: 'categoryName',
|
||||
label: $t('aiWorkflowCategory.categoryName'),
|
||||
type: 'input',
|
||||
required: true,
|
||||
placeholder: $t('aiWorkflowCategory.categoryName'),
|
||||
},
|
||||
{
|
||||
prop: 'sortNo',
|
||||
label: $t('aiWorkflowCategory.sortNo'),
|
||||
type: 'number',
|
||||
required: false,
|
||||
placeholder: $t('aiWorkflowCategory.sortNo'),
|
||||
},
|
||||
]);
|
||||
|
||||
const formData = ref<any>({});
|
||||
const dialogVisible = ref(false);
|
||||
const formRef = ref<FormInstance>();
|
||||
const saveLoading = ref(false);
|
||||
const sideList = ref<any[]>([]);
|
||||
const controlBtns = [
|
||||
{
|
||||
icon: Edit,
|
||||
label: $t('button.edit'),
|
||||
onClick(row: any) {
|
||||
showControlDialog(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'danger',
|
||||
icon: Delete,
|
||||
label: $t('button.delete'),
|
||||
onClick(row: any) {
|
||||
removeCategory(row);
|
||||
},
|
||||
},
|
||||
];
|
||||
const footerButton = {
|
||||
icon: Plus,
|
||||
label: $t('button.add'),
|
||||
onClick() {
|
||||
showControlDialog({});
|
||||
},
|
||||
};
|
||||
|
||||
const formRules = computed(() => {
|
||||
const rules: Record<string, any[]> = {};
|
||||
fieldDefinitions.value.forEach((field) => {
|
||||
const fieldRules = [];
|
||||
if (field.required) {
|
||||
fieldRules.push({
|
||||
required: true,
|
||||
message: `${$t('message.required')}`,
|
||||
trigger: 'blur',
|
||||
});
|
||||
}
|
||||
if (fieldRules.length > 0) {
|
||||
rules[field.prop] = fieldRules;
|
||||
}
|
||||
});
|
||||
return rules;
|
||||
});
|
||||
|
||||
function changeCategory(category: any) {
|
||||
pageDataRef.value.setQuery({ categoryId: category.id });
|
||||
}
|
||||
function showControlDialog(item: any) {
|
||||
formRef.value?.resetFields();
|
||||
formData.value = { ...item };
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
function removeCategory(row: any) {
|
||||
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
|
||||
confirmButtonText: $t('message.ok'),
|
||||
cancelButtonText: $t('message.cancel'),
|
||||
type: 'warning',
|
||||
beforeClose: (action, instance, done) => {
|
||||
if (action === 'confirm') {
|
||||
instance.confirmButtonLoading = true;
|
||||
api
|
||||
.post('/api/v1/workflowCategory/remove', { id: row.id })
|
||||
.then((res) => {
|
||||
instance.confirmButtonLoading = false;
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message);
|
||||
done();
|
||||
getSideList();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
instance.confirmButtonLoading = false;
|
||||
});
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
},
|
||||
}).catch(() => {});
|
||||
}
|
||||
function handleSubmit() {
|
||||
formRef.value?.validate((valid) => {
|
||||
if (valid) {
|
||||
saveLoading.value = true;
|
||||
const url = formData.value.id
|
||||
? '/api/v1/workflowCategory/update'
|
||||
: '/api/v1/workflowCategory/save';
|
||||
api.post(url, formData.value).then((res) => {
|
||||
saveLoading.value = false;
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message);
|
||||
dialogVisible.value = false;
|
||||
getSideList();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
const getSideList = async () => {
|
||||
const [, res] = await tryit(api.get)('/api/v1/workflowCategory/list', {
|
||||
params: { sortKey: 'sortNo', sortType: 'asc' },
|
||||
});
|
||||
|
||||
if (res && res.errorCode === 0) {
|
||||
sideList.value = [
|
||||
{
|
||||
id: '',
|
||||
categoryName: $t('common.allCategories'),
|
||||
},
|
||||
...res.data,
|
||||
];
|
||||
}
|
||||
};
|
||||
function handleHeaderButtonClick(data: any) {
|
||||
if (data.data.action === 'import') {
|
||||
showDialog({}, true);
|
||||
} else {
|
||||
showDialog({});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full flex-col gap-6 p-6">
|
||||
<WorkflowModal ref="saveDialog" @reload="reset" />
|
||||
<HeaderSearch
|
||||
:buttons="headerButtons"
|
||||
@search="handleSearch"
|
||||
@button-click="handleHeaderButtonClick"
|
||||
/>
|
||||
<div class="flex max-h-[calc(100vh-191px)] flex-1 gap-6">
|
||||
<PageSide
|
||||
label-key="categoryName"
|
||||
value-key="id"
|
||||
:menus="sideList"
|
||||
:control-btns="controlBtns"
|
||||
:footer-button="footerButton"
|
||||
@change="changeCategory"
|
||||
/>
|
||||
<div class="h-full flex-1 overflow-auto">
|
||||
<PageData
|
||||
ref="pageDataRef"
|
||||
page-url="/api/v1/workflow/page"
|
||||
:page-sizes="[12, 18, 24]"
|
||||
:page-size="12"
|
||||
>
|
||||
<template #default="{ pageList }">
|
||||
<CardList
|
||||
:default-icon="workflowIcon"
|
||||
:data="pageList"
|
||||
:actions="actions"
|
||||
/>
|
||||
</template>
|
||||
</PageData>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="formData.id ? `${$t('button.edit')}` : `${$t('button.add')}`"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<ElForm
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<!-- 动态生成表单项 -->
|
||||
<ElFormItem
|
||||
v-for="field in fieldDefinitions"
|
||||
:key="field.prop"
|
||||
:label="field.label"
|
||||
:prop="field.prop"
|
||||
>
|
||||
<ElInput
|
||||
v-if="!field.type || field.type === 'input'"
|
||||
v-model="formData[field.prop]"
|
||||
:placeholder="field.placeholder"
|
||||
/>
|
||||
<ElInputNumber
|
||||
v-else-if="field.type === 'number'"
|
||||
v-model="formData[field.prop]"
|
||||
:placeholder="field.placeholder"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<ElButton @click="dialogVisible = false">
|
||||
{{ $t('button.cancel') }}
|
||||
</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit" :loading="saveLoading">
|
||||
{{ $t('button.confirm') }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
231
easyflow-ui-admin/app/src/views/ai/workflow/WorkflowModal.vue
Normal file
231
easyflow-ui-admin/app/src/views/ai/workflow/WorkflowModal.vue
Normal file
@@ -0,0 +1,231 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance, UploadInstance, UploadProps } from 'element-plus';
|
||||
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElDialog,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
ElUpload,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import DictSelect from '#/components/dict/DictSelect.vue';
|
||||
// import Cropper from '#/components/upload/Cropper.vue';
|
||||
import UploadAvatar from '#/components/upload/UploadAvatar.vue';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const emit = defineEmits(['reload']);
|
||||
// vue
|
||||
onMounted(() => {});
|
||||
defineExpose({
|
||||
openDialog,
|
||||
});
|
||||
const saveForm = ref<FormInstance>();
|
||||
// variables
|
||||
const dialogVisible = ref(false);
|
||||
const isAdd = ref(true);
|
||||
const isImport = ref(false);
|
||||
const jsonFile = ref<any>(null);
|
||||
const uploadFileList = ref<any[]>([]);
|
||||
const uploadRef = ref<UploadInstance>();
|
||||
const entity = ref<any>({
|
||||
alias: '',
|
||||
deptId: '',
|
||||
title: '',
|
||||
description: '',
|
||||
icon: '',
|
||||
content: '',
|
||||
englishName: '',
|
||||
});
|
||||
const btnLoading = ref(false);
|
||||
const jsonFileModel = computed({
|
||||
get: () => (uploadFileList.value.length > 0 ? uploadFileList.value[0] : null),
|
||||
set: (value: any) => {
|
||||
if (!value) {
|
||||
uploadFileList.value = [];
|
||||
}
|
||||
},
|
||||
});
|
||||
const rules = computed(() => ({
|
||||
title: [{ required: true, message: $t('message.required'), trigger: 'blur' }],
|
||||
...(isImport.value && {
|
||||
jsonFile: [
|
||||
{ required: true, message: $t('message.required'), trigger: 'change' },
|
||||
],
|
||||
}),
|
||||
}));
|
||||
// functions
|
||||
function openDialog(row: any, importMode = false) {
|
||||
isImport.value = importMode;
|
||||
if (row.id) {
|
||||
isAdd.value = false;
|
||||
}
|
||||
entity.value = row;
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
const beforeUpload: UploadProps['beforeUpload'] = (file) => {
|
||||
jsonFile.value = file;
|
||||
uploadFileList.value = [file];
|
||||
saveForm.value?.clearValidate('jsonFile');
|
||||
return false;
|
||||
};
|
||||
const handleChange: UploadProps['onChange'] = (file, fileList) => {
|
||||
jsonFile.value = file.raw;
|
||||
uploadFileList.value = fileList.slice(-1);
|
||||
saveForm.value?.clearValidate('jsonFile');
|
||||
};
|
||||
const handleRemove: UploadProps['onRemove'] = () => {
|
||||
jsonFile.value = null;
|
||||
uploadFileList.value = [];
|
||||
saveForm.value?.clearValidate('jsonFile');
|
||||
};
|
||||
function save() {
|
||||
saveForm.value?.validate((valid) => {
|
||||
if (valid) {
|
||||
btnLoading.value = true;
|
||||
if (isImport.value) {
|
||||
const formData = new FormData();
|
||||
formData.append('jsonFile', jsonFile.value!);
|
||||
Object.keys(entity.value).forEach((key) => {
|
||||
if (entity.value[key] !== null && entity.value[key] !== undefined) {
|
||||
formData.append(key, entity.value[key]);
|
||||
}
|
||||
});
|
||||
api
|
||||
.post('/api/v1/workflow/importWorkFlow', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
})
|
||||
.then((res) => {
|
||||
btnLoading.value = false;
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message);
|
||||
emit('reload');
|
||||
closeDialog();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
btnLoading.value = false;
|
||||
});
|
||||
} else {
|
||||
api
|
||||
.post(
|
||||
isAdd.value ? '/api/v1/workflow/save' : '/api/v1/workflow/update',
|
||||
entity.value,
|
||||
)
|
||||
.then((res) => {
|
||||
btnLoading.value = false;
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message);
|
||||
emit('reload');
|
||||
closeDialog();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
btnLoading.value = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
function closeDialog() {
|
||||
saveForm.value?.resetFields();
|
||||
uploadRef.value?.clearFiles();
|
||||
uploadFileList.value = [];
|
||||
jsonFile.value = null;
|
||||
isAdd.value = true;
|
||||
isImport.value = false;
|
||||
entity.value = {};
|
||||
dialogVisible.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
draggable
|
||||
:title="
|
||||
isImport
|
||||
? $t('button.import')
|
||||
: isAdd
|
||||
? $t('button.add')
|
||||
: $t('button.edit')
|
||||
"
|
||||
:before-close="closeDialog"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<ElForm
|
||||
label-width="120px"
|
||||
ref="saveForm"
|
||||
:model="isImport ? { ...entity, jsonFile: jsonFileModel } : entity"
|
||||
status-icon
|
||||
:rules="rules"
|
||||
>
|
||||
<ElFormItem v-if="isImport" prop="jsonFile" label="JSON文件" required>
|
||||
<ElUpload
|
||||
class="w-full"
|
||||
ref="uploadRef"
|
||||
v-model:file-list="uploadFileList"
|
||||
:limit="1"
|
||||
:auto-upload="false"
|
||||
:on-change="handleChange"
|
||||
:before-upload="beforeUpload"
|
||||
:on-remove="handleRemove"
|
||||
accept=".json"
|
||||
drag
|
||||
>
|
||||
<div class="el-upload__text w-full">
|
||||
将 json 文件拖到此处,或<em>点击上传</em>
|
||||
</div>
|
||||
</ElUpload>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="icon" :label="$t('aiWorkflow.icon')">
|
||||
<!-- <Cropper v-model="entity.icon" crop /> -->
|
||||
<UploadAvatar v-model="entity.icon" />
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="title" :label="$t('aiWorkflow.title')">
|
||||
<ElInput v-model.trim="entity.title" />
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="categoryId" :label="$t('aiWorkflow.categoryId')">
|
||||
<DictSelect
|
||||
v-model="entity.categoryId"
|
||||
dict-code="aiWorkFlowCategory"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="alias" :label="$t('aiWorkflow.alias')">
|
||||
<ElInput v-model.trim="entity.alias" />
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="englishName" :label="$t('aiWorkflow.englishName')">
|
||||
<ElInput v-model.trim="entity.englishName" />
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="description" :label="$t('aiWorkflow.description')">
|
||||
<ElInput v-model.trim="entity.description" />
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="status" :label="$t('aiWorkflow.status')">
|
||||
<DictSelect v-model="entity.status" dict-code="showOrNot" />
|
||||
</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>
|
||||
@@ -0,0 +1,211 @@
|
||||
<script setup lang="ts">
|
||||
import { Download } from '@element-plus/icons-vue';
|
||||
import { ElIcon, ElText } from 'element-plus';
|
||||
|
||||
import confirmFile from '#/assets/ai/workflow/confirm-file.png';
|
||||
// 导入你的图片资源
|
||||
// 请确保路径正确,或者将图片放在 public 目录下引用
|
||||
import confirmOther from '#/assets/ai/workflow/confirm-other.png';
|
||||
|
||||
// 定义 Props
|
||||
const props = defineProps({
|
||||
// v-model 绑定值
|
||||
modelValue: {
|
||||
type: [String, Number, Object],
|
||||
default: null,
|
||||
},
|
||||
// 数据类型: text, image, video, audio, other, file
|
||||
selectionDataType: {
|
||||
type: String,
|
||||
default: 'text',
|
||||
},
|
||||
// 数据列表
|
||||
selectionData: {
|
||||
type: Array as () => any[],
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
// 定义 Emits
|
||||
const emit = defineEmits(['update:modelValue', 'change']);
|
||||
|
||||
// 判断是否选中
|
||||
const isSelected = (item: any) => {
|
||||
return props.modelValue === item;
|
||||
};
|
||||
|
||||
// 切换选中状态
|
||||
const changeValue = (item: any) => {
|
||||
if (props.modelValue === item) {
|
||||
// 如果点击已选中的,则取消选中
|
||||
emit('update:modelValue', null);
|
||||
emit('change', null); // 触发 Element Plus 表单验证
|
||||
} else {
|
||||
emit('update:modelValue', item);
|
||||
emit('change', item); // 触发 Element Plus 表单验证
|
||||
}
|
||||
};
|
||||
|
||||
// 获取图标
|
||||
const getIcon = (type: string) => {
|
||||
return type === 'other' ? confirmOther : confirmFile;
|
||||
};
|
||||
|
||||
// 下载处理
|
||||
const handleDownload = (url: string) => {
|
||||
window.open(url, '_blank');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="custom-radio-group">
|
||||
<template v-for="(item, index) in selectionData" :key="index">
|
||||
<!-- 类型: Text -->
|
||||
<div
|
||||
v-if="selectionDataType === 'text'"
|
||||
class="custom-radio-option"
|
||||
:class="{ selected: isSelected(item) }"
|
||||
style="width: 100%; flex-shrink: 0"
|
||||
@click="changeValue(item)"
|
||||
>
|
||||
{{ item }}
|
||||
</div>
|
||||
|
||||
<!-- 类型: Image -->
|
||||
<div
|
||||
v-else-if="selectionDataType === 'image'"
|
||||
class="custom-radio-option"
|
||||
:class="{ selected: isSelected(item) }"
|
||||
style="padding: 0"
|
||||
@click="changeValue(item)"
|
||||
>
|
||||
<img
|
||||
:src="item"
|
||||
alt=""
|
||||
style="width: 80px; height: 80px; border-radius: 8px; display: block"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 类型: Video -->
|
||||
<div
|
||||
v-else-if="selectionDataType === 'video'"
|
||||
class="custom-radio-option"
|
||||
:class="{ selected: isSelected(item) }"
|
||||
@click="changeValue(item)"
|
||||
>
|
||||
<video controls :src="item" style="width: 162px; height: 141px"></video>
|
||||
</div>
|
||||
|
||||
<!-- 类型: Audio -->
|
||||
<div
|
||||
v-else-if="selectionDataType === 'audio'"
|
||||
class="custom-radio-option"
|
||||
:class="{ selected: isSelected(item) }"
|
||||
style="width: 100%; flex-shrink: 0"
|
||||
@click="changeValue(item)"
|
||||
>
|
||||
<audio
|
||||
controls
|
||||
:src="item"
|
||||
style="width: 100%; height: 44px; margin-top: 8px"
|
||||
></audio>
|
||||
</div>
|
||||
|
||||
<!-- 类型: File 或 Other -->
|
||||
<div
|
||||
v-else-if="
|
||||
selectionDataType === 'other' || selectionDataType === 'file'
|
||||
"
|
||||
class="custom-radio-option"
|
||||
:class="{ selected: isSelected(item) }"
|
||||
style="width: 100%; flex-shrink: 0"
|
||||
@click="changeValue(item)"
|
||||
>
|
||||
<div
|
||||
style="
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
"
|
||||
>
|
||||
<div style="width: 92%; display: flex; align-items: center">
|
||||
<img
|
||||
style="width: 20px; height: 20px; margin-right: 8px"
|
||||
alt=""
|
||||
:src="getIcon(selectionDataType)"
|
||||
/>
|
||||
<!-- 使用 Element Plus 的 Text 组件处理省略号,如果没有安装 Element Plus,可以用普通的 span + css -->
|
||||
<ElText truncated>
|
||||
{{ item }}
|
||||
</ElText>
|
||||
</div>
|
||||
<div class="download-icon-btn" @click.stop="handleDownload(item)">
|
||||
<ElIcon><Download /></ElIcon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.custom-radio-group {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.custom-radio-option {
|
||||
background-color: var(--el-bg-color);
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
box-shadow: 0 0 0 1px var(--el-border-color);
|
||||
transition: all 0.2s;
|
||||
box-sizing: border-box; /* 确保 padding 不会撑大宽度 */
|
||||
}
|
||||
|
||||
.custom-radio-option:hover {
|
||||
box-shadow: 0 0 0 1px var(--el-color-primary-light-5);
|
||||
}
|
||||
|
||||
.custom-radio-option.selected {
|
||||
box-shadow: 0 0 0 1px var(--el-color-primary-light-3);
|
||||
padding: 8px;
|
||||
background: var(--el-color-primary-light-9);
|
||||
}
|
||||
|
||||
.custom-radio-option.selected::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-color: var(--el-color-primary);
|
||||
border-radius: 6px 2px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.custom-radio-option.selected::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 3px;
|
||||
bottom: 7px;
|
||||
width: 9px;
|
||||
height: 4px;
|
||||
border-left: 1px solid white;
|
||||
border-bottom: 1px solid white;
|
||||
transform: rotate(-45deg);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.download-icon-btn {
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
margin-right: 10px;
|
||||
display: flex; /* 为了对齐图标 */
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,216 @@
|
||||
<script setup lang="ts">
|
||||
import { Download } from '@element-plus/icons-vue';
|
||||
import { ElIcon, ElText } from 'element-plus';
|
||||
|
||||
import confirmFile from '#/assets/ai/workflow/confirm-file.png';
|
||||
// 导入你的图片资源
|
||||
import confirmOther from '#/assets/ai/workflow/confirm-other.png';
|
||||
|
||||
// 定义 Props
|
||||
const props = defineProps({
|
||||
// v-model 绑定值,多选版本这里是数组
|
||||
modelValue: {
|
||||
type: Array as () => any[],
|
||||
default: () => [],
|
||||
},
|
||||
// 数据类型: text, image, video, audio, other, file
|
||||
selectionDataType: {
|
||||
type: String,
|
||||
default: 'text',
|
||||
},
|
||||
// 数据列表
|
||||
selectionData: {
|
||||
type: Array as () => any[],
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
// 定义 Emits
|
||||
const emit = defineEmits(['update:modelValue', 'change']);
|
||||
|
||||
// 判断是否选中
|
||||
const isSelected = (item: any) => {
|
||||
return props.modelValue && props.modelValue.includes(item);
|
||||
};
|
||||
|
||||
// 切换选中状态 (多选逻辑)
|
||||
const changeValue = (item: any) => {
|
||||
// 复制一份当前数组,避免直接修改 prop
|
||||
const currentValues = props.modelValue ? [...props.modelValue] : [];
|
||||
|
||||
const index = currentValues.indexOf(item);
|
||||
|
||||
if (index === -1) {
|
||||
// 如果不存在,则添加
|
||||
currentValues.push(item);
|
||||
} else {
|
||||
// 如果已存在,则移除
|
||||
currentValues.splice(index, 1);
|
||||
}
|
||||
|
||||
// 更新 v-model
|
||||
emit('update:modelValue', currentValues);
|
||||
// 触发 Element Plus 表单验证
|
||||
emit('change', currentValues);
|
||||
};
|
||||
|
||||
// 获取图标
|
||||
const getIcon = (type: string) => {
|
||||
return type === 'other' ? confirmOther : confirmFile;
|
||||
};
|
||||
|
||||
// 下载处理
|
||||
const handleDownload = (url: string) => {
|
||||
window.open(url, '_blank');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="custom-radio-group">
|
||||
<template v-for="(item, index) in selectionData" :key="index">
|
||||
<!-- 类型: Text -->
|
||||
<div
|
||||
v-if="selectionDataType === 'text'"
|
||||
class="custom-radio-option"
|
||||
:class="{ selected: isSelected(item) }"
|
||||
style="width: 100%; flex-shrink: 0"
|
||||
@click="changeValue(item)"
|
||||
>
|
||||
{{ item }}
|
||||
</div>
|
||||
|
||||
<!-- 类型: Image -->
|
||||
<div
|
||||
v-else-if="selectionDataType === 'image'"
|
||||
class="custom-radio-option"
|
||||
:class="{ selected: isSelected(item) }"
|
||||
style="padding: 0"
|
||||
@click="changeValue(item)"
|
||||
>
|
||||
<img
|
||||
:src="item"
|
||||
alt=""
|
||||
style="width: 80px; height: 80px; border-radius: 8px; display: block"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 类型: Video -->
|
||||
<div
|
||||
v-else-if="selectionDataType === 'video'"
|
||||
class="custom-radio-option"
|
||||
:class="{ selected: isSelected(item) }"
|
||||
@click="changeValue(item)"
|
||||
>
|
||||
<video controls :src="item" style="width: 162px; height: 141px"></video>
|
||||
</div>
|
||||
|
||||
<!-- 类型: Audio -->
|
||||
<div
|
||||
v-else-if="selectionDataType === 'audio'"
|
||||
class="custom-radio-option"
|
||||
:class="{ selected: isSelected(item) }"
|
||||
style="width: 300px; flex-shrink: 0"
|
||||
@click="changeValue(item)"
|
||||
>
|
||||
<audio controls :src="item" style="width: 100%; height: 40px"></audio>
|
||||
</div>
|
||||
|
||||
<!-- 类型: File 或 Other -->
|
||||
<div
|
||||
v-else-if="
|
||||
selectionDataType === 'other' || selectionDataType === 'file'
|
||||
"
|
||||
class="custom-radio-option"
|
||||
:class="{ selected: isSelected(item) }"
|
||||
style="width: 100%; flex-shrink: 0"
|
||||
@click="changeValue(item)"
|
||||
>
|
||||
<div
|
||||
style="
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
"
|
||||
>
|
||||
<div style="width: 92%; display: flex; align-items: center">
|
||||
<img
|
||||
style="width: 20px; height: 20px; margin-right: 8px"
|
||||
alt=""
|
||||
:src="getIcon(selectionDataType)"
|
||||
/>
|
||||
<!-- 使用 Element Plus 的 Text 组件处理省略号 -->
|
||||
<ElText truncated>
|
||||
{{ item }}
|
||||
</ElText>
|
||||
</div>
|
||||
<div class="download-icon-btn" @click.stop="handleDownload(item)">
|
||||
<ElIcon><Download /></ElIcon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 这里复用之前的 CSS,样式完全一致 */
|
||||
.custom-radio-group {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.custom-radio-option {
|
||||
background-color: var(--el-bg-color);
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
box-shadow: 0 0 0 1px var(--el-border-color);
|
||||
transition: all 0.2s;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.custom-radio-option:hover {
|
||||
box-shadow: 0 0 0 1px var(--el-color-primary-light-5);
|
||||
}
|
||||
|
||||
.custom-radio-option.selected {
|
||||
box-shadow: 0 0 0 1px var(--el-color-primary-light-3);
|
||||
padding: 8px;
|
||||
background: var(--el-color-primary-light-9);
|
||||
}
|
||||
|
||||
.custom-radio-option.selected::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-color: var(--el-color-primary);
|
||||
border-radius: 6px 2px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.custom-radio-option.selected::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 3px;
|
||||
bottom: 7px;
|
||||
width: 9px;
|
||||
height: 5px;
|
||||
border-left: 1px solid white;
|
||||
border-bottom: 1px solid white;
|
||||
transform: rotate(-45deg);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.download-icon-btn {
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
margin-right: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,97 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import { preferences } from '@easyflow/preferences';
|
||||
|
||||
import { ElEmpty, ElMessage, ElRow } from 'element-plus';
|
||||
|
||||
import ShowJson from '#/components/json/ShowJson.vue';
|
||||
import { $t } from '#/locales';
|
||||
import ExecResultItem from '#/views/ai/workflow/components/ExecResultItem.vue';
|
||||
|
||||
export interface ExecResultProps {
|
||||
workflowId: any;
|
||||
nodeJson: any;
|
||||
initSignal?: boolean;
|
||||
pollingData?: any;
|
||||
}
|
||||
const props = defineProps<ExecResultProps>();
|
||||
|
||||
const finalNode = computed(() => {
|
||||
const nodes = props.nodeJson;
|
||||
if (nodes.length > 0) {
|
||||
let endNode = nodes[nodes.length - 1].original;
|
||||
for (const node of nodes) {
|
||||
if (node.original.type === 'endNode') {
|
||||
endNode = node.original;
|
||||
}
|
||||
}
|
||||
return endNode;
|
||||
}
|
||||
return {};
|
||||
});
|
||||
const result = ref('');
|
||||
const success = ref(false);
|
||||
watch(
|
||||
() => props.initSignal,
|
||||
() => {
|
||||
result.value = '';
|
||||
},
|
||||
);
|
||||
watch(
|
||||
() => props.pollingData,
|
||||
(newVal) => {
|
||||
if (newVal.status === 20) {
|
||||
ElMessage.success($t('message.success'));
|
||||
result.value = newVal.result;
|
||||
success.value = true;
|
||||
}
|
||||
if (newVal.status === 21) {
|
||||
ElMessage.error($t('message.fail'));
|
||||
result.value = newVal.message;
|
||||
success.value = false;
|
||||
}
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
function getResultCount(res: any[]) {
|
||||
if (res.length > 1 || finalNode.value.data.outputDefs.length > 1) {
|
||||
return 2;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
function getResult(res: any) {
|
||||
return Array.isArray(res) ? res : [res];
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="finalNode.type === 'endNode' && success">
|
||||
<ElRow :gutter="12" v-if="finalNode.data.outputDefs && result">
|
||||
<template
|
||||
v-for="outputDef in finalNode.data.outputDefs"
|
||||
:key="outputDef.id"
|
||||
>
|
||||
<ExecResultItem
|
||||
:result="getResult(result[outputDef.name])"
|
||||
:result-count="getResultCount(getResult(result[outputDef.name]))"
|
||||
:content-type="outputDef.contentType || 'text'"
|
||||
:def-name="outputDef.name"
|
||||
/>
|
||||
</template>
|
||||
</ElRow>
|
||||
</div>
|
||||
<div v-if="finalNode.type !== 'endNode' && !success">
|
||||
<ShowJson :value="result" />
|
||||
</div>
|
||||
<div>
|
||||
<ElEmpty
|
||||
:image="`/empty${preferences.theme.mode === 'dark' ? '-dark' : ''}.png`"
|
||||
v-if="!result"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,88 @@
|
||||
<script setup lang="ts">
|
||||
import { ElCard, ElCol, ElImage } from 'element-plus';
|
||||
|
||||
import fileIcon from '#/assets/ai/workflow/fileIcon.png';
|
||||
|
||||
const props = defineProps({
|
||||
defName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
contentType: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
resultCount: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
result: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
function makeItem(item: any, index: number) {
|
||||
const name = `${props.defName}-${index + 1}`;
|
||||
// 保存需要用
|
||||
return {
|
||||
resourceName: name,
|
||||
resourceUrl: item,
|
||||
title: name,
|
||||
filePath: item,
|
||||
content: typeof item === 'string' ? item : JSON.stringify(item),
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElCol
|
||||
:span="resultCount === 1 ? 24 : 12"
|
||||
v-for="(item, idx) of result"
|
||||
:key="idx"
|
||||
>
|
||||
<ElCard shadow="hover" class="mb-3">
|
||||
<template #header>
|
||||
<div>
|
||||
<div class="font-medium">
|
||||
{{ makeItem(item, idx).resourceName }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="h-40 w-full overflow-auto break-words">
|
||||
<ElImage
|
||||
v-if="contentType === 'image'"
|
||||
:src="`${item}`"
|
||||
:preview-src-list="[`${item}`]"
|
||||
class="h-36 w-full"
|
||||
fit="contain"
|
||||
/>
|
||||
<video
|
||||
v-if="contentType === 'video'"
|
||||
controls
|
||||
:src="`${item}`"
|
||||
class="h-36 w-full"
|
||||
></video>
|
||||
<audio
|
||||
v-if="contentType === 'audio'"
|
||||
controls
|
||||
:src="`${item}`"
|
||||
class="h-3/5 w-full"
|
||||
></audio>
|
||||
<div v-if="contentType === 'text'">
|
||||
{{ typeof item === 'string' ? item : JSON.stringify(item) }}
|
||||
</div>
|
||||
<div v-if="contentType === 'other' || contentType === 'file'">
|
||||
<div class="mt-5 flex justify-center">
|
||||
<img :src="fileIcon" alt="" class="h-20 w-20" />
|
||||
</div>
|
||||
<div class="mt-3 text-center">
|
||||
<a :href="`${item}`" target="_blank">{{ $t('button.download') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,81 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from 'element-plus';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Position } from '@element-plus/icons-vue';
|
||||
import { ElButton, ElForm, ElFormItem, ElInput, ElMessage } from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import ShowJson from '#/components/json/ShowJson.vue';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
interface Props {
|
||||
workflowId: any;
|
||||
node: any;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const singleRunForm = ref<FormInstance>();
|
||||
const runParams = ref<any>({});
|
||||
const submitLoading = ref(false);
|
||||
const result = ref<any>('');
|
||||
function submit() {
|
||||
singleRunForm.value?.validate((valid) => {
|
||||
if (valid) {
|
||||
const params = {
|
||||
workflowId: props.workflowId,
|
||||
nodeId: props.node.id,
|
||||
variables: runParams.value,
|
||||
};
|
||||
submitLoading.value = true;
|
||||
api.post('/api/v1/workflow/singleRun', params).then((res) => {
|
||||
submitLoading.value = false;
|
||||
result.value = res.data;
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message);
|
||||
} else {
|
||||
ElMessage.error(res.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<ElForm label-position="top" ref="singleRunForm" :model="runParams">
|
||||
<ElFormItem
|
||||
v-for="(item, idx) in node?.data.parameters"
|
||||
:prop="item.name"
|
||||
:key="idx"
|
||||
:label="item.description || item.name"
|
||||
:rules="[{ required: true, message: $t('message.required') }]"
|
||||
>
|
||||
<ElInput
|
||||
v-if="item.formType === 'input' || !item.formType"
|
||||
v-model="runParams[item.name]"
|
||||
:placeholder="item.formPlaceholder"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem>
|
||||
<ElButton
|
||||
type="primary"
|
||||
@click="submit"
|
||||
:loading="submitLoading"
|
||||
:icon="Position"
|
||||
>
|
||||
{{ $t('button.run') }}
|
||||
</ElButton>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<div class="mb-2.5 mt-2.5 font-semibold">
|
||||
{{ $t('workflow.result') }}:
|
||||
</div>
|
||||
<ShowJson :value="result" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,131 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from 'element-plus';
|
||||
|
||||
import { computed, onUnmounted, ref } from 'vue';
|
||||
|
||||
import { Position } from '@element-plus/icons-vue';
|
||||
import { ElButton, ElForm, ElFormItem } from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import WorkflowFormItem from './WorkflowFormItem.vue';
|
||||
|
||||
export type WorkflowFormProps = {
|
||||
onAsyncExecute?: (values: any) => void;
|
||||
onSubmit?: (values: any) => void;
|
||||
tinyFlowData: any;
|
||||
workflowId: any;
|
||||
workflowParams: any;
|
||||
};
|
||||
const props = withDefaults(defineProps<WorkflowFormProps>(), {
|
||||
onExecuting: () => {
|
||||
console.warn('no execute method');
|
||||
},
|
||||
onSubmit: () => {
|
||||
console.warn('no submit method');
|
||||
},
|
||||
onAsyncExecute: () => {
|
||||
console.warn('no async execute method');
|
||||
},
|
||||
});
|
||||
defineExpose({
|
||||
resume,
|
||||
});
|
||||
const runForm = ref<FormInstance>();
|
||||
const runParams = ref<any>({});
|
||||
const submitLoading = ref(false);
|
||||
const parameters = computed(() => {
|
||||
return props.workflowParams.parameters;
|
||||
});
|
||||
const executeId = ref('');
|
||||
function resume(data: any) {
|
||||
data.executeId = executeId.value;
|
||||
submitLoading.value = true;
|
||||
api.post('/api/v1/workflow/resume', data).then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
startPolling(executeId.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
function submitV2() {
|
||||
runForm.value?.validate((valid) => {
|
||||
if (valid) {
|
||||
const data = {
|
||||
id: props.workflowId,
|
||||
variables: {
|
||||
...runParams.value,
|
||||
},
|
||||
};
|
||||
props.onSubmit?.(runParams.value);
|
||||
submitLoading.value = true;
|
||||
api.post('/api/v1/workflow/runAsync', data).then((res) => {
|
||||
if (res.errorCode === 0 && res.data) {
|
||||
// executeId
|
||||
executeId.value = res.data;
|
||||
startPolling(res.data);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
const timer = ref();
|
||||
const nodes = ref(
|
||||
props.tinyFlowData.nodes.map((node: any) => ({
|
||||
nodeId: node.id,
|
||||
nodeName: node.data.title,
|
||||
})),
|
||||
);
|
||||
// 轮询执行结果
|
||||
function startPolling(executeId: any) {
|
||||
if (timer.value) return;
|
||||
timer.value = setInterval(() => executePolling(executeId), 1000);
|
||||
}
|
||||
function executePolling(executeId: any) {
|
||||
api
|
||||
.post('/api/v1/workflow/getChainStatus', {
|
||||
executeId,
|
||||
nodes: nodes.value,
|
||||
})
|
||||
.then((res) => {
|
||||
// 5 是挂起状态
|
||||
if (res.data.status !== 1 || res.data.status === 5) {
|
||||
stopPolling();
|
||||
}
|
||||
props.onAsyncExecute?.(res.data);
|
||||
});
|
||||
}
|
||||
function stopPolling() {
|
||||
submitLoading.value = false;
|
||||
if (timer.value) {
|
||||
clearInterval(timer.value);
|
||||
timer.value = null;
|
||||
}
|
||||
}
|
||||
onUnmounted(() => {
|
||||
stopPolling();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<ElForm label-position="top" ref="runForm" :model="runParams">
|
||||
<WorkflowFormItem
|
||||
v-model:run-params="runParams"
|
||||
:parameters="parameters"
|
||||
/>
|
||||
<ElFormItem>
|
||||
<ElButton
|
||||
type="primary"
|
||||
@click="submitV2"
|
||||
:loading="submitLoading"
|
||||
:icon="Position"
|
||||
>
|
||||
{{ $t('button.run') }}
|
||||
</ElButton>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,122 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ElAlert,
|
||||
ElCheckboxGroup,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElRadioGroup,
|
||||
ElSelect,
|
||||
} from 'element-plus';
|
||||
|
||||
import { $t } from '#/locales';
|
||||
import ChooseResource from '#/views/ai/resource/ChooseResource.vue';
|
||||
|
||||
const props = defineProps({
|
||||
parameters: {
|
||||
type: Array<any>,
|
||||
required: true,
|
||||
},
|
||||
runParams: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
propPrefix: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['update:runParams']);
|
||||
function getContentType(item: any) {
|
||||
return item.contentType || 'text';
|
||||
}
|
||||
function isResource(contentType: any) {
|
||||
return ['audio', 'file', 'image', 'video'].includes(contentType);
|
||||
}
|
||||
function getCheckboxOptions(item: any) {
|
||||
if (item.enums) {
|
||||
return (
|
||||
item.enums?.map((option: any) => ({
|
||||
label: option,
|
||||
value: option,
|
||||
})) || []
|
||||
);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
function updateParam(name: string, value: any) {
|
||||
const newValue = { ...props.runParams, [name]: value };
|
||||
emit('update:runParams', newValue);
|
||||
}
|
||||
function choose(data: any, propName: string) {
|
||||
updateParam(propName, data.resourceUrl);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElFormItem
|
||||
v-for="(item, idx) in parameters"
|
||||
:prop="`${propPrefix}${item.name}`"
|
||||
:key="idx"
|
||||
:label="item.formLabel || item.name"
|
||||
:rules="
|
||||
item.required ? [{ required: true, message: $t('message.required') }] : []
|
||||
"
|
||||
>
|
||||
<template v-if="getContentType(item) === 'text'">
|
||||
<ElInput
|
||||
v-if="item.formType === 'input' || !item.formType"
|
||||
:model-value="runParams[item.name]"
|
||||
@update:model-value="(val) => updateParam(item.name, val)"
|
||||
:placeholder="item.formPlaceholder"
|
||||
/>
|
||||
<ElSelect
|
||||
v-if="item.formType === 'select'"
|
||||
:model-value="runParams[item.name]"
|
||||
@update:model-value="(val) => updateParam(item.name, val)"
|
||||
:placeholder="item.formPlaceholder"
|
||||
:options="getCheckboxOptions(item)"
|
||||
clearable
|
||||
/>
|
||||
<ElInput
|
||||
v-if="item.formType === 'textarea'"
|
||||
:model-value="runParams[item.name]"
|
||||
@update:model-value="(val) => updateParam(item.name, val)"
|
||||
:placeholder="item.formPlaceholder"
|
||||
:rows="3"
|
||||
type="textarea"
|
||||
/>
|
||||
<ElRadioGroup
|
||||
v-if="item.formType === 'radio'"
|
||||
:model-value="runParams[item.name]"
|
||||
@update:model-value="(val) => updateParam(item.name, val)"
|
||||
:options="getCheckboxOptions(item)"
|
||||
/>
|
||||
<ElCheckboxGroup
|
||||
v-if="item.formType === 'checkbox'"
|
||||
:model-value="runParams[item.name]"
|
||||
@update:model-value="(val) => updateParam(item.name, val)"
|
||||
:options="getCheckboxOptions(item)"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="getContentType(item) === 'other'">
|
||||
<ElInput
|
||||
:model-value="runParams[item.name]"
|
||||
@update:model-value="(val) => updateParam(item.name, val)"
|
||||
:placeholder="item.formPlaceholder"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="isResource(getContentType(item))">
|
||||
<ElInput
|
||||
:model-value="runParams[item.name]"
|
||||
@update:model-value="(val) => updateParam(item.name, val)"
|
||||
:placeholder="item.formPlaceholder"
|
||||
/>
|
||||
<ChooseResource :attr-name="item.name" @choose="choose" />
|
||||
</template>
|
||||
<ElAlert v-if="item.formDescription" type="info" style="margin-top: 5px">
|
||||
{{ item.formDescription }}
|
||||
</ElAlert>
|
||||
</ElFormItem>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,273 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from 'element-plus';
|
||||
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import {
|
||||
CircleCloseFilled,
|
||||
SuccessFilled,
|
||||
VideoPause,
|
||||
WarningFilled,
|
||||
} from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElAlert,
|
||||
ElButton,
|
||||
ElCollapse,
|
||||
ElCollapseItem,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElIcon,
|
||||
} from 'element-plus';
|
||||
|
||||
import ShowJson from '#/components/json/ShowJson.vue';
|
||||
import { $t } from '#/locales';
|
||||
import ConfirmItem from '#/views/ai/workflow/components/ConfirmItem.vue';
|
||||
import ConfirmItemMulti from '#/views/ai/workflow/components/ConfirmItemMulti.vue';
|
||||
|
||||
export interface WorkflowStepsProps {
|
||||
workflowId: any;
|
||||
nodeJson: any;
|
||||
initSignal?: boolean;
|
||||
pollingData?: any;
|
||||
}
|
||||
const props = defineProps<WorkflowStepsProps>();
|
||||
const emit = defineEmits(['resume']);
|
||||
const nodes = ref<any[]>([]);
|
||||
const nodeStatusMap = ref<Record<string, any>>({});
|
||||
const isChainError = ref(false);
|
||||
watch(
|
||||
() => props.pollingData,
|
||||
(newVal) => {
|
||||
const nodes = newVal.nodes;
|
||||
if (newVal.status === 21) {
|
||||
isChainError.value = true;
|
||||
chainErrMsg.value = newVal.message;
|
||||
}
|
||||
if (![20, 21].includes(newVal.status)) {
|
||||
confirmBtnLoading.value = false;
|
||||
}
|
||||
for (const nodeId in nodes) {
|
||||
nodeStatusMap.value[nodeId] = nodes[nodeId];
|
||||
if (nodes[nodeId].status === 5) {
|
||||
activeName.value = nodeId;
|
||||
}
|
||||
}
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
watch(
|
||||
() => props.initSignal,
|
||||
() => {
|
||||
nodeStatusMap.value = {};
|
||||
isChainError.value = false;
|
||||
confirmBtnLoading.value = false;
|
||||
chainErrMsg.value = '';
|
||||
},
|
||||
);
|
||||
watch(
|
||||
() => props.nodeJson,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
nodes.value = [...newVal];
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
const displayNodes = computed(() => {
|
||||
return nodes.value.map((node) => ({
|
||||
...node,
|
||||
...nodeStatusMap.value[node.key],
|
||||
}));
|
||||
});
|
||||
const activeName = ref('1');
|
||||
const confirmParams = ref<any>({});
|
||||
// 定义一个对象来存储所有的 form 实例,key 为 node.key
|
||||
const formRefs = ref<Record<string, FormInstance>>({});
|
||||
// 动态设置 Ref 的辅助函数
|
||||
const setFormRef = (el: any, key: string) => {
|
||||
if (el) {
|
||||
formRefs.value[key] = el as FormInstance;
|
||||
}
|
||||
};
|
||||
const confirmBtnLoading = ref(false);
|
||||
const chainErrMsg = ref('');
|
||||
function getSelectMode(ops: any) {
|
||||
return ops.formType || 'radio';
|
||||
}
|
||||
function handleConfirm(node: any) {
|
||||
const nodeKey = node.key;
|
||||
// 根据 key 获取具体的 form 实例
|
||||
const form = formRefs.value[nodeKey];
|
||||
|
||||
if (!form) {
|
||||
console.warn(`Form instance for ${nodeKey} not found`);
|
||||
return;
|
||||
}
|
||||
const confirmKey = node.suspendForParameters[0].name;
|
||||
form.validate((valid) => {
|
||||
if (valid) {
|
||||
const value = {
|
||||
confirmParams: {
|
||||
[confirmKey]: 'yes',
|
||||
...confirmParams.value,
|
||||
},
|
||||
};
|
||||
confirmBtnLoading.value = true;
|
||||
emit('resume', value);
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="mb-1">
|
||||
<ElAlert v-if="chainErrMsg" :title="chainErrMsg" type="error" />
|
||||
</div>
|
||||
<ElCollapse v-model="activeName" accordion expand-icon-position="left">
|
||||
<ElCollapseItem
|
||||
v-for="node in displayNodes"
|
||||
:key="node.key"
|
||||
:title="`${node.label}-${node.status}`"
|
||||
:name="node.key"
|
||||
>
|
||||
<template #title>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
{{ node.label }}
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<ElIcon
|
||||
v-if="node.status === 20 && !isChainError"
|
||||
color="green"
|
||||
size="20"
|
||||
>
|
||||
<SuccessFilled />
|
||||
</ElIcon>
|
||||
<div v-if="node.status === 1" class="spinner"></div>
|
||||
<ElIcon
|
||||
v-if="node.status === 21 && !isChainError"
|
||||
color="red"
|
||||
size="20"
|
||||
>
|
||||
<CircleCloseFilled />
|
||||
</ElIcon>
|
||||
<ElIcon
|
||||
v-if="node.status === 5 && !isChainError"
|
||||
color="orange"
|
||||
size="20"
|
||||
>
|
||||
<VideoPause />
|
||||
</ElIcon>
|
||||
<ElIcon v-if="isChainError" color="orange" size="20">
|
||||
<WarningFilled />
|
||||
</ElIcon>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="node.original.type === 'confirmNode'" class="p-2.5">
|
||||
<div class="mb-2 text-[16px] font-bold">
|
||||
{{ node.original.data.message }}
|
||||
</div>
|
||||
<ElForm
|
||||
:ref="(el) => setFormRef(el, node.key)"
|
||||
label-position="top"
|
||||
:model="confirmParams"
|
||||
>
|
||||
<template
|
||||
v-for="(ops, idx) in node.suspendForParameters"
|
||||
:key="idx"
|
||||
>
|
||||
<div class="header-container" v-if="ops.formType !== 'confirm'">
|
||||
<div class="blue-bar"> </div>
|
||||
<span>{{ ops.formLabel || $t('message.confirmItem') }}</span>
|
||||
</div>
|
||||
<div
|
||||
class="description-container"
|
||||
v-if="ops.formType !== 'confirm'"
|
||||
>
|
||||
{{ ops.formDescription }}
|
||||
</div>
|
||||
<ElFormItem
|
||||
v-if="ops.formType !== 'confirm'"
|
||||
:prop="ops.name"
|
||||
:rules="[{ required: true, message: $t('message.required') }]"
|
||||
>
|
||||
<ConfirmItem
|
||||
v-if="getSelectMode(ops) === 'radio'"
|
||||
v-model="confirmParams[ops.name]"
|
||||
:selection-data-type="ops.contentType || 'text'"
|
||||
:selection-data="ops.enums"
|
||||
/>
|
||||
<ConfirmItemMulti
|
||||
v-else
|
||||
v-model="confirmParams[ops.name]"
|
||||
:selection-data-type="ops.contentType || 'text'"
|
||||
:selection-data="ops.enums"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</template>
|
||||
<ElFormItem v-if="node.suspendForParameters?.length > 0">
|
||||
<div class="flex justify-end">
|
||||
<ElButton
|
||||
:disabled="confirmBtnLoading"
|
||||
type="primary"
|
||||
@click="handleConfirm(node)"
|
||||
>
|
||||
{{ $t('button.confirm') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</div>
|
||||
<div v-else>
|
||||
<ShowJson :value="node.result || node.message" />
|
||||
</div>
|
||||
</ElCollapseItem>
|
||||
</ElCollapse>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.spinner {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 2px solid rgb(255 255 255 / 30%);
|
||||
border-top-color: var(--el-color-primary);
|
||||
border-right-color: var(--el-color-primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.header-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: bold;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.blue-bar {
|
||||
display: inline-block;
|
||||
width: 2px;
|
||||
height: 16px;
|
||||
margin-right: 16px;
|
||||
background-color: var(--el-color-primary);
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.description-container {
|
||||
margin-bottom: 16px;
|
||||
color: #969799;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,37 @@
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import nodeNames from './nodeNames';
|
||||
|
||||
export default {
|
||||
[nodeNames.documentNode]: {
|
||||
title: $t('aiWorkflow.fileContentExtraction'),
|
||||
group: 'base',
|
||||
description: $t('aiWorkflow.descriptions.fileContentExtraction'),
|
||||
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M21 4V18.7215C21 18.9193 20.8833 19.0986 20.7024 19.1787L12 23.0313L3.29759 19.1787C3.11667 19.0986 3 18.9193 3 18.7215V4H1V2H23V4H21ZM5 4V17.7451L12 20.8441L19 17.7451V4H5ZM8 8H16V10H8V8ZM8 12H16V14H8V12Z"></path></svg>',
|
||||
sortNo: 801,
|
||||
parametersAddEnable: false,
|
||||
outputDefsAddEnable: false,
|
||||
parameters: [
|
||||
{
|
||||
name: 'fileUrl',
|
||||
nameDisabled: true,
|
||||
title: $t('aiWorkflow.documentAddress'),
|
||||
dataType: 'File',
|
||||
required: true,
|
||||
description: $t('aiWorkflow.descriptions.documentAddress'),
|
||||
},
|
||||
],
|
||||
outputDefs: [
|
||||
{
|
||||
name: 'content',
|
||||
title: $t('aiWorkflow.parsedText'),
|
||||
dataType: 'String',
|
||||
dataTypeDisabled: true,
|
||||
required: true,
|
||||
parametersAddEnable: false,
|
||||
description: $t('aiWorkflow.descriptions.parsedText'),
|
||||
deleteDisabled: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,90 @@
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import nodeNames from './nodeNames';
|
||||
|
||||
export default {
|
||||
[nodeNames.downloadNode]: {
|
||||
title: $t('aiWorkflow.resourceSync'),
|
||||
group: 'base',
|
||||
description: $t('aiWorkflow.descriptions.resourceSync'),
|
||||
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M13 12H16L12 16L8 12H11V8H13V12ZM15 4H5V20H19V8H15V4ZM3 2.9918C3 2.44405 3.44749 2 3.9985 2H16L20.9997 7L21 20.9925C21 21.5489 20.5551 22 20.0066 22H3.9934C3.44476 22 3 21.5447 3 21.0082V2.9918Z"></path></svg>',
|
||||
sortNo: 811,
|
||||
parametersAddEnable: false,
|
||||
outputDefsAddEnable: false,
|
||||
parameters: [
|
||||
{
|
||||
name: 'originUrl',
|
||||
nameDisabled: true,
|
||||
title: $t('aiWorkflow.originUrl'),
|
||||
dataType: 'String',
|
||||
required: true,
|
||||
description: $t('aiWorkflow.descriptions.originUrl'),
|
||||
},
|
||||
],
|
||||
outputDefs: [
|
||||
{
|
||||
name: 'resourceUrl',
|
||||
title: $t('aiWorkflow.savedUrl'),
|
||||
dataType: 'String',
|
||||
dataTypeDisabled: true,
|
||||
required: true,
|
||||
parametersAddEnable: false,
|
||||
description: $t('aiWorkflow.savedUrl'),
|
||||
deleteDisabled: true,
|
||||
},
|
||||
],
|
||||
forms: [
|
||||
// 节点表单
|
||||
{
|
||||
// 'input' | 'textarea' | 'select' | 'slider' | 'heading' | 'chosen'
|
||||
type: 'heading',
|
||||
label: $t('aiWorkflow.saveOptions'),
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
label: $t('aiResource.resourceType'),
|
||||
description: $t('aiWorkflow.descriptions.resourceType'),
|
||||
name: 'resourceType', // 属性名称
|
||||
defaultValue: '99',
|
||||
options: [
|
||||
{
|
||||
label: $t('aiWorkflow.image'),
|
||||
value: '0',
|
||||
},
|
||||
{
|
||||
label: $t('aiWorkflow.video'),
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
label: $t('aiWorkflow.audio'),
|
||||
value: '2',
|
||||
},
|
||||
{
|
||||
label: $t('aiWorkflow.document'),
|
||||
value: '3',
|
||||
},
|
||||
{
|
||||
label: $t('aiWorkflow.other'),
|
||||
value: '99',
|
||||
},
|
||||
],
|
||||
},
|
||||
// {
|
||||
// // 用法可参考插件节点的代码
|
||||
// type: 'chosen',
|
||||
// label: '插件选择',
|
||||
// chosen: {
|
||||
// // 节点自定义属性
|
||||
// labelDataKey: 'pluginName',
|
||||
// valueDataKey: 'pluginId',
|
||||
// // updateNodeData 可动态更新节点属性
|
||||
// // value 为选中的 value
|
||||
// // label 为选中的 label
|
||||
// onChosen: ((updateNodeData: (data: Record<string, any>) => void, value?: string, label?: string, event?: Event) => {
|
||||
// console.warn('No onChosen handler provided for plugin-node');
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import docNode from './documentNode';
|
||||
import downloadNode from './downloadNode';
|
||||
import makeFileNode from './makeFileNode';
|
||||
import nodeNames from './nodeNames';
|
||||
import { PluginNode } from './pluginNode';
|
||||
import { SaveToDatacenterNode } from './saveToDatacenter';
|
||||
import { SearchDatacenterNode } from './searchDatacenter';
|
||||
import sqlNode from './sqlNode';
|
||||
import { WorkflowNode } from './workflowNode';
|
||||
|
||||
export interface CustomNodeOptions {
|
||||
handleChosen?: (nodeType: string, updateNodeData: any, value: string) => void;
|
||||
}
|
||||
export const getCustomNode = async (options: CustomNodeOptions) => {
|
||||
const pluginNode = PluginNode({ onChosen: options.handleChosen });
|
||||
const workflowNode = WorkflowNode({ onChosen: options.handleChosen });
|
||||
const searchDatacenterNode = await SearchDatacenterNode();
|
||||
const saveToDatacenterNode = await SaveToDatacenterNode();
|
||||
return {
|
||||
...docNode,
|
||||
...makeFileNode,
|
||||
...downloadNode,
|
||||
...sqlNode,
|
||||
[nodeNames.pluginNode]: pluginNode,
|
||||
[nodeNames.workflowNode]: workflowNode,
|
||||
[nodeNames.searchDatacenterNode]: searchDatacenterNode,
|
||||
[nodeNames.saveToDatacenterNode]: saveToDatacenterNode,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import nodeNames from './nodeNames';
|
||||
|
||||
export default {
|
||||
[nodeNames.makeFileNode]: {
|
||||
title: $t('aiWorkflow.fileGeneration'),
|
||||
group: 'base',
|
||||
description: $t('aiWorkflow.descriptions.fileGeneration'),
|
||||
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M14 13.5V8C14 5.79086 12.2091 4 10 4C7.79086 4 6 5.79086 6 8V13.5C6 17.0899 8.91015 20 12.5 20C16.0899 20 19 17.0899 19 13.5V4H21V13.5C21 18.1944 17.1944 22 12.5 22C7.80558 22 4 18.1944 4 13.5V8C4 4.68629 6.68629 2 10 2C13.3137 2 16 4.68629 16 8V13.5C16 15.433 14.433 17 12.5 17C10.567 17 9 15.433 9 13.5V8H11V13.5C11 14.3284 11.6716 15 12.5 15C13.3284 15 14 14.3284 14 13.5Z"></path></svg>',
|
||||
sortNo: 802,
|
||||
parametersAddEnable: true,
|
||||
outputDefsAddEnable: true,
|
||||
forms: [
|
||||
{
|
||||
type: 'heading',
|
||||
label: $t('aiWorkflow.fileSettings'),
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
label: $t('documentCollection.splitterDoc.fileType'),
|
||||
description: $t('aiWorkflow.descriptions.fileType'),
|
||||
name: 'suffix',
|
||||
defaultValue: 'docx',
|
||||
options: [
|
||||
{
|
||||
label: 'docx',
|
||||
value: 'docx',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
parameters: [
|
||||
{
|
||||
name: 'content',
|
||||
nameDisabled: true,
|
||||
title: $t('preferences.content'),
|
||||
dataType: 'String',
|
||||
required: true,
|
||||
description: $t('preferences.content'),
|
||||
deleteDisabled: true,
|
||||
},
|
||||
],
|
||||
outputDefs: [
|
||||
{
|
||||
name: 'url',
|
||||
nameDisabled: true,
|
||||
title: $t('aiWorkflow.fileDownloadURL'),
|
||||
dataType: 'String',
|
||||
dataTypeDisabled: true,
|
||||
required: true,
|
||||
parametersAddEnable: false,
|
||||
description: $t('aiWorkflow.descriptions.fileDownloadURL'),
|
||||
deleteDisabled: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
export default {
|
||||
documentNode: 'document-node',
|
||||
makeFileNode: 'make-file',
|
||||
downloadNode: 'download-node',
|
||||
sqlNode: 'sql-node',
|
||||
pluginNode: 'plugin-node',
|
||||
workflowNode: 'workflow-node',
|
||||
searchDatacenterNode: 'search-datacenter-node',
|
||||
saveToDatacenterNode: 'save-to-datacenter-node',
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import nodeNames from './nodeNames';
|
||||
|
||||
export interface PluginNodeOptions {
|
||||
onChosen?: (nodeType: string, updateNodeData: any, value: string) => void;
|
||||
}
|
||||
|
||||
export const PluginNode = (options: PluginNodeOptions = {}) => ({
|
||||
title: $t('menus.ai.plugin'),
|
||||
group: 'base',
|
||||
description: $t('aiWorkflow.descriptions.plugin'),
|
||||
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M13 18V20H19V22H13C11.8954 22 11 21.1046 11 20V18H8C5.79086 18 4 16.2091 4 14V7C4 6.44772 4.44772 6 5 6H8V2H10V6H14V2H16V6H19C19.5523 6 20 6.44772 20 7V14C20 16.2091 18.2091 18 16 18H13ZM8 16H16C17.1046 16 18 15.1046 18 14V11H6V14C6 15.1046 6.89543 16 8 16ZM18 8H6V9H18V8ZM12 14.5C11.4477 14.5 11 14.0523 11 13.5C11 12.9477 11.4477 12.5 12 12.5C12.5523 12.5 13 12.9477 13 13.5C13 14.0523 12.5523 14.5 12 14.5Z"></path></svg>',
|
||||
sortNo: 810,
|
||||
parametersAddEnable: false,
|
||||
outputDefsAddEnable: false,
|
||||
forms: [
|
||||
{
|
||||
type: 'chosen',
|
||||
label: $t('aiWorkflow.pluginSelect'),
|
||||
chosen: {
|
||||
labelDataKey: 'pluginName',
|
||||
valueDataKey: 'pluginId',
|
||||
onChosen: (updateNodeData: any, value: any) => {
|
||||
options.onChosen?.(nodeNames.pluginNode, updateNodeData, value);
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import { getOptions } from '@easyflow/utils';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
export const SaveToDatacenterNode = async () => {
|
||||
const res = await api.get('/api/v1/datacenterTable/list');
|
||||
|
||||
return {
|
||||
title: $t('aiWorkflow.saveData'),
|
||||
group: 'base',
|
||||
description: $t('aiWorkflow.descriptions.saveData'),
|
||||
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11 19V9H4V19H11ZM11 7V4C11 3.44772 11.4477 3 12 3H21C21.5523 3 22 3.44772 22 4V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V8C2 7.44772 2.44772 7 3 7H11ZM13 5V19H20V5H13ZM5 16H10V18H5V16ZM14 16H19V18H14V16ZM14 13H19V15H14V13ZM14 10H19V12H14V10ZM5 13H10V15H5V13Z"></path></svg>',
|
||||
sortNo: 812,
|
||||
parametersAddEnable: false,
|
||||
outputDefsAddEnable: false,
|
||||
parameters: [
|
||||
{
|
||||
name: 'saveList',
|
||||
title: $t('aiWorkflow.dataToBeSaved'),
|
||||
dataType: 'Array',
|
||||
dataTypeDisabled: true,
|
||||
required: true,
|
||||
parametersAddEnable: false,
|
||||
description: $t('aiWorkflow.descriptions.dataToBeSaved'),
|
||||
deleteDisabled: true,
|
||||
nameDisabled: true,
|
||||
},
|
||||
],
|
||||
outputDefs: [
|
||||
{
|
||||
name: 'successRows',
|
||||
title: $t('aiWorkflow.successInsertedRecords'),
|
||||
dataType: 'Number',
|
||||
dataTypeDisabled: true,
|
||||
required: true,
|
||||
parametersAddEnable: false,
|
||||
description: $t('aiWorkflow.successInsertedRecords'),
|
||||
deleteDisabled: true,
|
||||
nameDisabled: true,
|
||||
},
|
||||
],
|
||||
forms: [
|
||||
{
|
||||
type: 'heading',
|
||||
label: $t('aiWorkflow.dataTable'),
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
label: '',
|
||||
description: $t('aiWorkflow.descriptions.dataTable'),
|
||||
name: 'tableId',
|
||||
defaultValue: '',
|
||||
options: getOptions('tableName', 'id', res.data),
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
import { getOptions } from '@easyflow/utils';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
export const SearchDatacenterNode = async () => {
|
||||
const res = await api.get('/api/v1/datacenterTable/list');
|
||||
|
||||
return {
|
||||
title: $t('aiWorkflow.queryData'),
|
||||
group: 'base',
|
||||
description: $t('aiWorkflow.descriptions.queryData'),
|
||||
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11 2C15.968 2 20 6.032 20 11C20 15.968 15.968 20 11 20C6.032 20 2 15.968 2 11C2 6.032 6.032 2 11 2ZM11 18C14.8675 18 18 14.8675 18 11C18 7.1325 14.8675 4 11 4C7.1325 4 4 7.1325 4 11C4 14.8675 7.1325 18 11 18ZM19.4853 18.0711L22.3137 20.8995L20.8995 22.3137L18.0711 19.4853L19.4853 18.0711Z"></path></svg>',
|
||||
sortNo: 813,
|
||||
parametersAddEnable: true,
|
||||
outputDefsAddEnable: false,
|
||||
parameters: [],
|
||||
outputDefs: [
|
||||
{
|
||||
name: 'rows',
|
||||
title: $t('aiWorkflow.queryResult'),
|
||||
dataType: 'Array',
|
||||
dataTypeDisabled: true,
|
||||
required: true,
|
||||
parametersAddEnable: false,
|
||||
description: $t('aiWorkflow.queryResult'),
|
||||
deleteDisabled: true,
|
||||
nameDisabled: false,
|
||||
},
|
||||
],
|
||||
forms: [
|
||||
{
|
||||
type: 'heading',
|
||||
label: $t('aiWorkflow.dataTable'),
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
label: '',
|
||||
description: $t('aiWorkflow.descriptions.dataTable'),
|
||||
name: 'tableId',
|
||||
defaultValue: '',
|
||||
options: getOptions('tableName', 'id', res.data),
|
||||
},
|
||||
{
|
||||
type: 'heading',
|
||||
label: $t('aiWorkflow.filterConditions'),
|
||||
},
|
||||
{
|
||||
type: 'textarea',
|
||||
label: "如:name='张三' and age=21 or field = {{流程变量}}",
|
||||
description: '',
|
||||
name: 'where',
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
type: 'heading',
|
||||
label: $t('aiWorkflow.limit'),
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
label: '',
|
||||
description: '',
|
||||
name: 'limit',
|
||||
defaultValue: '10',
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import nodeNames from './nodeNames';
|
||||
|
||||
export default {
|
||||
[nodeNames.sqlNode]: {
|
||||
title: $t('aiWorkflow.sqlQuery'),
|
||||
group: 'base',
|
||||
description: $t('aiWorkflow.descriptions.sqlQuery'),
|
||||
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="rgba(37,99,235,1)"><path d="M5 12.5C5 12.8134 5.46101 13.3584 6.53047 13.8931C7.91405 14.5849 9.87677 15 12 15C14.1232 15 16.0859 14.5849 17.4695 13.8931C18.539 13.3584 19 12.8134 19 12.5V10.3287C17.35 11.3482 14.8273 12 12 12C9.17273 12 6.64996 11.3482 5 10.3287V12.5ZM19 15.3287C17.35 16.3482 14.8273 17 12 17C9.17273 17 6.64996 16.3482 5 15.3287V17.5C5 17.8134 5.46101 18.3584 6.53047 18.8931C7.91405 19.5849 9.87677 20 12 20C14.1232 20 16.0859 19.5849 17.4695 18.8931C18.539 18.3584 19 17.8134 19 17.5V15.3287ZM3 17.5V7.5C3 5.01472 7.02944 3 12 3C16.9706 3 21 5.01472 21 7.5V17.5C21 19.9853 16.9706 22 12 22C7.02944 22 3 19.9853 3 17.5ZM12 10C14.1232 10 16.0859 9.58492 17.4695 8.89313C18.539 8.3584 19 7.81342 19 7.5C19 7.18658 18.539 6.6416 17.4695 6.10687C16.0859 5.41508 14.1232 5 12 5C9.87677 5 7.91405 5.41508 6.53047 6.10687C5.46101 6.6416 5 7.18658 5 7.5C5 7.81342 5.46101 8.3584 6.53047 8.89313C7.91405 9.58492 9.87677 10 12 10Z"></path></svg>',
|
||||
sortNo: 803,
|
||||
parametersAddEnable: true,
|
||||
outputDefsAddEnable: true,
|
||||
parameters: [],
|
||||
forms: [
|
||||
{
|
||||
name: 'sql',
|
||||
type: 'textarea',
|
||||
label: 'SQL',
|
||||
placeholder: $t('aiWorkflow.descriptions.enterSQL'),
|
||||
},
|
||||
],
|
||||
outputDefs: [
|
||||
{
|
||||
name: 'queryData',
|
||||
title: $t('aiWorkflow.queryResult'),
|
||||
dataType: 'Array',
|
||||
dataTypeDisabled: true,
|
||||
required: true,
|
||||
parametersAddEnable: false,
|
||||
description: $t('aiWorkflow.descriptions.queryResultJson'),
|
||||
deleteDisabled: true,
|
||||
nameDisabled: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import nodeNames from './nodeNames';
|
||||
|
||||
export interface WorkflowNodeOptions {
|
||||
onChosen?: (nodeType: string, updateNodeData: any, value: string) => void;
|
||||
}
|
||||
|
||||
export const WorkflowNode = (options: WorkflowNodeOptions = {}) => ({
|
||||
title: $t('aiWorkflow.subProcess'),
|
||||
group: 'base',
|
||||
description: $t('aiWorkflow.descriptions.subProcess'),
|
||||
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M6 21.5C4.067 21.5 2.5 19.933 2.5 18C2.5 16.067 4.067 14.5 6 14.5C7.5852 14.5 8.92427 15.5539 9.35481 16.9992L15 16.9994V15L17 14.9994V9.24339L14.757 6.99938H9V9.00003H3V3.00003H9V4.99939H14.757L18 1.75739L22.2426 6.00003L19 9.24139V14.9994L21 15V21H15V18.9994L9.35499 19.0003C8.92464 20.4459 7.58543 21.5 6 21.5ZM6 16.5C5.17157 16.5 4.5 17.1716 4.5 18C4.5 18.8285 5.17157 19.5 6 19.5C6.82843 19.5 7.5 18.8285 7.5 18C7.5 17.1716 6.82843 16.5 6 16.5ZM19 17H17V19H19V17ZM18 4.58581L16.5858 6.00003L18 7.41424L19.4142 6.00003L18 4.58581ZM7 5.00003H5V7.00003H7V5.00003Z"></path></svg>',
|
||||
sortNo: 815,
|
||||
parametersAddEnable: false,
|
||||
outputDefsAddEnable: false,
|
||||
forms: [
|
||||
{
|
||||
type: 'chosen',
|
||||
label: $t('aiWorkflow.workflowSelect'),
|
||||
chosen: {
|
||||
labelDataKey: 'workflowName',
|
||||
valueDataKey: 'workflowId',
|
||||
onChosen: (updateNodeData: any, value: any) => {
|
||||
options.onChosen?.(nodeNames.workflowNode, updateNodeData, value);
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,244 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from 'element-plus';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { ArrowLeft, DeleteFilled, MoreFilled } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElButton,
|
||||
ElDropdown,
|
||||
ElDropdownItem,
|
||||
ElDropdownMenu,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
ElTag,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import PageData from '#/components/page/PageData.vue';
|
||||
import { $t } from '#/locales';
|
||||
import { useDictStore } from '#/store';
|
||||
|
||||
const router = useRouter();
|
||||
const $route = useRoute();
|
||||
onMounted(() => {
|
||||
initDict();
|
||||
});
|
||||
const formRef = ref<FormInstance>();
|
||||
const pageDataRef = ref();
|
||||
const formInline = ref({
|
||||
execKey: '',
|
||||
});
|
||||
const dictStore = useDictStore();
|
||||
function initDict() {
|
||||
dictStore.fetchDictionary('dataStatus');
|
||||
}
|
||||
function search(formEl: FormInstance | undefined) {
|
||||
formEl?.validate((valid) => {
|
||||
if (valid) {
|
||||
pageDataRef.value.setQuery(formInline.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
function reset(formEl: FormInstance | undefined) {
|
||||
formEl?.resetFields();
|
||||
pageDataRef.value.setQuery({});
|
||||
}
|
||||
function remove(row: any) {
|
||||
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
|
||||
confirmButtonText: $t('message.ok'),
|
||||
cancelButtonText: $t('message.cancel'),
|
||||
type: 'warning',
|
||||
beforeClose: (action, instance, done) => {
|
||||
if (action === 'confirm') {
|
||||
instance.confirmButtonLoading = true;
|
||||
api
|
||||
.get('/api/v1/workflowExecResult/del', { params: { id: row.id } })
|
||||
.then((res) => {
|
||||
instance.confirmButtonLoading = false;
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message);
|
||||
reset(formRef.value);
|
||||
done();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
instance.confirmButtonLoading = false;
|
||||
});
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
},
|
||||
}).catch(() => {});
|
||||
}
|
||||
function toStepPage(row: any) {
|
||||
router.push({
|
||||
name: 'RecordStep',
|
||||
query: {
|
||||
recordId: row.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
function getTagType(row: any) {
|
||||
switch (row.status) {
|
||||
case 1: {
|
||||
return 'primary';
|
||||
}
|
||||
case 5: {
|
||||
return 'warning';
|
||||
}
|
||||
case 10: {
|
||||
return 'danger';
|
||||
}
|
||||
case 20: {
|
||||
return 'success';
|
||||
}
|
||||
case 21: {
|
||||
return 'danger';
|
||||
}
|
||||
default: {
|
||||
return 'info';
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container border-border border">
|
||||
<div class="mb-3">
|
||||
<ElButton :icon="ArrowLeft" @click="router.back()">
|
||||
{{ $t('button.back') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
<ElForm ref="formRef" :inline="true" :model="formInline">
|
||||
<ElFormItem class="w-full max-w-[300px]" prop="execKey">
|
||||
<ElInput
|
||||
v-model="formInline.execKey"
|
||||
:placeholder="$t('aiWorkflowExecRecord.execKey')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem>
|
||||
<ElButton @click="search(formRef)" type="primary">
|
||||
{{ $t('button.query') }}
|
||||
</ElButton>
|
||||
<ElButton @click="reset(formRef)">
|
||||
{{ $t('button.reset') }}
|
||||
</ElButton>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<div class="handle-div"></div>
|
||||
<PageData
|
||||
ref="pageDataRef"
|
||||
page-url="/api/v1/workflowExecResult/page"
|
||||
:page-size="10"
|
||||
:extra-query-params="{
|
||||
workflowId: $route.query.workflowId,
|
||||
}"
|
||||
>
|
||||
<template #default="{ pageList }">
|
||||
<ElTable :data="pageList" border>
|
||||
<ElTableColumn
|
||||
prop="execKey"
|
||||
show-overflow-tooltip
|
||||
:label="$t('aiWorkflowExecRecord.execKey')"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ row.execKey }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="title" :label="$t('aiWorkflowExecRecord.title')">
|
||||
<template #default="{ row }">
|
||||
{{ row.title }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
prop="description"
|
||||
:label="$t('aiWorkflowExecRecord.description')"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ row.description }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
show-overflow-tooltip
|
||||
prop="input"
|
||||
:label="$t('aiWorkflowExecRecord.input')"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ row.input }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
show-overflow-tooltip
|
||||
prop="output"
|
||||
:label="$t('aiWorkflowExecRecord.output')"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ row.output }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
prop="startTime"
|
||||
:label="$t('aiWorkflowExecRecord.execTime')"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ row.execTime || '-' }} ms
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
prop="status"
|
||||
:label="$t('aiWorkflowExecRecord.status')"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<ElTag :type="getTagType(row)">
|
||||
{{ $t(`aiWorkflowExecRecord.status${row.status}`) }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
show-overflow-tooltip
|
||||
prop="errorInfo"
|
||||
:label="$t('aiWorkflowExecRecord.errorInfo')"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ row.errorInfo }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn :label="$t('common.handle')" width="110" align="right">
|
||||
<template #default="{ row }">
|
||||
<div class="flex items-center gap-1">
|
||||
<ElButton link type="primary" @click="toStepPage(row)">
|
||||
{{ $t('aiWorkflowRecordStep.moduleName') }}
|
||||
</ElButton>
|
||||
|
||||
<ElDropdown>
|
||||
<ElButton :icon="MoreFilled" link />
|
||||
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<div v-access:code="'/api/v1/workflow/save'">
|
||||
<ElDropdownItem @click="remove(row)">
|
||||
<ElButton type="danger" :icon="DeleteFilled" link>
|
||||
{{ $t('button.delete') }}
|
||||
</ElButton>
|
||||
</ElDropdownItem>
|
||||
</div>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</template>
|
||||
</PageData>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,184 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from 'element-plus';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { ArrowLeft } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElButton,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
ElTag,
|
||||
} from 'element-plus';
|
||||
|
||||
import PageData from '#/components/page/PageData.vue';
|
||||
import { $t } from '#/locales';
|
||||
import { useDictStore } from '#/store';
|
||||
|
||||
const router = useRouter();
|
||||
const $route = useRoute();
|
||||
onMounted(() => {
|
||||
initDict();
|
||||
});
|
||||
const formRef = ref<FormInstance>();
|
||||
const pageDataRef = ref();
|
||||
const formInline = ref({
|
||||
nodeName: '',
|
||||
});
|
||||
const dictStore = useDictStore();
|
||||
function initDict() {
|
||||
dictStore.fetchDictionary('dataStatus');
|
||||
}
|
||||
function search(formEl: FormInstance | undefined) {
|
||||
formEl?.validate((valid) => {
|
||||
if (valid) {
|
||||
pageDataRef.value.setQuery(formInline.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
function reset(formEl: FormInstance | undefined) {
|
||||
formEl?.resetFields();
|
||||
pageDataRef.value.setQuery({});
|
||||
}
|
||||
function getTagType(row: any) {
|
||||
switch (row.status) {
|
||||
case 1: {
|
||||
return 'primary';
|
||||
}
|
||||
case 6: {
|
||||
return 'warning';
|
||||
}
|
||||
case 10: {
|
||||
return 'danger';
|
||||
}
|
||||
case 20: {
|
||||
return 'success';
|
||||
}
|
||||
case 21: {
|
||||
return 'danger';
|
||||
}
|
||||
default: {
|
||||
return 'info';
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container border-border border">
|
||||
<div class="mb-3">
|
||||
<ElButton :icon="ArrowLeft" @click="router.back()">
|
||||
{{ $t('button.back') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
<ElForm ref="formRef" :inline="true" :model="formInline">
|
||||
<ElFormItem class="w-full max-w-[300px]" prop="nodeName">
|
||||
<ElInput
|
||||
v-model="formInline.nodeName"
|
||||
:placeholder="$t('aiWorkflowRecordStep.nodeName')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem>
|
||||
<ElButton @click="search(formRef)" type="primary">
|
||||
{{ $t('button.query') }}
|
||||
</ElButton>
|
||||
<ElButton @click="reset(formRef)">
|
||||
{{ $t('button.reset') }}
|
||||
</ElButton>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<div class="handle-div"></div>
|
||||
<PageData
|
||||
ref="pageDataRef"
|
||||
page-url="/api/v1/workflowExecStep/page"
|
||||
:page-size="10"
|
||||
:extra-query-params="{
|
||||
recordId: $route.query.recordId,
|
||||
sortKey: 'startTime',
|
||||
sortType: 'asc',
|
||||
}"
|
||||
>
|
||||
<template #default="{ pageList }">
|
||||
<ElTable :data="pageList" border>
|
||||
<ElTableColumn
|
||||
show-overflow-tooltip
|
||||
prop="execKey"
|
||||
:label="$t('aiWorkflowRecordStep.execKey')"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ row.execKey }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
show-overflow-tooltip
|
||||
prop="nodeId"
|
||||
:label="$t('aiWorkflowRecordStep.nodeId')"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ row.nodeId }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
prop="nodeName"
|
||||
:label="$t('aiWorkflowRecordStep.nodeName')"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ row.nodeName }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
show-overflow-tooltip
|
||||
prop="input"
|
||||
:label="$t('aiWorkflowRecordStep.input')"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ row.input }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
show-overflow-tooltip
|
||||
prop="output"
|
||||
:label="$t('aiWorkflowRecordStep.output')"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ row.output }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
prop="execTime"
|
||||
:label="$t('aiWorkflowRecordStep.execTime')"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ row.execTime || '-' }} ms
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
prop="status"
|
||||
:label="$t('aiWorkflowRecordStep.status')"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<ElTag :type="getTagType(row)">
|
||||
{{ $t(`aiWorkflowRecordStep.status${row.status}`) }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
show-overflow-tooltip
|
||||
prop="errorInfo"
|
||||
:label="$t('aiWorkflowRecordStep.errorInfo')"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ row.errorInfo }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</template>
|
||||
</PageData>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
30
easyflow-ui-admin/app/src/views/config/apikey/SysApiKey.vue
Normal file
30
easyflow-ui-admin/app/src/views/config/apikey/SysApiKey.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { ElDialog } from 'element-plus';
|
||||
|
||||
import SysApiKeyList from '#/views/config/apikey/SysApiKeyList.vue';
|
||||
import SysApiKeyResourcePermissionList from '#/views/config/apikey/SysApiKeyResourcePermissionList.vue';
|
||||
|
||||
const dialogVisible = ref(false);
|
||||
</script>
|
||||
<template>
|
||||
<div class="sys-apikey-container">
|
||||
<SysApiKeyList />
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
draggable
|
||||
:close-on-click-modal="false"
|
||||
align-center
|
||||
>
|
||||
<SysApiKeyResourcePermissionList />
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sys-apikey-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
201
easyflow-ui-admin/app/src/views/config/apikey/SysApiKeyList.vue
Normal file
201
easyflow-ui-admin/app/src/views/config/apikey/SysApiKeyList.vue
Normal file
@@ -0,0 +1,201 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from 'element-plus';
|
||||
|
||||
import { markRaw, ref } from 'vue';
|
||||
|
||||
import { Delete, MoreFilled, Plus } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElButton,
|
||||
ElDialog,
|
||||
ElDropdown,
|
||||
ElDropdownItem,
|
||||
ElDropdownMenu,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
ElTag,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
||||
import PageData from '#/components/page/PageData.vue';
|
||||
import { $t } from '#/locales';
|
||||
import SysApiKeyResourcePermissionList from '#/views/config/apikey/SysApiKeyResourcePermissionList.vue';
|
||||
|
||||
import SysApiKeyModal from './SysApiKeyModal.vue';
|
||||
|
||||
const formRef = ref<FormInstance>();
|
||||
const pageDataRef = ref();
|
||||
const saveDialog = ref();
|
||||
const headerButtons = [
|
||||
{
|
||||
key: 'addApiKey',
|
||||
text: $t('sysApiKey.addApiKey'),
|
||||
icon: markRaw(Plus),
|
||||
type: 'primary',
|
||||
data: { action: 'create' },
|
||||
permission: '',
|
||||
},
|
||||
];
|
||||
|
||||
const handleSearch = (params: string) => {
|
||||
pageDataRef.value.setQuery({ apiKey: params, isQueryOr: true });
|
||||
};
|
||||
const headerButtonClick = (action: any) => {
|
||||
if (action.key === 'addApiKey') {
|
||||
addNewApiKey();
|
||||
} else if (action.key === 'addPermission') {
|
||||
showAddPermissionDialog({});
|
||||
}
|
||||
};
|
||||
|
||||
function reset(formEl?: FormInstance) {
|
||||
formEl?.resetFields();
|
||||
pageDataRef.value.setQuery({});
|
||||
}
|
||||
|
||||
function showDialog(row: any) {
|
||||
saveDialog.value.openDialog({ ...row });
|
||||
}
|
||||
function showAddPermissionDialog(_row: any) {
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
const dialogVisible = ref(false);
|
||||
function remove(row: any) {
|
||||
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
|
||||
confirmButtonText: $t('message.ok'),
|
||||
cancelButtonText: $t('message.cancel'),
|
||||
type: 'warning',
|
||||
beforeClose: (action, instance, done) => {
|
||||
if (action === 'confirm') {
|
||||
instance.confirmButtonLoading = true;
|
||||
api
|
||||
.post('/api/v1/sysApiKey/remove', { id: row.id })
|
||||
.then((res) => {
|
||||
instance.confirmButtonLoading = false;
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message);
|
||||
reset(formRef.value);
|
||||
done();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
instance.confirmButtonLoading = false;
|
||||
});
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
},
|
||||
}).catch(() => {});
|
||||
}
|
||||
function addNewApiKey() {
|
||||
ElMessageBox.confirm(
|
||||
$t('sysApiKey.addApiKeyNotice'),
|
||||
$t('message.noticeTitle'),
|
||||
{
|
||||
confirmButtonText: $t('message.ok'),
|
||||
cancelButtonText: $t('message.cancel'),
|
||||
type: 'warning',
|
||||
},
|
||||
).then(() => {
|
||||
api.post('/api/v1/sysApiKey/key/save', {}).then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success($t('message.saveOkMessage'));
|
||||
pageDataRef.value.setQuery({});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full flex-col gap-6 p-6">
|
||||
<SysApiKeyModal ref="saveDialog" @reload="reset" />
|
||||
<HeaderSearch
|
||||
:buttons="headerButtons"
|
||||
@search="handleSearch"
|
||||
@button-click="headerButtonClick"
|
||||
/>
|
||||
|
||||
<div class="bg-background border-border flex-1 rounded-lg border p-5">
|
||||
<PageData
|
||||
ref="pageDataRef"
|
||||
page-url="/api/v1/sysApiKey/page"
|
||||
:page-size="10"
|
||||
>
|
||||
<template #default="{ pageList }">
|
||||
<ElTable :data="pageList" border>
|
||||
<ElTableColumn
|
||||
prop="apiKey"
|
||||
:label="$t('sysApiKey.apiKey')"
|
||||
width="280"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ row.apiKey }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="created" :label="$t('sysApiKey.created')">
|
||||
<template #default="{ row }">
|
||||
{{ row.created }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="status" :label="$t('sysApiKey.status')">
|
||||
<template #default="{ row }">
|
||||
<ElTag type="primary" v-if="row.status === 1">
|
||||
{{ $t('sysApiKey.actions.enable') }}
|
||||
</ElTag>
|
||||
<ElTag type="danger" v-else-if="row.status === 0">
|
||||
{{ $t('sysApiKey.actions.disable') }}
|
||||
</ElTag>
|
||||
<ElTag type="warning" v-else>
|
||||
{{ $t('sysApiKey.actions.failure') }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="expiredAt" :label="$t('sysApiKey.expiredAt')">
|
||||
<template #default="{ row }">
|
||||
{{ row.expiredAt }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
:label="$t('common.handle')"
|
||||
width="90"
|
||||
align="right"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="flex items-center gap-3">
|
||||
<ElButton link type="primary" @click="showDialog(row)">
|
||||
{{ $t('button.edit') }}
|
||||
</ElButton>
|
||||
|
||||
<ElDropdown>
|
||||
<ElButton link :icon="MoreFilled" />
|
||||
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownItem @click="remove(row)">
|
||||
<ElButton link :icon="Delete" type="danger">
|
||||
{{ $t('button.delete') }}
|
||||
</ElButton>
|
||||
</ElDropdownItem>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</template>
|
||||
</PageData>
|
||||
</div>
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="$t('sysApiKeyResourcePermission.addPermission')"
|
||||
draggable
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<SysApiKeyResourcePermissionList />
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
264
easyflow-ui-admin/app/src/views/config/apikey/SysApiKeyModal.vue
Normal file
264
easyflow-ui-admin/app/src/views/config/apikey/SysApiKeyModal.vue
Normal file
@@ -0,0 +1,264 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from 'element-plus';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import {
|
||||
ElAlert,
|
||||
ElButton,
|
||||
ElCheckbox,
|
||||
ElCheckboxGroup,
|
||||
ElDatePicker,
|
||||
ElDialog,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElMessage,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import DictSelect from '#/components/dict/DictSelect.vue';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
// 定义权限接口
|
||||
interface ResourcePermission {
|
||||
id: number;
|
||||
title: string;
|
||||
requestInterface: string;
|
||||
}
|
||||
|
||||
// 定义表单数据接口
|
||||
interface Entity {
|
||||
apiKey: string;
|
||||
status: number | string;
|
||||
deptId: number | string;
|
||||
expiredAt: Date | null | string;
|
||||
permissionIds: (number | string)[]; // 绑定值:权限 ID 数组
|
||||
id?: number; // 编辑时的主键
|
||||
}
|
||||
|
||||
const emit = defineEmits(['reload']);
|
||||
|
||||
// 表单实例
|
||||
const saveForm = ref<FormInstance>();
|
||||
// 对话框状态
|
||||
const dialogVisible = ref(false);
|
||||
const isAdd = ref(true);
|
||||
// 表单数据(初始化默认值)
|
||||
const entity = ref<Entity>({
|
||||
apiKey: '',
|
||||
status: '',
|
||||
deptId: '',
|
||||
expiredAt: null,
|
||||
permissionIds: [],
|
||||
});
|
||||
// 加载状态
|
||||
const btnLoading = ref(false);
|
||||
// 资源权限列表
|
||||
const resourcePermissionList = ref<ResourcePermission[]>([]);
|
||||
|
||||
// 表单校验规则(必填项校验)
|
||||
const rules = ref({
|
||||
status: [
|
||||
{
|
||||
required: true,
|
||||
message: $t('message.pleaseSelect', { name: $t('sysApiKey.status') }),
|
||||
trigger: 'change',
|
||||
},
|
||||
],
|
||||
expiredAt: [
|
||||
{
|
||||
required: true,
|
||||
message: $t('message.pleaseSelect', { name: $t('sysApiKey.expiredAt') }),
|
||||
trigger: 'change',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
function openDialog(row: Partial<Entity> = {}) {
|
||||
saveForm.value?.resetFields();
|
||||
entity.value = {
|
||||
apiKey: '',
|
||||
status: '',
|
||||
deptId: '',
|
||||
expiredAt: null,
|
||||
permissionIds: [],
|
||||
...row,
|
||||
};
|
||||
isAdd.value = !row.id;
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
// 获取资源权限列表
|
||||
function getResourcePermissionList() {
|
||||
api
|
||||
.get('/api/v1/sysApiKeyResourcePermission/list')
|
||||
.then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
resourcePermissionList.value = res.data;
|
||||
} else {
|
||||
ElMessage.error(res.message || $t('message.getDataError'));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.error($t('message.getDataError'));
|
||||
});
|
||||
}
|
||||
|
||||
// 保存表单
|
||||
function save() {
|
||||
saveForm.value?.validate((valid) => {
|
||||
if (valid) {
|
||||
btnLoading.value = true;
|
||||
const url = isAdd.value
|
||||
? 'api/v1/sysApiKey/save'
|
||||
: 'api/v1/sysApiKey/update';
|
||||
api
|
||||
.post(url, entity.value)
|
||||
.then((res) => {
|
||||
btnLoading.value = false;
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message || $t('message.saveOkMessage'));
|
||||
emit('reload');
|
||||
closeDialog();
|
||||
} else {
|
||||
ElMessage.error(res.message || $t('message.saveFailMessage'));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
btnLoading.value = false;
|
||||
ElMessage.error($t('message.saveFailMessage'));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
function closeDialog() {
|
||||
saveForm.value?.resetFields();
|
||||
// 重置表单数据
|
||||
entity.value = {
|
||||
apiKey: '',
|
||||
status: '',
|
||||
deptId: '',
|
||||
expiredAt: null,
|
||||
permissionIds: [],
|
||||
};
|
||||
isAdd.value = true;
|
||||
dialogVisible.value = false;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getResourcePermissionList();
|
||||
});
|
||||
|
||||
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="50%"
|
||||
>
|
||||
<ElForm
|
||||
label-width="120px"
|
||||
ref="saveForm"
|
||||
:model="entity"
|
||||
status-icon
|
||||
:rules="rules"
|
||||
class="form-container"
|
||||
>
|
||||
<!-- 状态选择 -->
|
||||
<ElFormItem prop="status" :label="$t('sysApiKey.status')">
|
||||
<DictSelect
|
||||
v-model="entity.status"
|
||||
dict-code="dataStatus"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem prop="expiredAt" :label="$t('sysApiKey.expiredAt')">
|
||||
<ElDatePicker
|
||||
v-model="entity.expiredAt"
|
||||
type="datetime"
|
||||
:placeholder="
|
||||
$t('message.pleaseSelect', { name: $t('sysApiKey.expiredAt') })
|
||||
"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
prop="permissions"
|
||||
:label="$t('sysApiKey.permissions')"
|
||||
class="permission-form-item"
|
||||
>
|
||||
<ElAlert type="info">
|
||||
接口信息请运行:tech.easyflow.publicapi.SyncApis 的 main
|
||||
方法同步到数据库。
|
||||
</ElAlert>
|
||||
<ElCheckboxGroup
|
||||
v-model="entity.permissionIds"
|
||||
class="permission-checkbox-group"
|
||||
>
|
||||
<ElCheckbox
|
||||
v-for="item in resourcePermissionList"
|
||||
:key="item.id"
|
||||
:value="item.id"
|
||||
class="permission-checkbox"
|
||||
>
|
||||
{{ item.requestInterface }} - {{ item.title }}
|
||||
</ElCheckbox>
|
||||
</ElCheckboxGroup>
|
||||
</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>
|
||||
.form-container {
|
||||
max-height: 60vh;
|
||||
padding-right: 10px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.permission-form-item .el-form-item__content {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.permission-checkbox {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.form-container::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.form-container::-webkit-scrollbar-thumb {
|
||||
background-color: #e5e7eb;
|
||||
border-radius: 3px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,157 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from 'element-plus';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Delete, MoreFilled, Plus } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElButton,
|
||||
ElDropdown,
|
||||
ElDropdownItem,
|
||||
ElDropdownMenu,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import PageData from '#/components/page/PageData.vue';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import SysApiKeyResourcePermissionModal from './SysApiKeyResourcePermissionModal.vue';
|
||||
|
||||
const formRef = ref<FormInstance>();
|
||||
const pageDataRef = ref();
|
||||
const saveDialog = ref();
|
||||
const formInline = ref({
|
||||
id: '',
|
||||
});
|
||||
function search(formEl: FormInstance | undefined) {
|
||||
formEl?.validate((valid) => {
|
||||
if (valid) {
|
||||
pageDataRef.value.setQuery(formInline.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
function reset(formEl: FormInstance | undefined) {
|
||||
formEl?.resetFields();
|
||||
pageDataRef.value.setQuery({});
|
||||
}
|
||||
function showDialog(row: any) {
|
||||
saveDialog.value.openDialog({ ...row });
|
||||
}
|
||||
function remove(row: any) {
|
||||
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
|
||||
confirmButtonText: $t('message.ok'),
|
||||
cancelButtonText: $t('message.cancel'),
|
||||
type: 'warning',
|
||||
beforeClose: (action, instance, done) => {
|
||||
if (action === 'confirm') {
|
||||
instance.confirmButtonLoading = true;
|
||||
api
|
||||
.post('/api/v1/sysApiKeyResourcePermission/remove', { id: row.id })
|
||||
.then((res) => {
|
||||
instance.confirmButtonLoading = false;
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message);
|
||||
reset(formRef.value);
|
||||
done();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
instance.confirmButtonLoading = false;
|
||||
});
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
},
|
||||
}).catch(() => {});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container !m-0 !pl-0">
|
||||
<SysApiKeyResourcePermissionModal ref="saveDialog" @reload="reset" />
|
||||
<div class="flex items-center justify-between">
|
||||
<ElForm ref="formRef" :inline="true" :model="formInline">
|
||||
<ElFormItem prop="title" class="!mr-3">
|
||||
<ElInput
|
||||
v-model="formInline.title"
|
||||
:placeholder="$t('sysApiKeyResourcePermission.title')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem>
|
||||
<ElButton @click="search(formRef)" type="primary">
|
||||
{{ $t('button.query') }}
|
||||
</ElButton>
|
||||
<ElButton @click="reset(formRef)">
|
||||
{{ $t('button.reset') }}
|
||||
</ElButton>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<div class="mb-5">
|
||||
<ElButton @click="showDialog({})" type="primary">
|
||||
<ElIcon class="mr-1">
|
||||
<Plus />
|
||||
</ElIcon>
|
||||
{{ $t('button.add') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
<PageData
|
||||
ref="pageDataRef"
|
||||
page-url="/api/v1/sysApiKeyResourcePermission/page"
|
||||
:page-size="10"
|
||||
>
|
||||
<template #default="{ pageList }">
|
||||
<ElTable :data="pageList" border>
|
||||
<ElTableColumn
|
||||
prop="requestInterface"
|
||||
:label="$t('sysApiKeyResourcePermission.requestInterface')"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ row.requestInterface }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
prop="title"
|
||||
:label="$t('sysApiKeyResourcePermission.title')"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ row.title }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn :label="$t('common.handle')" width="90" align="right">
|
||||
<template #default="{ row }">
|
||||
<div class="flex items-center gap-3">
|
||||
<ElButton link type="primary" @click="showDialog(row)">
|
||||
{{ $t('button.edit') }}
|
||||
</ElButton>
|
||||
|
||||
<ElDropdown>
|
||||
<ElButton link :icon="MoreFilled" />
|
||||
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownItem @click="remove(row)">
|
||||
<ElButton link :icon="Delete" type="danger">
|
||||
{{ $t('button.delete') }}
|
||||
</ElButton>
|
||||
</ElDropdownItem>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</template>
|
||||
</PageData>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,116 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from 'element-plus';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElDialog,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const emit = defineEmits(['reload']);
|
||||
// vue
|
||||
onMounted(() => {});
|
||||
defineExpose({
|
||||
openDialog,
|
||||
});
|
||||
const saveForm = ref<FormInstance>();
|
||||
// variables
|
||||
const dialogVisible = ref(false);
|
||||
const isAdd = ref(true);
|
||||
const entity = ref<any>({
|
||||
requestInterface: '',
|
||||
title: '',
|
||||
});
|
||||
const btnLoading = ref(false);
|
||||
const rules = ref({});
|
||||
// functions
|
||||
function openDialog(row: any) {
|
||||
if (row.id) {
|
||||
isAdd.value = false;
|
||||
}
|
||||
entity.value = row;
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
function save() {
|
||||
saveForm.value?.validate((valid) => {
|
||||
if (valid) {
|
||||
btnLoading.value = true;
|
||||
api
|
||||
.post(
|
||||
isAdd.value
|
||||
? 'api/v1/sysApiKeyResourcePermission/save'
|
||||
: 'api/v1/sysApiKeyResourcePermission/update',
|
||||
entity.value,
|
||||
)
|
||||
.then((res) => {
|
||||
btnLoading.value = false;
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message);
|
||||
emit('reload');
|
||||
closeDialog();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
btnLoading.value = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
function closeDialog() {
|
||||
saveForm.value?.resetFields();
|
||||
isAdd.value = true;
|
||||
entity.value = {};
|
||||
dialogVisible.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
draggable
|
||||
:title="isAdd ? $t('button.add') : $t('button.edit')"
|
||||
:before-close="closeDialog"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<ElForm
|
||||
label-width="120px"
|
||||
ref="saveForm"
|
||||
:model="entity"
|
||||
status-icon
|
||||
:rules="rules"
|
||||
>
|
||||
<ElFormItem
|
||||
prop="requestInterface"
|
||||
:label="$t('sysApiKeyResourcePermission.requestInterface')"
|
||||
>
|
||||
<ElInput v-model.trim="entity.requestInterface" />
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="title" :label="$t('sysApiKeyResourcePermission.title')">
|
||||
<ElInput v-model.trim="entity.title" />
|
||||
</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>
|
||||
@@ -0,0 +1,130 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from 'element-plus';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElDatePicker,
|
||||
ElDialog,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElMessage,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import DictSelect from '#/components/dict/DictSelect.vue';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const emit = defineEmits(['reload']);
|
||||
// vue
|
||||
onMounted(() => {});
|
||||
defineExpose({
|
||||
openDialog,
|
||||
});
|
||||
const saveForm = ref<FormInstance>();
|
||||
// variables
|
||||
const dialogVisible = ref(false);
|
||||
const isAdd = ref(true);
|
||||
const entity = ref<any>({
|
||||
apiKey: '',
|
||||
status: '',
|
||||
deptId: '',
|
||||
expiredAt: '',
|
||||
});
|
||||
const btnLoading = ref(false);
|
||||
const rules = ref({});
|
||||
|
||||
// functions
|
||||
function openDialog(row: any) {
|
||||
if (row.id) {
|
||||
isAdd.value = false;
|
||||
}
|
||||
entity.value = row;
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function save() {
|
||||
saveForm.value?.validate((valid) => {
|
||||
if (valid) {
|
||||
btnLoading.value = true;
|
||||
api
|
||||
.post(
|
||||
isAdd.value ? 'api/v1/sysApiKey/save' : 'api/v1/sysApiKey/update',
|
||||
entity.value,
|
||||
)
|
||||
.then((res) => {
|
||||
btnLoading.value = false;
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message);
|
||||
emit('reload');
|
||||
closeDialog();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
btnLoading.value = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
saveForm.value?.resetFields();
|
||||
isAdd.value = true;
|
||||
entity.value = {};
|
||||
dialogVisible.value = false;
|
||||
}
|
||||
const resourcePermissionList = ref([]);
|
||||
function getResourcePermissionList() {
|
||||
api.get('/api/v1/sysApiKeyResourcePermission/list').then((res) => {
|
||||
console.log('资源');
|
||||
console.log(res);
|
||||
if (res.errorCode === 0) {
|
||||
resourcePermissionList.value = res.data;
|
||||
}
|
||||
});
|
||||
}
|
||||
onMounted(() => {
|
||||
getResourcePermissionList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
draggable
|
||||
:title="isAdd ? $t('button.add') : $t('button.edit')"
|
||||
:before-close="closeDialog"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<ElForm
|
||||
label-width="120px"
|
||||
ref="saveForm"
|
||||
:model="entity"
|
||||
status-icon
|
||||
:rules="rules"
|
||||
>
|
||||
<ElFormItem prop="status" :label="$t('sysApiKey.status')">
|
||||
<DictSelect v-model="entity.status" dict-code="dataStatus" />
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="expiredAt" :label="$t('sysApiKey.expiredAt')">
|
||||
<ElDatePicker v-model="entity.expiredAt" type="datetime" />
|
||||
</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>
|
||||
161
easyflow-ui-admin/app/src/views/config/settings/Settings.vue
Normal file
161
easyflow-ui-admin/app/src/views/config/settings/Settings.vue
Normal file
@@ -0,0 +1,161 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { $t } from '@easyflow/locales';
|
||||
|
||||
import {
|
||||
ElAlert,
|
||||
ElButton,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request.js';
|
||||
import providerList from '#/views/ai/model/modelUtils/providerList.json';
|
||||
|
||||
const providerOptions =
|
||||
ref<Array<{ label: string; options: any; value: string }>>(providerList);
|
||||
const brands = ref([]);
|
||||
const llmOptions = ref([]);
|
||||
|
||||
// 获取品牌接口数据
|
||||
function getBrands() {
|
||||
api.get('/api/v1/modelProvider/list').then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
brands.value = res.data;
|
||||
llmOptions.value = formatLlmList(res.data);
|
||||
}
|
||||
});
|
||||
}
|
||||
function getOptions() {
|
||||
api
|
||||
.get(
|
||||
'/api/v1/sysOption/list?keys=model_of_chat&keys=chatgpt_endpoint&keys=chatgpt_chatPath&keys=chatgpt_api_key&keys=chatgpt_model_name',
|
||||
)
|
||||
.then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
entity.value = res.data;
|
||||
}
|
||||
});
|
||||
}
|
||||
onMounted(() => {
|
||||
getOptions();
|
||||
getBrands();
|
||||
});
|
||||
|
||||
const entity = ref({
|
||||
model_of_chat: '',
|
||||
chatgpt_api_key: '',
|
||||
chatgpt_chatPath: '',
|
||||
chatgpt_endpoint: '',
|
||||
chatgpt_model_name: '',
|
||||
});
|
||||
|
||||
function formatLlmList(data) {
|
||||
return data.map((item) => {
|
||||
const extra = new Map([
|
||||
['chatPath', item.options?.chatPath],
|
||||
['llmEndpoint', item.options?.llmEndpoint],
|
||||
]);
|
||||
return {
|
||||
label: item.title,
|
||||
value: item.key,
|
||||
extra,
|
||||
};
|
||||
});
|
||||
}
|
||||
function handleChangeModel(value) {
|
||||
const extra = providerList.find((item) => item.value === value);
|
||||
entity.value.chatgpt_chatPath = extra.options.chatPath;
|
||||
entity.value.chatgpt_endpoint = extra.options.llmEndpoint;
|
||||
}
|
||||
function handleSave() {
|
||||
api.post('/api/v1/sysOption/save', entity.value).then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success($t('message.saveOkMessage'));
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="settings-container">
|
||||
<div class="settings-config-container border-border border">
|
||||
<div class="mb-6">
|
||||
{{ $t('settingsConfig.systemAIFunctionSettings') }}
|
||||
</div>
|
||||
<ElAlert
|
||||
class="!mb-5"
|
||||
:title="$t('settingsConfig.note')"
|
||||
type="warning"
|
||||
/>
|
||||
<ElForm :model="entity" class="demo-form-inline" label-width="150px">
|
||||
<ElFormItem :label="$t('settingsConfig.modelOfChat')">
|
||||
<ElSelect
|
||||
v-model="entity.model_of_chat"
|
||||
clearable
|
||||
@change="handleChangeModel"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in providerOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem :label="$t('settingsConfig.modelName')">
|
||||
<ElInput v-model="entity.chatgpt_model_name" clearable />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="Endpoint">
|
||||
<ElInput v-model="entity.chatgpt_endpoint" clearable />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="ChatPath">
|
||||
<ElInput v-model="entity.chatgpt_chatPath" clearable />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="ApiKey">
|
||||
<ElInput v-model="entity.chatgpt_api_key" clearable />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<div class="settings-button-container">
|
||||
<ElButton type="primary" @click="handleSave">
|
||||
{{ $t('button.save') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.settings-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 30px 143px;
|
||||
}
|
||||
|
||||
.settings-config-container {
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
background-color: var(--el-bg-color);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.settings-notice {
|
||||
margin-bottom: 20px;
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
|
||||
.settings-button-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user