feat(v1.9.0): 引入 Bootstrap 5(第 1/4 轮:框架与大屏头部)

本次开始把界面全量迁移到 Bootstrap 5 默认风格,分 4 轮:
- 第 1 轮(本次):Bootstrap 资源本地化 + 后台框架(导航/搜索/徽标/用户菜单/提示) + 大屏头部与面板容器
- 第 2 轮:登录/初始化页 + 客户/订阅/套餐/日志/设置页的表单与列表
- 第 3 轮:日历抽屉、快速完成、追加面板、客户月历、弹窗/下拉
- 第 4 轮:大屏内部(KPI/热力/TOP/今日列表/浮层)与收尾打磨

本轮改动(纯前端,无业务逻辑与数据库变更):
- 新增 internal/web/static/vendor/bootstrap.min.css、bootstrap.bundle.min.js(本地内嵌,离线可用)
- _layout.html 重写为 Bootstrap navbar + container-fluid:导航、搜索框、到期徽标、主题开关、用户下拉菜单、alert 提示
- 大屏布局引入 Bootstrap,html 使用 data-bs-theme 跟随亮/暗主题;头部改为 navbar,面板改为 card
- 版本号 1.8.2 → 1.9.0

验证:
- 无头 Chrome 实拍日历、大屏、登录三页(shots/bs1-*.png),Bootstrap 深色主题生效,导航与徽标样式正常
- 业务逻辑与数据库结构未改动
This commit is contained in:
subtrack
2026-10-19 22:14:19 +08:00
parent bb923aab4d
commit d955d0d7d2
6 changed files with 61 additions and 41 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+39 -34
View File
@@ -1,49 +1,54 @@
{{define "layout"}}<!doctype html> {{define "layout"}}<!doctype html>
<html lang="zh-CN"{{if .Theme}} data-theme="{{.Theme}}"{{end}}> <html lang="zh-CN" data-bs-theme="{{if eq .Theme "dark"}}dark{{else}}light{{end}}">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{.Title}} · 代打台</title> <title>{{.Title}} · 代打台</title>
<link rel="icon" href="/static/favicon.svg" type="image/svg+xml"> <link rel="icon" href="/static/favicon.svg" type="image/svg+xml">
<link rel="stylesheet" href="/static/vendor/bootstrap.min.css?v={{.Version}}">
<link rel="stylesheet" href="/static/app.css?v={{.Version}}"> <link rel="stylesheet" href="/static/app.css?v={{.Version}}">
</head> </head>
<body> <body class="bg-body-tertiary">
{{if .User}} {{if .User}}
<header class="topbar"> <nav class="navbar navbar-expand-lg bg-body border-bottom sticky-top">
<a class="brand" href="/calendar"><span class="brand-mark">代</span><span class="brand-name">代打台</span></a> <div class="container-fluid">
<form class="search" action="/customers" method="get" role="search"> <a class="navbar-brand fw-bold text-success" href="/calendar">代打台</a>
<input type="search" name="q" value="{{.Query}}" placeholder="手机尾号 / 姓名 / 游戏名" aria-label="搜索客户" autocomplete="off" data-search> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mainNav"><span class="navbar-toggler-icon"></span></button>
<kbd>/</kbd> <div class="collapse navbar-collapse" id="mainNav">
</form> <ul class="navbar-nav me-auto">
<nav class="nav"> <li class="nav-item"><a class="nav-link{{if eq .Nav "calendar"}} active{{end}}" href="/calendar">日历</a></li>
<a href="/calendar" class="{{if eq .Nav "calendar"}}on{{end}}">日历</a> <li class="nav-item"><a class="nav-link{{if eq .Nav "customers"}} active{{end}}" href="/customers">客户</a></li>
<a href="/customers" class="{{if eq .Nav "customers"}}on{{end}}">客户</a> <li class="nav-item"><a class="nav-link{{if eq .Nav "subscriptions"}} active{{end}}" href="/subscriptions">订阅</a></li>
<a href="/subscriptions" class="{{if eq .Nav "subscriptions"}}on{{end}}">订阅</a> <li class="nav-item"><a class="nav-link{{if eq .Nav "packages"}} active{{end}}" href="/packages">套餐</a></li>
<a href="/packages" class="{{if eq .Nav "packages"}}on{{end}}">套餐</a> <li class="nav-item"><a class="nav-link" href="/screen" target="_blank" rel="noopener">大屏 ↗</a></li>
<a href="/screen" target="_blank" rel="noopener" class="nav-screen">大屏 ↗</a> </ul>
</nav> <form class="d-flex me-2" role="search" action="/customers" method="get">
<div class="topbar-right"> <input class="form-control form-control-sm" type="search" name="q" value="{{.Query}}" placeholder="手机尾号 / 姓名 / 游戏名" aria-label="搜索客户" data-search>
<a class="pill pill-due" href="/subscriptions?state=due_today" title="今天到期的订阅">今日到期 <b>{{.DueToday}}</b></a> </form>
<a class="pill pill-warn" href="/subscriptions?state=due3" title="3 天内到期">3 天内 <b>{{.Due3}}</b></a> <div class="d-flex align-items-center gap-2">
<a class="pill" href="/subscriptions?state=due7" title="7 天内到期">7 天内 <b>{{.Due7}}</b></a> <a class="badge text-bg-danger text-decoration-none" href="/subscriptions?state=due_today">今日到期 {{.DueToday}}</a>
<button class="theme-toggle" type="button" data-theme-toggle aria-label="切换亮色/暗色" title="切换亮色/暗色"> <a class="badge text-bg-warning text-decoration-none" href="/subscriptions?state=due3">3 天内 {{.Due3}}</a>
<span class="ic-moon" aria-hidden="true">☾</span><span class="ic-sun" aria-hidden="true">☀</span> <a class="badge text-bg-secondary text-decoration-none" href="/subscriptions?state=due7">7 天内 {{.Due7}}</a>
</button> <button class="btn btn-sm btn-outline-secondary" type="button" data-theme-toggle aria-label="切换亮色/暗色"><span class="ic-moon">☾</span><span class="ic-sun">☀</span></button>
<div class="me"> <div class="dropdown">
<a href="/settings">{{if .User.DisplayName}}{{.User.DisplayName}}{{else}}{{.User.Username}}{{end}}</a> <button class="btn btn-sm btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown">{{if .User.DisplayName}}{{.User.DisplayName}}{{else}}{{.User.Username}}{{end}}</button>
<form method="post" action="/logout"><button type="submit" class="link-btn">退出</button></form> <ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="/settings">设置</a></li>
<li><a class="dropdown-item" href="/logs">操作日志</a></li>
<li><hr class="dropdown-divider"></li>
<li><form method="post" action="/logout"><button class="dropdown-item text-danger" type="submit">退出登录</button></form></li>
</ul>
</div>
</div>
</div> </div>
</div> </div>
</header> </nav>
{{else}}
<button class="theme-toggle auth-theme" type="button" data-theme-toggle aria-label="切换亮色/暗色" title="切换亮色/暗色">
<span class="ic-moon" aria-hidden="true">☾</span><span class="ic-sun" aria-hidden="true">☀</span>
</button>
{{end}} {{end}}
{{if .Flash}}<div class="flash" data-flash>{{.Flash}}</div>{{end}} {{if .Flash}}<div class="alert alert-success alert-dismissible fade show m-3" role="alert" data-flash>{{.Flash}}<button type="button" class="btn-close" data-bs-dismiss="alert"></button></div>{{end}}
{{if .Error}}<div class="flash flash-error">{{.Error}}</div>{{end}} {{if .Error}}<div class="alert alert-danger m-3" role="alert">{{.Error}}</div>{{end}}
<main{{if .User}} class="with-top"{{end}}>{{template "content" .}}</main> <main class="{{if .User}}container-fluid py-3{{end}}">{{template "content" .}}</main>
{{if .User}}<footer class="foot">代打台 v{{.Version}} · <a href="/logs">操作日志</a> · <a href="/settings">设置</a> · <a href="/screen" target="_blank" rel="noopener">大屏</a></footer>{{end}} {{if .User}}<footer class="container-fluid text-secondary small border-top py-3 mt-4">代打台 v{{.Version}} · <a href="/logs">操作日志</a> · <a href="/settings">设置</a> · <a href="/screen" target="_blank" rel="noopener">大屏</a></footer>{{end}}
<script src="/static/vendor/bootstrap.bundle.min.js?v={{.Version}}" defer></script>
<script src="/static/app.js?v={{.Version}}" defer></script> <script src="/static/app.js?v={{.Version}}" defer></script>
</body> </body>
</html>{{end}} </html>{{end}}
+3 -1
View File
@@ -1,15 +1,17 @@
{{define "screenlayout"}}<!doctype html> {{define "screenlayout"}}<!doctype html>
<html lang="zh-CN"{{if .Theme}} data-theme="{{.Theme}}"{{end}}> <html lang="zh-CN" data-bs-theme="{{if eq .Theme "light"}}light{{else}}dark{{end}}"{{if .Theme}} data-theme="{{.Theme}}"{{end}}>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="refresh" content="{{.D.Refresh}}"> <meta http-equiv="refresh" content="{{.D.Refresh}}">
<title>{{.Title}} · 代打台</title> <title>{{.Title}} · 代打台</title>
<link rel="icon" href="/static/favicon.svg" type="image/svg+xml"> <link rel="icon" href="/static/favicon.svg" type="image/svg+xml">
<link rel="stylesheet" href="/static/vendor/bootstrap.min.css?v={{.D.Version}}">
<link rel="stylesheet" href="/static/screen.css?v={{.D.Version}}"> <link rel="stylesheet" href="/static/screen.css?v={{.D.Version}}">
</head> </head>
<body class="screen-body"> <body class="screen-body">
{{template "content" .}} {{template "content" .}}
<script src="/static/vendor/bootstrap.bundle.min.js?v={{.D.Version}}" defer></script>
<script src="/static/screen.js?v={{.D.Version}}" defer></script> <script src="/static/screen.js?v={{.D.Version}}" defer></script>
</body> </body>
</html>{{end}} </html>{{end}}
+5 -5
View File
@@ -1,7 +1,7 @@
{{define "content"}} {{define "content"}}
<div class="scr" data-pkg="{{.D.PkgID}}" data-heat="{{.D.HeatMode}}" data-api="/api/screen/day"> <div class="scr" data-pkg="{{.D.PkgID}}" data-heat="{{.D.HeatMode}}" data-api="/api/screen/day">
<header class="scr-head"> <header class="scr-head navbar bg-body-tertiary border-bottom px-3 py-2">
<div class="scr-brand"><span class="mark">代</span>代打台<em>控制台</em></div> <div class="scr-brand navbar-brand mb-0"><span class="mark">代</span>代打台<em>控制台</em></div>
{{if .D.HasTabs}} {{if .D.HasTabs}}
<nav class="scr-tabs"> <nav class="scr-tabs">
@@ -41,8 +41,8 @@
</section> </section>
<section class="scr-main"> <section class="scr-main">
<div class="panel panel-cal"> <div class="panel panel-cal card bg-body-tertiary border-secondary-subtle">
<div class="panel-head"> <div class="panel-head card-header bg-transparent border-secondary-subtle py-2">
<h2>{{if eq .D.HeatMode "due"}}到期热力{{else}}每日待完成{{end}}</h2> <h2>{{if eq .D.HeatMode "due"}}到期热力{{else}}每日待完成{{end}}</h2>
<span>{{.D.MonthTitle}} · 本月合计 {{.D.HeatTotal}} 单</span> <span>{{.D.MonthTitle}} · 本月合计 {{.D.HeatTotal}} 单</span>
</div> </div>
@@ -69,7 +69,7 @@
</div> </div>
</div> </div>
<div class="panel panel-top"> <div class="panel panel-top card bg-body-tertiary border-secondary-subtle">
<div class="panel-head"><h2>即将到期 TOP 10</h2><span>按剩余量排序</span></div> <div class="panel-head"><h2>即将到期 TOP 10</h2><span>按剩余量排序</span></div>
{{if .D.Top}} {{if .D.Top}}
<ol class="top-list"> <ol class="top-list">
+1 -1
View File
@@ -26,7 +26,7 @@ import (
"daili/internal/web" "daili/internal/web"
) )
const version = "1.8.2" const version = "1.9.0"
func main() { func main() {
cfgFlag := flag.String("config", "", "配置文件路径(默认:exe 同级的 "+config.FileName+")") cfgFlag := flag.String("config", "", "配置文件路径(默认:exe 同级的 "+config.FileName+")")