diff --git a/.gitignore b/.gitignore index 87f9ddf..2114bd0 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ /data/ # 前端依赖与构建缓存 +/node_modules/ /frontend/node_modules/ /frontend/dist/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..db1aa52 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,46 @@ +# 参与贡献 + +感谢你对姜十三论坛的关注!这是一个开源项目,欢迎提交 Issue 和 Pull Request。 + +## 开发环境 + +**要求:** Go 1.26+、Node.js 18+ + +```powershell +# Windows:一键启动后端 + 前端热更新 +.\build.ps1 -Target dev +# 浏览器访问 http://localhost:5173 +``` + +```bash +# Linux / macOS +make dev +``` + +## 提交规范 + +- 一个 PR 只做一件事,保持 diff 小而清晰 +- 前端改动请确认浅色 / 暗色主题下都正常 +- 涉及 UI 变更时,建议在 PR 中附上截图 + +## 完整构建 + +发布单二进制前需先构建前端并 embed: + +```powershell +.\build.ps1 # Windows +make build # Linux / macOS +``` + +## 报告问题 + +在 [Issues](https://git.iioio.com/freefire/jiang13-forum/issues) 中描述: + +1. 复现步骤 +2. 期望行为 vs 实际行为 +3. 环境信息(系统、浏览器、Go/Node 版本) +4. 截图或日志(如有) + +## 行为准则 + +请保持友善、尊重他人。骚扰、歧视或恶意行为不被容忍。 diff --git a/README.md b/README.md index ba68cc9..152feee 100644 --- a/README.md +++ b/README.md @@ -1,68 +1,127 @@ +
+ # 姜十三论坛 Jiang13 Forum -轻量自用论坛系统,面向小圈子内部交流。与 Gitea 相同,编译为**单个独立 Go 二进制**,前端静态资源 embed 内嵌,内置 SQLite,开箱即用。 +**轻量 · 好看 · 单文件部署的现代化论坛** -## 项目目录结构 +面向小圈子内部交流,编译为单个 Go 二进制,前端 SPA 内嵌,内置 SQLite,开箱即用。 -``` -js13-forum/ -├── cmd/jiang13/main.go # 程序入口 -├── config/config.go # 命令行参数与配置 -├── model/ -│ ├── models.go # GORM 模型定义 -│ └── db.go # 数据库初始化与自动迁移 -├── service/ # 业务逻辑层 -│ ├── auth.go # 注册/登录/JWT -│ ├── user.go # 用户资料/头像 -│ ├── board.go # 板块 CRUD -│ ├── post.go # 帖子/点赞/收藏 -│ ├── comment.go # 楼层评论 -│ ├── backup.go # SQLite 备份 -│ ├── ratelimit.go # 访问限流 -│ └── common.go # 敏感词过滤等 -├── handler/ -│ ├── handlers.go # 前台页面与 API -│ └── admin.go # 后台管理 -├── middleware/auth.go # JWT 鉴权与限流中间件 -├── router/router.go # 路由注册 -├── embed_static/ -│ ├── embed.go # go:embed 声明 -│ ├── static/css/style.css # 内嵌 CSS -│ ├── static/js/app.js # 内嵌 JS -│ └── templates/ # 内嵌 HTML 模板 -├── go.mod -├── Makefile -└── README.md -``` +
+ +[![License: MIT](https://img.shields.io/badge/License-MIT-18a058?style=flat-square)](LICENSE) +[![Go](https://img.shields.io/badge/Go-1.26-00ADD8?style=flat-square&logo=go&logoColor=white)](go.mod) +[![React](https://img.shields.io/badge/React-18-61DAFB?style=flat-square&logo=react&logoColor=white)](frontend/package.json) +[![SQLite](https://img.shields.io/badge/SQLite-内置-003B57?style=flat-square&logo=sqlite&logoColor=white)](#) + +[快速开始](#-快速开始) · +[界面预览](#-界面预览) · +[功能亮点](#-功能亮点) · +[参与贡献](CONTRIBUTING.md) + +
+ +姜十三论坛首页 - 浅色主题三栏布局 + +浅色主题 · V2EX/NGA 风格三栏布局 · 虚拟滚动帖列表 + +
+ +--- + +## 界面预览 + +> 论坛用户第一眼看到的是界面。姜十三论坛采用清新绿色主题、高密度信息布局,兼顾桌面与移动端体验。 + + + + + + + + + + +
+ 浅色主题首页 +
浅色主题
+ 左栏板块导航 · 中间帖列表 · 右栏热门/动态/在线 +
+ 暗色主题首页 +
暗色主题
+ 一键切换 · 护眼阅读 · 全局色彩自适应 +
+ 帖子详情页 +
帖子详情
+ 楼层式回复 · 引用回复 · 登录可见内容 +
+ 移动端首页 +
移动端适配
+ 侧栏自动收起 · 触控友好 · 板块快捷筛选 +
+ +

+ 登录页 +
登录 / 注册 — 居中卡片式表单,简洁无干扰 +

+ +--- + +## 功能亮点 + +### 界面与交互 + +| 特性 | 说明 | +|------|------| +| **三栏布局** | 左栏板块菜单(可折叠)+ 中间虚拟滚动帖列表 + 右栏热门/通知/在线 | +| **虚拟滚动** | `@tanstack/react-virtual` 驱动帖列表与楼层回复,长列表依然流畅 | +| **已读 / 未读** | 未读高亮、角标提醒、批量标记已读 | +| **主题切换** | 浅色 / 暗色一键切换,跟随 `prefers-color-scheme` 与本地记忆 | +| **响应式** | 平板 / 手机自动收起侧栏,搜索、发帖、登录触手可及 | +| **高密度排版** | V2EX / NGA 风格信息密度,一屏浏览更多内容 | + +### 社区功能 + +- 用户注册 / 登录(bcrypt + JWT Cookie) +- 普通用户 / 管理员两级权限,**首个注册用户自动成为管理员** +- 板块管理、发帖、Markdown / 富文本、标签、置顶 +- 楼层式评论,支持回复指定楼层、@ 高亮、引用回复 +- 点赞、收藏、热门帖、最新动态 +- 管理员后台:删帖、删评论、禁言、SQLite 一键备份 +- 内置敏感词过滤、发帖 / 评论限流 + +### 部署体验 + +- **单二进制部署** — 与 Gitea 同款 `go:embed` 打包,无需 Nginx 反代静态资源 +- **零依赖数据库** — SQLite 内建,数据目录 `--data` 一处管理 +- **跨平台** — Windows / Linux / macOS 一键编译 + +--- ## 快速开始 ### 1. 编译 -**Windows(推荐,无需 GNU Make):** +**Windows(推荐):** ```powershell -# PowerShell .\build.ps1 - -# 或双击 / cmd -build.bat +# 或双击 build.bat ``` -**Linux / macOS(需 GNU Make):** +**Linux / macOS:** ```bash make build ``` -**手动分步(全平台通用):** +**手动分步(全平台):** ```bash cd frontend && npm install && npm run build -cd .. && go build -trimpath -ldflags "-s -w" -o dist/jiang13.exe ./cmd/jiang13 +cd .. && go build -trimpath -ldflags "-s -w" -o dist/jiang13 ./cmd/jiang13 ``` -> Windows 自带的 `make` 通常是 Embarcadero MAKE,**不能**识别本项目 Makefile。请用 `.\build.ps1` 或安装 GNU Make(`choco install make`)后再用 `make build`。 +> Windows 自带的 `make` 通常是 Embarcadero MAKE,不能识别本项目 Makefile。请用 `.\build.ps1` 或安装 GNU Make 后再用 `make build`。 跨平台编译: @@ -84,104 +143,90 @@ cd .. && go build -trimpath -ldflags "-s -w" -o dist/jiang13.exe ./cmd/jiang13 ### 3. 首次使用 -1. 浏览器打开 http://localhost:3000/register 注册账号 -2. **第一个注册的用户自动成为管理员**,无需额外命令 -3. 登录后可访问 http://localhost:3000/admin/dashboard 进入后台 +1. 浏览器打开 `http://localhost:3000/register` 注册账号 +2. **第一个注册的用户自动成为管理员** +3. 登录后访问 `http://localhost:3000/admin/dashboard` 进入后台 -## 启动参数 +### 启动参数 | 参数 | 默认值 | 说明 | |------|--------|------| -| `--port` | 3000 | HTTP 监听端口 | -| `--data` | ./data | 数据目录(SQLite、上传、日志) | -| `--jwt-secret` | 自动生成 | JWT 签名密钥(留空则自动生成并持久化到 data/.jwt_secret) | +| `--port` | `3000` | HTTP 监听端口 | +| `--data` | `./data` | 数据目录(SQLite、上传、日志) | +| `--jwt-secret` | 自动生成 | JWT 签名密钥(留空则持久化到 `data/.jwt_secret`) | -## 数据目录说明 +--- + +## 技术栈 + +| 层级 | 技术 | +|------|------| +| **后端** | Go 1.26 · Gin · GORM · SQLite | +| **前端** | React 18 · Radix UI · Tailwind CSS · TanStack Virtual | +| **构建** | Vite → `go:embed` 内嵌 SPA,单二进制发布 | +| **认证** | bcrypt · JWT Cookie | + +--- + +## 前端开发 + +日常改前端**不需要**重新 `npm run build` 或 `go build`,Vite 开发服务器支持秒级热更新(HMR,热模块替换): + +```powershell +.\build.ps1 -Target dev # Windows +make dev # Linux / macOS +``` + +浏览器访问 `http://localhost:5173`,API 自动代理到 `http://localhost:3000`。 + +**何时需要完整构建:** + +- 修改 Go 代码、HTML 模板、embed 静态资源 → `go build` / `make build` +- 发布单二进制前 → `npm run build` + `make build` + +> 直接访问 `:3000` 看到的是上次 build 嵌入的前端;开发时请用 `:5173`。 + +--- + +## 项目结构 + +``` +jiang13-forum/ +├── cmd/jiang13/ # 程序入口 +├── config/ # 命令行参数与配置 +├── model/ # GORM 模型与数据库迁移 +├── service/ # 业务逻辑(认证、帖子、评论…) +├── handler/ # HTTP 处理器(前台 + 后台) +├── middleware/ # JWT 鉴权、在线状态 +├── router/ # 路由注册 +├── embed_static/ # go:embed 内嵌的 SPA 与模板 +├── frontend/ # React 源码(Vite 构建) +├── docs/screenshots/ # README 界面截图 +└── scripts/ # 开发辅助脚本 +``` + +--- + +## 数据目录 ``` data/ ├── jiang13.db # SQLite 主数据库 ├── jiang13.log # 运行日志 -├── filter_words.txt # 敏感词配置(可编辑) +├── filter_words.txt # 敏感词配置 ├── .jwt_secret # JWT 密钥(自动生成) -├── uploads/avatars/ # 用户头像(运行时写入,非 embed) -└── jiang13_backup_*.db # 后台导出的备份文件 +├── uploads/avatars/ # 用户头像 +└── jiang13_backup_*.db # 后台导出的备份 ``` -## 修改前端并重新打包(Gitea 同款流程) +--- -1. 编辑 `embed_static/templates/` 下的 HTML 模板 -2. 编辑 `embed_static/static/css/` 或 `static/js/` 下的静态资源 -3. 重新编译:`make build` -4. 替换旧二进制,重启服务 +## 参与贡献 -前端资源通过 `//go:embed` 编译进二进制,**运行时不需要单独的前端文件夹**。只有用户上传的头像保存在 `--data` 目录。 +欢迎提交 Issue 和 Pull Request!详见 [CONTRIBUTING.md](CONTRIBUTING.md)。 -```go -// embed_static/embed.go -//go:embed static/* -var staticFS embed.FS - -//go:embed templates/* -var templatesFS embed.FS -``` - -## 功能概览 - -- 用户注册/登录(bcrypt + JWT Cookie) -- 普通用户 / 管理员两级权限 -- 板块管理、发帖、富文本 HTML、标签、置顶 -- 楼层式评论,支持回复指定楼层 -- 点赞、收藏 -- 管理员:删帖、删评论、禁言、SQLite 备份 -- 内置敏感词过滤、发帖/评论限流 - -## 技术栈 - -- **后端**:Go 1.26 + Gin + GORM + SQLite -- **前端**:React 18 + Arco Design + TanStack Virtual(虚拟滚动) -- **打包**:Vite 构建 → `go:embed` 内嵌 SPA,与 Gitea 同款单二进制 - -## 前端开发(热更新 HMR) - -日常改前端**不需要**重新 `npm run build` 或 `go build`,用 Vite 开发服务器即可秒级热更新: - -```bash -# 一键启动:Go 后端 + Vite 热更新(推荐) -.\build.ps1 -Target dev # Windows -make dev # Linux / macOS(需 GNU Make) - -# 浏览器访问 http://localhost:5173 -# API 自动代理到 http://localhost:3000 -``` - -也可开两个终端分别启动: - -```bash -# 终端 1:仅后端 -.\build.ps1 -Target run # 或 make run - -# 终端 2:仅前端热更新 -cd frontend && npm install && npm run dev -``` - -**何时才需要完整构建:** - -- 改 Go 代码、模板、embed 静态资源 → `go build` / `make build` -- 发布单二进制前 → `npm run build` + `make build`(前端产物写入 `embed_static/static/spa/` 后 embed 进二进制) - -> 若直接访问 `:3000`,看到的是**上次 build 嵌入**的前端,不会有热更新。开发时请用 `:5173`。 - -### 前端特性(高密度 V2EX/NGA 风格) - -| 特性 | 实现 | -|------|------| -| 三栏布局 | 左栏板块菜单(可折叠)+ 中间虚拟滚动帖列表 + 右栏热门/通知/在线 | -| 虚拟滚动 | `@tanstack/react-virtual` 帖列表 & 楼层回复 | -| 已读/未读 | 未读高亮 + 角标 + 批量标记已读 | -| 主题 | 浅色/暗黑一键切换,平板/手机自动收起侧栏 | -| 交互 | 滚动加载更多、快捷回帖、楼层锚点、引用回复、@高亮 | +--- ## 许可证 -MIT — 自用随意,欢迎修改。 +[MIT](LICENSE) — 自由使用、修改与分发。 diff --git a/cmd/jiang13/main.go b/cmd/jiang13/main.go index 97d747f..a238376 100644 --- a/cmd/jiang13/main.go +++ b/cmd/jiang13/main.go @@ -11,9 +11,9 @@ import ( "syscall" "time" - "github.com/jiang13/forum/config" - "github.com/jiang13/forum/model" - "github.com/jiang13/forum/router" + "git.iioio.com/freefire/jiang13-forum/config" + "git.iioio.com/freefire/jiang13-forum/model" + "git.iioio.com/freefire/jiang13-forum/router" ) func main() { diff --git a/docs/screenshots/compose.png b/docs/screenshots/compose.png new file mode 100644 index 0000000..5ee246d Binary files /dev/null and b/docs/screenshots/compose.png differ diff --git a/docs/screenshots/home-dark.png b/docs/screenshots/home-dark.png new file mode 100644 index 0000000..729e952 Binary files /dev/null and b/docs/screenshots/home-dark.png differ diff --git a/docs/screenshots/home-light.png b/docs/screenshots/home-light.png new file mode 100644 index 0000000..2d27755 Binary files /dev/null and b/docs/screenshots/home-light.png differ diff --git a/docs/screenshots/mobile-home.png b/docs/screenshots/mobile-home.png new file mode 100644 index 0000000..8b63ce1 Binary files /dev/null and b/docs/screenshots/mobile-home.png differ diff --git a/docs/screenshots/post-detail.png b/docs/screenshots/post-detail.png new file mode 100644 index 0000000..a8239ed Binary files /dev/null and b/docs/screenshots/post-detail.png differ diff --git a/go.mod b/go.mod index 8e9235b..b8c8965 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/jiang13/forum +module git.iioio.com/freefire/jiang13-forum go 1.26 diff --git a/handler/admin.go b/handler/admin.go index c544b32..609f1af 100644 --- a/handler/admin.go +++ b/handler/admin.go @@ -7,8 +7,8 @@ import ( "strings" "github.com/gin-gonic/gin" - "github.com/jiang13/forum/model" - "github.com/jiang13/forum/service" + "git.iioio.com/freefire/jiang13-forum/model" + "git.iioio.com/freefire/jiang13-forum/service" ) // --- 后台管理页面 --- diff --git a/handler/api.go b/handler/api.go index 2ca13a7..b29a747 100644 --- a/handler/api.go +++ b/handler/api.go @@ -5,8 +5,8 @@ import ( "strconv" "github.com/gin-gonic/gin" - "github.com/jiang13/forum/model" - "github.com/jiang13/forum/service" + "git.iioio.com/freefire/jiang13-forum/model" + "git.iioio.com/freefire/jiang13-forum/service" ) // APIMe 当前登录用户 diff --git a/handler/handlers.go b/handler/handlers.go index 9c1c0fd..016eda5 100644 --- a/handler/handlers.go +++ b/handler/handlers.go @@ -6,10 +6,10 @@ import ( "strings" "github.com/gin-gonic/gin" - "github.com/jiang13/forum/config" - "github.com/jiang13/forum/middleware" - "github.com/jiang13/forum/model" - "github.com/jiang13/forum/service" + "git.iioio.com/freefire/jiang13-forum/config" + "git.iioio.com/freefire/jiang13-forum/middleware" + "git.iioio.com/freefire/jiang13-forum/model" + "git.iioio.com/freefire/jiang13-forum/service" ) // Handlers 聚合所有 HTTP 处理器 diff --git a/middleware/auth.go b/middleware/auth.go index e8f82f4..91757f9 100644 --- a/middleware/auth.go +++ b/middleware/auth.go @@ -6,8 +6,8 @@ import ( "strings" "github.com/gin-gonic/gin" - "github.com/jiang13/forum/model" - "github.com/jiang13/forum/service" + "git.iioio.com/freefire/jiang13-forum/model" + "git.iioio.com/freefire/jiang13-forum/service" ) const ( diff --git a/middleware/presence.go b/middleware/presence.go index ef85529..f596b77 100644 --- a/middleware/presence.go +++ b/middleware/presence.go @@ -5,7 +5,7 @@ import ( "encoding/hex" "github.com/gin-gonic/gin" - "github.com/jiang13/forum/service" + "git.iioio.com/freefire/jiang13-forum/service" ) const VisitorCookieName = "j13_vid" diff --git a/router/router.go b/router/router.go index cbd9167..a70cf91 100644 --- a/router/router.go +++ b/router/router.go @@ -6,11 +6,11 @@ import ( "time" "github.com/gin-gonic/gin" - "github.com/jiang13/forum/config" - "github.com/jiang13/forum/embed_static" - "github.com/jiang13/forum/handler" - "github.com/jiang13/forum/middleware" - "github.com/jiang13/forum/service" + "git.iioio.com/freefire/jiang13-forum/config" + "git.iioio.com/freefire/jiang13-forum/embed_static" + "git.iioio.com/freefire/jiang13-forum/handler" + "git.iioio.com/freefire/jiang13-forum/middleware" + "git.iioio.com/freefire/jiang13-forum/service" ) func Setup(cfg *config.Config) (*gin.Engine, error) { diff --git a/scripts/capture-screenshots.mjs b/scripts/capture-screenshots.mjs new file mode 100644 index 0000000..7d6fa04 --- /dev/null +++ b/scripts/capture-screenshots.mjs @@ -0,0 +1,86 @@ +import { chromium } from 'playwright'; +import { mkdir } from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const outDir = path.join(__dirname, '..', 'docs', 'screenshots'); +const base = process.env.J13_URL || 'http://localhost:8080'; + +async function shot(page, name, opts = {}) { + const file = path.join(outDir, name); + await page.screenshot({ path: file, type: 'png', ...opts }); + console.log('saved', file); +} + +async function setTheme(page, theme) { + await page.addInitScript((t) => { + localStorage.setItem('j13-theme', t); + document.documentElement.classList.toggle('dark', t === 'dark'); + document.documentElement.style.colorScheme = t; + }, theme); +} + +const browser = await chromium.launch(); +const context = await browser.newContext({ deviceScaleFactor: 2 }); + +try { + await mkdir(outDir, { recursive: true }); + + // 首页 · 浅色 + { + const page = await context.newPage(); + await page.setViewportSize({ width: 1440, height: 900 }); + await setTheme(page, 'light'); + await page.goto(`${base}/`, { waitUntil: 'networkidle' }); + await page.waitForTimeout(800); + await shot(page, 'home-light.png'); + await page.close(); + } + + // 首页 · 暗色 + { + const page = await context.newPage(); + await page.setViewportSize({ width: 1440, height: 900 }); + await setTheme(page, 'dark'); + await page.goto(`${base}/`, { waitUntil: 'networkidle' }); + await page.waitForTimeout(800); + await shot(page, 'home-dark.png'); + await page.close(); + } + + // 帖子详情 + { + const page = await context.newPage(); + await page.setViewportSize({ width: 1440, height: 900 }); + await setTheme(page, 'light'); + await page.goto(`${base}/post/2`, { waitUntil: 'networkidle' }); + await page.waitForTimeout(1000); + await shot(page, 'post-detail.png'); + await page.close(); + } + + // 发帖编辑器 + { + const page = await context.newPage(); + await page.setViewportSize({ width: 1440, height: 900 }); + await setTheme(page, 'light'); + await page.goto(`${base}/compose`, { waitUntil: 'networkidle' }); + await page.waitForTimeout(800); + await shot(page, 'compose.png'); + await page.close(); + } + + // 移动端 + { + const page = await context.newPage(); + await page.setViewportSize({ width: 390, height: 844 }); + await setTheme(page, 'light'); + await page.goto(`${base}/`, { waitUntil: 'networkidle' }); + await page.waitForTimeout(800); + await shot(page, 'mobile-home.png', { fullPage: true }); + await page.close(); + } +} finally { + await browser.close(); +} diff --git a/service/auth.go b/service/auth.go index fa82368..3c23b1a 100644 --- a/service/auth.go +++ b/service/auth.go @@ -5,7 +5,7 @@ import ( "time" "github.com/golang-jwt/jwt/v5" - "github.com/jiang13/forum/model" + "git.iioio.com/freefire/jiang13-forum/model" ) const TokenExpire = 7 * 24 * time.Hour diff --git a/service/board.go b/service/board.go index 9cdd4ca..0358ab2 100644 --- a/service/board.go +++ b/service/board.go @@ -3,7 +3,7 @@ package service import ( "errors" - "github.com/jiang13/forum/model" + "git.iioio.com/freefire/jiang13-forum/model" ) type BoardService struct{} diff --git a/service/comment.go b/service/comment.go index 9c0476e..f7012ba 100644 --- a/service/comment.go +++ b/service/comment.go @@ -6,7 +6,7 @@ import ( "net/url" "strings" - "github.com/jiang13/forum/model" + "git.iioio.com/freefire/jiang13-forum/model" ) type CommentService struct { diff --git a/service/online.go b/service/online.go index 10460a5..3c06ba4 100644 --- a/service/online.go +++ b/service/online.go @@ -4,7 +4,7 @@ import ( "sync" "time" - "github.com/jiang13/forum/model" + "git.iioio.com/freefire/jiang13-forum/model" ) const onlineTTL = 5 * time.Minute diff --git a/service/post.go b/service/post.go index f8a68c7..9ac6811 100644 --- a/service/post.go +++ b/service/post.go @@ -4,7 +4,7 @@ import ( "errors" "strings" - "github.com/jiang13/forum/model" + "git.iioio.com/freefire/jiang13-forum/model" "gorm.io/gorm" ) diff --git a/service/user.go b/service/user.go index 5e516c1..3f8c3d1 100644 --- a/service/user.go +++ b/service/user.go @@ -10,7 +10,7 @@ import ( "strings" "time" - "github.com/jiang13/forum/model" + "git.iioio.com/freefire/jiang13-forum/model" ) type UserService struct {