diff --git a/apps/web-tdesign/src/layouts/basic.vue b/apps/web-tdesign/src/layouts/basic.vue index 805b8a73..2226c68a 100644 --- a/apps/web-tdesign/src/layouts/basic.vue +++ b/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'; @@ -23,6 +24,7 @@ import LoginForm from '#/views/_core/authentication/login.vue'; const notifications = ref([ { + id: 1, avatar: 'https://avatar.vercel.sh/vercel.svg?text=VB', date: '3小时前', isRead: true, @@ -30,6 +32,7 @@ const notifications = ref([ title: '收到了 14 份新周报', }, { + id: 2, avatar: 'https://avatar.vercel.sh/1', date: '刚刚', isRead: false, @@ -37,6 +40,7 @@ const notifications = ref([ title: '朱偏右 回复了你', }, { + id: 3, avatar: 'https://avatar.vercel.sh/1', date: '2024-01-01', isRead: false, @@ -44,14 +48,34 @@ const notifications = ref([ title: '曲丽丽 评论了你', }, { + id: 4, avatar: 'https://avatar.vercel.sh/satori', date: '1天前', isRead: false, message: '描述信息描述信息描述信息', 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 authStore = useAuthStore(); const accessStore = useAccessStore(); @@ -61,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, { @@ -102,6 +133,17 @@ function handleNoticeClear() { 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() { notifications.value.forEach((item) => (item.isRead = true)); } @@ -144,6 +186,8 @@ watch( :dot="showDot" :notifications="notifications" @clear="handleNoticeClear" + @read="(item) => item.id && markRead(item.id)" + @remove="(item) => item.id && remove(item.id)" @make-all="handleMakeAll" /> diff --git a/apps/web-tdesign/src/locales/langs/en-US/page.json b/apps/web-tdesign/src/locales/langs/en-US/page.json index 618a258c..39f1641c 100644 --- a/apps/web-tdesign/src/locales/langs/en-US/page.json +++ b/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", diff --git a/apps/web-tdesign/src/locales/langs/zh-CN/page.json b/apps/web-tdesign/src/locales/langs/zh-CN/page.json index 4cb67081..2192d1d5 100644 --- a/apps/web-tdesign/src/locales/langs/zh-CN/page.json +++ b/apps/web-tdesign/src/locales/langs/zh-CN/page.json @@ -4,7 +4,8 @@ "register": "注册", "codeLogin": "验证码登录", "qrcodeLogin": "二维码登录", - "forgetPassword": "忘记密码" + "forgetPassword": "忘记密码", + "profile": "个人中心" }, "dashboard": { "title": "概览", diff --git a/apps/web-tdesign/src/router/routes/modules/vben.ts b/apps/web-tdesign/src/router/routes/modules/vben.ts index b1a3537c..a5f0aa4d 100644 --- a/apps/web-tdesign/src/router/routes/modules/vben.ts +++ b/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; diff --git a/apps/web-tdesign/src/views/_core/profile/base-setting.vue b/apps/web-tdesign/src/views/_core/profile/base-setting.vue new file mode 100644 index 00000000..aa8a4c26 --- /dev/null +++ b/apps/web-tdesign/src/views/_core/profile/base-setting.vue @@ -0,0 +1,65 @@ + + diff --git a/apps/web-tdesign/src/views/_core/profile/index.vue b/apps/web-tdesign/src/views/_core/profile/index.vue new file mode 100644 index 00000000..8740894e --- /dev/null +++ b/apps/web-tdesign/src/views/_core/profile/index.vue @@ -0,0 +1,49 @@ + + diff --git a/apps/web-tdesign/src/views/_core/profile/notification-setting.vue b/apps/web-tdesign/src/views/_core/profile/notification-setting.vue new file mode 100644 index 00000000..324a4b39 --- /dev/null +++ b/apps/web-tdesign/src/views/_core/profile/notification-setting.vue @@ -0,0 +1,31 @@ + + diff --git a/apps/web-tdesign/src/views/_core/profile/password-setting.vue b/apps/web-tdesign/src/views/_core/profile/password-setting.vue new file mode 100644 index 00000000..7e1c0f08 --- /dev/null +++ b/apps/web-tdesign/src/views/_core/profile/password-setting.vue @@ -0,0 +1,66 @@ + + diff --git a/apps/web-tdesign/src/views/_core/profile/security-setting.vue b/apps/web-tdesign/src/views/_core/profile/security-setting.vue new file mode 100644 index 00000000..be30db58 --- /dev/null +++ b/apps/web-tdesign/src/views/_core/profile/security-setting.vue @@ -0,0 +1,43 @@ + + diff --git a/packages/@core/ui-kit/form-ui/src/components/form-actions.vue b/packages/@core/ui-kit/form-ui/src/components/form-actions.vue index 42101c12..36b1caa0 100644 --- a/packages/@core/ui-kit/form-ui/src/components/form-actions.vue +++ b/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); diff --git a/packages/@core/ui-kit/form-ui/src/form-api.ts b/packages/@core/ui-kit/form-ui/src/form-api.ts index 401748c3..f5f353dc 100644 --- a/packages/@core/ui-kit/form-ui/src/form-api.ts +++ b/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: [], diff --git a/packages/@core/ui-kit/form-ui/src/types.ts b/packages/@core/ui-kit/form-ui/src/types.ts index 824b7a44..6d704145 100644 --- a/packages/@core/ui-kit/form-ui/src/types.ts +++ b/packages/@core/ui-kit/form-ui/src/types.ts @@ -379,6 +379,10 @@ export interface VbenFormProps< * 表单字段映射 */ fieldMappingTime?: FieldMappingTime; + /** + * 表单收起展开状态变化回调 + */ + handleCollapsedChange?: (collapsed: boolean) => void; /** * 表单重置回调 */ diff --git a/packages/@core/ui-kit/form-ui/src/use-form-context.ts b/packages/@core/ui-kit/form-ui/src/use-form-context.ts index 4ef182ed..e95c87b9 100644 --- a/packages/@core/ui-kit/form-ui/src/use-form-context.ts +++ b/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, FormActions]>( diff --git a/packages/@core/ui-kit/form-ui/src/vben-form.vue b/packages/@core/ui-kit/form-ui/src/vben-form.vue index 260e75cd..f3ba37f0 100644 --- a/packages/@core/ui-kit/form-ui/src/vben-form.vue +++ b/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(() => { diff --git a/packages/@core/ui-kit/form-ui/src/vben-use-form.vue b/packages/@core/ui-kit/form-ui/src/vben-use-form.vue index 3e7b00b6..e9386725 100644 --- a/packages/@core/ui-kit/form-ui/src/vben-use-form.vue +++ b/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(); @@ -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 = {}; @@ -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, changedFields, ); } @@ -109,7 +112,7 @@ onMounted(async () => {
{