Render visitor QR code on proxy page

main
wangqiyang 3 weeks ago
parent 9e431f2210
commit 280ca87082

@ -22,6 +22,8 @@ import (
"strings"
"sync"
"time"
qrcode "github.com/skip2/go-qrcode"
)
const (
@ -328,13 +330,118 @@ func renderResultHTML(raw []byte) template.HTML {
return template.HTML(`<div class="empty">返回内容不是 JSON已显示在原始响应里。</div>`)
}
result := value
message := ""
if obj, ok := value.(map[string]any); ok {
if msg, _ := obj["message"].(string); msg != "" {
message = msg
}
if v, ok := obj["result"]; ok {
result = v
}
}
var b strings.Builder
b.WriteString(`<div class="result-block">`)
b.WriteString(renderAny(value, "查询结果"))
if message != "" {
b.WriteString(`<div class="api-message">` + html.EscapeString(message) + `</div>`)
}
records := collectRecords(result)
if len(records) > 0 {
for i, record := range records {
b.WriteString(renderVisitorCard(record, i+1, len(records)))
}
b.WriteString(`<details class="field-details"><summary>详细字段</summary>`)
b.WriteString(renderAny(result, "访客记录"))
b.WriteString(`</details>`)
} else {
b.WriteString(renderAny(result, "访客记录"))
}
b.WriteString(`</div>`)
return template.HTML(b.String())
}
func collectRecords(v any) []map[string]any {
switch x := v.(type) {
case []any:
records := make([]map[string]any, 0, len(x))
for _, item := range x {
if record, ok := item.(map[string]any); ok {
records = append(records, record)
}
}
return records
case map[string]any:
if len(x) == 0 {
return nil
}
return []map[string]any{x}
case string:
var nested any
if json.Unmarshal([]byte(x), &nested) == nil {
return collectRecords(nested)
}
}
return nil
}
func renderVisitorCard(obj map[string]any, index, total int) string {
name := firstField(obj, "姓名", "访客姓名")
if name == "" {
name = "访客"
}
gender := firstField(obj, "性别", "性别_dict", "性别_dictText")
title := "访客"
if gender == "男" {
title = "先生"
} else if gender == "女" {
title = "女士"
}
company := firstField(obj, "访客公司", "公司", "单位")
start := firstField(obj, "开始时间", "来访时间")
end := firstField(obj, "结束时间", "离开时间")
reason := firstField(obj, "来访事由_dictText", "来访事由_dict", "来访事由")
host := firstField(obj, "接待人名称", "接待人", "person")
qrText := firstField(obj, "备注", "二维码", "通行码", "访客码", "QRCode", "qrCode", "qr")
qrSrc, qrErr := qrImageDataURL(qrText)
var b strings.Builder
b.WriteString(`<article class="visitor-card">`)
b.WriteString(`<div class="card-head"><span>访客证</span>`)
if total > 1 {
b.WriteString(`<strong>` + html.EscapeString(fmt.Sprintf("%d / %d", index, total)) + `</strong>`)
}
b.WriteString(`</div>`)
b.WriteString(`<div class="welcome"><span>尊敬的</span><b>` + html.EscapeString(name) + `</b><span>` + html.EscapeString(title) + `</span></div>`)
if qrSrc != "" {
b.WriteString(`<div class="qr-wrap"><img class="qr-code" src="` + html.EscapeString(qrSrc) + `" alt="访客二维码"></div>`)
} else {
msg := "记录里没有可生成二维码的备注字段。"
if qrErr != nil {
msg = qrErr.Error()
}
b.WriteString(`<div class="qr-missing">` + html.EscapeString(msg) + `</div>`)
}
b.WriteString(`<div class="qr-tip">进园区时,请出示此二维码</div>`)
b.WriteString(`<div class="company">` + html.EscapeString(firstNonEmpty(company, "-")) + `</div>`)
b.WriteString(`<div class="card-meta">`)
b.WriteString(renderMetaRow("开始时间", start))
b.WriteString(renderMetaRow("结束时间", end))
b.WriteString(renderMetaRow("来访事由", reason))
b.WriteString(renderMetaRow("接待人", host))
b.WriteString(`</div></article>`)
return b.String()
}
func renderMetaRow(label, value string) string {
if strings.TrimSpace(value) == "" {
return ""
}
return `<div class="meta-row"><span>` + html.EscapeString(label) + `</span><strong>` + html.EscapeString(value) + `</strong></div>`
}
func renderAny(v any, title string) string {
switch x := v.(type) {
case map[string]any:
@ -391,6 +498,31 @@ func renderObject(obj map[string]any, title string) string {
return b.String()
}
func firstField(obj map[string]any, keys ...string) string {
for _, key := range keys {
if v, ok := obj[key]; ok {
if s := strings.TrimSpace(stringify(v)); s != "" {
return s
}
}
}
return ""
}
func qrImageDataURL(text string) (string, error) {
text = strings.TrimSpace(text)
if text == "" {
return "", errors.New("记录里没有可生成二维码的备注字段。")
}
png, err := qrcode.Encode(text, qrcode.Medium, 220)
if err != nil {
return "", fmt.Errorf("二维码生成失败:%w", err)
}
return "data:image/png;base64," + base64.StdEncoding.EncodeToString(png), nil
}
func stringify(v any) string {
switch x := v.(type) {
case nil:
@ -568,6 +700,103 @@ var pageTemplate = template.Must(template.New("page").Parse(`<!doctype html>
.status.err { color: var(--err); }
.status.muted { color: var(--muted); }
.result-block { margin-top: 18px; }
.visitor-card {
max-width: 390px;
margin: 0 auto 14px;
background: #fff;
border: 1px solid #dcc698;
border-radius: 8px;
box-shadow: 0 18px 50px rgba(85, 61, 23, .14);
overflow: hidden;
}
.card-head {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 48px;
padding: 0 18px;
color: #fff;
background: #8f6f34;
font-size: 18px;
font-weight: 700;
}
.card-head strong { font-size: 13px; font-weight: 600; }
.welcome {
display: flex;
align-items: baseline;
justify-content: center;
gap: 8px;
padding: 20px 18px 12px;
font-size: 16px;
}
.welcome b { font-size: 22px; }
.qr-wrap {
display: flex;
justify-content: center;
padding: 8px 0 10px;
background: #fff;
}
.qr-code {
width: 220px;
height: 220px;
display: block;
image-rendering: pixelated;
}
.qr-missing {
margin: 12px 18px;
padding: 18px;
border: 1px dashed var(--line);
border-radius: 8px;
color: var(--err);
text-align: center;
line-height: 1.6;
background: #fff7f7;
}
.qr-tip {
padding: 0 18px 14px;
color: #8f6f34;
text-align: center;
font-size: 14px;
}
.company {
padding: 14px 18px;
border-top: 1px solid #efe2c8;
border-bottom: 1px solid #efe2c8;
text-align: center;
font-size: 15px;
line-height: 1.6;
word-break: break-word;
}
.card-meta { padding: 12px 18px 16px; }
.meta-row {
display: flex;
justify-content: space-between;
gap: 14px;
padding: 6px 0;
color: var(--muted);
font-size: 14px;
line-height: 1.5;
}
.meta-row strong {
color: var(--text);
font-weight: 500;
text-align: right;
word-break: break-word;
}
.field-details, .raw-json {
margin-top: 12px;
background: #fff;
border: 1px solid var(--line);
border-radius: 8px;
overflow: hidden;
}
.field-details summary, .raw-json summary {
cursor: pointer;
padding: 12px 14px;
color: var(--primary);
font-size: 14px;
user-select: none;
}
.api-message, .empty, .single-value {
padding: 14px 16px;
color: var(--muted);
@ -606,7 +835,7 @@ var pageTemplate = template.Must(template.New("page").Parse(`<!doctype html>
}
.image-text { color: var(--muted); font-size: 12px; }
pre {
margin: 18px 0 0;
margin: 0;
max-height: 360px;
overflow: auto;
white-space: pre-wrap;
@ -622,6 +851,15 @@ var pageTemplate = template.Must(template.New("page").Parse(`<!doctype html>
.captcha-grid, .actions, .fields { grid-template-columns: 1fr; }
.field-key { border-bottom: 0; padding-bottom: 4px; }
.field-value { padding-top: 4px; }
.visitor-card { max-width: 100%; }
.meta-row {
display: block;
}
.meta-row strong {
display: block;
margin-top: 2px;
text-align: left;
}
}
</style>
</head>
@ -669,7 +907,7 @@ var pageTemplate = template.Must(template.New("page").Parse(`<!doctype html>
{{.ResultHTML}}
{{if .RawJSON}}<pre>{{.RawJSON}}</pre>{{end}}
{{if .RawJSON}}<details class="raw-json"><summary></summary><pre>{{.RawJSON}}</pre></details>{{end}}
</main>
</body>
</html>`))

@ -58,6 +58,7 @@ require (
github.com/olekukonko/ll v0.0.9 // indirect
github.com/olekukonko/tablewriter v1.1.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
github.com/syndtr/goleveldb v1.0.0 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect

@ -241,6 +241,8 @@ github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1 h1:NVK+OqnavpyFmUiKfUMHrpvbCi2VFoWTrcpI7aDaJ2I=
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=

Loading…
Cancel
Save