|
|
|
@ -22,6 +22,8 @@ import (
|
|
|
|
"strings"
|
|
|
|
"strings"
|
|
|
|
"sync"
|
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
qrcode "github.com/skip2/go-qrcode"
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
const (
|
|
|
|
@ -328,13 +330,118 @@ func renderResultHTML(raw []byte) template.HTML {
|
|
|
|
return template.HTML(`<div class="empty">返回内容不是 JSON,已显示在原始响应里。</div>`)
|
|
|
|
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
|
|
|
|
var b strings.Builder
|
|
|
|
b.WriteString(`<div class="result-block">`)
|
|
|
|
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>`)
|
|
|
|
b.WriteString(`</div>`)
|
|
|
|
return template.HTML(b.String())
|
|
|
|
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 {
|
|
|
|
func renderAny(v any, title string) string {
|
|
|
|
switch x := v.(type) {
|
|
|
|
switch x := v.(type) {
|
|
|
|
case map[string]any:
|
|
|
|
case map[string]any:
|
|
|
|
@ -391,6 +498,31 @@ func renderObject(obj map[string]any, title string) string {
|
|
|
|
return b.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 {
|
|
|
|
func stringify(v any) string {
|
|
|
|
switch x := v.(type) {
|
|
|
|
switch x := v.(type) {
|
|
|
|
case nil:
|
|
|
|
case nil:
|
|
|
|
@ -568,6 +700,103 @@ var pageTemplate = template.Must(template.New("page").Parse(`<!doctype html>
|
|
|
|
.status.err { color: var(--err); }
|
|
|
|
.status.err { color: var(--err); }
|
|
|
|
.status.muted { color: var(--muted); }
|
|
|
|
.status.muted { color: var(--muted); }
|
|
|
|
.result-block { margin-top: 18px; }
|
|
|
|
.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 {
|
|
|
|
.api-message, .empty, .single-value {
|
|
|
|
padding: 14px 16px;
|
|
|
|
padding: 14px 16px;
|
|
|
|
color: var(--muted);
|
|
|
|
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; }
|
|
|
|
.image-text { color: var(--muted); font-size: 12px; }
|
|
|
|
pre {
|
|
|
|
pre {
|
|
|
|
margin: 18px 0 0;
|
|
|
|
margin: 0;
|
|
|
|
max-height: 360px;
|
|
|
|
max-height: 360px;
|
|
|
|
overflow: auto;
|
|
|
|
overflow: auto;
|
|
|
|
white-space: pre-wrap;
|
|
|
|
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; }
|
|
|
|
.captcha-grid, .actions, .fields { grid-template-columns: 1fr; }
|
|
|
|
.field-key { border-bottom: 0; padding-bottom: 4px; }
|
|
|
|
.field-key { border-bottom: 0; padding-bottom: 4px; }
|
|
|
|
.field-value { padding-top: 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>
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
</head>
|
|
|
|
@ -669,7 +907,7 @@ var pageTemplate = template.Must(template.New("page").Parse(`<!doctype html>
|
|
|
|
|
|
|
|
|
|
|
|
{{.ResultHTML}}
|
|
|
|
{{.ResultHTML}}
|
|
|
|
|
|
|
|
|
|
|
|
{{if .RawJSON}}<pre>{{.RawJSON}}</pre>{{end}}
|
|
|
|
{{if .RawJSON}}<details class="raw-json"><summary>原始响应</summary><pre>{{.RawJSON}}</pre></details>{{end}}
|
|
|
|
</main>
|
|
|
|
</main>
|
|
|
|
</body>
|
|
|
|
</body>
|
|
|
|
</html>`))
|
|
|
|
</html>`))
|
|
|
|
|