Ver Fonte

feat: increase support for multiple time zones

 * 优化实现方法
zhongming4762 há 7 meses atrás
pai
commit
4d713db546
31 ficheiros alterados com 272 adições e 422 exclusões
  1. 0 0
      apps/backend-mock/api/timezone/getTimezone.ts
  2. 0 0
      apps/backend-mock/api/timezone/getTimezoneOptions.ts
  3. 1 1
      apps/backend-mock/api/timezone/setTimezone.ts
  4. 2 2
      apps/backend-mock/utils/mock-data.ts
  5. 0 1
      apps/web-antd/src/api/core/index.ts
  6. 0 23
      apps/web-antd/src/api/core/user-profile.ts
  7. 2 41
      apps/web-antd/src/layouts/basic.vue
  8. 0 1
      apps/web-antd/src/store/index.ts
  9. 0 58
      apps/web-antd/src/store/user-profile.ts
  10. 0 1
      apps/web-ele/src/api/core/index.ts
  11. 0 23
      apps/web-ele/src/api/core/user-profile.ts
  12. 2 41
      apps/web-ele/src/layouts/basic.vue
  13. 0 1
      apps/web-ele/src/store/index.ts
  14. 0 58
      apps/web-ele/src/store/user-profile.ts
  15. 0 1
      apps/web-naive/src/api/core/index.ts
  16. 0 23
      apps/web-naive/src/api/core/user-profile.ts
  17. 2 42
      apps/web-naive/src/layouts/basic.vue
  18. 0 1
      apps/web-naive/src/store/index.ts
  19. 0 58
      apps/web-naive/src/store/user-profile.ts
  20. 10 0
      packages/@core/base/typings/src/app.d.ts
  21. 0 1
      packages/@core/base/typings/src/index.ts
  22. 0 9
      packages/@core/base/typings/src/user-profile.d.ts
  23. 33 2
      packages/@core/preferences/src/constants.ts
  24. 4 0
      packages/effects/layouts/src/basic/header/header.vue
  25. 35 34
      packages/effects/layouts/src/widgets/timezone/timezone-button.vue
  26. 1 0
      packages/stores/src/modules/index.ts
  27. 125 0
      packages/stores/src/modules/timezone.ts
  28. 1 0
      playground/src/api/core/index.ts
  29. 30 0
      playground/src/api/core/timezone.ts
  30. 4 0
      playground/src/bootstrap.ts
  31. 20 0
      playground/src/timezone-init.ts

+ 0 - 0
apps/backend-mock/api/user/timezone.ts → apps/backend-mock/api/timezone/getTimezone.ts


+ 0 - 0
apps/backend-mock/api/profile/timezone.ts → apps/backend-mock/api/timezone/getTimezoneOptions.ts


+ 1 - 1
apps/backend-mock/api/user/setTimezone.ts → apps/backend-mock/api/timezone/setTimezone.ts

@@ -10,5 +10,5 @@ export default eventHandler(async (event) => {
   }
   const { timezone } = await readBody(event);
   setTimezone(timezone);
-  return useResponseSuccess();
+  return useResponseSuccess({});
 });

+ 2 - 2
apps/backend-mock/utils/mock-data.ts

@@ -7,7 +7,7 @@ export interface UserInfo {
   homePath?: string;
 }
 
