diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..22ae157 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +node_modules +.next +.git +.env*.local +npm-debug.log* +/data +*.db diff --git a/.gitignore b/.gitignore index 5ef6a52..45254b6 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,5 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +/app/generated/prisma diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1b0a79c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,34 @@ +FROM node:20-alpine AS base +WORKDIR /app + +# 阶段1: 安装所有依赖(包括 devDependencies,因为构建需要) +FROM base AS deps +COPY package*.json ./ +RUN npm ci + +# 阶段2: 构建应用 +FROM base AS builder +COPY --from=deps /app/node_modules ./node_modules +COPY . . +RUN npx prisma generate +RUN npm run build + +# 阶段3: 生产运行环境 +FROM base AS runner +WORKDIR /app +ENV NODE_ENV=production + +RUN addgroup --system --gid 1001 nodejs && \ + adduser --system --uid 1001 nextjs + +# 复制 standalone 构建输出 +COPY --from=builder /app/.next/standalone ./ +# 复制静态文件 +COPY --from=builder /app/.next/static ./.next/static +# 复制 prisma schema (用于数据库迁移) +COPY --from=builder /app/prisma ./prisma + +RUN mkdir -p /app/data && chown nextjs:nodejs /app/data +USER nextjs +EXPOSE 3000 +CMD ["node", "server.js"] diff --git a/README.md b/README.md index e215bc4..59070c2 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,247 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). +# NextApp - 门户系统使用说明 -## Getting Started +基于 Next.js 15 + Prisma 6 + SQLite 构建的最小化门户网站。 -First, run the development server: +## 环境要求 + +- Node.js 18.x 或更高版本 +- npm 或 yarn + +## 快速开始 + +### 1. 安装依赖 + +```bash +npm install +``` + +### 2. 数据库初始化 + +项目使用 SQLite 数据库,数据存储在 `dev.db` 文件中。 + +```bash +# 生成 Prisma 客户端 +npx prisma generate + +# 应用数据库迁移(创建表结构) +npx prisma migrate dev + +# 种子数据(添加初始页面) +curl http://localhost:3000/api/seed +# 或在浏览器访问: http://localhost:3000/api/seed +``` + +### 3. 启动开发服务器 ```bash npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev ``` -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +服务器启动后,访问 **http://localhost:3000** -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +### 4. 停止服务器 -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. +在运行服务器的终端按 `Ctrl + C`,或: -## Learn More +```bash +# 查找进程 +ps aux | grep "next" -To learn more about Next.js, take a look at the following resources: +# 结束进程 +kill -9 +# 或 +pkill -f "next-server" +``` -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +## 常用命令 -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! +| 命令 | 说明 | +|------|------| +| `npm run dev` | 启动开发服务器(支持热重载)| +| `npm run build` | 构建生产版本 | +| `npm run start` | 启动生产服务器 | +| `npm run lint` | 代码检查 | -## Deploy on Vercel +## 数据库操作 -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. +### Prisma 命令 -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. +```bash +# 生成 Prisma 客户端 +npx prisma generate + +# 创建新迁移 +npx prisma migrate dev --name <迁移名称> + +# 重置数据库(清空所有数据) +npx prisma migrate reset + +# 打开 Prisma Studio(数据库可视化工具) +npx prisma studio +``` + +### 数据库文件 + +- **位置**: `prisma/dev.db` +- **类型**: SQLite +- **工具**: 可使用 `npx prisma studio` 或 SQLite 客户端(如 DB Browser for SQLite)查看 + +## 项目结构 + +``` +nextapp/ +├── app/ # Next.js App Router +│ ├── api/ # API 路由 +│ │ ├── pages/ # 页面相关 API +│ │ │ ├── route.ts # GET/POST 所有页面 +│ │ │ └── [slug]/ +│ │ │ └── route.ts # GET 单个页面 +│ │ └── seed/ +│ │ └── route.ts # 种子数据接口 +│ ├── components/ # React 组件 +│ │ └── TestButton.tsx # 测试按钮组件 +│ ├── lib/ +│ │ └── prisma.ts # Prisma 客户端实例 +│ ├── globals.css # 全局样式 +│ ├── layout.tsx # 根布局 +│ └── page.tsx # 首页 +├── prisma/ +│ ├── schema.prisma # 数据模型定义 +│ └── migrations/ # 数据库迁移文件 +├── dev.db # SQLite 数据库(运行时生成) +├── .env # 环境变量 +├── package.json +└── tsconfig.json +``` + +## API 端点 + +### 页面相关 + +| 方法 | 端点 | 说明 | +|------|------|------| +| GET | `/api/pages` | 获取所有页面 | +| POST | `/api/pages` | 创建新页面 | +| GET | `/api/pages/[slug]` | 获取指定页面 | +| GET | `/api/seed` | 初始化种子数据 | + +### 示例 + +```bash +# 获取所有页面 +curl http://localhost:3000/api/pages + +# 创建新页面 +curl -X POST http://localhost:3000/api/pages \ + -H "Content-Type: application/json" \ + -d '{"slug":"contact","title":"联系我们","content":"

