fix(pay): 测试页对齐官网暖色风格 + 修地址逐字换行布局

- 布局 bug:收款地址原挤在 flex 行里(与复制键争宽)→ word-break 逐字换行。
  改为独立整行 .addr 块(全宽自然换行),复制键下移全宽。
- 风格:改用官网 light 暖色 token(sand/clay,主色 #B96A3D、sage 绿
  #5B8C5A、JetBrains Mono),内联对齐 design/prototype/tokens.css。
- 页脚补一句「测什么」:收款订单闭环 POST /order + GET /order/{id} 轮询到 paid。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-07-09 16:00:25 +08:00
parent 5fd0d3c148
commit e421f03790
+82 -51
View File
@@ -8,6 +8,11 @@ import "net/http"
// money-critical Phase E loop (create order → pay exact amount → watch paid),
// NOT the production checkout (that lives on the storefront). It only creates
// orders and polls status — no secrets, no keys.
//
// Styling mirrors the marketing site's warm sand/clay palette (design tokens in
// design/prototype/tokens.css, light theme default, brand = clay-500 #B96A3D).
// Values are inlined here because pangolin-pay is a separate origin and cannot
// @import the site's stylesheet; keep them in sync if the brand palette changes.
func (h *Handler) testPage(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Header().Set("Cache-Control", "no-store")
@@ -20,55 +25,78 @@ const testPageHTML = `<!doctype html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex,nofollow">
<meta name="theme-color" content="#B96A3D">
<title>pangolin-pay · 收款测试</title>
<style>
:root{
--bg:#0b0f14; --card:#131a22; --card2:#0f151c; --line:#223040;
--fg:#e6edf3; --muted:#8899a6; --accent:#3fb950; --accent2:#58a6ff;
--warn:#d29922; --err:#f85149; --radius:14px;
--bg:#FAF8F4; --surface:#FFFFFF; --surface-2:#F2EEE7;
--fg1:#1F1C18; --fg2:#6B6253; --fg3:#8C8270;
--accent:#B96A3D; --accent-hover:#9E5630; --accent-subtle:#FAF3ED; --accent-border:#E6C7AC;
--border:#E6DFD3; --border-strong:#D2C8B8;
--success:#5B8C5A; --success-subtle:#E9F0E6;
--warning:#D69A3C; --warning-subtle:#F8EED6;
--danger:#C0533B; --danger-subtle:#F6E1DA;
--radius-lg:14px; --radius-md:10px; --radius-sm:8px;
--font-display:'Sora','Noto Sans SC',system-ui,sans-serif;
--font-sans:'Manrope','Noto Sans SC',system-ui,sans-serif;
--font-mono:'JetBrains Mono',ui-monospace,'SFMono-Regular',Menlo,monospace;
--shadow-md:0 4px 14px rgba(45,30,20,.08);
}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--fg);
font:15px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"PingFang SC","Microsoft YaHei",sans-serif;
display:flex;justify-content:center;padding:32px 16px;}
.wrap{width:100%;max-width:520px}
h1{font-size:19px;margin:0 0 4px;font-weight:650}
.sub{color:var(--muted);font-size:13px;margin:0 0 20px}
.card{background:var(--card);border:1px solid var(--line);border-radius:var(--radius);padding:20px;margin-bottom:16px}
label{display:block;font-size:12px;color:var(--muted);margin:12px 0 5px}
input{width:100%;background:var(--card2);border:1px solid var(--line);border-radius:10px;
color:var(--fg);padding:11px 12px;font-size:15px;outline:none;font-family:inherit}
input:focus{border-color:var(--accent2)}
button{width:100%;margin-top:18px;background:var(--accent);color:#04130a;border:0;border-radius:10px;
padding:13px;font-size:15px;font-weight:650;cursor:pointer}
body{margin:0;background:var(--bg);color:var(--fg1);
font-family:var(--font-sans);font-size:15px;line-height:1.55;
-webkit-font-smoothing:antialiased;
display:flex;justify-content:center;padding:40px 16px;}
.wrap{width:100%;max-width:480px}
h1{font-family:var(--font-display);font-size:22px;margin:0 0 5px;font-weight:700;letter-spacing:-.02em}
h1 .dot{color:var(--accent)}
.sub{color:var(--fg2);font-size:13px;margin:0 0 24px}
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);
padding:22px;margin-bottom:16px;box-shadow:var(--shadow-md)}
label{display:block;font-size:12px;color:var(--fg2);margin:14px 0 6px;font-weight:500}
label:first-child{margin-top:0}
input{width:100%;background:var(--bg);border:1px solid var(--border-strong);border-radius:var(--radius-md);
color:var(--fg1);padding:11px 13px;font-size:15px;outline:none;font-family:inherit;transition:border-color .15s}
input:focus{border-color:var(--accent)}
button{width:100%;margin-top:20px;background:var(--accent);color:#fff;border:0;border-radius:var(--radius-md);
padding:13px;font-size:15px;font-weight:600;cursor:pointer;font-family:inherit;transition:background .15s}
button:hover:not(:disabled){background:var(--accent-hover)}
button:disabled{opacity:.5;cursor:not-allowed}
button.ghost{background:transparent;border:1px solid var(--line);color:var(--fg);font-weight:500;margin-top:10px}
.row{display:flex;align-items:center;gap:8px}
.kv{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;
padding:11px 0;border-bottom:1px solid var(--line)}
.kv:last-child{border-bottom:0}
.kv .k{color:var(--muted);font-size:13px;white-space:nowrap}
.kv .v{text-align:right;word-break:break-all;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:13px}
.amt{font-size:26px;font-weight:700;color:var(--accent);font-family:ui-monospace,monospace}
.amt small{font-size:14px;color:var(--muted);font-weight:400;margin-left:4px}
.copy{background:var(--card2);border:1px solid var(--line);color:var(--accent2);
border-radius:7px;padding:3px 9px;font-size:12px;cursor:pointer;flex:none}
.copy:active{background:var(--line)}
.badge{display:inline-block;padding:3px 11px;border-radius:20px;font-size:12px;font-weight:600}
.b-pending{background:#3a2d0a;color:var(--warn)}
.b-paid{background:#0f2a17;color:var(--accent)}
.b-expired{background:#2a1215;color:var(--err)}
.note{font-size:12px;color:var(--muted);margin-top:14px;line-height:1.6}
.err{color:var(--err);font-size:13px;margin-top:12px;min-height:18px}
button.ghost{background:transparent;border:1px solid var(--border-strong);color:var(--fg1);font-weight:500;margin-top:12px}
button.ghost:hover:not(:disabled){background:var(--surface-2)}
button.copy{margin-top:10px;background:var(--accent-subtle);border:1px solid var(--accent-border);
color:var(--accent-hover);font-weight:600;font-size:13px;padding:9px}
button.copy:hover:not(:disabled){background:#F5E7D8}
.kv{display:flex;justify-content:space-between;align-items:center;gap:12px;
padding:12px 0;border-bottom:1px solid var(--border)}
.kv .k{color:var(--fg2);font-size:13px;white-space:nowrap;flex:none}
.kv .v{text-align:right;font-family:var(--font-mono);font-size:13px;word-break:break-all}
.amtbox{text-align:center;padding:20px 0 6px}
.amtbox .cap{color:var(--fg2);font-size:12px;margin-bottom:8px}
.amtbox .cap b{color:var(--warning)}
.amt{font-family:var(--font-mono);font-size:30px;font-weight:700;color:var(--success);letter-spacing:-.01em}
.amt small{font-size:14px;color:var(--fg3);font-weight:400;margin-left:5px;font-family:var(--font-sans)}
.addrblock{margin:6px 0 2px}
.addrblock .k{color:var(--fg2);font-size:12px;margin-bottom:6px}
.addr{font-family:var(--font-mono);font-size:14px;background:var(--surface-2);border:1px solid var(--border);
border-radius:var(--radius-md);padding:12px 14px;word-break:break-all;line-height:1.5;color:var(--fg1)}
.badge{display:inline-block;padding:4px 12px;border-radius:999px;font-size:12px;font-weight:600;font-family:var(--font-mono)}
.b-pending{background:var(--warning-subtle);color:#9A6B1F}
.b-paid{background:var(--success-subtle);color:#3E6B3D}
.b-expired{background:var(--danger-subtle);color:#8F3826}
.note{font-size:12px;color:var(--fg2);margin-top:16px;line-height:1.65}
.note b{color:var(--fg1)}
.err{color:var(--danger);font-size:13px;margin-top:12px;min-height:18px}
.hidden{display:none}
.warnbox{background:#3a2d0a33;border:1px solid #d2992244;border-radius:10px;
padding:11px 13px;font-size:12.5px;color:var(--warn);margin-top:14px;line-height:1.6}
code{background:var(--card2);padding:1px 5px;border-radius:5px;font-size:12px}
.warnbox{background:var(--warning-subtle);border:1px solid #EBD7A6;border-radius:var(--radius-md);
padding:12px 14px;font-size:12.5px;color:#7A5514;margin-top:16px;line-height:1.65}
.warnbox b{color:#5E4110}
code{background:var(--surface-2);border:1px solid var(--border);padding:1px 5px;border-radius:5px;font-size:12px;font-family:var(--font-mono)}
</style>
</head>
<body>
<div class="wrap">
<h1>pangolin-pay · 收款测试</h1>
<h1>pangolin-pay <span class="dot">·</span> 收款测试</h1>
<p class="sub">USDT-TRC20 · 单地址 + 唯一金额 · 手动测试页(非生产收银台)</p>
<div class="card" id="form-card">
@@ -91,19 +119,20 @@ const testPageHTML = `<!doctype html>
<span class="k">状态</span>
<span class="v"><span class="badge" id="o-status"></span></span>
</div>
<div style="text-align:center;padding:14px 0 6px">
<div style="color:var(--muted);font-size:12px;margin-bottom:4px">请支付<b style="color:var(--warn)">精确金额</b>(多付少付都不匹配)</div>
<div class="amtbox">
<div class="cap">请支付<b>精确金额</b>(多付少付都不匹配)</div>
<div class="amt" id="o-amount"></div>
<button class="copy" id="copy-amt" style="margin-top:8px">复制金额</button>
</div>
<div class="kv">
<span class="k">收款地址</span>
<span class="row" style="justify-content:flex-end">
<span class="v" id="o-addr"></span>
<button class="copy" id="copy-addr">复制</button>
</span>
<button class="copy" id="copy-amt">复制金额</button>
<div class="addrblock">
<div class="k">收款地址</div>
<div class="addr" id="o-addr"></div>
</div>
<div class="kv">
<button class="copy" id="copy-addr">复制地址</button>
<div class="kv" style="margin-top:6px">
<span class="k">TxID</span>
<span class="v" id="o-tx">—</span>
</div>
@@ -111,18 +140,20 @@ const testPageHTML = `<!doctype html>
<span class="k">过期倒计时</span>
<span class="v" id="o-ttl"></span>
</div>
<div class="warnbox">
发往此地址、且金额<b>正好等于</b>上面数字的 USDT-TRC20 转账,会在 watcher 下一轮轮询后自动标记
<code>paid</code>。金额不符 → 进 orphan 需人工对账。<br>
⚠️ 需 <code>TRONGRID_API_KEY</code> 已配置,否则 watcher 被限流可能侦测很慢。
⚠️ 需 <code>TRONGRID_API_KEY</code> 已配置,否则 watcher 被限流侦测很慢。
</div>
<button class="ghost" id="reset">新建另一单</button>
<div class="err" id="order-err"></div>
</div>
<p class="note">
地址模型:所有订单收到<b>同一地址</b>,靠<b>唯一金额</b>(基准价 + 微尾数 ≤0.01U)区分。
本页仅调 <code>POST /order</code> 与 <code>GET /order/{id}</code>,无私钥、无密钥。
测什么:<b>收款订单闭环</b> —— <code>POST /order</code> 建单拿地址+唯一金额、<code>GET /order/{id}</code>
轮询到 <code>paid</code>。地址模型:所有订单收到<b>同一地址</b>,靠<b>唯一金额</b>(基准价 + 微尾数
≤0.01U)区分。本页无私钥、无密钥。
</p>
</div>