@wzyjs/cli 0.3.32 → 0.3.37

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 (107) hide show
  1. package/dist/index.js +350 -35
  2. package/package.json +6 -4
  3. package/template/web/base/.next/cache/tsconfig.tsbuildinfo +1 -0
  4. package/template/web/base/Dockerfile +37 -0
  5. package/template/web/base/_env +2 -0
  6. package/template/web/base/eslint.config.js +94 -0
  7. package/template/web/base/next.config.js +29 -0
  8. package/template/web/base/package.json +53 -0
  9. package/template/web/base/postcss.config.js +5 -0
  10. package/template/web/base/prisma/models/demo/DemoItem.zmodel +8 -0
  11. package/template/web/base/prisma/schema.prisma +29 -0
  12. package/template/web/base/prisma/schema.zmodel +1 -0
  13. package/template/web/base/src/app/api/trpc/[trpc]/route.ts +24 -0
  14. package/template/web/base/src/app/auth/disabled/page.tsx +29 -0
  15. package/template/web/base/src/app/auth/error/page.tsx +47 -0
  16. package/template/web/base/src/app/auth/signin/page.tsx +53 -0
  17. package/template/web/base/src/app/auth/unauthorized/page.tsx +60 -0
  18. package/template/web/base/src/app/demo/page.tsx +59 -0
  19. package/template/web/base/src/app/layout.tsx +32 -0
  20. package/template/web/base/src/app/not-found.tsx +21 -0
  21. package/template/web/base/src/app/page.tsx +92 -0
  22. package/template/web/base/src/components/base/Layout/Header/context.tsx +56 -0
  23. package/template/web/base/src/components/base/Layout/Header/index.tsx +32 -0
  24. package/template/web/base/src/components/base/Layout/index.tsx +27 -0
  25. package/template/web/base/src/components/base/Providers/TRPCReactProvider.tsx +25 -0
  26. package/template/web/base/src/components/base/Providers/index.tsx +31 -0
  27. package/template/web/base/src/components/base/display/DisplayProvider.tsx +44 -0
  28. package/template/web/base/src/components/base/display/consts.ts +10 -0
  29. package/template/web/base/src/components/base/display/context.ts +6 -0
  30. package/template/web/base/src/components/base/display/index.ts +4 -0
  31. package/template/web/base/src/components/base/display/types.ts +12 -0
  32. package/template/web/base/src/components/base/display/useDisplay.ts +9 -0
  33. package/template/web/base/src/components/base/display/utils.ts +18 -0
  34. package/template/web/base/src/components/base/theme/ThemeProvider.tsx +83 -0
  35. package/template/web/base/src/components/base/theme/ThemeToggle.tsx +26 -0
  36. package/template/web/base/src/components/base/theme/consts.tsx +59 -0
  37. package/template/web/base/src/components/base/theme/context.ts +14 -0
  38. package/template/web/base/src/components/base/theme/useAppTheme.ts +15 -0
  39. package/template/web/base/src/components/base/theme/utils.ts +17 -0
  40. package/template/web/base/src/components/index.ts +1 -0
  41. package/template/web/base/src/consts/index.ts +6 -0
  42. package/template/web/base/src/enums/index.ts +1 -0
  43. package/template/web/base/src/env.js +44 -0
  44. package/template/web/base/src/hooks/index.ts +1 -0
  45. package/template/web/base/src/server/db/client.ts +19 -0
  46. package/template/web/base/src/server/routers/index.ts +3 -0
  47. package/template/web/base/src/server/trpc/context.ts +14 -0
  48. package/template/web/base/src/server/trpc/index.ts +1 -0
  49. package/template/web/base/src/server/trpc/init.ts +27 -0
  50. package/template/web/base/src/server/trpc/procedures.ts +5 -0
  51. package/template/web/base/src/server/trpc/router.ts +11 -0
  52. package/template/web/base/src/server/utils/index.ts +1 -0
  53. package/template/web/base/src/styles/globals.css +3 -0
  54. package/template/web/base/src/types/index.ts +1 -0
  55. package/template/web/base/src/utils/index.ts +4 -0
  56. package/template/web/base/src/utils/query-client/index.ts +31 -0
  57. package/template/web/base/src/utils/trpc/index.ts +23 -0
  58. package/template/web/base/src/utils/trpc/utils.ts +61 -0
  59. package/template/web/base/tailwind.config.cjs +7 -0
  60. package/template/web/base/tsconfig.json +46 -0
  61. package/template/web/google/_env +8 -0
  62. package/template/web/google/package.json +55 -0
  63. package/template/web/google/prisma/models/auth/Account.zmodel +26 -0
  64. package/template/web/google/prisma/models/auth/Session.zmodel +17 -0
  65. package/template/web/google/prisma/models/auth/User.zmodel +22 -0
  66. package/template/web/google/prisma/schema.zmodel +2 -0
  67. package/template/web/google/src/app/api/auth/[...nextauth]/route.ts +7 -0
  68. package/template/web/google/src/app/auth/error/page.tsx +56 -0
  69. package/template/web/google/src/app/auth/signin/page.tsx +83 -0
  70. package/template/web/google/src/app/layout.tsx +35 -0
  71. package/template/web/google/src/components/base/AuthGuard/Loading.tsx +19 -0
  72. package/template/web/google/src/components/base/AuthGuard/index.tsx +45 -0
  73. package/template/web/google/src/components/base/Layout/Header/UserAuthStatus.tsx +93 -0
  74. package/template/web/google/src/components/base/Layout/Header/index.tsx +34 -0
  75. package/template/web/google/src/components/base/Providers/index.tsx +34 -0
  76. package/template/web/google/src/env.js +52 -0
  77. package/template/web/google/src/server/auth/next-auth/adapter.ts +77 -0
  78. package/template/web/google/src/server/auth/next-auth/options.ts +53 -0
  79. package/template/web/google/src/server/trpc/context.ts +21 -0
  80. package/template/web/google/src/server/trpc/procedures.ts +16 -0
  81. package/template/web/middle/_env +10 -0
  82. package/template/web/middle/package.json +55 -0
  83. package/template/web/middle/src/app/api/auth/[...nextauth]/route.ts +7 -0
  84. package/template/web/middle/src/app/auth/disabled/page.tsx +71 -0
  85. package/template/web/middle/src/app/auth/error/page.tsx +56 -0
  86. package/template/web/middle/src/app/auth/signin/page.tsx +91 -0
  87. package/template/web/middle/src/app/auth/unauthorized/page.tsx +88 -0
  88. package/template/web/middle/src/app/layout.tsx +35 -0
  89. package/template/web/middle/src/app/middle/page.tsx +10 -0
  90. package/template/web/middle/src/app/page.tsx +20 -0
  91. package/template/web/middle/src/components/base/AuthGuard/Loading.tsx +19 -0
  92. package/template/web/middle/src/components/base/AuthGuard/index.tsx +45 -0
  93. package/template/web/middle/src/components/base/Layout/Header/UserAuthStatus.tsx +93 -0
  94. package/template/web/middle/src/components/base/Layout/Header/index.tsx +34 -0
  95. package/template/web/middle/src/components/base/Layout/Sidebar/index.tsx +103 -0
  96. package/template/web/middle/src/components/base/Layout/index.tsx +35 -0
  97. package/template/web/middle/src/components/base/Providers/MiddleProvider.tsx +33 -0
  98. package/template/web/middle/src/components/base/Providers/index.tsx +37 -0
  99. package/template/web/middle/src/env.js +57 -0
  100. package/template/web/middle/src/server/auth/next-auth/options.ts +26 -0
  101. package/template/web/middle/src/server/trpc/context.ts +22 -0
  102. package/template/web/middle/src/server/trpc/procedures.ts +16 -0
  103. package/template/web/middle/src/server/utils/index.ts +2 -0
  104. package/template/web/middle/src/server/utils/middle.ts +34 -0
  105. package/template/web/middle/src/types/index.ts +3 -0
  106. package/template/web/middle/src/utils/index.ts +5 -0
  107. package/template/web/middle/src/utils/middle.ts +28 -0
