@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,88 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { signOut, useSession } from 'next-auth/react'
|
|
4
|
+
import { useRouter, useSearchParams } from 'next/navigation'
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
ArrowRightOutlined,
|
|
8
|
+
Button,
|
|
9
|
+
LogoutOutlined,
|
|
10
|
+
PanelPage,
|
|
11
|
+
Result,
|
|
12
|
+
RetweetOutlined,
|
|
13
|
+
StopOutlined,
|
|
14
|
+
Typography,
|
|
15
|
+
} from '@/components'
|
|
16
|
+
|
|
17
|
+
import { getSafeCallbackPath } from '@/utils'
|
|
18
|
+
|
|
19
|
+
export default function UnauthorizedPage() {
|
|
20
|
+
const { data: session } = useSession()
|
|
21
|
+
const router = useRouter()
|
|
22
|
+
const searchParams = useSearchParams()
|
|
23
|
+
|
|
24
|
+
const retryUrl = getSafeCallbackPath(searchParams.get('callbackUrl'), '/')
|
|
25
|
+
|
|
26
|
+
const onSignOut = async () => {
|
|
27
|
+
await signOut({ callbackUrl: '/auth/signin' })
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<PanelPage eyebrow='需要授权' title='暂时不能访问'>
|
|
32
|
+
<Result
|
|
33
|
+
status='403'
|
|
34
|
+
title='你还没有这个页面的访问权限'
|
|
35
|
+
icon={<StopOutlined style={{ color: '#f5222d' }} />}
|
|
36
|
+
subTitle={(
|
|
37
|
+
<div className='mt-4 space-y-4'>
|
|
38
|
+
<Typography.Paragraph className='!mb-0 !text-sm !leading-6 !text-slate-600 dark:!text-slate-300'>
|
|
39
|
+
请联系管理员开通权限。权限生效后,你可以回来重试,或切换到已经授权的账号。
|
|
40
|
+
</Typography.Paragraph>
|
|
41
|
+
<div className='rounded-lg border border-slate-200/70 bg-white/70 px-4 py-3 text-left dark:border-white/10 dark:bg-white/5'>
|
|
42
|
+
<div className='text-xs font-semibold uppercase text-slate-500 dark:text-slate-400'>
|
|
43
|
+
当前登录账号
|
|
44
|
+
</div>
|
|
45
|
+
<div className='mt-2 break-all text-sm font-medium text-slate-900 dark:text-slate-100'>
|
|
46
|
+
{session?.user?.email || '暂未读取到账号信息'}
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
)}
|
|
51
|
+
extra={(
|
|
52
|
+
<div className='flex w-full flex-col gap-3 sm:flex-row sm:justify-center'>
|
|
53
|
+
<Button
|
|
54
|
+
type='primary'
|
|
55
|
+
icon={<RetweetOutlined />}
|
|
56
|
+
className='!rounded-lg'
|
|
57
|
+
onClick={() => {
|
|
58
|
+
router.push(retryUrl)
|
|
59
|
+
}}
|
|
60
|
+
>
|
|
61
|
+
重试访问
|
|
62
|
+
</Button>
|
|
63
|
+
<Button
|
|
64
|
+
icon={<ArrowRightOutlined />}
|
|
65
|
+
className='!rounded-lg'
|
|
66
|
+
onClick={() => {
|
|
67
|
+
router.push('/')
|
|
68
|
+
}}
|
|
69
|
+
>
|
|
70
|
+
返回首页
|
|
71
|
+
</Button>
|
|
72
|
+
<Button
|
|
73
|
+
type='primary'
|
|
74
|
+
danger
|
|
75
|
+
icon={<LogoutOutlined />}
|
|
76
|
+
className='!rounded-lg'
|
|
77
|
+
onClick={() => {
|
|
78
|
+
void onSignOut()
|
|
79
|
+
}}
|
|
80
|
+
>
|
|
81
|
+
换个账号登录
|
|
82
|
+
</Button>
|
|
83
|
+
</div>
|
|
84
|
+
)}
|
|
85
|
+
/>
|
|
86
|
+
</PanelPage>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
@@ -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,20 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useSession } from 'next-auth/react'
|
|
4
|
+
|
|
5
|
+
export default function HomePage() {
|
|
6
|
+
const { data: session } = useSession()
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<div className='min-h-full bg-slate-50 px-5 py-8 text-slate-950 dark:bg-[#14171c] dark:text-[#e8edf5]'>
|
|
10
|
+
<section className='mx-auto max-w-4xl border-b border-slate-200 pb-6 dark:border-[#303846]'>
|
|
11
|
+
<h1 className='text-2xl font-semibold tracking-normal'>
|
|
12
|
+
__WZYJS_APP_DISPLAY_NAME__
|
|
13
|
+
</h1>
|
|
14
|
+
<p className='mt-3 text-sm leading-6 text-slate-600 dark:text-[#aab4c2]'>
|
|
15
|
+
{session?.user?.name || session?.user?.email || '当前账号'} 已登录。
|
|
16
|
+
</p>
|
|
17
|
+
</section>
|
|
18
|
+
</div>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
@@ -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('middle')
|
|
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,103 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import Link from 'next/link'
|
|
4
|
+
import { useSession } from 'next-auth/react'
|
|
5
|
+
import { usePathname, useSearchParams } from 'next/navigation'
|
|
6
|
+
import { resolveMiddleMenuGroups, type MiddleResolvedPage } from '@wzyjs/middle-sdk/page'
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
AppstoreOutlined,
|
|
10
|
+
LinkOutlined,
|
|
11
|
+
Typography,
|
|
12
|
+
getIconNode,
|
|
13
|
+
} from '@/components'
|
|
14
|
+
|
|
15
|
+
const menuItemClassName = (isActive: boolean) => [
|
|
16
|
+
'flex h-10 items-center gap-2 rounded-md px-3 text-sm font-medium transition-colors',
|
|
17
|
+
isActive
|
|
18
|
+
? 'bg-blue-50 text-blue-700 dark:bg-blue-500/15 dark:text-blue-200'
|
|
19
|
+
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-950 dark:text-[#aab4c2] dark:hover:bg-[#202632] dark:hover:text-[#e8edf5]',
|
|
20
|
+
].join(' ')
|
|
21
|
+
|
|
22
|
+
const subMenuItemClassName = (isActive: boolean) => [
|
|
23
|
+
menuItemClassName(isActive),
|
|
24
|
+
'h-9 pl-8 text-[13px]',
|
|
25
|
+
].join(' ')
|
|
26
|
+
|
|
27
|
+
const getPageIcon = (item: MiddleResolvedPage) => (
|
|
28
|
+
getIconNode(item.page.icon) ?? (item.isExternal ? <LinkOutlined /> : <AppstoreOutlined />)
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
const MenuItemContent = (props: { item: MiddleResolvedPage }) => {
|
|
32
|
+
const { item } = props
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<>
|
|
36
|
+
<span className='text-base leading-none'>{getPageIcon(item)}</span>
|
|
37
|
+
<span className='truncate'>{item.page.name}</span>
|
|
38
|
+
</>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const LayoutSidebar = () => {
|
|
43
|
+
const { data: session } = useSession()
|
|
44
|
+
const pathname = usePathname()
|
|
45
|
+
const searchParams = useSearchParams()
|
|
46
|
+
|
|
47
|
+
const menuGroups = resolveMiddleMenuGroups({
|
|
48
|
+
iframePagePath: '/middle',
|
|
49
|
+
pages: session?.pages,
|
|
50
|
+
pathname,
|
|
51
|
+
iframeUrl: searchParams.get('url'),
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<aside className='hidden w-56 shrink-0 border-r border-slate-200 bg-white/90 transition-colors dark:border-[#303846] dark:bg-[#171c23] md:block'>
|
|
56
|
+
<nav className='p-3' aria-label='主菜单'>
|
|
57
|
+
<div className='flex flex-col gap-3'>
|
|
58
|
+
{menuGroups.length ? menuGroups.map(group => (
|
|
59
|
+
<section key={group.groupName} className='flex flex-col gap-1'>
|
|
60
|
+
<div className='px-3 pt-1 text-[11px] font-semibold tracking-[0.08em] text-slate-400 dark:text-[#6f7b8d]'>
|
|
61
|
+
{group.groupName}
|
|
62
|
+
</div>
|
|
63
|
+
<div className='flex flex-col gap-1'>
|
|
64
|
+
{group.items.map(item => {
|
|
65
|
+
if (item.isBlank) {
|
|
66
|
+
return (
|
|
67
|
+
<a
|
|
68
|
+
key={item.page.path}
|
|
69
|
+
href={item.href}
|
|
70
|
+
target={item.target}
|
|
71
|
+
rel={item.rel}
|
|
72
|
+
className={subMenuItemClassName(false)}
|
|
73
|
+
>
|
|
74
|
+
<MenuItemContent item={item} />
|
|
75
|
+
</a>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<Link
|
|
81
|
+
key={item.page.path}
|
|
82
|
+
href={item.href}
|
|
83
|
+
aria-current={item.isActive ? 'page' : undefined}
|
|
84
|
+
className={subMenuItemClassName(item.isActive)}
|
|
85
|
+
>
|
|
86
|
+
<MenuItemContent item={item} />
|
|
87
|
+
</Link>
|
|
88
|
+
)
|
|
89
|
+
})}
|
|
90
|
+
</div>
|
|
91
|
+
</section>
|
|
92
|
+
)) : (
|
|
93
|
+
<div className='px-3 py-2 text-sm leading-6'>
|
|
94
|
+
<Typography.Text type='secondary'>
|
|
95
|
+
暂未获取到中台菜单,请重新登录或检查当前账号是否已启用应用访问。
|
|
96
|
+
</Typography.Text>
|
|
97
|
+
</div>
|
|
98
|
+
)}
|
|
99
|
+
</div>
|
|
100
|
+
</nav>
|
|
101
|
+
</aside>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { type ReactNode } from 'react'
|
|
4
|
+
import { usePathname } from 'next/navigation'
|
|
5
|
+
|
|
6
|
+
import { LayoutHeader } from './Header'
|
|
7
|
+
import { LayoutHeaderProvider } from './Header/context'
|
|
8
|
+
import { LayoutSidebar } from './Sidebar'
|
|
9
|
+
import { useDisplay } from '../display'
|
|
10
|
+
|
|
11
|
+
interface LayoutProps {
|
|
12
|
+
children: ReactNode
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const Layout = (props: LayoutProps) => {
|
|
16
|
+
const { children } = props
|
|
17
|
+
const { isEmbedded } = useDisplay()
|
|
18
|
+
const pathname = usePathname()
|
|
19
|
+
|
|
20
|
+
const shouldShowSidebar = !isEmbedded && !pathname?.startsWith('/auth/')
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className='flex h-screen flex-col overflow-hidden bg-slate-50 text-slate-950 transition-colors dark:bg-[#14171c] dark:text-[#e8edf5]'>
|
|
24
|
+
<LayoutHeaderProvider>
|
|
25
|
+
{isEmbedded ? null : <LayoutHeader />}
|
|
26
|
+
<div className='flex min-h-0 flex-1'>
|
|
27
|
+
{shouldShowSidebar ? <LayoutSidebar /> : null}
|
|
28
|
+
<main className='min-w-0 flex-1 overflow-auto'>
|
|
29
|
+
{children}
|
|
30
|
+
</main>
|
|
31
|
+
</div>
|
|
32
|
+
</LayoutHeaderProvider>
|
|
33
|
+
</div>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { type ReactNode, useMemo, useState } from 'react'
|
|
4
|
+
import { getSession } from 'next-auth/react'
|
|
5
|
+
import { middle } from '@wzyjs/middle-sdk/api/react'
|
|
6
|
+
|
|
7
|
+
import { env } from '@/env'
|
|
8
|
+
import { getQueryClient } from '@/utils'
|
|
9
|
+
|
|
10
|
+
interface MiddleProviderProps {
|
|
11
|
+
children: ReactNode
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const MiddleProvider = (props: MiddleProviderProps) => {
|
|
15
|
+
const { children } = props
|
|
16
|
+
|
|
17
|
+
const [queryClient] = useState(getQueryClient)
|
|
18
|
+
|
|
19
|
+
const accessToken = useMemo(
|
|
20
|
+
() => async () => (await getSession())?.middleAccessToken,
|
|
21
|
+
[],
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<middle.Provider
|
|
26
|
+
accessToken={accessToken}
|
|
27
|
+
baseUrl={env.NEXT_PUBLIC_MIDDLE_API_URL}
|
|
28
|
+
queryClient={queryClient}
|
|
29
|
+
>
|
|
30
|
+
{children}
|
|
31
|
+
</middle.Provider>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
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 { TRPCReactProvider } from './TRPCReactProvider'
|
|
9
|
+
import { MiddleProvider } from './MiddleProvider'
|
|
10
|
+
import { DisplayProvider } from '../display'
|
|
11
|
+
import { ThemeProvider } from '../theme/ThemeProvider'
|
|
12
|
+
|
|
13
|
+
export interface ProvidersProps {
|
|
14
|
+
children: ReactNode
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const Providers = (props: ProvidersProps) => {
|
|
18
|
+
const { children } = props
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<TRPCReactProvider>
|
|
22
|
+
<SessionProvider>
|
|
23
|
+
<DisplayProvider>
|
|
24
|
+
<MiddleProvider>
|
|
25
|
+
<AntdRegistry>
|
|
26
|
+
<ThemeProvider>
|
|
27
|
+
<App>
|
|
28
|
+
{children}
|
|
29
|
+
</App>
|
|
30
|
+
</ThemeProvider>
|
|
31
|
+
</AntdRegistry>
|
|
32
|
+
</MiddleProvider>
|
|
33
|
+
</DisplayProvider>
|
|
34
|
+
</SessionProvider>
|
|
35
|
+
</TRPCReactProvider>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
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
|
+
NEXTAUTH_URL: z.string().url(),
|
|
14
|
+
NEXTAUTH_SECRET: z.string().min(1),
|
|
15
|
+
MIDDLE_OIDC_CLIENT_ID: z.string().min(1),
|
|
16
|
+
MIDDLE_OIDC_CLIENT_SECRET: z.string().min(1),
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 在此处指定客户端环境变量模式。
|
|
21
|
+
* 这样可以确保应用程序不会使用无效的环境变量构建。
|
|
22
|
+
* 要将它们暴露给客户端,请使用 `NEXT_PUBLIC_` 前缀。
|
|
23
|
+
*/
|
|
24
|
+
client: {
|
|
25
|
+
NEXT_PUBLIC_MIDDLE_API_URL: z.string().url(),
|
|
26
|
+
NEXT_PUBLIC_MIDDLE_APPID: z.string().min(1),
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 在这里显式传入运行时环境变量的实际值。
|
|
31
|
+
* `server` / `client` 负责声明校验规则,
|
|
32
|
+
* `runtimeEnv` 负责把 `process.env` 中对应的值提供给 `createEnv`。
|
|
33
|
+
*/
|
|
34
|
+
runtimeEnv: {
|
|
35
|
+
NODE_ENV: process.env.NODE_ENV,
|
|
36
|
+
// 数据库
|
|
37
|
+
DATABASE_URL: process.env.DATABASE_URL,
|
|
38
|
+
NEXTAUTH_URL: process.env.NEXTAUTH_URL,
|
|
39
|
+
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
|
|
40
|
+
NEXT_PUBLIC_MIDDLE_API_URL: process.env.NEXT_PUBLIC_MIDDLE_API_URL,
|
|
41
|
+
NEXT_PUBLIC_MIDDLE_APPID: process.env.NEXT_PUBLIC_MIDDLE_APPID,
|
|
42
|
+
MIDDLE_OIDC_CLIENT_ID: process.env.MIDDLE_OIDC_CLIENT_ID,
|
|
43
|
+
MIDDLE_OIDC_CLIENT_SECRET: process.env.MIDDLE_OIDC_CLIENT_SECRET,
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 使用 `SKIP_ENV_VALIDATION` 运行 `build` 或 `dev` 可以跳过环境变量验证。
|
|
48
|
+
* 这在 Docker 构建时特别有用。
|
|
49
|
+
*/
|
|
50
|
+
skipValidation: !!process.env.SKIP_ENV_VALIDATION,
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 使空字符串被视为未定义。
|
|
54
|
+
* 例如:`SOME_VAR: z.string()` 和 `SOME_VAR=''` 将抛出错误。
|
|
55
|
+
*/
|
|
56
|
+
emptyStringAsUndefined: true,
|
|
57
|
+
})
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createMiddleNextAuthOptions, type MiddleNextAuthSessionFields } from '@wzyjs/middle-sdk/login'
|
|
2
|
+
|
|
3
|
+
import { env } from '@/env'
|
|
4
|
+
|
|
5
|
+
declare module 'next-auth' {
|
|
6
|
+
interface Session extends MiddleNextAuthSessionFields {
|
|
7
|
+
user: {
|
|
8
|
+
id: string
|
|
9
|
+
name?: string | null
|
|
10
|
+
email?: string | null
|
|
11
|
+
image?: string | null
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const auth = createMiddleNextAuthOptions({
|
|
17
|
+
cookiePrefix: '__WZYJS_APP_PACKAGE_NAME__',
|
|
18
|
+
clientId: env.MIDDLE_OIDC_CLIENT_ID,
|
|
19
|
+
clientSecret: env.MIDDLE_OIDC_CLIENT_SECRET,
|
|
20
|
+
secret: env.NEXTAUTH_SECRET,
|
|
21
|
+
issuer: env.NEXT_PUBLIC_MIDDLE_API_URL,
|
|
22
|
+
pages: {
|
|
23
|
+
signIn: '/auth/signin',
|
|
24
|
+
error: '/auth/error',
|
|
25
|
+
},
|
|
26
|
+
})
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { getServerSession } from 'next-auth'
|
|
2
|
+
|
|
3
|
+
import { auth } from '@/server/auth/next-auth/options'
|
|
4
|
+
import { db } from '@/server/db/client'
|
|
5
|
+
|
|
6
|
+
import { enhance } from '../generated/enhancer/enhance'
|
|
7
|
+
|
|
8
|
+
export const createTRPCContext = async (opts?: { req?: Request }) => {
|
|
9
|
+
const session = await getServerSession(auth)
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
// db, 严禁使用这个模块,而应该使用 prisma
|
|
13
|
+
session,
|
|
14
|
+
req: opts?.req,
|
|
15
|
+
headers: opts?.req?.headers,
|
|
16
|
+
prisma: enhance(db, {
|
|
17
|
+
user: session?.user?.id ? { id: session.user.id } : undefined,
|
|
18
|
+
}),
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type TRPCContext = Awaited<ReturnType<typeof createTRPCContext>>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { TRPCError } from '@trpc/server'
|
|
2
|
+
|
|
3
|
+
import { createTRPCProcedure } from './init'
|
|
4
|
+
|
|
5
|
+
export const publicProcedure = createTRPCProcedure
|
|
6
|
+
|
|
7
|
+
export const procedure = createTRPCProcedure.use(({ ctx, next }) => {
|
|
8
|
+
if (!ctx.session?.user?.id) {
|
|
9
|
+
throw new TRPCError({
|
|
10
|
+
code: 'UNAUTHORIZED',
|
|
11
|
+
message: '请先登录',
|
|
12
|
+
})
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return next()
|
|
16
|
+
})
|