瀏覽代碼

Merge branch 'main' into main

Jin Mao 7 月之前
父節點
當前提交
f59e33682c
共有 62 個文件被更改,包括 1809 次插入25 次删除
  1. 9 0
      apps/web-antd/src/layouts/basic.vue
  2. 2 1
      apps/web-antd/src/locales/langs/en-US/page.json
  3. 2 1
      apps/web-antd/src/locales/langs/zh-CN/page.json
  4. 10 0
      apps/web-antd/src/router/routes/modules/vben.ts
  5. 65 0
      apps/web-antd/src/views/_core/profile/base-setting.vue
  6. 49 0
      apps/web-antd/src/views/_core/profile/index.vue
  7. 31 0
      apps/web-antd/src/views/_core/profile/notification-setting.vue
  8. 66 0
      apps/web-antd/src/views/_core/profile/password-setting.vue
  9. 43 0
      apps/web-antd/src/views/_core/profile/security-setting.vue
  10. 9 0
      apps/web-ele/src/layouts/basic.vue
  11. 2 1
      apps/web-ele/src/locales/langs/en-US/page.json
  12. 2 1
      apps/web-ele/src/locales/langs/zh-CN/page.json
  13. 10 0
      apps/web-ele/src/router/routes/modules/vben.ts
  14. 65 0
      apps/web-ele/src/views/_core/profile/base-setting.vue
  15. 49 0
      apps/web-ele/src/views/_core/profile/index.vue
  16. 31 0
      apps/web-ele/src/views/_core/profile/notification-setting.vue
  17. 66 0
      apps/web-ele/src/views/_core/profile/password-setting.vue
  18. 43 0
      apps/web-ele/src/views/_core/profile/security-setting.vue
  19. 9 0
      apps/web-naive/src/layouts/basic.vue
  20. 2 1
      apps/web-naive/src/locales/langs/en-US/page.json
  21. 2 1
      apps/web-naive/src/locales/langs/zh-CN/page.json
  22. 10 0
      apps/web-naive/src/router/routes/modules/vben.ts
  23. 65 0
      apps/web-naive/src/views/_core/profile/base-setting.vue
  24. 49 0
      apps/web-naive/src/views/_core/profile/index.vue
  25. 31 0
      apps/web-naive/src/views/_core/profile/notification-setting.vue
  26. 66 0
      apps/web-naive/src/views/_core/profile/password-setting.vue
  27. 43 0
      apps/web-naive/src/views/_core/profile/security-setting.vue
  28. 9 0
      apps/web-tdesign/src/layouts/basic.vue
  29. 2 1
      apps/web-tdesign/src/locales/langs/en-US/page.json
  30. 2 1
      apps/web-tdesign/src/locales/langs/zh-CN/page.json
  31. 10 0
      apps/web-tdesign/src/router/routes/modules/vben.ts
  32. 65 0
      apps/web-tdesign/src/views/_core/profile/base-setting.vue
  33. 49 0
      apps/web-tdesign/src/views/_core/profile/index.vue
  34. 31 0
      apps/web-tdesign/src/views/_core/profile/notification-setting.vue
  35. 66 0
      apps/web-tdesign/src/views/_core/profile/password-setting.vue
  36. 43 0
      apps/web-tdesign/src/views/_core/profile/security-setting.vue
  37. 1 0
      docs/src/components/common-ui/vben-form.md
  38. 2 2
      packages/@core/ui-kit/form-ui/src/components/form-actions.vue
  39. 1 0
      packages/@core/ui-kit/form-ui/src/form-api.ts
  40. 4 0
      packages/@core/ui-kit/form-ui/src/types.ts
  41. 1 1
      packages/@core/ui-kit/form-ui/src/use-form-context.ts
  42. 3 1
      packages/@core/ui-kit/form-ui/src/vben-form.vue
  43. 12 9
      packages/@core/ui-kit/form-ui/src/vben-use-form.vue
  44. 1 0
      packages/effects/common-ui/src/ui/index.ts
  45. 56 0
      packages/effects/common-ui/src/ui/profile/base-setting.vue
  46. 6 0
      packages/effects/common-ui/src/ui/profile/index.ts
  47. 53 0
      packages/effects/common-ui/src/ui/profile/notification-setting.vue
  48. 56 0
      packages/effects/common-ui/src/ui/profile/password-setting.vue
  49. 62 0
      packages/effects/common-ui/src/ui/profile/profile.vue
  50. 53 0
      packages/effects/common-ui/src/ui/profile/security-setting.vue
  51. 21 0
      packages/effects/common-ui/src/ui/profile/types.ts
  52. 71 0
      packages/effects/request/src/request-client/modules/downloader.test.ts
  53. 21 2
      packages/effects/request/src/request-client/modules/downloader.ts
  54. 9 0
      playground/src/layouts/basic.vue
  55. 2 1
      playground/src/locales/langs/en-US/page.json
  56. 2 1
      playground/src/locales/langs/zh-CN/page.json
  57. 10 0
      playground/src/router/routes/modules/vben.ts
  58. 65 0
      playground/src/views/_core/profile/base-setting.vue
  59. 49 0
      playground/src/views/_core/profile/index.vue
  60. 31 0
      playground/src/views/_core/profile/notification-setting.vue
  61. 66 0
      playground/src/views/_core/profile/password-setting.vue
  62. 43 0
      playground/src/views/_core/profile/security-setting.vue

