diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b4afdd93..bb59abb0 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -30,7 +30,7 @@ jobs: run: pnpm build:play - name: Sync Playground files - uses: SamKirkland/FTP-Deploy-Action@v4.3.5 + uses: SamKirkland/FTP-Deploy-Action@v4.3.6 with: server: ${{ secrets.PRO_FTP_HOST }} username: ${{ secrets.WEB_PLAYGROUND_FTP_ACCOUNT }} @@ -54,7 +54,7 @@ jobs: run: pnpm build:docs - name: Sync Docs files - uses: SamKirkland/FTP-Deploy-Action@v4.3.5 + uses: SamKirkland/FTP-Deploy-Action@v4.3.6 with: server: ${{ secrets.PRO_FTP_HOST }} username: ${{ secrets.WEBSITE_FTP_ACCOUNT }} @@ -85,7 +85,7 @@ jobs: run: pnpm run build:antd - name: Sync files - uses: SamKirkland/FTP-Deploy-Action@v4.3.5 + uses: SamKirkland/FTP-Deploy-Action@v4.3.6 with: server: ${{ secrets.PRO_FTP_HOST }} username: ${{ secrets.WEB_ANTD_FTP_ACCOUNT }} @@ -116,7 +116,7 @@ jobs: run: pnpm run build:ele - name: Sync files - uses: SamKirkland/FTP-Deploy-Action@v4.3.5 + uses: SamKirkland/FTP-Deploy-Action@v4.3.6 with: server: ${{ secrets.PRO_FTP_HOST }} username: ${{ secrets.WEB_ELE_FTP_ACCOUNT }} @@ -147,7 +147,7 @@ jobs: run: pnpm run build:naive - name: Sync files - uses: SamKirkland/FTP-Deploy-Action@v4.3.5 + uses: SamKirkland/FTP-Deploy-Action@v4.3.6 with: server: ${{ secrets.PRO_FTP_HOST }} username: ${{ secrets.WEB_NAIVE_FTP_ACCOUNT }} diff --git a/.npmrc b/.npmrc index 21147aff..aeac1ae9 100644 --- a/.npmrc +++ b/.npmrc @@ -1,4 +1,4 @@ -registry = "https://registry.npmmirror.com" +registry=https://registry.npmmirror.com public-hoist-pattern[]=lefthook public-hoist-pattern[]=eslint public-hoist-pattern[]=prettier diff --git a/README.md b/README.md index b9dd73eb..ce8e8975 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@

Vue Vben Admin