联系我们

联系方式...

"}' + +# 获取指定页面 +curl http://localhost:3000/api/pages/home +``` + +## 数据模型 + +### Page(页面) + +```prisma +model Page { + id Int @id @default(autoincrement()) + slug String @unique # 页面标识(如:home, about) + title String # 页面标题 + content String # 页面内容(HTML) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} +``` + +### Setting(设置) + +```prisma +model Setting { + id Int @id @default(autoincrement()) + key String @unique # 设置键名 + value String # 设置值 + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} +``` + +## 功能说明 + +### 首页 + +- 访问 `/` 显示 slug 为 `home` 的页面内容 +- 导航栏包含 Logo 和"测试按钮" +- 点击"测试按钮"弹出 "hello" 提示 + +### 测试按钮 + +- 位置:导航栏右侧 +- 功能:点击弹出 alert 提示 "hello" +- 实现:`app/components/TestButton.tsx`(客户端组件) + +## 生产部署 + +### 构建 + +```bash +npm run build +``` + +### 启动生产服务器 + +```bash +npm run start +``` + +生产服务器默认运行在 **http://localhost:3000** + +## 注意事项 + +1. **数据库文件**:`dev.db` 包含数据库,不要手动编辑 +2. **环境变量**:数据库 URL 配置在 `.env` 文件中 +3. **Prisma 客户端**:修改 `schema.prisma` 后需运行 `npx prisma generate` +4. **迁移**:修改数据模型后需创建新的迁移:`npx prisma migrate dev --name <描述>` +5. **热重载**:开发模式下代码修改会自动刷新浏览器 + +## 技术栈 + +- **框架**: Next.js 15 (App Router) +- **ORM**: Prisma 6 +- **数据库**: SQLite +- **样式**: Tailwind CSS 4 +- **语言**: TypeScript + +## 故障排查 + +### 数据库不同步 + +```bash +npx prisma migrate reset +npx prisma migrate dev +``` + +### Prisma 客户端未生成 + +```bash +npx prisma generate +``` + +### 端口被占用 + +```bash +# 修改端口启动 +npm run dev -- -p 3001 +``` + +### 清除缓存重新构建 + +```bash +rm -rf .next node_modules/.cache +npm run dev +``` + +## 许可证 + +MIT diff --git a/app/admin/agents/AgentForm.tsx b/app/admin/agents/AgentForm.tsx new file mode 100644 index 0000000..d5cde01 --- /dev/null +++ b/app/admin/agents/AgentForm.tsx @@ -0,0 +1,196 @@ +'use client' + +import { useState } from "react" +import { useRouter } from "next/navigation" + +export default function AgentForm({ + categories, + agent, +}: { + categories: { id: number; name: string }[] + agent?: { + id?: number + name: string + slug: string + description: string + icon?: string + categoryId?: number + features?: string + status?: string + } +}) { + const router = useRouter() + const [formData, setFormData] = useState({ + name: agent?.name || "", + slug: agent?.slug || "", + description: agent?.description || "", + icon: agent?.icon || "", + categoryId: agent?.categoryId || "", + features: agent?.features || "", + status: agent?.status || "active", + }) + const [loading, setLoading] = useState(false) + const [error, setError] = useState("") + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setLoading(true) + setError("") + + try { + const url = agent?.id + ? `/api/admin/agents/${agent.id}` + : "/api/admin/agents" + + const method = agent?.id ? "PUT" : "POST" + + const res = await fetch(url, { + method, + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(formData), + }) + + if (res.ok) { + router.push("/admin/agents") + } else { + setError("保存失败") + } + } catch (err) { + setError("保存失败,请稍后重试") + } finally { + setLoading(false) + } + } + + return ( +
+ {error && ( +
+ {error} +
+ )} + +
+
+ + setFormData({ ...formData, name: e.target.value })} + className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500" + placeholder="智能客服助手" + /> +
+ +
+ + setFormData({ ...formData, slug: e.target.value })} + className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500" + placeholder="smart-customer-service" + /> +
+
+ +
+ +