+ 9 - 0
apps/web-antd/src/layouts/basic.vue

@@ -2,6 +2,7 @@
 import type { NotificationItem } from '@vben/layouts';
 
 import { computed, ref, watch } from 'vue';
+import { useRouter } from 'vue-router';
 
 import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
 import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
@@ -74,6 +75,7 @@ const notifications = ref<NotificationItem[]>([
   },
 ]);
 
+const router = useRouter();
 const userStore = useUserStore();
 const authStore = useAuthStore();
 const accessStore = useAccessStore();
@@ -83,6 +85,13 @@ const showDot = computed(() =>
 );
 
 const menus = computed(() => [
+  {
+    handler: () => {
+      router.push({ name: 'Profile' });
+    },
+    icon: 'lucide:user',
+    text: $t('page.auth.profile'),
+  },
   {
     handler: () => {
       openWindow(VBEN_DOC_URL, {

+ 2 - 1
apps/web-antd/src/locales/langs/en-US/page.json

@@ -4,7 +4,8 @@
     "register": "Register",
     "codeLogin": "Code Login",
     "qrcodeLogin": "Qr Code Login",
-    "forgetPassword": "Forget Password"
+    "forgetPassword": "Forget Password",
+    "profile": "Profile"
   },
   "dashboard": {
     "title": "Dashboard",

+ 2 - 1
apps/web-antd/src/locales/langs/zh-CN/page.json

@@ -4,7 +4,8 @@
     "register": "注册",
     "codeLogin": "验证码登录",
     "qrcodeLogin": "二维码登录",
-    "forgetPassword": "忘记密码"
+    "forgetPassword": "忘记密码",
+    "profile": "个人中心"
   },
   "dashboard": {
     "title": "概览",

+ 10 - 0
apps/web-antd/src/router/routes/modules/vben.ts

@@ -89,6 +89,16 @@ const routes: RouteRecordRaw[] = [
       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;

+ 65 - 0
apps/web-antd/src/views/_core/profile/base-setting.vue

@@ -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 - 0
apps/web-antd/src/views/_core/profile/index.vue

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

+ 31 - 0
apps/web-antd/src/views/_core/profile/notification-setting.vue

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

+ 66 - 0
apps/web-antd/src/views/_core/profile/password-setting.vue

@@ -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 'ant-design-vue';
+
+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>

+ 43 - 0
apps/web-antd/src/views/_core/profile/security-setting.vue

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

+ 9 - 0
apps/web-ele/src/layouts/basic.vue

@@ -2,6 +2,7 @@
 import type { NotificationItem } from '@vben/layouts';
 
 import { computed, ref, watch } from 'vue';
+import { useRouter } from 'vue-router';
 
 import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
 import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
@@ -74,6 +75,7 @@ const notifications = ref<NotificationItem[]>([
   },
 ]);
 
+const router = useRouter();
 const userStore = useUserStore();
 const authStore = useAuthStore();
 const accessStore = useAccessStore();
@@ -83,6 +85,13 @@ const showDot = computed(() =>
 );
 
 const menus = computed(() => [
+  {
+    handler: () => {
+      router.push({ name: 'Profile' });
+    },
+    icon: 'lucide:user',
+    text: $t('page.auth.profile'),
+  },
   {
     handler: () => {
       openWindow(VBEN_DOC_URL, {

+ 2 - 1
apps/web-ele/src/locales/langs/en-US/page.json

@@ -4,7 +4,8 @@
     "register": "Register",
     "codeLogin": "Code Login",
     "qrcodeLogin": "Qr Code Login",
-    "forgetPassword": "Forget Password"
+    "forgetPassword": "Forget Password",
+    "profile": "Profile"
   },
   "dashboard": {
     "title": "Dashboard",

+ 2 - 1
apps/web-ele/src/locales/langs/zh-CN/page.json

@@ -4,7 +4,8 @@
     "register": "注册",
     "codeLogin": "验证码登录",
     "qrcodeLogin": "二维码登录",
-    "forgetPassword": "忘记密码"
+    "forgetPassword": "忘记密码",
+    "profile": "个人中心"
   },
   "dashboard": {
     "title": "概览",

+ 10 - 0
apps/web-ele/src/router/routes/modules/vben.ts

@@ -89,6 +89,16 @@ const routes: RouteRecordRaw[] = [
       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;

+ 65 - 0
apps/web-ele/src/views/_core/profile/base-setting.vue

@@ -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 - 0
apps/web-ele/src/views/_core/profile/index.vue

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

+ 31 - 0
apps/web-ele/src/views/_core/profile/notification-setting.vue

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

+ 66 - 0
apps/web-ele/src/views/_core/profile/password-setting.vue

@@ -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 { ElMessage } from 'element-plus';
+
+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() {
+  ElMessage.success('密码修改成功');
+}
+</script>
+<template>
+  <ProfilePasswordSetting
+    ref="profilePasswordSettingRef"
+    class="w-1/3"
+    :form-schema="formSchema"
+    @submit="handleSubmit"
+  />
+</template>

+ 43 - 0
apps/web-ele/src/views/_core/profile/security-setting.vue

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

+ 9 - 0
apps/web-naive/src/layouts/basic.vue

@@ -2,6 +2,7 @@
 import type { NotificationItem } from '@vben/layouts';
 
 import { computed, ref, watch } from 'vue';
+import { useRouter } from 'vue-router';
 
 import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
 import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
@@ -74,6 +75,7 @@ const notifications = ref<NotificationItem[]>([
   },
 ]);
 
+const router = useRouter();
 const userStore = useUserStore();
 const authStore = useAuthStore();
 const accessStore = useAccessStore();
@@ -83,6 +85,13 @@ const showDot = computed(() =>
 );
 
 const menus = computed(() => [
+  {
+    handler: () => {
+      router.push({ name: 'Profile' });
+    },
+    icon: 'lucide:user',
+    text: $t('page.auth.profile'),
+  },
   {
     handler: () => {
       openWindow(VBEN_DOC_URL, {

+ 2 - 1
apps/web-naive/src/locales/langs/en-US/page.json

@@ -4,7 +4,8 @@
     "register": "Register",
     "codeLogin": "Code Login",
     "qrcodeLogin": "Qr Code Login",
-    "forgetPassword": "Forget Password"
+    "forgetPassword": "Forget Password",
+    "profile": "Profile"
   },
   "dashboard": {
     "title": "Dashboard",

+ 2 - 1
apps/web-naive/src/locales/langs/zh-CN/page.json

@@ -4,7 +4,8 @@
     "register": "注册",
     "codeLogin": "验证码登录",
     "qrcodeLogin": "二维码登录",
-    "forgetPassword": "忘记密码"
+    "forgetPassword": "忘记密码",
+    "profile": "个人中心"
   },
   "dashboard": {
     "title": "概览",

+ 10 - 0
apps/web-naive/src/router/routes/modules/vben.ts

@@ -89,6 +89,16 @@ const routes: RouteRecordRaw[] = [
       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;

+ 65 - 0
apps/web-naive/src/views/_core/profile/base-setting.vue

@@ -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 - 0
apps/web-naive/src/views/_core/profile/index.vue

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

+ 31 - 0
apps/web-naive/src/views/_core/profile/notification-setting.vue

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

+ 66 - 0
apps/web-naive/src/views/_core/profile/password-setting.vue

@@ -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/naive';
+
+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>

+ 43 - 0
apps/web-naive/src/views/_core/profile/security-setting.vue

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

+ 9 - 0
apps/web-tdesign/src/layouts/basic.vue

@@ -2,6 +2,7 @@
 import type { NotificationItem } from '@vben/layouts';
 
 import { computed, ref, watch } from 'vue';
+import { useRouter } from 'vue-router';
 
 import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
 import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
@@ -74,6 +75,7 @@ const notifications = ref<NotificationItem[]>([
   },
 ]);
 
+const router = useRouter();
 const userStore = useUserStore();
 const authStore = useAuthStore();
 const accessStore = useAccessStore();
@@ -83,6 +85,13 @@ const showDot = computed(() =>
 );
 
 const menus = computed(() => [
+  {
+    handler: () => {
+      router.push({ name: 'Profile' });
+    },
+    icon: 'lucide:user',
+    text: $t('page.auth.profile'),
+  },
   {
     handler: () => {
       openWindow(VBEN_DOC_URL, {

+ 2 - 1
apps/web-tdesign/src/locales/langs/en-US/page.json

@@ -4,7 +4,8 @@
     "register": "Register",
     "codeLogin": "Code Login",
     "qrcodeLogin": "Qr Code Login",
-    "forgetPassword": "Forget Password"
+    "forgetPassword": "Forget Password",
+    "profile": "Profile"
   },
   "dashboard": {
     "title": "Dashboard",

+ 2 - 1
apps/web-tdesign/src/locales/langs/zh-CN/page.json

@@ -4,7 +4,8 @@
     "register": "注册",
     "codeLogin": "验证码登录",
     "qrcodeLogin": "二维码登录",
-    "forgetPassword": "忘记密码"
+    "forgetPassword": "忘记密码",
+    "profile": "个人中心"
   },
   "dashboard": {
     "title": "概览",

+ 10 - 0
apps/web-tdesign/src/router/routes/modules/vben.ts

@@ -89,6 +89,16 @@ const routes: RouteRecordRaw[] = [
       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;

+ 65 - 0
apps/web-tdesign/src/views/_core/profile/base-setting.vue

@@ -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 - 0
apps/web-tdesign/src/views/_core/profile/index.vue

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

+ 31 - 0
apps/web-tdesign/src/views/_core/profile/notification-setting.vue

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

+ 66 - 0
apps/web-tdesign/src/views/_core/profile/password-setting.vue

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

+ 43 - 0
apps/web-tdesign/src/views/_core/profile/security-setting.vue

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

+ 1 - 0
docs/src/components/common-ui/vben-form.md

@@ -335,6 +335,7 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单
 | handleReset | 表单重置回调 | `(values: Record<string, any>,) => Promise<void> \| void` | - |
 | handleSubmit | 表单提交回调 | `(values: Record<string, any>,) => Promise<void> \| void` | - |
 | handleValuesChange | 表单值变化回调 | `(values: Record<string, any>, fieldsChanged: string[]) => void` | - |
+| handleCollapsedChange | 表单收起展开状态变化回调 | `(collapsed: boolean) => void` | - |
 | actionButtonsReverse | 调换操作按钮位置 | `boolean` | `false` |
 | resetButtonOptions | 重置按钮组件参数 | `ActionButtonOptions` | - |
 | submitButtonOptions | 提交按钮组件参数 | `ActionButtonOptions` | - |

+ 2 - 2
packages/@core/ui-kit/form-ui/src/components/form-actions.vue

@@ -47,7 +47,7 @@ async function handleSubmit(e: Event) {
     return;
   }
 
-  const values = toRaw(await props.formApi.getValues());
+  const values = toRaw(await props.formApi.getValues()) ?? {};
   await props.handleSubmit?.(values);
 }
 
@@ -56,7 +56,7 @@ async function handleReset(e: Event) {
   e?.stopPropagation();
   const props = unref(rootProps);
 
-  const values = toRaw(await props.formApi?.getValues());
+  const values = toRaw(await props.formApi?.getValues()) ?? {};
 
   if (isFunction(props.handleReset)) {
     await props.handleReset?.(values);

+ 1 - 0
packages/@core/ui-kit/form-ui/src/form-api.ts

@@ -36,6 +36,7 @@ function getDefaultState(): VbenFormProps {
     handleReset: undefined,
     handleSubmit: undefined,
     handleValuesChange: undefined,
+    handleCollapsedChange: undefined,
     layout: 'horizontal',
     resetButtonOptions: {},
     schema: [],

+ 4 - 0
packages/@core/ui-kit/form-ui/src/types.ts

@@ -379,6 +379,10 @@ export interface VbenFormProps<
    * 表单字段映射
    */
   fieldMappingTime?: FieldMappingTime;
+  /**
+   * 表单收起展开状态变化回调
+   */
+  handleCollapsedChange?: (collapsed: boolean) => void;
   /**
    * 表单重置回调
    */

+ 1 - 1
packages/@core/ui-kit/form-ui/src/use-form-context.ts

@@ -13,7 +13,7 @@ import { useForm } from 'vee-validate';
 import { object, ZodIntersection, ZodNumber, ZodObject, ZodString } from 'zod';
 import { getDefaultsForSchema } from 'zod-defaults';
 
-type ExtendFormProps = VbenFormProps & { formApi: ExtendedFormApi };
+type ExtendFormProps = VbenFormProps & { formApi?: ExtendedFormApi };
 
 export const [injectFormProps, provideFormProps] =
   createContext<[ComputedRef<ExtendFormProps> | ExtendFormProps, FormActions]>(

+ 3 - 1
packages/@core/ui-kit/form-ui/src/vben-form.vue

@@ -40,7 +40,9 @@ const { delegatedSlots, form } = useFormInitial(props);
 provideFormProps([props, form]);
 
 const handleUpdateCollapsed = (value: boolean) => {
-  currentCollapsed.value = !!value;
+  currentCollapsed.value = value;
+  // 触发收起展开状态变化回调
+  props.handleCollapsedChange?.(value);
 };
 
 watchEffect(() => {

+ 12 - 9
packages/@core/ui-kit/form-ui/src/vben-use-form.vue

@@ -25,7 +25,7 @@ import {
 } from './use-form-context';
 // 通过 extends 会导致热更新卡死,所以重复写了一遍
 interface Props extends VbenFormProps {
-  formApi: ExtendedFormApi;
+  formApi?: ExtendedFormApi;
 }
 
 const props = defineProps<Props>();
@@ -44,11 +44,13 @@ provideComponentRefMap(componentRefMap);
 props.formApi?.mount?.(form, componentRefMap);
 
 const handleUpdateCollapsed = (value: boolean) => {
-  props.formApi?.setState({ collapsed: !!value });
+  props.formApi?.setState({ collapsed: value });
+  // 触发收起展开状态变化回调
+  forward.value.handleCollapsedChange?.(value);
 };
 
 function handleKeyDownEnter(event: KeyboardEvent) {
-  if (!state.value.submitOnEnter || !forward.value.formApi?.isMounted) {
+  if (!state?.value.submitOnEnter || !forward.value.formApi?.isMounted) {
     return;
   }
   // 如果是 textarea 不阻止默认行为,否则会导致无法换行。
@@ -58,11 +60,11 @@ function handleKeyDownEnter(event: KeyboardEvent) {
   }
   event.preventDefault();
 
-  forward.value.formApi.validateAndSubmitForm();
+  forward.value.formApi?.validateAndSubmitForm();
 }
 
 const handleValuesChangeDebounced = useDebounceFn(async () => {
-  state.value.submitOnChange && forward.value.formApi?.validateAndSubmitForm();
+  state?.value.submitOnChange && forward.value.formApi?.validateAndSubmitForm();
 }, 300);
 
 const valuesCache: Recordable<any> = {};
@@ -74,7 +76,7 @@ onMounted(async () => {
     () => form.values,
     async (newVal) => {
       if (forward.value.handleValuesChange) {
-        const fields = state.value.schema?.map((item) => {
+        const fields = state?.value.schema?.map((item) => {
           return item.fieldName;
         });
 
@@ -91,8 +93,9 @@ onMounted(async () => {
 
           if (changedFields.length > 0) {
             // 调用handleValuesChange回调,传入所有表单值的深拷贝和变更的字段列表
+            const values = await forward.value.formApi?.getValues();
             forward.value.handleValuesChange(
-              cloneDeep(await forward.value.formApi.getValues()),
+              cloneDeep(values ?? {}) as Record<string, any>,
               changedFields,
             );
           }
@@ -109,7 +112,7 @@ onMounted(async () => {
   <Form
     @keydown.enter="handleKeyDownEnter"
     v-bind="forward"
-    :collapsed="state.collapsed"
+    :collapsed="state?.collapsed"
     :component-bind-event-map="COMPONENT_BIND_EVENT_MAP"
     :component-map="COMPONENT_MAP"
     :form="form"
@@ -126,7 +129,7 @@ onMounted(async () => {
       <slot v-bind="slotProps">
         <FormActions
           v-if="forward.showDefaultActions"
-          :model-value="state.collapsed"
+          :model-value="state?.collapsed"
           @update:model-value="handleUpdateCollapsed"
         >
           <template #reset-before="resetSlotProps">

+ 1 - 0
packages/effects/common-ui/src/ui/index.ts

@@ -2,3 +2,4 @@ export * from './about';
 export * from './authentication';
 export * from './dashboard';
 export * from './fallback';
+export * from './profile';

+ 56 - 0
packages/effects/common-ui/src/ui/profile/base-setting.vue

@@ -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 - 0
packages/effects/common-ui/src/ui/profile/index.ts

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

+ 53 - 0
packages/effects/common-ui/src/ui/profile/notification-setting.vue

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

+ 56 - 0
packages/effects/common-ui/src/ui/profile/password-setting.vue

@@ -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 - 0
packages/effects/common-ui/src/ui/profile/profile.vue

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

+ 53 - 0
packages/effects/common-ui/src/ui/profile/security-setting.vue

@@ -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 - 0
packages/effects/common-ui/src/ui/profile/types.ts

@@ -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[];
+}

+ 71 - 0
packages/effects/request/src/request-client/modules/downloader.test.ts

@@ -30,6 +30,7 @@ describe('fileDownloader', () => {
     expect(result).toBeInstanceOf(Blob);
     expect(result).toEqual(mockBlob);
     expect(mockAxiosInstance.get).toHaveBeenCalledWith(url, {
+      method: 'GET',
       responseType: 'blob',
       responseReturn: 'body',
     });
@@ -51,6 +52,7 @@ describe('fileDownloader', () => {
     expect(result).toEqual(mockBlob);
     expect(mockAxiosInstance.get).toHaveBeenCalledWith(url, {
       ...customConfig,
+      method: 'GET',
       responseType: 'blob',
       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.',
+    );
+  });
+});

+ 21 - 2
packages/effects/request/src/request-client/modules/downloader.ts

@@ -28,13 +28,32 @@ class FileDownloader {
   ): Promise<T> {
     const finalConfig: DownloadRequestConfig = {
       responseReturn: 'body',
+      method: 'GET',
       ...config,
       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.`,
+    );
   }
 }
 

+ 9 - 0
playground/src/layouts/basic.vue

@@ -2,6 +2,7 @@
 import type { NotificationItem } from '@vben/layouts';
 
 import { computed, onBeforeMount, ref, watch } from 'vue';
+import { useRouter } from 'vue-router';
 
 import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
 import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
@@ -87,6 +88,7 @@ const notifications = ref<NotificationItem[]>([
   },
 ]);
 
+const router = useRouter();
 const userStore = useUserStore();
 const authStore = useAuthStore();
 const accessStore = useAccessStore();
@@ -96,6 +98,13 @@ const showDot = computed(() =>
 );
 
 const menus = computed(() => [
+  {
+    handler: () => {
+      router.push({ name: 'Profile' });
+    },
+    icon: 'lucide:user',
+    text: $t('page.auth.profile'),
+  },
   {
     handler: () => {
       openWindow(VBEN_DOC_URL, {

+ 2 - 1
playground/src/locales/langs/en-US/page.json

@@ -6,7 +6,8 @@
     "qrcodeLogin": "Qr Code Login",
     "forgetPassword": "Forget Password",
     "sendingCode": "SMS Code is sending...",
-    "codeSentTo": "Code has been sent to {0}"
+    "codeSentTo": "Code has been sent to {0}",
+    "profile": "Profile"
   },
   "dashboard": {
     "title": "Dashboard",

+ 2 - 1
playground/src/locales/langs/zh-CN/page.json

@@ -6,7 +6,8 @@
     "qrcodeLogin": "二维码登录",
     "forgetPassword": "忘记密码",
     "sendingCode": "正在发送验证码",
-    "codeSentTo": "验证码已发送至{0}"
+    "codeSentTo": "验证码已发送至{0}",
+    "profile": "个人中心"
   },
   "dashboard": {
     "title": "概览",

+ 10 - 0
playground/src/router/routes/modules/vben.ts

@@ -101,6 +101,16 @@ const routes: RouteRecordRaw[] = [
     name: 'VbenAbout',
     path: '/vben-admin/about',
   },
+  {
+    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;

+ 65 - 0
playground/src/views/_core/profile/base-setting.vue

@@ -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 - 0
playground/src/views/_core/profile/index.vue

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

+ 31 - 0
playground/src/views/_core/profile/notification-setting.vue

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

+ 66 - 0
playground/src/views/_core/profile/password-setting.vue

@@ -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 'ant-design-vue';
+
+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>

+ 43 - 0
playground/src/views/_core/profile/security-setting.vue

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