@@ -0,0 +1,92 @@
1
+ 'use client'
2
+
3
+ import { useMemo } from 'react'
4
+
5
+ import { Button, PlusOutlined, Spin } from '@/components'
6
+ import { api } from '@/utils'
7
+
8
+ export default function HomePage() {
9
+ const projectName = '__WZYJS_APP_DISPLAY_NAME__'
10
+ const utils = api.useUtils()
11
+
12
+ const demoItemsQuery = api.demoItem.findMany.useQuery({
13
+ orderBy: { createdAt: 'desc' },
14
+ take: 10,
15
+ })
16
+
17
+ const createDemoItem = api.demoItem.create.useMutation({
18
+ onSuccess: async () => {
19
+ await utils.demoItem.findMany.invalidate()
20
+ },
21
+ })
22
+
23
+ const items = useMemo(() => demoItemsQuery.data ?? [], [demoItemsQuery.data])
24
+
25
+ const onCreate = () => {
26
+ const index = items.length + 1
27
+ createDemoItem.mutate({
28
+ data: {
29
+ title: `Demo Item ${index}`,
30
+ description: `Created from ${projectName}`,
31
+ },
32
+ })
33
+ }
34
+
35
+ return (
36
+ <div className='px-5 py-10 text-slate-950 dark:text-[#e8edf5]'>
37
+ <section className='mx-auto max-w-5xl rounded-lg border border-slate-200 bg-white p-8 shadow-sm dark:border-[#303846] dark:bg-[#1b2028] sm:p-10'>
38
+ <div className='max-w-2xl'>
39
+ <p className='text-xs font-semibold uppercase text-sky-700 dark:text-sky-300'>
40
+ Template Project
41
+ </p>
42
+ <h1 className='mt-5 text-4xl font-semibold text-slate-950 dark:text-[#f8fafc] sm:text-5xl'>
43
+ {projectName}
44
+ </h1>
45
+ <p className='mt-5 text-base leading-8 text-slate-600 dark:text-[#aab4c2]'>
46
+ 这里保留 Next.js、tRPC、Prisma 和 ZenStack 接入。登录和业务模块按实际项目需要再添加。
47
+ </p>
48
+ </div>
49
+
50
+ <div className='mt-10 rounded-lg border border-slate-200 bg-slate-50 p-5 dark:border-[#303846] dark:bg-[#202632]'>
51
+ <div className='flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between'>
52
+ <div>
53
+ <h2 className='text-base font-semibold text-slate-950 dark:text-[#f8fafc]'>Demo Items</h2>
54
+ <p className='mt-1 text-sm text-slate-500 dark:text-[#aab4c2]'>
55
+ 这个列表通过 tRPC 读取 `DemoItem` 数据。
56
+ </p>
57
+ </div>
58
+ <Button
59
+ type='primary'
60
+ icon={<PlusOutlined />}
61
+ loading={createDemoItem.isPending}
62
+ onClick={onCreate}
63
+ >
64
+ 创建一条
65
+ </Button>
66
+ </div>
67
+
68
+ <div className='mt-5 space-y-3'>
69
+ {demoItemsQuery.isLoading ? (
70
+ <div className='flex items-center justify-center rounded-lg border border-dashed border-slate-300 bg-white py-8 dark:border-[#3b4656] dark:bg-[#1b2028]'>
71
+ <Spin />
72
+ </div>
73
+ ) : null}
74
+
75
+ {!demoItemsQuery.isLoading && items.length === 0 ? (
76
+ <div className='rounded-lg border border-dashed border-slate-300 bg-white p-6 text-center text-sm text-slate-500 dark:border-[#3b4656] dark:bg-[#1b2028] dark:text-[#aab4c2]'>
77
+ 暂无数据,点击按钮创建一条。
78
+ </div>
79
+ ) : null}
80
+
81
+ {items.map(item => (
82
+ <div key={item.id} className='rounded-lg border border-slate-200 bg-white p-4 dark:border-[#303846] dark:bg-[#1b2028]'>
83
+ <div className='font-medium text-slate-950 dark:text-[#f8fafc]'>{item.title}</div>
84
+ <div className='mt-1 text-sm leading-6 text-slate-500 dark:text-[#aab4c2]'>{item.description}</div>
85
+ </div>
86
+ ))}
87
+ </div>
88
+ </div>
89
+ </section>
90
+ </div>
91
+ )
92
+ }
@@ -0,0 +1,56 @@
1
+ 'use client'
2
+
3
+ import { createContext, type ReactNode, useCallback, useContext, useEffect, useMemo, useState } from 'react'
4
+
5
+ interface LayoutHeaderContextValue {
6
+ leftContent?: ReactNode
7
+ setLeftContent: (content?: ReactNode) => void
8
+ }
9
+
10
+ const LayoutHeaderContext = createContext<LayoutHeaderContextValue | undefined>(undefined)
11
+
12
+ interface LayoutHeaderProviderProps {
13
+ children: ReactNode
14
+ }
15
+
16
+ export const LayoutHeaderProvider = (props: LayoutHeaderProviderProps) => {
17
+ const { children } = props
18
+ const [leftContent, setLeftContentState] = useState<ReactNode>()
19
+
20
+ const setLeftContent = useCallback((content?: ReactNode) => {
21
+ setLeftContentState(content)
22
+ }, [])
23
+
24
+ const value = useMemo<LayoutHeaderContextValue>(() => ({
25
+ leftContent,
26
+ setLeftContent,
27
+ }), [leftContent, setLeftContent])
28
+
29
+ return (
30
+ <LayoutHeaderContext.Provider value={value}>
31
+ {children}
32
+ </LayoutHeaderContext.Provider>
33
+ )
34
+ }
35
+
36
+ export const useLayoutHeader = () => {
37
+ const context = useContext(LayoutHeaderContext)
38
+
39
+ if (!context) {
40
+ throw new Error('useLayoutHeader must be used within LayoutHeaderProvider')
41
+ }
42
+
43
+ return context
44
+ }
45
+
46
+ export const useLayoutHeaderLeft = (content?: ReactNode) => {
47
+ const { setLeftContent } = useLayoutHeader()
48
+
49
+ useEffect(() => {
50
+ setLeftContent(content)
51
+
52
+ return () => {
53
+ setLeftContent(undefined)
54
+ }
55
+ }, [content, setLeftContent])
56
+ }
@@ -0,0 +1,32 @@
1
+ 'use client'
2
+
3
+ import { Button, MenuOutlined } from '@/components'
4
+
5
+ import { useLayoutHeader } from './context'
6
+ import { ThemeToggle } from '../../theme/ThemeToggle'
7
+
8
+ export const LayoutHeader = () => {
9
+ const { leftContent } = useLayoutHeader()
10
+
11
+ return (
12
+ <header className='flex h-[72px] shrink-0 items-center justify-between border-b border-slate-200/80 bg-white/95 px-5 backdrop-blur transition-colors dark:border-[#303846] dark:bg-[#1b2028]/95'>
13
+ <div className='min-w-0 flex-1'>
14
+ {leftContent ?? (
15
+ <div className='flex min-w-0 items-center gap-3'>
16
+ <Button
17
+ type='primary'
18
+ icon={<MenuOutlined />}
19
+ className='flex h-9 w-9 items-center justify-center rounded-lg shadow-sm'
20
+ />
21
+ <div className='truncate text-xl font-semibold tracking-normal text-slate-950 dark:text-[#e8edf5]'>
22
+ __WZYJS_APP_DISPLAY_NAME__
23
+ </div>
24
+ </div>
25
+ )}
26
+ </div>
27
+ <div className='ml-4 flex shrink-0 items-center gap-3'>
28
+ <ThemeToggle />
29
+ </div>
30
+ </header>
31
+ )
32
+ }
@@ -0,0 +1,27 @@
1
+ 'use client'
2
+
3
+ import { type ReactNode } from 'react'
4
+
5
+ import { LayoutHeader } from './Header'
6
+ import { LayoutHeaderProvider } from './Header/context'
7
+ import { useDisplay } from '../display'
8
+
9
+ interface LayoutProps {
10
+ children: ReactNode
11
+ }
12
+
13
+ export const Layout = (props: LayoutProps) => {
14
+ const { children } = props
15
+ const { isEmbedded } = useDisplay()
16
+
17
+ return (
18
+ <div className='flex h-screen flex-col overflow-hidden bg-slate-50 text-slate-950 transition-colors dark:bg-[#14171c] dark:text-[#e8edf5]'>
19
+ <LayoutHeaderProvider>
20
+ {isEmbedded ? null : <LayoutHeader />}
21
+ <main className='flex-1 overflow-auto'>
22
+ {children}
23
+ </main>
24
+ </LayoutHeaderProvider>
25
+ </div>
26
+ )
27
+ }
@@ -0,0 +1,25 @@
1
+ 'use client'
2
+
3
+ import { type ReactNode, useState } from 'react'
4
+ import { QueryClientProvider } from '@tanstack/react-query'
5
+
6
+ import { api, createApiClient, getQueryClient } from '@/utils'
7
+
8
+ export interface TRPCReactProviderProps {
9
+ children: ReactNode
10
+ }
11
+
12
+ export const TRPCReactProvider = (props: TRPCReactProviderProps) => {
13
+ const { children } = props
14
+
15
+ const [apiClient] = useState(createApiClient)
16
+ const [queryClient] = useState(getQueryClient)
17
+
18
+ return (
19
+ <QueryClientProvider client={queryClient}>
20
+ <api.Provider client={apiClient} queryClient={queryClient}>
21
+ {children}
22
+ </api.Provider>
23
+ </QueryClientProvider>
24
+ )
25
+ }
@@ -0,0 +1,31 @@
1
+ 'use client'
2
+
3
+ import { type ReactNode } from 'react'
4
+
5
+ import { App, AntdRegistry } from '@/components'
6
+
7
+ import { TRPCReactProvider } from './TRPCReactProvider'
8
+ import { DisplayProvider } from '../display'
9
+ import { ThemeProvider } from '../theme/ThemeProvider'
10
+
11
+ export interface ProvidersProps {
12
+ children: ReactNode
13
+ }
14
+
15
+ export const Providers = (props: ProvidersProps) => {
16
+ const { children } = props
17
+
18
+ return (
19
+ <TRPCReactProvider>
20
+ <DisplayProvider>
21
+ <AntdRegistry>
22
+ <ThemeProvider>
23
+ <App>
24
+ {children}
25
+ </App>
26
+ </ThemeProvider>
27
+ </AntdRegistry>
28
+ </DisplayProvider>
29
+ </TRPCReactProvider>
30
+ )
31
+ }
@@ -0,0 +1,44 @@
1
+ 'use client'
2
+
3
+ import { Suspense, useMemo } from 'react'
4
+ import { useSearchParams } from 'next/navigation'
5
+
6
+ import { defaultDisplay } from './consts'
7
+ import { DisplayContext } from './context'
8
+ import { type DisplayProviderProps } from './types'
9
+ import { getDisplay } from './utils'
10
+
11
+ const DisplayFallbackProvider = (props: DisplayProviderProps) => {
12
+ const { children } = props
13
+
14
+ return (
15
+ <DisplayContext.Provider value={defaultDisplay}>
16
+ {children}
17
+ </DisplayContext.Provider>
18
+ )
19
+ }
20
+
21
+ const DisplaySearchParamsProvider = (props: DisplayProviderProps) => {
22
+ const { children } = props
23
+ const searchParams = useSearchParams()
24
+
25
+ const display = useMemo(() => getDisplay(searchParams), [searchParams])
26
+
27
+ return (
28
+ <DisplayContext.Provider value={display}>
29
+ {children}
30
+ </DisplayContext.Provider>
31
+ )
32
+ }
33
+
34
+ export const DisplayProvider = (props: DisplayProviderProps) => {
35
+ const { children } = props
36
+
37
+ return (
38
+ <Suspense fallback={<DisplayFallbackProvider>{children}</DisplayFallbackProvider>}>
39
+ <DisplaySearchParamsProvider>
40
+ {children}
41
+ </DisplaySearchParamsProvider>
42
+ </Suspense>
43
+ )
44
+ }
@@ -0,0 +1,10 @@
1
+ import { type Display } from './types'
2
+
3
+ export const urlParamKeys = {
4
+ embedded: 'embedded',
5
+ theme: 'theme',
6
+ } as const
7
+
8
+ export const defaultDisplay: Display = {
9
+ isEmbedded: false,
10
+ }
@@ -0,0 +1,6 @@
1
+ import { createContext } from 'react'
2
+
3
+ import { defaultDisplay } from './consts'
4
+ import { type Display } from './types'
5
+
6
+ export const DisplayContext = createContext<Display>(defaultDisplay)
@@ -0,0 +1,4 @@
1
+ export { DisplayProvider } from './DisplayProvider'
2
+ export { useDisplay } from './useDisplay'
3
+ export { getDisplay, getThemeModeFromValue } from './utils'
4
+ export type { Display } from './types'
@@ -0,0 +1,12 @@
1
+ import { type ReactNode } from 'react'
2
+
3
+ import { type ThemeMode } from '../theme/consts'
4
+
5
+ export interface Display {
6
+ isEmbedded: boolean
7
+ themeMode?: ThemeMode
8
+ }
9
+
10
+ export interface DisplayProviderProps {
11
+ children: ReactNode
12
+ }
@@ -0,0 +1,9 @@
1
+ 'use client'
2
+
3
+ import { useContext } from 'react'
4
+
5
+ import { DisplayContext } from './context'
6
+
7
+ export const useDisplay = () => {
8
+ return useContext(DisplayContext)
9
+ }
@@ -0,0 +1,18 @@
1
+ import { type ThemeMode, themeModes } from '../theme/consts'
2
+ import { urlParamKeys } from './consts'
3
+ import { type Display } from './types'
4
+
5
+ export const getThemeModeFromValue = (value: string | null): ThemeMode | undefined => {
6
+ if (value === themeModes.system || value === themeModes.light || value === themeModes.dark) {
7
+ return value
8
+ }
9
+
10
+ return undefined
11
+ }
12
+
13
+ export const getDisplay = (searchParams: URLSearchParams): Display => {
14
+ return {
15
+ isEmbedded: searchParams.get(urlParamKeys.embedded) === '1',
16
+ themeMode: getThemeModeFromValue(searchParams.get(urlParamKeys.theme)),
17
+ }
18
+ }
@@ -0,0 +1,83 @@
1
+ 'use client'
2
+
3
+ import { type ReactNode, useEffect, useMemo, useState } from 'react'
4
+
5
+ import { ConfigProvider, zh_CN } from '@/components'
6
+
7
+ import { useProjectLocalStorage } from '@/hooks'
8
+
9
+ import { AppThemeContext, type AppThemeContextValue } from './context'
10
+ import { getThemeModeFromValue, useDisplay } from '../display'
11
+ import { getNextThemeMode, getThemeAlgorithm, getSystemThemeMode } from './utils'
12
+ import { baseThemeToken, darkThemeToken, themeStoragePath, themeModes, type ThemeMode } from './consts'
13
+
14
+ interface AppThemeProviderProps {
15
+ children: ReactNode
16
+ }
17
+
18
+ export const ThemeProvider = (props: AppThemeProviderProps) => {
19
+ const { children } = props
20
+
21
+ const { themeMode } = useDisplay()
22
+ const { getValue, setValue } = useProjectLocalStorage('__WZYJS_APP_PACKAGE_NAME__')
23
+ const [storedMode, setStoredMode] = useState<ThemeMode>(themeModes.system)
24
+ const [systemMode, setSystemMode] = useState<ThemeMode>(themeModes.light)
25
+ const [isThemeReady, setIsThemeReady] = useState(false)
26
+
27
+ const mode = themeMode ?? storedMode
28
+ const resolvedMode: ThemeMode = mode === themeModes.system ? systemMode : mode
29
+
30
+ useEffect(() => {
31
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
32
+ const updateSystemMode = () => setSystemMode(getSystemThemeMode())
33
+
34
+ updateSystemMode()
35
+
36
+ const savedMode = getValue<ThemeMode>(themeStoragePath)
37
+ setStoredMode(getThemeModeFromValue(savedMode ?? null) ?? themeModes.system)
38
+
39
+ setIsThemeReady(true)
40
+
41
+ mediaQuery.addEventListener('change', updateSystemMode)
42
+
43
+ return () => {
44
+ mediaQuery.removeEventListener('change', updateSystemMode)
45
+ }
46
+ }, [getValue])
47
+
48
+ useEffect(() => {
49
+ if (!isThemeReady) {
50
+ return
51
+ }
52
+
53
+ setValue(themeStoragePath, storedMode)
54
+ document.documentElement.classList.toggle(themeModes.dark, resolvedMode === themeModes.dark)
55
+ document.documentElement.style.colorScheme = resolvedMode
56
+ }, [isThemeReady, resolvedMode, setValue, storedMode])
57
+
58
+ const themeValue = useMemo<AppThemeContextValue>(() => ({
59
+ mode,
60
+ resolvedMode,
61
+ setMode: setStoredMode,
62
+ toggleMode: () => setStoredMode(getNextThemeMode),
63
+ }), [mode, resolvedMode])
64
+
65
+ return (
66
+ <AppThemeContext.Provider value={themeValue}>
67
+ <ConfigProvider
68
+ locale={zh_CN}
69
+ theme={{
70
+ algorithm: getThemeAlgorithm(resolvedMode),
71
+ token: {
72
+ ...baseThemeToken,
73
+ ...(resolvedMode === themeModes.dark
74
+ ? darkThemeToken
75
+ : {}),
76
+ },
77
+ }}
78
+ >
79
+ {children}
80
+ </ConfigProvider>
81
+ </AppThemeContext.Provider>
82
+ )
83
+ }
@@ -0,0 +1,26 @@
1
+ import { Button, Dropdown } from '@/components'
2
+
3
+ import { useAppTheme } from './useAppTheme'
4
+ import { type ThemeMode, themeComponent, themeLabel, themeMenuItems } from './consts'
5
+
6
+ export const ThemeToggle = () => {
7
+ const { mode, setMode } = useAppTheme()
8
+
9
+ return (
10
+ <Dropdown
11
+ trigger={['click']}
12
+ placement='bottomRight'
13
+ menu={{
14
+ items: themeMenuItems,
15
+ selectedKeys: [mode],
16
+ onClick: ({ key }) => setMode(key as ThemeMode),
17
+ }}
18
+ >
19
+ <Button
20
+ shape='circle'
21
+ icon={themeComponent[mode]}
22
+ aria-label={`当前主题:${themeLabel[mode]}`}
23
+ />
24
+ </Dropdown>
25
+ )
26
+ }
@@ -0,0 +1,59 @@
1
+ import { type ReactNode } from 'react'
2
+
3
+ import { DesktopOutlined, MoonOutlined, SunOutlined, type MenuProps } from '@/components'
4
+
5
+ export const themeStoragePath = 'theme.mode'
6
+
7
+ export const themeModes = {
8
+ system: 'system',
9
+ light: 'light',
10
+ dark: 'dark',
11
+ } as const
12
+
13
+ export type ThemeMode = typeof themeModes[keyof typeof themeModes]
14
+
15
+ export const themeMenuItems: MenuProps['items'] = [
16
+ {
17
+ key: themeModes.system,
18
+ icon: <DesktopOutlined />,
19
+ label: '跟随系统',
20
+ },
21
+ {
22
+ key: themeModes.light,
23
+ icon: <SunOutlined />,
24
+ label: '明亮模式',
25
+ },
26
+ {
27
+ key: themeModes.dark,
28
+ icon: <MoonOutlined />,
29
+ label: '暗黑模式',
30
+ },
31
+ ]
32
+
33
+ export const themeLabel: Record<ThemeMode, string> = {
34
+ [themeModes.system]: '跟随系统',
35
+ [themeModes.light]: '明亮模式',
36
+ [themeModes.dark]: '暗黑模式',
37
+ }
38
+
39
+ export const themeComponent: Record<ThemeMode, ReactNode> = {
40
+ [themeModes.system]: <DesktopOutlined />,
41
+ [themeModes.light]: <SunOutlined />,
42
+ [themeModes.dark]: <MoonOutlined />,
43
+ }
44
+
45
+ export const baseThemeToken = {
46
+ borderRadius: 8,
47
+ colorPrimary: '#2563eb',
48
+ fontFamily: 'Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
49
+ }
50
+
51
+ export const darkThemeToken = {
52
+ colorBgLayout: '#14171c',
53
+ colorBgContainer: '#1b2028',
54
+ colorBgElevated: '#202632',
55
+ colorBorder: '#303846',
56
+ colorFillAlter: '#242b36',
57
+ colorText: '#e8edf5',
58
+ colorTextSecondary: '#aab4c2',
59
+ }
@@ -0,0 +1,14 @@
1
+ 'use client'
2
+
3
+ import { createContext } from 'react'
4
+
5
+ import { type ThemeMode } from './consts'
6
+
7
+ export interface AppThemeContextValue {
8
+ mode: ThemeMode
9
+ setMode: (mode: ThemeMode) => void
10
+ toggleMode: () => void
11
+ resolvedMode: ThemeMode
12
+ }
13
+
14
+ export const AppThemeContext = createContext<AppThemeContextValue | null>(null)
@@ -0,0 +1,15 @@
1
+ 'use client'
2
+
3
+ import { useContext } from 'react'
4
+
5
+ import { AppThemeContext } from './context'
6
+
7
+ export const useAppTheme = () => {
8
+ const context = useContext(AppThemeContext)
9
+
10
+ if (!context) {
11
+ throw new Error('useAppTheme must be used within AppThemeProvider')
12
+ }
13
+
14
+ return context
15
+ }
@@ -0,0 +1,17 @@
1
+ import { theme } from '@/components'
2
+ import { type ThemeMode, themeModes } from './consts'
3
+
4
+ export const getNextThemeMode = (mode: ThemeMode): ThemeMode => {
5
+ if (mode === themeModes.system) {
6
+ return themeModes.light
7
+ }
8
+ return mode === themeModes.light ? themeModes.dark : themeModes.system
9
+ }
10
+
11
+ export const getThemeAlgorithm = (mode: ThemeMode) => {
12
+ return mode === themeModes.dark ? theme.darkAlgorithm : theme.defaultAlgorithm
13
+ }
14
+
15
+ export const getSystemThemeMode = () => {
16
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? themeModes.dark : themeModes.light
17
+ }
@@ -0,0 +1 @@
1
+ export * from '@wzyjs/uis/web'
@@ -0,0 +1,6 @@
1
+ export const authExemptPaths = [
2
+ '/auth/signin',
3
+ '/auth/unauthorized',
4
+ '/auth/disabled',
5
+ '/auth/error',
6
+ ]
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,44 @@
1
+ import { z } from 'zod'
2
+ import { createEnv } from '@t3-oss/env-nextjs'
3
+
4
+ export const env = createEnv({
5
+ /**
6
+ * 在此处指定服务器端环境变量模式。
7
+ * 这样可以确保应用程序不会使用无效的环境变量构建。
8
+ */
9
+ server: {
10
+ NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
11
+ // 数据库
12
+ DATABASE_URL: z.string().url(),
13
+ },
14
+
15
+ /**
16
+ * 在此处指定客户端环境变量模式。
17
+ * 这样可以确保应用程序不会使用无效的环境变量构建。
18
+ * 要将它们暴露给客户端,请使用 `NEXT_PUBLIC_` 前缀。
19
+ */
20
+ client: {},
21
+
22
+ /**
23
+ * 在这里显式传入运行时环境变量的实际值。
24
+ * `server` / `client` 负责声明校验规则,
25
+ * `runtimeEnv` 负责把 `process.env` 中对应的值提供给 `createEnv`。
26
+ */
27
+ runtimeEnv: {
28
+ NODE_ENV: process.env.NODE_ENV,
29
+ // 数据库
30
+ DATABASE_URL: process.env.DATABASE_URL,
31
+ },
32
+
33
+ /**
34
+ * 使用 `SKIP_ENV_VALIDATION` 运行 `build` 或 `dev` 可以跳过环境变量验证。
35
+ * 这在 Docker 构建时特别有用。
36
+ */
37
+ skipValidation: !!process.env.SKIP_ENV_VALIDATION,
38
+
39
+ /**
40
+ * 使空字符串被视为未定义。
41
+ * 例如:`SOME_VAR: z.string()` 和 `SOME_VAR=''` 将抛出错误。
42
+ */
43
+ emptyStringAsUndefined: true,
44
+ })
@@ -0,0 +1 @@
1
+ export * from '@wzyjs/hooks/web'