@wzyjs/cli 0.3.32 → 0.3.36

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 (106) hide show
  1. package/dist/index.js +350 -35
  2. package/package.json +6 -4
  3. package/template/web/base/Dockerfile +37 -0
  4. package/template/web/base/_env +2 -0
  5. package/template/web/base/eslint.config.js +94 -0
  6. package/template/web/base/next.config.js +29 -0
  7. package/template/web/base/package.json +53 -0
  8. package/template/web/base/postcss.config.js +5 -0
  9. package/template/web/base/prisma/models/demo/DemoItem.zmodel +8 -0
  10. package/template/web/base/prisma/schema.prisma +29 -0
  11. package/template/web/base/prisma/schema.zmodel +1 -0
  12. package/template/web/base/src/app/api/trpc/[trpc]/route.ts +24 -0
  13. package/template/web/base/src/app/auth/disabled/page.tsx +29 -0
  14. package/template/web/base/src/app/auth/error/page.tsx +47 -0
  15. package/template/web/base/src/app/auth/signin/page.tsx +53 -0
  16. package/template/web/base/src/app/auth/unauthorized/page.tsx +60 -0
  17. package/template/web/base/src/app/demo/page.tsx +59 -0
  18. package/template/web/base/src/app/layout.tsx +32 -0
  19. package/template/web/base/src/app/not-found.tsx +21 -0
  20. package/template/web/base/src/app/page.tsx +92 -0
  21. package/template/web/base/src/components/base/Layout/Header/context.tsx +56 -0
  22. package/template/web/base/src/components/base/Layout/Header/index.tsx +32 -0
  23. package/template/web/base/src/components/base/Layout/index.tsx +27 -0
  24. package/template/web/base/src/components/base/Providers/TRPCReactProvider.tsx +25 -0
  25. package/template/web/base/src/components/base/Providers/index.tsx +31 -0
  26. package/template/web/base/src/components/base/display/DisplayProvider.tsx +44 -0
  27. package/template/web/base/src/components/base/display/consts.ts +10 -0
  28. package/template/web/base/src/components/base/display/context.ts +6 -0
  29. package/template/web/base/src/components/base/display/index.ts +4 -0
  30. package/template/web/base/src/components/base/display/types.ts +12 -0
  31. package/template/web/base/src/components/base/display/useDisplay.ts +9 -0
  32. package/template/web/base/src/components/base/display/utils.ts +18 -0
  33. package/template/web/base/src/components/base/theme/ThemeProvider.tsx +83 -0
  34. package/template/web/base/src/components/base/theme/ThemeToggle.tsx +26 -0
  35. package/template/web/base/src/components/base/theme/consts.tsx +59 -0
  36. package/template/web/base/src/components/base/theme/context.ts +14 -0
  37. package/template/web/base/src/components/base/theme/useAppTheme.ts +15 -0
  38. package/template/web/base/src/components/base/theme/utils.ts +17 -0
  39. package/template/web/base/src/components/index.ts +1 -0
  40. package/template/web/base/src/consts/index.ts +6 -0
  41. package/template/web/base/src/enums/index.ts +1 -0
  42. package/template/web/base/src/env.js +44 -0
  43. package/template/web/base/src/hooks/index.ts +1 -0
  44. package/template/web/base/src/server/db/client.ts +19 -0
  45. package/template/web/base/src/server/routers/index.ts +3 -0
  46. package/template/web/base/src/server/trpc/context.ts +14 -0
  47. package/template/web/base/src/server/trpc/index.ts +1 -0
  48. package/template/web/base/src/server/trpc/init.ts +27 -0
  49. package/template/web/base/src/server/trpc/procedures.ts +5 -0
  50. package/template/web/base/src/server/trpc/router.ts +11 -0
  51. package/template/web/base/src/server/utils/index.ts +1 -0
  52. package/template/web/base/src/styles/globals.css +3 -0
  53. package/template/web/base/src/types/index.ts +1 -0
  54. package/template/web/base/src/utils/index.ts +4 -0
  55. package/template/web/base/src/utils/query-client/index.ts +31 -0
  56. package/template/web/base/src/utils/trpc/index.ts +23 -0
  57. package/template/web/base/src/utils/trpc/utils.ts +61 -0
  58. package/template/web/base/tailwind.config.cjs +7 -0
  59. package/template/web/base/tsconfig.json +46 -0
  60. package/template/web/google/_env +8 -0
  61. package/template/web/google/package.json +55 -0
  62. package/template/web/google/prisma/models/auth/Account.zmodel +26 -0
  63. package/template/web/google/prisma/models/auth/Session.zmodel +17 -0
  64. package/template/web/google/prisma/models/auth/User.zmodel +22 -0
  65. package/template/web/google/prisma/schema.zmodel +2 -0
  66. package/template/web/google/src/app/api/auth/[...nextauth]/route.ts +7 -0
  67. package/template/web/google/src/app/auth/error/page.tsx +56 -0
  68. package/template/web/google/src/app/auth/signin/page.tsx +83 -0
  69. package/template/web/google/src/app/layout.tsx +35 -0
  70. package/template/web/google/src/components/base/AuthGuard/Loading.tsx +19 -0
  71. package/template/web/google/src/components/base/AuthGuard/index.tsx +45 -0
  72. package/template/web/google/src/components/base/Layout/Header/UserAuthStatus.tsx +93 -0
  73. package/template/web/google/src/components/base/Layout/Header/index.tsx +34 -0
  74. package/template/web/google/src/components/base/Providers/index.tsx +34 -0
  75. package/template/web/google/src/env.js +52 -0
  76. package/template/web/google/src/server/auth/next-auth/adapter.ts +77 -0
  77. package/template/web/google/src/server/auth/next-auth/options.ts +53 -0
  78. package/template/web/google/src/server/trpc/context.ts +21 -0
  79. package/template/web/google/src/server/trpc/procedures.ts +16 -0
  80. package/template/web/middle/_env +10 -0
  81. package/template/web/middle/package.json +55 -0
  82. package/template/web/middle/src/app/api/auth/[...nextauth]/route.ts +7 -0
  83. package/template/web/middle/src/app/auth/disabled/page.tsx +71 -0
  84. package/template/web/middle/src/app/auth/error/page.tsx +56 -0
  85. package/template/web/middle/src/app/auth/signin/page.tsx +91 -0
  86. package/template/web/middle/src/app/auth/unauthorized/page.tsx +88 -0
  87. package/template/web/middle/src/app/layout.tsx +35 -0
  88. package/template/web/middle/src/app/middle/page.tsx +10 -0
  89. package/template/web/middle/src/app/page.tsx +20 -0
  90. package/template/web/middle/src/components/base/AuthGuard/Loading.tsx +19 -0
  91. package/template/web/middle/src/components/base/AuthGuard/index.tsx +45 -0
  92. package/template/web/middle/src/components/base/Layout/Header/UserAuthStatus.tsx +93 -0
  93. package/template/web/middle/src/components/base/Layout/Header/index.tsx +34 -0
  94. package/template/web/middle/src/components/base/Layout/Sidebar/index.tsx +88 -0
  95. package/template/web/middle/src/components/base/Layout/index.tsx +35 -0
  96. package/template/web/middle/src/components/base/Providers/MiddleProvider.tsx +33 -0
  97. package/template/web/middle/src/components/base/Providers/index.tsx +37 -0
  98. package/template/web/middle/src/env.js +57 -0
  99. package/template/web/middle/src/server/auth/next-auth/options.ts +26 -0
  100. package/template/web/middle/src/server/trpc/context.ts +22 -0
  101. package/template/web/middle/src/server/trpc/procedures.ts +16 -0
  102. package/template/web/middle/src/server/utils/index.ts +2 -0
  103. package/template/web/middle/src/server/utils/middle.ts +34 -0
  104. package/template/web/middle/src/types/index.ts +3 -0
  105. package/template/web/middle/src/utils/index.ts +5 -0
  106. package/template/web/middle/src/utils/middle.ts +28 -0
