初始化

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

View File

@@ -0,0 +1,3 @@
# \_core
此目录包含应用程序正常运行所需的基本视图。这些视图是应用程序布局中使用的视图。

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
<script lang="ts" setup>
import { Fallback } from '@easyflow/common-ui';
</script>
<template>
<Fallback status="coming-soon" />
</template>

View File

@@ -0,0 +1,9 @@
<script lang="ts" setup>
import { Fallback } from '@easyflow/common-ui';
defineOptions({ name: 'Fallback403Demo' });
</script>
<template>
<Fallback status="403" />
</template>

View File

@@ -0,0 +1,9 @@
<script lang="ts" setup>
import { Fallback } from '@easyflow/common-ui';
defineOptions({ name: 'Fallback500Demo' });
</script>
<template>
<Fallback status="500" />
</template>

View File

@@ -0,0 +1,9 @@
<script lang="ts" setup>
import { Fallback } from '@easyflow/common-ui';
defineOptions({ name: 'Fallback404Demo' });
</script>
<template>
<Fallback status="404" />
</template>

View File

@@ -0,0 +1,9 @@
<script lang="ts" setup>
import { Fallback } from '@easyflow/common-ui';
defineOptions({ name: 'FallbackOfflineDemo' });
</script>
<template>
<Fallback status="offline" />
</template>

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,47 @@
<script setup lang="ts">
import { ref } from 'vue';
import { ElDialog, ElImage } from 'element-plus';
defineExpose({
openDialog,
});
const dialogVisible = ref(false);
const data = ref<any>();
function openDialog(row: any) {
data.value = row;
dialogVisible.value = true;
}
function closeDialog() {
dialogVisible.value = false;
}
</script>
<template>
<ElDialog
v-model="dialogVisible"
draggable
:title="$t('message.preview')"
:before-close="closeDialog"
:close-on-click-modal="false"
destroy-on-close
>
<div class="flex justify-center">
<ElImage
v-if="data.resourceType === 0"
style="width: 200px"
:preview-src-list="[data.resourceUrl]"
:src="data.resourceUrl"
/>
<video v-if="data.resourceType === 1" controls width="640" height="360">
<source :src="data.resourceUrl" type="video/mp4" />
您的浏览器不支持 video 元素
</video>
<audio v-if="data.resourceType === 2" controls :src="data.resourceUrl">
您的浏览器不支持 audio 元素
</audio>
</div>
</ElDialog>
</template>
<style scoped></style>

View File

@@ -0,0 +1,155 @@
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue';
import {
ElCard,
ElCheckbox,
ElCol,
ElImage,
ElRadio,
ElRow,
ElText,
ElTooltip,
} from 'element-plus';
import Tag from '#/components/tag/Tag.vue';
import { $t } from '#/locales';
import { useDictStore } from '#/store';
import {
getResourceOriginColor,
getResourceTypeColor,
getSrc,
} from '#/utils/resource';
import PreviewModal from '#/views/ai/resource/PreviewModal.vue';
export interface ResourceCardProps {
data: any[];
multiple?: boolean;
valueProp?: string;
}
const props = withDefaults(defineProps<ResourceCardProps>(), {
multiple: false,
valueProp: 'id',
});
const emit = defineEmits(['update:modelValue']);
onMounted(() => {
initDict();
});
const dictStore = useDictStore();
function initDict() {
dictStore.fetchDictionary('resourceType');
dictStore.fetchDictionary('resourceOriginType');
}
const previewDialog = ref();
const radioValue = ref('');
const checkAll = ref(false);
function choose() {
const arr = [];
if (props.multiple) {
for (const data of props.data) {
if (data.checkboxValue) {
arr.push(data);
}
}
} else {
if (radioValue.value) {
for (const data of props.data) {
if (data[props.valueProp] === radioValue.value) {
arr.push(data);
}
}
}
}
emit('update:modelValue', arr);
}
function handleCheckAllChange(val: any) {
if (val) {
for (const data of props.data) {
data.checkboxValue = data[props.valueProp];
}
} else {
for (const data of props.data) {
data.checkboxValue = '';
}
}
}
function preview(row: any) {
previewDialog.value.openDialog({ ...row });
}
watch(
[() => radioValue.value, () => props.data],
() => {
choose();
},
{ deep: true },
);
</script>
<template>
<div>
<PreviewModal ref="previewDialog" />
<ElCheckbox
v-if="multiple"
:label="$t('button.selectAll')"
v-model="checkAll"
@change="handleCheckAllChange"
/>
<ElRow :gutter="20">
<ElCol :span="6" v-for="item in data" :key="item.id" class="mb-5">
<ElCard
:body-style="{ padding: '12px', height: '285px' }"
shadow="hover"
>
<div>
<div>
<ElCheckbox
v-if="multiple"
v-model="item.checkboxValue"
:true-value="item[valueProp]"
false-value=""
/>
<ElRadio v-else v-model="radioValue" :value="item[valueProp]" />
</div>
<div>
<ElImage
@click="preview(item)"
:src="getSrc(item)"
style="width: 100%; height: 150px; cursor: pointer"
/>
</div>
<div>
<ElTooltip
:content="`${item.resourceName}.${item.suffix}`"
placement="top"
>
<ElText truncated>
{{ item.resourceName }}.{{ item.suffix }}
</ElText>
</ElTooltip>
</div>
<div class="flex gap-1.5">
<Tag
size="small"
:background-color="`${getResourceOriginColor(item)}15`"
:text-color="getResourceOriginColor(item)"
:text="
dictStore.getDictLabel('resourceOriginType', item.origin)
"
/>
<Tag
size="small"
:background-color="`${getResourceTypeColor(item)}15`"
:text-color="getResourceTypeColor(item)"
:text="
dictStore.getDictLabel('resourceType', item.resourceType)
"
/>
</div>
</div>
</ElCard>
</ElCol>
</ElRow>
</div>
</template>
<style scoped></style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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">&nbsp;</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>

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

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

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

View File

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

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

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

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

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

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

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

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

View File

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

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

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