├── app ├── favicon.ico ├── (client) │ ├── [locale] │ │ ├── dashboard │ │ │ ├── page.js │ │ │ └── layout.js │ │ ├── page.js │ │ ├── (auth) │ │ │ └── login │ │ │ │ └── page.js │ │ └── layout.js │ └── actions │ │ ├── test-actions.js │ │ └── user.js ├── api │ ├── auth │ │ └── [...all] │ │ │ └── route.js │ └── v1 │ │ ├── pub │ │ └── cms │ │ │ └── getList │ │ │ └── route.js │ │ ├── auth │ │ └── user │ │ │ └── profile │ │ │ └── route.js │ │ └── sys │ │ └── test │ │ └── route.js ├── (admin) │ ├── admin │ │ ├── [...slug] │ │ │ └── page.js │ │ ├── system │ │ │ ├── settings │ │ │ │ └── page.js │ │ │ ├── usage │ │ │ │ └── page.js │ │ │ └── login_logs │ │ │ │ └── page.js │ │ └── example │ │ │ └── data-table │ │ │ └── data-table-basic │ │ │ └── page.js │ ├── not-found.js │ ├── actions │ │ ├── system │ │ │ ├── admin-login-logs.js │ │ │ ├── upload-actions.js │ │ │ ├── admin-action-logs.js │ │ │ └── crud-action.assets.js │ │ ├── dashboard │ │ │ └── dashboard-stats.js │ │ ├── cms │ │ │ └── crud-action.post.js │ │ ├── README.md │ │ └── rbac │ │ │ └── user-permissions.js │ └── layout.js ├── page.js └── globals.css ├── public ├── logo.png ├── screenshots │ └── admin-preview.png ├── vercel.svg ├── logo.svg ├── nextjsbase.svg ├── window.svg ├── file.svg ├── globe.svg └── next.svg ├── jsconfig.json ├── postcss.config.mjs ├── lib ├── utils.js ├── validation │ └── index.js ├── upload │ ├── index.js │ ├── use-upload.js │ └── upload-guard.js ├── auth │ ├── auth-client.js │ ├── README.md │ ├── auth.js │ ├── admin-auth.js │ └── page-auth.js ├── function │ ├── deepClone.js │ ├── deepMerge.js │ ├── debounce.js │ ├── throttle.js │ ├── category-utils.js │ ├── nb.filters.js │ └── queryParams.js ├── database │ └── prisma.js ├── api │ ├── api-context.js │ ├── action-client.js │ └── fetch-client.js ├── crud │ ├── search-transformer.js │ └── README.md └── core │ └── crud-helper.js ├── .vscode └── settings.json ├── components ├── common │ ├── theme-provider.jsx │ ├── logo.js │ ├── LanguageSwitcherSimple.jsx │ └── LanguageSwitcher.jsx ├── ui │ ├── skeleton.jsx │ ├── label.jsx │ ├── separator.jsx │ ├── input.jsx │ ├── sonner.jsx │ ├── spotlight-card.jsx │ ├── tooltip.jsx │ ├── animated-theme-toggler.jsx │ ├── button.jsx │ ├── card.jsx │ └── sheet.jsx ├── admin │ ├── uploads │ │ └── index.js │ ├── smart-form │ │ └── index.js │ ├── markdown-editor.jsx │ ├── antd-config-provider.jsx │ └── page-access-guard.jsx ├── client │ ├── home │ │ ├── section-header.jsx │ │ ├── features.jsx │ │ └── cta-section.jsx │ └── layout │ │ ├── sidebar │ │ ├── app-sidebar.jsx │ │ ├── nav-menu.jsx │ │ └── user-menu.jsx │ │ ├── footer.jsx │ │ └── navbar │ │ └── navbar.jsx └── motion │ └── decrypted-text.jsx ├── prisma.config.ts ├── VERSION.md ├── eslint.config.mjs ├── .gitmessage ├── i18n ├── config.js ├── request.js └── messages │ ├── zh.json │ └── en.json ├── components.json ├── hooks ├── use-mobile.js └── use-permission.js ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── .gitignore ├── next.config.mjs ├── config └── admin-pages.js ├── .env.example ├── package.json ├── .cursorrules ├── README.zh-CN.md ├── README.md └── scripts └── setup-admin.js /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oldwarma/nextjs-base/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oldwarma/nextjs-base/HEAD/public/logo.png -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "paths": { 4 | "@/*": ["./*"] 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/(client)/[locale]/dashboard/page.js: -------------------------------------------------------------------------------- 1 | export default function DashboardPage() { 2 | return
DashboardPage
; 3 | } -------------------------------------------------------------------------------- /public/screenshots/admin-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oldwarma/nextjs-base/HEAD/public/screenshots/admin-preview.png -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: { 3 | "@tailwindcss/postcss": {}, 4 | }, 5 | }; 6 | 7 | export default config; 8 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | import { clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "i18n-ally.localesPaths": [ 3 | "i18n", 4 | "messages" 5 | ], 6 | "i18n-ally.keystyle": "nested", 7 | "WillLuke.nextjs.hasPrompted": true 8 | } -------------------------------------------------------------------------------- /app/api/auth/[...all]/route.js: -------------------------------------------------------------------------------- 1 | import { auth } from '@/lib/auth/auth'; // path to your auth file 2 | import { toNextJsHandler } from 'better-auth/next-js'; 3 | export const { POST, GET } = toNextJsHandler(auth); 4 | -------------------------------------------------------------------------------- /app/(admin)/admin/[...slug]/page.js: -------------------------------------------------------------------------------- 1 | import { notFound } from 'next/navigation'; 2 | 3 | /** 4 | * 捕获所有不存在的 /admin/* 路径 5 | * 触发 not-found.js 显示自定义 404 页面 6 | */ 7 | export default function CatchAllAdminPage() { 8 | notFound(); 9 | } 10 | -------------------------------------------------------------------------------- /app/page.js: -------------------------------------------------------------------------------- 1 | import { redirect } from 'next/navigation'; 2 | import { defaultLocale } from '@/i18n/config'; 3 | 4 | /** 5 | * 根页面 - 自动重定向到默认语言 6 | */ 7 | export default function RootPage() { 8 | // 这个页面实际上不会被渲染,因为中间件会处理重定向 9 | // 但为了防止某些情况下中间件失效,这里也做一个重定向 10 | redirect(`/${defaultLocale}`); 11 | } 12 | -------------------------------------------------------------------------------- /lib/validation/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 验证模块入口 3 | * 4 | * 提供自动 Schema 转换功能,将简单的 validation 配置自动转换为 Zod Schema 5 | */ 6 | 7 | export { 8 | validationToZod, 9 | getOrCreateSchema, 10 | validateWithConfig, 11 | runCustomValidators, 12 | } from './auto-schema.js'; 13 | 14 | // 导出 zod 供高级用户直接使用 15 | export { z } from 'zod'; 16 | -------------------------------------------------------------------------------- /components/common/theme-provider.jsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { ThemeProvider as NextThemesProvider } from 'next-themes'; 4 | 5 | /** 6 | * 主题提供者组件 7 | * 支持深色/浅色模式切换 8 | */ 9 | export function ThemeProvider({ children, ...props }) { 10 | return {children}; 11 | } 12 | 13 | 14 | -------------------------------------------------------------------------------- /components/ui/skeleton.jsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }) { 7 | return ( 8 |
12 | ); 13 | } 14 | 15 | export { Skeleton } 16 | -------------------------------------------------------------------------------- /prisma.config.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import { defineConfig } from 'prisma/config'; 3 | 4 | export default defineConfig({ 5 | earlyAccess: true, 6 | schema: 'prisma/schema.prisma', 7 | migrate: { 8 | migrations: 'prisma/migrations', 9 | }, 10 | datasource: { 11 | url: process.env.DATABASE_URL!, 12 | }, 13 | }); 14 | 15 | -------------------------------------------------------------------------------- /VERSION.md: -------------------------------------------------------------------------------- 1 | # Version 2 | 3 | - Current: v0.1.0 (first open-source release) 4 | - Highlights: simple landing page + admin demo, pre-seeded roles/users to explore RBAC, core CRUD/logging flows intact. 5 | - Source of truth: `package.json#version`. 6 | - When bumping: update `package.json`, `VERSION.md`, and the version badge/history in `docs/README.md`. 7 | -------------------------------------------------------------------------------- /app/(admin)/not-found.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button, Result } from 'antd'; 3 | 4 | const NotFound = () => { 5 | return ( 6 | Back Home} 11 | /> 12 | ); 13 | }; 14 | 15 | export default NotFound; 16 | -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/nextjsbase.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/admin/uploads/index.js: -------------------------------------------------------------------------------- 1 | export { default as SingleImageUpload } from './single-image-upload'; 2 | export { default as MultiImageUpload } from './multi-image-upload'; 3 | export { default as AvatarUpload } from './avatar-upload'; 4 | export { default as FileUpload } from './file-upload'; 5 | export { default as FileSelect } from './file-select'; 6 | export { default as FileSelectModal } from './file-select-modal'; 7 | 8 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig, globalIgnores } from "eslint/config"; 2 | import nextVitals from "eslint-config-next/core-web-vitals"; 3 | 4 | const eslintConfig = defineConfig([ 5 | ...nextVitals, 6 | // Override default ignores of eslint-config-next. 7 | globalIgnores([ 8 | // Default ignores of eslint-config-next: 9 | ".next/**", 10 | "out/**", 11 | "build/**", 12 | "next-env.d.ts", 13 | ]), 14 | ]); 15 | 16 | export default eslintConfig; 17 | -------------------------------------------------------------------------------- /.gitmessage: -------------------------------------------------------------------------------- 1 | # <类型>: <简短描述(不超过 50 字)> 2 | # 3 | # [可选的详细说明] 4 | # 5 | # 类型可以是: 6 | # feat - 新功能 7 | # fix - 修复 bug 8 | # docs - 文档改动 9 | # style - 代码格式调整 10 | # refactor - 代码重构 11 | # perf - 性能优化 12 | # test - 测试相关 13 | # chore - 构建、依赖、配置等 14 | # ui - UI/UX 改动 15 | # i18n - 多语言相关 16 | # security - 安全相关 17 | # 18 | # 示例: 19 | # feat: 添加用户积分系统 20 | # fix: 修复图片生成失败的问题 21 | # docs: 更新 API 使用文档 22 | # refactor: 优化数据库连接逻辑 23 | 24 | -------------------------------------------------------------------------------- /lib/upload/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 上传模块统一导出 3 | */ 4 | 5 | // 服务端模块(R2 客户端) 6 | export { checkR2Config, getR2Client, generateFileKey, getPublicUrl, uploadToR2, deleteFromR2, getFromR2, getR2Config } from './r2-client'; 7 | 8 | // 服务端模块(上传服务) 9 | export { validateFile, uploadFile, uploadFiles, deleteFile, getUserUploads, getUploadTypeConfig } from './upload-service'; 10 | 11 | // 客户端模块(上传 Hook) 12 | export { uploadSingleFile, uploadMultipleFiles, createCustomRequest, checkUploadService } from './use-upload'; 13 | -------------------------------------------------------------------------------- /lib/auth/auth-client.js: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { createAuthClient } from 'better-auth/react'; 4 | import { adminClient, usernameClient } from 'better-auth/client/plugins'; 5 | 6 | export const authClient = createAuthClient({ 7 | baseURL: process.env.NEXT_PUBLIC_BETTER_AUTH_URL || 'http://localhost:3000', 8 | plugins: [adminClient(), usernameClient()], 9 | }); 10 | 11 | // 导出常用的方法,但完整的 authClient 也可以直接使用 12 | export const { useSession, signIn, signOut, signUp, changePassword, changeEmail, updateUser, deleteUser } = authClient; -------------------------------------------------------------------------------- /components/client/home/section-header.jsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | /** 4 | * Section Header 组件 5 | * 用于所有首页 section 的标题和副标题,保持一致的样式 6 | */ 7 | export default function SectionHeader({ title, subtitle, className = '' }) { 8 | return ( 9 |
10 |

