@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.
- package/dist/index.js +350 -35
- package/package.json +6 -4
- 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 +88 -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,29 @@
|
|
|
1
|
+
// 只要 Next 读取配置,这个文件就会先执行一次。如果环境变量缺了或格式不对,项目会尽早报错,而不是等运行到某个接口时才炸。
|
|
2
|
+
import './src/env.js'
|
|
3
|
+
|
|
4
|
+
/** @type {import('next').NextConfig} */
|
|
5
|
+
const config = {
|
|
6
|
+
output: 'standalone', // 减小打包体积
|
|
7
|
+
logging: {
|
|
8
|
+
incomingRequests: false, // 控制台不显示每次请求的日志
|
|
9
|
+
},
|
|
10
|
+
poweredByHeader: false, // 响应头里不暴露 x-powered-by: Next.js
|
|
11
|
+
productionBrowserSourceMaps: false, // 生产环境不生成浏览器端的 .map 文件
|
|
12
|
+
// transpilePackages: [], // 即使这个包在 node_modules 里,也要把它当源码重新编译
|
|
13
|
+
experimental: {
|
|
14
|
+
serverSourceMaps: false, // 不生成服务端的 .map 文件
|
|
15
|
+
},
|
|
16
|
+
// 裁掉 Prisma 未使用的 wasm/edge runtime,保留 native query engine,减小 standalone 体积
|
|
17
|
+
outputFileTracingExcludes: {
|
|
18
|
+
'/**': [
|
|
19
|
+
'node_modules/.bun/@prisma+client@*/node_modules/@prisma/client/runtime/query_*',
|
|
20
|
+
'node_modules/.bun/@prisma+client@*/node_modules/@prisma/client/runtime/wasm-*',
|
|
21
|
+
'node_modules/.bun/@prisma+client@*/node_modules/@prisma/client/runtime/edge*',
|
|
22
|
+
'node_modules/.bun/@prisma+client@*/node_modules/.prisma/client/query_engine_bg.*',
|
|
23
|
+
'node_modules/.bun/@prisma+client@*/node_modules/.prisma/client/wasm*',
|
|
24
|
+
'node_modules/.bun/@prisma+client@*/node_modules/.prisma/client/edge.js',
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default config
|
|
@@ -0,0 +1,53 @@
|
|
|
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
|
+
"@prisma/client": "^6.14.0",
|
|
19
|
+
"@t3-oss/env-nextjs": "^0.10.1",
|
|
20
|
+
"@tanstack/react-query": "^5.50.0",
|
|
21
|
+
"@trpc/client": "^11.7.0",
|
|
22
|
+
"@trpc/next": "^11.7.0",
|
|
23
|
+
"@trpc/react-query": "^11.7.0",
|
|
24
|
+
"@trpc/server": "^11.7.0",
|
|
25
|
+
"@wzyjs/hooks": "workspace:*",
|
|
26
|
+
"@wzyjs/next": "workspace:*",
|
|
27
|
+
"@wzyjs/uis": "workspace:*",
|
|
28
|
+
"@wzyjs/utils": "workspace:*",
|
|
29
|
+
"@zenstackhq/runtime": "2.22.1",
|
|
30
|
+
"@zenstackhq/trpc": "2.22.1",
|
|
31
|
+
"antd": "^6.3.1",
|
|
32
|
+
"next": "^16.2.4",
|
|
33
|
+
"react": "^19.1.0",
|
|
34
|
+
"react-dom": "^19.1.0",
|
|
35
|
+
"server-only": "^0.0.1",
|
|
36
|
+
"superjson": "^2.2.1",
|
|
37
|
+
"zenstack": "2.22.1",
|
|
38
|
+
"zod": "^3.23.3",
|
|
39
|
+
"zod-validation-error": "^3.4.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/node": "^20",
|
|
43
|
+
"@types/react": "19.1.10",
|
|
44
|
+
"@types/react-dom": "^19",
|
|
45
|
+
"eslint": "^9",
|
|
46
|
+
"eslint-config-next": "^16.2.4",
|
|
47
|
+
"postcss": "^8.4.39",
|
|
48
|
+
"prisma": "^6.14.0",
|
|
49
|
+
"prisma-json-types-generator": "^3.3.0",
|
|
50
|
+
"tailwindcss": "^3.4.3",
|
|
51
|
+
"typescript": "^5"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//////////////////////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// DO NOT MODIFY THIS FILE //
|
|
3
|
+
// This file is automatically generated by ZenStack CLI and should not be manually updated. //
|
|
4
|
+
//////////////////////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
|
|
6
|
+
datasource db {
|
|
7
|
+
provider = "postgresql"
|
|
8
|
+
url = env("DATABASE_URL")
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
generator client {
|
|
12
|
+
provider = "prisma-client-js"
|
|
13
|
+
output = "../src/server/generated/prisma-client"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
generator json {
|
|
17
|
+
provider = "prisma-json-types-generator"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
model DemoItem {
|
|
21
|
+
id String @id() @default(cuid())
|
|
22
|
+
createdAt DateTime @default(now())
|
|
23
|
+
updatedAt DateTime? @updatedAt()
|
|
24
|
+
sort Int?
|
|
25
|
+
enabled Boolean @default(true)
|
|
26
|
+
isDeleted Boolean @default(false)
|
|
27
|
+
title String
|
|
28
|
+
description String
|
|
29
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "./models/demo/DemoItem"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { NextRequest } from 'next/server'
|
|
2
|
+
import { fetchRequestHandler } from '@trpc/server/adapters/fetch'
|
|
3
|
+
|
|
4
|
+
import { apiRouter } from '@/server/trpc'
|
|
5
|
+
import { createTRPCContext } from '@/server/trpc/context'
|
|
6
|
+
import { cors } from '@/server/utils'
|
|
7
|
+
|
|
8
|
+
const handler = async (req: NextRequest) => {
|
|
9
|
+
const response = await fetchRequestHandler({
|
|
10
|
+
req,
|
|
11
|
+
router: apiRouter,
|
|
12
|
+
endpoint: '/api/trpc',
|
|
13
|
+
createContext: () => createTRPCContext({ req }),
|
|
14
|
+
onError: ({ path, error }) => {
|
|
15
|
+
console.error(666, `❌ ${req.method} ${req.nextUrl.pathname} failed on ${path ?? '<no-path>'}: ${error.message}`)
|
|
16
|
+
},
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
return cors.applyCorsHeaders(req, response)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const OPTIONS = cors.createCorsPreflightResponse
|
|
23
|
+
|
|
24
|
+
export { handler as GET, handler as POST, OPTIONS }
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import Link from 'next/link'
|
|
4
|
+
|
|
5
|
+
import { Button, PanelPage, Result, StopOutlined, Typography } from '@/components'
|
|
6
|
+
|
|
7
|
+
export default function DisabledPage() {
|
|
8
|
+
return (
|
|
9
|
+
<PanelPage eyebrow='Access Disabled' title='账号已禁用'>
|
|
10
|
+
<Result
|
|
11
|
+
status='403'
|
|
12
|
+
icon={<StopOutlined style={{ color: '#f5222d' }} />}
|
|
13
|
+
title='当前账号已被禁用'
|
|
14
|
+
subTitle={(
|
|
15
|
+
<Typography.Paragraph className='!mb-0 !text-sm !leading-6 !text-slate-600 dark:!text-slate-300'>
|
|
16
|
+
登录模块接入后,这里会展示当前账号在应用里的禁用状态。
|
|
17
|
+
</Typography.Paragraph>
|
|
18
|
+
)}
|
|
19
|
+
extra={(
|
|
20
|
+
<Link href='/auth/signin'>
|
|
21
|
+
<Button type='primary' danger className='!rounded-lg'>
|
|
22
|
+
返回登录页
|
|
23
|
+
</Button>
|
|
24
|
+
</Link>
|
|
25
|
+
)}
|
|
26
|
+
/>
|
|
27
|
+
</PanelPage>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import Link from 'next/link'
|
|
4
|
+
import { useSearchParams } from 'next/navigation'
|
|
5
|
+
|
|
6
|
+
import { Button, PanelPage, Result, Typography } from '@/components'
|
|
7
|
+
|
|
8
|
+
const errorMessageMap: Record<string, string> = {
|
|
9
|
+
OAuthSignin: '无法发起登录,请稍后再试。',
|
|
10
|
+
OAuthCallback: '登录回调失败,请检查回调地址配置。',
|
|
11
|
+
OAuthCreateAccount: '创建登录账号失败,请稍后再试。',
|
|
12
|
+
EmailCreateAccount: '创建登录账号失败,请稍后再试。',
|
|
13
|
+
Callback: '登录流程中断,请重新发起登录。',
|
|
14
|
+
OAuthAccountNotLinked: '当前邮箱已绑定其他登录方式。',
|
|
15
|
+
EmailSignin: '邮件登录失败。',
|
|
16
|
+
CredentialsSignin: '账号校验失败。',
|
|
17
|
+
SessionRequired: '当前页面需要先登录。',
|
|
18
|
+
default: '登录失败,请稍后重试。',
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default function AuthErrorPage() {
|
|
22
|
+
const searchParams = useSearchParams()
|
|
23
|
+
const error = searchParams.get('error') || 'default'
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<PanelPage eyebrow='Authentication Exception' title='登录失败'>
|
|
27
|
+
<Result
|
|
28
|
+
status='error'
|
|
29
|
+
title='本次认证没有成功完成'
|
|
30
|
+
subTitle={errorMessageMap[error]}
|
|
31
|
+
extra={[
|
|
32
|
+
<Link href='/auth/signin' key='signin'>
|
|
33
|
+
<Button type='primary' className='!rounded-lg px-6'>
|
|
34
|
+
返回登录页
|
|
35
|
+
</Button>
|
|
36
|
+
</Link>,
|
|
37
|
+
]}
|
|
38
|
+
/>
|
|
39
|
+
|
|
40
|
+
<div className='mx-4 mb-4 rounded-lg bg-white/70 p-5 dark:bg-white/5 sm:mx-6'>
|
|
41
|
+
<Typography.Paragraph className='!mb-0 text-center !text-sm !leading-6 !text-slate-500 dark:!text-slate-300'>
|
|
42
|
+
登录模块接入后,请检查对应的客户端配置、回调地址和会话密钥。
|
|
43
|
+
</Typography.Paragraph>
|
|
44
|
+
</div>
|
|
45
|
+
</PanelPage>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Suspense } from 'react'
|
|
4
|
+
|
|
5
|
+
import { useSearchParams } from 'next/navigation'
|
|
6
|
+
|
|
7
|
+
import { Button, PanelPage, Space, Spin, Typography } from '@/components'
|
|
8
|
+
|
|
9
|
+
const { Text } = Typography
|
|
10
|
+
|
|
11
|
+
const SignInPage = () => {
|
|
12
|
+
const searchParams = useSearchParams()
|
|
13
|
+
const error = searchParams.get('error')
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<PanelPage
|
|
17
|
+
eyebrow='Secure Sign In'
|
|
18
|
+
title='账号登录'
|
|
19
|
+
description='登录模块接入后,这里会展示对应的登录入口。'
|
|
20
|
+
>
|
|
21
|
+
<Space orientation='vertical' size='large' className='w-full'>
|
|
22
|
+
{error ? (
|
|
23
|
+
<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'>
|
|
24
|
+
<div className='text-sm font-semibold text-red-700 dark:text-red-300'>
|
|
25
|
+
登录流程没有完成
|
|
26
|
+
</div>
|
|
27
|
+
<p className='mt-2 text-sm leading-6 text-red-600 dark:text-red-200'>
|
|
28
|
+
请重试一次;如果问题持续出现,优先检查登录配置和回调地址。
|
|
29
|
+
</p>
|
|
30
|
+
</div>
|
|
31
|
+
) : null}
|
|
32
|
+
|
|
33
|
+
<Button type='primary' size='large' className='!h-12 !rounded-lg !text-sm !font-semibold shadow-sm' block disabled>
|
|
34
|
+
登录模块待接入
|
|
35
|
+
</Button>
|
|
36
|
+
|
|
37
|
+
<div className='rounded-lg bg-white/70 p-5 text-center dark:bg-white/5'>
|
|
38
|
+
<Text className='text-sm leading-6 text-slate-500 dark:text-slate-300'>
|
|
39
|
+
基础模板只保留页面骨架。使用 `google` 或 `middle` 模板后会替换为真实登录流程。
|
|
40
|
+
</Text>
|
|
41
|
+
</div>
|
|
42
|
+
</Space>
|
|
43
|
+
</PanelPage>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default function SignIn() {
|
|
48
|
+
return (
|
|
49
|
+
<Suspense fallback={<Spin />}>
|
|
50
|
+
<SignInPage />
|
|
51
|
+
</Suspense>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useRouter, useSearchParams } from 'next/navigation'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
ArrowRightOutlined,
|
|
7
|
+
Button,
|
|
8
|
+
PanelPage,
|
|
9
|
+
Result,
|
|
10
|
+
RetweetOutlined,
|
|
11
|
+
StopOutlined,
|
|
12
|
+
Typography,
|
|
13
|
+
} from '@/components'
|
|
14
|
+
|
|
15
|
+
import { getSafeCallbackPath } from '@/utils'
|
|
16
|
+
|
|
17
|
+
export default function UnauthorizedPage() {
|
|
18
|
+
const router = useRouter()
|
|
19
|
+
const searchParams = useSearchParams()
|
|
20
|
+
|
|
21
|
+
const retryUrl = getSafeCallbackPath(searchParams.get('callbackUrl'), '/')
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<PanelPage eyebrow='Access Control' title='访问受限'>
|
|
25
|
+
<Result
|
|
26
|
+
status='403'
|
|
27
|
+
title='当前账号暂无访问权限'
|
|
28
|
+
icon={<StopOutlined style={{ color: '#f5222d' }} />}
|
|
29
|
+
subTitle={(
|
|
30
|
+
<Typography.Paragraph className='!mb-0 !text-sm !leading-6 !text-slate-600 dark:!text-slate-300'>
|
|
31
|
+
登录模块接入后,这里会展示当前账号的权限状态。
|
|
32
|
+
</Typography.Paragraph>
|
|
33
|
+
)}
|
|
34
|
+
extra={(
|
|
35
|
+
<div className='flex w-full flex-col gap-3 sm:flex-row sm:justify-center'>
|
|
36
|
+
<Button
|
|
37
|
+
type='primary'
|
|
38
|
+
icon={<RetweetOutlined />}
|
|
39
|
+
className='!rounded-lg'
|
|
40
|
+
onClick={() => {
|
|
41
|
+
router.push(retryUrl)
|
|
42
|
+
}}
|
|
43
|
+
>
|
|
44
|
+
重试
|
|
45
|
+
</Button>
|
|
46
|
+
<Button
|
|
47
|
+
icon={<ArrowRightOutlined />}
|
|
48
|
+
className='!rounded-lg'
|
|
49
|
+
onClick={() => {
|
|
50
|
+
router.push('/')
|
|
51
|
+
}}
|
|
52
|
+
>
|
|
53
|
+
回到首页
|
|
54
|
+
</Button>
|
|
55
|
+
</div>
|
|
56
|
+
)}
|
|
57
|
+
/>
|
|
58
|
+
</PanelPage>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Button } from '@/components'
|
|
4
|
+
import { api } from '@/utils'
|
|
5
|
+
|
|
6
|
+
export default function DemoPage() {
|
|
7
|
+
const itemsQuery = api.demoItem.findMany.useQuery({
|
|
8
|
+
orderBy: { createdAt: 'desc' },
|
|
9
|
+
select: {
|
|
10
|
+
id: true,
|
|
11
|
+
title: true,
|
|
12
|
+
description: true,
|
|
13
|
+
},
|
|
14
|
+
take: 20,
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
const createItem = api.demoItem.create.useMutation({
|
|
18
|
+
onSuccess: () => {
|
|
19
|
+
void itemsQuery.refetch()
|
|
20
|
+
},
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const onCreate = () => {
|
|
24
|
+
void createItem.mutateAsync({
|
|
25
|
+
data: {
|
|
26
|
+
title: `示例记录 ${new Date().toLocaleTimeString('zh-CN')}`,
|
|
27
|
+
description: 'Created from middle template',
|
|
28
|
+
},
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div className='mx-auto max-w-3xl px-5 py-8'>
|
|
34
|
+
<div className='mb-6 flex items-center justify-between gap-4'>
|
|
35
|
+
<h1 className='text-2xl font-semibold'>Demo Items</h1>
|
|
36
|
+
<Button type='primary' loading={createItem.isPending} onClick={onCreate}>
|
|
37
|
+
新增示例
|
|
38
|
+
</Button>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
{itemsQuery.isLoading ? (
|
|
42
|
+
<div className='text-sm text-slate-500 dark:text-[#aab4c2]'>加载中...</div>
|
|
43
|
+
) : itemsQuery.data?.length ? (
|
|
44
|
+
<div className='space-y-3'>
|
|
45
|
+
{itemsQuery.data.map(item => (
|
|
46
|
+
<div key={item.id} className='rounded-lg border border-slate-200 bg-white p-4 dark:border-[#303846] dark:bg-[#171c23]'>
|
|
47
|
+
<div className='font-medium'>{item.title}</div>
|
|
48
|
+
<div className='mt-2 text-xs text-slate-500 dark:text-[#aab4c2]'>{item.description || '-'}</div>
|
|
49
|
+
</div>
|
|
50
|
+
))}
|
|
51
|
+
</div>
|
|
52
|
+
) : (
|
|
53
|
+
<div className='rounded-lg border border-slate-200 bg-white p-6 text-sm text-slate-500 dark:border-[#303846] dark:bg-[#171c23] dark:text-[#aab4c2]'>
|
|
54
|
+
暂无数据,点“新增示例”创建一条。
|
|
55
|
+
</div>
|
|
56
|
+
)}
|
|
57
|
+
</div>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type ReactNode } from 'react'
|
|
2
|
+
import { type Metadata } from 'next'
|
|
3
|
+
|
|
4
|
+
import { Providers } from '@/components/base/Providers'
|
|
5
|
+
import { Layout } from '@/components/base/Layout'
|
|
6
|
+
|
|
7
|
+
import '@/styles/globals.css'
|
|
8
|
+
|
|
9
|
+
export const metadata: Metadata = {
|
|
10
|
+
title: '__WZYJS_APP_DISPLAY_NAME__',
|
|
11
|
+
icons: [{ rel: 'icon', url: '/favicon.ico' }],
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface RootLayoutProps {
|
|
15
|
+
children: ReactNode
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default function RootLayout(props: RootLayoutProps) {
|
|
19
|
+
const { children } = props
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<html lang='zh-CN' suppressHydrationWarning>
|
|
23
|
+
<body suppressHydrationWarning>
|
|
24
|
+
<Providers>
|
|
25
|
+
<Layout>
|
|
26
|
+
{children}
|
|
27
|
+
</Layout>
|
|
28
|
+
</Providers>
|
|
29
|
+
</body>
|
|
30
|
+
</html>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import Link from 'next/link'
|
|
2
|
+
|
|
3
|
+
export default function NotFound() {
|
|
4
|
+
return (
|
|
5
|
+
<main className='flex min-h-screen items-center justify-center bg-slate-50 px-5 py-8 transition-colors dark:bg-[#14171c]'>
|
|
6
|
+
<section className='w-full max-w-xl rounded-lg border border-slate-200 bg-white p-8 text-center shadow-sm transition-colors dark:border-[#303846] dark:bg-[#1b2028]'>
|
|
7
|
+
<p className='text-sm font-semibold uppercase text-slate-400 dark:text-slate-500'>404</p>
|
|
8
|
+
<h1 className='mt-3 text-3xl font-semibold text-slate-950 dark:text-[#e8edf5]'>页面没有找到</h1>
|
|
9
|
+
<p className='mt-4 text-sm leading-6 text-slate-500 dark:text-[#aab4c2]'>
|
|
10
|
+
这个页面可能已经移动、删除,或者地址输入有误。
|
|
11
|
+
</p>
|
|
12
|
+
<Link
|
|
13
|
+
href='/'
|
|
14
|
+
className='mt-6 inline-flex h-10 items-center rounded-lg bg-slate-950 px-4 text-sm font-medium text-white transition-colors hover:bg-slate-800 dark:bg-[#e8edf5] dark:text-[#14171c] dark:hover:bg-white'
|
|
15
|
+
>
|
|
16
|
+
回到首页
|
|
17
|
+
</Link>
|
|
18
|
+
</section>
|
|
19
|
+
</main>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
@@ -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
|
+
}
|