- M3 认证/权限/审计/限流 + 全部 REST 接口 - M4 管理面板(MIUIx + 成绩录入/积分榜下钻/大屏控制) - M5a 大屏页面(六页 + 明细覆盖层 + 缩放适配) - M6 WebSocket 实时同步中心 - M7 xlsx 导出 + 赛事包 zip + 备份 - 规则包 YAML 导入导出(差异预览) - 字体 vendor 子集化(4 套 x2 字重) - 集成:realtime/export/rules 接入 main+httpapi;backup 子命令 - 修复:中间件包装 ResponseWriter 丢失 Hijacker,导致 WS 握手 501
153 lines
3.6 KiB
Go
153 lines
3.6 KiB
Go
package export
|
|
|
|
import (
|
|
"database/sql"
|
|
"io"
|
|
"strings"
|
|
|
|
"github.com/xuri/excelize/v2"
|
|
)
|
|
|
|
// EventResultRow 是分项成绩表的一行。
|
|
// Rank 用字符串是为了承载「1(并列)」这种展示值。
|
|
type EventResultRow struct {
|
|
Rank string
|
|
Bib string
|
|
TeamName string
|
|
Participant string
|
|
ResultText string
|
|
Unit string
|
|
Points float64
|
|
Status string
|
|
Remark string
|
|
}
|
|
|
|
// EventSheet 是一个小项的一个组别(无组别时 Category 为空)对应的一张表。
|
|
type EventSheet struct {
|
|
SportName string
|
|
EventName string
|
|
Category string
|
|
Rows []EventResultRow
|
|
}
|
|
|
|
// EventsWorkbook 是「分项成绩表」的内容:一个小项可含多个组别 sheet,
|
|
// 也可承载全部小项(ExportAllXLSX 用)。
|
|
type EventsWorkbook struct {
|
|
Meta Meta
|
|
Sheets []EventSheet
|
|
}
|
|
|
|
var eventColumns = []column{
|
|
{"名次", 8, kindCenter},
|
|
{"号码", 9, kindCenter},
|
|
{"单位", 22, kindText},
|
|
{"运动员/队", 18, kindText},
|
|
{"成绩", 12, kindCenter},
|
|
{"积分", 8, kindNumber},
|
|
{"状态", 8, kindCenter},
|
|
{"备注", 20, kindText},
|
|
}
|
|
|
|
// ExportEventXLSX 导出单个小项的成绩表;eventID 为 0 时导出全部小项。
|
|
func ExportEventXLSX(db *sql.DB, eventID int64, dst string, opts Options) error {
|
|
wb, err := LoadEventWorkbook(db, eventID, opts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
f, err := eventFile(wb)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return saveWorkbook(f, dst)
|
|
}
|
|
|
|
// WriteEventXLSX 把分项成绩表写成 xlsx 流。
|
|
func WriteEventXLSX(w io.Writer, wb EventsWorkbook) error {
|
|
f, err := eventFile(wb)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return f.Write(w)
|
|
}
|
|
|
|
func eventFile(wb EventsWorkbook) (*excelize.File, error) {
|
|
return buildWorkbook("成绩表", func(f *excelize.File, st *styleSet) error {
|
|
return writeEventSheets(f, st, wb.Meta, wb.Sheets, &sheetNamer{})
|
|
})
|
|
}
|
|
|
|
func writeEventSheets(f *excelize.File, st *styleSet, meta Meta, sheets []EventSheet, namer *sheetNamer) error {
|
|
meta = meta.normalized()
|
|
for _, sh := range sheets {
|
|
name := namer.add(eventSheetBase(sh))
|
|
if err := addSheet(f, name); err != nil {
|
|
return err
|
|
}
|
|
if err := writeSheet(f, st, name, meta, eventSheetTitle(meta, sh), eventColumns, eventRows(sh.Rows)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// eventSheetBase 是工作表名:大项·小项·组别,够短且能区分。
|
|
func eventSheetBase(sh EventSheet) string {
|
|
parts := make([]string, 0, 3)
|
|
if sh.SportName != "" {
|
|
parts = append(parts, sh.SportName)
|
|
}
|
|
parts = append(parts, sh.EventName)
|
|
if sh.Category != "" {
|
|
parts = append(parts, sh.Category)
|
|
}
|
|
return strings.Join(parts, "·")
|
|
}
|
|
|
|
// eventSheetTitle 是表内大标题,带赛事名。
|
|
func eventSheetTitle(meta Meta, sh EventSheet) string {
|
|
base := eventSheetBase(sh)
|
|
if base == "" {
|
|
base = "成绩表"
|
|
}
|
|
return sheetTitle(meta, base)
|
|
}
|
|
|
|
func eventRows(rows []EventResultRow) [][]any {
|
|
out := make([][]any, 0, len(rows))
|
|
for _, r := range rows {
|
|
out = append(out, []any{
|
|
r.Rank, r.Bib, r.TeamName, r.Participant,
|
|
displayResult(r.ResultText, r.Unit), r.Points, r.Status, r.Remark,
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// displayResult 把成绩与单位拼成「11.58 秒」;单位已含在文本里时不重复拼接。
|
|
func displayResult(text, unit string) string {
|
|
text = strings.TrimSpace(text)
|
|
unit = strings.TrimSpace(unit)
|
|
if text == "" {
|
|
return ""
|
|
}
|
|
if unit == "" || strings.HasSuffix(text, unit) {
|
|
return text
|
|
}
|
|
return text + " " + unitLabel(unit)
|
|
}
|
|
|
|
func unitLabel(unit string) string {
|
|
switch unit {
|
|
case "s":
|
|
return "秒"
|
|
case "m":
|
|
return "米"
|
|
case "pts":
|
|
return "分"
|
|
case "次":
|
|
return "次"
|
|
default:
|
|
return unit
|
|
}
|
|
}
|