@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.
Files changed (104) hide show
  1. package/CHANGELOG.md +328 -0
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.js +1 -1
  4. package/dist/templates/next-app/.env.template +22 -0
  5. package/dist/templates/next-app/.prettierignore +58 -0
  6. package/dist/templates/next-app/README.md +36 -0
  7. package/dist/templates/next-app/build/generateLocales.ts +25 -0
  8. package/dist/templates/next-app/config/IOCIdentifier.ts +58 -0
  9. package/dist/templates/next-app/config/Identifier/common.error.ts +41 -0
  10. package/dist/templates/next-app/config/Identifier/common.ts +62 -0
  11. package/dist/templates/next-app/config/Identifier/index.ts +10 -0
  12. package/dist/templates/next-app/config/Identifier/page.about.ts +181 -0
  13. package/dist/templates/next-app/config/Identifier/page.executor.ts +272 -0
  14. package/dist/templates/next-app/config/Identifier/page.home.ts +63 -0
  15. package/dist/templates/next-app/config/Identifier/page.identifiter.ts +39 -0
  16. package/dist/templates/next-app/config/Identifier/page.jsonStorage.ts +72 -0
  17. package/dist/templates/next-app/config/Identifier/page.login.ts +165 -0
  18. package/dist/templates/next-app/config/Identifier/page.register.ts +147 -0
  19. package/dist/templates/next-app/config/Identifier/page.request.ts +182 -0
  20. package/dist/templates/next-app/config/common.ts +34 -0
  21. package/dist/templates/next-app/config/i18n/PageI18nInterface.ts +51 -0
  22. package/dist/templates/next-app/config/i18n/i18nConfig.ts +12 -0
  23. package/dist/templates/next-app/config/i18n/index.ts +3 -0
  24. package/dist/templates/next-app/config/i18n/loginI18n.ts +42 -0
  25. package/dist/templates/next-app/config/theme.ts +23 -0
  26. package/dist/templates/next-app/docs/env.md +94 -0
  27. package/dist/templates/next-app/eslint.config.mjs +181 -0
  28. package/dist/templates/next-app/next.config.ts +21 -0
  29. package/dist/templates/next-app/package.json +58 -0
  30. package/dist/templates/next-app/postcss.config.mjs +5 -0
  31. package/dist/templates/next-app/public/file.svg +1 -0
  32. package/dist/templates/next-app/public/globe.svg +1 -0
  33. package/dist/templates/next-app/public/locales/en.json +184 -0
  34. package/dist/templates/next-app/public/locales/zh.json +184 -0
  35. package/dist/templates/next-app/public/next.svg +1 -0
  36. package/dist/templates/next-app/public/vercel.svg +1 -0
  37. package/dist/templates/next-app/public/window.svg +1 -0
  38. package/dist/templates/next-app/src/app/[locale]/favicon.ico +0 -0
  39. package/dist/templates/next-app/src/app/[locale]/layout.tsx +32 -0
  40. package/dist/templates/next-app/src/app/[locale]/login/FeatureItem.tsx +13 -0
  41. package/dist/templates/next-app/src/app/[locale]/login/LoginForm.tsx +114 -0
  42. package/dist/templates/next-app/src/app/[locale]/login/page.tsx +86 -0
  43. package/dist/templates/next-app/src/app/[locale]/not-found.tsx +24 -0
  44. package/dist/templates/next-app/src/app/[locale]/page.tsx +119 -0
  45. package/dist/templates/next-app/src/base/cases/AppConfig.ts +15 -0
  46. package/dist/templates/next-app/src/base/cases/DialogHandler.ts +92 -0
  47. package/dist/templates/next-app/src/base/cases/InversifyContainer.ts +33 -0
  48. package/dist/templates/next-app/src/base/cases/NavigateBridge.ts +16 -0
  49. package/dist/templates/next-app/src/base/cases/PageParams.ts +74 -0
  50. package/dist/templates/next-app/src/base/cases/RouterService.ts +35 -0
  51. package/dist/templates/next-app/src/base/cases/ServerAuth.ts +17 -0
  52. package/dist/templates/next-app/src/base/cases/ServerErrorHandler.ts +27 -0
  53. package/dist/templates/next-app/src/base/port/I18nServiceInterface.ts +25 -0
  54. package/dist/templates/next-app/src/base/port/IOCInterface.ts +24 -0
  55. package/dist/templates/next-app/src/base/port/ParamsHandlerInterface.ts +11 -0
  56. package/dist/templates/next-app/src/base/port/RouterInterface.ts +11 -0
  57. package/dist/templates/next-app/src/base/port/ServerAuthInterface.ts +3 -0
  58. package/dist/templates/next-app/src/base/port/ServerInterface.ts +12 -0
  59. package/dist/templates/next-app/src/base/port/UserServiceInterface.ts +11 -0
  60. package/dist/templates/next-app/src/base/services/I18nService.ts +115 -0
  61. package/dist/templates/next-app/src/base/services/UserService.ts +23 -0
  62. package/dist/templates/next-app/src/base/types/PageProps.ts +9 -0
  63. package/dist/templates/next-app/src/core/bootstraps/BootstrapClient.ts +61 -0
  64. package/dist/templates/next-app/src/core/bootstraps/BootstrapServer.ts +78 -0
  65. package/dist/templates/next-app/src/core/bootstraps/BootstrapsRegistry.ts +47 -0
  66. package/dist/templates/next-app/src/core/bootstraps/IocIdentifierTest.ts +26 -0
  67. package/dist/templates/next-app/src/core/bootstraps/PrintBootstrap.ts +18 -0
  68. package/dist/templates/next-app/src/core/clientIoc/ClientIOC.ts +37 -0
  69. package/dist/templates/next-app/src/core/clientIoc/ClientIOCRegister.ts +97 -0
  70. package/dist/templates/next-app/src/core/globals.ts +24 -0
  71. package/dist/templates/next-app/src/core/serverIoc/ServerIOC.ts +52 -0
  72. package/dist/templates/next-app/src/core/serverIoc/ServerIOCRegister.ts +63 -0
  73. package/dist/templates/next-app/src/i18n/request.ts +21 -0
  74. package/dist/templates/next-app/src/i18n/routing.ts +30 -0
  75. package/dist/templates/next-app/src/middleware.ts +25 -0
  76. package/dist/templates/next-app/src/styles/css/antd-themes/_default.css +239 -0
  77. package/dist/templates/next-app/src/styles/css/antd-themes/dark.css +178 -0
  78. package/dist/templates/next-app/src/styles/css/antd-themes/index.css +3 -0
  79. package/dist/templates/next-app/src/styles/css/antd-themes/no-context.css +34 -0
  80. package/dist/templates/next-app/src/styles/css/antd-themes/pink.css +204 -0
  81. package/dist/templates/next-app/src/styles/css/index.css +6 -0
  82. package/dist/templates/next-app/src/styles/css/page.css +19 -0
  83. package/dist/templates/next-app/src/styles/css/tailwind.css +5 -0
  84. package/dist/templates/next-app/src/styles/css/themes/_default.css +29 -0
  85. package/dist/templates/next-app/src/styles/css/themes/dark.css +29 -0
  86. package/dist/templates/next-app/src/styles/css/themes/index.css +3 -0
  87. package/dist/templates/next-app/src/styles/css/themes/pink.css +29 -0
  88. package/dist/templates/next-app/src/styles/css/zIndex.css +9 -0
  89. package/dist/templates/next-app/src/uikit/components/BaseHeader.tsx +47 -0
  90. package/dist/templates/next-app/src/uikit/components/BootstrapsProvider.tsx +37 -0
  91. package/dist/templates/next-app/src/uikit/components/ComboProvider.tsx +50 -0
  92. package/dist/templates/next-app/src/uikit/components/LanguageSwitcher.tsx +52 -0
  93. package/dist/templates/next-app/src/uikit/components/LocaleLink.tsx +51 -0
  94. package/dist/templates/next-app/src/uikit/components/LogoutButton.tsx +34 -0
  95. package/dist/templates/next-app/src/uikit/components/NextIntlProvider.tsx +21 -0
  96. package/dist/templates/next-app/src/uikit/components/ThemeSwitcher.tsx +99 -0
  97. package/dist/templates/next-app/src/uikit/context/IOCContext.ts +13 -0
  98. package/dist/templates/next-app/src/uikit/hook/useI18nInterface.ts +28 -0
  99. package/dist/templates/next-app/src/uikit/hook/useIOC.ts +37 -0
  100. package/dist/templates/next-app/src/uikit/hook/useMountedClient.ts +11 -0
  101. package/dist/templates/next-app/src/uikit/hook/useStore.ts +15 -0
  102. package/dist/templates/next-app/tailwind.config.ts +8 -0
  103. package/dist/templates/next-app/tsconfig.json +36 -0
  104. package/package.json +1 -1
