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
+2
-1
@@ -38,6 +38,7 @@ console/
|
||||
├── styles.css
|
||||
├── app.js
|
||||
├── preload.js # 管理窗口的 contextBridge 白名单
|
||||
├── display-preload.js # 大屏窗口的 window.displayShell 白名单(窗口级全屏状态,供页面收放自绘标题栏)
|
||||
└── shell-preload.js # 服务端管理面板窗口的 window.desktopShell 白名单(窗口控制 + 显示器菜单)
|
||||
```
|
||||
|
||||
@@ -117,7 +118,7 @@ electron-builder 生成的卸载器结尾是 `RMDir /r $INSTDIR`,而**覆盖
|
||||
|
||||
| 场景 | 行为 |
|
||||
| --- | --- |
|
||||
| 只有一块屏 | 窗口化(屏幕 80% 大小、居中),不做多屏编排,**不挂** `display-metrics-changed` 等监听(行为冻结);`F11` 手动切换全屏 |
|
||||
| 只有一块屏 | 窗口化(屏幕 80% 大小、居中),不做多屏编排,**不挂** `display-metrics-changed` 等监听(行为冻结);`F11` 手动切换全屏,窗口级全屏状态经 `display-preload.js` 下发给页面,页面据此收起自绘标题栏 |
|
||||
| 多块屏 | 除内置屏外的每块屏一个 kiosk 窗口,按 `display.bounds`(DIP)定位,窗口 `setIgnoreMouseEvents(true)`,光标由页面 CSS 隐藏 |
|
||||
| 选屏 | 运行时枚举,不写死坐标;`selection` 可选 `all-secondary`(默认,用 `display.internal` 排除内置屏)/ `all` / `specified` |
|
||||
| 页面锁定 | 每块屏各锁一页,URL 形如 `http://127.0.0.1:19540/display?page=standings&lock=1&screen=<id>&instance=<host>-<id>`,页序按 `display.pages` 轮转 |
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use strict'
|
||||
|
||||
const os = require('os')
|
||||
const path = require('path')
|
||||
const { BrowserWindow, screen } = require('electron')
|
||||
const { healthHost } = require('./service')
|
||||
|
||||
@@ -133,6 +134,8 @@ class DisplayManager {
|
||||
backgroundColor: this.display.backgroundColor || '#000000',
|
||||
title: `大屏 · ${page}`,
|
||||
webPreferences: {
|
||||
// 页面看不到窗口级全屏,靠这个 preload 转达(见 display-preload.js)
|
||||
preload: path.join(__dirname, '..', 'renderer', 'display-preload.js'),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
sandbox: true,
|
||||
@@ -156,6 +159,17 @@ class DisplayManager {
|
||||
})
|
||||
}
|
||||
|
||||
// 窗口级全屏只在主进程可见:页面拿不到 setFullScreen 的结果,标题栏也就不会让位。
|
||||
// 状态变化下发一次之外,加载完成还要补发一次——重载/渲染进程崩溃恢复后窗口可能
|
||||
// 已经是全屏,只挂事件会让页面一直画着标题栏。
|
||||
const notifyFullscreen = () => {
|
||||
if (win.isDestroyed()) return
|
||||
win.webContents.send('window:fullscreen-changed', win.isFullScreen())
|
||||
}
|
||||
win.on('enter-full-screen', notifyFullscreen)
|
||||
win.on('leave-full-screen', notifyFullscreen)
|
||||
win.webContents.on('did-finish-load', notifyFullscreen)
|
||||
|
||||
win.once('ready-to-show', () => {
|
||||
if (!win.isDestroyed()) win.show()
|
||||
})
|
||||
|
||||
@@ -569,6 +569,10 @@ function main() {
|
||||
const win = BrowserWindow.fromWebContents(event.sender)
|
||||
return Boolean(win && !win.isDestroyed() && win.isMaximized())
|
||||
})
|
||||
ipcMain.handle('window:is-fullscreen', (event) => {
|
||||
const win = BrowserWindow.fromWebContents(event.sender)
|
||||
return Boolean(win && !win.isDestroyed() && win.isFullScreen())
|
||||
})
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
'use strict'
|
||||
|
||||
const { contextBridge, ipcRenderer } = require('electron')
|
||||
|
||||
// 大屏窗口的 F11 走主进程的 win.setFullScreen(Electron 窗口级全屏),页面里的
|
||||
// document.fullscreenElement 永远为 null,自绘标题栏便不知道该让位。这里只放行
|
||||
// 「查一次当前状态」和「订阅状态变化」两条窄通道,不把 ipcRenderer 交给页面。
|
||||
contextBridge.exposeInMainWorld('displayShell', {
|
||||
isFullscreen: () => ipcRenderer.invoke('window:is-fullscreen'),
|
||||
onFullscreenChange: (callback) => {
|
||||
const handler = (_event, fullscreen) => callback(fullscreen === true)
|
||||
ipcRenderer.on('window:fullscreen-changed', handler)
|
||||
return () => ipcRenderer.removeListener('window:fullscreen-changed', handler)
|
||||
}
|
||||
})
|
||||
Reference in new issue
Block a user