@qlover/create-app 0.10.0 → 0.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +145 -0
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/templates/next-app/config/IOCIdentifier.ts +2 -2
- package/dist/templates/next-app/config/Identifier/common/common.ts +14 -0
- package/dist/templates/next-app/config/Identifier/pages/index.ts +1 -0
- package/dist/templates/next-app/config/Identifier/pages/page.about.ts +20 -0
- package/dist/templates/next-app/config/common.ts +1 -1
- package/dist/templates/next-app/config/cookies.ts +23 -0
- package/dist/templates/next-app/config/i18n/AboutI18n.ts +14 -0
- package/dist/templates/next-app/config/i18n/i18nConfig.ts +3 -1
- package/dist/templates/next-app/config/i18n/index.ts +1 -0
- package/dist/templates/next-app/config/i18n/loginI18n.ts +8 -0
- package/dist/templates/next-app/config/theme.ts +4 -0
- package/dist/templates/next-app/eslint.config.mjs +4 -1
- package/dist/templates/next-app/migrations/schema/UserSchema.ts +17 -3
- package/dist/templates/next-app/next.config.ts +1 -0
- package/dist/templates/next-app/package.json +15 -7
- package/dist/templates/next-app/public/locales/en.json +5 -0
- package/dist/templates/next-app/public/locales/zh.json +5 -0
- package/dist/templates/next-app/src/app/[locale]/admin/AdminI18nProvider.tsx +37 -0
- package/dist/templates/next-app/src/app/[locale]/admin/layout.tsx +30 -6
- package/dist/templates/next-app/src/app/[locale]/admin/locales/page.tsx +1 -1
- package/dist/templates/next-app/src/app/[locale]/layout.tsx +47 -10
- package/dist/templates/next-app/src/app/[locale]/login/LoginForm.tsx +1 -1
- package/dist/templates/next-app/src/app/[locale]/login/page.tsx +22 -10
- package/dist/templates/next-app/src/app/[locale]/page.tsx +23 -8
- package/dist/templates/next-app/src/app/[locale]/register/page.tsx +21 -9
- package/dist/templates/next-app/src/app/api/admin/locales/create/route.ts +7 -28
- package/dist/templates/next-app/src/app/api/admin/locales/import/route.ts +7 -34
- package/dist/templates/next-app/src/app/api/admin/locales/route.ts +12 -34
- package/dist/templates/next-app/src/app/api/admin/locales/update/route.ts +7 -26
- package/dist/templates/next-app/src/app/api/admin/users/route.ts +14 -33
- package/dist/templates/next-app/src/app/api/locales/json/route.ts +13 -25
- package/dist/templates/next-app/src/app/api/user/login/route.ts +6 -46
- package/dist/templates/next-app/src/app/api/user/logout/route.ts +5 -24
- package/dist/templates/next-app/src/app/api/user/register/route.ts +6 -45
- package/dist/templates/next-app/src/app/manifest.ts +16 -0
- package/dist/templates/next-app/src/app/robots.txt +2 -0
- package/dist/templates/next-app/src/base/cases/DialogHandler.ts +1 -2
- package/dist/templates/next-app/src/base/cases/NavigateBridge.ts +12 -2
- package/dist/templates/next-app/src/base/cases/RequestEncryptPlugin.ts +4 -5
- package/dist/templates/next-app/src/base/cases/RouterService.ts +5 -5
- package/dist/templates/next-app/src/base/cases/UserServiceApi.ts +44 -29
- package/dist/templates/next-app/src/base/cases/ZodColumnBuilder.ts +1 -2
- package/dist/templates/next-app/src/base/port/AppApiInterface.ts +22 -0
- package/dist/templates/next-app/src/base/port/AppUserApiInterface.ts +22 -10
- package/dist/templates/next-app/src/base/port/IOCInterface.ts +9 -0
- package/dist/templates/next-app/src/base/port/UserServiceInterface.ts +17 -9
- package/dist/templates/next-app/src/base/services/ResourceService.ts +3 -4
- package/dist/templates/next-app/src/base/services/UserService.ts +37 -13
- package/dist/templates/next-app/src/base/services/appApi/AppApiRequester.ts +8 -7
- package/dist/templates/next-app/src/base/services/appApi/AppUserApi.ts +15 -26
- package/dist/templates/next-app/src/base/types/{PageProps.ts → AppPageRouter.ts} +4 -1
- package/dist/templates/next-app/src/base/types/PagesRouter.ts +9 -0
- package/dist/templates/next-app/src/core/bootstraps/BootstrapClient.ts +19 -5
- package/dist/templates/next-app/src/core/bootstraps/BootstrapServer.ts +2 -2
- package/dist/templates/next-app/src/core/bootstraps/BootstrapsRegistry.ts +0 -1
- package/dist/templates/next-app/src/core/clientIoc/ClientIOC.ts +33 -11
- package/dist/templates/next-app/src/core/clientIoc/ClientIOCRegister.ts +8 -5
- package/dist/templates/next-app/src/core/globals.ts +2 -1
- package/dist/templates/next-app/src/core/serverIoc/ServerIOC.ts +29 -10
- package/dist/templates/next-app/src/core/serverIoc/ServerIOCRegister.ts +6 -7
- package/dist/templates/next-app/src/i18n/loadMessages.ts +103 -0
- package/dist/templates/next-app/src/i18n/request.ts +3 -22
- package/dist/templates/next-app/src/pages/[locale]/about.tsx +61 -0
- package/dist/templates/next-app/src/pages/_app.tsx +50 -0
- package/dist/templates/next-app/src/pages/_document.tsx +13 -0
- package/dist/templates/next-app/src/{middleware.ts → proxy.ts} +2 -1
- package/dist/templates/next-app/src/server/AppPageRouteParams.ts +94 -0
- package/dist/templates/next-app/src/server/NextApiServer.ts +53 -0
- package/dist/templates/next-app/src/server/PagesRouteParams.ts +136 -0
- package/dist/templates/next-app/src/server/{sqlBridges/SupabaseBridge.ts → SupabaseBridge.ts} +2 -0
- package/dist/templates/next-app/src/server/UserCredentialToken.ts +1 -3
- package/dist/templates/next-app/src/server/controllers/AdminLocalesController.ts +74 -0
- package/dist/templates/next-app/src/server/controllers/AdminUserController.ts +39 -0
- package/dist/templates/next-app/src/server/controllers/LocalesController.ts +33 -0
- package/dist/templates/next-app/src/server/controllers/UserController.ts +77 -0
- package/dist/templates/next-app/src/server/port/AIControllerInterface.ts +8 -0
- package/dist/templates/next-app/src/server/port/AdminLocalesControllerInterface.ts +21 -0
- package/dist/templates/next-app/src/server/port/AdminUserControllerInterface.ts +11 -0
- package/dist/templates/next-app/src/server/port/LocalesControllerInterface.ts +10 -0
- package/dist/templates/next-app/src/server/port/{ParamsHandlerInterface.ts → RouteParamsnHandlerInterface.ts} +9 -2
- package/dist/templates/next-app/src/server/port/ServerInterface.ts +2 -2
- package/dist/templates/next-app/src/server/port/UserControllerInerface.ts +8 -0
- package/dist/templates/next-app/src/server/port/UserServiceInterface.ts +1 -1
- package/dist/templates/next-app/src/server/port/ValidatorInterface.ts +2 -2
- package/dist/templates/next-app/src/server/repositorys/LocalesRepository.ts +8 -2
- package/dist/templates/next-app/src/{base → server}/services/AdminLocalesService.ts +2 -2
- package/dist/templates/next-app/src/server/services/ApiLocaleService.ts +25 -10
- package/dist/templates/next-app/src/server/services/ApiUserService.ts +5 -2
- package/dist/templates/next-app/src/server/validators/LocalesValidator.ts +4 -2
- package/dist/templates/next-app/src/server/validators/LoginValidator.ts +1 -1
- package/dist/templates/next-app/src/server/validators/PaginationValidator.ts +10 -10
- package/dist/templates/next-app/src/styles/css/antd-themes/_common/_default.css +0 -44
- package/dist/templates/next-app/src/styles/css/antd-themes/_common/dark.css +0 -44
- package/dist/templates/next-app/src/styles/css/antd-themes/_common/pink.css +0 -44
- package/dist/templates/next-app/src/styles/css/index.css +1 -1
- package/dist/templates/next-app/src/styles/css/scrollbar.css +34 -0
- package/dist/templates/next-app/src/uikit/components/AdminLayout.tsx +34 -11
- package/dist/templates/next-app/src/uikit/components/BootstrapsProvider.tsx +69 -39
- package/dist/templates/next-app/src/uikit/components/ClientRootProvider.tsx +64 -0
- package/dist/templates/next-app/src/uikit/components/ClinetRenderProvider.tsx +42 -0
- package/dist/templates/next-app/src/uikit/components/IOCProvider.tsx +34 -0
- package/dist/templates/next-app/src/uikit/components/LocaleLink.tsx +1 -2
- package/dist/templates/next-app/src/uikit/components-app/AppBridge.tsx +17 -0
- package/dist/templates/next-app/src/uikit/components-app/AppRoutePage.tsx +112 -0
- package/dist/templates/next-app/src/uikit/{components → components-app}/LanguageSwitcher.tsx +15 -19
- package/dist/templates/next-app/src/uikit/{components → components-app}/ThemeSwitcher.tsx +53 -52
- package/dist/templates/next-app/src/uikit/components-pages/LanguageSwitcher.tsx +98 -0
- package/dist/templates/next-app/src/uikit/components-pages/PagesRoutePage.tsx +93 -0
- package/dist/templates/next-app/src/uikit/context/IOCContext.ts +16 -4
- package/dist/templates/next-app/src/uikit/hook/useStrictEffect.ts +32 -0
- package/dist/templates/next-app/tsconfig.json +3 -2
- package/dist/templates/react-app/tsconfig.app.json +1 -3
- package/dist/templates/react-app/tsconfig.node.json +0 -4
- package/dist/templates/react-app/tsconfig.test.json +1 -3
- package/package.json +1 -1
- package/dist/templates/next-app/src/server/PageParams.ts +0 -66
- package/dist/templates/next-app/src/uikit/components/BaseHeader.tsx +0 -80
- package/dist/templates/next-app/src/uikit/components/BaseLayout.tsx +0 -65
- package/dist/templates/next-app/src/uikit/components/ComboProvider.tsx +0 -58
- package/dist/templates/next-app/src/uikit/components/NextIntlProvider.tsx +0 -21
- /package/dist/templates/next-app/{src/app/[locale] → public}/favicon.ico +0 -0
- /package/dist/templates/next-app/src/uikit/{components → components-app}/LogoutButton.tsx +0 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useMountedClient } from '@brain-toolkit/react-kit';
|
|
4
|
+
|
|
5
|
+
export interface ClinetRenderProviderProps {
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* ClinetRenderProvider is a provider for the client components
|
|
11
|
+
*
|
|
12
|
+
*
|
|
13
|
+
* 当前组件仅用于需要客户端渲染的组件, 比如 adminLayout 等完全客户端渲染的组件
|
|
14
|
+
*
|
|
15
|
+
*
|
|
16
|
+
* @param children - The children components
|
|
17
|
+
* @returns
|
|
18
|
+
*/
|
|
19
|
+
export function ClinetRenderProvider(props: ClinetRenderProviderProps) {
|
|
20
|
+
const { children } = props;
|
|
21
|
+
|
|
22
|
+
const mounted = useMountedClient();
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<>
|
|
26
|
+
{children}
|
|
27
|
+
|
|
28
|
+
{/* 为了防止语言切换时页面闪烁, 使用一个固定定位的div, 当客户端渲染时才渲染 */}
|
|
29
|
+
{!mounted && (
|
|
30
|
+
<div
|
|
31
|
+
role="status"
|
|
32
|
+
aria-label="Loading..."
|
|
33
|
+
aria-busy="true"
|
|
34
|
+
style={{
|
|
35
|
+
zIndex: '99999 !important'
|
|
36
|
+
}}
|
|
37
|
+
className="fixed inset-0 overflow-hidden cursor-wait no-scrollbar bg-primary pointer-events-none"
|
|
38
|
+
></div>
|
|
39
|
+
)}
|
|
40
|
+
</>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
|
+
import { appConfig } from '@/core/globals';
|
|
5
|
+
import { clientIOC, IOCContext, IOCInstance } from '../context/IOCContext';
|
|
6
|
+
|
|
7
|
+
export function IOCProvider(props: { children: React.ReactNode }) {
|
|
8
|
+
/**
|
|
9
|
+
* 加载组件就立即注册
|
|
10
|
+
*
|
|
11
|
+
* 这样在渲染子组件时保证 IOC.get 正常工作
|
|
12
|
+
*
|
|
13
|
+
* 但是这样会导致注册时无法传递浏览器端的依赖, 比如 window.location.pathname
|
|
14
|
+
*
|
|
15
|
+
* - 如果有需要,可以将注册放在下面 useStrictEffect 中, 然后 IocMounted=true 时在渲染子节点
|
|
16
|
+
*
|
|
17
|
+
* **但是这样会有一个问题, 组件会重新挂载渲染,当切换语言时会闪烁**
|
|
18
|
+
*
|
|
19
|
+
* 因为页面初始化时有些组件可能已经使用了容器注入,这样就会丢失注册的依赖
|
|
20
|
+
*
|
|
21
|
+
* TODO: 这是一个需要解决的问题
|
|
22
|
+
*/
|
|
23
|
+
useMemo(() => {
|
|
24
|
+
clientIOC.register({
|
|
25
|
+
appConfig: appConfig
|
|
26
|
+
});
|
|
27
|
+
}, []);
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<IOCContext.Provider data-testid="IOCProvider" value={IOCInstance}>
|
|
31
|
+
{props.children}
|
|
32
|
+
</IOCContext.Provider>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -5,8 +5,7 @@ import type { LinkProps } from 'next/link';
|
|
|
5
5
|
import type { ReactNode } from 'react';
|
|
6
6
|
|
|
7
7
|
interface LocaleLinkProps
|
|
8
|
-
extends Omit<LinkProps, 'href'>,
|
|
9
|
-
React.HTMLAttributes<HTMLAnchorElement> {
|
|
8
|
+
extends Omit<LinkProps, 'href'>, React.HTMLAttributes<HTMLAnchorElement> {
|
|
10
9
|
href:
|
|
11
10
|
| string
|
|
12
11
|
| {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect } from 'react';
|
|
4
|
+
import { NavigateBridge } from '@/base/cases/NavigateBridge';
|
|
5
|
+
import { useRouter } from '@/i18n/routing';
|
|
6
|
+
import { useIOC } from '../hook/useIOC';
|
|
7
|
+
|
|
8
|
+
export function AppBridge() {
|
|
9
|
+
const router = useRouter();
|
|
10
|
+
const navigateBridge = useIOC(NavigateBridge);
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
navigateBridge.setUIBridge(router);
|
|
14
|
+
}, [router, navigateBridge]);
|
|
15
|
+
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { TeamOutlined } from '@ant-design/icons';
|
|
2
|
+
import { clsx } from 'clsx';
|
|
3
|
+
import { useLocale } from 'next-intl';
|
|
4
|
+
import { useMemo, type HTMLAttributes } from 'react';
|
|
5
|
+
import { AppBridge } from './AppBridge';
|
|
6
|
+
import { LanguageSwitcher } from './LanguageSwitcher';
|
|
7
|
+
import { LogoutButton } from './LogoutButton';
|
|
8
|
+
import { ThemeSwitcher } from './ThemeSwitcher';
|
|
9
|
+
import { LocaleLink } from '../components/LocaleLink';
|
|
10
|
+
|
|
11
|
+
export interface AppRoutePageTT {
|
|
12
|
+
title: string;
|
|
13
|
+
adminTitle: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface AppRoutePageProps extends HTMLAttributes<HTMLDivElement> {
|
|
17
|
+
showLogoutButton?: boolean;
|
|
18
|
+
showAdminButton?: boolean;
|
|
19
|
+
mainProps?: HTMLAttributes<HTMLElement>;
|
|
20
|
+
headerClassName?: string;
|
|
21
|
+
headerHref?: string;
|
|
22
|
+
tt: AppRoutePageTT;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* App Route Page
|
|
27
|
+
*
|
|
28
|
+
* 主要用于 /src/app 目录下页面的基础布局,包含头部、主体内容等
|
|
29
|
+
*
|
|
30
|
+
* @description
|
|
31
|
+
* - /src/app/[locale]/page.tsx
|
|
32
|
+
* - /src/app/[locale]/login/page.tsx
|
|
33
|
+
*
|
|
34
|
+
*/
|
|
35
|
+
export function AppRoutePage({
|
|
36
|
+
children,
|
|
37
|
+
showLogoutButton,
|
|
38
|
+
showAdminButton,
|
|
39
|
+
mainProps,
|
|
40
|
+
headerClassName,
|
|
41
|
+
tt,
|
|
42
|
+
headerHref = '/',
|
|
43
|
+
...props
|
|
44
|
+
}: AppRoutePageProps) {
|
|
45
|
+
const locale = useLocale();
|
|
46
|
+
const adminTitle = tt.adminTitle;
|
|
47
|
+
|
|
48
|
+
const actions = useMemo(() => {
|
|
49
|
+
return [
|
|
50
|
+
showAdminButton && (
|
|
51
|
+
<LocaleLink
|
|
52
|
+
key="admin-button"
|
|
53
|
+
href="/admin"
|
|
54
|
+
title={adminTitle}
|
|
55
|
+
locale={locale}
|
|
56
|
+
className="text-text hover:text-text-hover cursor-pointer text-lg transition-colors"
|
|
57
|
+
>
|
|
58
|
+
<TeamOutlined className="text-lg text-text" />
|
|
59
|
+
</LocaleLink>
|
|
60
|
+
),
|
|
61
|
+
|
|
62
|
+
<LanguageSwitcher key="language-switcher" />,
|
|
63
|
+
|
|
64
|
+
<ThemeSwitcher key="theme-switcher" />,
|
|
65
|
+
|
|
66
|
+
showLogoutButton && <LogoutButton key="logout-button" />
|
|
67
|
+
].filter(Boolean);
|
|
68
|
+
}, [adminTitle, showAdminButton, showLogoutButton, locale]);
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div
|
|
72
|
+
data-testid="AppRoutePage"
|
|
73
|
+
className="flex flex-col min-h-screen"
|
|
74
|
+
{...props}
|
|
75
|
+
>
|
|
76
|
+
<AppBridge />
|
|
77
|
+
<header
|
|
78
|
+
data-testid="BaseHeader"
|
|
79
|
+
className="h-14 bg-secondary border-b border-c-border sticky top-0 z-50"
|
|
80
|
+
>
|
|
81
|
+
<div
|
|
82
|
+
className={clsx(
|
|
83
|
+
'flex items-center justify-between h-full px-4 mx-auto max-w-7xl',
|
|
84
|
+
headerClassName
|
|
85
|
+
)}
|
|
86
|
+
>
|
|
87
|
+
<div className="flex items-center">
|
|
88
|
+
<LocaleLink
|
|
89
|
+
data-testid="BaseHeaderLogo"
|
|
90
|
+
title={tt.title}
|
|
91
|
+
href={headerHref}
|
|
92
|
+
locale={locale}
|
|
93
|
+
className="flex items-center hover:opacity-80 transition-opacity"
|
|
94
|
+
>
|
|
95
|
+
<span
|
|
96
|
+
data-testid="base-header-app-name"
|
|
97
|
+
className="text-lg font-semibold text-text"
|
|
98
|
+
>
|
|
99
|
+
{tt.title}
|
|
100
|
+
</span>
|
|
101
|
+
</LocaleLink>
|
|
102
|
+
</div>
|
|
103
|
+
<div className="flex items-center gap-2 md:gap-4">{actions}</div>
|
|
104
|
+
</div>
|
|
105
|
+
</header>
|
|
106
|
+
|
|
107
|
+
<main className="flex flex-1 flex-col bg-primary" {...mainProps}>
|
|
108
|
+
{children}
|
|
109
|
+
</main>
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
}
|
package/dist/templates/next-app/src/uikit/{components → components-app}/LanguageSwitcher.tsx
RENAMED
|
@@ -2,22 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
import { TranslationOutlined } from '@ant-design/icons';
|
|
4
4
|
import { Dropdown } from 'antd';
|
|
5
|
+
import { useParams } from 'next/navigation';
|
|
5
6
|
import { useLocale } from 'next-intl';
|
|
6
|
-
import { useCallback, useMemo } from 'react';
|
|
7
|
-
import type { I18nServiceLocale } from '@/base/port/I18nServiceInterface';
|
|
7
|
+
import { useCallback, useMemo, useTransition } from 'react';
|
|
8
8
|
import { usePathname, useRouter } from '@/i18n/routing';
|
|
9
9
|
import { i18nConfig } from '@config/i18n';
|
|
10
10
|
import type { LocaleType } from '@config/i18n';
|
|
11
|
-
import { I } from '@config/IOCIdentifier';
|
|
12
|
-
import { useIOC } from '../hook/useIOC';
|
|
13
11
|
import type { ItemType } from 'antd/es/menu/interface';
|
|
14
12
|
|
|
15
13
|
export function LanguageSwitcher() {
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
const
|
|
14
|
+
const pathname = usePathname();
|
|
15
|
+
const router = useRouter();
|
|
16
|
+
const currentLocale = useLocale() as LocaleType;
|
|
17
|
+
const [isPending, startTransition] = useTransition();
|
|
18
|
+
const params = useParams();
|
|
21
19
|
|
|
22
20
|
const options: ItemType[] = useMemo(() => {
|
|
23
21
|
return i18nConfig.supportedLngs.map(
|
|
@@ -34,18 +32,16 @@ export function LanguageSwitcher() {
|
|
|
34
32
|
|
|
35
33
|
const handleLanguageChange = useCallback(
|
|
36
34
|
async (value: string) => {
|
|
37
|
-
|
|
38
|
-
document.cookie = `NEXT_LOCALE=${value}; path=/; max-age=31536000; SameSite=Lax`;
|
|
39
|
-
// Route to the same page in the selected locale
|
|
40
|
-
router.replace(pathname, { locale: value });
|
|
35
|
+
if (isPending) return;
|
|
41
36
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
37
|
+
startTransition(() => {
|
|
38
|
+
// @ts-expect-error -- TypeScript will validate that only known `params`
|
|
39
|
+
// are used in combination with a given `pathname`. Since the two will
|
|
40
|
+
// always match for the current route, we can skip runtime checks.
|
|
41
|
+
router.replace({ pathname, params }, { locale: value });
|
|
42
|
+
});
|
|
47
43
|
},
|
|
48
|
-
[
|
|
44
|
+
[pathname, router, isPending, params]
|
|
49
45
|
);
|
|
50
46
|
|
|
51
47
|
const nextLocale = useMemo(() => {
|
|
@@ -14,11 +14,20 @@ import { useMountedClient } from '@brain-toolkit/react-kit';
|
|
|
14
14
|
import { Dropdown } from 'antd';
|
|
15
15
|
import { clsx } from 'clsx';
|
|
16
16
|
import { useTheme } from 'next-themes';
|
|
17
|
-
import { useMemo } from 'react';
|
|
17
|
+
import { useEffect, useMemo } from 'react';
|
|
18
|
+
import {
|
|
19
|
+
COMMON_THEME_DARK,
|
|
20
|
+
COMMON_THEME_DEFAULT,
|
|
21
|
+
COMMON_THEME_LIGHT,
|
|
22
|
+
COMMON_THEME_PINK
|
|
23
|
+
} from '@config/Identifier';
|
|
24
|
+
import { I } from '@config/IOCIdentifier';
|
|
18
25
|
import { type SupportedTheme, themeConfig } from '@config/theme';
|
|
26
|
+
import { useIOC } from '../hook/useIOC';
|
|
27
|
+
import { useWarnTranslations } from '../hook/useWarnTranslations';
|
|
19
28
|
import type { ItemType } from 'antd/es/menu/interface';
|
|
20
29
|
|
|
21
|
-
const { supportedThemes } = themeConfig;
|
|
30
|
+
const { supportedThemes, storageKey } = themeConfig;
|
|
22
31
|
|
|
23
32
|
const defaultTheme = supportedThemes[0] || 'system';
|
|
24
33
|
const themesList = ['system', ...supportedThemes];
|
|
@@ -31,71 +40,76 @@ const colorMap: Record<
|
|
|
31
40
|
normalColor: string;
|
|
32
41
|
Icon: React.ElementType;
|
|
33
42
|
SelectedIcon: React.ElementType;
|
|
34
|
-
TriggerIcon: React.ElementType;
|
|
35
43
|
}
|
|
36
44
|
> = {
|
|
37
45
|
system: {
|
|
38
|
-
i18nkey:
|
|
46
|
+
i18nkey: COMMON_THEME_DEFAULT,
|
|
39
47
|
selectedColor: 'text-text',
|
|
40
48
|
normalColor: 'text-text-secondary',
|
|
41
49
|
Icon: SettingOutlined,
|
|
42
|
-
SelectedIcon: SettingFilled
|
|
43
|
-
TriggerIcon: SettingOutlined
|
|
50
|
+
SelectedIcon: SettingFilled
|
|
44
51
|
},
|
|
45
52
|
light: {
|
|
46
|
-
i18nkey:
|
|
53
|
+
i18nkey: COMMON_THEME_LIGHT,
|
|
47
54
|
selectedColor: 'text-text',
|
|
48
55
|
normalColor: 'text-text-secondary',
|
|
49
56
|
Icon: SunOutlined,
|
|
50
|
-
SelectedIcon: SunFilled
|
|
51
|
-
TriggerIcon: SunOutlined
|
|
57
|
+
SelectedIcon: SunFilled
|
|
52
58
|
},
|
|
53
59
|
dark: {
|
|
54
|
-
i18nkey:
|
|
60
|
+
i18nkey: COMMON_THEME_DARK,
|
|
55
61
|
selectedColor: 'text-[#9333ea]',
|
|
56
62
|
normalColor: 'text-[#a855f7]',
|
|
57
63
|
Icon: MoonOutlined,
|
|
58
|
-
SelectedIcon: MoonFilled
|
|
59
|
-
TriggerIcon: MoonOutlined
|
|
64
|
+
SelectedIcon: MoonFilled
|
|
60
65
|
},
|
|
61
66
|
pink: {
|
|
62
|
-
i18nkey:
|
|
67
|
+
i18nkey: COMMON_THEME_PINK,
|
|
63
68
|
selectedColor: 'text-[#f472b6]',
|
|
64
69
|
normalColor: 'text-[#ec4899]',
|
|
65
70
|
Icon: HeartOutlined,
|
|
66
|
-
SelectedIcon: HeartFilled
|
|
67
|
-
TriggerIcon: HeartOutlined
|
|
71
|
+
SelectedIcon: HeartFilled
|
|
68
72
|
}
|
|
69
73
|
};
|
|
70
74
|
|
|
71
75
|
export function ThemeSwitcher() {
|
|
72
76
|
const { theme: currentTheme, resolvedTheme, setTheme } = useTheme();
|
|
73
77
|
const mounted = useMountedClient();
|
|
78
|
+
const cookieStorage = useIOC(I.CookieStorage);
|
|
79
|
+
const t = useWarnTranslations();
|
|
80
|
+
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (resolvedTheme) {
|
|
83
|
+
cookieStorage.setItem(storageKey, resolvedTheme);
|
|
84
|
+
}
|
|
85
|
+
}, [resolvedTheme, cookieStorage]);
|
|
74
86
|
|
|
75
|
-
const themeOptions =
|
|
76
|
-
|
|
77
|
-
|
|
87
|
+
const themeOptions = useMemo(() => {
|
|
88
|
+
return themesList.map((themeName) => {
|
|
89
|
+
const { i18nkey, selectedColor, normalColor, Icon, SelectedIcon } =
|
|
90
|
+
colorMap[themeName] || colorMap.light;
|
|
78
91
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
92
|
+
const isCurrentTheme =
|
|
93
|
+
currentTheme === themeName ||
|
|
94
|
+
(themeName === resolvedTheme && currentTheme === 'system');
|
|
82
95
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
96
|
+
return {
|
|
97
|
+
key: themeName,
|
|
98
|
+
value: themeName,
|
|
99
|
+
label: (
|
|
100
|
+
<div
|
|
101
|
+
className={clsx(
|
|
102
|
+
'flex items-center gap-2',
|
|
103
|
+
isCurrentTheme ? selectedColor : normalColor
|
|
104
|
+
)}
|
|
105
|
+
>
|
|
106
|
+
{isCurrentTheme ? <SelectedIcon /> : <Icon />}
|
|
107
|
+
<span>{t(i18nkey)}</span>
|
|
108
|
+
</div>
|
|
109
|
+
)
|
|
110
|
+
} as ItemType;
|
|
111
|
+
});
|
|
112
|
+
}, [currentTheme, resolvedTheme, t]);
|
|
99
113
|
|
|
100
114
|
const nextTheme = useMemo(() => {
|
|
101
115
|
if (!currentTheme) {
|
|
@@ -106,26 +120,13 @@ export function ThemeSwitcher() {
|
|
|
106
120
|
return supportedThemes[targetIndex % supportedThemes.length];
|
|
107
121
|
}, [currentTheme]);
|
|
108
122
|
|
|
109
|
-
const TriggerIcon = colorMap[currentTheme || defaultTheme].TriggerIcon;
|
|
110
|
-
|
|
111
|
-
if (!mounted) {
|
|
112
|
-
return (
|
|
113
|
-
<span
|
|
114
|
-
data-testid="ThemeSwitcher"
|
|
115
|
-
className="text-text hover:text-text-hover cursor-pointer text-lg transition-colors"
|
|
116
|
-
>
|
|
117
|
-
<SettingOutlined />
|
|
118
|
-
</span>
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
123
|
return (
|
|
123
124
|
<Dropdown
|
|
124
125
|
data-testid="ThemeSwitcherDropdown"
|
|
125
126
|
trigger={['hover']}
|
|
126
127
|
menu={{
|
|
127
128
|
items: themeOptions,
|
|
128
|
-
selectedKeys: [
|
|
129
|
+
selectedKeys: mounted ? [resolvedTheme!] : undefined,
|
|
129
130
|
onClick: ({ key }) => {
|
|
130
131
|
setTheme(key);
|
|
131
132
|
}
|
|
@@ -136,7 +137,7 @@ export function ThemeSwitcher() {
|
|
|
136
137
|
className="text-text hover:text-text-hover cursor-pointer text-lg transition-colors"
|
|
137
138
|
onClick={() => setTheme(nextTheme)}
|
|
138
139
|
>
|
|
139
|
-
<
|
|
140
|
+
<SunOutlined />
|
|
140
141
|
</span>
|
|
141
142
|
</Dropdown>
|
|
142
143
|
);
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { TranslationOutlined } from '@ant-design/icons';
|
|
4
|
+
import { Dropdown } from 'antd';
|
|
5
|
+
import { useRouter } from 'next/router';
|
|
6
|
+
import { useLocale } from 'next-intl';
|
|
7
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
8
|
+
import { useLocaleRoutes } from '@config/common';
|
|
9
|
+
import { i18nConfig } from '@config/i18n';
|
|
10
|
+
import type { LocaleType } from '@config/i18n';
|
|
11
|
+
import type { ItemType } from 'antd/es/menu/interface';
|
|
12
|
+
|
|
13
|
+
export function LanguageSwitcher() {
|
|
14
|
+
const router = useRouter();
|
|
15
|
+
const currentLocale = useLocale() as LocaleType;
|
|
16
|
+
const [isPending, setIsPending] = useState(false);
|
|
17
|
+
|
|
18
|
+
const options: ItemType[] = useMemo(() => {
|
|
19
|
+
return i18nConfig.supportedLngs.map(
|
|
20
|
+
(lang) =>
|
|
21
|
+
({
|
|
22
|
+
type: 'item',
|
|
23
|
+
key: lang,
|
|
24
|
+
value: lang,
|
|
25
|
+
label:
|
|
26
|
+
i18nConfig.localeNames[lang as keyof typeof i18nConfig.localeNames]
|
|
27
|
+
}) as ItemType
|
|
28
|
+
);
|
|
29
|
+
}, []);
|
|
30
|
+
|
|
31
|
+
const handleLanguageChange = useCallback(
|
|
32
|
+
async (value: string) => {
|
|
33
|
+
if (isPending || value === currentLocale) return;
|
|
34
|
+
|
|
35
|
+
setIsPending(true);
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
// Get current path
|
|
39
|
+
let newPath = router.asPath;
|
|
40
|
+
|
|
41
|
+
if (useLocaleRoutes) {
|
|
42
|
+
// Replace locale in path (e.g., /en/about -> /zh/about)
|
|
43
|
+
const pathWithoutLocale = newPath.replace(
|
|
44
|
+
new RegExp(`^/${currentLocale}(/|$)`),
|
|
45
|
+
'/'
|
|
46
|
+
);
|
|
47
|
+
// Remove leading slash if path is root
|
|
48
|
+
const cleanPath = pathWithoutLocale === '/' ? '' : pathWithoutLocale;
|
|
49
|
+
newPath = `/${value}${cleanPath}`;
|
|
50
|
+
} else {
|
|
51
|
+
// If not using locale routes, just update query param
|
|
52
|
+
newPath = router.pathname;
|
|
53
|
+
router.replace({
|
|
54
|
+
pathname: router.pathname,
|
|
55
|
+
query: { ...router.query, locale: value }
|
|
56
|
+
});
|
|
57
|
+
setIsPending(false);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Replace the route
|
|
62
|
+
router.replace(newPath);
|
|
63
|
+
} finally {
|
|
64
|
+
setIsPending(false);
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
[router, currentLocale, isPending]
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const nextLocale = useMemo(() => {
|
|
71
|
+
const targetIndex = i18nConfig.supportedLngs.indexOf(currentLocale) + 1;
|
|
72
|
+
return i18nConfig.supportedLngs[
|
|
73
|
+
targetIndex % i18nConfig.supportedLngs.length
|
|
74
|
+
];
|
|
75
|
+
}, [currentLocale]);
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<Dropdown
|
|
79
|
+
data-testid="LanguageSwitcherDropdown"
|
|
80
|
+
trigger={['hover']}
|
|
81
|
+
menu={{
|
|
82
|
+
selectedKeys: [currentLocale],
|
|
83
|
+
items: options,
|
|
84
|
+
onClick: ({ key }) => {
|
|
85
|
+
handleLanguageChange(key);
|
|
86
|
+
}
|
|
87
|
+
}}
|
|
88
|
+
>
|
|
89
|
+
<span
|
|
90
|
+
data-testid="LanguageSwitcher"
|
|
91
|
+
className="text-text hover:text-text-hover cursor-pointer text-lg transition-colors"
|
|
92
|
+
onClick={() => handleLanguageChange(nextLocale)}
|
|
93
|
+
>
|
|
94
|
+
<TranslationOutlined />
|
|
95
|
+
</span>
|
|
96
|
+
</Dropdown>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { TeamOutlined } from '@ant-design/icons';
|
|
2
|
+
import { clsx } from 'clsx';
|
|
3
|
+
import { useLocale } from 'next-intl';
|
|
4
|
+
import { useMemo } from 'react';
|
|
5
|
+
import { LanguageSwitcher } from './LanguageSwitcher';
|
|
6
|
+
import { LocaleLink } from '../components/LocaleLink';
|
|
7
|
+
import { ThemeSwitcher } from '../components-app/ThemeSwitcher';
|
|
8
|
+
import type { AppRoutePageProps } from '../components-app/AppRoutePage';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* App Route Page
|
|
12
|
+
*
|
|
13
|
+
* 主要用于 /src/app 目录下页面的基础布局,包含头部、主体内容等
|
|
14
|
+
*
|
|
15
|
+
* @description
|
|
16
|
+
* - /src/app/[locale]/page.tsx
|
|
17
|
+
* - /src/app/[locale]/login/page.tsx
|
|
18
|
+
*
|
|
19
|
+
*/
|
|
20
|
+
export function PagesRoutePage({
|
|
21
|
+
children,
|
|
22
|
+
showAdminButton,
|
|
23
|
+
mainProps,
|
|
24
|
+
headerClassName,
|
|
25
|
+
tt,
|
|
26
|
+
headerHref = '/',
|
|
27
|
+
...props
|
|
28
|
+
}: AppRoutePageProps) {
|
|
29
|
+
const locale = useLocale();
|
|
30
|
+
const adminTitle = tt.adminTitle;
|
|
31
|
+
|
|
32
|
+
const actions = useMemo(() => {
|
|
33
|
+
return [
|
|
34
|
+
showAdminButton && (
|
|
35
|
+
<LocaleLink
|
|
36
|
+
key="admin-button"
|
|
37
|
+
href="/admin"
|
|
38
|
+
title={adminTitle}
|
|
39
|
+
locale={locale}
|
|
40
|
+
className="text-text hover:text-text-hover cursor-pointer text-lg transition-colors"
|
|
41
|
+
>
|
|
42
|
+
<TeamOutlined className="text-lg text-text" />
|
|
43
|
+
</LocaleLink>
|
|
44
|
+
),
|
|
45
|
+
|
|
46
|
+
<LanguageSwitcher key="language-switcher" />,
|
|
47
|
+
|
|
48
|
+
<ThemeSwitcher key="theme-switcher" />
|
|
49
|
+
].filter(Boolean);
|
|
50
|
+
}, [adminTitle, showAdminButton, locale]);
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div
|
|
54
|
+
data-testid="AppRoutePage"
|
|
55
|
+
className="flex flex-col min-h-screen"
|
|
56
|
+
{...props}
|
|
57
|
+
>
|
|
58
|
+
<header
|
|
59
|
+
data-testid="BaseHeader"
|
|
60
|
+
className="h-14 bg-secondary border-b border-c-border sticky top-0 z-50"
|
|
61
|
+
>
|
|
62
|
+
<div
|
|
63
|
+
className={clsx(
|
|
64
|
+
'flex items-center justify-between h-full px-4 mx-auto max-w-7xl',
|
|
65
|
+
headerClassName
|
|
66
|
+
)}
|
|
67
|
+
>
|
|
68
|
+
<div className="flex items-center">
|
|
69
|
+
<LocaleLink
|
|
70
|
+
data-testid="BaseHeaderLogo"
|
|
71
|
+
title={tt.title}
|
|
72
|
+
href={headerHref}
|
|
73
|
+
locale={locale}
|
|
74
|
+
className="flex items-center hover:opacity-80 transition-opacity"
|
|
75
|
+
>
|
|
76
|
+
<span
|
|
77
|
+
data-testid="base-header-app-name"
|
|
78
|
+
className="text-lg font-semibold text-text"
|
|
79
|
+
>
|
|
80
|
+
{tt.title}
|
|
81
|
+
</span>
|
|
82
|
+
</LocaleLink>
|
|
83
|
+
</div>
|
|
84
|
+
<div className="flex items-center gap-2 md:gap-4">{actions}</div>
|
|
85
|
+
</div>
|
|
86
|
+
</header>
|
|
87
|
+
|
|
88
|
+
<main className="flex flex-1 flex-col bg-primary" {...mainProps}>
|
|
89
|
+
{children}
|
|
90
|
+
</main>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import {
|
|
4
|
+
type IOCContainerInterface,
|
|
5
|
+
type IOCFunctionInterface
|
|
6
|
+
} from '@qlover/corekit-bridge';
|
|
3
7
|
import { createContext } from 'react';
|
|
8
|
+
import { ClientIOC } from '@/core/clientIoc/ClientIOC';
|
|
4
9
|
import type { IOCIdentifierMap } from '@config/IOCIdentifier';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
10
|
+
|
|
11
|
+
// export const IOCInstance = new ClientIOC();
|
|
12
|
+
|
|
13
|
+
// export const IOCContext =
|
|
14
|
+
// createContext<IOCInterface<IOCIdentifierMap, IOCContainerInterface>>(
|
|
15
|
+
// IOCInstance
|
|
16
|
+
// );
|
|
17
|
+
|
|
18
|
+
export const clientIOC = new ClientIOC();
|
|
19
|
+
|
|
20
|
+
export const IOCInstance = clientIOC.create();
|
|
9
21
|
|
|
10
22
|
export const IOCContext = createContext<IOCFunctionInterface<
|
|
11
23
|
IOCIdentifierMap,
|