@@ -0,0 +1,5 @@
1
+ @layer theme, base, antd, components, utilities;
2
+
3
+ @import 'tailwindcss';
4
+
5
+ @import './themes/index.css';
@@ -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,3 @@
1
+ @import './_default.css';
2
+ @import './dark.css';
3
+ @import './pink.css';
@@ -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,9 @@
1
+ /**
2
+ * This file is used to define the z-index values for the app.
3
+ */
4
+ :root {
5
+ /* antd select popup zindex */
6
+ --zi-select: 1050;
7
+ /* antd message zindex */
8
+ --zi-message: 2010;
9
+ }
@@ -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,11 @@
1
+ import { useEffect, useState } from 'react';
2
+
3
+ export const useMountedClient = () => {
4
+ const [mounted, setMounted] = useState(false);
5
+
6
+ useEffect(() => {
7
+ setMounted(true);
8
+ }, []);
9
+
10
+ return mounted;
11
+ };
@@ -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,8 @@
1
+ import type { Config } from 'tailwindcss';
2
+
3
+ const config: Config = {
4
+ darkMode: ['class', '[data-theme="dark"]'],
5
+ content: ['./src/**/*.{js,ts,jsx,tsx}']
6
+ };
7
+
8
+ export default config;
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qlover/create-app",
3
- "version": "0.7.6",
3
+ "version": "0.7.8",
4
4
  "description": "Create a new app with a single command",
5
5
  "private": false,
6
6
  "type": "module",