-export interface TimeZoneOption {
+export interface TimezoneOption {
   offset: number;
   timeZone: string;
 }
@@ -397,7 +397,7 @@ export function getMenuIds(menus: any[]) {
 /**
  * 时区选项
  */
-export const TIME_ZONE_OPTIONS: TimeZoneOption[] = [
+export const TIME_ZONE_OPTIONS: TimezoneOption[] = [
   {
     offset: -5,
     timezone: 'America/New_York',

+ 0 - 1
apps/web-antd/src/api/core/index.ts

@@ -1,4 +1,3 @@
 export * from './auth';
 export * from './menu';
 export * from './user';
-export * from './user-profile';

+ 0 - 23
apps/web-antd/src/api/core/user-profile.ts

@@ -1,23 +0,0 @@
-import type { TimezoneOption } from '@vben/types';
-
-import { requestClient } from '#/api/request';
-
-/**
- * 获取系统支持的时区列表
- */
-export async function getTimezoneOptionsApi() {
-  return requestClient.get<TimezoneOption[]>('/profile/timezone');
-}
-/**
- * 获取用户时区
- */
-export async function getUserTimezoneApi(): Promise<null | string | undefined> {
-  return requestClient.get<null | string | undefined>('/user/timezone');
-}
-/**
- * 设置用户时区
- * @param timezone 时区
- */
-export async function setUserTimezoneApi(timezone: string) {
-  return requestClient.post('/user/setTimezone', { timezone });
-}

+ 2 - 41
apps/web-antd/src/layouts/basic.vue

@@ -1,8 +1,7 @@
 <script lang="ts" setup>
-import type { ExtendedModalApi } from '@vben/common-ui';
 import type { NotificationItem } from '@vben/layouts';
 
-import { computed, onMounted, ref, unref, watch } from 'vue';
+import { computed, ref, watch } from 'vue';
 
 import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
 import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
@@ -12,18 +11,14 @@ import {
   BasicLayout,
   LockScreen,
   Notification,
-  TimezoneButton,
   UserDropdown,
 } from '@vben/layouts';
 import { preferences } from '@vben/preferences';
 import { useAccessStore, useUserStore } from '@vben/stores';
 import { openWindow } from '@vben/utils';
 
-import { message } from 'ant-design-vue';
-
-import { getTimezoneOptionsApi } from '#/api';
 import { $t } from '#/locales';
-import { useAuthStore, useUserProfileStore } from '#/store';
+import { useAuthStore } from '#/store';
 import LoginForm from '#/views/_core/authentication/login.vue';
 
 const notifications = ref<NotificationItem[]>([
@@ -65,32 +60,6 @@ const showDot = computed(() =>
   notifications.value.some((item) => !item.isRead),
 );
 
-const userProfileStore = useUserProfileStore();
-const computedTimezone = computed(() => unref(userProfileStore.timezone));
-
-const timezoneOptions = ref<string[]>([]);
-onMounted(async () => {
-  timezoneOptions.value = ((await getTimezoneOptionsApi()) || []).map(
-    (item) => item.timezone,
-  );
-});
-const handleSetTimezone = async (
-  modalApi: ExtendedModalApi,
-  timezone?: string,
-) => {
-  if (!timezone) {
-    return;
-  }
-  try {
-    modalApi.setState({ confirmLoading: true });
-    await userProfileStore.setTimezone(timezone);
-    message.success($t('ui.widgets.timezone.setSuccess'));
-    modalApi.close();
-  } finally {
-    modalApi.setState({ confirmLoading: false });
-  }
-};
-
 const menus = computed(() => [
   {
     handler: () => {
@@ -178,14 +147,6 @@ watch(
         @make-all="handleMakeAll"
       />
     </template>
-    <template #timezone>
-      <TimezoneButton
-        :ok-handler="handleSetTimezone"
-        :timezone="computedTimezone"
-        :timezone-options="timezoneOptions"
-        name="out"
-      />
-    </template>
     <template #extra>
       <AuthenticationLoginExpiredModal
         v-model:open="accessStore.loginExpired"

+ 0 - 1
apps/web-antd/src/store/index.ts

@@ -1,2 +1 @@
 export * from './auth';
-export * from './user-profile';

+ 0 - 58
apps/web-antd/src/store/user-profile.ts

@@ -1,58 +0,0 @@
-import { ref } from 'vue';
-
-import { getTimezone, setDefaultTimezone } from '@vben/utils';
-
-import { acceptHMRUpdate, defineStore } from 'pinia';
-
-import { getUserTimezoneApi, setUserTimezoneApi } from '#/api';
-
-const useUserProfileStore = defineStore('user-profile', () => {
-  const timezoneRef = ref(
-    getTimezone() || new Intl.DateTimeFormat().resolvedOptions().timeZone,
-  );
-
-  /**
-   * 设置用户时区
-   * Set the user's timezone
-   * @param timezone 时区字符串
-   */
-  async function setTimezone(timezone: string) {
-    // 保存用户的时区设置
-    await setUserTimezoneApi(timezone);
-    timezoneRef.value = timezone;
-    // 设置dayjs默认时区
-    setDefaultTimezone(timezone);
-  }
-
-  /**
-   * 初始化用户时区
-   * Initialize the user's timezone
-   */
-  async function initTimezone() {
-    // 从服务器获取用户时区
-    const timezone = await getUserTimezoneApi();
-    if (timezone) {
-      timezoneRef.value = timezone;
-      // 设置dayjs默认时区
-      setDefaultTimezone(timezone);
-    }
-  }
-
-  initTimezone().catch((error) => {
-    console.error('Failed to initialize timezone during store setup:', error);
-  });
-
-  return {
-    timezone: timezoneRef,
-    setTimezone,
-    initTimezone,
-  };
-});
-
-export { useUserProfileStore };
-
-// 解决热更新问题
-const hot = import.meta.hot;
-if (hot) {
-  hot.accept(acceptHMRUpdate(useUserProfileStore, hot));
-}

+ 0 - 1
apps/web-ele/src/api/core/index.ts

@@ -1,4 +1,3 @@
 export * from './auth';
 export * from './menu';
 export * from './user';
-export * from './user-profile';

+ 0 - 23
apps/web-ele/src/api/core/user-profile.ts

@@ -1,23 +0,0 @@
-import type { TimezoneOption } from '@vben/types';
-
-import { requestClient } from '#/api/request';
-
-/**
- * 获取系统支持的时区列表
- */
-export async function getTimezoneOptionsApi() {
-  return requestClient.get<TimezoneOption[]>('/profile/timezone');
-}
-/**
- * 获取用户时区
- */
-export async function getUserTimezoneApi(): Promise<null | string | undefined> {
-  return requestClient.get<null | string | undefined>('/user/timezone');
-}
-/**
- * 设置用户时区
- * @param timezone 时区
- */
-export async function setUserTimezoneApi(timezone: string) {
-  return requestClient.post('/user/setTimezone', { timezone });
-}

+ 2 - 41
apps/web-ele/src/layouts/basic.vue

@@ -1,8 +1,7 @@
 <script lang="ts" setup>
-import type { ExtendedModalApi } from '@vben/common-ui';
 import type { NotificationItem } from '@vben/layouts';
 
-import { computed, onMounted, ref, unref, watch } from 'vue';
+import { computed, ref, watch } from 'vue';
 
 import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
 import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
@@ -12,18 +11,14 @@ import {
   BasicLayout,
   LockScreen,
   Notification,
-  TimezoneButton,
   UserDropdown,
 } from '@vben/layouts';
 import { preferences } from '@vben/preferences';
 import { useAccessStore, useUserStore } from '@vben/stores';
 import { openWindow } from '@vben/utils';
 
-import { ElMessage } from 'element-plus';
-
-import { getTimezoneOptionsApi } from '#/api';
 import { $t } from '#/locales';
-import { useAuthStore, useUserProfileStore } from '#/store';
+import { useAuthStore } from '#/store';
 import LoginForm from '#/views/_core/authentication/login.vue';
 
 const notifications = ref<NotificationItem[]>([
@@ -65,32 +60,6 @@ const showDot = computed(() =>
   notifications.value.some((item) => !item.isRead),
 );
 
-const userProfileStore = useUserProfileStore();
-const computedTimezone = computed(() => unref(userProfileStore.timezone));
-
-const timezoneOptions = ref<string[]>([]);
-onMounted(async () => {
-  timezoneOptions.value = ((await getTimezoneOptionsApi()) || []).map(
-    (item) => item.timezone,
-  );
-});
-const handleSetTimezone = async (
-  modalApi: ExtendedModalApi,
-  timezone?: string,
-) => {
-  if (!timezone) {
-    return;
-  }
-  try {
-    modalApi.setState({ confirmLoading: true });
-    await userProfileStore.setTimezone(timezone);
-    ElMessage.success($t('ui.widgets.timezone.setSuccess'));
-    modalApi.close();
-  } finally {
-    modalApi.setState({ confirmLoading: false });
-  }
-};
-
 const menus = computed(() => [
   {
     handler: () => {
@@ -178,14 +147,6 @@ watch(
         @make-all="handleMakeAll"
       />
     </template>
-    <template #timezone>
-      <TimezoneButton
-        :ok-handler="handleSetTimezone"
-        :timezone="computedTimezone"
-        :timezone-options="timezoneOptions"
-        name="out"
-      />
-    </template>
     <template #extra>
       <AuthenticationLoginExpiredModal
         v-model:open="accessStore.loginExpired"

+ 0 - 1
apps/web-ele/src/store/index.ts

@@ -1,2 +1 @@
 export * from './auth';
-export * from './user-profile';

+ 0 - 58
apps/web-ele/src/store/user-profile.ts

@@ -1,58 +0,0 @@
-import { ref } from 'vue';
-
-import { getTimezone, setDefaultTimezone } from '@vben/utils';
-
-import { acceptHMRUpdate, defineStore } from 'pinia';
-
-import { getUserTimezoneApi, setUserTimezoneApi } from '#/api';
-
-const useUserProfileStore = defineStore('user-profile', () => {
-  const timezoneRef = ref(
-    getTimezone() || new Intl.DateTimeFormat().resolvedOptions().timeZone,
-  );
-
-  /**
-   * 设置用户时区
-   * Set the user's timezone
-   * @param timezone 时区字符串
-   */
-  async function setTimezone(timezone: string) {
-    // 保存用户的时区设置
-    await setUserTimezoneApi(timezone);
-    timezoneRef.value = timezone;
-    // 设置dayjs默认时区
-    setDefaultTimezone(timezone);
-  }
-
-  /**
-   * 初始化用户时区
-   * Initialize the user's timezone
-   */
-  async function initTimezone() {
-    // 从服务器获取用户时区
-    const timezone = await getUserTimezoneApi();
-    if (timezone) {
-      timezoneRef.value = timezone;
-      // 设置dayjs默认时区
-      setDefaultTimezone(timezone);
-    }
-  }
-
-  initTimezone().catch((error) => {
-    console.error('Failed to initialize timezone during store setup:', error);
-  });
-
-  return {
-    timezone: timezoneRef,
-    setTimezone,
-    initTimezone,
-  };
-});
-
-export { useUserProfileStore };
-
-// 解决热更新问题
-const hot = import.meta.hot;
-if (hot) {
-  hot.accept(acceptHMRUpdate(useUserProfileStore, hot));
-}

+ 0 - 1
apps/web-naive/src/api/core/index.ts

@@ -1,4 +1,3 @@
 export * from './auth';
 export * from './menu';
 export * from './user';
-export * from './user-profile';

+ 0 - 23
apps/web-naive/src/api/core/user-profile.ts

@@ -1,23 +0,0 @@
-import type { TimezoneOption } from '@vben/types';
-
-import { requestClient } from '#/api/request';
-
-/**
- * 获取系统支持的时区列表
- */
-export async function getTimezoneOptionsApi() {
-  return requestClient.get<TimezoneOption[]>('/profile/timezone');
-}
-/**
- * 获取用户时区
- */
-export async function getUserTimezoneApi(): Promise<null | string | undefined> {
-  return requestClient.get<null | string | undefined>('/user/timezone');
-}
-/**
- * 设置用户时区
- * @param timezone 时区
- */
-export async function setUserTimezoneApi(timezone: string) {
-  return requestClient.post('/user/setTimezone', { timezone });
-}

+ 2 - 42
apps/web-naive/src/layouts/basic.vue

@@ -1,8 +1,7 @@
 <script lang="ts" setup>
-import type { ExtendedModalApi } from '@vben/common-ui';
 import type { NotificationItem } from '@vben/layouts';
 
-import { computed, onMounted, ref, unref, watch } from 'vue';
+import { computed, ref, watch } from 'vue';
 
 import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
 import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
@@ -12,21 +11,16 @@ import {
   BasicLayout,
   LockScreen,
   Notification,
-  TimezoneButton,
   UserDropdown,
 } from '@vben/layouts';
 import { preferences } from '@vben/preferences';
 import { useAccessStore, useUserStore } from '@vben/stores';
 import { openWindow } from '@vben/utils';
 
-import { useMessage } from 'naive-ui';
-
-import { getTimezoneOptionsApi } from '#/api';
 import { $t } from '#/locales';
-import { useAuthStore, useUserProfileStore } from '#/store';
+import { useAuthStore } from '#/store';
 import LoginForm from '#/views/_core/authentication/login.vue';
 
-const message = useMessage();
 const notifications = ref<NotificationItem[]>([
   {
     avatar: 'https://avatar.vercel.sh/vercel.svg?text=VB',
@@ -66,32 +60,6 @@ const showDot = computed(() =>
   notifications.value.some((item) => !item.isRead),
 );
 
-const userProfileStore = useUserProfileStore();
-const computedTimezone = computed(() => unref(userProfileStore.timezone));
-
-const timezoneOptions = ref<string[]>([]);
-onMounted(async () => {
-  timezoneOptions.value = ((await getTimezoneOptionsApi()) || []).map(
-    (item) => item.timezone,
-  );
-});
-const handleSetTimezone = async (
-  modalApi: ExtendedModalApi,
-  timezone?: string,
-) => {
-  if (!timezone) {
-    return;
-  }
-  try {
-    modalApi.setState({ confirmLoading: true });
-    await userProfileStore.setTimezone(timezone);
-    message.success($t('ui.widgets.timezone.setSuccess'));
-    modalApi.close();
-  } finally {
-    modalApi.setState({ confirmLoading: false });
-  }
-};
-
 const menus = computed(() => [
   {
     handler: () => {
@@ -180,14 +148,6 @@ watch(
         @make-all="handleMakeAll"
       />
     </template>
-    <template #timezone>
-      <TimezoneButton
-        :ok-handler="handleSetTimezone"
-        :timezone="computedTimezone"
-        :timezone-options="timezoneOptions"
-        name="out"
-      />
-    </template>
     <template #extra>
       <AuthenticationLoginExpiredModal
         v-model:open="accessStore.loginExpired"

+ 0 - 1
apps/web-naive/src/store/index.ts

@@ -1,2 +1 @@
 export * from './auth';
-export * from './user-profile';

+ 0 - 58
apps/web-naive/src/store/user-profile.ts

@@ -1,58 +0,0 @@
-import { ref } from 'vue';
-
-import { getTimezone, setDefaultTimezone } from '@vben/utils';
-
-import { acceptHMRUpdate, defineStore } from 'pinia';
-
-import { getUserTimezoneApi, setUserTimezoneApi } from '#/api';
-
-const useUserProfileStore = defineStore('user-profile', () => {
-  const timezoneRef = ref(
-    getTimezone() || new Intl.DateTimeFormat().resolvedOptions().timeZone,
-  );
-
-  /**
-   * 设置用户时区
-   * Set the user's timezone
-   * @param timezone 时区字符串
-   */
-  async function setTimezone(timezone: string) {
-    // 保存用户的时区设置
-    await setUserTimezoneApi(timezone);
-    timezoneRef.value = timezone;
-    // 设置dayjs默认时区
-    setDefaultTimezone(timezone);
-  }
-
-  /**
-   * 初始化用户时区
-   * Initialize the user's timezone
-   */
-  async function initTimezone() {
-    // 从服务器获取用户时区
-    const timezone = await getUserTimezoneApi();
-    if (timezone) {
-      timezoneRef.value = timezone;
-      // 设置dayjs默认时区
-      setDefaultTimezone(timezone);
-    }
-  }
-
-  initTimezone().catch((error) => {
-    console.error('Failed to initialize timezone during store setup:', error);
-  });
-
-  return {
-    timezone: timezoneRef,
-    setTimezone,
-    initTimezone,
-  };
-});
-
-export { useUserProfileStore };
-
-// 解决热更新问题
-const hot = import.meta.hot;
-if (hot) {
-  hot.accept(acceptHMRUpdate(useUserProfileStore, hot));
-}

+ 10 - 0
packages/@core/base/typings/src/app.d.ts

@@ -93,6 +93,15 @@ type PageTransitionType = 'fade' | 'fade-down' | 'fade-slide' | 'fade-up';
  */
 type AuthPageLayoutType = 'panel-center' | 'panel-left' | 'panel-right';
 
+/**
+ * 时区选项
+ */
+interface TimezoneOption {
+  label: string;
+  offset: number;
+  timezone: string;
+}
+
 export type {
   AccessModeType,
   AuthPageLayoutType,
@@ -108,4 +117,5 @@ export type {
   PreferencesButtonPositionType,
   TabsStyleType,
   ThemeModeType,
+  TimezoneOption,
 };

+ 0 - 1
packages/@core/base/typings/src/index.ts

@@ -3,5 +3,4 @@ export type * from './basic';
 export type * from './helper';
 export type * from './menu-record';
 export type * from './tabs';
-export type * from './user-profile';
 export type * from './vue-router';

+ 0 - 9
packages/@core/base/typings/src/user-profile.d.ts

@@ -1,9 +0,0 @@
-/**
- * 时区选项
- */
-interface TimezoneOption {
-  offset: number;
-  timezone: string;
-}
-
-export type { TimezoneOption };

+ 33 - 2
packages/@core/preferences/src/constants.ts

@@ -1,4 +1,4 @@
-import type { BuiltinThemeType } from '@vben-core/typings';
+import type { BuiltinThemeType, TimezoneOption } from '@vben-core/typings';
 
 interface BuiltinThemePreset {
   color: string;
@@ -81,8 +81,39 @@ const BUILT_IN_THEME_PRESETS: BuiltinThemePreset[] = [
   },
 ];
 
+/**
+ * 时区选项
+ */
+const DEFAULT_TIME_ZONE_OPTIONS: TimezoneOption[] = [
+  {
+    offset: -5,
+    timezone: 'America/New_York',
+    label: 'America/New_York(GMT-5)',
+  },
+  {
+    offset: 0,
+    timezone: 'Europe/London',
+    label: 'Europe/London(GMT0)',
+  },
+  {
+    offset: 8,
+    timezone: 'Asia/Shanghai',
+    label: 'Asia/Shanghai(GMT+8)',
+  },
+  {
+    offset: 9,
+    timezone: 'Asia/Tokyo',
+    label: 'Asia/Tokyo(GMT+9)',
+  },
+  {
+    offset: 9,
+    timezone: 'Asia/Seoul',
+    label: 'Asia/Seoul(GMT+9)',
+  },
+];
+
 export const COLOR_PRESETS = [...BUILT_IN_THEME_PRESETS].slice(0, 7);
 
-export { BUILT_IN_THEME_PRESETS };
+export { BUILT_IN_THEME_PRESETS, DEFAULT_TIME_ZONE_OPTIONS };
 
 export type { BuiltinThemePreset };

+ 4 - 0
packages/effects/layouts/src/basic/header/header.vue

@@ -13,6 +13,7 @@ import {
   LanguageToggle,
   PreferencesButton,
   ThemeToggle,
+  TimezoneButton,
 } from '../../widgets';
 
 interface Props {
@@ -172,6 +173,9 @@ function clearPreferencesAndLogout() {
         <template v-else-if="slot.name === 'fullscreen'">
           <VbenFullScreen class="mr-1" />
         </template>
+        <template v-else-if="slot.name === 'timezone'">
+          <TimezoneButton class="mr-1 mt-[2px]" />
+        </template>
       </slot>
     </template>
   </div>

+ 35 - 34
packages/effects/layouts/src/widgets/timezone/timezone-button.vue

@@ -1,10 +1,9 @@
 <script setup lang="ts">
-import type { ExtendedModalApi } from '@vben-core/popup-ui';
-
-import { ref, unref, watch } from 'vue';
+import { ref, unref } from 'vue';
 
 import { createIconifyIcon } from '@vben/icons';
 import { $t } from '@vben/locales';
+import { useTimezoneStore } from '@vben/stores';
 
 import { useVbenModal } from '@vben-core/popup-ui';
 import {
@@ -13,42 +12,43 @@ import {
   VbenIconButton,
 } from '@vben-core/shadcn-ui';
 
-interface Props {
-  timezoneOptions: string[];
-  okHandler?: (
-    modalApi: ExtendedModalApi,
-    timezone?: string,
-  ) => Promise<void> | void;
-  timezone?: string;
-}
+const TimezoneIcon = createIconifyIcon('fluent-mdl2:world-clock');
 
-const props = defineProps<Props>();
-const emit = defineEmits<{ change: [string] }>();
+const timezoneStore = useTimezoneStore();
 
-const TimezoneIcon = createIconifyIcon('fluent-mdl2:world-clock');
+const timezoneRef = ref<null | string>(null);
+
+const timezoneOptionsRef = ref<
+  {
+    label: string;
+    value: string;
+  }[]
+>([]);
 
 const [Modal, modalApi] = useVbenModal({
   fullscreenButton: false,
-  onConfirm: () => {
-    props.okHandler?.(modalApi, unref(timezoneValue));
+  onConfirm: async () => {
+    try {
+      modalApi.setState({ confirmLoading: true });
+      if (timezoneRef.value) {
+        await timezoneStore.setTimezone(unref(timezoneRef));
+      }
+      modalApi.close();
+    } finally {
+      modalApi.setState({ confirmLoading: false });
+    }
+  },
+  async onOpenChange(isOpen) {
+    if (isOpen) {
+      timezoneRef.value = unref(timezoneStore.timezone);
+      timezoneOptionsRef.value = await timezoneStore.getTimezoneOptions();
+    }
   },
 });
 
 const handleClick = () => {
   modalApi.open();
 };
-
-const timezoneValue = ref<string | undefined>(unref(props.timezone));
-watch(
-  () => props.timezone,
-  (newTimezone) => {
-    timezoneValue.value = unref(newTimezone);
-  },
-);
-const handleClickItem = (timezone: string) => {
-  timezoneValue.value = timezone;
-  emit('change', timezone);
-};
 </script>
 
 <template>
@@ -62,15 +62,16 @@ const handleClickItem = (timezone: string) => {
     </VbenIconButton>
     <Modal :title="$t('ui.widgets.timezone.setTimezone')">
       <div class="timezone-container">
-        <RadioGroup v-model="timezoneValue" class="flex flex-col gap-2">
+        <RadioGroup v-model="timezoneRef" class="flex flex-col gap-2">
           <div
             class="flex cursor-pointer items-center gap-2"
-            v-for="item in props.timezoneOptions"
-            :key="`container${item}`"
-            @click="handleClickItem(item)"
+            v-for="item in timezoneOptionsRef"
+            :key="`container${item.value}`"
           >
-            <RadioGroupItem :id="item" :value="item" />
-            <label :for="item" class="cursor-pointer">{{ item }}</label>
+            <RadioGroupItem :id="item.value" :value="item.value" />
+            <label :for="item.value" class="cursor-pointer">{{
+              item.label
+            }}</label>
           </div>
         </RadioGroup>
       </div>

+ 1 - 0
packages/stores/src/modules/index.ts

@@ -1,3 +1,4 @@
 export * from './access';
 export * from './tabbar';
+export * from './timezone';
 export * from './user';

+ 125 - 0
packages/stores/src/modules/timezone.ts

@@ -0,0 +1,125 @@
+import { ref, unref } from 'vue';
+
+import { DEFAULT_TIME_ZONE_OPTIONS } from '@vben-core/preferences';
+import { getTimezone, setDefaultTimezone } from '@vben-core/shared/utils';
+
+import { acceptHMRUpdate, defineStore } from 'pinia';
+
+interface TimezoneHandler {
+  getTimezone?: () => Promise<null | string | undefined>;
+  getTimezoneOptions?: () => Promise<
+    {
+      label: string;
+      value: string;
+    }[]
+  >;
+  onTimezoneChange?: (timezone: string) => Promise<void>;
+}
+
+/**
+ * 默认时区处理模块
+ * 时区存储基于pinia存储插件
+ */
+const getDefaultTimezoneHandler = (): TimezoneHandler => {
+  return {
+    getTimezoneOptions: () => {
+      return Promise.resolve(
+        DEFAULT_TIME_ZONE_OPTIONS.map((item) => {
+          return {
+            label: item.label,
+            value: item.timezone,
+          };
+        }),
+      );
+    },
+  };
+};
+
+/**
+ * 自定义时区处理模块
+ */
+let customTimezoneHandler: null | Partial<TimezoneHandler> = null;
+const setTimezoneHandler = (handler: Partial<TimezoneHandler>) => {
+  customTimezoneHandler = handler;
+};
+
+/**
+ * 获取时区处理模块
+ */
+const getTimezoneHandler = () => {
+  return {
+    ...getDefaultTimezoneHandler(),
+    ...customTimezoneHandler,
+  };
+};
+
+/**
+ * timezone支持模块
+ */
+const useTimezoneStore = defineStore(
+  'core-timezone',
+  () => {
+    const timezoneRef = ref(
+      getTimezone() || new Intl.DateTimeFormat().resolvedOptions().timeZone,
+    );
+
+    const timezoneHandler = getTimezoneHandler();
+
+    /**
+     * 初始化时区
+     * Initialize the timezone
+     */
+    async function initTimezone() {
+      const timezone = await timezoneHandler.getTimezone?.();
+      if (timezone) {
+        timezoneRef.value = timezone;
+      }
+      // 设置dayjs默认时区
+      setDefaultTimezone(unref(timezoneRef));
+    }
+
+    /**
+     * 设置时区
+     * Set the timezone
+     * @param timezone 时区字符串
+     */
+    async function setTimezone(timezone: string) {
+      await timezoneHandler.onTimezoneChange?.(timezone);
+      timezoneRef.value = timezone;
+      // 设置dayjs默认时区
+      setDefaultTimezone(timezone);
+    }
+
+    /**
+     * 获取时区选项
+     * Get the timezone options
+     */
+    async function getTimezoneOptions() {
+      return await timezoneHandler.getTimezoneOptions();
+    }
+
+    initTimezone().catch((error) => {
+      console.error('Failed to initialize timezone during store setup:', error);
+    });
+
+    return {
+      timezone: timezoneRef,
+      setTimezone,
+      getTimezoneOptions,
+    };
+  },
+  {
+    persist: {
+      // 持久化
+      pick: ['timezone'],
+    },
+  },
+);
+
+export { setTimezoneHandler, useTimezoneStore };
+
+// 解决热更新问题
+const hot = import.meta.hot;
+if (hot) {
+  hot.accept(acceptHMRUpdate(useTimezoneStore, hot));
+}

+ 1 - 0
playground/src/api/core/index.ts

@@ -1,3 +1,4 @@
 export * from './auth';
 export * from './menu';
+export * from './timezone';
 export * from './user';

+ 30 - 0
playground/src/api/core/timezone.ts

@@ -0,0 +1,30 @@
+import type { TimezoneOption } from '@vben/types';
+
+import { requestClient } from '#/api/request';
+
+/**
+ * 获取系统支持的时区列表
+ */
+export async function getTimezoneOptionsApi() {
+  const dataList =
+    (await requestClient.get<TimezoneOption[]>(
+      '/timezone/getTimezoneOptions',
+    )) || [];
+  return dataList.map((item) => ({
+    label: item.timezone,
+    value: item.timezone,
+  }));
+}
+/**
+ * 获取用户时区
+ */
+export async function getTimezoneApi(): Promise<null | string | undefined> {
+  return requestClient.get<null | string | undefined>('/timezone/getTimezone');
+}
+/**
+ * 设置用户时区
+ * @param timezone 时区
+ */
+export async function setTimezoneApi(timezone: string) {
+  return requestClient.post('/timezone/setTimezone', { timezone });
+}

+ 4 - 0
playground/src/bootstrap.ts

@@ -15,6 +15,7 @@ import { router } from '#/router';
 import { initComponentAdapter } from './adapter/component';
 import { initSetupVbenForm } from './adapter/form';
 import App from './app.vue';
+import { initTimezone } from './timezone-init';
 
 async function bootstrap(namespace: string) {
   // 初始化组件适配器
@@ -46,6 +47,9 @@ async function bootstrap(namespace: string) {
   // 配置 pinia-tore
   await initStores(app, { namespace });
 
+  // 初始化时区HANDLER
+  initTimezone();
+
   // 安装权限指令
   registerAccessDirective(app);
 

+ 20 - 0
playground/src/timezone-init.ts

@@ -0,0 +1,20 @@
+import { setTimezoneHandler } from '@vben/stores';
+
+import { getTimezoneApi, getTimezoneOptionsApi, setTimezoneApi } from '#/api';
+
+/**
+ * 初始化时区处理,通过API保存时区设置
+ */
+export function initTimezone() {
+  setTimezoneHandler({
+    getTimezone() {
+      return getTimezoneApi();
+    },
+    setTimezone(timezone: string) {
+      return setTimezoneApi(timezone);
+    },
+    getTimezoneOptions() {
+      return getTimezoneOptionsApi();
+    },
+  });
+}