Merge remote-tracking branch 'origin/main'
This commit is contained in:
commit
16ce2708c3
@ -2,6 +2,7 @@
|
|||||||
import type { NotificationItem } from '@vben/layouts';
|
import type { NotificationItem } from '@vben/layouts';
|
||||||
|
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
|
import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
|
||||||
import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
|
import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
|
||||||
@ -23,6 +24,7 @@ import LoginForm from '#/views/_core/authentication/login.vue';
|
|||||||
|
|
||||||
const notifications = ref<NotificationItem[]>([
|
const notifications = ref<NotificationItem[]>([
|
||||||
{
|
{
|
||||||
|
id: 1,
|
||||||
avatar: 'https://avatar.vercel.sh/vercel.svg?text=VB',
|
avatar: 'https://avatar.vercel.sh/vercel.svg?text=VB',
|
||||||
date: '3小时前',
|
date: '3小时前',
|
||||||
isRead: true,
|
isRead: true,
|
||||||
@ -30,6 +32,7 @@ const notifications = ref<NotificationItem[]>([
|
|||||||
title: '收到了 14 份新周报',
|
title: '收到了 14 份新周报',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: 2,
|
||||||
avatar: 'https://avatar.vercel.sh/1',
|
avatar: 'https://avatar.vercel.sh/1',
|
||||||
date: '刚刚',
|
date: '刚刚',
|
||||||
isRead: false,
|
isRead: false,
|
||||||
@ -37,6 +40,7 @@ const notifications = ref<NotificationItem[]>([
|
|||||||
title: '朱偏右 回复了你',
|
title: '朱偏右 回复了你',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: 3,
|
||||||
avatar: 'https://avatar.vercel.sh/1',
|
avatar: 'https://avatar.vercel.sh/1',
|
||||||
date: '2024-01-01',
|
date: '2024-01-01',
|
||||||
isRead: false,
|
isRead: false,
|
||||||
@ -44,14 +48,34 @@ const notifications = ref<NotificationItem[]>([
|
|||||||
title: '曲丽丽 评论了你',
|
title: '曲丽丽 评论了你',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: 4,
|
||||||
avatar: 'https://avatar.vercel.sh/satori',
|
avatar: 'https://avatar.vercel.sh/satori',
|
||||||
date: '1天前',
|
date: '1天前',
|
||||||
isRead: false,
|
isRead: false,
|
||||||
message: '描述信息描述信息描述信息',
|
message: '描述信息描述信息描述信息',
|
||||||
title: '代办提醒',
|
title: '代办提醒',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
avatar: 'https://avatar.vercel.sh/satori',
|
||||||
|
date: '1天前',
|
||||||
|
isRead: false,
|
||||||
|
message: '描述信息描述信息描述信息',
|
||||||
|
title: '跳转Workspace示例',
|
||||||
|
link: '/workspace',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
avatar: 'https://avatar.vercel.sh/satori',
|
||||||
|
date: '1天前',
|
||||||
|
isRead: false,
|
||||||
|
message: '描述信息描述信息描述信息',
|
||||||
|
title: '跳转外部链接示例',
|
||||||
|
link: 'https://doc.vben.pro',
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
const accessStore = useAccessStore();
|
const accessStore = useAccessStore();
|
||||||
@ -61,6 +85,13 @@ const showDot = computed(() =>
|
|||||||
);
|
);
|
||||||
|
|
||||||
const menus = computed(() => [
|
const menus = computed(() => [
|
||||||
|
{
|
||||||
|
handler: () => {
|
||||||
|
router.push({ name: 'Profile' });
|
||||||
|
},
|
||||||
|
icon: 'lucide:user',
|
||||||
|
text: $t('page.auth.profile'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
handler: () => {
|
handler: () => {
|
||||||
openWindow(VBEN_DOC_URL, {
|
openWindow(VBEN_DOC_URL, {
|
||||||
@ -102,6 +133,17 @@ function handleNoticeClear() {
|
|||||||
notifications.value = [];
|
notifications.value = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function markRead(id: number | string) {
|
||||||
|
const item = notifications.value.find((item) => item.id === id);
|
||||||
|
if (item) {
|
||||||
|
item.isRead = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove(id: number | string) {
|
||||||
|
notifications.value = notifications.value.filter((item) => item.id !== id);
|
||||||
|
}
|
||||||
|
|
||||||
function handleMakeAll() {
|
function handleMakeAll() {
|
||||||
notifications.value.forEach((item) => (item.isRead = true));
|
notifications.value.forEach((item) => (item.isRead = true));
|
||||||
}
|
}
|
||||||
@ -144,6 +186,8 @@ watch(
|
|||||||
:dot="showDot"
|
:dot="showDot"
|
||||||
:notifications="notifications"
|
:notifications="notifications"
|
||||||
@clear="handleNoticeClear"
|
@clear="handleNoticeClear"
|
||||||
|
@read="(item) => item.id && markRead(item.id)"
|
||||||
|
@remove="(item) => item.id && remove(item.id)"
|
||||||
@make-all="handleMakeAll"
|
@make-all="handleMakeAll"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -4,7 +4,8 @@
|
|||||||
"register": "Register",
|
"register": "Register",
|
||||||
"codeLogin": "Code Login",
|
"codeLogin": "Code Login",
|
||||||
"qrcodeLogin": "Qr Code Login",
|
"qrcodeLogin": "Qr Code Login",
|
||||||
"forgetPassword": "Forget Password"
|
"forgetPassword": "Forget Password",
|
||||||
|
"profile": "Profile"
|
||||||
},
|
},
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"title": "Dashboard",
|
"title": "Dashboard",
|
||||||
|
|||||||
@ -4,7 +4,8 @@
|
|||||||
"register": "注册",
|
"register": "注册",
|
||||||
"codeLogin": "验证码登录",
|
"codeLogin": "验证码登录",
|
||||||
"qrcodeLogin": "二维码登录",
|
"qrcodeLogin": "二维码登录",
|
||||||
"forgetPassword": "忘记密码"
|
"forgetPassword": "忘记密码",
|
||||||
|
"profile": "个人中心"
|
||||||
},
|
},
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"title": "概览",
|
"title": "概览",
|
||||||
|
|||||||
@ -89,6 +89,16 @@ const routes: RouteRecordRaw[] = [
|
|||||||
order: 9999,
|
order: 9999,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Profile',
|
||||||
|
path: '/profile',
|
||||||
|
component: () => import('#/views/_core/profile/index.vue'),
|
||||||
|
meta: {
|
||||||
|
icon: 'lucide:user',
|
||||||
|
hideInMenu: true,
|
||||||
|
title: $t('page.auth.profile'),
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default routes;
|
export default routes;
|
||||||
|
|||||||
65
apps/web-tdesign/src/views/_core/profile/base-setting.vue
Normal file
65
apps/web-tdesign/src/views/_core/profile/base-setting.vue
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { BasicOption } from '@vben/types';
|
||||||
|
|
||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
|
||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { ProfileBaseSetting } from '@vben/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((): VbenFormSchema[] => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'realName',
|
||||||
|
component: 'Input',
|
||||||
|
label: '姓名',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'username',
|
||||||
|
component: 'Input',
|
||||||
|
label: '用户名',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'roles',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
mode: 'tags',
|
||||||
|
options: MOCK_ROLES_OPTIONS,
|
||||||
|
},
|
||||||
|
label: '角色',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'introduction',
|
||||||
|
component: 'Textarea',
|
||||||
|
label: '个人简介',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const data = await getUserInfoApi();
|
||||||
|
profileBaseSettingRef.value.getFormApi().setValues(data);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<ProfileBaseSetting ref="profileBaseSettingRef" :form-schema="formSchema" />
|
||||||
|
</template>
|
||||||
49
apps/web-tdesign/src/views/_core/profile/index.vue
Normal file
49
apps/web-tdesign/src/views/_core/profile/index.vue
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { Profile } from '@vben/common-ui';
|
||||||
|
import { useUserStore } from '@vben/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="个人中心"
|
||||||
|
:user-info="userStore.userInfo"
|
||||||
|
:tabs="tabs"
|
||||||
|
>
|
||||||
|
<template #content>
|
||||||
|
<ProfileBase v-if="tabsValue === 'basic'" />
|
||||||
|
<ProfileSecuritySetting v-if="tabsValue === 'security'" />
|
||||||
|
<ProfilePasswordSetting v-if="tabsValue === 'password'" />
|
||||||
|
<ProfileNotificationSetting v-if="tabsValue === 'notice'" />
|
||||||
|
</template>
|
||||||
|
</Profile>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import { ProfileNotificationSetting } from '@vben/common-ui';
|
||||||
|
|
||||||
|
const formSchema = computed(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
value: true,
|
||||||
|
fieldName: 'accountPassword',
|
||||||
|
label: '账户密码',
|
||||||
|
description: '其他用户的消息将以站内信的形式通知',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: true,
|
||||||
|
fieldName: 'systemMessage',
|
||||||
|
label: '系统消息',
|
||||||
|
description: '系统消息将以站内信的形式通知',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: true,
|
||||||
|
fieldName: 'todoTask',
|
||||||
|
label: '待办任务',
|
||||||
|
description: '待办任务将以站内信的形式通知',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<ProfileNotificationSetting :form-schema="formSchema" />
|
||||||
|
</template>
|
||||||
@ -0,0 +1,66 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { ProfilePasswordSetting, z } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { message } from '#/adapter/tdesign';
|
||||||
|
|
||||||
|
const profilePasswordSettingRef = ref();
|
||||||
|
|
||||||
|
const formSchema = computed((): VbenFormSchema[] => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'oldPassword',
|
||||||
|
label: '旧密码',
|
||||||
|
component: 'VbenInputPassword',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入旧密码',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'newPassword',
|
||||||
|
label: '新密码',
|
||||||
|
component: 'VbenInputPassword',
|
||||||
|
componentProps: {
|
||||||
|
passwordStrength: true,
|
||||||
|
placeholder: '请输入新密码',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'confirmPassword',
|
||||||
|
label: '确认密码',
|
||||||
|
component: 'VbenInputPassword',
|
||||||
|
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() {
|
||||||
|
message.success('密码修改成功');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<ProfilePasswordSetting
|
||||||
|
ref="profilePasswordSettingRef"
|
||||||
|
class="w-1/3"
|
||||||
|
:form-schema="formSchema"
|
||||||
|
@submit="handleSubmit"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import { ProfileSecuritySetting } from '@vben/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>
|
||||||
@ -47,7 +47,7 @@ async function handleSubmit(e: Event) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const values = toRaw(await props.formApi.getValues());
|
const values = toRaw(await props.formApi.getValues()) ?? {};
|
||||||
await props.handleSubmit?.(values);
|
await props.handleSubmit?.(values);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,7 +56,7 @@ async function handleReset(e: Event) {
|
|||||||
e?.stopPropagation();
|
e?.stopPropagation();
|
||||||
const props = unref(rootProps);
|
const props = unref(rootProps);
|
||||||
|
|
||||||
const values = toRaw(await props.formApi?.getValues());
|
const values = toRaw(await props.formApi?.getValues()) ?? {};
|
||||||
|
|
||||||
if (isFunction(props.handleReset)) {
|
if (isFunction(props.handleReset)) {
|
||||||
await props.handleReset?.(values);
|
await props.handleReset?.(values);
|
||||||
|
|||||||
@ -36,6 +36,7 @@ function getDefaultState(): VbenFormProps {
|
|||||||
handleReset: undefined,
|
handleReset: undefined,
|
||||||
handleSubmit: undefined,
|
handleSubmit: undefined,
|
||||||
handleValuesChange: undefined,
|
handleValuesChange: undefined,
|
||||||
|
handleCollapsedChange: undefined,
|
||||||
layout: 'horizontal',
|
layout: 'horizontal',
|
||||||
resetButtonOptions: {},
|
resetButtonOptions: {},
|
||||||
schema: [],
|
schema: [],
|
||||||
|
|||||||
@ -379,6 +379,10 @@ export interface VbenFormProps<
|
|||||||
* 表单字段映射
|
* 表单字段映射
|
||||||
*/
|
*/
|
||||||
fieldMappingTime?: FieldMappingTime;
|
fieldMappingTime?: FieldMappingTime;
|
||||||
|
/**
|
||||||
|
* 表单收起展开状态变化回调
|
||||||
|
*/
|
||||||
|
handleCollapsedChange?: (collapsed: boolean) => void;
|
||||||
/**
|
/**
|
||||||
* 表单重置回调
|
* 表单重置回调
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import { useForm } from 'vee-validate';
|
|||||||
import { object, ZodIntersection, ZodNumber, ZodObject, ZodString } from 'zod';
|
import { object, ZodIntersection, ZodNumber, ZodObject, ZodString } from 'zod';
|
||||||
import { getDefaultsForSchema } from 'zod-defaults';
|
import { getDefaultsForSchema } from 'zod-defaults';
|
||||||
|
|
||||||
type ExtendFormProps = VbenFormProps & { formApi: ExtendedFormApi };
|
type ExtendFormProps = VbenFormProps & { formApi?: ExtendedFormApi };
|
||||||
|
|
||||||
export const [injectFormProps, provideFormProps] =
|
export const [injectFormProps, provideFormProps] =
|
||||||
createContext<[ComputedRef<ExtendFormProps> | ExtendFormProps, FormActions]>(
|
createContext<[ComputedRef<ExtendFormProps> | ExtendFormProps, FormActions]>(
|
||||||
|
|||||||
@ -40,7 +40,9 @@ const { delegatedSlots, form } = useFormInitial(props);
|
|||||||
provideFormProps([props, form]);
|
provideFormProps([props, form]);
|
||||||
|
|
||||||
const handleUpdateCollapsed = (value: boolean) => {
|
const handleUpdateCollapsed = (value: boolean) => {
|
||||||
currentCollapsed.value = !!value;
|
currentCollapsed.value = value;
|
||||||
|
// 触发收起展开状态变化回调
|
||||||
|
props.handleCollapsedChange?.(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
|
|||||||
@ -25,7 +25,7 @@ import {
|
|||||||
} from './use-form-context';
|
} from './use-form-context';
|
||||||
// 通过 extends 会导致热更新卡死,所以重复写了一遍
|
// 通过 extends 会导致热更新卡死,所以重复写了一遍
|
||||||
interface Props extends VbenFormProps {
|
interface Props extends VbenFormProps {
|
||||||
formApi: ExtendedFormApi;
|
formApi?: ExtendedFormApi;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
@ -44,11 +44,13 @@ provideComponentRefMap(componentRefMap);
|
|||||||
props.formApi?.mount?.(form, componentRefMap);
|
props.formApi?.mount?.(form, componentRefMap);
|
||||||
|
|
||||||
const handleUpdateCollapsed = (value: boolean) => {
|
const handleUpdateCollapsed = (value: boolean) => {
|
||||||
props.formApi?.setState({ collapsed: !!value });
|
props.formApi?.setState({ collapsed: value });
|
||||||
|
// 触发收起展开状态变化回调
|
||||||
|
forward.value.handleCollapsedChange?.(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
function handleKeyDownEnter(event: KeyboardEvent) {
|
function handleKeyDownEnter(event: KeyboardEvent) {
|
||||||
if (!state.value.submitOnEnter || !forward.value.formApi?.isMounted) {
|
if (!state?.value.submitOnEnter || !forward.value.formApi?.isMounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 如果是 textarea 不阻止默认行为,否则会导致无法换行。
|
// 如果是 textarea 不阻止默认行为,否则会导致无法换行。
|
||||||
@ -58,11 +60,11 @@ function handleKeyDownEnter(event: KeyboardEvent) {
|
|||||||
}
|
}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
forward.value.formApi.validateAndSubmitForm();
|
forward.value.formApi?.validateAndSubmitForm();
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleValuesChangeDebounced = useDebounceFn(async () => {
|
const handleValuesChangeDebounced = useDebounceFn(async () => {
|
||||||
state.value.submitOnChange && forward.value.formApi?.validateAndSubmitForm();
|
state?.value.submitOnChange && forward.value.formApi?.validateAndSubmitForm();
|
||||||
}, 300);
|
}, 300);
|
||||||
|
|
||||||
const valuesCache: Recordable<any> = {};
|
const valuesCache: Recordable<any> = {};
|
||||||
@ -74,7 +76,7 @@ onMounted(async () => {
|
|||||||
() => form.values,
|
() => form.values,
|
||||||
async (newVal) => {
|
async (newVal) => {
|
||||||
if (forward.value.handleValuesChange) {
|
if (forward.value.handleValuesChange) {
|
||||||
const fields = state.value.schema?.map((item) => {
|
const fields = state?.value.schema?.map((item) => {
|
||||||
return item.fieldName;
|
return item.fieldName;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -91,8 +93,9 @@ onMounted(async () => {
|
|||||||
|
|
||||||
if (changedFields.length > 0) {
|
if (changedFields.length > 0) {
|
||||||
// 调用handleValuesChange回调,传入所有表单值的深拷贝和变更的字段列表
|
// 调用handleValuesChange回调,传入所有表单值的深拷贝和变更的字段列表
|
||||||
|
const values = await forward.value.formApi?.getValues();
|
||||||
forward.value.handleValuesChange(
|
forward.value.handleValuesChange(
|
||||||
cloneDeep(await forward.value.formApi.getValues()),
|
cloneDeep(values ?? {}) as Record<string, any>,
|
||||||
changedFields,
|
changedFields,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -109,7 +112,7 @@ onMounted(async () => {
|
|||||||
<Form
|
<Form
|
||||||
@keydown.enter="handleKeyDownEnter"
|
@keydown.enter="handleKeyDownEnter"
|
||||||
v-bind="forward"
|
v-bind="forward"
|
||||||
:collapsed="state.collapsed"
|
:collapsed="state?.collapsed"
|
||||||
:component-bind-event-map="COMPONENT_BIND_EVENT_MAP"
|
:component-bind-event-map="COMPONENT_BIND_EVENT_MAP"
|
||||||
:component-map="COMPONENT_MAP"
|
:component-map="COMPONENT_MAP"
|
||||||
:form="form"
|
:form="form"
|
||||||
@ -126,7 +129,7 @@ onMounted(async () => {
|
|||||||
<slot v-bind="slotProps">
|
<slot v-bind="slotProps">
|
||||||
<FormActions
|
<FormActions
|
||||||
v-if="forward.showDefaultActions"
|
v-if="forward.showDefaultActions"
|
||||||
:model-value="state.collapsed"
|
:model-value="state?.collapsed"
|
||||||
@update:model-value="handleUpdateCollapsed"
|
@update:model-value="handleUpdateCollapsed"
|
||||||
>
|
>
|
||||||
<template #reset-before="resetSlotProps">
|
<template #reset-before="resetSlotProps">
|
||||||
|
|||||||
@ -2,3 +2,4 @@ export * from './about';
|
|||||||
export * from './authentication';
|
export * from './authentication';
|
||||||
export * from './dashboard';
|
export * from './dashboard';
|
||||||
export * from './fallback';
|
export * from './fallback';
|
||||||
|
export * from './profile';
|
||||||
|
|||||||
56
packages/effects/common-ui/src/ui/profile/base-setting.vue
Normal file
56
packages/effects/common-ui/src/ui/profile/base-setting.vue
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { Recordable } from '@vben/types';
|
||||||
|
|
||||||
|
import type { VbenFormSchema } from '@vben-core/form-ui';
|
||||||
|
|
||||||
|
import { computed, reactive } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenForm } from '@vben-core/form-ui';
|
||||||
|
import { VbenButton } from '@vben-core/shadcn-ui';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
formSchema?: VbenFormSchema[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
formSchema: () => [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
submit: [Recordable<any>];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm(
|
||||||
|
reactive({
|
||||||
|
commonConfig: {
|
||||||
|
// 所有表单项
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: computed(() => props.formSchema),
|
||||||
|
showDefaultActions: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
const { valid } = await formApi.validate();
|
||||||
|
const values = await formApi.getValues();
|
||||||
|
if (valid) {
|
||||||
|
emit('submit', values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
getFormApi: () => formApi,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div @keydown.enter.prevent="handleSubmit">
|
||||||
|
<Form />
|
||||||
|
<VbenButton type="submit" class="mt-4" @click="handleSubmit">
|
||||||
|
更新基本信息
|
||||||
|
</VbenButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
6
packages/effects/common-ui/src/ui/profile/index.ts
Normal file
6
packages/effects/common-ui/src/ui/profile/index.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export { default as ProfileBaseSetting } from './base-setting.vue';
|
||||||
|
export { default as ProfileNotificationSetting } from './notification-setting.vue';
|
||||||
|
export { default as ProfilePasswordSetting } from './password-setting.vue';
|
||||||
|
export { default as Profile } from './profile.vue';
|
||||||
|
export { default as ProfileSecuritySetting } from './security-setting.vue';
|
||||||
|
export type * from './types';
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { Recordable } from '@vben/types';
|
||||||
|
|
||||||
|
import type { SettingProps } from './types';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
Switch,
|
||||||
|
} from '@vben-core/shadcn-ui';
|
||||||
|
|
||||||
|
withDefaults(defineProps<SettingProps>(), {
|
||||||
|
formSchema: () => [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
change: [Recordable<any>];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
function handleChange(fieldName: string, value: boolean) {
|
||||||
|
emit('change', { fieldName, value });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Form class="space-y-8">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<template v-for="item in formSchema" :key="item.fieldName">
|
||||||
|
<FormField type="checkbox" :name="item.fieldName">
|
||||||
|
<FormItem
|
||||||
|
class="flex flex-row items-center justify-between rounded-lg border p-4"
|
||||||
|
>
|
||||||
|
<div class="space-y-0.5">
|
||||||
|
<FormLabel class="text-base"> {{ item.label }} </FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
{{ item.description }}
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
:model-value="item.value"
|
||||||
|
@update:model-value="handleChange(item.fieldName, $event)"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { Recordable } from '@vben/types';
|
||||||
|
|
||||||
|
import type { VbenFormSchema } from '@vben-core/form-ui';
|
||||||
|
|
||||||
|
import { computed, reactive } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenForm } from '@vben-core/form-ui';
|
||||||
|
import { VbenButton } from '@vben-core/shadcn-ui';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
formSchema?: VbenFormSchema[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
formSchema: () => [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
submit: [Recordable<any>];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm(
|
||||||
|
reactive({
|
||||||
|
commonConfig: {
|
||||||
|
// 所有表单项
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: computed(() => props.formSchema),
|
||||||
|
showDefaultActions: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
const { valid } = await formApi.validate();
|
||||||
|
const values = await formApi.getValues();
|
||||||
|
if (valid) {
|
||||||
|
emit('submit', values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
getFormApi: () => formApi,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Form />
|
||||||
|
<VbenButton type="submit" class="mt-4" @click="handleSubmit">
|
||||||
|
更新密码
|
||||||
|
</VbenButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
62
packages/effects/common-ui/src/ui/profile/profile.vue
Normal file
62
packages/effects/common-ui/src/ui/profile/profile.vue
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { Props } from './types';
|
||||||
|
|
||||||
|
import { preferences } from '@vben-core/preferences';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
Separator,
|
||||||
|
Tabs,
|
||||||
|
TabsList,
|
||||||
|
TabsTrigger,
|
||||||
|
VbenAvatar,
|
||||||
|
} from '@vben-core/shadcn-ui';
|
||||||
|
|
||||||
|
import { Page } from '../../components';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'ProfileUI',
|
||||||
|
});
|
||||||
|
|
||||||
|
withDefaults(defineProps<Props>(), {
|
||||||
|
title: '关于项目',
|
||||||
|
tabs: () => [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const tabsValue = defineModel<string>('modelValue');
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<div class="flex h-full w-full">
|
||||||
|
<Card class="w-1/6 flex-none">
|
||||||
|
<div class="mt-4 flex h-40 flex-col items-center justify-center gap-4">
|
||||||
|
<VbenAvatar
|
||||||
|
:src="userInfo?.avatar ?? preferences.app.defaultAvatar"
|
||||||
|
class="size-20"
|
||||||
|
/>
|
||||||
|
<span class="text-lg font-semibold">
|
||||||
|
{{ userInfo?.realName ?? '' }}
|
||||||
|
</span>
|
||||||
|
<span class="text-foreground/80 text-sm">
|
||||||
|
{{ userInfo?.username ?? '' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Separator class="my-4" />
|
||||||
|
<Tabs v-model="tabsValue" orientation="vertical" class="m-4">
|
||||||
|
<TabsList class="bg-card grid w-full grid-cols-1">
|
||||||
|
<TabsTrigger
|
||||||
|
v-for="tab in tabs"
|
||||||
|
:key="tab.value"
|
||||||
|
:value="tab.value"
|
||||||
|
class="data-[state=active]:bg-primary data-[state=active]:text-primary-foreground h-12 justify-start"
|
||||||
|
>
|
||||||
|
{{ tab.label }}
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
</Tabs>
|
||||||
|
</Card>
|
||||||
|
<Card class="ml-4 w-5/6 flex-auto p-8">
|
||||||
|
<slot name="content"></slot>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { Recordable } from '@vben/types';
|
||||||
|
|
||||||
|
import type { SettingProps } from './types';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
Switch,
|
||||||
|
} from '@vben-core/shadcn-ui';
|
||||||
|
|
||||||
|
withDefaults(defineProps<SettingProps>(), {
|
||||||
|
formSchema: () => [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
change: [Recordable<any>];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
function handleChange(fieldName: string, value: boolean) {
|
||||||
|
emit('change', { fieldName, value });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Form class="space-y-8">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<template v-for="item in formSchema" :key="item.fieldName">
|
||||||
|
<FormField type="checkbox" :name="item.fieldName">
|
||||||
|
<FormItem
|
||||||
|
class="flex flex-row items-center justify-between rounded-lg border p-4"
|
||||||
|
>
|
||||||
|
<div class="space-y-0.5">
|
||||||
|
<FormLabel class="text-base"> {{ item.label }} </FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
{{ item.description }}
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
:model-value="item.value"
|
||||||
|
@update:model-value="handleChange(item.fieldName, $event)"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</template>
|
||||||
21
packages/effects/common-ui/src/ui/profile/types.ts
Normal file
21
packages/effects/common-ui/src/ui/profile/types.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import type { BasicUserInfo } from '@vben/types';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
title?: string;
|
||||||
|
userInfo: BasicUserInfo | null;
|
||||||
|
tabs: {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FormSchemaItem {
|
||||||
|
description: string;
|
||||||
|
fieldName: string;
|
||||||
|
label: string;
|
||||||
|
value: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SettingProps {
|
||||||
|
formSchema: FormSchemaItem[];
|
||||||
|
}
|
||||||
@ -1,7 +1,9 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { NotificationItem } from './types';
|
import type { NotificationItem } from './types';
|
||||||
|
|
||||||
import { Bell, MailCheck } from '@vben/icons';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
import { Bell, CircleCheckBig, CircleX, MailCheck } from '@vben/icons';
|
||||||
import { $t } from '@vben/locales';
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -35,9 +37,11 @@ const emit = defineEmits<{
|
|||||||
clear: [];
|
clear: [];
|
||||||
makeAll: [];
|
makeAll: [];
|
||||||
read: [NotificationItem];
|
read: [NotificationItem];
|
||||||
|
remove: [NotificationItem];
|
||||||
viewAll: [];
|
viewAll: [];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
const [open, toggle] = useToggle();
|
const [open, toggle] = useToggle();
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
@ -58,7 +62,28 @@ function handleClear() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleClick(item: NotificationItem) {
|
function handleClick(item: NotificationItem) {
|
||||||
emit('read', item);
|
// 如果通知项有链接,点击时跳转
|
||||||
|
if (item.link) {
|
||||||
|
navigateTo(item.link, item.query, item.state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigateTo(
|
||||||
|
link: string,
|
||||||
|
query?: Record<string, any>,
|
||||||
|
state?: Record<string, any>,
|
||||||
|
) {
|
||||||
|
if (link.startsWith('http://') || link.startsWith('https://')) {
|
||||||
|
// 外部链接,在新标签页打开
|
||||||
|
window.open(link, '_blank');
|
||||||
|
} else {
|
||||||
|
// 内部路由链接,支持 query 参数和 state
|
||||||
|
router.push({
|
||||||
|
path: link,
|
||||||
|
query: query || {},
|
||||||
|
state,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
@ -91,7 +116,7 @@ function handleClick(item: NotificationItem) {
|
|||||||
</div>
|
</div>
|
||||||
<VbenScrollbar v-if="notifications.length > 0">
|
<VbenScrollbar v-if="notifications.length > 0">
|
||||||
<ul class="!flex max-h-[360px] w-full flex-col">
|
<ul class="!flex max-h-[360px] w-full flex-col">
|
||||||
<template v-for="item in notifications" :key="item.title">
|
<template v-for="item in notifications" :key="item.id ?? item.title">
|
||||||
<li
|
<li
|
||||||
class="hover:bg-accent border-border relative flex w-full cursor-pointer items-start gap-5 border-t px-3 py-3"
|
class="hover:bg-accent border-border relative flex w-full cursor-pointer items-start gap-5 border-t px-3 py-3"
|
||||||
@click="handleClick(item)"
|
@click="handleClick(item)"
|
||||||
@ -107,7 +132,6 @@ function handleClick(item: NotificationItem) {
|
|||||||
<img
|
<img
|
||||||
:src="item.avatar"
|
:src="item.avatar"
|
||||||
class="aspect-square h-full w-full object-cover"
|
class="aspect-square h-full w-full object-cover"
|
||||||
role="img"
|
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<div class="flex flex-col gap-1 leading-none">
|
<div class="flex flex-col gap-1 leading-none">
|
||||||
@ -119,6 +143,30 @@ function handleClick(item: NotificationItem) {
|
|||||||
{{ item.date }}
|
{{ item.date }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
class="absolute right-3 top-1/2 flex -translate-y-1/2 flex-col gap-2"
|
||||||
|
>
|
||||||
|
<VbenIconButton
|
||||||
|
v-if="!item.isRead"
|
||||||
|
size="xs"
|
||||||
|
variant="ghost"
|
||||||
|
class="h-6 px-2"
|
||||||
|
:tooltip="$t('common.confirm')"
|
||||||
|
@click.stop="emit('read', item)"
|
||||||
|
>
|
||||||
|
<CircleCheckBig class="size-4" />
|
||||||
|
</VbenIconButton>
|
||||||
|
<VbenIconButton
|
||||||
|
v-if="item.isRead"
|
||||||
|
size="xs"
|
||||||
|
variant="ghost"
|
||||||
|
class="text-destructive h-6 px-2"
|
||||||
|
:tooltip="$t('common.delete')"
|
||||||
|
@click.stop="emit('remove', item)"
|
||||||
|
>
|
||||||
|
<CircleX class="size-4" />
|
||||||
|
</VbenIconButton>
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@ -1,9 +1,17 @@
|
|||||||
interface NotificationItem {
|
interface NotificationItem {
|
||||||
|
id: number | string;
|
||||||
avatar: string;
|
avatar: string;
|
||||||
date: string;
|
date: string;
|
||||||
isRead?: boolean;
|
isRead?: boolean;
|
||||||
message: string;
|
message: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
/**
|
||||||
|
* 跳转链接,可以是路由路径或完整 URL
|
||||||
|
* @example '/dashboard' 或 'https://example.com'
|
||||||
|
*/
|
||||||
|
link?: string;
|
||||||
|
query?: Record<string, any>;
|
||||||
|
state?: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type { NotificationItem };
|
export type { NotificationItem };
|
||||||
|
|||||||
@ -30,6 +30,7 @@ describe('fileDownloader', () => {
|
|||||||
expect(result).toBeInstanceOf(Blob);
|
expect(result).toBeInstanceOf(Blob);
|
||||||
expect(result).toEqual(mockBlob);
|
expect(result).toEqual(mockBlob);
|
||||||
expect(mockAxiosInstance.get).toHaveBeenCalledWith(url, {
|
expect(mockAxiosInstance.get).toHaveBeenCalledWith(url, {
|
||||||
|
method: 'GET',
|
||||||
responseType: 'blob',
|
responseType: 'blob',
|
||||||
responseReturn: 'body',
|
responseReturn: 'body',
|
||||||
});
|
});
|
||||||
@ -51,6 +52,7 @@ describe('fileDownloader', () => {
|
|||||||
expect(result).toEqual(mockBlob);
|
expect(result).toEqual(mockBlob);
|
||||||
expect(mockAxiosInstance.get).toHaveBeenCalledWith(url, {
|
expect(mockAxiosInstance.get).toHaveBeenCalledWith(url, {
|
||||||
...customConfig,
|
...customConfig,
|
||||||
|
method: 'GET',
|
||||||
responseType: 'blob',
|
responseType: 'blob',
|
||||||
responseReturn: 'body',
|
responseReturn: 'body',
|
||||||
});
|
});
|
||||||
@ -84,3 +86,72 @@ describe('fileDownloader', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('fileDownloader use other method', () => {
|
||||||
|
let fileDownloader: FileDownloader;
|
||||||
|
|
||||||
|
it('should call request using get', async () => {
|
||||||
|
const url = 'https://example.com/file';
|
||||||
|
const mockBlob = new Blob(['file content'], { type: 'text/plain' });
|
||||||
|
const mockResponse: Blob = mockBlob;
|
||||||
|
|
||||||
|
const mockAxiosInstance = {
|
||||||
|
request: vi.fn(),
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
fileDownloader = new FileDownloader(mockAxiosInstance);
|
||||||
|
|
||||||
|
mockAxiosInstance.request.mockResolvedValueOnce(mockResponse);
|
||||||
|
|
||||||
|
const result = await fileDownloader.download(url);
|
||||||
|
|
||||||
|
expect(result).toBeInstanceOf(Blob);
|
||||||
|
expect(result).toEqual(mockBlob);
|
||||||
|
expect(mockAxiosInstance.request).toHaveBeenCalledWith(url, {
|
||||||
|
method: 'GET',
|
||||||
|
responseType: 'blob',
|
||||||
|
responseReturn: 'body',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call post', async () => {
|
||||||
|
const url = 'https://example.com/file';
|
||||||
|
|
||||||
|
const mockAxiosInstance = {
|
||||||
|
post: vi.fn(),
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
fileDownloader = new FileDownloader(mockAxiosInstance);
|
||||||
|
|
||||||
|
const customConfig: AxiosRequestConfig = {
|
||||||
|
method: 'POST',
|
||||||
|
data: { name: 'aa' },
|
||||||
|
};
|
||||||
|
|
||||||
|
await fileDownloader.download(url, customConfig);
|
||||||
|
|
||||||
|
expect(mockAxiosInstance.post).toHaveBeenCalledWith(
|
||||||
|
url,
|
||||||
|
{ name: 'aa' },
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
responseType: 'blob',
|
||||||
|
responseReturn: 'body',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle errors gracefully', async () => {
|
||||||
|
const url = 'https://example.com/file';
|
||||||
|
const mockAxiosInstance = {
|
||||||
|
post: vi.fn(),
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
fileDownloader = new FileDownloader(mockAxiosInstance);
|
||||||
|
await expect(() =>
|
||||||
|
fileDownloader.download(url, { method: 'postt' }),
|
||||||
|
).rejects.toThrow(
|
||||||
|
'RequestClient does not support method "POSTT". Please ensure the method is properly implemented in your RequestClient instance.',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@ -28,13 +28,32 @@ class FileDownloader {
|
|||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const finalConfig: DownloadRequestConfig = {
|
const finalConfig: DownloadRequestConfig = {
|
||||||
responseReturn: 'body',
|
responseReturn: 'body',
|
||||||
|
method: 'GET',
|
||||||
...config,
|
...config,
|
||||||
responseType: 'blob',
|
responseType: 'blob',
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await this.client.get<T>(url, finalConfig);
|
// Prefer a generic request if available; otherwise, dispatch to method-specific calls.
|
||||||
|
const method = (finalConfig.method || 'GET').toUpperCase();
|
||||||
|
const clientAny = this.client as any;
|
||||||
|
|
||||||
return response;
|
if (typeof clientAny.request === 'function') {
|
||||||
|
return await clientAny.request(url, finalConfig);
|
||||||
|
}
|
||||||
|
const lower = method.toLowerCase();
|
||||||
|
|
||||||
|
if (typeof clientAny[lower] === 'function') {
|
||||||
|
if (['POST', 'PUT'].includes(method)) {
|
||||||
|
const { data, ...rest } = finalConfig as Record<string, any>;
|
||||||
|
return await clientAny[lower](url, data, rest);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await clientAny[lower](url, finalConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`RequestClient does not support method "${method}". Please ensure the method is properly implemented in your RequestClient instance.`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,29 +1,9 @@
|
|||||||
{
|
{
|
||||||
"folders": [
|
"folders": [
|
||||||
{
|
|
||||||
"name": "@vben/backend-mock",
|
|
||||||
"path": "apps/backend-mock",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "@vben/web-antd",
|
|
||||||
"path": "apps/web-antd",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "@vben/web-ele",
|
|
||||||
"path": "apps/web-ele",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "@vben/web-naive",
|
|
||||||
"path": "apps/web-naive",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "@vben/web-tdesign",
|
"name": "@vben/web-tdesign",
|
||||||
"path": "apps/web-tdesign",
|
"path": "apps/web-tdesign",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "@vben/docs",
|
|
||||||
"path": "docs",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "@vben/commitlint-config",
|
"name": "@vben/commitlint-config",
|
||||||
"path": "internal/lint-configs/commitlint-config",
|
"path": "internal/lint-configs/commitlint-config",
|
||||||
@ -160,10 +140,6 @@
|
|||||||
"name": "@vben/utils",
|
"name": "@vben/utils",
|
||||||
"path": "packages/utils",
|
"path": "packages/utils",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "@vben/playground",
|
|
||||||
"path": "playground",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "@vben/turbo-run",
|
"name": "@vben/turbo-run",
|
||||||
"path": "scripts/turbo-run",
|
"path": "scripts/turbo-run",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user