2026-08-04 14:57:40 +08:00
|
|
|
// Package webui 渲染采购服务当前可用的服务端页面。
|
|
|
|
|
package webui
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"embed"
|
2026-08-04 19:42:35 +08:00
|
|
|
"fmt"
|
2026-08-04 14:57:40 +08:00
|
|
|
"html/template"
|
|
|
|
|
"io"
|
2026-08-04 18:24:39 +08:00
|
|
|
"time"
|
2026-08-04 16:33:22 +08:00
|
|
|
|
2026-08-04 19:42:35 +08:00
|
|
|
"cmbuyer/admin/internal/taskdetail"
|
2026-08-04 16:33:22 +08:00
|
|
|
"cmbuyer/admin/internal/tasks"
|
2026-08-04 14:57:40 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
//go:embed templates/*.html
|
|
|
|
|
var templateFiles embed.FS
|
|
|
|
|
|
2026-08-04 18:24:39 +08:00
|
|
|
//go:embed static/tasks.js
|
|
|
|
|
var tasksScript []byte
|
|
|
|
|
|
|
|
|
|
var shanghaiLocation = time.FixedZone("Asia/Shanghai", 8*60*60)
|
|
|
|
|
|
|
|
|
|
var templates = template.Must(template.New("webui").Funcs(template.FuncMap{
|
2026-08-04 19:42:35 +08:00
|
|
|
"list": func(values ...any) []any { return values },
|
|
|
|
|
"statusLabel": statusLabel,
|
|
|
|
|
"shanghaiDateTime": func(value time.Time) string { return value.In(shanghaiLocation).Format(time.RFC3339) },
|
|
|
|
|
"shanghaiTime": func(value time.Time) string { return value.In(shanghaiLocation).Format("2006-01-02 15:04") },
|
|
|
|
|
"canonicalURL": tasks.CanonicalURL,
|
|
|
|
|
"formatBytes": formatBytes,
|
|
|
|
|
"taskSafetyTitle": taskSafetyTitle,
|
|
|
|
|
"taskSafetyText": taskSafetyText,
|
|
|
|
|
"authorizationStatusLabel": authorizationStatusLabel,
|
|
|
|
|
"attemptStatusLabel": attemptStatusLabel,
|
|
|
|
|
"submissionStatusLabel": submissionStatusLabel,
|
|
|
|
|
"evidenceKindLabel": evidenceKindLabel,
|
2026-08-04 18:24:39 +08:00
|
|
|
}).ParseFS(templateFiles, "templates/*.html"))
|
2026-08-04 14:57:40 +08:00
|
|
|
|
|
|
|
|
// LoginData 是登录页面所需的非敏感展示数据。
|
|
|
|
|
type LoginData struct {
|
|
|
|
|
CSRFToken string
|
|
|
|
|
ReturnTo string
|
|
|
|
|
Username string
|
|
|
|
|
Error string
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-04 18:24:39 +08:00
|
|
|
// TasksData 是受保护的建单与任务工作台页面所需数据。
|
2026-08-04 14:57:40 +08:00
|
|
|
type TasksData struct {
|
2026-08-04 18:24:39 +08:00
|
|
|
CSRFToken string
|
|
|
|
|
Tasks []tasks.TaskRow
|
|
|
|
|
Filter tasks.TaskFilter
|
|
|
|
|
FilterErrors tasks.Errors
|
|
|
|
|
HasFilter bool
|
|
|
|
|
StartKey string
|
|
|
|
|
Form tasks.Form
|
|
|
|
|
Errors tasks.Errors
|
|
|
|
|
OpenForm bool
|
|
|
|
|
FullPage bool
|
|
|
|
|
FocusField string
|
|
|
|
|
Success bool
|
2026-08-04 14:57:40 +08:00
|
|
|
}
|
|
|
|
|
|
2026-08-04 19:42:35 +08:00
|
|
|
type TaskDetailData struct{ Detail taskdetail.Detail }
|
|
|
|
|
|
2026-08-04 14:57:40 +08:00
|
|
|
// RenderLogin 写入登录页。
|
|
|
|
|
func RenderLogin(writer io.Writer, data LoginData) error {
|
|
|
|
|
return templates.ExecuteTemplate(writer, "login.html", data)
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-04 16:33:22 +08:00
|
|
|
// RenderTasks 写入登录后的受保护任务页。
|
2026-08-04 14:57:40 +08:00
|
|
|
func RenderTasks(writer io.Writer, data TasksData) error {
|
|
|
|
|
return templates.ExecuteTemplate(writer, "tasks.html", data)
|
|
|
|
|
}
|
2026-08-04 18:24:39 +08:00
|
|
|
|
2026-08-04 19:42:35 +08:00
|
|
|
func RenderTaskDetailPage(writer io.Writer, data TaskDetailData) error {
|
|
|
|
|
return templates.ExecuteTemplate(writer, "task-detail-page.html", data)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func RenderTaskDetailFragment(writer io.Writer, data TaskDetailData) error {
|
|
|
|
|
return templates.ExecuteTemplate(writer, "task-detail-content", data)
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-04 18:24:39 +08:00
|
|
|
func TasksScript() []byte { return tasksScript }
|
|
|
|
|
|
|
|
|
|
func statusLabel(status string) string {
|
|
|
|
|
labels := map[string]string{
|
|
|
|
|
"DRAFT": "待开始",
|
|
|
|
|
"PENDING": "已授权待领取",
|
|
|
|
|
"CLAIMED": "已领取",
|
|
|
|
|
"ORDERING": "执行中",
|
|
|
|
|
"NEEDS_MANUAL": "待人工处理",
|
|
|
|
|
"WAITING_PAYMENT": "待付款",
|
|
|
|
|
"RECONCILIATION_REQUIRED": "围栏后待调和",
|
|
|
|
|
"SUCCEEDED": "已完成",
|
|
|
|
|
"FAILED": "失败",
|
|
|
|
|
"CANCELED": "已取消",
|
|
|
|
|
}
|
|
|
|
|
if label, ok := labels[status]; ok {
|
|
|
|
|
return label
|
|
|
|
|
}
|
|
|
|
|
return "未知状态"
|
|
|
|
|
}
|
2026-08-04 19:42:35 +08:00
|
|
|
|
|
|
|
|
func taskSafetyTitle(status string) string {
|
|
|
|
|
if status == "WAITING_PAYMENT" {
|
|
|
|
|
return "订单已创建,系统尚未付款。"
|
|
|
|
|
}
|
|
|
|
|
if status == "RECONCILIATION_REQUIRED" {
|
|
|
|
|
return "订单可能已创建,只能调和同一提交。"
|
|
|
|
|
}
|
|
|
|
|
return "系统只创建待付款订单,不会自动付款。"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func taskSafetyText(status string) string {
|
|
|
|
|
if status == "DRAFT" {
|
|
|
|
|
return "创建任务不构成授权;请回到列表勾选后开始采购。"
|
|
|
|
|
}
|
|
|
|
|
if status == "RECONCILIATION_REQUIRED" {
|
|
|
|
|
return "围栏保持占用,禁止重新授权、再次提交或释放。"
|
|
|
|
|
}
|
|
|
|
|
return "截图只供内部审计,不替代实时价格闸门,也不会触发设备动作。"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func authorizationStatusLabel(status string) string {
|
|
|
|
|
labels := map[string]string{"ACTIVE": "授权有效", "CLAIMED": "已被领取", "FENCED": "提交围栏已建立", "CONSUMED": "授权已消费", "EXPIRED": "授权已过期", "ABANDONED": "授权已关闭"}
|
|
|
|
|
if value, ok := labels[status]; ok {
|
|
|
|
|
return value
|
|
|
|
|
}
|
|
|
|
|
return "未知授权状态"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func attemptStatusLabel(status string) string {
|
|
|
|
|
labels := map[string]string{"CLAIMED": "已领取", "ORDERING": "执行中", "FAILED": "围栏前失败", "FENCED": "已建立围栏", "ABANDONED": "已安全停止"}
|
|
|
|
|
if value, ok := labels[status]; ok {
|
|
|
|
|
return value
|
|
|
|
|
}
|
|
|
|
|
return "未知执行状态"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func submissionStatusLabel(status string) string {
|
|
|
|
|
labels := map[string]string{"FENCED": "围栏已建立", "SUBMITTED": "已创建待付款订单", "RECONCILIATION_REQUIRED": "结果待调和", "MANUAL_RESOLVED": "已人工调和"}
|
|
|
|
|
if value, ok := labels[status]; ok {
|
|
|
|
|
return value
|
|
|
|
|
}
|
|
|
|
|
return "未知提交状态"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func evidenceKindLabel(kind string) string {
|
|
|
|
|
if kind == "SKU_PANEL_GATE_1" {
|
|
|
|
|
return "规格面板 · 闸门一"
|
|
|
|
|
}
|
|
|
|
|
return "内部截图"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func formatBytes(value int64) string {
|
|
|
|
|
if value >= 1<<20 {
|
|
|
|
|
return fmt.Sprintf("%.1f MiB", float64(value)/(1<<20))
|
|
|
|
|
}
|
|
|
|
|
if value >= 1<<10 {
|
|
|
|
|
return fmt.Sprintf("%.1f KiB", float64(value)/(1<<10))
|
|
|
|
|
}
|
|
|
|
|
return fmt.Sprintf("%d B", value)
|
|
|
|
|
}
|