11 | {title} 12 |

13 | {subtitle && ( 14 |

15 | {subtitle} 16 |

17 | )} 18 |
19 | ); 20 | } 21 | 22 | -------------------------------------------------------------------------------- /i18n/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * i18n 配置文件 3 | */ 4 | 5 | // 支持的语言列表 6 | export const locales = ['en', 'zh']; 7 | 8 | // 默认语言 9 | export const defaultLocale = 'en'; 10 | 11 | // 语言名称(用于语言切换器) 12 | // flagCode 使用 ISO 3166-1 alpha-2 国家代码(小写),对应 flag-icons 的类名 13 | export const localeNames = { 14 | en: { 15 | name: 'English', 16 | shortName: 'EN', 17 | flagCode: 'us', // 美国国旗 18 | }, 19 | zh: { 20 | name: '简体中文', 21 | shortName: 'CN', 22 | flagCode: 'cn', // 中国国旗 23 | } 24 | }; 25 | 26 | // 语言方向(从左到右 ltr / 从右到左 rtl) 27 | export const localeDirections = { 28 | en: 'ltr', 29 | zh: 'ltr' 30 | }; 31 | -------------------------------------------------------------------------------- /app/(client)/[locale]/dashboard/layout.js: -------------------------------------------------------------------------------- 1 | import { AppSidebar } from '@/components/client/layout/sidebar/app-sidebar'; 2 | import { auth } from '@/lib/auth/auth'; 3 | import { headers } from 'next/headers'; 4 | 5 | export default async function GenerateLayout({ children }) { 6 | // 获取用户 session 7 | const session = await auth.api.getSession({ 8 | headers: await headers(), 9 | }); 10 | 11 | return ( 12 |
13 | 14 |
{children}
15 |
16 | ); 17 | } 18 | 19 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": false, 6 | "tailwind": { 7 | "config": "", 8 | "css": "app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "iconLibrary": "lucide", 14 | "aliases": { 15 | "components": "@/components", 16 | "utils": "@/lib/utils", 17 | "ui": "@/components/ui", 18 | "lib": "@/lib", 19 | "hooks": "@/hooks" 20 | }, 21 | "registries": { 22 | "@magicui": "https://magicui.design/r/{name}.json" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /hooks/use-mobile.js: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | const MOBILE_BREAKPOINT = 768 4 | 5 | export function useIsMobile() { 6 | const [isMobile, setIsMobile] = React.useState(undefined) 7 | 8 | React.useEffect(() => { 9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) 10 | const onChange = () => { 11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) 12 | } 13 | mql.addEventListener("change", onChange) 14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) 15 | return () => mql.removeEventListener("change", onChange); 16 | }, []) 17 | 18 | return !!isMobile 19 | } 20 | -------------------------------------------------------------------------------- /i18n/request.js: -------------------------------------------------------------------------------- 1 | import { getRequestConfig } from 'next-intl/server'; 2 | import { cookies } from 'next/headers'; 3 | 4 | // 支持的语言列表 5 | export const locales = ['en', 'zh']; 6 | 7 | // 默认语言 8 | export const defaultLocale = 'en'; 9 | 10 | export default getRequestConfig(async ({ requestLocale }) => { 11 | // 获取请求的语言,如果没有则使用默认语言 12 | let locale = await requestLocale; 13 | 14 | // 验证语言是否支持 15 | if (!locale || !locales.includes(locale)) { 16 | locale = defaultLocale; 17 | } 18 | 19 | return { 20 | locale, 21 | messages: (await import(`./messages/${locale}.json`)).default, 22 | timeZone: 'UTC', 23 | now: new Date(), 24 | }; 25 | }); 26 | -------------------------------------------------------------------------------- /components/ui/label.jsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | function Label({ 9 | className, 10 | ...props 11 | }) { 12 | return ( 13 | 20 | ); 21 | } 22 | 23 | export { Label } 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /lib/function/deepClone.js: -------------------------------------------------------------------------------- 1 | // 判断arr是否为一个数组,返回一个bool值 2 | function isArray (arr) { 3 | return Object.prototype.toString.call(arr) === '[object Array]'; 4 | } 5 | 6 | // 深度克隆 7 | function deepClone (obj) { 8 | // 对常见的“非”值,直接返回原来值 9 | if([null, undefined, NaN, false].includes(obj)) return obj; 10 | if(typeof obj !== "object" && typeof obj !== 'function') { 11 | //原始类型直接返回 12 | return obj; 13 | } 14 | var o = isArray(obj) ? [] : {}; 15 | for(let i in obj) { 16 | if(obj.hasOwnProperty(i)){ 17 | o[i] = typeof obj[i] === "object" ? deepClone(obj[i]) : obj[i]; 18 | } 19 | } 20 | return o; 21 | } 22 | 23 | export default deepClone; 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env 35 | .env.local 36 | 37 | # vercel 38 | .vercel 39 | 40 | # typescript 41 | *.tsbuildinfo 42 | next-env.d.ts 43 | 44 | /lib/generated/prisma 45 | -------------------------------------------------------------------------------- /app/api/v1/pub/cms/getList/route.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 公开 API - 获取 CMS 列表 3 | * 4 | * 路径:/api/v1/pub/cms/getList 5 | * 权限:公开(无需登录,由 proxy 自动处理) 6 | */ 7 | 8 | import { NextResponse } from 'next/server'; 9 | 10 | export async function GET(request) { 11 | // 直接写业务逻辑,不需要权限检查代码! 12 | // proxy 已经根据路径 /api/v1/pub/* 自动放行 13 | 14 | try { 15 | // 业务逻辑 16 | const data = { 17 | list: [ 18 | { id: 1, title: '文章1' }, 19 | { id: 2, title: '文章2' }, 20 | ], 21 | total: 2, 22 | message: '公开 API 访问成功(无需登录)', 23 | }; 24 | 25 | return NextResponse.json({ success: true, data }); 26 | } catch (error) { 27 | return NextResponse.json( 28 | { success: false, error: error.message }, 29 | { status: 500 } 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/(client)/[locale]/page.js: -------------------------------------------------------------------------------- 1 | import Navbar from '@/components/client/layout/navbar/navbar'; 2 | import Hero from '@/components/client/home/hero'; 3 | import Features from '@/components/client/home/features'; 4 | import CodeShowcase from '@/components/client/home/code-showcase'; 5 | import CTASection from '@/components/client/home/cta-section'; 6 | import Footer from '@/components/client/layout/footer'; 7 | 8 | export default function HomePage() { 9 | return ( 10 |
11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /components/ui/separator.jsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SeparatorPrimitive from "@radix-ui/react-separator" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | function Separator({ 9 | className, 10 | orientation = "horizontal", 11 | decorative = true, 12 | ...props 13 | }) { 14 | return ( 15 | 24 | ); 25 | } 26 | 27 | export { Separator } 28 | -------------------------------------------------------------------------------- /app/api/v1/auth/user/profile/route.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 需要登录的 API - 获取用户资料 3 | * 4 | * 路径:/api/v1/auth/user/profile 5 | * 权限:需要登录(由 proxy 自动检查) 6 | */ 7 | 8 | import { NextResponse } from 'next/server'; 9 | import { getApiContext } from '@/lib/api/api-context'; 10 | 11 | export async function GET(request) { 12 | // proxy 已经验证登录,未登录会返回 401 13 | // 这里直接获取用户信息 14 | const { userId, userRole, isAdmin } = getApiContext(request); 15 | 16 | try { 17 | // 业务逻辑 18 | const profile = { 19 | id: userId, 20 | role: userRole, 21 | isAdmin, 22 | message: '成功获取用户资料(需要登录)', 23 | }; 24 | 25 | return NextResponse.json({ success: true, data: profile }); 26 | } catch (error) { 27 | return NextResponse.json( 28 | { success: false, error: error.message }, 29 | { status: 500 } 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/api/v1/sys/test/route.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 后台 API - 测试 3 | * 4 | * 路径:/api/v1/sys/test 5 | * 权限:需要后台权限(由 proxy 自动检查 RBAC) 6 | */ 7 | 8 | import { NextResponse } from 'next/server'; 9 | import { getApiContext } from '@/lib/api/api-context'; 10 | 11 | export async function GET(request) { 12 | // proxy 已经验证后台权限 13 | // - admin 直接通过 14 | // - 非 admin 需要 isBackendAllowed + RBAC 权限 15 | const { userId, userRole, isAdmin } = getApiContext(request); 16 | 17 | try { 18 | const data = { 19 | message: '成功访问后台 API(需要后台权限)', 20 | userId, 21 | userRole, 22 | isAdmin, 23 | timestamp: new Date().toISOString(), 24 | }; 25 | 26 | return NextResponse.json({ success: true, data }); 27 | } catch (error) { 28 | return NextResponse.json( 29 | { success: false, error: error.message }, 30 | { status: 500 } 31 | ); 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /lib/database/prisma.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Prisma 7 客户端单例 3 | * 使用 @prisma/adapter-pg 驱动适配器连接 PostgreSQL 4 | */ 5 | 6 | import { PrismaPg } from '@prisma/adapter-pg'; 7 | import { PrismaClient } from '@/lib/generated/prisma/client'; 8 | 9 | const globalForPrisma = globalThis; 10 | 11 | function createPrismaClient() { 12 | const connectionString = process.env.DATABASE_URL; 13 | 14 | if (!connectionString) { 15 | throw new Error('DATABASE_URL environment variable is not set'); 16 | } 17 | 18 | const adapter = new PrismaPg({ connectionString }); 19 | 20 | return new PrismaClient({ 21 | adapter, 22 | log: ['error'], 23 | errorFormat: 'minimal', 24 | }); 25 | } 26 | 27 | export const prisma = globalForPrisma.prisma ?? createPrismaClient(); 28 | 29 | if (process.env.NODE_ENV !== 'production') { 30 | globalForPrisma.prisma = prisma; 31 | } 32 | 33 | export default prisma; 34 | -------------------------------------------------------------------------------- /lib/function/deepMerge.js: -------------------------------------------------------------------------------- 1 | import deepClone from "./deepClone"; 2 | 3 | // JS对象深度合并 4 | function deepMerge(target = {}, source = {}) { 5 | target = deepClone(target); 6 | if (typeof target !== 'object' || typeof source !== 'object') return false; 7 | for (var prop in source) { 8 | if (!source.hasOwnProperty(prop)) continue; 9 | if (prop in target) { 10 | if (typeof target[prop] !== 'object') { 11 | target[prop] = source[prop]; 12 | } else { 13 | if (typeof source[prop] !== 'object') { 14 | target[prop] = source[prop]; 15 | } else { 16 | if (target[prop].concat && source[prop].concat) { 17 | target[prop] = target[prop].concat(source[prop]); 18 | } else { 19 | target[prop] = deepMerge(target[prop], source[prop]); 20 | } 21 | } 22 | } 23 | } else { 24 | target[prop] = source[prop]; 25 | } 26 | } 27 | return target; 28 | } 29 | 30 | export default deepMerge; -------------------------------------------------------------------------------- /components/common/logo.js: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils'; 2 | 3 | const Logo = ({ className, iconClassName, textClassName, showText = true }) => { 4 | return ( 5 |
6 | 11 | 12 | 13 | 14 | {showText && ( 15 |
16 | NEXT 17 | .JS 18 | Base 19 |
20 | )} 21 |
22 | ); 23 | }; 24 | 25 | export default Logo; 26 | -------------------------------------------------------------------------------- /public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /components/ui/input.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | function Input({ 6 | className, 7 | type, 8 | ...props 9 | }) { 10 | return ( 11 | 21 | ); 22 | } 23 | 24 | export { Input } 25 | -------------------------------------------------------------------------------- /lib/function/debounce.js: -------------------------------------------------------------------------------- 1 | let timeoutArr = []; 2 | /** 3 | * 防抖函数 4 | * 防抖原理:一定时间内,只有最后一次或第一次调用,回调函数才会执行 5 | * @param {Function} fn 要执行的回调函数 6 | * @param {Number} time 延时的时间 7 | * @param {Boolean} isImmediate 是否立即执行 默认true 8 | * @param {String} timeoutName 定时器ID 9 | * @return null 10 | nb.pubfn.debounce(() => { 11 | 12 | }, 1000); 13 | */ 14 | function debounce(fn, time = 500, isImmediate = true, timeoutName = "default") { 15 | // 清除定时器 16 | if(!timeoutArr[timeoutName]) timeoutArr[timeoutName] = null; 17 | if (timeoutArr[timeoutName] !== null) clearTimeout(timeoutArr[timeoutName]); 18 | // 立即执行一次 19 | if (isImmediate) { 20 | var callNow = !timeoutArr[timeoutName]; 21 | timeoutArr[timeoutName] = setTimeout(() => { 22 | timeoutArr[timeoutName] = null; 23 | }, time); 24 | if (callNow){ 25 | if(typeof fn === 'function') return fn(); 26 | } 27 | } else { 28 | // 设置定时器,当最后一次操作后,timeout不会再被清除,所以在延时time毫秒后执行fn回调方法 29 | timeoutArr[timeoutName] = setTimeout(() => { 30 | if(typeof fn === 'function') return fn(); 31 | }, time); 32 | } 33 | } 34 | export default debounce 35 | -------------------------------------------------------------------------------- /components/ui/sonner.jsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { 4 | CircleCheckIcon, 5 | InfoIcon, 6 | Loader2Icon, 7 | OctagonXIcon, 8 | TriangleAlertIcon, 9 | } from 'lucide-react'; 10 | import { useTheme } from 'next-themes'; 11 | import { Toaster as Sonner } from 'sonner'; 12 | 13 | const Toaster = ({ ...props }) => { 14 | const { theme = 'dark' } = useTheme(); 15 | 16 | return ( 17 | , 23 | info: , 24 | warning: , 25 | error: , 26 | loading: , 27 | }} 28 | toastOptions={{ 29 | style: { 30 | fontFamily: 'var(--font-harmony-os)', 31 | background: '#27272a', // zinc-800 32 | color: '#fafafa', // zinc-50 33 | border: '1px solid rgba(255, 255, 255, 0.1)', 34 | }, 35 | }} 36 | {...props} 37 | /> 38 | ); 39 | }; 40 | 41 | export { Toaster }; 42 | -------------------------------------------------------------------------------- /app/(client)/[locale]/(auth)/login/page.js: -------------------------------------------------------------------------------- 1 | import { LoginForm } from './login-form'; 2 | import Prism from '@/components/motion/prism-bg'; 3 | 4 | /** 5 | * 登录页面 6 | * 支持通过 callbackUrl 查询参数指定登录后的重定向地址 7 | * 例如:/login?callbackUrl=/generate/image 8 | */ 9 | export default async function LoginPage({ searchParams }) { 10 | // 从查询参数中获取回调地址,用于登录成功后重定向 11 | // Next.js 16+ 中 searchParams 可能是 Promise,需要 await 12 | const resolvedSearchParams = await searchParams; 13 | const callbackUrl = resolvedSearchParams?.callbackUrl || null; 14 | 15 | return ( 16 |
17 |
18 | 19 |
20 |
21 | 32 |
33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | import createNextIntlPlugin from 'next-intl/plugin'; 2 | 3 | const withNextIntl = createNextIntlPlugin('./i18n/request.js'); 4 | 5 | /** @type {import('next').NextConfig} */ 6 | const nextConfig = { 7 | /* config options here */ 8 | 9 | reactStrictMode: false, // 关闭后再重启 dev 服务器 10 | 11 | // 明确指定项目根目录 12 | turbopack: { 13 | root: import.meta.dirname, 14 | }, 15 | 16 | images: { 17 | remotePatterns: [ 18 | { 19 | protocol: 'https', 20 | hostname: 'r2.nextjsbase.com', 21 | pathname: '/**', 22 | }, 23 | { 24 | protocol: 'https', 25 | hostname: 'lh3.googleusercontent.com', 26 | pathname: '/**', 27 | }, 28 | { 29 | protocol: 'https', 30 | hostname: 'avatars.githubusercontent.com', 31 | pathname: '/**', 32 | }, 33 | { 34 | protocol: 'https', 35 | hostname: '*.googleusercontent.com', 36 | pathname: '/**', 37 | }, 38 | ], 39 | dangerouslyAllowSVG: true, 40 | contentDispositionType: 'attachment', 41 | contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;", 42 | }, 43 | }; 44 | 45 | export default withNextIntl(nextConfig); 46 | -------------------------------------------------------------------------------- /lib/function/throttle.js: -------------------------------------------------------------------------------- 1 | let timeoutArr = []; 2 | let flagArr = []; 3 | /** 4 | * 节流函数 5 | * 节流原理:在一定时间内,只能触发一次 6 | * @param {Function} fn 要执行的回调函数 7 | * @param {Number} time 延时的时间 8 | * @param {Boolean} isImmediate 是否立即执行 9 | * @param {String} timeoutName 定时器ID 10 | * @return null 11 | nb.pubfn.throttle(function() { 12 | 13 | }, 1000); 14 | */ 15 | function throttle(fn, time = 500, isImmediate = true, timeoutName = "default") { 16 | if(!timeoutArr[timeoutName]) timeoutArr[timeoutName] = null; 17 | if (isImmediate) { 18 | if (!flagArr[timeoutName]) { 19 | flagArr[timeoutName] = true; 20 | // 如果是立即执行,则在time毫秒内开始时执行 21 | if(typeof fn === 'function') fn(); 22 | timeoutArr[timeoutName] = setTimeout(() => { 23 | flagArr[timeoutName] = false; 24 | }, time); 25 | } 26 | } else { 27 | if (!flagArr[timeoutName]) { 28 | flagArr[timeoutName] = true; 29 | // 如果是非立即执行,则在time毫秒内的结束处执行 30 | timeoutArr[timeoutName] = setTimeout(() => { 31 | flagArr[timeoutName] = false; 32 | if(typeof fn === 'function') fn(); 33 | }, time); 34 | } 35 | } 36 | }; 37 | export default throttle -------------------------------------------------------------------------------- /components/admin/smart-form/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SmartForm 组件导出入口 3 | * 4 | * 万能表单组件集合,通过配置驱动自动生成表单 5 | * 6 | * @example 7 | * ```jsx 8 | * import { SmartForm, SmartModalForm, SmartDrawerForm } from '@/components/admin/smart-form'; 9 | * 10 | * // 基础表单 11 | * 15 | * 16 | * // 模态框表单 17 | * 24 | * 25 | * // 抽屉表单 26 | * 33 | * ``` 34 | */ 35 | 36 | // 基础表单组件 37 | export { default as SmartForm } from './smart-form'; 38 | 39 | // 模态框表单组件 40 | export { default as SmartModalForm } from './smart-modal-form'; 41 | 42 | // 抽屉表单组件 43 | export { default as SmartDrawerForm } from './smart-drawer-form'; 44 | 45 | // 默认导出 SmartModalForm(最常用) 46 | export { default } from './smart-modal-form'; 47 | 48 | -------------------------------------------------------------------------------- /app/(admin)/admin/system/settings/page.js: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { ProCard } from '@ant-design/pro-components'; 4 | import { Button, Space, Tabs } from 'antd'; 5 | import { SaveOutlined } from '@ant-design/icons'; 6 | 7 | export default function SettingsPage() { 8 | const items = [ 9 | { 10 | key: 'general', 11 | label: 'General', 12 | children:
General settings coming soon...
, 13 | }, 14 | { 15 | key: 'api', 16 | label: 'API Keys', 17 | children:
API key management coming soon...
, 18 | }, 19 | { 20 | key: 'notifications', 21 | label: 'Notifications', 22 | children:
Notification settings coming soon...
, 23 | }, 24 | ]; 25 | 26 | return ( 27 |
28 | 32 | 33 | 36 | 37 | } 38 | headerBordered 39 | > 40 | 41 | 42 |
43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /components/common/LanguageSwitcherSimple.jsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useLocale } from 'next-intl'; 4 | import Link from 'next/link'; 5 | import { usePathname } from 'next/navigation'; 6 | import { locales, localeNames } from '@/i18n/config'; 7 | 8 | /** 9 | * 简单的语言切换器 - 使用链接而非下拉菜单 10 | */ 11 | export default function LanguageSwitcherSimple() { 12 | const locale = useLocale(); 13 | const pathname = usePathname(); 14 | 15 | // 获取当前路径(移除语言前缀) 16 | const getLocalizedPath = (newLocale) => { 17 | const pathWithoutLocale = pathname.replace(`/${locale}`, ''); 18 | return `/${newLocale}${pathWithoutLocale}`; 19 | }; 20 | 21 | return ( 22 |
23 | {locales.map((loc) => ( 24 | 33 | {localeNames[loc]} 34 | 35 | ))} 36 |
37 | ); 38 | } 39 | 40 | -------------------------------------------------------------------------------- /app/(admin)/actions/system/admin-login-logs.js: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | /** 4 | * 登录日志 Server Actions(基于 Session 表) 5 | * 使用 createReadOnlyActions 只读查询 6 | */ 7 | 8 | import { createReadOnlyActions } from '@/lib/core/crud-helper'; 9 | 10 | const loginLogsCrudConfig = { 11 | modelName: 'session', 12 | tableName: 'sessions', 13 | primaryKey: 'id', 14 | fields: { 15 | readable: ['userId', 'token', 'expiresAt', 'ipAddress', 'userAgent', 'createdAt', 'updatedAt'], 16 | creatable: [], 17 | updatable: [], 18 | searchable: ['userId', 'ipAddress'], 19 | }, 20 | query: { 21 | defaultSort: { createdAt: 'desc' }, 22 | defaultPageSize: 20, 23 | baseFilter: {}, 24 | foreignDB: [ 25 | { 26 | dbName: 'users', 27 | localKey: 'userId', 28 | foreignKey: 'id', 29 | as: 'userInfo', 30 | type: 'one', 31 | fieldJson: { id: true, name: true, email: true, role: true, lastLoginAt: true }, 32 | }, 33 | ], 34 | }, 35 | softDelete: false, 36 | }; 37 | 38 | const crudActions = createReadOnlyActions(loginLogsCrudConfig); 39 | 40 | export const getLoginLogListAction = crudActions.getList; 41 | export const getLoginLogDetailAction = crudActions.getDetail; 42 | -------------------------------------------------------------------------------- /app/(admin)/actions/system/upload-actions.js: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { prisma } from '@/lib/database/prisma'; 4 | import { wrapAction } from '@/lib/core/action-wrapper'; 5 | 6 | /** 7 | * 获取上传文件列表 8 | */ 9 | export const getUploadList = wrapAction('sysQueryUploadList', async ({ 10 | pageIndex = 1, 11 | pageSize = 20, 12 | search = '', 13 | type = 'all', 14 | } = {}, ctx) => { 15 | const where = {}; 16 | 17 | // 搜索文件名 18 | if (search) { 19 | where.originalName = { contains: search, mode: 'insensitive' }; 20 | } 21 | 22 | // 类型筛选 23 | if (type && type !== 'all') { 24 | if (type === 'image') { 25 | where.type = { in: ['image', 'images'] }; 26 | } else { 27 | where.type = type; 28 | } 29 | } 30 | 31 | const skip = (pageIndex - 1) * pageSize; 32 | 33 | const [rows, total] = await Promise.all([ 34 | prisma.asset.findMany({ 35 | where, 36 | orderBy: { createdAt: 'desc' }, 37 | skip, 38 | take: pageSize, 39 | }), 40 | prisma.asset.count({ where }), 41 | ]); 42 | 43 | return { 44 | success: true, 45 | data: rows, 46 | total, 47 | pageIndex, 48 | pageSize, 49 | totalPages: Math.ceil(total / pageSize) || 1, 50 | hasMore: pageIndex < Math.ceil(total / pageSize), 51 | }; 52 | }, { skipLog: true }); 53 | -------------------------------------------------------------------------------- /app/(admin)/admin/system/usage/page.js: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { ProCard, StatisticCard } from '@ant-design/pro-components'; 4 | import { Button } from 'antd'; 5 | import { ReloadOutlined } from '@ant-design/icons'; 6 | 7 | export default function UsagePage() { 8 | return ( 9 |
10 | }>Refresh} 13 | headerBordered 14 | > 15 | 16 | 23 | 30 | 37 | 38 |
39 |

