@qlover/create-app 0.7.6 → 0.7.8
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 +328 -0
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/templates/next-app/.env.template +22 -0
- package/dist/templates/next-app/.prettierignore +58 -0
- package/dist/templates/next-app/README.md +36 -0
- package/dist/templates/next-app/build/generateLocales.ts +25 -0
- package/dist/templates/next-app/config/IOCIdentifier.ts +58 -0
- package/dist/templates/next-app/config/Identifier/common.error.ts +41 -0
- package/dist/templates/next-app/config/Identifier/common.ts +62 -0
- package/dist/templates/next-app/config/Identifier/index.ts +10 -0
- package/dist/templates/next-app/config/Identifier/page.about.ts +181 -0
- package/dist/templates/next-app/config/Identifier/page.executor.ts +272 -0
- package/dist/templates/next-app/config/Identifier/page.home.ts +63 -0
- package/dist/templates/next-app/config/Identifier/page.identifiter.ts +39 -0
- package/dist/templates/next-app/config/Identifier/page.jsonStorage.ts +72 -0
- package/dist/templates/next-app/config/Identifier/page.login.ts +165 -0
- package/dist/templates/next-app/config/Identifier/page.register.ts +147 -0
- package/dist/templates/next-app/config/Identifier/page.request.ts +182 -0
- package/dist/templates/next-app/config/common.ts +34 -0
- package/dist/templates/next-app/config/i18n/PageI18nInterface.ts +51 -0
- package/dist/templates/next-app/config/i18n/i18nConfig.ts +12 -0
- package/dist/templates/next-app/config/i18n/index.ts +3 -0
- package/dist/templates/next-app/config/i18n/loginI18n.ts +42 -0
- package/dist/templates/next-app/config/theme.ts +23 -0
- package/dist/templates/next-app/docs/env.md +94 -0
- package/dist/templates/next-app/eslint.config.mjs +181 -0
- package/dist/templates/next-app/next.config.ts +21 -0
- package/dist/templates/next-app/package.json +58 -0
- package/dist/templates/next-app/postcss.config.mjs +5 -0
- package/dist/templates/next-app/public/file.svg +1 -0
- package/dist/templates/next-app/public/globe.svg +1 -0
- package/dist/templates/next-app/public/locales/en.json +184 -0
- package/dist/templates/next-app/public/locales/zh.json +184 -0
- package/dist/templates/next-app/public/next.svg +1 -0
- package/dist/templates/next-app/public/vercel.svg +1 -0
- package/dist/templates/next-app/public/window.svg +1 -0
- package/dist/templates/next-app/src/app/[locale]/favicon.ico +0 -0
- package/dist/templates/next-app/src/app/[locale]/layout.tsx +32 -0
- package/dist/templates/next-app/src/app/[locale]/login/FeatureItem.tsx +13 -0
- package/dist/templates/next-app/src/app/[locale]/login/LoginForm.tsx +114 -0
- package/dist/templates/next-app/src/app/[locale]/login/page.tsx +86 -0
- package/dist/templates/next-app/src/app/[locale]/not-found.tsx +24 -0
- package/dist/templates/next-app/src/app/[locale]/page.tsx +119 -0
- package/dist/templates/next-app/src/base/cases/AppConfig.ts +15 -0
- package/dist/templates/next-app/src/base/cases/DialogHandler.ts +92 -0
- package/dist/templates/next-app/src/base/cases/InversifyContainer.ts +33 -0
- package/dist/templates/next-app/src/base/cases/NavigateBridge.ts +16 -0
- package/dist/templates/next-app/src/base/cases/PageParams.ts +74 -0
- package/dist/templates/next-app/src/base/cases/RouterService.ts +35 -0
- package/dist/templates/next-app/src/base/cases/ServerAuth.ts +17 -0
- package/dist/templates/next-app/src/base/cases/ServerErrorHandler.ts +27 -0
- package/dist/templates/next-app/src/base/port/I18nServiceInterface.ts +25 -0
- package/dist/templates/next-app/src/base/port/IOCInterface.ts +24 -0
- package/dist/templates/next-app/src/base/port/ParamsHandlerInterface.ts +11 -0
- package/dist/templates/next-app/src/base/port/RouterInterface.ts +11 -0
- package/dist/templates/next-app/src/base/port/ServerAuthInterface.ts +3 -0
- package/dist/templates/next-app/src/base/port/ServerInterface.ts +12 -0
- package/dist/templates/next-app/src/base/port/UserServiceInterface.ts +11 -0
- package/dist/templates/next-app/src/base/services/I18nService.ts +115 -0
- package/dist/templates/next-app/src/base/services/UserService.ts +23 -0
- package/dist/templates/next-app/src/base/types/PageProps.ts +9 -0
- package/dist/templates/next-app/src/core/bootstraps/BootstrapClient.ts +61 -0
- package/dist/templates/next-app/src/core/bootstraps/BootstrapServer.ts +78 -0
- package/dist/templates/next-app/src/core/bootstraps/BootstrapsRegistry.ts +47 -0
- package/dist/templates/next-app/src/core/bootstraps/IocIdentifierTest.ts +26 -0
- package/dist/templates/next-app/src/core/bootstraps/PrintBootstrap.ts +18 -0
- package/dist/templates/next-app/src/core/clientIoc/ClientIOC.ts +37 -0
- package/dist/templates/next-app/src/core/clientIoc/ClientIOCRegister.ts +97 -0
- package/dist/templates/next-app/src/core/globals.ts +24 -0
- package/dist/templates/next-app/src/core/serverIoc/ServerIOC.ts +52 -0
- package/dist/templates/next-app/src/core/serverIoc/ServerIOCRegister.ts +63 -0
- package/dist/templates/next-app/src/i18n/request.ts +21 -0
- package/dist/templates/next-app/src/i18n/routing.ts +30 -0
- package/dist/templates/next-app/src/middleware.ts +25 -0
- package/dist/templates/next-app/src/styles/css/antd-themes/_default.css +239 -0
- package/dist/templates/next-app/src/styles/css/antd-themes/dark.css +178 -0
- package/dist/templates/next-app/src/styles/css/antd-themes/index.css +3 -0
- package/dist/templates/next-app/src/styles/css/antd-themes/no-context.css +34 -0
- package/dist/templates/next-app/src/styles/css/antd-themes/pink.css +204 -0
- package/dist/templates/next-app/src/styles/css/index.css +6 -0
- package/dist/templates/next-app/src/styles/css/page.css +19 -0
- package/dist/templates/next-app/src/styles/css/tailwind.css +5 -0
- package/dist/templates/next-app/src/styles/css/themes/_default.css +29 -0
- package/dist/templates/next-app/src/styles/css/themes/dark.css +29 -0
- package/dist/templates/next-app/src/styles/css/themes/index.css +3 -0
- package/dist/templates/next-app/src/styles/css/themes/pink.css +29 -0
- package/dist/templates/next-app/src/styles/css/zIndex.css +9 -0
- package/dist/templates/next-app/src/uikit/components/BaseHeader.tsx +47 -0
- package/dist/templates/next-app/src/uikit/components/BootstrapsProvider.tsx +37 -0
- package/dist/templates/next-app/src/uikit/components/ComboProvider.tsx +50 -0
- package/dist/templates/next-app/src/uikit/components/LanguageSwitcher.tsx +52 -0
- package/dist/templates/next-app/src/uikit/components/LocaleLink.tsx +51 -0
- package/dist/templates/next-app/src/uikit/components/LogoutButton.tsx +34 -0
- package/dist/templates/next-app/src/uikit/components/NextIntlProvider.tsx +21 -0
- package/dist/templates/next-app/src/uikit/components/ThemeSwitcher.tsx +99 -0
- package/dist/templates/next-app/src/uikit/context/IOCContext.ts +13 -0
- package/dist/templates/next-app/src/uikit/hook/useI18nInterface.ts +28 -0
- package/dist/templates/next-app/src/uikit/hook/useIOC.ts +37 -0
- package/dist/templates/next-app/src/uikit/hook/useMountedClient.ts +11 -0
- package/dist/templates/next-app/src/uikit/hook/useStore.ts +15 -0
- package/dist/templates/next-app/tailwind.config.ts +8 -0
- package/dist/templates/next-app/tsconfig.json +36 -0
- package/package.json +1 -1
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/* theme variables - for tailwind */
|
|
2
|
+
@layer base {
|
|
3
|
+
:root {
|
|
4
|
+
/* 基础背景色 */
|
|
5
|
+
--color-bg-base: 255 255 255;
|
|
6
|
+
--color-bg-secondary: 241 245 249; /* slate-100 */
|
|
7
|
+
--color-bg-elevated: 248 250 252; /* slate-50 */
|
|
8
|
+
|
|
9
|
+
/* 文字颜色 */
|
|
10
|
+
--text-primary: 15 23 42; /* slate-900 */
|
|
11
|
+
--text-secondary: 71 85 105; /* slate-600 */
|
|
12
|
+
--text-tertiary: 100 116 139; /* slate-500 */
|
|
13
|
+
|
|
14
|
+
/* 边框颜色 */
|
|
15
|
+
--color-border: 226 232 240; /* slate-200 */
|
|
16
|
+
|
|
17
|
+
/* 品牌色 */
|
|
18
|
+
--color-brand: 37 99 235; /* blue-600 */
|
|
19
|
+
--color-brand-hover: 59 130 246; /* blue-500 */
|
|
20
|
+
|
|
21
|
+
/* 登录页特定样式 */
|
|
22
|
+
--login-card-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
|
23
|
+
--login-input-bg: var(--fe-color-bg-container);
|
|
24
|
+
--login-input-border: var(--fe-color-border);
|
|
25
|
+
--login-button-text: rgb(255 255 255);
|
|
26
|
+
--login-social-button-bg: var(--fe-color-bg-container);
|
|
27
|
+
--login-social-button-border: var(--fe-color-border);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/* theme variables - for tailwind */
|
|
2
|
+
@layer base {
|
|
3
|
+
[data-theme='dark'] {
|
|
4
|
+
/* 基础背景色 */
|
|
5
|
+
--color-bg-base: 15 23 42; /* slate-900 */
|
|
6
|
+
--color-bg-secondary: 30 41 59; /* slate-800 */
|
|
7
|
+
--color-bg-elevated: 51 65 85; /* slate-700 */
|
|
8
|
+
|
|
9
|
+
/* 文字颜色 */
|
|
10
|
+
--text-primary: 255 255 255;
|
|
11
|
+
--text-secondary: 148 163 184; /* slate-400 */
|
|
12
|
+
--text-tertiary: 100 116 139; /* slate-500 */
|
|
13
|
+
|
|
14
|
+
/* 边框颜色 */
|
|
15
|
+
--color-border: 51 65 85; /* slate-700 */
|
|
16
|
+
|
|
17
|
+
/* 品牌色 */
|
|
18
|
+
--color-brand: 37 99 235; /* blue-600 */
|
|
19
|
+
--color-brand-hover: 59 130 246; /* blue-500 */
|
|
20
|
+
|
|
21
|
+
/* 登录页特定样式 */
|
|
22
|
+
--login-card-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
23
|
+
--login-input-bg: var(--fe-color-bg-container);
|
|
24
|
+
--login-input-border: var(--fe-color-border);
|
|
25
|
+
--login-button-text: rgb(255 255 255);
|
|
26
|
+
--login-social-button-bg: var(--fe-color-bg-container);
|
|
27
|
+
--login-social-button-border: var(--fe-color-border);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/* theme variables - for tailwind */
|
|
2
|
+
@layer base {
|
|
3
|
+
[data-theme='pink'] {
|
|
4
|
+
/* 基础背景色 */
|
|
5
|
+
--color-bg-base: 255 241 242; /* rose-50 */
|
|
6
|
+
--color-bg-secondary: 255 228 230; /* rose-100 */
|
|
7
|
+
--color-bg-elevated: 254 205 211; /* rose-200 */
|
|
8
|
+
|
|
9
|
+
/* 文字颜色 */
|
|
10
|
+
--text-primary: 190 18 60; /* rose-700 */
|
|
11
|
+
--text-secondary: 225 29 72; /* rose-600 */
|
|
12
|
+
--text-tertiary: 244 63 94; /* rose-500 */
|
|
13
|
+
|
|
14
|
+
/* 边框颜色 */
|
|
15
|
+
--color-border: 254 205 211; /* rose-200 */
|
|
16
|
+
|
|
17
|
+
/* 品牌色 */
|
|
18
|
+
--color-brand: 225 29 72; /* rose-600 */
|
|
19
|
+
--color-brand-hover: 244 63 94; /* rose-500 */
|
|
20
|
+
|
|
21
|
+
/* 登录页特定样式 */
|
|
22
|
+
--login-card-shadow: 0 4px 12px rgba(244 63 94 / 0.15);
|
|
23
|
+
--login-input-bg: var(--ant-color-bg-container);
|
|
24
|
+
--login-input-border: var(--ant-color-border);
|
|
25
|
+
--login-button-text: rgb(255 255 255);
|
|
26
|
+
--login-social-button-bg: var(--ant-color-bg-container);
|
|
27
|
+
--login-social-button-border: var(--ant-color-border);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import Link from 'next/link';
|
|
4
|
+
import { IOCIdentifier } from '@config/IOCIdentifier';
|
|
5
|
+
import { useIOC } from '@/uikit/hook/useIOC';
|
|
6
|
+
import { LanguageSwitcher } from './LanguageSwitcher';
|
|
7
|
+
import { LogoutButton } from './LogoutButton';
|
|
8
|
+
import { ThemeSwitcher } from './ThemeSwitcher';
|
|
9
|
+
|
|
10
|
+
export function BaseHeader(props: { showLogoutButton?: boolean }) {
|
|
11
|
+
const { showLogoutButton } = props;
|
|
12
|
+
const appConfig = useIOC(IOCIdentifier.AppConfig);
|
|
13
|
+
return (
|
|
14
|
+
<header
|
|
15
|
+
data-testid="base-header"
|
|
16
|
+
className="h-14 bg-secondary border-b border-border sticky top-0 z-50"
|
|
17
|
+
>
|
|
18
|
+
<div className="flex items-center justify-between h-full px-4 mx-auto max-w-7xl">
|
|
19
|
+
<div className="flex items-center">
|
|
20
|
+
<Link
|
|
21
|
+
href="/"
|
|
22
|
+
className="flex items-center hover:opacity-80 transition-opacity"
|
|
23
|
+
>
|
|
24
|
+
{/* <img
|
|
25
|
+
data-testid="base-header-logo"
|
|
26
|
+
src={IOC(PublicAssetsPath).getPath('/logo.svg')}
|
|
27
|
+
alt="logo"
|
|
28
|
+
className="h-8 w-auto"
|
|
29
|
+
/> */}
|
|
30
|
+
<span
|
|
31
|
+
data-testid="base-header-app-name"
|
|
32
|
+
className="ml-2 text-lg font-semibold text-text"
|
|
33
|
+
>
|
|
34
|
+
{appConfig.appName}
|
|
35
|
+
</span>
|
|
36
|
+
</Link>
|
|
37
|
+
</div>
|
|
38
|
+
<div className="flex items-center gap-4">
|
|
39
|
+
<LanguageSwitcher />
|
|
40
|
+
<ThemeSwitcher />
|
|
41
|
+
|
|
42
|
+
{showLogoutButton && <LogoutButton />}
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</header>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import '@ant-design/v5-patch-for-react-19';
|
|
3
|
+
import { useRouter } from 'next/navigation';
|
|
4
|
+
import { useLocale } from 'next-intl';
|
|
5
|
+
import { useEffect } from 'react';
|
|
6
|
+
import { I } from '@config/IOCIdentifier';
|
|
7
|
+
import { NavigateBridge } from '@/base/cases/NavigateBridge';
|
|
8
|
+
import { BootstrapClient } from '@/core/bootstraps/BootstrapClient';
|
|
9
|
+
import { clientIOC } from '@/core/clientIoc/ClientIOC';
|
|
10
|
+
import { IOCContext } from '../context/IOCContext';
|
|
11
|
+
|
|
12
|
+
export function BootstrapsProvider(props: { children: React.ReactNode }) {
|
|
13
|
+
const IOC = clientIOC.create();
|
|
14
|
+
const locale = useLocale();
|
|
15
|
+
const router = useRouter();
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
IOC(I.RouterServiceInterface).setLocale(locale);
|
|
19
|
+
IOC(NavigateBridge).setUIBridge(router);
|
|
20
|
+
}, [locale, router, IOC]);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (typeof window !== 'undefined') {
|
|
24
|
+
BootstrapClient.main({
|
|
25
|
+
root: window,
|
|
26
|
+
pathname: window.location.pathname,
|
|
27
|
+
IOC: IOC
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}, [IOC]);
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<IOCContext.Provider data-testid="BootstrapsProvider" value={IOC}>
|
|
34
|
+
{props.children}
|
|
35
|
+
</IOCContext.Provider>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import '@ant-design/v5-patch-for-react-19';
|
|
3
|
+
import { AntdRegistry } from '@ant-design/nextjs-registry';
|
|
4
|
+
import { AntdThemeProvider } from '@brain-toolkit/antd-theme-override/react';
|
|
5
|
+
import { ThemeProvider } from 'next-themes';
|
|
6
|
+
import { IOCIdentifier } from '@config/IOCIdentifier';
|
|
7
|
+
import { clientIOC } from '@/core/clientIoc/ClientIOC';
|
|
8
|
+
import { BootstrapsProvider } from './BootstrapsProvider';
|
|
9
|
+
import type { CommonThemeConfig } from '@config/theme';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* CommonProvider is a provider for the common components
|
|
13
|
+
*
|
|
14
|
+
* - IOCProvider
|
|
15
|
+
* - BootstrapsProvider
|
|
16
|
+
* - ThemeProvider
|
|
17
|
+
* - AntdProvider
|
|
18
|
+
*
|
|
19
|
+
* @param param0
|
|
20
|
+
* @returns
|
|
21
|
+
*/
|
|
22
|
+
export function ComboProvider(props: {
|
|
23
|
+
themeConfig: CommonThemeConfig;
|
|
24
|
+
children: React.ReactNode;
|
|
25
|
+
}) {
|
|
26
|
+
const { themeConfig, children } = props;
|
|
27
|
+
|
|
28
|
+
const IOC = clientIOC.create();
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<AntdThemeProvider
|
|
32
|
+
data-testid="ComboProvider"
|
|
33
|
+
theme={themeConfig.antdTheme}
|
|
34
|
+
staticApi={IOC(IOCIdentifier.DialogHandler)}
|
|
35
|
+
>
|
|
36
|
+
<ThemeProvider
|
|
37
|
+
themes={themeConfig.supportedThemes as unknown as string[]}
|
|
38
|
+
attribute={themeConfig.domAttribute}
|
|
39
|
+
defaultTheme={themeConfig.defaultTheme}
|
|
40
|
+
enableSystem
|
|
41
|
+
enableColorScheme={false}
|
|
42
|
+
storageKey={themeConfig.storageKey}
|
|
43
|
+
>
|
|
44
|
+
<BootstrapsProvider>
|
|
45
|
+
<AntdRegistry>{children}</AntdRegistry>
|
|
46
|
+
</BootstrapsProvider>
|
|
47
|
+
</ThemeProvider>
|
|
48
|
+
</AntdThemeProvider>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Select } from 'antd';
|
|
4
|
+
import { useLocale } from 'next-intl';
|
|
5
|
+
import { useCallback } from 'react';
|
|
6
|
+
import { i18nConfig } from '@config/i18n';
|
|
7
|
+
import { IOCIdentifier } from '@config/IOCIdentifier';
|
|
8
|
+
import type { I18nServiceLocale } from '@/base/port/I18nServiceInterface';
|
|
9
|
+
import { usePathname, useRouter } from '@/i18n/routing';
|
|
10
|
+
import { useIOC } from '../hook/useIOC';
|
|
11
|
+
import { useStore } from '../hook/useStore';
|
|
12
|
+
import type { LocaleType } from '@config/i18n';
|
|
13
|
+
|
|
14
|
+
export function LanguageSwitcher() {
|
|
15
|
+
const i18nService = useIOC(IOCIdentifier.I18nServiceInterface);
|
|
16
|
+
const { loading } = useStore(i18nService);
|
|
17
|
+
const pathname = usePathname(); // current pathname, aware of i18n
|
|
18
|
+
|
|
19
|
+
const router = useRouter(); // i18n-aware router instance
|
|
20
|
+
const currentLocale = useLocale() as LocaleType; // currently active locale
|
|
21
|
+
|
|
22
|
+
const handleLanguageChange = useCallback(
|
|
23
|
+
async (value: string) => {
|
|
24
|
+
// Set a persistent cookie with the user's preferred locale (valid for 1 year)
|
|
25
|
+
document.cookie = `NEXT_LOCALE=${value}; path=/; max-age=31536000; SameSite=Lax`;
|
|
26
|
+
// Route to the same page in the selected locale
|
|
27
|
+
router.replace(pathname, { locale: value });
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
await i18nService.changeLanguage(value as I18nServiceLocale);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error('Failed to change language:', error);
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
[i18nService, pathname, router]
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<Select
|
|
40
|
+
data-testid="LanguageSwitcher"
|
|
41
|
+
loading={loading}
|
|
42
|
+
value={currentLocale}
|
|
43
|
+
onChange={handleLanguageChange}
|
|
44
|
+
options={i18nConfig.supportedLngs.map((lang) => ({
|
|
45
|
+
value: lang,
|
|
46
|
+
label:
|
|
47
|
+
i18nConfig.localeNames[lang as keyof typeof i18nConfig.localeNames]
|
|
48
|
+
}))}
|
|
49
|
+
className="w-24"
|
|
50
|
+
/>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import Link from 'next/link';
|
|
2
|
+
import { useLocaleRoutes } from '@config/common';
|
|
3
|
+
import { i18nConfig } from '@config/i18n';
|
|
4
|
+
import type { LinkProps } from 'next/link';
|
|
5
|
+
import type { ReactNode } from 'react';
|
|
6
|
+
|
|
7
|
+
interface LocaleLinkProps
|
|
8
|
+
extends Omit<LinkProps, 'href'>,
|
|
9
|
+
React.HTMLAttributes<HTMLAnchorElement> {
|
|
10
|
+
href:
|
|
11
|
+
| string
|
|
12
|
+
| {
|
|
13
|
+
pathname: string;
|
|
14
|
+
query?: Record<string, string>;
|
|
15
|
+
hash?: string;
|
|
16
|
+
};
|
|
17
|
+
locale?: string;
|
|
18
|
+
title: string;
|
|
19
|
+
children: ReactNode;
|
|
20
|
+
defaultLocale?: string;
|
|
21
|
+
className?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const LocaleLink: React.FC<LocaleLinkProps> = ({
|
|
25
|
+
href,
|
|
26
|
+
locale,
|
|
27
|
+
children,
|
|
28
|
+
defaultLocale,
|
|
29
|
+
...props
|
|
30
|
+
}) => {
|
|
31
|
+
locale = locale || i18nConfig.fallbackLng;
|
|
32
|
+
|
|
33
|
+
const isDefaultLocale = locale === defaultLocale;
|
|
34
|
+
const shouldAddLocale = useLocaleRoutes && !isDefaultLocale;
|
|
35
|
+
|
|
36
|
+
let localizedHref: typeof href;
|
|
37
|
+
if (typeof href === 'string') {
|
|
38
|
+
localizedHref = shouldAddLocale ? `/${locale}${href}` : href;
|
|
39
|
+
} else {
|
|
40
|
+
localizedHref = {
|
|
41
|
+
...href,
|
|
42
|
+
pathname: shouldAddLocale ? `/${locale}${href.pathname}` : href.pathname
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<Link data-testid="locale-link" {...props} href={localizedHref}>
|
|
48
|
+
{children}
|
|
49
|
+
</Link>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Button } from 'antd';
|
|
2
|
+
import { useCallback } from 'react';
|
|
3
|
+
import {
|
|
4
|
+
AUTH_LOGOUT_DIALOG_CONTENT,
|
|
5
|
+
AUTH_LOGOUT_DIALOG_TITLE
|
|
6
|
+
} from '@config/Identifier';
|
|
7
|
+
import { IOCIdentifier } from '@config/IOCIdentifier';
|
|
8
|
+
import { useI18nInterface } from '../hook/useI18nInterface';
|
|
9
|
+
import { useIOC } from '../hook/useIOC';
|
|
10
|
+
import type { PageI18nInterface } from '@config/i18n';
|
|
11
|
+
|
|
12
|
+
export function LogoutButton() {
|
|
13
|
+
const IOC = useIOC();
|
|
14
|
+
const tt = useI18nInterface({
|
|
15
|
+
title: AUTH_LOGOUT_DIALOG_TITLE,
|
|
16
|
+
content: AUTH_LOGOUT_DIALOG_CONTENT
|
|
17
|
+
} as PageI18nInterface);
|
|
18
|
+
|
|
19
|
+
const onClick = useCallback(() => {
|
|
20
|
+
IOC(IOCIdentifier.DialogHandler).confirm({
|
|
21
|
+
title: tt.title,
|
|
22
|
+
content: tt.content,
|
|
23
|
+
onOk: () => {
|
|
24
|
+
IOC(IOCIdentifier.UserServiceInterface).logout();
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}, [tt, IOC]);
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<Button data-testid="LogoutButton" danger onClick={onClick}>
|
|
31
|
+
{tt.title}
|
|
32
|
+
</Button>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { NextIntlClientProvider, useMessages } from 'next-intl';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
type Props = {
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
locale: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function NextIntlProvider({ children, locale }: Props) {
|
|
10
|
+
const messages = useMessages();
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<NextIntlClientProvider
|
|
14
|
+
data-testid="NextIntlProvider"
|
|
15
|
+
locale={locale}
|
|
16
|
+
messages={messages}
|
|
17
|
+
>
|
|
18
|
+
{children}
|
|
19
|
+
</NextIntlClientProvider>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
BulbOutlined,
|
|
5
|
+
BulbFilled,
|
|
6
|
+
HeartFilled,
|
|
7
|
+
HeartOutlined,
|
|
8
|
+
DesktopOutlined
|
|
9
|
+
} from '@ant-design/icons';
|
|
10
|
+
import { Select } from 'antd';
|
|
11
|
+
import { clsx } from 'clsx';
|
|
12
|
+
import { useTheme } from 'next-themes';
|
|
13
|
+
import { themeConfig } from '@config/theme';
|
|
14
|
+
import { useMountedClient } from '../hook/useMountedClient';
|
|
15
|
+
|
|
16
|
+
const { supportedThemes } = themeConfig;
|
|
17
|
+
|
|
18
|
+
const colorMap: Record<
|
|
19
|
+
string,
|
|
20
|
+
{ i18nkey: string; colors: string[]; icons: React.ElementType[] }
|
|
21
|
+
> = {
|
|
22
|
+
system: {
|
|
23
|
+
i18nkey: 'System',
|
|
24
|
+
colors: ['text-text', 'text-text-secondary'],
|
|
25
|
+
icons: [DesktopOutlined, DesktopOutlined]
|
|
26
|
+
},
|
|
27
|
+
light: {
|
|
28
|
+
i18nkey: 'Light',
|
|
29
|
+
colors: ['text-text', 'text-text-secondary'],
|
|
30
|
+
icons: [BulbFilled, BulbOutlined]
|
|
31
|
+
},
|
|
32
|
+
dark: {
|
|
33
|
+
i18nkey: 'Dark',
|
|
34
|
+
colors: ['text-[#9333ea]', 'text-[#a855f7]'],
|
|
35
|
+
icons: [BulbFilled, BulbOutlined]
|
|
36
|
+
},
|
|
37
|
+
pink: {
|
|
38
|
+
i18nkey: 'Pink',
|
|
39
|
+
colors: ['text-[#f472b6]', 'text-[#ec4899]'],
|
|
40
|
+
icons: [HeartFilled, HeartOutlined]
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export function ThemeSwitcher() {
|
|
45
|
+
const { theme, resolvedTheme, setTheme } = useTheme();
|
|
46
|
+
const mounted = useMountedClient();
|
|
47
|
+
|
|
48
|
+
// 如果组件未挂载,返回空的 Select 以避免闪烁
|
|
49
|
+
if (!mounted) {
|
|
50
|
+
return (
|
|
51
|
+
<Select
|
|
52
|
+
data-testid="ThemeSwitcher"
|
|
53
|
+
loading
|
|
54
|
+
value="system"
|
|
55
|
+
options={[]}
|
|
56
|
+
style={{ width: 120 }}
|
|
57
|
+
className="min-w-40 max-w-full"
|
|
58
|
+
disabled
|
|
59
|
+
/>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const themeOptions = ['system', ...supportedThemes!].map((themeName) => {
|
|
64
|
+
const { i18nkey, colors, icons } = colorMap[themeName] || colorMap.light;
|
|
65
|
+
const [currentColor, normalColor] = colors;
|
|
66
|
+
const [CurrentIcon, NormalIcon] = icons;
|
|
67
|
+
|
|
68
|
+
const isCurrentTheme =
|
|
69
|
+
theme === themeName ||
|
|
70
|
+
(themeName === resolvedTheme && theme === 'system');
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
key: themeName,
|
|
74
|
+
value: themeName,
|
|
75
|
+
label: (
|
|
76
|
+
<div
|
|
77
|
+
className={clsx(
|
|
78
|
+
'flex items-center gap-2',
|
|
79
|
+
isCurrentTheme ? currentColor : normalColor
|
|
80
|
+
)}
|
|
81
|
+
>
|
|
82
|
+
{isCurrentTheme ? <CurrentIcon /> : <NormalIcon />}
|
|
83
|
+
<span>{i18nkey}</span>
|
|
84
|
+
</div>
|
|
85
|
+
)
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<Select
|
|
91
|
+
data-testid="ThemeSwitcher"
|
|
92
|
+
value={theme}
|
|
93
|
+
onChange={setTheme}
|
|
94
|
+
options={themeOptions}
|
|
95
|
+
style={{ width: 120 }}
|
|
96
|
+
className="min-w-40 max-w-full"
|
|
97
|
+
/>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { createContext } from 'react';
|
|
4
|
+
import type { IOCIdentifierMap } from '@config/IOCIdentifier';
|
|
5
|
+
import type {
|
|
6
|
+
IOCContainerInterface,
|
|
7
|
+
IOCFunctionInterface
|
|
8
|
+
} from '@qlover/corekit-bridge';
|
|
9
|
+
|
|
10
|
+
export const IOCContext = createContext<IOCFunctionInterface<
|
|
11
|
+
IOCIdentifierMap,
|
|
12
|
+
IOCContainerInterface
|
|
13
|
+
> | null>(null);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useTranslations } from 'next-intl';
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import type { PageI18nInterface } from '@config/i18n/PageI18nInterface';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Hook to get the i18n interface
|
|
7
|
+
*
|
|
8
|
+
* @param i18nInterface - The i18n interface to get
|
|
9
|
+
* @returns The i18n interface
|
|
10
|
+
*/
|
|
11
|
+
export function useI18nInterface<T extends PageI18nInterface>(
|
|
12
|
+
i18nInterface: T
|
|
13
|
+
): T {
|
|
14
|
+
const t = useTranslations();
|
|
15
|
+
|
|
16
|
+
const i18n = useMemo(() => {
|
|
17
|
+
return Object.fromEntries(
|
|
18
|
+
Object.entries(i18nInterface).map(([key, value]) => {
|
|
19
|
+
if (typeof value === 'string') {
|
|
20
|
+
return [key, t(value)];
|
|
21
|
+
}
|
|
22
|
+
return [key, value];
|
|
23
|
+
})
|
|
24
|
+
) as T;
|
|
25
|
+
}, [i18nInterface, t]);
|
|
26
|
+
|
|
27
|
+
return i18n;
|
|
28
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
import { IOCContext } from '../context/IOCContext';
|
|
3
|
+
import type { IOCIdentifierMap } from '@config/IOCIdentifier';
|
|
4
|
+
import type {
|
|
5
|
+
IOCContainerInterface,
|
|
6
|
+
IOCFunctionInterface
|
|
7
|
+
} from '@qlover/corekit-bridge';
|
|
8
|
+
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
10
|
+
type IOCIdentifier = keyof IOCIdentifierMap | (new (...args: any[]) => any);
|
|
11
|
+
|
|
12
|
+
export function useIOC(): IOCFunctionInterface<
|
|
13
|
+
IOCIdentifierMap,
|
|
14
|
+
IOCContainerInterface
|
|
15
|
+
>;
|
|
16
|
+
export function useIOC<T extends IOCIdentifier>(
|
|
17
|
+
identifier: T
|
|
18
|
+
): T extends keyof IOCIdentifierMap
|
|
19
|
+
? IOCIdentifierMap[T]
|
|
20
|
+
: // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
21
|
+
T extends new (...args: any[]) => any
|
|
22
|
+
? InstanceType<T>
|
|
23
|
+
: never;
|
|
24
|
+
|
|
25
|
+
export function useIOC<T extends IOCIdentifier>(identifier?: T) {
|
|
26
|
+
const IOC = useContext(IOCContext);
|
|
27
|
+
|
|
28
|
+
if (!IOC) {
|
|
29
|
+
throw new Error('IOC is not found');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (identifier === undefined) {
|
|
33
|
+
return IOC;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return IOC(identifier);
|
|
37
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { useSliceStore, type SliceStore } from '@qlover/slice-store-react';
|
|
2
|
+
import type {
|
|
3
|
+
StoreInterface,
|
|
4
|
+
StoreStateInterface
|
|
5
|
+
} from '@qlover/corekit-bridge';
|
|
6
|
+
|
|
7
|
+
export function useStore<
|
|
8
|
+
C extends StoreInterface<StoreStateInterface>,
|
|
9
|
+
State = C['state']
|
|
10
|
+
>(store: C, selector?: (state: C['state']) => State): State {
|
|
11
|
+
return useSliceStore(
|
|
12
|
+
store as unknown as SliceStore<StoreStateInterface>,
|
|
13
|
+
selector
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2017",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"jsx": "preserve",
|
|
15
|
+
"incremental": true,
|
|
16
|
+
"experimentalDecorators": true,
|
|
17
|
+
"emitDecoratorMetadata": true,
|
|
18
|
+
"plugins": [
|
|
19
|
+
{
|
|
20
|
+
"name": "next"
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
"paths": {
|
|
24
|
+
"@/*": ["./src/*"],
|
|
25
|
+
"@config/*": ["./config/*"]
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"include": [
|
|
29
|
+
"next-env.d.ts",
|
|
30
|
+
"src/**/*.ts",
|
|
31
|
+
"src/**/*.tsx",
|
|
32
|
+
".next/types/**/*.ts",
|
|
33
|
+
"config/**/*.ts"
|
|
34
|
+
],
|
|
35
|
+
"exclude": ["node_modules"]
|
|
36
|
+
}
|