@@ -0,0 +1,61 @@
1
+ 'use client'
2
+
3
+ import { type TRPCLink } from '@trpc/client'
4
+ import { observable } from '@trpc/server/observable'
5
+
6
+ import { message } from '@/components'
7
+ import { authExemptPaths } from '@/consts'
8
+ import { type ApiRouter } from '@/server/trpc'
9
+
10
+ const isRN = () => {
11
+ return typeof navigator !== 'undefined' && navigator.product === 'ReactNative'
12
+ }
13
+
14
+ const redirectToSignIn = () => {
15
+ if (typeof window === 'undefined') {
16
+ return
17
+ }
18
+
19
+ const authExemptPathSet = new Set(authExemptPaths)
20
+ if (authExemptPathSet.has(window.location.pathname)) {
21
+ return
22
+ }
23
+
24
+ const callbackUrl = `${window.location.pathname}${window.location.search}${window.location.hash}`
25
+ window.location.replace(`/auth/signin?callbackUrl=${encodeURIComponent(callbackUrl)}`)
26
+ }
27
+
28
+ export const getBaseUrl = () => {
29
+ if (isRN()) {
30
+ if (__DEV__) {
31
+ return 'http://192.168.1.16:3000'
32
+ }
33
+ return 'https://app.zhenyu.pro'
34
+ }
35
+ if (typeof window !== 'undefined') {
36
+ return window.location.origin
37
+ }
38
+ return `http://localhost:${process.env.PORT ?? 3000}`
39
+ }
40
+
41
+ export const errorLink: TRPCLink<ApiRouter> = () => {
42
+ return ({ next, op }) => {
43
+ return observable(observer => {
44
+ const subscription = next(op).subscribe({
45
+ next: value => observer.next(value),
46
+ complete: () => observer.complete(),
47
+ error: err => {
48
+ if (isRN()) {
49
+ console.error(666, 'tRPC request failed:', err)
50
+ } else if (err.data?.code === 'UNAUTHORIZED') {
51
+ redirectToSignIn()
52
+ } else {
53
+ void message.error(err.data?.formattedError || err.message || '操作失败')
54
+ }
55
+ observer.error(err)
56
+ },
57
+ })
58
+ return () => subscription.unsubscribe()
59
+ })
60
+ }
61
+ }
@@ -0,0 +1,7 @@
1
+ /** @type {import('tailwindcss').Config} */
2
+ module.exports = {
3
+ content: ['./src/**/*.tsx', './src/**/*.ts'],
4
+ darkMode: 'class',
5
+ plugins: [],
6
+ theme: {},
7
+ }
@@ -0,0 +1,46 @@
1
+ {
2
+ "compilerOptions": {
3
+ "esModuleInterop": true,
4
+ "skipLibCheck": true,
5
+ "target": "es2022",
6
+ "allowJs": true,
7
+ "resolveJsonModule": true,
8
+ "moduleDetection": "force",
9
+ "isolatedModules": true,
10
+ "strict": true,
11
+ "noUncheckedIndexedAccess": true,
12
+ "checkJs": true,
13
+ "lib": ["dom", "dom.iterable", "ES2022"],
14
+ "noEmit": true,
15
+ "module": "ESNext",
16
+ "moduleResolution": "Bundler",
17
+ "jsx": "react-jsx",
18
+ "plugins": [
19
+ {
20
+ "name": "next"
21
+ }
22
+ ],
23
+ "incremental": true,
24
+ "tsBuildInfoFile": ".next/cache/tsconfig.tsbuildinfo",
25
+ "baseUrl": ".",
26
+ "paths": {
27
+ "@/*": ["./src/*"],
28
+ "@prisma/client": ["./src/server/generated/prisma-client"]
29
+ }
30
+ },
31
+ "include": [
32
+ ".eslintrc.cjs",
33
+ "next-env.d.ts",
34
+ "**/*.ts",
35
+ "**/*.tsx",
36
+ "**/*.cjs",
37
+ "**/*.js",
38
+ ".next/types/**/*.ts",
39
+ ".next/dev/types/**/*.ts"
40
+ ],
41
+ "exclude": [
42
+ "node_modules",
43
+ "src/server/generated",
44
+ "**/.next"
45
+ ]
46
+ }
@@ -0,0 +1,8 @@
1
+ # 数据库配置
2
+ DATABASE_URL=postgresql://root:password.@localhost:5432/demo
3
+
4
+ # Google 登录
5
+ NEXTAUTH_URL=http://localhost:4000
6
+ NEXTAUTH_SECRET=
7
+ GOOGLE_CLIENT_ID=
8
+ GOOGLE_CLIENT_SECRET=
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "__WZYJS_APP_NAME__",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "next dev --turbopack",
8
+ "build": "NODE_ENV=production next build --turbopack",
9
+ "start": "next start",
10
+ "generate": "rm -rf src/server/generated && zenstack generate --output ./src/server/generated/.zenstack --no-compile --schema ./prisma/schema.zmodel",
11
+ "db:push": "prisma db push",
12
+ "lint": "eslint .",
13
+ "typecheck": "tsc --noEmit",
14
+ "check": "bun run lint && bun run typecheck",
15
+ "postinstall": "bun run generate"
16
+ },
17
+ "dependencies": {
18
+ "@auth/prisma-adapter": "^2.9.1",
19
+ "@prisma/client": "^6.14.0",
20
+ "@t3-oss/env-nextjs": "^0.10.1",
21
+ "@tanstack/react-query": "^5.50.0",
22
+ "@trpc/client": "^11.7.0",
23
+ "@trpc/next": "^11.7.0",
24
+ "@trpc/react-query": "^11.7.0",
25
+ "@trpc/server": "^11.7.0",
26
+ "@wzyjs/hooks": "workspace:*",
27
+ "@wzyjs/next": "workspace:*",
28
+ "@wzyjs/utils": "workspace:*",
29
+ "@wzyjs/uis": "workspace:*",
30
+ "@zenstackhq/runtime": "2.22.1",
31
+ "@zenstackhq/trpc": "2.22.1",
32
+ "antd": "^6.3.1",
33
+ "next": "^16.2.4",
34
+ "next-auth": "^4.24.11",
35
+ "react": "^19.1.0",
36
+ "react-dom": "^19.1.0",
37
+ "server-only": "^0.0.1",
38
+ "superjson": "^2.2.1",
39
+ "zenstack": "2.22.1",
40
+ "zod": "^3.23.3",
41
+ "zod-validation-error": "^3.4.0"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^20",
45
+ "@types/react": "19.1.10",
46
+ "@types/react-dom": "^19",
47
+ "eslint": "^9",
48
+ "eslint-config-next": "^16.2.4",
49
+ "postcss": "^8.4.39",
50
+ "prisma": "^6.14.0",
51
+ "prisma-json-types-generator": "^3.3.0",
52
+ "tailwindcss": "^3.4.3",
53
+ "typescript": "^5"
54
+ }
55
+ }
@@ -0,0 +1,26 @@
1
+ import "@wzyjs/next/Base"
2
+ import "./User"
3
+
4
+ model AuthAccount extends Base {
5
+ userId String
6
+ type String
7
+ provider String
8
+ providerAccountId String
9
+ refresh_token String? @db.Text
10
+ access_token String? @db.Text
11
+ expires_at Int?
12
+ token_type String?
13
+ scope String?
14
+ id_token String? @db.Text
15
+ session_state String?
16
+
17
+ user AuthUser @relation("AuthUserToAccount", fields: [userId], references: [id], onDelete: Cascade)
18
+
19
+ @@unique([provider, providerAccountId])
20
+ @@deny('all', auth() == null)
21
+ @@deny('read', isDeleted)
22
+ @@deny('read', userId != auth().id)
23
+ @@deny('create', true)
24
+ @@deny('update', true)
25
+ @@deny('delete', true)
26
+ }
@@ -0,0 +1,17 @@
1
+ import "@wzyjs/next/Base"
2
+ import "./User"
3
+
4
+ model AuthSession extends Base {
5
+ sessionToken String @unique
6
+ userId String
7
+ expires DateTime
8
+
9
+ user AuthUser @relation("AuthUserToSession", fields: [userId], references: [id], onDelete: Cascade)
10
+
11
+ @@deny('all', auth() == null)
12
+ @@deny('read', isDeleted)
13
+ @@deny('read', userId != auth().id)
14
+ @@deny('create', true)
15
+ @@deny('update', true)
16
+ @@deny('delete', true)
17
+ }
@@ -0,0 +1,22 @@
1
+ import "@wzyjs/next/Base"
2
+
3
+ import "./Account"
4
+ import "./Session"
5
+
6
+ model AuthUser extends Base {
7
+ email String @unique @deny('update', true)
8
+ emailVerified DateTime?
9
+ name String?
10
+ image String?
11
+
12
+ accounts AuthAccount[] @relation("AuthUserToAccount")
13
+ sessions AuthSession[] @relation("AuthUserToSession")
14
+
15
+ @@auth()
16
+ @@deny('all', auth() == null)
17
+ @@deny('read', isDeleted)
18
+ @@deny('read', id != auth().id)
19
+ @@deny('update', id != auth().id)
20
+ @@deny('create', true)
21
+ @@deny('delete', true)
22
+ }
@@ -0,0 +1,2 @@
1
+ import "./models/auth/User"
2
+ import "./models/demo/DemoItem"
@@ -0,0 +1,7 @@
1
+ import NextAuth from 'next-auth'
2
+
3
+ import { auth } from '@/server/auth/next-auth/options'
4
+
5
+ const handler = NextAuth(auth)
6
+
7
+ export { handler as GET, handler as POST }
@@ -0,0 +1,56 @@
1
+ 'use client'
2
+
3
+ import Link from 'next/link'
4
+ import { Suspense } from 'react'
5
+ import { useSearchParams } from 'next/navigation'
6
+
7
+ import { Button, PanelPage, Result, Spin, Typography } from '@/components'
8
+
9
+ const errorMessageMap: Record<string, string> = {
10
+ OAuthSignin: '无法发起 Google 登录,请稍后再试。',
11
+ OAuthCallback: 'Google 登录回调失败,请检查回调地址配置。',
12
+ OAuthCreateAccount: '创建登录账号失败,请稍后再试。',
13
+ EmailCreateAccount: '创建登录账号失败,请稍后再试。',
14
+ Callback: '登录流程中断,请重新发起登录。',
15
+ OAuthAccountNotLinked: '当前邮箱已绑定其他登录方式。',
16
+ EmailSignin: '邮件登录失败。',
17
+ CredentialsSignin: '账号校验失败。',
18
+ SessionRequired: '当前页面需要先登录。',
19
+ default: '登录失败,请稍后重试。',
20
+ }
21
+
22
+ const AuthErrorContent = () => {
23
+ const searchParams = useSearchParams()
24
+ const error = searchParams.get('error') || 'default'
25
+
26
+ return (
27
+ <PanelPage eyebrow='Authentication Exception' title='登录失败'>
28
+ <Result
29
+ status='error'
30
+ title='本次认证没有成功完成'
31
+ subTitle={errorMessageMap[error] ?? errorMessageMap.default}
32
+ extra={[
33
+ <Link href='/auth/signin' key='signin'>
34
+ <Button type='primary' className='!rounded-lg px-6'>
35
+ 返回登录页
36
+ </Button>
37
+ </Link>,
38
+ ]}
39
+ />
40
+
41
+ <div className='mx-4 mb-4 rounded-lg bg-white/70 p-5 dark:bg-white/5 sm:mx-6'>
42
+ <Typography.Paragraph className='!mb-0 text-center !text-sm !leading-6 !text-slate-500 dark:!text-slate-300'>
43
+ 请检查 Google OAuth Client、NEXTAUTH_URL 和 NEXTAUTH_SECRET 配置。
44
+ </Typography.Paragraph>
45
+ </div>
46
+ </PanelPage>
47
+ )
48
+ }
49
+
50
+ export default function AuthErrorPage() {
51
+ return (
52
+ <Suspense fallback={<Spin />}>
53
+ <AuthErrorContent />
54
+ </Suspense>
55
+ )
56
+ }
@@ -0,0 +1,83 @@
1
+ 'use client'
2
+
3
+ import { Suspense, useEffect } from 'react'
4
+ import { signIn, useSession } from 'next-auth/react'
5
+ import { useRouter, useSearchParams } from 'next/navigation'
6
+
7
+ import { App, Button, GoogleOutlined, PanelPage, Space, Spin, Typography } from '@/components'
8
+ import { getSafeCallbackPath } from '@/utils'
9
+
10
+ const { Text } = Typography
11
+
12
+ const SignInPage = () => {
13
+ const { message } = App.useApp()
14
+ const { data: session, status } = useSession()
15
+ const router = useRouter()
16
+ const searchParams = useSearchParams()
17
+
18
+ const callbackUrl = getSafeCallbackPath(searchParams.get('callbackUrl'), '/')
19
+ const error = searchParams.get('error')
20
+
21
+ useEffect(() => {
22
+ if (status === 'authenticated' && session) {
23
+ router.push(callbackUrl)
24
+ }
25
+ }, [callbackUrl, router, session, status])
26
+
27
+ const onGoogleSignIn = async () => {
28
+ try {
29
+ await signIn('google', { callbackUrl })
30
+ } catch (signinError) {
31
+ console.error(666, '登录失败:', signinError)
32
+ void message.error('登录失败,请稍后重试')
33
+ }
34
+ }
35
+
36
+ return (
37
+ <PanelPage
38
+ eyebrow='Google Sign In'
39
+ title='账号登录'
40
+ description='使用 Google 账号登录后即可访问应用。'
41
+ >
42
+ <Space orientation='vertical' size='large' className='w-full'>
43
+ {error ? (
44
+ <div className='rounded-lg border border-red-200 bg-red-50/90 p-4 text-left dark:border-red-500/30 dark:bg-red-500/10'>
45
+ <div className='text-sm font-semibold text-red-700 dark:text-red-300'>
46
+ 登录流程没有完成
47
+ </div>
48
+ <p className='mt-2 text-sm leading-6 text-red-600 dark:text-red-200'>
49
+ 请重试一次;如果问题持续出现,优先检查 Google Client 配置和回调地址。
50
+ </p>
51
+ </div>
52
+ ) : null}
53
+
54
+ <Button
55
+ type='primary'
56
+ size='large'
57
+ className='!h-12 !rounded-lg !text-sm !font-semibold shadow-sm'
58
+ icon={<GoogleOutlined />}
59
+ onClick={() => {
60
+ void onGoogleSignIn()
61
+ }}
62
+ block
63
+ >
64
+ 使用 Google 登录
65
+ </Button>
66
+
67
+ <div className='rounded-lg bg-white/70 p-5 text-center dark:bg-white/5'>
68
+ <Text className='text-sm leading-6 text-slate-500 dark:text-slate-300'>
69
+ 首次使用前,请在 `.env` 中配置 Google OAuth Client 信息。
70
+ </Text>
71
+ </div>
72
+ </Space>
73
+ </PanelPage>
74
+ )
75
+ }
76
+
77
+ export default function SignIn() {
78
+ return (
79
+ <Suspense fallback={<Spin />}>
80
+ <SignInPage />
81
+ </Suspense>
82
+ )
83
+ }
@@ -0,0 +1,35 @@
1
+ import { type ReactNode } from 'react'
2
+ import { type Metadata } from 'next'
3
+
4
+ import { AuthGuard } from '@/components/base/AuthGuard'
5
+ import { Providers } from '@/components/base/Providers'
6
+ import { Layout } from '@/components/base/Layout'
7
+
8
+ import '@/styles/globals.css'
9
+
10
+ export const metadata: Metadata = {
11
+ title: '__WZYJS_APP_DISPLAY_NAME__',
12
+ icons: [{ rel: 'icon', url: '/favicon.ico' }],
13
+ }
14
+
15
+ interface RootLayoutProps {
16
+ children: ReactNode
17
+ }
18
+
19
+ export default function RootLayout(props: RootLayoutProps) {
20
+ const { children } = props
21
+
22
+ return (
23
+ <html lang='zh-CN' suppressHydrationWarning>
24
+ <body suppressHydrationWarning>
25
+ <Providers>
26
+ <AuthGuard>
27
+ <Layout>
28
+ {children}
29
+ </Layout>
30
+ </AuthGuard>
31
+ </Providers>
32
+ </body>
33
+ </html>
34
+ )
35
+ }
@@ -0,0 +1,19 @@
1
+ 'use client'
2
+
3
+ import { Spin, Typography } from '@/components'
4
+
5
+ export const LayoutLoading = () => {
6
+ return (
7
+ <div className='flex min-h-screen items-center justify-center bg-slate-50 p-6 text-slate-950 transition-colors dark:bg-[#14171c] dark:text-[#e8edf5]'>
8
+ <div className='w-full max-w-md rounded-lg border border-slate-200 bg-white p-8 text-center shadow-sm dark:border-[#303846] dark:bg-[#1b2028]'>
9
+ <Spin size='large' />
10
+ <Typography.Title level={4} className='!mb-0 !mt-6 !text-xl !font-semibold !text-slate-950 dark:!text-[#e8edf5]'>
11
+ 正在加载
12
+ </Typography.Title>
13
+ <Typography.Text className='mt-3 text-sm leading-6 text-slate-500 dark:text-slate-300'>
14
+ 请稍候,正在准备页面内容。
15
+ </Typography.Text>
16
+ </div>
17
+ </div>
18
+ )
19
+ }
@@ -0,0 +1,45 @@
1
+ 'use client'
2
+
3
+ import { type ReactNode, useEffect } from 'react'
4
+ import { useSession } from 'next-auth/react'
5
+ import { usePathname, useRouter } from 'next/navigation'
6
+
7
+ import { authExemptPaths } from '@/consts'
8
+
9
+ import { LayoutLoading } from './Loading'
10
+
11
+ interface AuthGuardProps {
12
+ children: ReactNode
13
+ }
14
+
15
+ export const AuthGuard = (props: AuthGuardProps) => {
16
+ const { children } = props
17
+
18
+ const { status } = useSession()
19
+ const pathname = usePathname()
20
+ const router = useRouter()
21
+
22
+ const isAuthExemptPath = pathname ? authExemptPaths.includes(pathname) : false
23
+
24
+ useEffect(() => {
25
+ if (!pathname || status !== 'unauthenticated' || isAuthExemptPath) {
26
+ return
27
+ }
28
+
29
+ router.replace(`/auth/signin?callbackUrl=${encodeURIComponent(pathname)}`)
30
+ }, [isAuthExemptPath, pathname, router, status])
31
+
32
+ if (status === 'loading') {
33
+ return <LayoutLoading />
34
+ }
35
+
36
+ if (isAuthExemptPath) {
37
+ return children
38
+ }
39
+
40
+ if (status === 'unauthenticated') {
41
+ return null
42
+ }
43
+
44
+ return children
45
+ }
@@ -0,0 +1,93 @@
1
+ 'use client'
2
+
3
+ import { signIn, signOut, useSession } from 'next-auth/react'
4
+
5
+ import {
6
+ App,
7
+ Avatar,
8
+ Button,
9
+ Dropdown,
10
+ LoginOutlined,
11
+ LogoutOutlined,
12
+ Space,
13
+ UserOutlined,
14
+ type MenuProps,
15
+ } from '@/components'
16
+
17
+ export const UserAuthStatus = () => {
18
+ const { message } = App.useApp()
19
+ const { data: session, status } = useSession()
20
+
21
+ const onLogin = async () => {
22
+ try {
23
+ await signIn('google')
24
+ } catch (error) {
25
+ console.error(666, '登录失败:', error)
26
+ void message.error('登录失败,请稍后重试')
27
+ }
28
+ }
29
+
30
+ const onLogout = async () => {
31
+ try {
32
+ await signOut({ callbackUrl: '/auth/signin' })
33
+ } catch (error) {
34
+ console.error(666, '退出登录失败:', error)
35
+ void message.error('退出登录失败,请稍后重试')
36
+ }
37
+ }
38
+
39
+ if (status === 'loading') {
40
+ return (
41
+ <div className='px-2 text-sm text-slate-400 dark:text-slate-500'>
42
+ 加载中...
43
+ </div>
44
+ )
45
+ }
46
+
47
+ if (status === 'unauthenticated') {
48
+ return (
49
+ <Button
50
+ type='text'
51
+ size='small'
52
+ className='flex items-center text-slate-700 dark:!text-slate-200'
53
+ icon={<LoginOutlined />}
54
+ onClick={onLogin}
55
+ >
56
+ 登录
57
+ </Button>
58
+ )
59
+ }
60
+
61
+ const items: MenuProps['items'] = [
62
+ {
63
+ key: 'profile',
64
+ label: (
65
+ <div className='max-w-[220px] p-2'>
66
+ <div className='truncate font-medium'>{session?.user?.name}</div>
67
+ <div className='truncate text-xs text-slate-500'>{session?.user?.email}</div>
68
+ </div>
69
+ ),
70
+ },
71
+ {
72
+ key: 'logout',
73
+ label: '退出登录',
74
+ icon: <LogoutOutlined />,
75
+ onClick: onLogout,
76
+ },
77
+ ]
78
+
79
+ return (
80
+ <Dropdown menu={{ items }} placement='bottomRight'>
81
+ <Space className='flex cursor-pointer items-center rounded-lg px-2 py-1 transition-colors hover:bg-slate-100 dark:hover:bg-slate-800'>
82
+ <Avatar
83
+ size='small'
84
+ src={session?.user?.image || undefined}
85
+ icon={!session?.user?.image ? <UserOutlined /> : undefined}
86
+ />
87
+ <span className='hidden max-w-[140px] overflow-hidden text-ellipsis whitespace-nowrap text-sm text-slate-700 dark:text-[#e8edf5] sm:inline'>
88
+ {session?.user?.name || session?.user?.email || '已登录用户'}
89
+ </span>
90
+ </Space>
91
+ </Dropdown>
92
+ )
93
+ }
@@ -0,0 +1,34 @@
1
+ 'use client'
2
+
3
+ import { Button, MenuOutlined } from '@/components'
4
+
5
+ import { UserAuthStatus } from './UserAuthStatus'
6
+ import { useLayoutHeader } from './context'
7
+ import { ThemeToggle } from '../../theme/ThemeToggle'
8
+
9
+ export const LayoutHeader = () => {
10
+ const { leftContent } = useLayoutHeader()
11
+
12
+ return (
13
+ <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'>
14
+ <div className='min-w-0 flex-1'>
15
+ {leftContent ?? (
16
+ <div className='flex min-w-0 items-center gap-3'>
17
+ <Button
18
+ type='primary'
19
+ icon={<MenuOutlined />}
20
+ className='flex h-9 w-9 items-center justify-center rounded-lg shadow-sm'
21
+ />
22
+ <div className='truncate text-xl font-semibold tracking-normal text-slate-950 dark:text-[#e8edf5]'>
23
+ __WZYJS_APP_DISPLAY_NAME__
24
+ </div>
25
+ </div>
26
+ )}
27
+ </div>
28
+ <div className='ml-4 flex shrink-0 items-center gap-3'>
29
+ <UserAuthStatus />
30
+ <ThemeToggle />
31
+ </div>
32
+ </header>
33
+ )
34
+ }
@@ -0,0 +1,34 @@
1
+ 'use client'
2
+
3
+ import { type ReactNode } from 'react'
4
+ import { SessionProvider } from 'next-auth/react'
5
+
6
+ import { App, AntdRegistry } from '@/components'
7
+
8
+ import { DisplayProvider } from '../display'
9
+ import { TRPCReactProvider } from './TRPCReactProvider'
10
+ import { ThemeProvider } from '../theme/ThemeProvider'
11
+
12
+ export interface ProvidersProps {
13
+ children: ReactNode
14
+ }
15
+
16
+ export const Providers = (props: ProvidersProps) => {
17
+ const { children } = props
18
+
19
+ return (
20
+ <TRPCReactProvider>
21
+ <SessionProvider>
22
+ <DisplayProvider>
23
+ <AntdRegistry>
24
+ <ThemeProvider>
25
+ <App>
26
+ {children}
27
+ </App>
28
+ </ThemeProvider>
29
+ </AntdRegistry>
30
+ </DisplayProvider>
31
+ </SessionProvider>
32
+ </TRPCReactProvider>
33
+ )
34
+ }