-[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=vbenjs_vue-vben-admin&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=vbenjs_vue-vben-admin) ![codeql](https://github.com/vbenjs/vue-vben-admin/actions/workflows/codeql.yml/badge.svg) ![build](https://github.com/vbenjs/vue-vben-admin/actions/workflows/build.yml/badge.svg) ![ci](https://github.com/vbenjs/vue-vben-admin/actions/workflows/ci.yml/badge.svg) ![deploy](https://github.com/vbenjs/vue-vben-admin/actions/workflows/deploy.yml/badge.svg) +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=vbenjs_vue-vben-admin&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=vbenjs_vue-vben-admin) [![codeql](https://github.com/vbenjs/vue-vben-admin/actions/workflows/codeql.yml/badge.svg)](https://github.com/vbenjs/vue-vben-admin/actions/workflows/codeql.yml) [![build](https://github.com/vbenjs/vue-vben-admin/actions/workflows/build.yml/badge.svg)](https://github.com/vbenjs/vue-vben-admin/actions/workflows/build.yml) [![ci](https://github.com/vbenjs/vue-vben-admin/actions/workflows/ci.yml/badge.svg)](https://github.com/vbenjs/vue-vben-admin/actions/workflows/ci.yml) [![deploy](https://github.com/vbenjs/vue-vben-admin/actions/workflows/deploy.yml/badge.svg)](https://github.com/vbenjs/vue-vben-admin/actions/workflows/deploy.yml) **English** | [中文](./README.zh-CN.md) | [日本語](./README.ja-JP.md) diff --git a/apps/backend-mock/api/timezone/getTimezone.ts b/apps/backend-mock/api/timezone/getTimezone.ts new file mode 100644 index 00000000..0cbcb6ec --- /dev/null +++ b/apps/backend-mock/api/timezone/getTimezone.ts @@ -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()); +}); diff --git a/apps/backend-mock/api/timezone/getTimezoneOptions.ts b/apps/backend-mock/api/timezone/getTimezoneOptions.ts new file mode 100644 index 00000000..6c241864 --- /dev/null +++ b/apps/backend-mock/api/timezone/getTimezoneOptions.ts @@ -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); +}); diff --git a/apps/backend-mock/api/timezone/setTimezone.ts b/apps/backend-mock/api/timezone/setTimezone.ts new file mode 100644 index 00000000..34d8f19e --- /dev/null +++ b/apps/backend-mock/api/timezone/setTimezone.ts @@ -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({}); +}); diff --git a/apps/backend-mock/utils/mock-data.ts b/apps/backend-mock/utils/mock-data.ts index 192f30a0..ee38e8ef 100644 --- a/apps/backend-mock/utils/mock-data.ts +++ b/apps/backend-mock/utils/mock-data.ts @@ -7,6 +7,11 @@ export interface UserInfo { homePath?: string; } +export interface TimezoneOption { + offset: number; + timezone: string; +} + export const MOCK_USERS: UserInfo[] = [ { id: 0, @@ -276,7 +281,7 @@ export const MOCK_MENU_LIST = [ children: [ { id: 20_401, - pid: 201, + pid: 202, name: 'SystemDeptCreate', status: 1, type: 'button', @@ -285,7 +290,7 @@ export const MOCK_MENU_LIST = [ }, { id: 20_402, - pid: 201, + pid: 202, name: 'SystemDeptEdit', status: 1, type: 'button', @@ -294,7 +299,7 @@ export const MOCK_MENU_LIST = [ }, { id: 20_403, - pid: 201, + pid: 202, name: 'SystemDeptDelete', status: 1, type: 'button', @@ -388,3 +393,29 @@ export function getMenuIds(menus: any[]) { }); return ids; } + +/** + * 时区选项 + */ +export const TIME_ZONE_OPTIONS: TimezoneOption[] = [ + { + offset: -5, + timezone: 'America/New_York', + }, + { + offset: 0, + timezone: 'Europe/London', + }, + { + offset: 8, + timezone: 'Asia/Shanghai', + }, + { + offset: 9, + timezone: 'Asia/Tokyo', + }, + { + offset: 9, + timezone: 'Asia/Seoul', + }, +]; diff --git a/apps/backend-mock/utils/timezone-utils.ts b/apps/backend-mock/utils/timezone-utils.ts new file mode 100644 index 00000000..da35f920 --- /dev/null +++ b/apps/backend-mock/utils/timezone-utils.ts @@ -0,0 +1,9 @@ +let mockTimeZone: null | string = null; + +export const setTimezone = (timeZone: string) => { + mockTimeZone = timeZone; +}; + +export const getTimezone = () => { + return mockTimeZone; +}; diff --git a/apps/web-antd/src/layouts/auth.vue b/apps/web-antd/src/layouts/auth.vue index 18d415bc..8ba66e85 100644 --- a/apps/web-antd/src/layouts/auth.vue +++ b/apps/web-antd/src/layouts/auth.vue @@ -8,12 +8,14 @@ import { $t } from '#/locales'; const appName = computed(() => preferences.app.name); const logo = computed(() => preferences.logo.source); +const logoDark = computed(() => preferences.logo.sourceDark); diff --git a/packages/@core/ui-kit/popup-ui/src/modal/modal.vue b/packages/@core/ui-kit/popup-ui/src/modal/modal.vue index 89184750..a44a616c 100644 --- a/packages/@core/ui-kit/popup-ui/src/modal/modal.vue +++ b/packages/@core/ui-kit/popup-ui/src/modal/modal.vue @@ -180,7 +180,7 @@ function escapeKeyDown(e: KeyboardEvent) { } } -function handerOpenAutoFocus(e: Event) { +function handleOpenAutoFocus(e: Event) { if (!openAutoFocus.value) { e?.preventDefault(); } @@ -209,6 +209,12 @@ const getForceMount = computed(() => { return !unref(destroyOnClose) && unref(firstOpened); }); +const handleOpened = () => { + requestAnimationFrame(() => { + props.modalApi?.onOpened(); + }); +}; + function handleClosed() { isClosed.value = true; props.modalApi?.onClosed(); @@ -253,8 +259,8 @@ function handleClosed() { @escape-key-down="escapeKeyDown" @focus-outside="handleFocusOutside" @interact-outside="interactOutside" - @open-auto-focus="handerOpenAutoFocus" - @opened="() => modalApi?.onOpened()" + @open-auto-focus="handleOpenAutoFocus" + @opened="handleOpened" @pointer-down-outside="pointerDownOutside" > +import { computed } from 'vue'; + import { VbenAvatar } from '../avatar'; interface Props { @@ -22,6 +24,10 @@ interface Props { * @zh_CN Logo 图标 */ src?: string; + /** + * @zh_CN 暗色主题 Logo 图标 (可选,若不设置则使用 src) + */ + srcDark?: string; /** * @zh_CN Logo 文本 */ @@ -36,14 +42,27 @@ defineOptions({ name: 'VbenLogo', }); -withDefaults(defineProps(), { +const props = withDefaults(defineProps(), { collapsed: false, href: 'javascript:void 0', logoSize: 32, src: '', + srcDark: '', theme: 'light', fit: 'cover', }); + +/** + * @zh_CN 根据主题选择合适的 logo 图标 + */ +const logoSrc = computed(() => { + // 如果是暗色主题且提供了 srcDark,则使用暗色主题的 logo + if (props.theme === 'dark' && props.srcDark) { + return props.srcDark; + } + // 否则使用默认的 src + return props.src; +});