开源准备:重写 README 展示 UI/UX,更新模块路径

- 新增界面截图与 CONTRIBUTING.md
- README 以三栏布局、暗色主题、移动端等视觉亮点为主
- go module 迁移至 git.iioio.com/freefire/jiang13-forum

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
freefire
2026-06-15 21:15:51 +08:00
parent 99ae0ccdaa
commit b27289bbea
23 changed files with 327 additions and 149 deletions

1
.gitignore vendored
View File

@@ -2,6 +2,7 @@
/data/ /data/
# 前端依赖与构建缓存 # 前端依赖与构建缓存
/node_modules/
/frontend/node_modules/ /frontend/node_modules/
/frontend/dist/ /frontend/dist/

46
CONTRIBUTING.md Normal file
View File

@@ -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. 截图或日志(如有)
## 行为准则
请保持友善、尊重他人。骚扰、歧视或恶意行为不被容忍。

291
README.md
View File

@@ -1,68 +1,127 @@
<div align="center">
# 姜十三论坛 Jiang13 Forum # 姜十三论坛 Jiang13 Forum
轻量自用论坛系统,面向小圈子内部交流。与 Gitea 相同,编译为**单个独立 Go 二进制**,前端静态资源 embed 内嵌,内置 SQLite开箱即用。 **轻量 · 好看 · 单文件部署的现代化论坛**
## 项目目录结构 面向小圈子内部交流,编译为单个 Go 二进制,前端 SPA 内嵌,内置 SQLite开箱即用。
``` <br>
js13-forum/
├── cmd/jiang13/main.go # 程序入口 [![License: MIT](https://img.shields.io/badge/License-MIT-18a058?style=flat-square)](LICENSE)
├── config/config.go # 命令行参数与配置 [![Go](https://img.shields.io/badge/Go-1.26-00ADD8?style=flat-square&logo=go&logoColor=white)](go.mod)
├── model/ [![React](https://img.shields.io/badge/React-18-61DAFB?style=flat-square&logo=react&logoColor=white)](frontend/package.json)
│ ├── models.go # GORM 模型定义 [![SQLite](https://img.shields.io/badge/SQLite-内置-003B57?style=flat-square&logo=sqlite&logoColor=white)](#)
│ └── db.go # 数据库初始化与自动迁移
├── service/ # 业务逻辑层 [快速开始](#-快速开始) ·
│ ├── auth.go # 注册/登录/JWT [界面预览](#-界面预览) ·
│ ├── user.go # 用户资料/头像 [功能亮点](#-功能亮点) ·
│ ├── board.go # 板块 CRUD [参与贡献](CONTRIBUTING.md)
│ ├── post.go # 帖子/点赞/收藏
│ ├── comment.go # 楼层评论 <br>
│ ├── backup.go # SQLite 备份
│ ├── ratelimit.go # 访问限流 <img src="docs/screenshots/home-light.png" alt="姜十三论坛首页 - 浅色主题三栏布局" width="92%">
│ └── common.go # 敏感词过滤等
├── handler/ <sub>浅色主题 · V2EX/NGA 风格三栏布局 · 虚拟滚动帖列表</sub>
│ ├── handlers.go # 前台页面与 API
│ └── admin.go # 后台管理 </div>
├── 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 <table>
├── Makefile <tr>
└── README.md <td width="50%" align="center">
``` <img src="docs/screenshots/home-light.png" alt="浅色主题首页" width="100%">
<br><b>浅色主题</b><br>
<sub>左栏板块导航 · 中间帖列表 · 右栏热门/动态/在线</sub>
</td>
<td width="50%" align="center">
<img src="docs/screenshots/home-dark.png" alt="暗色主题首页" width="100%">
<br><b>暗色主题</b><br>
<sub>一键切换 · 护眼阅读 · 全局色彩自适应</sub>
</td>
</tr>
<tr>
<td width="50%" align="center">
<img src="docs/screenshots/post-detail.png" alt="帖子详情页" width="100%">
<br><b>帖子详情</b><br>
<sub>楼层式回复 · 引用回复 · 登录可见内容</sub>
</td>
<td width="50%" align="center">
<img src="docs/screenshots/mobile-home.png" alt="移动端首页" width="280">
<br><b>移动端适配</b><br>
<sub>侧栏自动收起 · 触控友好 · 板块快捷筛选</sub>
</td>
</tr>
</table>
<p align="center">
<img src="docs/screenshots/compose.png" alt="登录页" width="360">
<br><b>登录 / 注册</b> — 居中卡片式表单,简洁无干扰
</p>
---
## 功能亮点
### 界面与交互
| 特性 | 说明 |
|------|------|
| **三栏布局** | 左栏板块菜单(可折叠)+ 中间虚拟滚动帖列表 + 右栏热门/通知/在线 |
| **虚拟滚动** | `@tanstack/react-virtual` 驱动帖列表与楼层回复,长列表依然流畅 |
| **已读 / 未读** | 未读高亮、角标提醒、批量标记已读 |
| **主题切换** | 浅色 / 暗色一键切换,跟随 `prefers-color-scheme` 与本地记忆 |
| **响应式** | 平板 / 手机自动收起侧栏,搜索、发帖、登录触手可及 |
| **高密度排版** | V2EX / NGA 风格信息密度,一屏浏览更多内容 |
### 社区功能
- 用户注册 / 登录bcrypt + JWT Cookie
- 普通用户 / 管理员两级权限,**首个注册用户自动成为管理员**
- 板块管理、发帖、Markdown / 富文本、标签、置顶
- 楼层式评论,支持回复指定楼层、@ 高亮、引用回复
- 点赞、收藏、热门帖、最新动态
- 管理员后台删帖、删评论、禁言、SQLite 一键备份
- 内置敏感词过滤、发帖 / 评论限流
### 部署体验
- **单二进制部署** — 与 Gitea 同款 `go:embed` 打包,无需 Nginx 反代静态资源
- **零依赖数据库** — SQLite 内建,数据目录 `--data` 一处管理
- **跨平台** — Windows / Linux / macOS 一键编译
---
## 快速开始 ## 快速开始
### 1. 编译 ### 1. 编译
**Windows推荐,无需 GNU Make** **Windows推荐**
```powershell ```powershell
# PowerShell
.\build.ps1 .\build.ps1
# 或双击 build.bat
# 或双击 / cmd
build.bat
``` ```
**Linux / macOS(需 GNU Make** **Linux / macOS**
```bash ```bash
make build make build
``` ```
**手动分步(全平台通用** **手动分步(全平台):**
```bash ```bash
cd frontend && npm install && npm run build 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. 首次使用 ### 3. 首次使用
1. 浏览器打开 http://localhost:3000/register 注册账号 1. 浏览器打开 `http://localhost:3000/register` 注册账号
2. **第一个注册的用户自动成为管理员**,无需额外命令 2. **第一个注册的用户自动成为管理员**
3. 登录后访问 http://localhost:3000/admin/dashboard 进入后台 3. 登录后访问 `http://localhost:3000/admin/dashboard` 进入后台
## 启动参数 ### 启动参数
| 参数 | 默认值 | 说明 | | 参数 | 默认值 | 说明 |
|------|--------|------| |------|--------|------|
| `--port` | 3000 | HTTP 监听端口 | | `--port` | `3000` | HTTP 监听端口 |
| `--data` | ./data | 数据目录SQLite、上传、日志 | | `--data` | `./data` | 数据目录SQLite、上传、日志 |
| `--jwt-secret` | 自动生成 | JWT 签名密钥(留空则自动生成并持久化到 data/.jwt_secret | | `--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/ data/
├── jiang13.db # SQLite 主数据库 ├── jiang13.db # SQLite 主数据库
├── jiang13.log # 运行日志 ├── jiang13.log # 运行日志
├── filter_words.txt # 敏感词配置(可编辑) ├── filter_words.txt # 敏感词配置
├── .jwt_secret # JWT 密钥(自动生成) ├── .jwt_secret # JWT 密钥(自动生成)
├── uploads/avatars/ # 用户头像(运行时写入,非 embed ├── uploads/avatars/ # 用户头像
└── jiang13_backup_*.db # 后台导出的备份文件 └── 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) — 自由使用、修改与分发

View File

@@ -11,9 +11,9 @@ import (
"syscall" "syscall"
"time" "time"
"github.com/jiang13/forum/config" "git.iioio.com/freefire/jiang13-forum/config"
"github.com/jiang13/forum/model" "git.iioio.com/freefire/jiang13-forum/model"
"github.com/jiang13/forum/router" "git.iioio.com/freefire/jiang13-forum/router"
) )
func main() { func main() {

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 420 KiB

2
go.mod
View File

@@ -1,4 +1,4 @@
module github.com/jiang13/forum module git.iioio.com/freefire/jiang13-forum
go 1.26 go 1.26

View File

@@ -7,8 +7,8 @@ import (
"strings" "strings"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/jiang13/forum/model" "git.iioio.com/freefire/jiang13-forum/model"
"github.com/jiang13/forum/service" "git.iioio.com/freefire/jiang13-forum/service"
) )
// --- 后台管理页面 --- // --- 后台管理页面 ---

View File

@@ -5,8 +5,8 @@ import (
"strconv" "strconv"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/jiang13/forum/model" "git.iioio.com/freefire/jiang13-forum/model"
"github.com/jiang13/forum/service" "git.iioio.com/freefire/jiang13-forum/service"
) )
// APIMe 当前登录用户 // APIMe 当前登录用户

View File

@@ -6,10 +6,10 @@ import (
"strings" "strings"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/jiang13/forum/config" "git.iioio.com/freefire/jiang13-forum/config"
"github.com/jiang13/forum/middleware" "git.iioio.com/freefire/jiang13-forum/middleware"
"github.com/jiang13/forum/model" "git.iioio.com/freefire/jiang13-forum/model"
"github.com/jiang13/forum/service" "git.iioio.com/freefire/jiang13-forum/service"
) )
// Handlers 聚合所有 HTTP 处理器 // Handlers 聚合所有 HTTP 处理器

View File

@@ -6,8 +6,8 @@ import (
"strings" "strings"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/jiang13/forum/model" "git.iioio.com/freefire/jiang13-forum/model"
"github.com/jiang13/forum/service" "git.iioio.com/freefire/jiang13-forum/service"
) )
const ( const (

View File

@@ -5,7 +5,7 @@ import (
"encoding/hex" "encoding/hex"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/jiang13/forum/service" "git.iioio.com/freefire/jiang13-forum/service"
) )
const VisitorCookieName = "j13_vid" const VisitorCookieName = "j13_vid"

View File

@@ -6,11 +6,11 @@ import (
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/jiang13/forum/config" "git.iioio.com/freefire/jiang13-forum/config"
"github.com/jiang13/forum/embed_static" "git.iioio.com/freefire/jiang13-forum/embed_static"
"github.com/jiang13/forum/handler" "git.iioio.com/freefire/jiang13-forum/handler"
"github.com/jiang13/forum/middleware" "git.iioio.com/freefire/jiang13-forum/middleware"
"github.com/jiang13/forum/service" "git.iioio.com/freefire/jiang13-forum/service"
) )
func Setup(cfg *config.Config) (*gin.Engine, error) { func Setup(cfg *config.Config) (*gin.Engine, error) {

View File

@@ -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();
}

View File

@@ -5,7 +5,7 @@ import (
"time" "time"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
"github.com/jiang13/forum/model" "git.iioio.com/freefire/jiang13-forum/model"
) )
const TokenExpire = 7 * 24 * time.Hour const TokenExpire = 7 * 24 * time.Hour

View File

@@ -3,7 +3,7 @@ package service
import ( import (
"errors" "errors"
"github.com/jiang13/forum/model" "git.iioio.com/freefire/jiang13-forum/model"
) )
type BoardService struct{} type BoardService struct{}

View File

@@ -6,7 +6,7 @@ import (
"net/url" "net/url"
"strings" "strings"
"github.com/jiang13/forum/model" "git.iioio.com/freefire/jiang13-forum/model"
) )
type CommentService struct { type CommentService struct {

View File

@@ -4,7 +4,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/jiang13/forum/model" "git.iioio.com/freefire/jiang13-forum/model"
) )
const onlineTTL = 5 * time.Minute const onlineTTL = 5 * time.Minute

View File

@@ -4,7 +4,7 @@ import (
"errors" "errors"
"strings" "strings"
"github.com/jiang13/forum/model" "git.iioio.com/freefire/jiang13-forum/model"
"gorm.io/gorm" "gorm.io/gorm"
) )

View File

@@ -10,7 +10,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/jiang13/forum/model" "git.iioio.com/freefire/jiang13-forum/model"
) )
type UserService struct { type UserService struct {