初始化
This commit is contained in:
3
easyflow-ui-usercenter/app/src/views/_core/README.md
Normal file
3
easyflow-ui-usercenter/app/src/views/_core/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# \_core
|
||||
|
||||
此目录包含应用程序正常运行所需的基本视图。这些视图是应用程序布局中使用的视图。
|
||||
@@ -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>
|
||||
@@ -0,0 +1,102 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EasyFlowFormSchema } from '@easyflow/common-ui';
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { AuthenticationLogin, z } from '@easyflow/common-ui';
|
||||
import { useAppConfig } from '@easyflow/hooks';
|
||||
import { $t } from '@easyflow/locales';
|
||||
|
||||
import { useAuthStore } from '#/store';
|
||||
|
||||
defineOptions({ name: 'Login' });
|
||||
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
||||
const authStore = useAuthStore();
|
||||
|
||||
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}/userCenter/public/getCaptcha`,
|
||||
// 验证接口 (必选项,必须配置, 要符合tianai-captcha默认验证码校验接口规范)
|
||||
validCaptchaUrl: `${apiURL}/userCenter/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);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<AuthenticationLogin
|
||||
:form-schema="formSchema"
|
||||
:loading="authStore.loginLoading"
|
||||
@submit="onSubmit"
|
||||
/>
|
||||
<div id="captcha-box" class="captcha-div"></div>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped>
|
||||
.captcha-div {
|
||||
position: absolute;
|
||||
top: 30vh;
|
||||
left: 21vh;
|
||||
}
|
||||
</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,65 @@
|
||||
<script setup lang="ts">
|
||||
import type { BasicOption } from '@easyflow/types';
|
||||
|
||||
import type { EasyFlowFormSchema } from '#/adapter/form';
|
||||
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
|
||||
import { ProfileBaseSetting } from '@easyflow/common-ui';
|
||||
|
||||
import { getUserInfoApi } from '#/api';
|
||||
|
||||
const profileBaseSettingRef = ref();
|
||||
|
||||
const MOCK_ROLES_OPTIONS: BasicOption[] = [
|
||||
{
|
||||
label: '管理员',
|
||||
value: 'super',
|
||||
},
|
||||
{
|
||||
label: '用户',
|
||||
value: 'user',
|
||||
},
|
||||
{
|
||||
label: '测试',
|
||||
value: 'test',
|
||||
},
|
||||
];
|
||||
|
||||
const formSchema = computed((): EasyFlowFormSchema[] => {
|
||||
return [
|
||||
{
|
||||
fieldName: 'realName',
|
||||
component: 'Input',
|
||||
label: '姓名',
|
||||
},
|
||||
{
|
||||
fieldName: 'username',
|
||||
component: 'Input',
|
||||
label: '用户名',
|
||||
},
|
||||
{
|
||||
fieldName: 'roles',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
mode: 'tags',
|
||||
options: MOCK_ROLES_OPTIONS,
|
||||
},
|
||||
label: '角色',
|
||||
},
|
||||
{
|
||||
fieldName: 'introduction',
|
||||
component: 'Textarea',
|
||||
label: '个人简介',
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
const data = await getUserInfoApi();
|
||||
profileBaseSettingRef.value.getFormApi().setValues(data);
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<ProfileBaseSetting ref="profileBaseSettingRef" :form-schema="formSchema" />
|
||||
</template>
|
||||
49
easyflow-ui-usercenter/app/src/views/_core/profile/index.vue
Normal file
49
easyflow-ui-usercenter/app/src/views/_core/profile/index.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Profile } from '@easyflow/common-ui';
|
||||
import { useUserStore } from '@easyflow/stores';
|
||||
|
||||
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 userStore = useUserStore();
|
||||
|
||||
const tabsValue = ref<string>('basic');
|
||||
|
||||
const tabs = ref([
|
||||
{
|
||||
label: '基本设置',
|
||||
value: 'basic',
|
||||
},
|
||||
{
|
||||
label: '安全设置',
|
||||
value: 'security',
|
||||
},
|
||||
{
|
||||
label: '修改密码',
|
||||
value: 'password',
|
||||
},
|
||||
{
|
||||
label: '新消息提醒',
|
||||
value: 'notice',
|
||||
},
|
||||
]);
|
||||
</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,31 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { ProfileNotificationSetting } from '@easyflow/common-ui';
|
||||
|
||||
const formSchema = computed(() => {
|
||||
return [
|
||||
{
|
||||
value: true,
|
||||
fieldName: 'accountPassword',
|
||||
label: '账户密码',
|
||||
description: '其他用户的消息将以站内信的形式通知',
|
||||
},
|
||||
{
|
||||
value: true,
|
||||
fieldName: 'systemMessage',
|
||||
label: '系统消息',
|
||||
description: '系统消息将以站内信的形式通知',
|
||||
},
|
||||
{
|
||||
value: true,
|
||||
fieldName: 'todoTask',
|
||||
label: '待办任务',
|
||||
description: '待办任务将以站内信的形式通知',
|
||||
},
|
||||
];
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<ProfileNotificationSetting :form-schema="formSchema" />
|
||||
</template>
|
||||
@@ -0,0 +1,66 @@
|
||||
<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';
|
||||
|
||||
const profilePasswordSettingRef = ref();
|
||||
|
||||
const formSchema = computed((): EasyFlowFormSchema[] => {
|
||||
return [
|
||||
{
|
||||
fieldName: 'oldPassword',
|
||||
label: '旧密码',
|
||||
component: 'EasyFlowInputPassword',
|
||||
componentProps: {
|
||||
placeholder: '请输入旧密码',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'newPassword',
|
||||
label: '新密码',
|
||||
component: 'EasyFlowInputPassword',
|
||||
componentProps: {
|
||||
passwordStrength: true,
|
||||
placeholder: '请输入新密码',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'confirmPassword',
|
||||
label: '确认密码',
|
||||
component: 'EasyFlowInputPassword',
|
||||
componentProps: {
|
||||
passwordStrength: true,
|
||||
placeholder: '请再次输入新密码',
|
||||
},
|
||||
dependencies: {
|
||||
rules(values) {
|
||||
const { newPassword } = values;
|
||||
return z
|
||||
.string({ required_error: '请再次输入新密码' })
|
||||
.min(1, { message: '请再次输入新密码' })
|
||||
.refine((value) => value === newPassword, {
|
||||
message: '两次输入的密码不一致',
|
||||
});
|
||||
},
|
||||
triggerFields: ['newPassword'],
|
||||
},
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
function handleSubmit() {
|
||||
ElMessage.success('密码修改成功');
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<ProfilePasswordSetting
|
||||
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>
|
||||
@@ -0,0 +1,164 @@
|
||||
<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
|
||||
? '/userCenter/resource/save'
|
||||
: '/userCenter/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>
|
||||
</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,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="/userCenter/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>
|
||||
@@ -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" />
|
||||
您的浏览器不支持 video 元素。
|
||||
</video>
|
||||
<audio v-if="data.resourceType === 2" controls :src="data.resourceUrl">
|
||||
您的浏览器不支持 audio 元素。
|
||||
</audio>
|
||||
</div>
|
||||
</ElDialog>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,210 @@
|
||||
<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: [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,101 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
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;
|
||||
showMessage?: boolean;
|
||||
}
|
||||
const props = withDefaults(defineProps<ExecResultProps>(), {
|
||||
initSignal: false,
|
||||
showMessage: true,
|
||||
pollingData: {},
|
||||
});
|
||||
|
||||
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) {
|
||||
if (props.showMessage) {
|
||||
ElMessage.success($t('message.success'));
|
||||
}
|
||||
result.value = newVal.result;
|
||||
success.value = true;
|
||||
}
|
||||
if (newVal.status === 21) {
|
||||
if (props.showMessage) {
|
||||
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.png" v-if="!result" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,92 @@
|
||||
<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="!border-border !bg-background mb-3">
|
||||
<template #header>
|
||||
<div>
|
||||
<div class="text-foreground 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'" class="text-foreground">
|
||||
{{ 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="text-foreground mt-3 text-center">
|
||||
<a :href="`${item}`" target="_blank">下载</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.el-card__header) {
|
||||
border-color: hsl(var(--border));
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,130 @@
|
||||
<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 '#/views/ai/workflow/components/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('/userCenter/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('/userCenter/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('/userCenter/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>
|
||||
119
easyflow-ui-usercenter/app/src/views/assetLibrary/grid.vue
Normal file
119
easyflow-ui-usercenter/app/src/views/assetLibrary/grid.vue
Normal file
@@ -0,0 +1,119 @@
|
||||
<script setup lang="ts">
|
||||
import type { CheckboxValueType } from 'element-plus';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { cn } from '@easyflow/utils';
|
||||
|
||||
import { ElCheckbox, ElCheckboxGroup, ElSpace } from 'element-plus';
|
||||
|
||||
import { Card, CardContent, CardTitle } from '#/components/card';
|
||||
import Tag from '#/components/tag/Tag.vue';
|
||||
import { useDictStore } from '#/store';
|
||||
import {
|
||||
getResourceOriginColor,
|
||||
getResourceTypeColor,
|
||||
getSrc,
|
||||
} from '#/utils/resource';
|
||||
|
||||
interface GridProps {
|
||||
data: any[];
|
||||
onCheckedChange?: (ids: any[]) => void;
|
||||
}
|
||||
|
||||
const props = defineProps<GridProps>();
|
||||
const checkAll = ref(false);
|
||||
const isIndeterminate = ref(false);
|
||||
const checkedIds = ref<number[]>([]);
|
||||
|
||||
const handleCheckAllChange = (val: CheckboxValueType) => {
|
||||
checkedIds.value = val ? props.data.map((asset) => asset.id) : [];
|
||||
isIndeterminate.value = false;
|
||||
props.onCheckedChange?.(checkedItems());
|
||||
};
|
||||
const handleCheckedIdsChange = (value: CheckboxValueType[]) => {
|
||||
const checkedCount = value.length;
|
||||
checkAll.value = checkedCount === props.data.length;
|
||||
isIndeterminate.value = checkedCount > 0 && checkedCount < props.data.length;
|
||||
props.onCheckedChange?.(checkedItems());
|
||||
};
|
||||
onMounted(() => {
|
||||
initDict();
|
||||
});
|
||||
const dictStore = useDictStore();
|
||||
function initDict() {
|
||||
dictStore.fetchDictionary('resourceType');
|
||||
dictStore.fetchDictionary('resourceOriginType');
|
||||
}
|
||||
function checkedItems() {
|
||||
return props.data.filter((asset) => checkedIds.value.includes(asset.id));
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full flex-col items-start gap-6">
|
||||
<ElCheckbox
|
||||
v-model="checkAll"
|
||||
:indeterminate="isIndeterminate"
|
||||
@change="handleCheckAllChange"
|
||||
>
|
||||
全选
|
||||
</ElCheckbox>
|
||||
<ElCheckboxGroup
|
||||
class="grid w-full grid-cols-[repeat(auto-fill,minmax(300px,1fr))] gap-5"
|
||||
v-model="checkedIds"
|
||||
@change="handleCheckedIdsChange"
|
||||
>
|
||||
<Card
|
||||
class="bg-background dark:border-border group relative max-w-none flex-col gap-3 border border-[#f0f0f0] p-3 transition hover:-translate-y-2 hover:shadow-[0px_2px_16px_0px_rgba(6,27,57,0.07)]"
|
||||
v-for="asset in props.data"
|
||||
:key="asset.id"
|
||||
>
|
||||
<div
|
||||
class="bg-background-deep flex h-[174px] w-full items-center justify-center rounded-lg"
|
||||
>
|
||||
<img
|
||||
class="h-[100px] w-[100px] shrink-0 object-cover"
|
||||
:src="getSrc(asset)"
|
||||
/>
|
||||
</div>
|
||||
<CardContent class="w-full gap-3">
|
||||
<CardTitle class="font-medium">
|
||||
{{ asset.resourceName }}
|
||||
</CardTitle>
|
||||
<div class="flex items-center justify-between">
|
||||
<ElSpace :size="10">
|
||||
<Tag
|
||||
size="small"
|
||||
:background-color="`${getResourceOriginColor(asset)}15`"
|
||||
:text-color="getResourceOriginColor(asset)"
|
||||
:text="
|
||||
dictStore.getDictLabel('resourceOriginType', asset.origin)
|
||||
"
|
||||
/>
|
||||
<Tag
|
||||
size="small"
|
||||
:background-color="`${getResourceTypeColor(asset)}15`"
|
||||
:text-color="getResourceTypeColor(asset)"
|
||||
:text="
|
||||
dictStore.getDictLabel('resourceType', asset.resourceType)
|
||||
"
|
||||
/>
|
||||
</ElSpace>
|
||||
<span class="text-foreground/60 text-xs">{{ asset.size }}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'absolute left-2.5 top-2.5 group-hover:block',
|
||||
!checkedIds.includes(asset.id) && 'hidden',
|
||||
)
|
||||
"
|
||||
>
|
||||
<ElCheckbox style="--el-checkbox-height: 1" :value="asset.id" />
|
||||
</div>
|
||||
</Card>
|
||||
</ElCheckboxGroup>
|
||||
</div>
|
||||
</template>
|
||||
244
easyflow-ui-usercenter/app/src/views/assetLibrary/index.vue
Normal file
244
easyflow-ui-usercenter/app/src/views/assetLibrary/index.vue
Normal file
@@ -0,0 +1,244 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { IconifyIcon } from '@easyflow/icons';
|
||||
import { cn } from '@easyflow/utils';
|
||||
|
||||
import { Delete, EditPen, Plus, View } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElButton,
|
||||
ElContainer,
|
||||
ElHeader,
|
||||
ElMain,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElSpace,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import PageData from '#/components/page/PageData.vue';
|
||||
import { $t } from '#/locales';
|
||||
import AiResourceModal from '#/views/ai/resource/AiResourceModal.vue';
|
||||
import PreviewModal from '#/views/ai/resource/PreviewModal.vue';
|
||||
|
||||
import Grid from './grid.vue';
|
||||
import List from './list.vue';
|
||||
|
||||
type ViewType = 'grid' | 'list';
|
||||
export interface Asset {
|
||||
id: number;
|
||||
name: string;
|
||||
source: string;
|
||||
type: string;
|
||||
size: string;
|
||||
lastUpdateTime: string;
|
||||
}
|
||||
|
||||
const viewType = ref<ViewType>('list');
|
||||
const pageDataRef = ref();
|
||||
const checkedItems = ref<any[]>([]);
|
||||
const saveDialog = ref();
|
||||
const previewDialog = ref();
|
||||
function setCheckedItem(items: any[]) {
|
||||
checkedItems.value = items;
|
||||
}
|
||||
function reset() {
|
||||
pageDataRef.value.setQuery({});
|
||||
}
|
||||
function preview(row: any) {
|
||||
previewDialog.value.openDialog({ ...row });
|
||||
}
|
||||
function showDialog(row: any) {
|
||||
saveDialog.value.openDialog({ ...row });
|
||||
}
|
||||
function download(row: any) {
|
||||
window.open(row.resourceUrl, '_blank');
|
||||
}
|
||||
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('/userCenter/resource/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 batchRemove() {
|
||||
const ids = checkedItems.value.map((item) => item.id);
|
||||
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('/userCenter/resource/removeBatch', { ids })
|
||||
.then((res) => {
|
||||
instance.confirmButtonLoading = false;
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message);
|
||||
reset();
|
||||
done();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
instance.confirmButtonLoading = false;
|
||||
});
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
},
|
||||
}).catch(() => {});
|
||||
}
|
||||
function handleOperation(type: string) {
|
||||
if (checkedItems.value.length > 1 || checkedItems.value.length === 0) {
|
||||
ElMessage.warning('只能操作一项数据');
|
||||
return;
|
||||
}
|
||||
switch (type) {
|
||||
case 'edit': {
|
||||
showDialog(checkedItems.value[0]);
|
||||
break;
|
||||
}
|
||||
case 'preview': {
|
||||
preview(checkedItems.value[0]);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElContainer class="bg-background-deep h-full">
|
||||
<ElHeader class="flex flex-col gap-6 !p-8 !pb-0" height="auto">
|
||||
<ElSpace :size="24">
|
||||
<h1 class="text-2xl font-medium">素材库</h1>
|
||||
<!--<ElSpace
|
||||
class="rounded-lg border border-[#E6E9EE] bg-[#F8FBFE] px-3.5 py-2.5"
|
||||
>
|
||||
<span class="text-sm font-medium text-[#969799]">
|
||||
<span class="text-[#1A1A1A]">256G</span> / 1T
|
||||
</span>
|
||||
<ElProgress
|
||||
class="w-[132px]"
|
||||
:percentage="20"
|
||||
:stroke-width="4"
|
||||
:show-text="false"
|
||||
/>
|
||||
</ElSpace>-->
|
||||
</ElSpace>
|
||||
<div class="flex w-full items-center justify-between">
|
||||
<ElSpace class="text-2xl text-[#969799]">
|
||||
<IconifyIcon
|
||||
icon="svg:list"
|
||||
:class="
|
||||
cn(
|
||||
'h-8 w-8 cursor-pointer text-[#969799]',
|
||||
viewType === 'list'
|
||||
? 'text-primary'
|
||||
: 'hover:text-foreground dark:hover:text-accent',
|
||||
)
|
||||
"
|
||||
@click="viewType = 'list'"
|
||||
/>
|
||||
<IconifyIcon
|
||||
icon="svg:grid"
|
||||
:class="
|
||||
cn(
|
||||
'h-8 w-8 cursor-pointer text-[#969799]',
|
||||
viewType === 'grid'
|
||||
? 'text-primary'
|
||||
: 'hover:text-foreground dark:hover:text-accent',
|
||||
)
|
||||
"
|
||||
@click="viewType = 'grid'"
|
||||
/>
|
||||
</ElSpace>
|
||||
<div class="flex items-center gap-2.5">
|
||||
<div
|
||||
v-if="checkedItems.length > 0"
|
||||
class="border-border bg-background flex items-center rounded border px-2 py-1.5"
|
||||
>
|
||||
<ElButton
|
||||
class="[--el-font-weight-primary:400]"
|
||||
link
|
||||
:icon="View"
|
||||
@click="handleOperation('preview')"
|
||||
>
|
||||
预览
|
||||
</ElButton>
|
||||
<ElButton
|
||||
class="[--el-font-weight-primary:400]"
|
||||
link
|
||||
:icon="EditPen"
|
||||
@click="handleOperation('edit')"
|
||||
>
|
||||
编辑
|
||||
</ElButton>
|
||||
<ElButton
|
||||
class="[--el-font-weight-primary:400]"
|
||||
link
|
||||
:icon="Delete"
|
||||
@click="batchRemove"
|
||||
>
|
||||
删除
|
||||
</ElButton>
|
||||
</div>
|
||||
<ElButton type="primary" :icon="Plus" @click="showDialog({})">
|
||||
本地上传
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</ElHeader>
|
||||
<ElMain class="!px-8 !py-6">
|
||||
<PageData
|
||||
ref="pageDataRef"
|
||||
page-url="/userCenter/resource/page"
|
||||
:page-size="10"
|
||||
>
|
||||
<template #default="{ pageList }">
|
||||
<div class="flex flex-col items-center gap-5">
|
||||
<List
|
||||
:on-checked-change="setCheckedItem"
|
||||
v-show="viewType === 'list'"
|
||||
:data="pageList"
|
||||
:on-download="download"
|
||||
:on-edit="showDialog"
|
||||
:on-preview="preview"
|
||||
:on-remove="remove"
|
||||
/>
|
||||
<Grid
|
||||
:on-checked-change="setCheckedItem"
|
||||
v-show="viewType === 'grid'"
|
||||
:data="pageList"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</PageData>
|
||||
</ElMain>
|
||||
<PreviewModal ref="previewDialog" />
|
||||
<AiResourceModal ref="saveDialog" @reload="reset" />
|
||||
</ElContainer>
|
||||
</template>
|
||||
167
easyflow-ui-usercenter/app/src/views/assetLibrary/list.vue
Normal file
167
easyflow-ui-usercenter/app/src/views/assetLibrary/list.vue
Normal file
@@ -0,0 +1,167 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
import { formatBytes } from '@easyflow/utils';
|
||||
|
||||
import { Delete, Download, MoreFilled } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElAvatar,
|
||||
ElButton,
|
||||
ElDropdown,
|
||||
ElDropdownItem,
|
||||
ElDropdownMenu,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
} from 'element-plus';
|
||||
|
||||
import Tag from '#/components/tag/Tag.vue';
|
||||
import { useDictStore } from '#/store';
|
||||
import {
|
||||
getResourceOriginColor,
|
||||
getResourceTypeColor,
|
||||
getSrc,
|
||||
} from '#/utils/resource';
|
||||
|
||||
interface ListProps {
|
||||
data: any[];
|
||||
onCheckedChange?: (ids: any[]) => void;
|
||||
onPreview?: (row: any) => void;
|
||||
onEdit?: (row: any) => void;
|
||||
onRemove?: (row: any) => void;
|
||||
onDownload?: (row: any) => void;
|
||||
}
|
||||
const props = defineProps<ListProps>();
|
||||
onMounted(() => {
|
||||
initDict();
|
||||
});
|
||||
const dictStore = useDictStore();
|
||||
function initDict() {
|
||||
dictStore.fetchDictionary('resourceType');
|
||||
dictStore.fetchDictionary('resourceOriginType');
|
||||
}
|
||||
function handleSelectionChange(items: any[]) {
|
||||
props.onCheckedChange?.(items);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-background w-full rounded-lg p-5">
|
||||
<ElTable :data="props.data" @selection-change="handleSelectionChange">
|
||||
<ElTableColumn type="selection" width="30" />
|
||||
<ElTableColumn label="文件名称" show-overflow-tooltip :width="300">
|
||||
<template #default="{ row }">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<ElAvatar
|
||||
class="shrink-0"
|
||||
:src="getSrc(row)"
|
||||
shape="square"
|
||||
:size="32"
|
||||
/>
|
||||
<span class="w-full overflow-hidden text-ellipsis">{{
|
||||
row.resourceName
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="文件来源" align="center">
|
||||
<template #default="{ row }">
|
||||
<Tag
|
||||
size="small"
|
||||
:background-color="`${getResourceOriginColor(row)}15`"
|
||||
:text-color="getResourceOriginColor(row)"
|
||||
:text="dictStore.getDictLabel('resourceOriginType', row.origin)"
|
||||
/>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="文件类型" align="center">
|
||||
<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="fileSize" label="文件大小" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ formatBytes(row.fileSize) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="modified" label="修改时间" align="center" />
|
||||
<ElTableColumn label="操作" width="150" align="center">
|
||||
<template #default="{ row }">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center">
|
||||
<ElButton
|
||||
class="[--el-font-weight-primary:400]"
|
||||
link
|
||||
type="primary"
|
||||
@click="onPreview?.(row)"
|
||||
>
|
||||
预览
|
||||
</ElButton>
|
||||
<ElButton
|
||||
class="[--el-font-weight-primary:400]"
|
||||
link
|
||||
type="primary"
|
||||
@click="onEdit?.(row)"
|
||||
>
|
||||
编辑
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<ElDropdown>
|
||||
<ElButton link :icon="MoreFilled" />
|
||||
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownItem @click="onDownload?.(row)">
|
||||
<ElButton :icon="Download" link>下载</ElButton>
|
||||
</ElDropdownItem>
|
||||
<ElDropdownItem @click="onRemove?.(row)">
|
||||
<ElButton type="danger" :icon="Delete" link>
|
||||
删除
|
||||
</ElButton>
|
||||
</ElDropdownItem>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.el-table {
|
||||
--el-table-text-color: hsl(var(--foreground) / 0.9);
|
||||
--el-font-size-base: 14px;
|
||||
--el-table-header-text-color: hsl(var(--accent-foreground));
|
||||
--el-table-header-bg-color: #f7f9fd;
|
||||
--el-table-border: none;
|
||||
}
|
||||
|
||||
.el-table:where(.dark, .dark *) {
|
||||
--el-table-header-bg-color: hsl(var(--accent));
|
||||
}
|
||||
|
||||
:deep(.el-table__header) {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.el-table :deep(.el-table__inner-wrapper:before) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-table :deep(thead),
|
||||
.el-table :deep(tr) {
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.el-table :deep(thead th) {
|
||||
font-weight: 400;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,186 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { cloneDeep } from '@easyflow/utils';
|
||||
|
||||
import { ArrowLeft, Minus, Plus } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElAside,
|
||||
ElAvatar,
|
||||
ElButton,
|
||||
ElContainer,
|
||||
ElIcon,
|
||||
ElMain,
|
||||
ElMessage,
|
||||
ElSpace,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import defaultBotAvatar from '#/assets/defaultBotAvatar.png';
|
||||
import { Card, CardDescription, CardTitle } from '#/components/card';
|
||||
import { ChatBubbleList, ChatSender } from '#/components/chat';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
onMounted(async () => {
|
||||
getUserUsed();
|
||||
getBotDetail();
|
||||
});
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const usedList = ref<any[]>([]);
|
||||
const botInfo = ref<any>({});
|
||||
const btnLoading = ref(false);
|
||||
const conversationId = ref('');
|
||||
function getUserUsed() {
|
||||
api.get('/userCenter/botRecentlyUsed/list').then((res) => {
|
||||
usedList.value = res.data.map((item: any) => item.botId);
|
||||
});
|
||||
}
|
||||
function getBotDetail() {
|
||||
api
|
||||
.get('/userCenter/bot/getDetail', {
|
||||
params: {
|
||||
id: route.params.id,
|
||||
},
|
||||
})
|
||||
.then((res) => {
|
||||
botInfo.value = res.data;
|
||||
});
|
||||
api.get('/userCenter/bot/generateConversationId').then((res) => {
|
||||
conversationId.value = res.data;
|
||||
});
|
||||
}
|
||||
function addBotToRecentlyUsed(botId: any) {
|
||||
btnLoading.value = true;
|
||||
api
|
||||
.post('/userCenter/botRecentlyUsed/save', {
|
||||
botId,
|
||||
})
|
||||
.then((res) => {
|
||||
btnLoading.value = false;
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success($t('message.success'));
|
||||
getUserUsed();
|
||||
}
|
||||
});
|
||||
}
|
||||
function removeBotFromRecentlyUsed(botId: any) {
|
||||
btnLoading.value = true;
|
||||
api
|
||||
.get('/userCenter/botRecentlyUsed/removeByBotId', {
|
||||
params: {
|
||||
botId,
|
||||
},
|
||||
})
|
||||
.then((res) => {
|
||||
btnLoading.value = false;
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success($t('message.success'));
|
||||
getUserUsed();
|
||||
}
|
||||
});
|
||||
}
|
||||
const messageList = ref<any>([]);
|
||||
function addMessage(message: any) {
|
||||
messageList.value.push(message);
|
||||
}
|
||||
function updateLastMessage(item: any) {
|
||||
const lastIndex = messageList.value.length - 1;
|
||||
let message = item;
|
||||
|
||||
if (typeof item === 'function') {
|
||||
message = item(messageList.value[lastIndex]);
|
||||
}
|
||||
|
||||
if (lastIndex >= 0) {
|
||||
messageList.value[lastIndex] = {
|
||||
...messageList.value[lastIndex],
|
||||
...message,
|
||||
};
|
||||
}
|
||||
}
|
||||
const stopThinking = () => {
|
||||
const lastIndex = messageList.value.length - 1;
|
||||
|
||||
if (lastIndex >= 0 && messageList.value[lastIndex]?.chains) {
|
||||
const chains = cloneDeep(messageList.value[lastIndex].chains);
|
||||
|
||||
for (const chain of chains) {
|
||||
if (!('id' in chain) && chain.thinkingStatus === 'thinking') {
|
||||
chain.thinkingStatus = 'end';
|
||||
}
|
||||
}
|
||||
|
||||
messageList.value[lastIndex].chains = chains;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElContainer class="bg-background-deep h-full p-6 pr-0">
|
||||
<ElMain
|
||||
class="border-border bg-background !flex flex-col rounded-xl border !p-6"
|
||||
>
|
||||
<ElSpace :size="16" class="cursor-pointer" @click="router.back()">
|
||||
<ElIcon :size="24"><ArrowLeft /></ElIcon>
|
||||
<ElSpace :size="12">
|
||||
<ElAvatar :size="36" :src="botInfo.icon || defaultBotAvatar" />
|
||||
<h1 class="text-base font-semibold">
|
||||
{{ botInfo.title }}
|
||||
</h1>
|
||||
</ElSpace>
|
||||
</ElSpace>
|
||||
<div class="relative mx-auto w-full max-w-[884px] flex-1">
|
||||
<Card
|
||||
v-if="messageList.length === 0"
|
||||
class="absolute left-1/2 top-1/2 max-w-[340px] -translate-x-1/2 -translate-y-1/2 flex-col items-center gap-0"
|
||||
>
|
||||
<ElAvatar :size="64" :src="botInfo.icon || defaultBotAvatar" />
|
||||
<CardTitle class="mt-4">{{ botInfo.title }}</CardTitle>
|
||||
<CardDescription class="mt-2.5 text-center text-[#566882]">
|
||||
{{ botInfo.description }}
|
||||
</CardDescription>
|
||||
</Card>
|
||||
<ChatBubbleList v-else :bot="botInfo" :messages="messageList" />
|
||||
<ChatSender
|
||||
class="absolute bottom-5 left-0 w-full"
|
||||
:add-message="addMessage"
|
||||
:update-last-message="updateLastMessage"
|
||||
:stop-thinking="stopThinking"
|
||||
:bot="botInfo"
|
||||
:conversation-id="conversationId"
|
||||
/>
|
||||
</div>
|
||||
</ElMain>
|
||||
<ElAside width="407px" class="px-3 pt-10">
|
||||
<Card class="mx-auto max-w-[340px] flex-col items-center gap-0">
|
||||
<ElAvatar :size="64" :src="botInfo.icon || defaultBotAvatar" />
|
||||
<CardTitle class="mt-4">{{ botInfo.title }}</CardTitle>
|
||||
<CardDescription class="mt-2.5 text-center text-[#566882]">
|
||||
{{ botInfo.description }}
|
||||
</CardDescription>
|
||||
<ElButton
|
||||
v-if="!usedList.includes(botInfo.id)"
|
||||
:loading="btnLoading"
|
||||
class="mt-8 !h-9 w-full"
|
||||
type="primary"
|
||||
:icon="Plus"
|
||||
@click="addBotToRecentlyUsed(botInfo.id)"
|
||||
>
|
||||
添加到聊天助理
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-else
|
||||
:loading="btnLoading"
|
||||
class="mt-8 !h-9 w-full"
|
||||
type="primary"
|
||||
:icon="Minus"
|
||||
@click="removeBotFromRecentlyUsed(botInfo.id)"
|
||||
>
|
||||
从聊天助理中移除
|
||||
</ElButton>
|
||||
</Card>
|
||||
</ElAside>
|
||||
</ElContainer>
|
||||
</template>
|
||||
233
easyflow-ui-usercenter/app/src/views/assistantMarket/index.vue
Normal file
233
easyflow-ui-usercenter/app/src/views/assistantMarket/index.vue
Normal file
@@ -0,0 +1,233 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { IconifyIcon } from '@easyflow/icons';
|
||||
import { cn } from '@easyflow/utils';
|
||||
|
||||
import { Minus, Plus, Search } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElButton,
|
||||
ElContainer,
|
||||
ElHeader,
|
||||
ElInput,
|
||||
ElMain,
|
||||
ElMessage,
|
||||
ElSpace,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import defaultBotAvatar from '#/assets/defaultBotAvatar.png';
|
||||
import {
|
||||
Card,
|
||||
CardAvatar,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardTitle,
|
||||
} from '#/components/card';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const router = useRouter();
|
||||
const categories = ref<any[]>([]);
|
||||
const botList = ref<any[]>([]);
|
||||
const queryParams = ref<any>({});
|
||||
const pageLoading = ref(false);
|
||||
const activeTag = ref('');
|
||||
const usedList = ref<any[]>([]);
|
||||
const btnLoading = ref(false);
|
||||
onMounted(async () => {
|
||||
getBotList();
|
||||
getCategoryList();
|
||||
getUserUsed();
|
||||
});
|
||||
function getCategoryList() {
|
||||
api.get('/userCenter/botCategory/list').then((res) => {
|
||||
categories.value = [
|
||||
{
|
||||
id: '',
|
||||
categoryName: '全部',
|
||||
},
|
||||
...res.data,
|
||||
];
|
||||
});
|
||||
}
|
||||
function getBotList() {
|
||||
pageLoading.value = true;
|
||||
api
|
||||
.get('/userCenter/bot/list', {
|
||||
params: { ...queryParams.value, status: 1 },
|
||||
})
|
||||
.then((res) => {
|
||||
pageLoading.value = false;
|
||||
botList.value = res.data;
|
||||
});
|
||||
}
|
||||
function handleTagClick(tag: any) {
|
||||
activeTag.value = tag;
|
||||
queryParams.value.categoryId = tag;
|
||||
getBotList();
|
||||
}
|
||||
function getUserUsed() {
|
||||
api.get('/userCenter/botRecentlyUsed/list').then((res) => {
|
||||
usedList.value = res.data.map((item: any) => item.botId);
|
||||
});
|
||||
}
|
||||
function addBotToRecentlyUsed(botId: any) {
|
||||
btnLoading.value = true;
|
||||
api
|
||||
.post('/userCenter/botRecentlyUsed/save', {
|
||||
botId,
|
||||
})
|
||||
.then((res) => {
|
||||
btnLoading.value = false;
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success($t('message.success'));
|
||||
getUserUsed();
|
||||
getBotList();
|
||||
}
|
||||
});
|
||||
}
|
||||
function removeBotFromRecentlyUsed(botId: any) {
|
||||
btnLoading.value = true;
|
||||
api
|
||||
.get('/userCenter/botRecentlyUsed/removeByBotId', {
|
||||
params: {
|
||||
botId,
|
||||
},
|
||||
})
|
||||
.then((res) => {
|
||||
btnLoading.value = false;
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success($t('message.success'));
|
||||
getUserUsed();
|
||||
getBotList();
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElContainer class="bg-background-deep h-full">
|
||||
<ElHeader class="!h-auto !p-8 !pb-0">
|
||||
<ElSpace direction="vertical" :size="24" alignment="flex-start">
|
||||
<h1 class="text-2xl font-medium">助理市场</h1>
|
||||
<ElSpace :size="20">
|
||||
<ElInput
|
||||
placeholder="搜索"
|
||||
v-model="queryParams.title"
|
||||
@keyup.enter="getBotList"
|
||||
:prefix-icon="Search"
|
||||
/>
|
||||
<ElSpace :size="12">
|
||||
<button
|
||||
type="button"
|
||||
:class="
|
||||
cn(
|
||||
'border-border text-foreground bg-background h-[35px] w-[94px] rounded-3xl border text-sm',
|
||||
activeTag === category.id
|
||||
? 'border-primary text-primary bg-primary/10'
|
||||
: 'hover:bg-accent',
|
||||
)
|
||||
"
|
||||
v-for="category in categories"
|
||||
:key="category.id"
|
||||
@click="handleTagClick(category.id)"
|
||||
>
|
||||
{{ category.categoryName }}
|
||||
</button>
|
||||
</ElSpace>
|
||||
</ElSpace>
|
||||
</ElSpace>
|
||||
</ElHeader>
|
||||
<ElMain class="!px-8">
|
||||
<div
|
||||
class="grid grid-cols-[repeat(auto-fill,minmax(300px,1fr))] gap-5"
|
||||
v-loading="pageLoading"
|
||||
>
|
||||
<Card
|
||||
class="border-border bg-background h-[168px] w-full max-w-none flex-col justify-between rounded-xl border p-6 pb-5 transition hover:-translate-y-2 hover:shadow-[0px_2px_16px_0px_rgba(6,27,57,0.07)]"
|
||||
v-for="assistant in botList"
|
||||
:key="assistant.id"
|
||||
>
|
||||
<CardContent class="gap-3">
|
||||
<CardContent class="flex-row items-center gap-3">
|
||||
<CardAvatar
|
||||
:src="assistant.icon"
|
||||
:default-avatar="defaultBotAvatar"
|
||||
/>
|
||||
<CardTitle :title="assistant.title">
|
||||
{{ assistant.title }}
|
||||
</CardTitle>
|
||||
</CardContent>
|
||||
<CardDescription
|
||||
class="text-foreground/50 line-clamp-2 text-wrap text-sm"
|
||||
:title="assistant.description"
|
||||
>
|
||||
{{ assistant.description }}
|
||||
</CardDescription>
|
||||
</CardContent>
|
||||
<div class="flex w-full items-center">
|
||||
<ElButton
|
||||
v-if="!usedList.includes(assistant.id)"
|
||||
:loading="btnLoading"
|
||||
class="w-full"
|
||||
type="primary"
|
||||
style="--el-border: none"
|
||||
:icon="Plus"
|
||||
plain
|
||||
@click="addBotToRecentlyUsed(assistant.id)"
|
||||
>
|
||||
添加到聊天助理
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-else
|
||||
:loading="btnLoading"
|
||||
class="w-full"
|
||||
type="primary"
|
||||
style="--el-border: none"
|
||||
:icon="Minus"
|
||||
plain
|
||||
@click="removeBotFromRecentlyUsed(assistant.id)"
|
||||
>
|
||||
从聊天助理中移除
|
||||
</ElButton>
|
||||
|
||||
<ElButton
|
||||
class="w-full"
|
||||
type="primary"
|
||||
style="--el-border: none"
|
||||
plain
|
||||
@click="router.push(`/assistantMarket/${assistant.id}`)"
|
||||
>
|
||||
<template #icon>
|
||||
<IconifyIcon icon="mdi:play-outline" />
|
||||
</template>
|
||||
立即体验
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<!-- <ElRow class="w-full" :gutter="16">
|
||||
<ElCol :span="12">
|
||||
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
|
||||
</ElCol>
|
||||
</ElRow> -->
|
||||
</Card>
|
||||
</div>
|
||||
</ElMain>
|
||||
</ElContainer>
|
||||
</template>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.el-input :deep(.el-input__wrapper) {
|
||||
--el-input-border-radius: 18px;
|
||||
--el-input-border-color: #e6e9ee;
|
||||
}
|
||||
|
||||
:deep(.el-button) {
|
||||
--el-font-size-base: 12px;
|
||||
--el-button-font-weight: 400;
|
||||
}
|
||||
</style>
|
||||
63
easyflow-ui-usercenter/app/src/views/bots/bot/form.vue
Normal file
63
easyflow-ui-usercenter/app/src/views/bots/bot/form.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<script setup lang="ts">
|
||||
import { IconifyIcon } from '@easyflow/icons';
|
||||
|
||||
import { Upload } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElButton,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElSelect,
|
||||
ElUpload,
|
||||
} from 'element-plus';
|
||||
|
||||
const options = [
|
||||
{
|
||||
value: 'Option1',
|
||||
label: 'Option1',
|
||||
},
|
||||
{
|
||||
value: 'Option2',
|
||||
label: 'Option2',
|
||||
},
|
||||
{
|
||||
value: 'Option3',
|
||||
label: 'Option3',
|
||||
},
|
||||
{
|
||||
value: 'Option4',
|
||||
label: 'Option4',
|
||||
},
|
||||
{
|
||||
value: 'Option5',
|
||||
label: 'Option5',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-1 flex-col justify-between">
|
||||
<ElForm>
|
||||
<ElFormItem required label="input:">
|
||||
<div class="flex w-full flex-col gap-2">
|
||||
<ElInput />
|
||||
<span class="text-xs text-[#969799]">提示词</span>
|
||||
</div>
|
||||
</ElFormItem>
|
||||
<ElFormItem required label="idash:">
|
||||
<ElUpload :auto-upload="false" list-type="picture" :limit="1">
|
||||
<ElButton :icon="Upload">上传</ElButton>
|
||||
</ElUpload>
|
||||
</ElFormItem>
|
||||
<ElFormItem required label="idash:">
|
||||
<ElSelect :options="options" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<ElButton color="#0066FF" class="!h-11">
|
||||
<template #icon>
|
||||
<IconifyIcon icon="mdi:play-circle" />
|
||||
</template>
|
||||
开始运行
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
170
easyflow-ui-usercenter/app/src/views/bots/bot/index.vue
Normal file
170
easyflow-ui-usercenter/app/src/views/bots/bot/index.vue
Normal file
@@ -0,0 +1,170 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { sortNodes } from '@easyflow/utils';
|
||||
|
||||
import { ArrowLeft } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElAside,
|
||||
ElButton,
|
||||
ElContainer,
|
||||
ElHeader,
|
||||
ElIcon,
|
||||
ElMain,
|
||||
ElText,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import defaultBotAvatar from '#/assets/defaultBotAvatar.png';
|
||||
import {
|
||||
Card,
|
||||
CardAvatar,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardTitle,
|
||||
} from '#/components/card';
|
||||
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 router = useRouter();
|
||||
const workflowId = ref(route.params.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(`/userCenter/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(`/userCenter/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>
|
||||
<ElContainer class="h-full">
|
||||
<ElHeader class="!px-8 !py-4" height="fit-content">
|
||||
<div class="flex flex-col gap-6">
|
||||
<div
|
||||
class="flex cursor-pointer items-center gap-2.5"
|
||||
@click="router.back()"
|
||||
>
|
||||
<ElIcon size="24"><ArrowLeft /></ElIcon>
|
||||
<ElText truncated class="!text-lg font-medium">
|
||||
{{ workflowInfo?.title }}
|
||||
</ElText>
|
||||
</div>
|
||||
<div
|
||||
class="bg-background border-border flex items-center justify-between overflow-hidden rounded-lg border px-5 py-6"
|
||||
>
|
||||
<Card
|
||||
class="w-full max-w-none cursor-auto items-center gap-7 overflow-hidden"
|
||||
>
|
||||
<CardAvatar
|
||||
:size="72"
|
||||
:src="workflowInfo?.icon"
|
||||
:default-avatar="defaultBotAvatar"
|
||||
/>
|
||||
<CardContent class="gap-3">
|
||||
<CardTitle
|
||||
class="text-lg font-medium"
|
||||
:title="workflowInfo?.title"
|
||||
>
|
||||
{{ workflowInfo?.title }}
|
||||
</CardTitle>
|
||||
<CardDescription
|
||||
class="text-sm"
|
||||
:title="workflowInfo?.description"
|
||||
>
|
||||
{{ workflowInfo?.description }}
|
||||
</CardDescription>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<RouterLink to="/execHistory">
|
||||
<ElButton type="primary" size="large" round plain>
|
||||
执行记录
|
||||
</ElButton>
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</ElHeader>
|
||||
<ElMain class="!px-8 !pb-4 !pt-0">
|
||||
<ElContainer class="h-full gap-4">
|
||||
<ElAside
|
||||
width="366px"
|
||||
class="border-border bg-background flex flex-col gap-6 rounded-lg border p-5"
|
||||
>
|
||||
<h1 class="text-base font-medium">输入参数</h1>
|
||||
<WorkflowForm
|
||||
v-if="runParams && tinyFlowData"
|
||||
ref="workflowForm"
|
||||
:workflow-id="workflowId"
|
||||
:workflow-params="runParams"
|
||||
:on-submit="onSubmit"
|
||||
:on-async-execute="onAsyncExecute"
|
||||
:tiny-flow-data="tinyFlowData"
|
||||
/>
|
||||
</ElAside>
|
||||
<ElAside width="366px">
|
||||
<div
|
||||
class="border-border bg-background flex h-full flex-col gap-6 rounded-lg border p-5"
|
||||
>
|
||||
<h1 class="text-base font-medium">执行步骤</h1>
|
||||
<WorkflowSteps
|
||||
v-if="tinyFlowData"
|
||||
:workflow-id="workflowId"
|
||||
:node-json="sortNodes(tinyFlowData)"
|
||||
:init-signal="initState"
|
||||
:polling-data="chainInfo"
|
||||
@resume="resumeChain"
|
||||
/>
|
||||
</div>
|
||||
</ElAside>
|
||||
<div
|
||||
class="bg-background border-border flex flex-1 flex-col gap-6 rounded-lg border p-5"
|
||||
>
|
||||
<h1 class="text-base font-medium">运行结果</h1>
|
||||
<div
|
||||
class="bg-background-deep border-border flex-1 rounded-lg border p-4"
|
||||
>
|
||||
<ExecResult
|
||||
v-if="tinyFlowData"
|
||||
:workflow-id="workflowId"
|
||||
:node-json="sortNodes(tinyFlowData)"
|
||||
:init-signal="initState"
|
||||
:polling-data="chainInfo"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ElContainer>
|
||||
</ElMain>
|
||||
</ElContainer>
|
||||
</template>
|
||||
130
easyflow-ui-usercenter/app/src/views/bots/index.vue
Normal file
130
easyflow-ui-usercenter/app/src/views/bots/index.vue
Normal file
@@ -0,0 +1,130 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { cn } from '@easyflow/utils';
|
||||
|
||||
import { Search } from '@element-plus/icons-vue';
|
||||
import { ElContainer, ElHeader, ElInput, ElMain, ElSpace } from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import defaultBotAvatar from '#/assets/ai/workflow/workflowIcon.png';
|
||||
import {
|
||||
Card,
|
||||
CardAvatar,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardTitle,
|
||||
} from '#/components/card';
|
||||
|
||||
const categories = ref<any[]>([]);
|
||||
const workflowList = ref<any[]>([]);
|
||||
const queryParams = ref<any>({});
|
||||
const pageLoading = ref(false);
|
||||
const activeTag = ref('');
|
||||
onMounted(async () => {
|
||||
getWorkflowList();
|
||||
getCategoryList();
|
||||
});
|
||||
function getCategoryList() {
|
||||
api.get('/userCenter/workflowCategory/list').then((res) => {
|
||||
categories.value = [
|
||||
{
|
||||
id: '',
|
||||
categoryName: '全部',
|
||||
},
|
||||
...res.data,
|
||||
];
|
||||
});
|
||||
}
|
||||
function getWorkflowList() {
|
||||
pageLoading.value = true;
|
||||
api
|
||||
.get('/userCenter/workflow/list', {
|
||||
params: { ...queryParams.value, status: 1 },
|
||||
})
|
||||
.then((res) => {
|
||||
pageLoading.value = false;
|
||||
workflowList.value = res.data;
|
||||
});
|
||||
}
|
||||
function handleTagClick(tag: any) {
|
||||
activeTag.value = tag;
|
||||
queryParams.value.categoryId = tag;
|
||||
getWorkflowList();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElContainer class="bg-background-deep h-full">
|
||||
<ElHeader class="!h-auto !p-8 !pb-0">
|
||||
<ElSpace direction="vertical" :size="24" alignment="flex-start">
|
||||
<h1 class="text-2xl font-medium">智能体</h1>
|
||||
<ElSpace :size="20">
|
||||
<ElInput
|
||||
placeholder="搜索"
|
||||
v-model="queryParams.title"
|
||||
@keyup.enter="getWorkflowList"
|
||||
:prefix-icon="Search"
|
||||
/>
|
||||
<ElSpace :size="12">
|
||||
<button
|
||||
type="button"
|
||||
:class="
|
||||
cn(
|
||||
'border-border text-foreground bg-background h-[35px] w-[94px] rounded-3xl border text-sm',
|
||||
activeTag === category.id
|
||||
? 'border-primary text-primary bg-primary/10'
|
||||
: 'hover:bg-accent',
|
||||
)
|
||||
"
|
||||
v-for="category in categories"
|
||||
:key="category.id"
|
||||
@click="handleTagClick(category.id)"
|
||||
>
|
||||
{{ category.categoryName }}
|
||||
</button>
|
||||
</ElSpace>
|
||||
</ElSpace>
|
||||
</ElSpace>
|
||||
</ElHeader>
|
||||
<ElMain class="!px-8">
|
||||
<div
|
||||
class="grid grid-cols-[repeat(auto-fill,minmax(300px,1fr))] gap-5"
|
||||
v-loading="pageLoading"
|
||||
>
|
||||
<RouterLink
|
||||
v-for="workflow in workflowList"
|
||||
:key="workflow.id"
|
||||
:to="`/workflow/${workflow.id}`"
|
||||
>
|
||||
<Card
|
||||
class="border-border bg-background h-[168px] max-w-none flex-col gap-3 rounded-xl border p-6 pb-0 pr-4 transition hover:-translate-y-2 hover:shadow-[0px_2px_16px_0px_rgba(6,27,57,0.07)]"
|
||||
>
|
||||
<CardContent class="flex-row items-center gap-3">
|
||||
<CardAvatar
|
||||
:src="workflow.icon"
|
||||
:default-avatar="defaultBotAvatar"
|
||||
/>
|
||||
<CardTitle :title="workflow.title">
|
||||
{{ workflow.title }}
|
||||
</CardTitle>
|
||||
</CardContent>
|
||||
<CardDescription
|
||||
class="line-clamp-2 text-sm text-[#566882]"
|
||||
:title="workflow.description"
|
||||
>
|
||||
{{ workflow.description }}
|
||||
</CardDescription>
|
||||
</Card>
|
||||
</RouterLink>
|
||||
</div>
|
||||
</ElMain>
|
||||
</ElContainer>
|
||||
</template>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.el-input :deep(.el-input__wrapper) {
|
||||
--el-input-border-radius: 18px;
|
||||
--el-input-border-color: #e6e9ee;
|
||||
}
|
||||
</style>
|
||||
184
easyflow-ui-usercenter/app/src/views/chatAssistant/index.vue
Normal file
184
easyflow-ui-usercenter/app/src/views/chatAssistant/index.vue
Normal file
@@ -0,0 +1,184 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { IconifyIcon } from '@easyflow/icons';
|
||||
import { cloneDeep, cn } from '@easyflow/utils';
|
||||
|
||||
import { ElAside, ElContainer, ElMain } from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import defaultAssistantAvatar from '#/assets/defaultAssistantAvatar.svg';
|
||||
import {
|
||||
Card,
|
||||
CardAvatar,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardTitle,
|
||||
} from '#/components/card';
|
||||
import { ChatBubbleList, ChatContainer, ChatSender } from '#/components/chat';
|
||||
|
||||
onMounted(() => {
|
||||
getAssistantList();
|
||||
});
|
||||
const recentUsedAssistant = ref<any[]>([]);
|
||||
const currentBot = ref<any>({});
|
||||
const handleSelectAssistant = (bot: any) => {
|
||||
currentBot.value = bot;
|
||||
messageList.value = [];
|
||||
};
|
||||
function getAssistantList() {
|
||||
api.get('/userCenter/botRecentlyUsed/getRecentlyBot').then((res) => {
|
||||
recentUsedAssistant.value = res.data;
|
||||
if (recentUsedAssistant.value.length > 0) {
|
||||
currentBot.value = recentUsedAssistant.value[0];
|
||||
}
|
||||
});
|
||||
}
|
||||
const messageList = ref<any>([]);
|
||||
function addMessage(message: any) {
|
||||
messageList.value.push(message);
|
||||
}
|
||||
function updateLastMessage(item: any) {
|
||||
const lastIndex = messageList.value.length - 1;
|
||||
let message = item;
|
||||
|
||||
if (typeof item === 'function') {
|
||||
message = item(messageList.value[lastIndex]);
|
||||
}
|
||||
|
||||
if (lastIndex >= 0) {
|
||||
messageList.value[lastIndex] = {
|
||||
...messageList.value[lastIndex],
|
||||
...message,
|
||||
};
|
||||
}
|
||||
}
|
||||
const stopThinking = () => {
|
||||
const lastIndex = messageList.value.length - 1;
|
||||
|
||||
if (lastIndex >= 0 && messageList.value[lastIndex]?.chains) {
|
||||
const chains = cloneDeep(messageList.value[lastIndex].chains);
|
||||
|
||||
for (const chain of chains) {
|
||||
if (!('id' in chain) && chain.thinkingStatus === 'thinking') {
|
||||
chain.thinkingStatus = 'end';
|
||||
}
|
||||
}
|
||||
|
||||
messageList.value[lastIndex].chains = chains;
|
||||
}
|
||||
};
|
||||
function setMessageList(messages: any) {
|
||||
messageList.value = messages;
|
||||
}
|
||||
const isFold = ref(false);
|
||||
const toggleFold = () => {
|
||||
isFold.value = !isFold.value;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-background-deep h-full w-full p-6">
|
||||
<ElContainer
|
||||
class="bg-background border-border h-full overflow-hidden rounded-lg border"
|
||||
>
|
||||
<ElMain class="!p-0">
|
||||
<ChatContainer
|
||||
class="border-none"
|
||||
:bot="currentBot"
|
||||
:is-fold="isFold"
|
||||
:on-message-list="setMessageList"
|
||||
:toggle-fold="toggleFold"
|
||||
>
|
||||
<template #default="{ conversationId }">
|
||||
<div class="flex h-full flex-col justify-between">
|
||||
<ChatBubbleList :bot="currentBot" :messages="messageList" />
|
||||
<div class="mx-auto w-full max-w-[1000px]">
|
||||
<ChatSender
|
||||
:add-message="addMessage"
|
||||
:update-last-message="updateLastMessage"
|
||||
:stop-thinking="stopThinking"
|
||||
:bot="currentBot"
|
||||
:conversation-id="conversationId"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ChatContainer>
|
||||
</ElMain>
|
||||
<transition name="collapse-horizontal">
|
||||
<ElAside
|
||||
v-if="!isFold"
|
||||
width="283px"
|
||||
class="bg-background border-border flex flex-col gap-5 border-l p-5 pt-4"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="pl-2.5 text-base font-medium">助理</span>
|
||||
<IconifyIcon
|
||||
icon="svg:assistant-fold"
|
||||
class="cursor-pointer"
|
||||
@click="toggleFold"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex h-full flex-col gap-5 overflow-auto">
|
||||
<Card
|
||||
v-for="assistant in recentUsedAssistant"
|
||||
:key="assistant.id"
|
||||
:class="
|
||||
cn(
|
||||
currentBot.id === assistant.id
|
||||
? 'bg-[hsl(var(--primary)/15%)] dark:bg-[hsl(var(--accent))]'
|
||||
: 'hover:bg-[hsl(var(--accent))]',
|
||||
)
|
||||
"
|
||||
@click="handleSelectAssistant(assistant)"
|
||||
>
|
||||
<CardAvatar
|
||||
:src="assistant.icon"
|
||||
:default-avatar="defaultAssistantAvatar"
|
||||
/>
|
||||
<CardContent>
|
||||
<CardTitle
|
||||
:title="assistant.title"
|
||||
:class="cn(assistant.checked && 'text-primary')"
|
||||
>
|
||||
{{ assistant.title }}
|
||||
</CardTitle>
|
||||
<CardDescription :title="assistant.description">
|
||||
{{ assistant.description }}
|
||||
</CardDescription>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</ElAside>
|
||||
</transition>
|
||||
</ElContainer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.el-aside::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.collapse-horizontal-enter-active,
|
||||
.collapse-horizontal-leave-active {
|
||||
overflow: hidden;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.collapse-horizontal-enter-from,
|
||||
.collapse-horizontal-leave-to {
|
||||
max-width: 0;
|
||||
padding: 0;
|
||||
opacity: 0;
|
||||
transform-origin: left;
|
||||
}
|
||||
|
||||
.collapse-horizontal-enter-to,
|
||||
.collapse-horizontal-leave-from {
|
||||
max-width: 283px;
|
||||
opacity: 1;
|
||||
transform-origin: left;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,168 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { IconifyIcon } from '@easyflow/icons';
|
||||
import { copyToClipboard } from '@easyflow/utils';
|
||||
|
||||
import { ArrowLeft, Delete, MoreFilled } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElButton,
|
||||
ElContainer,
|
||||
ElDropdown,
|
||||
ElDropdownItem,
|
||||
ElHeader,
|
||||
ElMain,
|
||||
ElMessage,
|
||||
} from 'element-plus';
|
||||
import { tryit } from 'radash';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import { ChatBubbleList } from '#/components/chat';
|
||||
import { router } from '#/router';
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const ids = reactive({
|
||||
botId: '',
|
||||
conversationId: '',
|
||||
});
|
||||
const conversationInfo = ref<any>();
|
||||
const messageList = ref<any[]>([]);
|
||||
const loading = ref(true);
|
||||
|
||||
onMounted(() => {
|
||||
if (route.params.id) {
|
||||
ids.conversationId = route.params.id as string;
|
||||
getConversationDetails();
|
||||
}
|
||||
});
|
||||
|
||||
function getConversationDetails() {
|
||||
api
|
||||
.get('/userCenter/botConversation/detail', {
|
||||
params: {
|
||||
id: ids.conversationId,
|
||||
},
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
conversationInfo.value = res.data;
|
||||
ids.botId = res.data.botId;
|
||||
getMessageList();
|
||||
}
|
||||
});
|
||||
}
|
||||
function getMessageList() {
|
||||
api
|
||||
.get('/userCenter/botMessage/getMessages', {
|
||||
params: ids,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
messageList.value = res.data;
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
async function handleShare() {
|
||||
const shareLink = import.meta.env.DEV
|
||||
? `${location.origin}/share/${ids.conversationId}`
|
||||
: `${location.origin}/#/share/${ids.conversationId}`;
|
||||
const { success, error } = await copyToClipboard(shareLink);
|
||||
|
||||
if (success) {
|
||||
ElMessage.success('分享链接复制成功!');
|
||||
} else {
|
||||
ElMessage.error(error);
|
||||
}
|
||||
}
|
||||
async function handleDelete() {
|
||||
const [, res] = await tryit(api.post)('/userCenter/botConversation/remove', {
|
||||
id: ids.conversationId,
|
||||
});
|
||||
|
||||
if (res && res.errorCode === 0) {
|
||||
ElMessage.success('删除成功');
|
||||
router.back();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElContainer class="bg-background h-full">
|
||||
<ElHeader height="100px" class="border-border border-b !pr-10">
|
||||
<div class="flex h-full w-full items-center justify-between">
|
||||
<!-- Left -->
|
||||
<div class="flex items-center gap-3">
|
||||
<ElButton
|
||||
link
|
||||
style="font-size: 20px"
|
||||
:icon="ArrowLeft"
|
||||
@click="router.back()"
|
||||
/>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-lg font-medium">{{
|
||||
conversationInfo?.title
|
||||
}}</span>
|
||||
<div
|
||||
v-if="conversationInfo?.bot.title"
|
||||
class="text-foreground/70 rounded bg-[var(--el-fill-color-light)] p-1 text-xs"
|
||||
>
|
||||
{{ conversationInfo.bot.title }}
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-foreground/50 text-sm">{{
|
||||
conversationInfo?.created
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right -->
|
||||
<div class="flex items-center gap-5">
|
||||
<ElButton link style="font-size: 20px" @click="handleShare">
|
||||
<template #icon>
|
||||
<IconifyIcon icon="svg:share" />
|
||||
</template>
|
||||
</ElButton>
|
||||
<ElDropdown>
|
||||
<ElButton link style="font-size: 20px" :icon="MoreFilled" />
|
||||
|
||||
<template #dropdown>
|
||||
<ElDropdownItem
|
||||
style="color: var(--el-color-danger)"
|
||||
:icon="Delete"
|
||||
@click="handleDelete"
|
||||
>
|
||||
删除
|
||||
</ElDropdownItem>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
</div>
|
||||
</div>
|
||||
</ElHeader>
|
||||
<ElMain class="relative" v-loading="loading">
|
||||
<div
|
||||
class="absolute bottom-5 left-1/2 top-5 w-full max-w-[1000px] -translate-x-1/2"
|
||||
>
|
||||
<ChatBubbleList
|
||||
:bot="conversationInfo?.bot"
|
||||
:messages="messageList"
|
||||
:editable="false"
|
||||
:open-editor="() => {}"
|
||||
/>
|
||||
</div>
|
||||
</ElMain>
|
||||
</ElContainer>
|
||||
</template>
|
||||
|
||||
<style lang="css" scoped>
|
||||
:deep(.el-bubble-list) {
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-bubble-content-wrapper .el-bubble-content) {
|
||||
--bubble-content-max-width: calc(100% - 52px);
|
||||
}
|
||||
</style>
|
||||
177
easyflow-ui-usercenter/app/src/views/chatHistory/index.vue
Normal file
177
easyflow-ui-usercenter/app/src/views/chatHistory/index.vue
Normal file
@@ -0,0 +1,177 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { Delete, MoreFilled, Search } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElButton,
|
||||
ElContainer,
|
||||
ElDropdown,
|
||||
ElDropdownItem,
|
||||
ElDropdownMenu,
|
||||
ElHeader,
|
||||
ElInput,
|
||||
ElMain,
|
||||
ElMessage,
|
||||
ElSelect,
|
||||
ElSpace,
|
||||
ElText,
|
||||
} from 'element-plus';
|
||||
import { tryit } from 'radash';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import PageData from '#/components/page/PageData.vue';
|
||||
|
||||
const listTitles = ['聊天助理名称', '话题', '创建时间', '操作'];
|
||||
|
||||
const router = useRouter();
|
||||
const assistantList = ref<any[]>([]);
|
||||
const queryParams = ref<any>({});
|
||||
const pageRef = ref();
|
||||
|
||||
onMounted(() => {
|
||||
getAssistantList();
|
||||
});
|
||||
|
||||
async function getAssistantList() {
|
||||
api
|
||||
.get('/userCenter/bot/list', {
|
||||
params: { ...queryParams.value, status: 1 },
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
assistantList.value = res.data.map((item: any) => ({
|
||||
label: item.title,
|
||||
value: item.id,
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
function search() {
|
||||
pageRef.value.setQuery({ ...queryParams.value, status: 1 });
|
||||
}
|
||||
function toDetail(record: any) {
|
||||
router.push({ path: `/chatHistory/${record.id}` });
|
||||
}
|
||||
async function handleDelete(id: string) {
|
||||
const [, res] = await tryit(api.post)('/userCenter/botConversation/remove', {
|
||||
id,
|
||||
});
|
||||
|
||||
if (res && res.errorCode === 0) {
|
||||
search();
|
||||
ElMessage.success('删除成功');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElContainer class="bg-background-deep h-full">
|
||||
<ElHeader class="!h-auto !p-8 !pb-0">
|
||||
<ElSpace direction="vertical" :size="24" alignment="flex-start">
|
||||
<h1 class="text-2xl font-medium">聊天记录</h1>
|
||||
<div class="flex items-center gap-5">
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="text-nowrap text-sm">聊天助理</span>
|
||||
<ElSelect
|
||||
clearable
|
||||
v-model="queryParams.botId"
|
||||
:options="assistantList"
|
||||
placeholder="请选择聊天助理"
|
||||
@change="search"
|
||||
/>
|
||||
</div>
|
||||
<ElInput
|
||||
placeholder="搜索关键词"
|
||||
v-model="queryParams.title"
|
||||
@keyup.enter="search"
|
||||
@change="search"
|
||||
:prefix-icon="Search"
|
||||
/>
|
||||
</div>
|
||||
</ElSpace>
|
||||
</ElHeader>
|
||||
<ElMain class="!px-8">
|
||||
<ElContainer class="bg-background rounded-lg p-5">
|
||||
<ElHeader
|
||||
class="dark:bg-accent grid grid-cols-[repeat(3,minmax(0,1fr))_120px] place-items-center rounded-lg bg-[#f7f9fd] !p-0"
|
||||
height="54px"
|
||||
>
|
||||
<span
|
||||
class="text-accent-foreground text-sm"
|
||||
v-for="title in listTitles"
|
||||
:key="title"
|
||||
>
|
||||
{{ title }}
|
||||
</span>
|
||||
</ElHeader>
|
||||
<ElMain class="!p-0">
|
||||
<div class="flex flex-col items-center gap-5">
|
||||
<div class="w-full">
|
||||
<PageData
|
||||
page-url="/userCenter/botConversation/pageList"
|
||||
ref="pageRef"
|
||||
>
|
||||
<template #default="{ pageList }">
|
||||
<div
|
||||
class="text-foreground/90 grid h-[60px] grid-cols-[repeat(3,minmax(0,1fr))_120px] place-items-center text-sm hover:bg-[var(--el-fill-color-light)]"
|
||||
v-for="record in pageList"
|
||||
:key="record.id"
|
||||
>
|
||||
<ElText truncated>{{ record.bot.title }}</ElText>
|
||||
<ElText line-clamp="2">{{ record.title }}</ElText>
|
||||
<span>{{ record.created }}</span>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<ElButton
|
||||
class="[--el-font-weight-primary:400]"
|
||||
link
|
||||
type="primary"
|
||||
@click="toDetail(record)"
|
||||
>
|
||||
查看详情
|
||||
</ElButton>
|
||||
|
||||
<ElDropdown>
|
||||
<ElButton :icon="MoreFilled" link />
|
||||
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownItem>
|
||||
<ElButton
|
||||
link
|
||||
type="danger"
|
||||
:icon="Delete"
|
||||
@click="handleDelete(record.id)"
|
||||
>
|
||||
删除
|
||||
</ElButton>
|
||||
</ElDropdownItem>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</PageData>
|
||||
</div>
|
||||
</div>
|
||||
</ElMain>
|
||||
</ElContainer>
|
||||
</ElMain>
|
||||
</ElContainer>
|
||||
</template>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.el-select {
|
||||
--el-select-width: 165px;
|
||||
}
|
||||
|
||||
.el-select.bot-select {
|
||||
--el-select-width: 343px;
|
||||
}
|
||||
|
||||
.el-select :deep(.el-select__wrapper) {
|
||||
--el-border-radius-base: 8px;
|
||||
}
|
||||
</style>
|
||||
126
easyflow-ui-usercenter/app/src/views/chatHistory/share/index.vue
Normal file
126
easyflow-ui-usercenter/app/src/views/chatHistory/share/index.vue
Normal file
@@ -0,0 +1,126 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { ElContainer, ElHeader, ElMain } from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import { ChatBubbleList } from '#/components/chat';
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const ids = reactive({
|
||||
botId: '',
|
||||
conversationId: '',
|
||||
});
|
||||
const conversationInfo = ref<any>();
|
||||
const messageList = ref<any[]>([]);
|
||||
const loading = ref(true);
|
||||
|
||||
onMounted(() => {
|
||||
if (route.params.id) {
|
||||
ids.conversationId = route.params.id as string;
|
||||
getConversationDetails();
|
||||
}
|
||||
});
|
||||
|
||||
function getConversationDetails() {
|
||||
api
|
||||
.get('/userCenter/botConversation/detail', {
|
||||
params: {
|
||||
id: ids.conversationId,
|
||||
},
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
conversationInfo.value = res.data;
|
||||
ids.botId = res.data.botId;
|
||||
getMessageList();
|
||||
}
|
||||
});
|
||||
}
|
||||
function getMessageList() {
|
||||
api
|
||||
.get('/userCenter/botMessage/getMessages', {
|
||||
params: ids,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
messageList.value = res.data;
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full w-full px-12 py-8 max-sm:p-3">
|
||||
<ElContainer class="bg-background h-full">
|
||||
<ElHeader
|
||||
height="80px"
|
||||
class="rounded-xl bg-[#F8F8F9] !pr-9 max-sm:!h-16 max-sm:!pr-3"
|
||||
>
|
||||
<div class="flex h-full w-full items-center justify-between">
|
||||
<!-- Left -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-lg font-medium max-sm:text-base">{{
|
||||
conversationInfo?.title
|
||||
}}</span>
|
||||
<div
|
||||
v-if="conversationInfo?.bot.title"
|
||||
class="text-foreground/70 rounded bg-[#ECECEE] p-1 text-xs"
|
||||
>
|
||||
{{ conversationInfo.bot.title }}
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-foreground/50 text-sm max-sm:text-xs">{{
|
||||
conversationInfo?.created
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<!-- Right -->
|
||||
<img src="/logo.svg" class="w-40 max-sm:w-28" />
|
||||
</div>
|
||||
</ElHeader>
|
||||
<ElMain class="relative max-sm:mt-2 max-sm:!p-0" v-loading="loading">
|
||||
<div
|
||||
class="absolute bottom-5 left-1/2 top-5 w-full max-w-[1000px] -translate-x-1/2 max-sm:bottom-0 max-sm:top-0"
|
||||
>
|
||||
<ChatBubbleList
|
||||
class="relative mx-auto h-full max-w-[1000px]"
|
||||
:bot="conversationInfo?.bot"
|
||||
:messages="messageList"
|
||||
:editable="false"
|
||||
:open-editor="() => {}"
|
||||
/>
|
||||
</div>
|
||||
</ElMain>
|
||||
</ElContainer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="css" scoped>
|
||||
:deep(.el-bubble-list) {
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-bubble-content-wrapper .el-bubble-content) {
|
||||
--bubble-content-max-width: calc(100% - 52px);
|
||||
}
|
||||
|
||||
@media not all and (min-width: 640px) {
|
||||
:deep(.el-bubble) {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
:deep(.el-avatar) {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
:deep(.el-bubble-content-wrapper .el-bubble-content) {
|
||||
--bubble-content-max-width: calc(100% - 38px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,98 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@easyflow/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@easyflow/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
onMounted(() => {
|
||||
renderEcharts({
|
||||
grid: {
|
||||
bottom: 0,
|
||||
containLabel: true,
|
||||
left: '1%',
|
||||
right: '1%',
|
||||
top: '2 %',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
areaStyle: {},
|
||||
data: [
|
||||
111, 2000, 6000, 16_000, 33_333, 55_555, 64_000, 33_333, 18_000,
|
||||
36_000, 70_000, 42_444, 23_222, 13_000, 8000, 4000, 1200, 333, 222,
|
||||
111,
|
||||
],
|
||||
itemStyle: {
|
||||
color: '#5ab1ef',
|
||||
},
|
||||
smooth: true,
|
||||
type: 'line',
|
||||
},
|
||||
{
|
||||
areaStyle: {},
|
||||
data: [
|
||||
33, 66, 88, 333, 3333, 6200, 20_000, 3000, 1200, 13_000, 22_000,
|
||||
11_000, 2221, 1201, 390, 198, 60, 30, 22, 11,
|
||||
],
|
||||
itemStyle: {
|
||||
color: '#019680',
|
||||
},
|
||||
smooth: true,
|
||||
type: 'line',
|
||||
},
|
||||
],
|
||||
tooltip: {
|
||||
axisPointer: {
|
||||
lineStyle: {
|
||||
color: '#019680',
|
||||
width: 1,
|
||||
},
|
||||
},
|
||||
trigger: 'axis',
|
||||
},
|
||||
// xAxis: {
|
||||
// axisTick: {
|
||||
// show: false,
|
||||
// },
|
||||
// boundaryGap: false,
|
||||
// data: Array.from({ length: 18 }).map((_item, index) => `${index + 6}:00`),
|
||||
// type: 'category',
|
||||
// },
|
||||
xAxis: {
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
boundaryGap: false,
|
||||
data: Array.from({ length: 18 }).map((_item, index) => `${index + 6}:00`),
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
type: 'solid',
|
||||
width: 1,
|
||||
},
|
||||
show: true,
|
||||
},
|
||||
type: 'category',
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
max: 80_000,
|
||||
splitArea: {
|
||||
show: true,
|
||||
},
|
||||
splitNumber: 4,
|
||||
type: 'value',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EchartsUI ref="chartRef" />
|
||||
</template>
|
||||
@@ -0,0 +1,82 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@easyflow/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@easyflow/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
onMounted(() => {
|
||||
renderEcharts({
|
||||
legend: {
|
||||
bottom: 0,
|
||||
data: ['访问', '趋势'],
|
||||
},
|
||||
radar: {
|
||||
indicator: [
|
||||
{
|
||||
name: '网页',
|
||||
},
|
||||
{
|
||||
name: '移动端',
|
||||
},
|
||||
{
|
||||
name: 'Ipad',
|
||||
},
|
||||
{
|
||||
name: '客户端',
|
||||
},
|
||||
{
|
||||
name: '第三方',
|
||||
},
|
||||
{
|
||||
name: '其它',
|
||||
},
|
||||
],
|
||||
radius: '60%',
|
||||
splitNumber: 8,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
areaStyle: {
|
||||
opacity: 1,
|
||||
shadowBlur: 0,
|
||||
shadowColor: 'rgba(0,0,0,.2)',
|
||||
shadowOffsetX: 0,
|
||||
shadowOffsetY: 10,
|
||||
},
|
||||
data: [
|
||||
{
|
||||
itemStyle: {
|
||||
color: '#b6a2de',
|
||||
},
|
||||
name: '访问',
|
||||
value: [90, 50, 86, 40, 50, 20],
|
||||
},
|
||||
{
|
||||
itemStyle: {
|
||||
color: '#5ab1ef',
|
||||
},
|
||||
name: '趋势',
|
||||
value: [70, 75, 70, 76, 20, 85],
|
||||
},
|
||||
],
|
||||
itemStyle: {
|
||||
// borderColor: '#fff',
|
||||
borderRadius: 10,
|
||||
borderWidth: 2,
|
||||
},
|
||||
symbolSize: 0,
|
||||
type: 'radar',
|
||||
},
|
||||
],
|
||||
tooltip: {},
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EchartsUI ref="chartRef" />
|
||||
</template>
|
||||
@@ -0,0 +1,46 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@easyflow/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@easyflow/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
onMounted(() => {
|
||||
renderEcharts({
|
||||
series: [
|
||||
{
|
||||
animationDelay() {
|
||||
return Math.random() * 400;
|
||||
},
|
||||
animationEasing: 'exponentialInOut',
|
||||
animationType: 'scale',
|
||||
center: ['50%', '50%'],
|
||||
color: ['#5ab1ef', '#b6a2de', '#67e0e3', '#2ec7c9'],
|
||||
data: [
|
||||
{ name: '外包', value: 500 },
|
||||
{ name: '定制', value: 310 },
|
||||
{ name: '技术支持', value: 274 },
|
||||
{ name: '远程', value: 400 },
|
||||
].sort((a, b) => {
|
||||
return a.value - b.value;
|
||||
}),
|
||||
name: '商业占比',
|
||||
radius: '80%',
|
||||
roseType: 'radius',
|
||||
type: 'pie',
|
||||
},
|
||||
],
|
||||
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EchartsUI ref="chartRef" />
|
||||
</template>
|
||||
@@ -0,0 +1,65 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@easyflow/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@easyflow/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
onMounted(() => {
|
||||
renderEcharts({
|
||||
legend: {
|
||||
bottom: '2%',
|
||||
left: 'center',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
animationDelay() {
|
||||
return Math.random() * 100;
|
||||
},
|
||||
animationEasing: 'exponentialInOut',
|
||||
animationType: 'scale',
|
||||
avoidLabelOverlap: false,
|
||||
color: ['#5ab1ef', '#b6a2de', '#67e0e3', '#2ec7c9'],
|
||||
data: [
|
||||
{ name: '搜索引擎', value: 1048 },
|
||||
{ name: '直接访问', value: 735 },
|
||||
{ name: '邮件营销', value: 580 },
|
||||
{ name: '联盟广告', value: 484 },
|
||||
],
|
||||
emphasis: {
|
||||
label: {
|
||||
fontSize: '12',
|
||||
fontWeight: 'bold',
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
itemStyle: {
|
||||
// borderColor: '#fff',
|
||||
borderRadius: 10,
|
||||
borderWidth: 2,
|
||||
},
|
||||
label: {
|
||||
position: 'center',
|
||||
show: false,
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
name: '访问来源',
|
||||
radius: ['40%', '65%'],
|
||||
type: 'pie',
|
||||
},
|
||||
],
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EchartsUI ref="chartRef" />
|
||||
</template>
|
||||
@@ -0,0 +1,55 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@easyflow/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@easyflow/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
onMounted(() => {
|
||||
renderEcharts({
|
||||
grid: {
|
||||
bottom: 0,
|
||||
containLabel: true,
|
||||
left: '1%',
|
||||
right: '1%',
|
||||
top: '2 %',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
barMaxWidth: 80,
|
||||
// color: '#4f69fd',
|
||||
data: [
|
||||
3000, 2000, 3333, 5000, 3200, 4200, 3200, 2100, 3000, 5100, 6000,
|
||||
3200, 4800,
|
||||
],
|
||||
type: 'bar',
|
||||
},
|
||||
],
|
||||
tooltip: {
|
||||
axisPointer: {
|
||||
lineStyle: {
|
||||
// color: '#4f69fd',
|
||||
width: 1,
|
||||
},
|
||||
},
|
||||
trigger: 'axis',
|
||||
},
|
||||
xAxis: {
|
||||
data: Array.from({ length: 12 }).map((_item, index) => `${index + 1}月`),
|
||||
type: 'category',
|
||||
},
|
||||
yAxis: {
|
||||
max: 8000,
|
||||
splitNumber: 4,
|
||||
type: 'value',
|
||||
},
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EchartsUI ref="chartRef" />
|
||||
</template>
|
||||
@@ -0,0 +1,90 @@
|
||||
<script lang="ts" setup>
|
||||
import type { AnalysisOverviewItem } from '@easyflow/common-ui';
|
||||
import type { TabOption } from '@easyflow/types';
|
||||
|
||||
import {
|
||||
AnalysisChartCard,
|
||||
AnalysisChartsTabs,
|
||||
AnalysisOverview,
|
||||
} from '@easyflow/common-ui';
|
||||
import {
|
||||
SvgBellIcon,
|
||||
SvgCakeIcon,
|
||||
SvgCardIcon,
|
||||
SvgDownloadIcon,
|
||||
} from '@easyflow/icons';
|
||||
|
||||
import AnalyticsTrends from './analytics-trends.vue';
|
||||
import AnalyticsVisitsData from './analytics-visits-data.vue';
|
||||
import AnalyticsVisitsSales from './analytics-visits-sales.vue';
|
||||
import AnalyticsVisitsSource from './analytics-visits-source.vue';
|
||||
import AnalyticsVisits from './analytics-visits.vue';
|
||||
|
||||
const overviewItems: AnalysisOverviewItem[] = [
|
||||
{
|
||||
icon: SvgCardIcon,
|
||||
title: '用户量',
|
||||
totalTitle: '总用户量',
|
||||
totalValue: 120_000,
|
||||
value: 2000,
|
||||
},
|
||||
{
|
||||
icon: SvgCakeIcon,
|
||||
title: '访问量',
|
||||
totalTitle: '总访问量',
|
||||
totalValue: 500_000,
|
||||
value: 20_000,
|
||||
},
|
||||
{
|
||||
icon: SvgDownloadIcon,
|
||||
title: '下载量',
|
||||
totalTitle: '总下载量',
|
||||
totalValue: 120_000,
|
||||
value: 8000,
|
||||
},
|
||||
{
|
||||
icon: SvgBellIcon,
|
||||
title: '使用量',
|
||||
totalTitle: '总使用量',
|
||||
totalValue: 50_000,
|
||||
value: 5000,
|
||||
},
|
||||
];
|
||||
|
||||
const chartTabs: TabOption[] = [
|
||||
{
|
||||
label: '流量趋势',
|
||||
value: 'trends',
|
||||
},
|
||||
{
|
||||
label: '月访问量',
|
||||
value: 'visits',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-5">
|
||||
<AnalysisOverview :items="overviewItems" />
|
||||
<AnalysisChartsTabs :tabs="chartTabs" class="mt-5">
|
||||
<template #trends>
|
||||
<AnalyticsTrends />
|
||||
</template>
|
||||
<template #visits>
|
||||
<AnalyticsVisits />
|
||||
</template>
|
||||
</AnalysisChartsTabs>
|
||||
|
||||
<div class="mt-5 w-full md:flex">
|
||||
<AnalysisChartCard class="mt-5 md:mr-4 md:mt-0 md:w-1/3" title="访问数量">
|
||||
<AnalyticsVisitsData />
|
||||
</AnalysisChartCard>
|
||||
<AnalysisChartCard class="mt-5 md:mr-4 md:mt-0 md:w-1/3" title="访问来源">
|
||||
<AnalyticsVisitsSource />
|
||||
</AnalysisChartCard>
|
||||
<AnalysisChartCard class="mt-5 md:mt-0 md:w-1/3" title="访问来源">
|
||||
<AnalyticsVisitsSales />
|
||||
</AnalysisChartCard>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,266 @@
|
||||
<script lang="ts" setup>
|
||||
import type {
|
||||
WorkbenchProjectItem,
|
||||
WorkbenchQuickNavItem,
|
||||
WorkbenchTodoItem,
|
||||
WorkbenchTrendItem,
|
||||
} from '@easyflow/common-ui';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import {
|
||||
AnalysisChartCard,
|
||||
WorkbenchHeader,
|
||||
WorkbenchProject,
|
||||
WorkbenchQuickNav,
|
||||
WorkbenchTodo,
|
||||
WorkbenchTrends,
|
||||
} from '@easyflow/common-ui';
|
||||
import { preferences } from '@easyflow/preferences';
|
||||
import { useUserStore } from '@easyflow/stores';
|
||||
import { openWindow } from '@easyflow/utils';
|
||||
|
||||
import AnalyticsVisitsSource from '../analytics/analytics-visits-source.vue';
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
// 这是一个示例数据,实际项目中需要根据实际情况进行调整
|
||||
// url 也可以是内部路由,在 navTo 方法中识别处理,进行内部跳转
|
||||
// 例如:url: /dashboard/workspace
|
||||
const projectItems: WorkbenchProjectItem[] = [
|
||||
{
|
||||
color: '',
|
||||
content: '不要等待机会,而要创造机会。',
|
||||
date: '2021-04-01',
|
||||
group: '开源组',
|
||||
icon: 'carbon:logo-github',
|
||||
title: 'Github',
|
||||
url: 'https://github.com',
|
||||
},
|
||||
{
|
||||
color: '#3fb27f',
|
||||
content: '现在的你决定将来的你。',
|
||||
date: '2021-04-01',
|
||||
group: '算法组',
|
||||
icon: 'ion:logo-vue',
|
||||
title: 'Vue',
|
||||
url: 'https://vuejs.org',
|
||||
},
|
||||
{
|
||||
color: '#e18525',
|
||||
content: '没有什么才能比努力更重要。',
|
||||
date: '2021-04-01',
|
||||
group: '上班摸鱼',
|
||||
icon: 'ion:logo-html5',
|
||||
title: 'Html5',
|
||||
url: 'https://developer.mozilla.org/zh-CN/docs/Web/HTML',
|
||||
},
|
||||
{
|
||||
color: '#bf0c2c',
|
||||
content: '热情和欲望可以突破一切难关。',
|
||||
date: '2021-04-01',
|
||||
group: 'UI',
|
||||
icon: 'ion:logo-angular',
|
||||
title: 'Angular',
|
||||
url: 'https://angular.io',
|
||||
},
|
||||
{
|
||||
color: '#00d8ff',
|
||||
content: '健康的身体是实现目标的基石。',
|
||||
date: '2021-04-01',
|
||||
group: '技术牛',
|
||||
icon: 'bx:bxl-react',
|
||||
title: 'React',
|
||||
url: 'https://reactjs.org',
|
||||
},
|
||||
{
|
||||
color: '#EBD94E',
|
||||
content: '路是走出来的,而不是空想出来的。',
|
||||
date: '2021-04-01',
|
||||
group: '架构组',
|
||||
icon: 'ion:logo-javascript',
|
||||
title: 'Js',
|
||||
url: 'https://developer.mozilla.org/zh-CN/docs/Web/JavaScript',
|
||||
},
|
||||
];
|
||||
|
||||
// 同样,这里的 url 也可以使用以 http 开头的外部链接
|
||||
const quickNavItems: WorkbenchQuickNavItem[] = [
|
||||
{
|
||||
color: '#1fdaca',
|
||||
icon: 'ion:home-outline',
|
||||
title: '首页',
|
||||
url: '/',
|
||||
},
|
||||
{
|
||||
color: '#bf0c2c',
|
||||
icon: 'ion:grid-outline',
|
||||
title: '仪表盘',
|
||||
url: '/dashboard',
|
||||
},
|
||||
{
|
||||
color: '#e18525',
|
||||
icon: 'ion:layers-outline',
|
||||
title: '组件',
|
||||
url: '/demos/features/icons',
|
||||
},
|
||||
{
|
||||
color: '#3fb27f',
|
||||
icon: 'ion:settings-outline',
|
||||
title: '系统管理',
|
||||
url: '/demos/features/login-expired', // 这里的 URL 是示例,实际项目中需要根据实际情况进行调整
|
||||
},
|
||||
{
|
||||
color: '#4daf1bc9',
|
||||
icon: 'ion:key-outline',
|
||||
title: '权限管理',
|
||||
url: '/demos/access/page-control',
|
||||
},
|
||||
{
|
||||
color: '#00d8ff',
|
||||
icon: 'ion:bar-chart-outline',
|
||||
title: '图表',
|
||||
url: '/analytics',
|
||||
},
|
||||
];
|
||||
|
||||
const todoItems = ref<WorkbenchTodoItem[]>([
|
||||
{
|
||||
completed: false,
|
||||
content: `审查最近提交到Git仓库的前端代码,确保代码质量和规范。`,
|
||||
date: '2024-07-30 11:00:00',
|
||||
title: '审查前端代码提交',
|
||||
},
|
||||
{
|
||||
completed: true,
|
||||
content: `检查并优化系统性能,降低CPU使用率。`,
|
||||
date: '2024-07-30 11:00:00',
|
||||
title: '系统性能优化',
|
||||
},
|
||||
{
|
||||
completed: false,
|
||||
content: `进行系统安全检查,确保没有安全漏洞或未授权的访问。 `,
|
||||
date: '2024-07-30 11:00:00',
|
||||
title: '安全检查',
|
||||
},
|
||||
{
|
||||
completed: false,
|
||||
content: `更新项目中的所有npm依赖包,确保使用最新版本。`,
|
||||
date: '2024-07-30 11:00:00',
|
||||
title: '更新项目依赖',
|
||||
},
|
||||
{
|
||||
completed: false,
|
||||
content: `修复用户报告的页面UI显示问题,确保在不同浏览器中显示一致。 `,
|
||||
date: '2024-07-30 11:00:00',
|
||||
title: '修复UI显示问题',
|
||||
},
|
||||
]);
|
||||
const trendItems: WorkbenchTrendItem[] = [
|
||||
{
|
||||
avatar: 'svg:avatar-1',
|
||||
content: `在 <a>开源组</a> 创建了项目 <a>Vue</a>`,
|
||||
date: '刚刚',
|
||||
title: '威廉',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-2',
|
||||
content: `关注了 <a>威廉</a> `,
|
||||
date: '1个小时前',
|
||||
title: '艾文',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-3',
|
||||
content: `发布了 <a>个人动态</a> `,
|
||||
date: '1天前',
|
||||
title: '克里斯',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-4',
|
||||
content: `发表文章 <a>如何编写一个Vite插件</a> `,
|
||||
date: '2天前',
|
||||
title: 'EasyFlow',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-1',
|
||||
content: `回复了 <a>杰克</a> 的问题 <a>如何进行项目优化?</a>`,
|
||||
date: '3天前',
|
||||
title: '皮特',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-2',
|
||||
content: `关闭了问题 <a>如何运行项目</a> `,
|
||||
date: '1周前',
|
||||
title: '杰克',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-3',
|
||||
content: `发布了 <a>个人动态</a> `,
|
||||
date: '1周前',
|
||||
title: '威廉',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-4',
|
||||
content: `推送了代码到 <a>Github</a>`,
|
||||
date: '2021-04-01 20:00',
|
||||
title: '威廉',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-4',
|
||||
content: `发表文章 <a>如何编写使用 Admin EasyFlow</a> `,
|
||||
date: '2021-03-01 20:00',
|
||||
title: 'EasyFlow',
|
||||
},
|
||||
];
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// 这是一个示例方法,实际项目中需要根据实际情况进行调整
|
||||
// This is a sample method, adjust according to the actual project requirements
|
||||
function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) {
|
||||
if (nav.url?.startsWith('http')) {
|
||||
openWindow(nav.url);
|
||||
return;
|
||||
}
|
||||
if (nav.url?.startsWith('/')) {
|
||||
router.push(nav.url).catch((error) => {
|
||||
console.error('Navigation failed:', error);
|
||||
});
|
||||
} else {
|
||||
console.warn(`Unknown URL for navigation item: ${nav.title} -> ${nav.url}`);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-5">
|
||||
<WorkbenchHeader
|
||||
:avatar="userStore.userInfo?.avatar || preferences.app.defaultAvatar"
|
||||
>
|
||||
<template #title>
|
||||
早安, {{ userStore.userInfo?.realName }}, 开始您一天的工作吧!
|
||||
</template>
|
||||
<template #description> 今日晴,20℃ - 32℃! </template>
|
||||
</WorkbenchHeader>
|
||||
|
||||
<div class="mt-5 flex flex-col lg:flex-row">
|
||||
<div class="mr-4 w-full lg:w-3/5">
|
||||
<WorkbenchProject :items="projectItems" title="项目" @click="navTo" />
|
||||
<WorkbenchTrends :items="trendItems" class="mt-5" title="最新动态" />
|
||||
</div>
|
||||
<div class="w-full lg:w-2/5">
|
||||
<WorkbenchQuickNav
|
||||
:items="quickNavItems"
|
||||
class="mt-5 lg:mt-0"
|
||||
title="快捷导航"
|
||||
@click="navTo"
|
||||
/>
|
||||
<WorkbenchTodo :items="todoItems" class="mt-5" title="待办事项" />
|
||||
<AnalysisChartCard class="mt-5" title="访问来源">
|
||||
<AnalyticsVisitsSource />
|
||||
</AnalysisChartCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
167
easyflow-ui-usercenter/app/src/views/demos/cardTest/index.vue
Normal file
167
easyflow-ui-usercenter/app/src/views/demos/cardTest/index.vue
Normal file
@@ -0,0 +1,167 @@
|
||||
<!-- ParentComponent.vue -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<PageData
|
||||
ref="pageDataRef"
|
||||
page-url="/api/v1/sysAccount/page"
|
||||
:page-size="10"
|
||||
:init-query-params="{ status: 1 }"
|
||||
>
|
||||
<template #default="{ pageList }">
|
||||
<CardPage
|
||||
title-key="id"
|
||||
description-key="loginName"
|
||||
:data="pageList"
|
||||
:actions="actions"
|
||||
@action-click="handleAction"
|
||||
/>
|
||||
</template>
|
||||
</PageData>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref, markRaw, onMounted} from 'vue'
|
||||
import {Upload, Edit, Plus, Delete, View, Share, Download} from '@element-plus/icons-vue'
|
||||
|
||||
import CardPage from "#/components/cardPage/CardPage.vue";
|
||||
import PageData from "#/components/page/PageData.vue";
|
||||
|
||||
// 用户数据
|
||||
const userList = ref([])
|
||||
|
||||
// 分页状态
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(5)
|
||||
|
||||
// 操作按钮配置
|
||||
const actions = ref([
|
||||
{
|
||||
name: 'edit',
|
||||
label: '编辑',
|
||||
type: 'primary',
|
||||
icon: markRaw(Edit)
|
||||
},
|
||||
{
|
||||
name: 'view',
|
||||
label: '查看',
|
||||
type: 'success',
|
||||
icon: markRaw(View)
|
||||
},
|
||||
{
|
||||
name: 'delete',
|
||||
label: '删除',
|
||||
type: 'danger',
|
||||
icon: markRaw(Delete)
|
||||
},
|
||||
{
|
||||
name: 'share',
|
||||
label: '分享',
|
||||
type: 'info',
|
||||
icon: markRaw(Share)
|
||||
},
|
||||
{
|
||||
name: 'download',
|
||||
label: '下载',
|
||||
type: 'warning',
|
||||
icon: markRaw(Download)
|
||||
},
|
||||
{
|
||||
name: 'download',
|
||||
label: '下载',
|
||||
type: 'warning',
|
||||
icon: markRaw(Download)
|
||||
}
|
||||
])
|
||||
|
||||
// 模拟数据加载
|
||||
onMounted(() => {
|
||||
// 模拟异步数据加载
|
||||
setTimeout(() => {
|
||||
userList.value = [
|
||||
{
|
||||
id: 1,
|
||||
avatar: 'https://copyright.bdstatic.com/vcg/creative/d90a05ca26b2ca79dc1cbaa4931b18ee.jpg@wm_1,k_cGljX2JqaHdhdGVyLmpwZw==',
|
||||
title: '张三',
|
||||
description: '前端开发工程师,专注于Vue和React技术栈,前端开发工程师,专注于Vue和React技术栈'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
avatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',
|
||||
title: '李四',
|
||||
description: '后端开发工程师,擅长Java和Spring框架'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
avatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',
|
||||
title: '王五',
|
||||
description: 'UI设计师,专注于用户体验设计'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
avatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',
|
||||
title: '赵六',
|
||||
description: '全栈开发工程师,熟悉前后端技术'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
avatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',
|
||||
title: '钱七',
|
||||
description: '产品经理,负责产品规划和设计'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
avatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',
|
||||
title: '孙八',
|
||||
description: '测试工程师,确保产品质量'
|
||||
}
|
||||
]
|
||||
}, 500)
|
||||
})
|
||||
|
||||
// 处理操作按钮点击
|
||||
const handleAction = ({action, item}) => {
|
||||
console.log('执行操作:', action.name, '数据:', item)
|
||||
// 根据不同的操作执行不同的逻辑
|
||||
switch (action.name) {
|
||||
case 'edit':
|
||||
// 编辑逻辑
|
||||
break
|
||||
case 'delete':
|
||||
// 删除逻辑
|
||||
break
|
||||
// 其他操作...
|
||||
}
|
||||
}
|
||||
|
||||
// 处理分页变化
|
||||
const handlePageChange = ({currentPage: page, pageSize: size}) => {
|
||||
console.log('分页变化:', {page, size})
|
||||
}
|
||||
|
||||
// 处理当前页更新
|
||||
const handleCurrentPageUpdate = (page) => {
|
||||
currentPage.value = page
|
||||
console.log('当前页更新:', page)
|
||||
}
|
||||
|
||||
// 处理每页数量更新
|
||||
const handlePageSizeUpdate = (size) => {
|
||||
pageSize.value = size
|
||||
console.log('每页数量更新:', size)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.parent-container {
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
color: #303133;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div style="width: 300px; height: 100%">
|
||||
<CategoryPanel
|
||||
icon-key="icon1"
|
||||
title-key="name"
|
||||
:categories="categoryData"
|
||||
@click="handleCategoryClick"
|
||||
@panel-toggle="handlePanelToggle"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import CategoryPanel from "#/components/categoryPanel/CategoryPanel.vue";
|
||||
import { User, Message, Setting } from '@element-plus/icons-vue'
|
||||
|
||||
// 示例分类数据
|
||||
const categoryData = [
|
||||
{ name: '用户管理', icon1: User },
|
||||
{ name: '消息中心', icon1: Message },
|
||||
{ name: '系统设置', icon1: Setting },
|
||||
]
|
||||
|
||||
// 处理分类点击
|
||||
const handleCategoryClick = (category) => {
|
||||
console.log('点击分类:', category)
|
||||
}
|
||||
|
||||
// 处理面板收缩状态改变
|
||||
const handlePanelToggle = (collapsed) => {
|
||||
console.log('面板状态:', collapsed ? '已收缩' : '已展开')
|
||||
}
|
||||
</script>
|
||||
117
easyflow-ui-usercenter/app/src/views/demos/element/index.vue
Normal file
117
easyflow-ui-usercenter/app/src/views/demos/element/index.vue
Normal file
@@ -0,0 +1,117 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page } from '@easyflow/common-ui';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElCard,
|
||||
ElMessage,
|
||||
ElNotification,
|
||||
ElSegmented,
|
||||
ElSpace,
|
||||
ElTable,
|
||||
} from 'element-plus';
|
||||
|
||||
type NotificationType = 'error' | 'info' | 'success' | 'warning';
|
||||
|
||||
function info() {
|
||||
ElMessage.info('How many roads must a man walk down');
|
||||
}
|
||||
|
||||
function error() {
|
||||
ElMessage.error({
|
||||
duration: 2500,
|
||||
message: 'Once upon a time you dressed so fine',
|
||||
});
|
||||
}
|
||||
|
||||
function warning() {
|
||||
ElMessage.warning('How many roads must a man walk down');
|
||||
}
|
||||
function success() {
|
||||
ElMessage.success(
|
||||
'Cause you walked hand in hand With another man in my place',
|
||||
);
|
||||
}
|
||||
|
||||
function notify(type: NotificationType) {
|
||||
ElNotification({
|
||||
duration: 2500,
|
||||
message: '说点啥呢',
|
||||
type,
|
||||
});
|
||||
}
|
||||
const tableData = [
|
||||
{ prop1: '1', prop2: 'A' },
|
||||
{ prop1: '2', prop2: 'B' },
|
||||
{ prop1: '3', prop2: 'C' },
|
||||
{ prop1: '4', prop2: 'D' },
|
||||
{ prop1: '5', prop2: 'E' },
|
||||
{ prop1: '6', prop2: 'F' },
|
||||
];
|
||||
|
||||
const segmentedValue = ref('Mon');
|
||||
|
||||
const segmentedOptions = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page
|
||||
description="支持多语言,主题功能集成切换等"
|
||||
title="Element Plus组件使用演示"
|
||||
>
|
||||
<div class="flex flex-wrap gap-5">
|
||||
<ElCard class="mb-5 w-auto">
|
||||
<template #header> 按钮 </template>
|
||||
<ElSpace>
|
||||
<ElButton text>Text</ElButton>
|
||||
<ElButton>Default</ElButton>
|
||||
<ElButton type="primary"> Primary </ElButton>
|
||||
<ElButton type="info"> Info </ElButton>
|
||||
<ElButton type="success"> Success </ElButton>
|
||||
<ElButton type="warning"> Warning </ElButton>
|
||||
<ElButton type="danger"> Error </ElButton>
|
||||
</ElSpace>
|
||||
</ElCard>
|
||||
<ElCard class="mb-5 w-80">
|
||||
<template #header> Message </template>
|
||||
<ElSpace>
|
||||
<ElButton type="info" @click="info"> 信息 </ElButton>
|
||||
<ElButton type="danger" @click="error"> 错误 </ElButton>
|
||||
<ElButton type="warning" @click="warning"> 警告 </ElButton>
|
||||
<ElButton type="success" @click="success"> 成功 </ElButton>
|
||||
</ElSpace>
|
||||
</ElCard>
|
||||
<ElCard class="mb-5 w-80">
|
||||
<template #header> Notification </template>
|
||||
<ElSpace>
|
||||
<ElButton type="info" @click="notify('info')"> 信息 </ElButton>
|
||||
<ElButton type="danger" @click="notify('error')"> 错误 </ElButton>
|
||||
<ElButton type="warning" @click="notify('warning')"> 警告 </ElButton>
|
||||
<ElButton type="success" @click="notify('success')"> 成功 </ElButton>
|
||||
</ElSpace>
|
||||
</ElCard>
|
||||
<ElCard class="mb-5 w-auto">
|
||||
<template #header> Segmented </template>
|
||||
<ElSegmented
|
||||
v-model="segmentedValue"
|
||||
:options="segmentedOptions"
|
||||
size="large"
|
||||
/>
|
||||
</ElCard>
|
||||
<ElCard class="mb-5 w-80">
|
||||
<template #header> V-Loading </template>
|
||||
<div class="flex size-72 items-center justify-center" v-loading="true">
|
||||
一些演示的内容
|
||||
</div>
|
||||
</ElCard>
|
||||
<ElCard class="mb-5 w-80">
|
||||
<ElTable :data="tableData" stripe>
|
||||
<ElTable.TableColumn label="测试列1" prop="prop1" />
|
||||
<ElTable.TableColumn label="测试列2" prop="prop2" />
|
||||
</ElTable>
|
||||
</ElCard>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
191
easyflow-ui-usercenter/app/src/views/demos/form/basic.vue
Normal file
191
easyflow-ui-usercenter/app/src/views/demos/form/basic.vue
Normal file
@@ -0,0 +1,191 @@
|
||||
<script lang="ts" setup>
|
||||
import { h } from 'vue';
|
||||
|
||||
import { Page, useEasyFlowDrawer } from '@easyflow/common-ui';
|
||||
|
||||
import { ElButton, ElCard, ElCheckbox, ElMessage } from 'element-plus';
|
||||
|
||||
import { useEasyFlowForm } from '#/adapter/form';
|
||||
import { getAllMenusApi } from '#/api';
|
||||
|
||||
const [Form, formApi] = useEasyFlowForm({
|
||||
commonConfig: {
|
||||
// 所有表单项
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
layout: 'horizontal',
|
||||
// 大屏一行显示3个,中屏一行显示2个,小屏一行显示1个
|
||||
// wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
|
||||
handleSubmit: (values) => {
|
||||
ElMessage.success(`表单数据:${JSON.stringify(values)}`);
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
component: 'IconPicker',
|
||||
fieldName: 'icon',
|
||||
label: 'IconPicker',
|
||||
},
|
||||
{
|
||||
// 组件需要在 #/adapter.ts内注册,并加上类型
|
||||
component: 'ApiSelect',
|
||||
// 对应组件的参数
|
||||
componentProps: {
|
||||
// 菜单接口转options格式
|
||||
afterFetch: (data: { name: string; path: string }[]) => {
|
||||
return data.map((item: any) => ({
|
||||
label: item.name,
|
||||
value: item.path,
|
||||
}));
|
||||
},
|
||||
// 菜单接口
|
||||
api: getAllMenusApi,
|
||||
},
|
||||
// 字段名
|
||||
fieldName: 'api',
|
||||
// 界面显示的label
|
||||
label: 'ApiSelect',
|
||||
},
|
||||
{
|
||||
component: 'ApiTreeSelect',
|
||||
// 对应组件的参数
|
||||
componentProps: {
|
||||
// 菜单接口
|
||||
api: getAllMenusApi,
|
||||
childrenField: 'children',
|
||||
// 菜单接口转options格式
|
||||
labelField: 'name',
|
||||
valueField: 'path',
|
||||
},
|
||||
// 字段名
|
||||
fieldName: 'apiTree',
|
||||
// 界面显示的label
|
||||
label: 'ApiTreeSelect',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'string',
|
||||
label: 'String',
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
fieldName: 'number',
|
||||
label: 'Number',
|
||||
},
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
fieldName: 'radio',
|
||||
label: 'Radio',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ value: 'A', label: 'A' },
|
||||
{ value: 'B', label: 'B' },
|
||||
{ value: 'C', label: 'C' },
|
||||
{ value: 'D', label: 'D' },
|
||||
{ value: 'E', label: 'E' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
fieldName: 'radioButton',
|
||||
label: 'RadioButton',
|
||||
componentProps: {
|
||||
isButton: true,
|
||||
options: ['A', 'B', 'C', 'D', 'E', 'F'].map((v) => ({
|
||||
value: v,
|
||||
label: `选项${v}`,
|
||||
})),
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'CheckboxGroup',
|
||||
fieldName: 'checkbox',
|
||||
label: 'Checkbox',
|
||||
componentProps: {
|
||||
options: ['A', 'B', 'C'].map((v) => ({ value: v, label: `选项${v}` })),
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'CheckboxGroup',
|
||||
fieldName: 'checkbox1',
|
||||
label: 'Checkbox1',
|
||||
renderComponentContent: () => {
|
||||
return {
|
||||
default: () => {
|
||||
return ['A', 'B', 'C', 'D'].map((v) =>
|
||||
h(ElCheckbox, { label: v, value: v }),
|
||||
);
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'CheckboxGroup',
|
||||
fieldName: 'checkbotton',
|
||||
label: 'CheckBotton',
|
||||
componentProps: {
|
||||
isButton: true,
|
||||
options: [
|
||||
{ value: 'A', label: '选项A' },
|
||||
{ value: 'B', label: '选项B' },
|
||||
{ value: 'C', label: '选项C' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'DatePicker',
|
||||
fieldName: 'date',
|
||||
label: 'Date',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'select',
|
||||
label: 'Select',
|
||||
componentProps: {
|
||||
filterable: true,
|
||||
options: [
|
||||
{ value: 'A', label: '选项A' },
|
||||
{ value: 'B', label: '选项B' },
|
||||
{ value: 'C', label: '选项C' },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const [Drawer, drawerApi] = useEasyFlowDrawer();
|
||||
function setFormValues() {
|
||||
formApi.setValues({
|
||||
string: 'string',
|
||||
number: 123,
|
||||
radio: 'B',
|
||||
radioButton: 'C',
|
||||
checkbox: ['A', 'C'],
|
||||
checkbotton: ['B', 'C'],
|
||||
checkbox1: ['A', 'B'],
|
||||
date: new Date(),
|
||||
select: 'B',
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Page
|
||||
description="我们重新包装了CheckboxGroup、RadioGroup、Select,可以通过options属性传入选项属性数组以自动生成选项"
|
||||
title="表单演示"
|
||||
>
|
||||
<Drawer class="w-[600px]" title="基础表单示例">
|
||||
<Form />
|
||||
</Drawer>
|
||||
<ElCard>
|
||||
<template #header>
|
||||
<div class="flex items-center">
|
||||
<span class="flex-auto">基础表单演示</span>
|
||||
<ElButton type="primary" @click="setFormValues">设置表单值</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
<ElButton type="primary" @click="drawerApi.open"> 打开抽屉 </ElButton>
|
||||
</ElCard>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -0,0 +1,124 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { sortNodes } from '@easyflow/utils';
|
||||
|
||||
import { ArrowLeft } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElContainer,
|
||||
ElDivider,
|
||||
ElHeader,
|
||||
ElIcon,
|
||||
ElMain,
|
||||
ElSpace,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import ExecResult from '#/views/ai/workflow/components/ExecResult.vue';
|
||||
import WorkflowSteps from '#/views/ai/workflow/components/WorkflowSteps.vue';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const workflowJson = localStorage.getItem(`${route.params.id}-workflow-json`);
|
||||
const nodeJson = JSON.parse(workflowJson || '{}');
|
||||
|
||||
onMounted(() => {
|
||||
getStepList();
|
||||
if (!workflowJson) {
|
||||
router.push({
|
||||
path: '/execHistory',
|
||||
});
|
||||
}
|
||||
});
|
||||
onBeforeRouteLeave(() => {
|
||||
localStorage.removeItem(`${route.params.id}-workflow-json`);
|
||||
});
|
||||
|
||||
const stepList = ref<any>([]);
|
||||
|
||||
function getStepList() {
|
||||
api
|
||||
.get('/userCenter/workflowExecStep/getListByRecordId', {
|
||||
params: {
|
||||
recordId: route.params.id,
|
||||
},
|
||||
})
|
||||
.then((res) => {
|
||||
stepList.value = res.data;
|
||||
});
|
||||
}
|
||||
const result = computed(() => {
|
||||
if (stepList.value.length > 0) {
|
||||
const finalNode = stepList.value[stepList.value.length - 1];
|
||||
return {
|
||||
status: finalNode.status,
|
||||
result: JSON.parse(finalNode.output),
|
||||
message: finalNode.errorInfo,
|
||||
};
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
});
|
||||
const steps = computed(() => {
|
||||
return stepList.value.length > 0
|
||||
? stepList.value.map((item: any) => {
|
||||
return {
|
||||
key: item.id,
|
||||
label: item.nodeName,
|
||||
status: item.status,
|
||||
message: item.errorInfo,
|
||||
result: JSON.parse(item.output || '{}'),
|
||||
original: {
|
||||
type: '',
|
||||
},
|
||||
};
|
||||
})
|
||||
: [];
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElContainer class="bg-background-deep h-full">
|
||||
<ElHeader class="!p-8 !pb-0" height="auto">
|
||||
<ElSpace class="cursor-pointer" :size="10" @click="router.back()">
|
||||
<ElIcon size="24"><ArrowLeft /></ElIcon>
|
||||
<h1 class="text-2xl font-medium">
|
||||
{{ route.query.title }}
|
||||
</h1>
|
||||
</ElSpace>
|
||||
</ElHeader>
|
||||
<ElMain class="h-full items-center !px-8">
|
||||
<div class="border-border flex h-full overflow-hidden rounded-xl border">
|
||||
<div
|
||||
class="bg-background flex flex-1 flex-col gap-6 overflow-hidden rounded-lg p-5"
|
||||
>
|
||||
<h1 class="text-base font-medium">运行结果</h1>
|
||||
<div
|
||||
class="border-border bg-background-deep flex-1 rounded-lg border p-4"
|
||||
>
|
||||
<ExecResult
|
||||
v-if="nodeJson"
|
||||
workflow-id="workflowId"
|
||||
:node-json="sortNodes(nodeJson)"
|
||||
:polling-data="result"
|
||||
:show-message="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ElDivider class="!border-border !m-0 !h-full" direction="vertical" />
|
||||
<div
|
||||
class="bg-background flex h-full flex-1 flex-col gap-6 rounded-lg p-5"
|
||||
>
|
||||
<h1 class="text-base font-medium">执行步骤</h1>
|
||||
<WorkflowSteps
|
||||
v-if="nodeJson"
|
||||
workflow-id="workflowId"
|
||||
:node-json="steps"
|
||||
:polling-data="result"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ElMain>
|
||||
</ElContainer>
|
||||
</template>
|
||||
246
easyflow-ui-usercenter/app/src/views/execHistory/index.vue
Normal file
246
easyflow-ui-usercenter/app/src/views/execHistory/index.vue
Normal file
@@ -0,0 +1,246 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { Delete, MoreFilled } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElButton,
|
||||
ElContainer,
|
||||
ElDatePicker,
|
||||
ElDropdown,
|
||||
ElDropdownItem,
|
||||
ElDropdownMenu,
|
||||
ElHeader,
|
||||
ElMain,
|
||||
ElSelect,
|
||||
ElSpace,
|
||||
ElTag,
|
||||
ElText,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import PageData from '#/components/page/PageData.vue';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
onMounted(() => {
|
||||
getWorkflowList();
|
||||
});
|
||||
const options = [
|
||||
{
|
||||
value: 1,
|
||||
label: $t('aiWorkflowExecRecord.status1'),
|
||||
},
|
||||
{
|
||||
value: 5,
|
||||
label: $t('aiWorkflowExecRecord.status5'),
|
||||
},
|
||||
{
|
||||
value: 10,
|
||||
label: $t('aiWorkflowExecRecord.status10'),
|
||||
},
|
||||
{
|
||||
value: 20,
|
||||
label: $t('aiWorkflowExecRecord.status20'),
|
||||
},
|
||||
{
|
||||
value: 21,
|
||||
label: $t('aiWorkflowExecRecord.status21'),
|
||||
},
|
||||
{
|
||||
value: 22,
|
||||
label: $t('aiWorkflowExecRecord.status22'),
|
||||
},
|
||||
];
|
||||
|
||||
const listTitles = ['任务名称', '启动时间', '耗时', '状态', '操作'];
|
||||
|
||||
const queryParams = ref<any>({});
|
||||
const pageRef = ref();
|
||||
const workflowList = ref<any[]>([]);
|
||||
const router = useRouter();
|
||||
function search() {
|
||||
getDateRange();
|
||||
pageRef.value.setQuery(queryParams.value);
|
||||
}
|
||||
function getWorkflowList() {
|
||||
api
|
||||
.get('/userCenter/workflow/list', {
|
||||
params: { ...queryParams.value },
|
||||
})
|
||||
.then((res) => {
|
||||
workflowList.value = res.data;
|
||||
});
|
||||
}
|
||||
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';
|
||||
}
|
||||
}
|
||||
}
|
||||
function toDetail(record: any) {
|
||||
router.push({
|
||||
path: `/execHistory/${record.id}`,
|
||||
query: {
|
||||
title: record.title,
|
||||
},
|
||||
});
|
||||
localStorage.setItem(`${record.id}-workflow-json`, record.workflowJson);
|
||||
}
|
||||
const dateRange = ref<any>('');
|
||||
function getDateRange() {
|
||||
if (dateRange.value) {
|
||||
queryParams.value.queryBegin = dateRange.value[0];
|
||||
queryParams.value.queryEnd = dateRange.value[1];
|
||||
} else {
|
||||
queryParams.value.queryBegin = '';
|
||||
queryParams.value.queryEnd = '';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElContainer class="bg-background-deep h-full">
|
||||
<ElHeader class="!h-auto !p-8 !pb-0">
|
||||
<ElSpace direction="vertical" :size="24" alignment="flex-start">
|
||||
<h1 class="text-2xl font-medium">执行记录</h1>
|
||||
<div class="flex items-center gap-8">
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="text-nowrap text-sm">执行状态</span>
|
||||
<ElSelect
|
||||
clearable
|
||||
v-model="queryParams.status"
|
||||
:options="options"
|
||||
placeholder="请选择执行状态"
|
||||
@change="search"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="text-nowrap text-sm">智能体</span>
|
||||
<ElSelect
|
||||
class="bot-select"
|
||||
clearable
|
||||
:options="workflowList"
|
||||
placeholder="请选择智能体"
|
||||
v-model="queryParams.workflowId"
|
||||
@change="search"
|
||||
:props="{
|
||||
value: 'id',
|
||||
label: 'title',
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="text-nowrap text-sm">筛选时间</span>
|
||||
<ElDatePicker
|
||||
clearable
|
||||
type="daterange"
|
||||
v-model="dateRange"
|
||||
start-placeholder="选择开始日期"
|
||||
end-placeholder="选择结束日期"
|
||||
@change="search"
|
||||
value-format="YYYY-MM-DD"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ElSpace>
|
||||
</ElHeader>
|
||||
<ElMain class="!px-8">
|
||||
<ElContainer class="bg-background rounded-lg p-5">
|
||||
<ElHeader
|
||||
class="dark:bg-accent grid grid-cols-[repeat(4,minmax(0,1fr))_120px] place-items-center rounded-lg bg-[#f7f9fd] !p-0"
|
||||
height="54px"
|
||||
>
|
||||
<span
|
||||
class="text-accent-foreground text-sm"
|
||||
v-for="title in listTitles"
|
||||
:key="title"
|
||||
>
|
||||
{{ title }}
|
||||
</span>
|
||||
</ElHeader>
|
||||
<ElMain class="!p-0">
|
||||
<div class="flex flex-col items-center gap-5">
|
||||
<div class="w-full">
|
||||
<PageData
|
||||
page-url="/userCenter/workflowExecResult/getPage"
|
||||
ref="pageRef"
|
||||
>
|
||||
<template #default="{ pageList }">
|
||||
<div
|
||||
class="text-foreground/90 grid h-[60px] grid-cols-[repeat(4,minmax(0,1fr))_120px] place-items-center text-sm hover:bg-[var(--el-fill-color-light)]"
|
||||
v-for="record in pageList"
|
||||
:key="record.id"
|
||||
>
|
||||
<ElText truncated>{{ record.title }}</ElText>
|
||||
<span>{{ record.startTime }}</span>
|
||||
<span>{{ record.execTime }} ms</span>
|
||||
<span>
|
||||
<ElTag :type="getTagType(record)">
|
||||
{{ $t(`aiWorkflowExecRecord.status${record.status}`) }}
|
||||
</ElTag>
|
||||
</span>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<ElButton
|
||||
class="[--el-font-weight-primary:400]"
|
||||
link
|
||||
type="primary"
|
||||
@click="toDetail(record)"
|
||||
>
|
||||
查看详情
|
||||
</ElButton>
|
||||
|
||||
<ElDropdown>
|
||||
<ElButton :icon="MoreFilled" link />
|
||||
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownItem>
|
||||
<ElButton type="danger" :icon="Delete" link>
|
||||
删除
|
||||
</ElButton>
|
||||
</ElDropdownItem>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</PageData>
|
||||
</div>
|
||||
</div>
|
||||
</ElMain>
|
||||
</ElContainer>
|
||||
</ElMain>
|
||||
</ElContainer>
|
||||
</template>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.el-select {
|
||||
--el-select-width: 165px;
|
||||
}
|
||||
|
||||
.el-select.bot-select {
|
||||
--el-select-width: 343px;
|
||||
}
|
||||
|
||||
.el-select :deep(.el-select__wrapper) {
|
||||
--el-border-radius-base: 8px;
|
||||
}
|
||||
</style>
|
||||
159
easyflow-ui-usercenter/app/src/views/personalCenter/index.vue
Normal file
159
easyflow-ui-usercenter/app/src/views/personalCenter/index.vue
Normal file
@@ -0,0 +1,159 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from 'element-plus';
|
||||
|
||||
import { reactive, ref } from 'vue';
|
||||
|
||||
import { useAppConfig } from '@easyflow/hooks';
|
||||
import { useAccessStore, useUserStore } from '@easyflow/stores';
|
||||
|
||||
import {
|
||||
ElAvatar,
|
||||
ElButton,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
ElSpace,
|
||||
ElUpload,
|
||||
} from 'element-plus';
|
||||
import { tryit } from 'radash';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import { useAuthStore } from '#/store';
|
||||
|
||||
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
||||
const userStore = useUserStore();
|
||||
const useAuth = useAuthStore();
|
||||
const formRef = ref<FormInstance>();
|
||||
const formData = reactive({
|
||||
nickname: userStore?.userInfo?.nickname,
|
||||
avatar: userStore?.userInfo?.avatar,
|
||||
mobile: userStore?.userInfo?.mobile,
|
||||
});
|
||||
|
||||
const editing = reactive({
|
||||
nickname: false,
|
||||
avatar: false,
|
||||
mobile: false,
|
||||
});
|
||||
const rules = {
|
||||
nickname: [{ required: true, message: '请输入用户名' }],
|
||||
mobile: [
|
||||
{ required: true, message: '请输入手机号' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号' },
|
||||
],
|
||||
};
|
||||
|
||||
const handleFieldChange = async (field: keyof typeof formData) => {
|
||||
if (!formRef.value) return;
|
||||
const [err] = await tryit(formRef.value.validateField)([field]);
|
||||
|
||||
if (!err) {
|
||||
submit(field);
|
||||
}
|
||||
};
|
||||
const handleCancelEdit = async (field: keyof typeof formData) => {
|
||||
editing[field] = false;
|
||||
formRef.value?.resetFields([field]);
|
||||
};
|
||||
|
||||
const handleUploadChange = (response: any) => {
|
||||
formData.avatar = response?.data.path;
|
||||
submit('avatar');
|
||||
};
|
||||
const accessStore = useAccessStore();
|
||||
const btnLoading = ref(false);
|
||||
function submit(field: keyof typeof formData) {
|
||||
btnLoading.value = true;
|
||||
api.post('/userCenter/sysAccount/updateProfile', formData).then((res) => {
|
||||
btnLoading.value = false;
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success('修改成功');
|
||||
editing[field] = false;
|
||||
useAuth.fetchUserInfo();
|
||||
}
|
||||
});
|
||||
}
|
||||
function logout() {
|
||||
useAuth.logout();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-background-deep h-full w-full pt-[156px]">
|
||||
<ElForm
|
||||
ref="formRef"
|
||||
class="mx-auto"
|
||||
style="max-width: 619px; width: 100%"
|
||||
label-position="top"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
hide-required-asterisk
|
||||
>
|
||||
<ElFormItem label="用户名:" prop="nickname">
|
||||
<div class="flex w-full justify-between gap-5">
|
||||
<template v-if="editing.nickname">
|
||||
<ElInput v-model="formData.nickname" />
|
||||
<ElSpace>
|
||||
<ElButton
|
||||
:loading="btnLoading"
|
||||
type="primary"
|
||||
@click="handleFieldChange('nickname')"
|
||||
>
|
||||
确定
|
||||
</ElButton>
|
||||
<ElButton @click="handleCancelEdit('nickname')"> 取消 </ElButton>
|
||||
</ElSpace>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span>{{ formData.nickname }}</span>
|
||||
<ElButton @click="editing.nickname = true">编辑用户名</ElButton>
|
||||
</template>
|
||||
</div>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="头像:">
|
||||
<ElSpace>
|
||||
<ElUpload
|
||||
:action="`${apiURL}/userCenter/commons/upload`"
|
||||
:headers="{
|
||||
'easyflow-token': accessStore.accessToken,
|
||||
}"
|
||||
:show-file-list="false"
|
||||
:on-success="handleUploadChange"
|
||||
>
|
||||
<ElAvatar :src="formData.avatar" :size="46" />
|
||||
</ElUpload>
|
||||
<span>支持 2M 以内的 JPG 或 PNG 图片</span>
|
||||
</ElSpace>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="手机号:" prop="mobile">
|
||||
<div class="flex w-full justify-between gap-5">
|
||||
<template v-if="editing.mobile">
|
||||
<ElInput v-model="formData.mobile" />
|
||||
<ElSpace>
|
||||
<ElButton
|
||||
:loading="btnLoading"
|
||||
type="primary"
|
||||
@click="handleFieldChange('mobile')"
|
||||
>
|
||||
确定
|
||||
</ElButton>
|
||||
<ElButton @click="handleCancelEdit('mobile')">取消</ElButton>
|
||||
</ElSpace>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span>{{ formData.mobile }}</span>
|
||||
<ElButton @click="editing.mobile = true">修改手机号</ElButton>
|
||||
</template>
|
||||
</div>
|
||||
</ElFormItem>
|
||||
<ElFormItem>
|
||||
<div class="mt-20 flex w-full justify-center">
|
||||
<ElButton @click="logout" type="primary" class="!h-11 w-[333px]">
|
||||
退出登录
|
||||
</ElButton>
|
||||
</div>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user