@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.
- package/dist/index.js +350 -35
- package/package.json +6 -4
- package/template/web/base/.next/cache/tsconfig.tsbuildinfo +1 -0
- package/template/web/base/Dockerfile +37 -0
- package/template/web/base/_env +2 -0
- package/template/web/base/eslint.config.js +94 -0
- package/template/web/base/next.config.js +29 -0
- package/template/web/base/package.json +53 -0
- package/template/web/base/postcss.config.js +5 -0
- package/template/web/base/prisma/models/demo/DemoItem.zmodel +8 -0
- package/template/web/base/prisma/schema.prisma +29 -0
- package/template/web/base/prisma/schema.zmodel +1 -0
- package/template/web/base/src/app/api/trpc/[trpc]/route.ts +24 -0
- package/template/web/base/src/app/auth/disabled/page.tsx +29 -0
- package/template/web/base/src/app/auth/error/page.tsx +47 -0
- package/template/web/base/src/app/auth/signin/page.tsx +53 -0
- package/template/web/base/src/app/auth/unauthorized/page.tsx +60 -0
- package/template/web/base/src/app/demo/page.tsx +59 -0
- package/template/web/base/src/app/layout.tsx +32 -0
- package/template/web/base/src/app/not-found.tsx +21 -0
- package/template/web/base/src/app/page.tsx +92 -0
- package/template/web/base/src/components/base/Layout/Header/context.tsx +56 -0
- package/template/web/base/src/components/base/Layout/Header/index.tsx +32 -0
- package/template/web/base/src/components/base/Layout/index.tsx +27 -0
- package/template/web/base/src/components/base/Providers/TRPCReactProvider.tsx +25 -0
- package/template/web/base/src/components/base/Providers/index.tsx +31 -0
- package/template/web/base/src/components/base/display/DisplayProvider.tsx +44 -0
- package/template/web/base/src/components/base/display/consts.ts +10 -0
- package/template/web/base/src/components/base/display/context.ts +6 -0
- package/template/web/base/src/components/base/display/index.ts +4 -0
- package/template/web/base/src/components/base/display/types.ts +12 -0
- package/template/web/base/src/components/base/display/useDisplay.ts +9 -0
- package/template/web/base/src/components/base/display/utils.ts +18 -0
- package/template/web/base/src/components/base/theme/ThemeProvider.tsx +83 -0
- package/template/web/base/src/components/base/theme/ThemeToggle.tsx +26 -0
- package/template/web/base/src/components/base/theme/consts.tsx +59 -0
- package/template/web/base/src/components/base/theme/context.ts +14 -0
- package/template/web/base/src/components/base/theme/useAppTheme.ts +15 -0
- package/template/web/base/src/components/base/theme/utils.ts +17 -0
- package/template/web/base/src/components/index.ts +1 -0
- package/template/web/base/src/consts/index.ts +6 -0
- package/template/web/base/src/enums/index.ts +1 -0
- package/template/web/base/src/env.js +44 -0
- package/template/web/base/src/hooks/index.ts +1 -0
- package/template/web/base/src/server/db/client.ts +19 -0
- package/template/web/base/src/server/routers/index.ts +3 -0
- package/template/web/base/src/server/trpc/context.ts +14 -0
- package/template/web/base/src/server/trpc/index.ts +1 -0
- package/template/web/base/src/server/trpc/init.ts +27 -0
- package/template/web/base/src/server/trpc/procedures.ts +5 -0
- package/template/web/base/src/server/trpc/router.ts +11 -0
- package/template/web/base/src/server/utils/index.ts +1 -0
- package/template/web/base/src/styles/globals.css +3 -0
- package/template/web/base/src/types/index.ts +1 -0
- package/template/web/base/src/utils/index.ts +4 -0
- package/template/web/base/src/utils/query-client/index.ts +31 -0
- package/template/web/base/src/utils/trpc/index.ts +23 -0
- package/template/web/base/src/utils/trpc/utils.ts +61 -0
- package/template/web/base/tailwind.config.cjs +7 -0
- package/template/web/base/tsconfig.json +46 -0
- package/template/web/google/_env +8 -0
- package/template/web/google/package.json +55 -0
- package/template/web/google/prisma/models/auth/Account.zmodel +26 -0
- package/template/web/google/prisma/models/auth/Session.zmodel +17 -0
- package/template/web/google/prisma/models/auth/User.zmodel +22 -0
- package/template/web/google/prisma/schema.zmodel +2 -0
- package/template/web/google/src/app/api/auth/[...nextauth]/route.ts +7 -0
- package/template/web/google/src/app/auth/error/page.tsx +56 -0
- package/template/web/google/src/app/auth/signin/page.tsx +83 -0
- package/template/web/google/src/app/layout.tsx +35 -0
- package/template/web/google/src/components/base/AuthGuard/Loading.tsx +19 -0
- package/template/web/google/src/components/base/AuthGuard/index.tsx +45 -0
- package/template/web/google/src/components/base/Layout/Header/UserAuthStatus.tsx +93 -0
- package/template/web/google/src/components/base/Layout/Header/index.tsx +34 -0
- package/template/web/google/src/components/base/Providers/index.tsx +34 -0
- package/template/web/google/src/env.js +52 -0
- package/template/web/google/src/server/auth/next-auth/adapter.ts +77 -0
- package/template/web/google/src/server/auth/next-auth/options.ts +53 -0
- package/template/web/google/src/server/trpc/context.ts +21 -0
- package/template/web/google/src/server/trpc/procedures.ts +16 -0
- package/template/web/middle/_env +10 -0
- package/template/web/middle/package.json +55 -0
- package/template/web/middle/src/app/api/auth/[...nextauth]/route.ts +7 -0
- package/template/web/middle/src/app/auth/disabled/page.tsx +71 -0
- package/template/web/middle/src/app/auth/error/page.tsx +56 -0
- package/template/web/middle/src/app/auth/signin/page.tsx +91 -0
- package/template/web/middle/src/app/auth/unauthorized/page.tsx +88 -0
- package/template/web/middle/src/app/layout.tsx +35 -0
- package/template/web/middle/src/app/middle/page.tsx +10 -0
- package/template/web/middle/src/app/page.tsx +20 -0
- package/template/web/middle/src/components/base/AuthGuard/Loading.tsx +19 -0
- package/template/web/middle/src/components/base/AuthGuard/index.tsx +45 -0
- package/template/web/middle/src/components/base/Layout/Header/UserAuthStatus.tsx +93 -0
- package/template/web/middle/src/components/base/Layout/Header/index.tsx +34 -0
- package/template/web/middle/src/components/base/Layout/Sidebar/index.tsx +103 -0
- package/template/web/middle/src/components/base/Layout/index.tsx +35 -0
- package/template/web/middle/src/components/base/Providers/MiddleProvider.tsx +33 -0
- package/template/web/middle/src/components/base/Providers/index.tsx +37 -0
- package/template/web/middle/src/env.js +57 -0
- package/template/web/middle/src/server/auth/next-auth/options.ts +26 -0
- package/template/web/middle/src/server/trpc/context.ts +22 -0
- package/template/web/middle/src/server/trpc/procedures.ts +16 -0
- package/template/web/middle/src/server/utils/index.ts +2 -0
- package/template/web/middle/src/server/utils/middle.ts +34 -0
- package/template/web/middle/src/types/index.ts +3 -0
- package/template/web/middle/src/utils/index.ts +5 -0
- 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,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 @@
|
|
|
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'
|