feat: 字体可配并随赛事包打包 + 大屏调试标识收敛 + 报名表 CSV 编码兼容
三块改动,均为现场反馈:
一、字体配置与随包打包(此前「没有入口可以配置字体」,导出包也不带字体)
- 服务端新增 httpapi/fonts.go 与 4 条路由:GET /api/fonts(recorder)、
POST /api/fonts(admin,multipart file)、DELETE /api/fonts/{name}(admin)、
GET /uploads/fonts/{name}(匿名——大屏 @font-face 不带会话)
- 上传限 ttf/otf/woff/woff2、单个 20 MiB;文件名经 safeFontName 收敛且需与
fontContentType 往返一致,挡住路径穿越;重名 409、删除被引用字体 409
- 新增设置键 display.font_title / display.font_body(DTO fontTitle/fontBody,
随快照下发给大屏),写入时校验取值合法
- 导出:接上一直没被赋值的 IncludeFonts/FontsDir;包内落点用 uploads/fonts/
(运行时只读这里,落顶层 fonts/ 等于没人读)
- 大屏按配置动态注册字体并挂 --display-font-title/body,加载失败静默回落默认字体
- 管理端「赛事信息」新增字体区块:标题/正文字体下拉、上传、字体库、实时预览
二、大屏显示收敛
- 数据版本号角标 vN 彻底移除(原先只有窗口化窗口会显示,两处显隐口径不一致)
- 窗口化窗口按 F11 全屏后自绘标题栏不让位:根因是 Console 用 win.setFullScreen()
做窗口级全屏,而页面用 document.fullscreenElement 判断,后者恒为 null;
新增 display-preload.js 下发窗口级全屏状态,页面侧 shell.ts 把两级全屏取或,
无 preload 时(浏览器直连 /display)回落到文档级监听
三、报名表 CSV 编码兼容(用户报「解析中文字符遇到编码问题」)
- 服务端不再把原始字节直接喂给 csv.Reader:先摘 UTF-8 BOM,再认 UTF-16 LE/BE
BOM,其余非合法 UTF-8 按 GB18030 解(GBK 是其子集)
- 修复前症状:GBK 文件(Excel 另存为 CSV 的默认编码)中文变乱码;带 BOM 的文件
(Excel「CSV UTF-8」,也正是我们自己模板的格式)把 BOM 拼进第一列表头,
报「报名表缺少列 event_code」——连「下载模板→填→传回来」都走不通
- golang.org/x/text 由 indirect 提升为直接依赖,未新增外部依赖
测试:服务端 12 包全绿(新增 fonts_test.go / fonts_internal_test.go /
handlers_import_encoding_test.go);前端 vue-tsc 通过、pnpm build 通过、157 例全过;
Console 语法检查 17 个文件通过。docs/ 与 devdocs/ 同步更新。
This commit is contained in:
1 parent
9c6d0bd3b3
commit
fc96d17660
42 files changed
+2451
-95
No files matched your search
@@ -63,8 +63,8 @@ type ProjectExportOptions struct {
|
||||
CreatedBy string
|
||||
AppVersion string
|
||||
UploadsDir string // 可选:背景图 / 会标目录
|
||||
FontsDir string // 可选:字体目录
|
||||
IncludeFonts bool // 是否把字体一并打包
|
||||
FontsDir string // 可选:上传字体目录(运行时的 <项目目录>/uploads/fonts)
|
||||
IncludeFonts bool // 是否把字体一并打包(打进包内 uploads/fonts/)
|
||||
RulesPath string // 可选:规则包 YAML
|
||||
Now time.Time
|
||||
}
|
||||
@@ -121,7 +121,9 @@ func ExportProjectZip(db *sql.DB, dst string, opts ProjectExportOptions) (Projec
|
||||
}
|
||||
}
|
||||
if opts.IncludeFonts && opts.FontsDir != "" {
|
||||
if err := copyTree(opts.FontsDir, filepath.Join(tmp, "fonts")); err != nil {
|
||||
// 字体在包内的位置就是运行时的位置(<项目目录>/uploads/fonts/),
|
||||
// 否则导入后字体落在没人读的 fonts/ 下,兜底就成了假兜底。
|
||||
if err := copyTree(opts.FontsDir, filepath.Join(uploads, "fonts")); err != nil {
|
||||
return ProjectMeta{}, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +89,36 @@ func TestExportImportProjectZip(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// 字体必须落在包内 uploads/fonts/:导入后它就在运行时的字体目录下,大屏的 @font-face 才找得到。
|
||||
// 这里只给 FontsDir、不给 UploadsDir,顺带验证兜底路径本身是通的。
|
||||
func TestExportProjectZipIncludesFontsFallback(t *testing.T) {
|
||||
db := openEventDB(t)
|
||||
seedEvent(t, db)
|
||||
|
||||
fonts := t.TempDir()
|
||||
writeFile(t, filepath.Join(fonts, "思源宋体.ttf"), "font-bytes")
|
||||
|
||||
zipPath := filepath.Join(t.TempDir(), "赛事包.zip")
|
||||
if _, err := ExportProjectZip(db, zipPath, ProjectExportOptions{
|
||||
Name: "字体赛事",
|
||||
FontsDir: fonts,
|
||||
IncludeFonts: true,
|
||||
}); err != nil {
|
||||
t.Fatalf("导出赛事包: %v", err)
|
||||
}
|
||||
if !zipHas(zipPath, t, "uploads/fonts/思源宋体.ttf") {
|
||||
t.Fatal("包内缺少 uploads/fonts/思源宋体.ttf")
|
||||
}
|
||||
|
||||
res, err := ImportProjectZip(zipPath, t.TempDir(), ImportOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("导入赛事包: %v", err)
|
||||
}
|
||||
if got := readFile(t, filepath.Join(res.Dir, "uploads", "fonts", "思源宋体.ttf")); got != "font-bytes" {
|
||||
t.Errorf("导入后字体内容 = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportRejectsBadPackages(t *testing.T) {
|
||||
base := ProjectMeta{
|
||||
Format: projectFormat,
|
||||
|
||||
Reference in new issue
Block a user