24 changed files with 444 additions and 5 deletions
@ -0,0 +1,12 @@ |
|||
import { eventHandler } from 'h3'; |
|||
import { verifyAccessToken } from '~/utils/jwt-utils'; |
|||
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response'; |
|||
import { getTimezone } from '~/utils/timezone-utils'; |
|||
|
|||
export default eventHandler((event) => { |
|||
const userinfo = verifyAccessToken(event); |
|||
if (!userinfo) { |
|||
return unAuthorizedResponse(event); |
|||
} |
|||
return useResponseSuccess(getTimezone()); |
|||
}); |
|||
@ -0,0 +1,11 @@ |
|||
import { eventHandler } from 'h3'; |
|||
import { TIME_ZONE_OPTIONS } from '~/utils/mock-data'; |
|||
import { useResponseSuccess } from '~/utils/response'; |
|||
|
|||
export default eventHandler(() => { |
|||
const data = TIME_ZONE_OPTIONS.map((o) => ({ |
|||
label: `${o.timezone} (GMT${o.offset >= 0 ? `+${o.offset}` : o.offset})`, |
|||
value: o.timezone, |
|||
})); |
|||
return useResponseSuccess(data); |
|||
}); |
|||
@ -0,0 +1,22 @@ |
|||
import { eventHandler, readBody } from 'h3'; |
|||
import { verifyAccessToken } from '~/utils/jwt-utils'; |
|||
import { TIME_ZONE_OPTIONS } from '~/utils/mock-data'; |
|||
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response'; |
|||
import { setTimezone } from '~/utils/timezone-utils'; |
|||
|
|||
export default eventHandler(async (event) => { |
|||
const userinfo = verifyAccessToken(event); |
|||
if (!userinfo) { |
|||
return unAuthorizedResponse(event); |
|||
} |
|||
const body = await readBody<{ timezone?: unknown }>(event); |
|||
const timezone = |
|||
typeof body?.timezone === 'string' ? body.timezone : undefined; |
|||
const allowed = TIME_ZONE_OPTIONS.some((o) => o.timezone === timezone); |
|||
if (!timezone || !allowed) { |
|||
setResponseStatus(event, 400); |
|||
return useResponseError('Bad Request', 'Invalid timezone'); |
|||
} |
|||
setTimezone(timezone); |
|||
return useResponseSuccess({}); |
|||
}); |
|||
@ -0,0 +1,9 @@ |
|||
let mockTimeZone: null | string = null; |
|||
|
|||
export const setTimezone = (timeZone: string) => { |
|||
mockTimeZone = timeZone; |
|||
}; |
|||
|
|||
export const getTimezone = () => { |
|||
return mockTimeZone; |
|||
}; |
|||
@ -0,0 +1 @@ |
|||
export { default as TimezoneButton } from './timezone-button.vue'; |
|||
@ -0,0 +1,87 @@ |
|||
<script setup lang="ts"> |
|||
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 { |
|||
RadioGroup, |
|||
RadioGroupItem, |
|||
VbenIconButton, |
|||
} from '@vben-core/shadcn-ui'; |
|||
|
|||
const TimezoneIcon = createIconifyIcon('fluent-mdl2:world-clock'); |
|||
|
|||
const timezoneStore = useTimezoneStore(); |
|||
|
|||
const timezoneRef = ref<string | undefined>(); |
|||
|
|||
const timezoneOptionsRef = ref< |
|||
{ |
|||
label: string; |
|||
value: string; |
|||
}[] |
|||
>([]); |
|||
|
|||
const [Modal, modalApi] = useVbenModal({ |
|||
fullscreenButton: false, |
|||
onConfirm: async () => { |
|||
try { |
|||
modalApi.setState({ confirmLoading: true }); |
|||
const timezone = unref(timezoneRef); |
|||
if (timezone) { |
|||
await timezoneStore.setTimezone(timezone); |
|||
} |
|||
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(); |
|||
}; |
|||
</script> |
|||
|
|||
<template> |
|||
<div> |
|||
<VbenIconButton |
|||
:tooltip="$t('ui.widgets.timezone.setTimezone')" |
|||
class="hover:animate-[shrink_0.3s_ease-in-out]" |
|||
@click="handleClick" |
|||
> |
|||
<TimezoneIcon class="text-foreground size-4" /> |
|||
</VbenIconButton> |
|||
<Modal :title="$t('ui.widgets.timezone.setTimezone')"> |
|||
<div class="timezone-container"> |
|||
<RadioGroup v-model="timezoneRef" class="flex flex-col gap-2"> |
|||
<div |
|||
class="flex cursor-pointer items-center gap-2" |
|||
v-for="item in timezoneOptionsRef" |
|||
:key="`container${item.value}`" |
|||
> |
|||
<RadioGroupItem :id="item.value" :value="item.value" /> |
|||
<label :for="item.value" class="cursor-pointer">{{ |
|||
item.label |
|||
}}</label> |
|||
</div> |
|||
</RadioGroup> |
|||
</div> |
|||
</Modal> |
|||
</div> |
|||
</template> |
|||
|
|||
<style scoped> |
|||
.timezone-container { |
|||
padding-left: 20px; |
|||
} |
|||
</style> |
|||
@ -1,3 +1,4 @@ |
|||
export * from './access'; |
|||
export * from './tabbar'; |
|||
export * from './timezone'; |
|||
export * from './user'; |
|||
|
|||
@ -0,0 +1,126 @@ |
|||
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; |
|||
}[] |
|||
>; |
|||
setTimezone?: (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, |
|||
); |
|||
|
|||
/** |
|||
* 初始化时区 |
|||
* Initialize the timezone |
|||
*/ |
|||
async function initTimezone() { |
|||
const timezoneHandler = getTimezoneHandler(); |
|||
const timezone = await timezoneHandler.getTimezone?.(); |
|||
if (timezone) { |
|||
timezoneRef.value = timezone; |
|||
} |
|||
// 设置dayjs默认时区
|
|||
setDefaultTimezone(unref(timezoneRef)); |
|||
} |
|||
|
|||
/** |
|||
* 设置时区 |
|||
* Set the timezone |
|||
* @param timezone 时区字符串 |
|||
*/ |
|||
async function setTimezone(timezone: string) { |
|||
const timezoneHandler = getTimezoneHandler(); |
|||
await timezoneHandler.setTimezone?.(timezone); |
|||
timezoneRef.value = timezone; |
|||
// 设置dayjs默认时区
|
|||
setDefaultTimezone(timezone); |
|||
} |
|||
|
|||
/** |
|||
* 获取时区选项 |
|||
* Get the timezone options |
|||
*/ |
|||
async function getTimezoneOptions() { |
|||
const timezoneHandler = getTimezoneHandler(); |
|||
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,3 +1,4 @@ |
|||
export * from './auth'; |
|||
export * from './menu'; |
|||
export * from './timezone'; |
|||
export * from './user'; |
|||
|
|||
@ -0,0 +1,26 @@ |
|||
import { requestClient } from '#/api/request'; |
|||
|
|||
/** |
|||
* 获取系统支持的时区列表 |
|||
*/ |
|||
export async function getTimezoneOptionsApi() { |
|||
return await requestClient.get< |
|||
{ |
|||
label: string; |
|||
value: string; |
|||
}[] |
|||
>('/timezone/getTimezoneOptions'); |
|||
} |
|||
/** |
|||
* 获取用户时区 |
|||
*/ |
|||
export async function getTimezoneApi(): Promise<null | string | undefined> { |
|||
return requestClient.get<null | string | undefined>('/timezone/getTimezone'); |
|||
} |
|||
/** |
|||
* 设置用户时区 |
|||
* @param timezone 时区 |
|||
*/ |
|||
export async function setTimezoneApi(timezone: string): Promise<void> { |
|||
return requestClient.post('/timezone/setTimezone', { timezone }); |
|||
} |
|||
@ -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(); |
|||
}, |
|||
}); |
|||
} |
|||
Loading…
Reference in new issue