diff --git a/cmd/jintai-visitor-proxy/main.go b/cmd/jintai-visitor-proxy/main.go
index 760938e..1aafe4d 100644
--- a/cmd/jintai-visitor-proxy/main.go
+++ b/cmd/jintai-visitor-proxy/main.go
@@ -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(`
返回内容不是 JSON,已显示在原始响应里。
`)
}
+ 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(``)
- b.WriteString(renderAny(value, "查询结果"))
+ if message != "" {
+ b.WriteString(`
` + html.EscapeString(message) + `
`)
+ }
+
+ records := collectRecords(result)
+ if len(records) > 0 {
+ for i, record := range records {
+ b.WriteString(renderVisitorCard(record, i+1, len(records)))
+ }
+ b.WriteString(`
详细字段
`)
+ b.WriteString(renderAny(result, "访客记录"))
+ b.WriteString(``)
+ } else {
+ b.WriteString(renderAny(result, "访客记录"))
+ }
b.WriteString(`
`)
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(``)
+ b.WriteString(`访客证`)
+ if total > 1 {
+ b.WriteString(`` + html.EscapeString(fmt.Sprintf("%d / %d", index, total)) + ``)
+ }
+ b.WriteString(`
`)
+ b.WriteString(`尊敬的` + html.EscapeString(name) + `` + html.EscapeString(title) + `
`)
+ if qrSrc != "" {
+ b.WriteString(``)
+ } else {
+ msg := "记录里没有可生成二维码的备注字段。"
+ if qrErr != nil {
+ msg = qrErr.Error()
+ }
+ b.WriteString(`` + html.EscapeString(msg) + `
`)
+ }
+ b.WriteString(`进园区时,请出示此二维码
`)
+ b.WriteString(`` + html.EscapeString(firstNonEmpty(company, "-")) + `
`)
+ b.WriteString(``)
+ b.WriteString(renderMetaRow("开始时间", start))
+ b.WriteString(renderMetaRow("结束时间", end))
+ b.WriteString(renderMetaRow("来访事由", reason))
+ b.WriteString(renderMetaRow("接待人", host))
+ b.WriteString(`
`)
+ return b.String()
+}
+
+func renderMetaRow(label, value string) string {
+ if strings.TrimSpace(value) == "" {
+ return ""
+ }
+ return `` + html.EscapeString(label) + `` + html.EscapeString(value) + `
`
+}
+
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(`
.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(`
}
.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(`
.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;
+ }
}
@@ -669,7 +907,7 @@ var pageTemplate = template.Must(template.New("page").Parse(`
{{.ResultHTML}}
- {{if .RawJSON}}{{.RawJSON}}{{end}}
+ {{if .RawJSON}}原始响应
{{.RawJSON}} {{end}}