Detailed usage charts and analytics will be displayed here

40 |
41 |
42 |
43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /lib/api/api-context.js: -------------------------------------------------------------------------------- 1 | /** 2 | * API Context - 从 Middleware 注入的 headers 中获取用户信息 3 | * 4 | * ## 使用方式 5 | * 6 | * ```javascript 7 | * import { getApiContext } from '@/lib/api/api-context'; 8 | * 9 | * export async function GET(request) { 10 | * const ctx = getApiContext(request); 11 | * const { userId, isAdmin } = ctx; 12 | * 13 | * // 业务逻辑... 14 | * return NextResponse.json({ success: true, data: ... }); 15 | * } 16 | * ``` 17 | */ 18 | 19 | /** 20 | * 从 request headers 获取用户上下文 21 | * 22 | * @param {Request} request - Next.js Request 对象 23 | * @returns {Object} 用户上下文 24 | */ 25 | export function getApiContext(request) { 26 | const userId = request.headers.get('x-user-id'); 27 | const userRole = request.headers.get('x-user-role'); 28 | const isAdmin = request.headers.get('x-is-admin') === 'true'; 29 | 30 | return { 31 | userId, 32 | userRole, 33 | isAdmin, 34 | // 是否已登录 35 | isAuthenticated: !!userId, 36 | }; 37 | } 38 | 39 | /** 40 | * 从 request headers 获取用户 ID 41 | */ 42 | export function getUserId(request) { 43 | return request.headers.get('x-user-id'); 44 | } 45 | 46 | /** 47 | * 检查是否是管理员 48 | */ 49 | export function isAdminRequest(request) { 50 | return request.headers.get('x-is-admin') === 'true'; 51 | } 52 | 53 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/crud/search-transformer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 搜索条件转换器 3 | * 4 | * 将 ProTable 排序参数转换为 Prisma orderBy 格式 5 | */ 6 | 7 | import nb from '@/lib/function'; 8 | 9 | /** 10 | * 构建排序条件 11 | * 12 | * 将 ProTable 的排序格式转换为 Prisma 的 orderBy 格式 13 | * ProTable: { fieldName: 'ascend' | 'descend' } 14 | * Prisma: { fieldName: 'asc' | 'desc' } 15 | * 16 | * @param {Object} sortParams - ProTable 的排序参数 17 | * @param {Array} fieldsConfig - 字段配置 18 | * @returns {Object} Prisma orderBy 条件 19 | */ 20 | export function buildSortCondition(sortParams, fieldsConfig) { 21 | // 如果有排序参数,转换格式 22 | if (sortParams && Object.keys(sortParams).length > 0) { 23 | const orderBy = {}; 24 | Object.keys(sortParams).forEach(key => { 25 | const value = sortParams[key]; 26 | if (value === 'ascend') { 27 | orderBy[key] = 'asc'; 28 | } else if (value === 'descend') { 29 | orderBy[key] = 'desc'; 30 | } 31 | }); 32 | return orderBy; 33 | } 34 | 35 | // 没有排序参数,使用字段配置中的默认排序 36 | if (fieldsConfig && nb.pubfn.isArray(fieldsConfig)) { 37 | const defaultSort = {}; 38 | fieldsConfig.forEach(field => { 39 | if (field.table?.defaultSort) { 40 | defaultSort[field.key] = field.table.defaultSort === 'desc' ? 'desc' : 'asc'; 41 | } 42 | }); 43 | 44 | if (Object.keys(defaultSort).length > 0) { 45 | return defaultSort; 46 | } 47 | } 48 | 49 | // 兜底:按创建时间降序 50 | return { createdAt: 'desc' }; 51 | } 52 | -------------------------------------------------------------------------------- /lib/function/category-utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 分类相关工具函数 3 | */ 4 | 5 | import nb from '@/lib/function'; 6 | 7 | /** 8 | * 获取分类的 weight 属性值 9 | * @param {Object} category - 分类对象 10 | * @returns {number} weight 值,默认为 0 11 | */ 12 | export function getCategoryWeight(category) { 13 | const weightProp = category.properties?.find((prop) => prop.name === 'weight'); 14 | return weightProp ? parseFloat(weightProp.value) || 0 : 0; 15 | } 16 | 17 | /** 18 | * 递归排序分类树(按 weight 属性) 19 | * @param {Array} categories - 分类数组 20 | * @returns {Array} 排序后的分类数组 21 | * 22 | * 注意:这里使用 mapTree 来处理排序,保持原有行为 23 | */ 24 | export function sortCategoriesByWeight(categories) { 25 | if (!Array.isArray(categories)) return categories; 26 | 27 | // 先排序当前层级 28 | const sorted = [...categories].sort((a, b) => getCategoryWeight(a) - getCategoryWeight(b)); 29 | 30 | // 使用 mapTree 递归处理子节点排序 31 | return nb.pubfn.tree.mapTree(sorted, (node) => ({ ...node }), { children: 'children' }) 32 | .map((node) => { 33 | if (node.children && node.children.length > 0) { 34 | node.children = sortCategoriesByWeight(node.children); 35 | } 36 | return node; 37 | }); 38 | } 39 | 40 | /** 41 | * 扁平化分类树 42 | * @param {Array} categories - 分类树数组 43 | * @returns {Array} 扁平化后的分类数组 44 | */ 45 | export function flattenCategories(categories) { 46 | // 使用 treeToArray 工具函数 47 | return nb.pubfn.tree.treeToArray(categories, { deleteChildren: false }); 48 | } 49 | 50 | -------------------------------------------------------------------------------- /lib/crud/README.md: -------------------------------------------------------------------------------- 1 | # CRUD 工具库 2 | 3 | SmartCrudPage 的核心工具集合,用于自动生成表格、表单和搜索功能。 4 | 5 | ## 📁 文件列表 6 | 7 | | 文件 | 说明 | 8 | |------|------| 9 | | `field-generator.js` | 字段生成器 - 根据配置生成表格列、表单字段、搜索条件 | 10 | | `field-types.js` | 字段类型注册表 - 定义所有可用的字段类型组件 | 11 | | `rule-evaluator.js` | 规则评估器 - 处理 showRule、disabled 等条件渲染逻辑 | 12 | | `search-transformer.js` | 搜索转换器 - 将前端搜索参数转换为数据库查询条件 | 13 | 14 | ## 🎯 使用方式 15 | 16 | 在 Smart CRUD Page 中自动导入使用: 17 | 18 | ```javascript 19 | import { 20 | generateTableColumns, 21 | generateDetailColumns, 22 | generateSearchConfig, 23 | generateSearchTransform, 24 | validateFieldsConfig, 25 | } from '@/lib/crud/field-generator'; 26 | 27 | import { buildSortCondition } from '@/lib/crud/search-transformer'; 28 | ``` 29 | 30 | 在动态表单组件中使用: 31 | 32 | ```javascript 33 | import { evaluateRule } from '@/lib/crud/rule-evaluator'; 34 | import { FIELD_TYPE_REGISTRY } from '@/lib/crud/field-types'; 35 | ``` 36 | 37 | ## 📖 相关文档 38 | 39 | - [Smart CRUD 使用指南](https://nextjsbase.com/zh/docs/admin/guides/SMART_CRUD) 40 | - [字段配置规范](https://nextjsbase.com/zh/docs/api/FIELDS_CONFIG) 41 | 42 | ## 🔗 依赖关系 43 | 44 | 这些工具依赖: 45 | - Ant Design Pro Components 46 | - React 47 | - `nb.pubfn` 工具函数(类型判断等) 48 | 49 | ```javascript 50 | import nb from '@/lib/function'; 51 | 52 | // 内部使用示例 53 | if (nb.pubfn.isFunction(options)) { ... } 54 | if (nb.pubfn.isArray(value)) { ... } 55 | ``` 56 | 57 | 详见 [lib/function/README.md](../function/README.md) 58 | -------------------------------------------------------------------------------- /app/(client)/actions/test-actions.js: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | /** 4 | * 测试用 Actions - 演示权限命名约定 5 | * 6 | * wrapAction 会在最后一个参数添加 context 7 | * handler 签名:(params, context) => result 8 | * 9 | * 注意:如果没有 params,也要写成 (_, ctx) 或 (params, ctx) 10 | */ 11 | 12 | import { wrapAction } from '@/lib/core/action-wrapper'; 13 | 14 | /** 15 | * 公开 Action - 无需登录 16 | */ 17 | export const pubGetServerTime = wrapAction('pubGetServerTime', async (_, ctx) => { 18 | return { 19 | success: true, 20 | data: { 21 | time: new Date().toISOString(), 22 | message: '公开 Action 访问成功(无需登录)', 23 | userId: ctx?.userId || null, 24 | }, 25 | }; 26 | }); 27 | 28 | /** 29 | * 需要登录的 Action 30 | */ 31 | export const authGetUserInfo = wrapAction('authGetUserInfo', async (_, ctx) => { 32 | const { userId, user, isAdmin } = ctx; 33 | 34 | return { 35 | success: true, 36 | data: { 37 | userId, 38 | email: user?.email, 39 | name: user?.name, 40 | role: user?.role, 41 | isAdmin, 42 | message: '需要登录的 Action 访问成功', 43 | }, 44 | }; 45 | }); 46 | 47 | /** 48 | * 后台 Action - 需要后台权限 49 | */ 50 | export const sysGetSystemInfo = wrapAction('sysGetSystemInfo', async (_, ctx) => { 51 | const { userId, isAdmin } = ctx; 52 | 53 | return { 54 | success: true, 55 | data: { 56 | userId, 57 | isAdmin, 58 | nodeVersion: process.version, 59 | platform: process.platform, 60 | uptime: process.uptime(), 61 | message: '后台 Action 访问成功(需要后台权限)', 62 | }, 63 | }; 64 | }); 65 | -------------------------------------------------------------------------------- /components/admin/markdown-editor.jsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { useState, useEffect } from 'react'; 4 | import dynamic from 'next/dynamic'; 5 | import '@uiw/react-md-editor/markdown-editor.css'; 6 | import '@uiw/react-markdown-preview/markdown.css'; 7 | 8 | // 动态导入 MDEditor,禁用 SSR 9 | const MDEditor = dynamic( 10 | () => import('@uiw/react-md-editor'), 11 | { ssr: false } 12 | ); 13 | 14 | /** 15 | * Markdown 编辑器组件 16 | * 17 | * 特性: 18 | * - 支持实时预览 19 | * - 支持工具栏 20 | * - 支持暗色模式 21 | * - 支持全屏编辑 22 | */ 23 | export default function MarkdownEditor({ 24 | value, 25 | onChange, 26 | placeholder = 'Enter markdown content...', 27 | height = 400, 28 | preview = 'live', // 'live' | 'edit' | 'preview' 29 | // 过滤掉 ProForm 可能传递的额外属性 30 | fieldProps: _fieldProps, 31 | formItemProps: _formItemProps, 32 | proFieldProps: _proFieldProps, 33 | // 保留其他属性传递给 MDEditor 34 | ...props 35 | }) { 36 | const [localValue, setLocalValue] = useState(value || ''); 37 | 38 | useEffect(() => { 39 | setLocalValue(value || ''); 40 | }, [value]); 41 | 42 | const handleChange = (val) => { 43 | setLocalValue(val); 44 | if (onChange) { 45 | onChange(val); 46 | } 47 | }; 48 | 49 | return ( 50 |
51 | 61 |
62 | ); 63 | } 64 | 65 | -------------------------------------------------------------------------------- /app/(admin)/actions/system/admin-action-logs.js: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | /** 4 | * 操作日志管理 Server Actions 5 | * 使用核心库(BaseDAO + action-wrapper)自动处理权限验证和日志记录 6 | */ 7 | 8 | import { createReadOnlyActions } from '@/lib/core/crud-helper'; 9 | 10 | /** 11 | * Action Logs CRUD 配置 12 | */ 13 | const actionLogsCrudConfig = { 14 | modelName: 'actionLog', 15 | tableName: 'action_logs', // 数据库表名,selects 连表查询需要 16 | primaryKey: 'id', 17 | 18 | fields: { 19 | readable: ['user_id', 'action', 'resource_type', 'resource_id', 'params', 'result', 'success', 'duration', 'createdAt', 'ip', 'user_agent'], 20 | creatable: [], 21 | updatable: [], 22 | searchable: ['user_id', 'action', 'resource_type', 'resource_id'], 23 | }, 24 | 25 | query: { 26 | defaultSort: { createdAt: 'desc' }, 27 | defaultPageSize: 20, 28 | baseFilter: {}, 29 | // 默认连表配置:查询用户信息 30 | // 注意:使用数据库的实际表名和列名! 31 | foreignDB: [ 32 | { 33 | dbName: 'users', // 数据库表名 34 | localKey: 'user_id', // action_logs 表的列名 35 | foreignKey: 'id', // users 表的列名 36 | as: 'userInfo', 37 | type: 'one', 38 | fieldJson: { id: true, name: true, email: true }, 39 | }, 40 | ], 41 | }, 42 | 43 | softDelete: false, 44 | }; 45 | 46 | /** 47 | * 创建只读 CRUD Actions(操作日志只能查看) 48 | * BaseDAO 已支持 SmartCrudPage 的 whereJson 参数 49 | */ 50 | const crudActions = createReadOnlyActions(actionLogsCrudConfig); 51 | 52 | /** 53 | * 导出标准查询 Actions 54 | * 注意:不能调用 getList(),只能导出函数本身 55 | */ 56 | export const getActionLogListAction = crudActions.getList; 57 | export const getActionLogDetailAction = crudActions.getDetail; 58 | -------------------------------------------------------------------------------- /config/admin-pages.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 后台管理系统已知页面路径配置 3 | * 用于 PageAccessGuard 判断页面是否存在 4 | * 5 | * 注意: 6 | * 1. 这个列表只包含静态路由 7 | * 2. 动态路由需要在 isKnownPage 函数中单独处理 8 | * 3. 添加新页面后需要更新此列表 9 | */ 10 | 11 | export const ADMIN_PAGES = [ 12 | // Dashboard 13 | '/admin', 14 | 15 | // RBAC 管理 16 | '/admin/rbac/users', 17 | '/admin/rbac/roles', 18 | '/admin/rbac/menus', 19 | '/admin/rbac/permissions', 20 | 21 | // 财务管理 22 | '/admin/finance/credits', 23 | '/admin/finance/packages', 24 | 25 | // 系统管理 26 | '/admin/system/settings', 27 | '/admin/system/usage', 28 | '/admin/system/action_logs', 29 | '/admin/system/login_logs', 30 | '/admin/system/assets', 31 | 32 | // CMS 33 | '/admin/cms/post', 34 | 35 | // 示例页面 36 | '/admin/example', 37 | '/admin/example/data-table/data-table-basic', 38 | '/admin/example/data-table/data-table-permission', 39 | '/admin/examples/protected-page-example', 40 | ]; 41 | 42 | /** 43 | * 动态路由模式 44 | * 用于匹配带参数的路由,如 /admin/users/[id] 45 | */ 46 | export const DYNAMIC_PAGE_PATTERNS = [ 47 | // 示例:/admin/rbac/users/edit/xxx 48 | // /^\/admin\/rbac\/users\/edit\/[a-zA-Z0-9_-]+$/, 49 | 50 | // 可以根据需要添加更多动态路由模式 51 | ]; 52 | 53 | /** 54 | * 检查路径是否是已知存在的页面 55 | * @param {string} pathname - 当前路径 56 | * @returns {boolean} 是否是已知页面 57 | */ 58 | export function isKnownPage(pathname) { 59 | // 1. 精确匹配静态路由 60 | if (ADMIN_PAGES.includes(pathname)) { 61 | return true; 62 | } 63 | 64 | // 2. 匹配动态路由模式 65 | for (const pattern of DYNAMIC_PAGE_PATTERNS) { 66 | if (pattern.test(pathname)) { 67 | return true; 68 | } 69 | } 70 | 71 | // 3. 未匹配到任何已知页面 72 | return false; 73 | } 74 | -------------------------------------------------------------------------------- /components/admin/antd-config-provider.jsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | // React 19 兼容补丁 - 必须在最顶部导入 4 | import '@ant-design/v5-patch-for-react-19'; 5 | 6 | import { useMemo } from 'react'; 7 | import { ConfigProvider, App, theme as antdTheme } from 'antd'; 8 | import { useTheme } from 'next-themes'; 9 | 10 | // Ant Design 英文语言包 11 | import enUS from 'antd/locale/en_US'; 12 | 13 | /** 14 | * Ant Design 配置提供者 15 | * 用于配置全局设置,包括 React 19 兼容性 16 | * 注意:不需要 StyleProvider,因为 @ant-design/nextjs-registry 已经处理了 SSR 样式注入 17 | */ 18 | export default function AntdConfigProvider({ children }) { 19 | const { resolvedTheme, theme, systemTheme } = useTheme(); 20 | const effectiveTheme = 21 | resolvedTheme || 22 | (theme === 'system' ? systemTheme : theme) || 23 | 'light'; 24 | const isDarkMode = effectiveTheme === 'dark'; 25 | 26 | const themeConfig = useMemo(() => { 27 | const algorithm = isDarkMode 28 | ? [antdTheme.darkAlgorithm] 29 | : [antdTheme.defaultAlgorithm]; 30 | 31 | return { 32 | algorithm, 33 | token: { 34 | colorPrimary: '#187ddc', 35 | colorBgLayout: isDarkMode ? '#0d0d0d' : '#f6f6f6', 36 | colorTextBase: isDarkMode ? '#f6f6f6' : '#0d0d0d', 37 | boxShadow: isDarkMode ? '0 8px 24px rgba(0,0,0,0.45)' : '0 8px 24px rgba(0,0,0,0.12)', 38 | boxShadowSecondary: isDarkMode ? '0 4px 14px rgba(0,0,0,0.35)' : '0 4px 14px rgba(0,0,0,0.1)', 39 | fontSize: 14, 40 | lineHeight: 1.5715, 41 | borderRadius: 6, 42 | }, 43 | }; 44 | }, [isDarkMode]); 45 | 46 | return ( 47 | 51 | {children} 52 | 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /app/(admin)/layout.js: -------------------------------------------------------------------------------- 1 | import { Poppins } from 'next/font/google'; 2 | import { checkBackendAccess } from '@/lib/auth/admin-auth'; 3 | import { Toaster } from '@/components/ui/sonner'; 4 | import { AntdRegistry } from '@ant-design/nextjs-registry'; 5 | import AdminLayout from '@/components/admin/admin-layout'; 6 | import AntdConfigProvider from '@/components/admin/antd-config-provider'; 7 | import { ThemeProvider } from '@/components/common/theme-provider'; 8 | import '../globals.css'; 9 | import './admin-styles.css'; 10 | 11 | // 使用 Google Fonts Poppins 字体 12 | const poppins = Poppins({ 13 | subsets: ['latin'], 14 | weight: ['400', '500', '600', '700', '800', '900'], 15 | variable: '--font-poppins', 16 | display: 'swap', 17 | }); 18 | 19 | export const metadata = { 20 | title: 'Admin Panel - NextJS Base', 21 | description: 'Administration panel for NextJS Base', 22 | }; 23 | 24 | /** 25 | * Admin Layout Root - 不使用多语言 26 | * 管理后台始终使用英文界面 27 | * 需要后台访问权限:admin 角色 或 user + isBackendAllowed 28 | */ 29 | export default async function AdminLayoutRoot({ children }) { 30 | // 验证后台访问权限 31 | const session = await checkBackendAccess(); 32 | 33 | return ( 34 | 35 | 36 | 42 | 43 | 44 | 45 | {children} 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /components/ui/spotlight-card.jsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { useRef, useState, useEffect } from 'react'; 3 | 4 | const SpotlightCard = ({ children, className = '', spotlightColor = 'rgba(255, 255, 255, 0.15)' }) => { 5 | const divRef = useRef(null); 6 | const [position, setPosition] = useState({ x: 0, y: 0 }); 7 | const [opacity, setOpacity] = useState(0); 8 | 9 | const handleMouseMove = (e) => { 10 | if (!divRef.current) return; 11 | const div = divRef.current; 12 | const rect = div.getBoundingClientRect(); 13 | setPosition({ x: e.clientX - rect.left, y: e.clientY - rect.top }); 14 | }; 15 | 16 | const handleFocus = () => { 17 | setOpacity(1); 18 | }; 19 | 20 | const handleBlur = () => { 21 | setOpacity(0); 22 | }; 23 | 24 | const handleMouseEnter = () => { 25 | setOpacity(1); 26 | }; 27 | 28 | const handleMouseLeave = () => { 29 | setOpacity(0); 30 | }; 31 | 32 | return ( 33 |
42 |
49 |
{children}
50 |
51 | ); 52 | }; 53 | 54 | export default SpotlightCard; 55 | 56 | -------------------------------------------------------------------------------- /lib/function/nb.filters.js: -------------------------------------------------------------------------------- 1 | /** 2 | * nb.filters 3 | * 全局过滤器 4 | */ 5 | import pubfn from './index.js'; 6 | var util = {}; 7 | // 将时间显示成1秒前、1天前 8 | util.dateDiff = function (starttime, showType) { 9 | return pubfn.dateDiff(starttime, showType); 10 | }; 11 | util.dateDiff2 = function (starttime, showType) { 12 | return pubfn.dateDiff2(starttime, showType); 13 | }; 14 | // 计数器1 15 | util.numStr = function (n) { 16 | return pubfn.numStr(n); 17 | }; 18 | 19 | util.timeStr = function (date) { 20 | return pubfn.timeFormat(date); 21 | }; 22 | /** 23 | * 日期对象转字符串 24 | * @description 最终转成 2020-08-01 12:12:12 25 | * @param {Date || Number} date 需要转换的时间 26 | * date参数支持 27 | * 支持:时间戳 28 | * 支持:2020-08 29 | * 支持:2020-08-24 30 | * 支持:2020-08-24 12 31 | * 支持:2020-08-24 12:12 32 | * 支持:2020-08-24 12:12:12 33 | */ 34 | util.timeFilter = function (date, fmt) { 35 | return pubfn.timeFormat(date, fmt); 36 | }; 37 | // 金额过滤器 38 | util.priceFilter = function (n, defaultValue = ' - ') { 39 | return pubfn.priceFilter(n, defaultValue); 40 | }; 41 | // 金额过滤器 - 只显示小数点左边 42 | util.priceLeftFilter = function (n) { 43 | return pubfn.priceLeftFilter(n); 44 | }; 45 | // 金额过滤器 - 只显示小数点右边 46 | util.priceRightFilter = function (n) { 47 | return pubfn.priceRightFilter(n); 48 | }; 49 | // 百分比过滤器 50 | util.percentageFilter = function (n, needShowSymbol, defaultValue = ' - ') { 51 | return pubfn.percentageFilter(n, needShowSymbol, defaultValue); 52 | }; 53 | // 折扣过滤器 54 | util.discountFilter = function (n, needShowSymbol, defaultValue = ' - ') { 55 | return pubfn.discountFilter(n, needShowSymbol, defaultValue); 56 | }; 57 | // 大小过滤器 sizeFilter(1024,3,["B","KB","MB","GB"]) 58 | util.sizeFilter = function (...obj) { 59 | let res = pubfn.calcSize(...obj); 60 | return res.title; 61 | }; 62 | 63 | export default util; 64 | -------------------------------------------------------------------------------- /lib/function/queryParams.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 对象转url参数 3 | * @param {*} data,对象 4 | * @param {*} isPrefix,是否自动加上"?" 5 | * 此函数参考uView 6 | */ 7 | function queryParams(data = {}, isPrefix = true, arrayFormat = 'brackets') { 8 | let newData = JSON.parse(JSON.stringify(data)); 9 | let prefix = isPrefix ? '?' : '' 10 | let _result = [] 11 | if (['indices', 'brackets', 'repeat', 'comma'].indexOf(arrayFormat) == -1) arrayFormat = 'brackets'; 12 | for (let key in newData) { 13 | let value = newData[key] 14 | // 去掉为空的参数 15 | if (['', undefined, null].indexOf(value) >= 0) { 16 | continue; 17 | } 18 | // 如果值为数组,另行处理 19 | if (value.constructor === Array) { 20 | // e.g. {ids: [1, 2, 3]} 21 | switch (arrayFormat) { 22 | case 'indices': 23 | // 结果: ids[0]=1&ids[1]=2&ids[2]=3 24 | for (let i = 0; i < value.length; i++) { 25 | _result.push(key + '[' + i + ']=' + value[i]) 26 | } 27 | break; 28 | case 'brackets': 29 | // 结果: ids[]=1&ids[]=2&ids[]=3 30 | value.forEach(_value => { 31 | _result.push(key + '[]=' + _value) 32 | }) 33 | break; 34 | case 'repeat': 35 | // 结果: ids=1&ids=2&ids=3 36 | value.forEach(_value => { 37 | _result.push(key + '=' + _value) 38 | }) 39 | break; 40 | case 'comma': 41 | // 结果: ids=1,2,3 42 | let commaStr = ""; 43 | value.forEach(_value => { 44 | commaStr += (commaStr ? "," : "") + _value; 45 | }) 46 | _result.push(key + '=' + commaStr) 47 | break; 48 | default: 49 | value.forEach(_value => { 50 | _result.push(key + '[]=' + _value) 51 | }) 52 | } 53 | } else { 54 | _result.push(key + '=' + value) 55 | } 56 | } 57 | return _result.length ? prefix + _result.join('&') : '' 58 | } 59 | 60 | export default queryParams; 61 | -------------------------------------------------------------------------------- /components/ui/tooltip.jsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | function TooltipProvider({ 9 | delayDuration = 0, 10 | ...props 11 | }) { 12 | return (); 13 | } 14 | 15 | function Tooltip({ 16 | ...props 17 | }) { 18 | return ( 19 | 20 | 21 | 22 | ); 23 | } 24 | 25 | function TooltipTrigger({ 26 | ...props 27 | }) { 28 | return ; 29 | } 30 | 31 | function TooltipContent({ 32 | className, 33 | sideOffset = 0, 34 | children, 35 | ...props 36 | }) { 37 | return ( 38 | 39 | 47 | {children} 48 | 50 | 51 | 52 | ); 53 | } 54 | 55 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } 56 | -------------------------------------------------------------------------------- /lib/auth/README.md: -------------------------------------------------------------------------------- 1 | # 认证与权限库 2 | 3 | 基于 Better Auth 的认证系统和 RBAC 权限管理工具集。 4 | 5 | ## 📁 文件列表 6 | 7 | | 文件 | 说明 | 8 | |------|------| 9 | | `auth.js` | Better Auth 核心配置 - 数据库连接、Session 管理 | 10 | | `auth-client.js` | Better Auth 客户端 - 前端认证 API | 11 | | `admin-auth.js` | 管理员认证 - 检查管理员权限(页面/Action) | 12 | | `page-auth.js` | 页面权限 - RBAC 页面访问控制 | 13 | | `permission-auth.js` | 权限验证 - RBAC 操作权限检查 | 14 | 15 | ## 🎯 使用方式 16 | 17 | ### 服务端认证 18 | 19 | ```javascript 20 | // 获取 Session 21 | import { auth } from '@/lib/auth/auth'; 22 | const session = await auth.api.getSession({ headers: await headers() }); 23 | 24 | // 管理员验证(页面) 25 | import { checkAdmin } from '@/lib/auth/admin-auth'; 26 | await checkAdmin(); // 不是管理员则重定向 27 | 28 | // 管理员验证(Action) 29 | import { checkAdminAction } from '@/lib/auth/admin-auth'; 30 | const { isAdmin, userId } = await checkAdminAction(); 31 | if (!isAdmin) return { success: false, error: 'Unauthorized' }; 32 | ``` 33 | 34 | ### RBAC 权限验证 35 | 36 | ```javascript 37 | // 页面权限检查 38 | import { checkPageAccess } from '@/lib/auth/page-auth'; 39 | await checkPageAccess('/admin/users'); // 无权限则重定向 403 40 | 41 | // Action 权限检查 42 | import { checkPermission } from '@/lib/auth/permission-auth'; 43 | const hasPermission = await checkPermission('user:create'); 44 | ``` 45 | 46 | ### 客户端认证 47 | 48 | ```javascript 49 | 'use client'; 50 | import { authClient } from '@/lib/auth/auth-client'; 51 | 52 | // 登录 53 | await authClient.signIn.email({ email, password }); 54 | 55 | // 注册 56 | await authClient.signUp.email({ email, password, name }); 57 | 58 | // OAuth 登录 59 | await authClient.signIn.social({ provider: 'google' }); 60 | ``` 61 | 62 | ## 📖 相关文档 63 | 64 | - [管理后台认证文档](https://nextjsbase.com/zh/docs/admin/client/AUTH) 65 | - [RBAC 系统文档](https://nextjsbase.com/zh/docs/admin/rbac/CONFIGURATION) 66 | - [前端认证文档](https://nextjsbase.com/zh/docs/admin/client/AUTH) 67 | - Better Auth UUID 集成(文档待补充) 68 | 69 | ## 🔗 依赖关系 70 | 71 | - Better Auth 72 | - Prisma (通过 `lib/database/prisma`) 73 | - Next.js (headers, cookies) 74 | -------------------------------------------------------------------------------- /app/(client)/actions/user.js: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { auth } from '@/lib/auth/auth'; 4 | import { headers } from 'next/headers'; 5 | import { getUserProfile, updateUserProfile, getUserStatistics } from '@/lib/business/user-profile'; 6 | 7 | /** 8 | * 获取当前用户资料 9 | * @returns {Promise} 用户资料 10 | */ 11 | export async function getUserProfileAction() { 12 | const session = await auth.api.getSession({ headers: await headers() }); 13 | 14 | if (!session) { 15 | return { 16 | success: false, 17 | error: 'Unauthorized', 18 | }; 19 | } 20 | 21 | try { 22 | const profile = await getUserProfile(session.user.id); 23 | return { 24 | success: true, 25 | data: profile, 26 | }; 27 | } catch (error) { 28 | return { 29 | success: false, 30 | error: error.message, 31 | }; 32 | } 33 | } 34 | 35 | /** 36 | * 更新用户资料 37 | * @param {Object} updates - 更新数据 { name, image, username } 38 | * @returns {Promise} 更新结果 39 | */ 40 | export async function updateUserProfileAction(updates) { 41 | const session = await auth.api.getSession({ headers: await headers() }); 42 | 43 | if (!session) { 44 | return { 45 | success: false, 46 | error: 'Unauthorized', 47 | }; 48 | } 49 | 50 | try { 51 | const result = await updateUserProfile(session.user.id, updates); 52 | return { 53 | success: true, 54 | data: result, 55 | message: 'Profile updated successfully', 56 | }; 57 | } catch (error) { 58 | return { 59 | success: false, 60 | error: error.message, 61 | }; 62 | } 63 | } 64 | 65 | /** 66 | * 获取用户统计信息 67 | * @returns {Promise} 统计信息 68 | */ 69 | export async function getUserStatisticsAction() { 70 | const session = await auth.api.getSession({ headers: await headers() }); 71 | 72 | if (!session) { 73 | return { 74 | success: false, 75 | error: 'Unauthorized', 76 | }; 77 | } 78 | 79 | try { 80 | const stats = await getUserStatistics(session.user.id); 81 | return { 82 | success: true, 83 | data: stats, 84 | }; 85 | } catch (error) { 86 | return { 87 | success: false, 88 | error: error.message, 89 | }; 90 | } 91 | } 92 | 93 | -------------------------------------------------------------------------------- /app/(client)/[locale]/layout.js: -------------------------------------------------------------------------------- 1 | import { Poppins } from 'next/font/google'; 2 | import { NextIntlClientProvider } from 'next-intl'; 3 | import { getMessages } from 'next-intl/server'; 4 | import { notFound } from 'next/navigation'; 5 | import { locales } from '@/i18n/config'; 6 | import { ThemeProvider } from '@/components/common/theme-provider'; 7 | import { Toaster } from '@/components/ui/sonner'; 8 | import '@/app/globals.css'; 9 | 10 | // 使用 Google Fonts Poppins 字体 11 | const poppins = Poppins({ 12 | subsets: ['latin'], 13 | weight: ['400', '500', '600', '700', '800', '900'], 14 | variable: '--font-poppins', 15 | display: 'swap', 16 | }); 17 | 18 | export function generateStaticParams() { 19 | return locales.map((locale) => ({ locale })); 20 | } 21 | 22 | export async function generateMetadata({ params }) { 23 | const { locale } = await params; 24 | 25 | return { 26 | title: 'NextJS Base - Configuration Driven Framework', 27 | description: 'Build enterprise admin systems in minutes with Next.js 16 and Prisma.', 28 | alternates: { 29 | canonical: `/${locale}`, 30 | languages: { 31 | en: '/en', 32 | zh: '/zh', 33 | ja: '/ja', 34 | }, 35 | }, 36 | }; 37 | } 38 | 39 | export default async function LocaleLayout({ children, params }) { 40 | const { locale } = await params; 41 | // 验证语言是否支持 42 | if (!locales.includes(locale)) { 43 | notFound(); 44 | } 45 | 46 | // 获取翻译消息 47 | const messages = await getMessages(); 48 | 49 | return ( 50 | 51 | 52 |