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
@@ -1,6 +1,7 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/csv"
|
||||
@@ -14,8 +15,12 @@ import (
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/xuri/excelize/v2"
|
||||
"golang.org/x/text/encoding/simplifiedchinese"
|
||||
textunicode "golang.org/x/text/encoding/unicode"
|
||||
"golang.org/x/text/transform"
|
||||
|
||||
"git.yeij.top/AskaEth/Scoreboard/server/internal/auth"
|
||||
"git.yeij.top/AskaEth/Scoreboard/server/internal/middleware"
|
||||
@@ -658,7 +663,11 @@ func readTable(ext string, file io.Reader) ([][]string, error) {
|
||||
}
|
||||
return rows, nil
|
||||
default:
|
||||
reader := csv.NewReader(file)
|
||||
text, err := decodeTableText(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reader := csv.NewReader(strings.NewReader(text))
|
||||
reader.FieldsPerRecord = -1
|
||||
reader.TrimLeadingSpace = true
|
||||
rows, err := reader.ReadAll()
|
||||
@@ -669,6 +678,56 @@ func readTable(ext string, file io.Reader) ([][]string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// 报名表文本可能带的字节序标记。
|
||||
var (
|
||||
tableUTF8BOM = []byte{0xEF, 0xBB, 0xBF}
|
||||
tableUTF16LEBOM = []byte{0xFF, 0xFE}
|
||||
tableUTF16BEBOM = []byte{0xFE, 0xFF}
|
||||
)
|
||||
|
||||
// decodeTableText 把报名表的字节流解码成 UTF-8 文本。
|
||||
//
|
||||
// 现场拿到的 CSV 基本都是直接从 Excel 存的,而 Excel 默认不写 UTF-8:中文 Windows 上
|
||||
// 「另存为 CSV」出的是 GB18030,「CSV UTF-8」会带 BOM,PowerShell 的 Export-Csv 出 UTF-16LE。
|
||||
// 这三种原样丢给 csv.Reader 都会炸,而且症状很误导——
|
||||
// 中文单位名、运动员名变乱码;BOM 会粘在第一列表头上("\ufeffevent_code"),
|
||||
// 于是报「报名表缺少列 event_code」,让人以为表头写错了。
|
||||
func decodeTableText(file io.Reader) (string, error) {
|
||||
// 上游 readImportForm 已用 MaxBytesReader 限住 body,整读不会失控。
|
||||
data, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
return "", badRequest("读取报名表失败", err.Error())
|
||||
}
|
||||
|
||||
switch {
|
||||
case bytes.HasPrefix(data, tableUTF8BOM):
|
||||
data = data[len(tableUTF8BOM):]
|
||||
case bytes.HasPrefix(data, tableUTF16LEBOM):
|
||||
return decodeUTF16Table(data[len(tableUTF16LEBOM):], textunicode.LittleEndian)
|
||||
case bytes.HasPrefix(data, tableUTF16BEBOM):
|
||||
return decodeUTF16Table(data[len(tableUTF16BEBOM):], textunicode.BigEndian)
|
||||
}
|
||||
|
||||
if utf8.Valid(data) {
|
||||
return string(data), nil
|
||||
}
|
||||
// GBK 是 GB18030 的子集,按 GB18030 解能一并覆盖。
|
||||
text, _, err := transform.String(simplifiedchinese.GB18030.NewDecoder(), string(data))
|
||||
if err != nil {
|
||||
return "", badRequest("报名表编码无法识别,请另存为 UTF-8 或 GB18030 编码的 CSV 后重试", err.Error())
|
||||
}
|
||||
return text, nil
|
||||
}
|
||||
|
||||
// decodeUTF16Table 解 UTF-16(BOM 已由调用方摘掉)。
|
||||
func decodeUTF16Table(data []byte, order textunicode.Endianness) (string, error) {
|
||||
text, _, err := transform.String(textunicode.UTF16(order, textunicode.IgnoreBOM).NewDecoder(), string(data))
|
||||
if err != nil {
|
||||
return "", badRequest("报名表编码无法识别,请另存为 UTF-8 编码的 CSV 后重试", err.Error())
|
||||
}
|
||||
return text, nil
|
||||
}
|
||||
|
||||
// importColumns 是报名表必须包含的表头(大小写不敏感);
|
||||
// id_card、gender、age 为可选列,缺列不报错,按空值处理。
|
||||
var importColumns = []string{
|
||||
|
||||
Reference in new issue
Block a user