@seasonkoh/webaz 0.1.28 → 0.1.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/README.zh-CN.md +7 -6
- package/dist/currency.js +16 -0
- package/dist/deposit-rails.js +52 -0
- package/dist/direct-pay-aml-monitor.js +67 -0
- package/dist/direct-pay-aml-review.js +40 -0
- package/dist/direct-pay-base-bond-entry.js +5 -0
- package/dist/direct-pay-bond-rail-clearance.js +94 -0
- package/dist/direct-pay-compliance-ingress.js +145 -0
- package/dist/direct-pay-controls.js +136 -0
- package/dist/direct-pay-create.js +162 -0
- package/dist/direct-pay-deferral-quota.js +43 -0
- package/dist/direct-pay-disclosures.js +44 -0
- package/dist/direct-pay-eligibility.js +46 -0
- package/dist/direct-pay-fee-ar.js +241 -0
- package/dist/direct-pay-launch-readiness.js +108 -0
- package/dist/direct-pay-launch-summary.js +68 -0
- package/dist/direct-pay-ledger.js +94 -0
- package/dist/direct-receive-account-qr.js +77 -0
- package/dist/direct-receive-accounts.js +82 -0
- package/dist/direct-receive-deferral.js +142 -0
- package/dist/direct-receive-deposits.js +260 -0
- package/dist/direct-receive-payment-instruction.js +26 -0
- package/dist/fx-rates.js +71 -0
- package/dist/layer0-foundation/L0-1-database/schema.js +531 -1
- package/dist/layer0-foundation/L0-2-state-machine/genuine-sale.js +15 -5
- package/dist/layer0-foundation/L0-2-state-machine/transitions.js +41 -0
- package/dist/layer0-foundation/L0-5-manifest/manifest.js +1 -1
- package/dist/layer1-agent/L1-1-mcp-server/cli.js +64 -0
- package/dist/layer1-agent/L1-1-mcp-server/network-mode.js +11 -0
- package/dist/layer1-agent/L1-1-mcp-server/server.js +18 -16
- package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +4 -4
- package/dist/ledger.js +12 -3
- package/dist/mcp.js +23 -4
- package/dist/merchant-bond-domain.js +64 -0
- package/dist/merchant-bond-exposure.js +78 -0
- package/dist/merchant-bond-watcher.js +26 -0
- package/dist/payment-rails.js +29 -0
- package/dist/product-verification.js +93 -0
- package/dist/pwa/acp-feed.js +3 -2
- package/dist/pwa/contract-fingerprint.js +3 -0
- package/dist/pwa/direct-pay-guards.js +20 -0
- package/dist/pwa/direct-pay-order-redaction.js +24 -0
- package/dist/pwa/endpoint-actions.js +3 -0
- package/dist/pwa/public/app-ai.js +10 -10
- package/dist/pwa/public/app-chat-poll.js +29 -0
- package/dist/pwa/public/app-create-kinds.js +17 -0
- package/dist/pwa/public/app-direct-pay-accounts.js +141 -0
- package/dist/pwa/public/app-direct-pay-buyer.js +72 -0
- package/dist/pwa/public/app-direct-pay-compliance.js +67 -0
- package/dist/pwa/public/app-direct-pay-deferral-admin.js +72 -0
- package/dist/pwa/public/app-direct-pay-deferral.js +61 -0
- package/dist/pwa/public/app-direct-pay-fee-center.js +33 -0
- package/dist/pwa/public/app-direct-pay-fee-ops.js +112 -0
- package/dist/pwa/public/app-direct-pay-product-verify.js +103 -0
- package/dist/pwa/public/app-direct-pay-readiness.js +38 -0
- package/dist/pwa/public/app-direct-pay-store-verify.js +100 -0
- package/dist/pwa/public/app-direct-pay.js +227 -0
- package/dist/pwa/public/app-discover.js +20 -20
- package/dist/pwa/public/app-external-links.js +32 -0
- package/dist/pwa/public/app-listings.js +4 -4
- package/dist/pwa/public/app-prelaunch-waz.js +39 -0
- package/dist/pwa/public/app-price.js +55 -0
- package/dist/pwa/public/app-product-media.js +15 -0
- package/dist/pwa/public/app-profile.js +6 -6
- package/dist/pwa/public/app-seller.js +5 -5
- package/dist/pwa/public/app-shop.js +19 -19
- package/dist/pwa/public/app.js +142 -146
- package/dist/pwa/public/i18n.js +398 -197
- package/dist/pwa/public/index.html +17 -0
- package/dist/pwa/public/openapi.json +495 -1
- package/dist/pwa/routes/admin-analytics.js +4 -2
- package/dist/pwa/routes/admin-direct-receive-deposits.js +321 -0
- package/dist/pwa/routes/buyer-feeds.js +2 -1
- package/dist/pwa/routes/chat.js +6 -1
- package/dist/pwa/routes/dashboards.js +2 -1
- package/dist/pwa/routes/direct-pay-availability.js +196 -0
- package/dist/pwa/routes/direct-pay-disclosure-acks.js +74 -0
- package/dist/pwa/routes/direct-pay-timeouts.js +71 -0
- package/dist/pwa/routes/direct-receive-accounts.js +155 -0
- package/dist/pwa/routes/direct-receive-payment-instructions.js +45 -0
- package/dist/pwa/routes/fx.js +12 -0
- package/dist/pwa/routes/leaderboard.js +47 -9
- package/dist/pwa/routes/listings.js +2 -2
- package/dist/pwa/routes/logistics.js +4 -0
- package/dist/pwa/routes/manifests.js +38 -0
- package/dist/pwa/routes/me-data.js +5 -2
- package/dist/pwa/routes/orders-action.js +117 -9
- package/dist/pwa/routes/orders-create.js +4 -0
- package/dist/pwa/routes/orders-read.js +36 -0
- package/dist/pwa/routes/p2p-products.js +2 -2
- package/dist/pwa/routes/products-create.js +5 -3
- package/dist/pwa/routes/products-links.js +34 -0
- package/dist/pwa/routes/products-list.js +2 -1
- package/dist/pwa/routes/products-update.js +22 -2
- package/dist/pwa/routes/promoter.js +3 -0
- package/dist/pwa/routes/referral.js +4 -0
- package/dist/pwa/routes/returns.js +26 -1
- package/dist/pwa/routes/rewards-apply.js +10 -5
- package/dist/pwa/routes/rewards-clearing-mature.js +96 -0
- package/dist/pwa/routes/rewards-escrow-expire.js +6 -2
- package/dist/pwa/routes/shops.js +2 -1
- package/dist/pwa/routes/url-claim.js +2 -2
- package/dist/pwa/routes/users-public.js +8 -0
- package/dist/pwa/routes/wallet-read.js +3 -0
- package/dist/pwa/routes/webauthn.js +1 -1
- package/dist/pwa/server.js +59 -102
- package/dist/runtime/webaz-schema-helpers.js +104 -0
- package/dist/store-verification.js +77 -0
- package/dist/version.js +1 -1
- package/package.json +71 -2
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// Direct Pay (Rail 1) — bilingual UI wiring (PR-4f-b / 5c)。UI ONLY;直付(direct_p2p)可用性 per-seller/product 实时数据驱动(见 GET /api/direct-pay/availability);注:escrow 轨仍模拟 WAZ(独立开关 _wazSimulated,非直付)。
|
|
2
|
+
// WebAZ【不托管本金】、不担保、不退款/维权,也不验证/路由卖家付款方式或币种;不做 USDC/fiat/PSP/链上、不碰
|
|
3
|
+
// wallet/escrow/settlement/refund/订单钱路。真实强制全在后端(#92/#93 两次披露 + 现场真人 Passkey 硬门控;前端只带输入)。
|
|
4
|
+
// 只把已合并后端能力(#96 收款说明、#94 建单、#92/#93 ack+Passkey、#98+ 控制面/可用性)接到 PWA。
|
|
5
|
+
// 面向用户中文文案走 t(),英文在 i18n.js _EN(双语 parity 由 test-direct-pay-ui.ts 守)。
|
|
6
|
+
|
|
7
|
+
// ── 错误码 → 双语文案(后端返回的 error_code → 清楚的中英文提示)──────────────────────────────
|
|
8
|
+
window.dpErrorText = (code, fallback) => {
|
|
9
|
+
const M = {
|
|
10
|
+
DIRECT_PAY_DISABLED: t('直付当前未开放'),
|
|
11
|
+
DIRECT_PAY_RAIL_BREAKER: t('直付暂停受理(运营维护中),请稍后再试'),
|
|
12
|
+
DIRECT_PAY_REGION_UNSUPPORTED: t('直付在你所在地区暂未开放'),
|
|
13
|
+
DIRECT_PAY_CAP_EXCEEDED: t('超出直付单笔上限(按 WebAZ 记录的订单金额计;不涉及你与卖家场外实际付款金额)'),
|
|
14
|
+
DIRECT_PAY_SELLER_NOT_ELIGIBLE: t('该卖家暂不支持直付'),
|
|
15
|
+
DIRECT_PAY_SELLER_SUSPENDED: t('该卖家直付已被暂停'),
|
|
16
|
+
DISCLOSURE_NOT_ACKED: t('需先完成两次风险披露确认(D1 + D2)'),
|
|
17
|
+
HUMAN_PRESENCE_REQUIRED: t('需现场真人 Passkey 确认'),
|
|
18
|
+
PASSKEY_REQUIRED_FOR_DIRECT_PAY: t('直付需要先注册 Passkey'),
|
|
19
|
+
NO_PAYMENT_INSTRUCTION: t('卖家尚未设置收款说明,暂不可直付'),
|
|
20
|
+
DIRECT_PAY_NOT_AVAILABLE: t('该卖家暂不支持直付'),
|
|
21
|
+
DIRECT_PAY_KYC_REQUIRED: t('该卖家暂不支持直付'), // 对买家脱敏:不暴露卖家 KYC/制裁具体状态
|
|
22
|
+
ORDER_NOT_DELIVERED: t('订单尚未送达,暂不可确认收货'),
|
|
23
|
+
DIRECT_PAY_SIMPLE_PRODUCT_ONLY: t('直付当前仅支持简单商品(无规格)'),
|
|
24
|
+
DIRECT_PAY_UNSUPPORTED_OPTION: t('直付当前不支持该下单选项'),
|
|
25
|
+
}
|
|
26
|
+
return M[code] || fallback || t('操作失败,请重试')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ── 买家结算:支付方式(rail)选择。escrow 默认;direct_p2p 可选,选中先查可用性(见 dpOnRailChange);D1/D2 ack 在建单后用 Passkey 完成。
|
|
30
|
+
window.dpRailSelectorHtml = (productId, priceUsdc) => `
|
|
31
|
+
<details style="margin-top:10px" id="dp-rail-block" data-product="${productId || ''}" data-amt="${priceUsdc != null && isFinite(Number(priceUsdc)) ? Number(priceUsdc) : ''}">
|
|
32
|
+
<summary style="font-size:13px;font-weight:600;color:#374151;cursor:pointer">${t('支付方式')}</summary>
|
|
33
|
+
<div style="padding:8px 2px 2px">
|
|
34
|
+
<label style="display:flex;gap:8px;align-items:flex-start;font-size:13px;margin-bottom:6px;cursor:pointer">
|
|
35
|
+
<input type="radio" name="dp-rail" value="escrow" ${window._wazSimulated ? '' : 'checked'} onchange="dpOnRailChange('${productId || ''}')">
|
|
36
|
+
<span><b>${window._wazSimulated ? t('托管(Escrow,模拟测试币)') : t('托管(Escrow,默认)')}</b><br><span style="font-size:11px;color:#6b7280">${t('本金由协议托管,确认收货后释放给卖家')}</span></span>
|
|
37
|
+
</label>
|
|
38
|
+
<label style="display:flex;gap:8px;align-items:flex-start;font-size:13px;cursor:pointer">
|
|
39
|
+
<input type="radio" name="dp-rail" value="direct_p2p" onchange="dpOnRailChange('${productId || ''}')">
|
|
40
|
+
<span><b>${t('直付(Direct Pay · 非托管)')}</b><br><span style="font-size:11px;color:#6b7280">${t('你直接付款给卖家(场外),本金不经 WebAZ')}</span></span>
|
|
41
|
+
</label>
|
|
42
|
+
<div id="dp-rail-note" style="display:none;margin-top:8px;font-size:11px;line-height:1.6;color:#92400e;background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:8px 10px">
|
|
43
|
+
⚠️ ${t('直付风险提醒:WebAZ 不托管本金、不担保、不退款、不代维权,也不验证卖家的付款方式或币种。下单后需用 Passkey 完成两次风险确认,再标记付款。')}
|
|
44
|
+
<div style="margin-top:6px">${t('需要 Passkey。')}<a href="#me" style="color:#854d0e;font-weight:600;text-decoration:underline">${t('前往「我的 → 安全与存储」注册 →')}</a></div>
|
|
45
|
+
</div>
|
|
46
|
+
<div id="dp-rail-unavailable" style="display:none;margin-top:8px;font-size:11px;line-height:1.6;color:#991b1b;background:#fef2f2;border:1px solid #fecaca;border-radius:8px;padding:8px 10px"></div><div id="dp-account-picker"></div>
|
|
47
|
+
</div>
|
|
48
|
+
</details>`
|
|
49
|
+
|
|
50
|
+
// 选直付先查 /direct-pay/availability(与建单门同源):available=true 才展示风险提醒+允许继续;否则用 dpErrorText 显示明确不可用原因(非系统错误/不裸露 JSON)并退回托管轨,阻止进入直付建单。
|
|
51
|
+
window.dpOnRailChange = async (productId) => {
|
|
52
|
+
const note = document.getElementById('dp-rail-note'); window._dpDirectAvailable = false // 进入即 pending:确认前 dpSelectedRail 只输出 escrow(防"选直付后立刻点确认"竞态)
|
|
53
|
+
const un = document.getElementById('dp-rail-unavailable')
|
|
54
|
+
if (note) note.style.display = 'none'
|
|
55
|
+
if (un) un.style.display = 'none'; { const _pk = document.getElementById('dp-account-picker'); if (_pk) _pk.innerHTML = '' } // D3:切轨/不可用即清空账号选择器
|
|
56
|
+
const _sel = document.querySelector('input[name="dp-rail"]:checked')?.value; if (window.wazEscrowRailNote) window.wazEscrowRailNote(_sel); if (_sel !== 'direct_p2p') return // [PRELAUNCH-WAZ-SIM] 选 escrow→测试币提醒
|
|
57
|
+
let av = null
|
|
58
|
+
try { av = await GET('/direct-pay/availability?product_id=' + encodeURIComponent(productId || '')) } catch { av = null }
|
|
59
|
+
if (av && av.available === true) { window._dpDirectAvailable = true; if (note) note.style.display = ''; if (window.dpLoadBuyerAccounts) window.dpLoadBuyerAccounts(productId); return } // D3:可用时加载卖家可选收款账号
|
|
60
|
+
if (un) { un.textContent = '⚠️ ' + window.dpErrorText(av && av.error_code, av && av.reason) ; un.style.display = '' } // 不可用:明确原因 + 退回 escrow
|
|
61
|
+
const esc = document.querySelector('input[name="dp-rail"][value="escrow"]')
|
|
62
|
+
if (esc) { esc.checked = true; if (window.wazEscrowRailNote) window.wazEscrowRailNote('escrow') } // [PRELAUNCH-WAZ-SIM] 直付不可用退回 escrow 时也提醒测试币
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 只有 availability 已确认 available:true(window._dpDirectAvailable)才输出 direct_p2p;pending/unavailable → escrow(竞态下确认只会下 escrow 单,绝不发 direct_p2p create)。
|
|
66
|
+
window.dpSelectedRail = () => { const c = document.querySelector('input[name="dp-rail"]:checked')?.value; if (c === 'direct_p2p') return window._dpDirectAvailable === true ? 'direct_p2p' : ''; return c === 'escrow' ? 'escrow' : (window._wazSimulated ? '' : 'escrow') } // #28 直付选中但未确认→''(永不静默回退 escrow,不分模拟);[PRELAUNCH-WAZ-SIM] 模拟期未选→''
|
|
67
|
+
|
|
68
|
+
// ── 建单成功后(direct_p2p):D1/D2 披露 ack(各一次 Passkey)→ ack 后才展示快照收款说明 → 跳订单页。res=POST /orders 返回;有错则双语提示并停。
|
|
69
|
+
window.dpAfterCreate = async (res) => {
|
|
70
|
+
if (!res || res.error || res.error_code) {
|
|
71
|
+
const m = window.dpErrorText(res?.error_code, res?.error)
|
|
72
|
+
if (typeof toast$ === 'function') toast$(m, 'error'); else alert(m)
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
const orderId = res.order_id
|
|
76
|
+
// 边界:必须【先】完成 D1/D2 + Passkey,之后【才】展示卖家收款说明(快照)。未完成则不展示,留到订单页继续。
|
|
77
|
+
const acked = await window.dpEnsureAcks(orderId)
|
|
78
|
+
if (!acked) {
|
|
79
|
+
if (typeof toast$ === 'function') toast$(t('需完成 D1/D2 Passkey 风险确认后才显示收款说明 · 可在订单页继续'), 'info')
|
|
80
|
+
setTimeout(() => navigate(`#order/${orderId}`), 600)
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
// 两次披露已 ack —— 收款说明【不】来自 create 响应(后端在 both-acked 前不下发);现在才 GET 订单读取 redaction-gated 快照。
|
|
84
|
+
const o = await GET(`/orders/${orderId}`)
|
|
85
|
+
const instr = o && o.order ? (o.order.direct_pay_instruction_snapshot || '') : ''
|
|
86
|
+
await confirmModal(
|
|
87
|
+
`${t('风险确认完成 · 这是卖家的收款说明(下单时快照,WebAZ 不验证)')}\n\n${instr}\n\n${t('请按此付款,然后在订单页标记"我已付款"')}`,
|
|
88
|
+
t('我知道了'), {})
|
|
89
|
+
setTimeout(() => navigate(`#order/${orderId}`), 300)
|
|
90
|
+
}
|
|
91
|
+
// ── 确保两次披露都已 ack(缺哪个补哪个;每次都需现场真人 Passkey)。返回 both-acked 与否。 ──────────
|
|
92
|
+
window.dpEnsureAcks = async (orderId) => {
|
|
93
|
+
const st = await GET(`/direct-pay/disclosure-acks/${orderId}`)
|
|
94
|
+
if (st.error) { if (typeof toast$ === 'function') toast$(window.dpErrorText(st.error_code, st.error), 'error'); return false }
|
|
95
|
+
const disc = st.disclosures || {}
|
|
96
|
+
const stages = [
|
|
97
|
+
{ key: 'pre_select', acked: st.acked?.pre_select, text: disc.pre_select },
|
|
98
|
+
{ key: 'pre_confirm', acked: st.acked?.pre_confirm, text: disc.pre_confirm },
|
|
99
|
+
]
|
|
100
|
+
for (const s of stages) {
|
|
101
|
+
if (s.acked) continue
|
|
102
|
+
const body = s.text ? (window._lang === 'en' ? s.text.en : s.text.zh) : t('风险披露')
|
|
103
|
+
const go = await confirmModal(`${body}\n\n${t('我已阅读并理解上述风险')}`, t('确认(需 Passkey)'), { danger: true })
|
|
104
|
+
if (!go) return false
|
|
105
|
+
const ok = await window.dpDoAck(orderId, s.key)
|
|
106
|
+
if (!ok) return false
|
|
107
|
+
}
|
|
108
|
+
return true
|
|
109
|
+
}
|
|
110
|
+
// Passkey 门失败(无 Passkey / 设备不支持 / 用户取消)→ 给出明确的【注册 Passkey 入口】(前往「我的 → 安全与存储」)。
|
|
111
|
+
window.dpPromptRegisterPasskey = async (reason) => {
|
|
112
|
+
const go = await confirmModal(
|
|
113
|
+
`${reason ? reason + '\n\n' : ''}${t('直付的风险确认与付款标记需要 Passkey。前往「我的 → 安全与存储」注册一个?')}`,
|
|
114
|
+
t('前往注册 Passkey'), {})
|
|
115
|
+
if (go) navigate('#me')
|
|
116
|
+
}
|
|
117
|
+
// 单次披露 ack:Passkey ceremony(purpose=direct_pay_disclosure_ack,order+stage 绑 purpose_data)→ POST ack。
|
|
118
|
+
window.dpDoAck = async (orderId, stage) => {
|
|
119
|
+
let token
|
|
120
|
+
try { token = await requestPasskeyGate('direct_pay_disclosure_ack', { order_id: orderId, stage }) }
|
|
121
|
+
catch (e) { await window.dpPromptRegisterPasskey(e && e.message); return false }
|
|
122
|
+
const r = await POST('/direct-pay/disclosure-acks', { order_id: orderId, stage, webauthn_token: token })
|
|
123
|
+
if (r.error) { if (typeof toast$ === 'function') toast$(window.dpErrorText(r.error_code, r.error), 'error'); return false }
|
|
124
|
+
return true
|
|
125
|
+
}
|
|
126
|
+
// ── 订单详情:direct_p2p 诚实边界始终显示;卖家收款说明快照【不】内联进 HTML,由 dpHydrateOrderDisclosure 在 both-acked 后才另取渲染(未 ack 时 DOM 里也没有快照)。
|
|
127
|
+
window.dpOrderDisclosureHtml = (_order) => `
|
|
128
|
+
<div class="card" style="border:1px solid #fde68a;background:linear-gradient(135deg,#fffbeb,#fef3c7)">
|
|
129
|
+
<div style="font-size:13px;font-weight:700;color:#92400e;margin-bottom:8px">💸 ${t('直付订单(非托管)')}</div>
|
|
130
|
+
<ul style="margin:0 0 8px;padding-left:18px;font-size:12px;line-height:1.7;color:#374151">
|
|
131
|
+
<li>${t('本金不经 WebAZ —— 你直接付款给卖家(场外)')}</li>
|
|
132
|
+
<li>${t('WebAZ 不托管、不担保、不退款、不代维权')}</li>
|
|
133
|
+
<li>${t('卖家收款说明来自卖家自填,WebAZ 不验证付款方式或币种')}</li>
|
|
134
|
+
</ul>
|
|
135
|
+
<div id="dp-order-instr">${loading$()}</div>
|
|
136
|
+
</div>`
|
|
137
|
+
// ack-gated 收款说明:both-acked → 另取订单快照并展示;否则只显示"先完成 D1/D2 Passkey"的门(快照不入 DOM)。
|
|
138
|
+
window.dpHydrateOrderDisclosure = async (orderId) => {
|
|
139
|
+
const box = document.getElementById('dp-order-instr')
|
|
140
|
+
if (!box) return
|
|
141
|
+
const st = await GET(`/direct-pay/disclosure-acks/${orderId}`)
|
|
142
|
+
if (st.error) { box.innerHTML = `<div style="font-size:12px;color:#dc2626">${window.dpErrorText(st.error_code, st.error)}</div>`; return }
|
|
143
|
+
if (!st.both) {
|
|
144
|
+
box.innerHTML = `<div style="font-size:12px;color:#92400e;background:#fff;border:1px solid #fde68a;border-radius:8px;padding:8px 10px">
|
|
145
|
+
${t('需先完成 D1/D2 Passkey 风险确认,确认后才显示卖家收款说明。')}
|
|
146
|
+
<div style="margin-top:6px"><button class="btn btn-outline btn-sm" onclick="dpCompleteAcksThenReveal('${orderId}')">${t('完成风险确认')}</button></div></div>`
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
const o = await GET(`/orders/${orderId}`)
|
|
150
|
+
const snap = o && o.order ? (o.order.direct_pay_instruction_snapshot || '') : ''
|
|
151
|
+
box.innerHTML = snap ? `<div style="font-size:12px;color:#374151;background:#fff;border:1px solid #fde68a;border-radius:8px;padding:8px 10px">
|
|
152
|
+
<div style="font-size:11px;color:#9ca3af;margin-bottom:2px">${t('卖家收款说明(下单时快照)')}</div>${escHtml(snap)}<div id="dp-order-qr"></div></div>`
|
|
153
|
+
: `<div style="font-size:12px;color:#9ca3af">${t('卖家尚未设置收款说明,暂不可直付')}</div>`; if (snap && window.dpLoadOrderQr) window.dpLoadOrderQr(orderId) // D3:ack 门后取所选账号的收款二维码(若有)
|
|
154
|
+
}
|
|
155
|
+
window.dpCompleteAcksThenReveal = async (orderId) => {
|
|
156
|
+
const ok = await window.dpEnsureAcks(orderId)
|
|
157
|
+
if (ok) window.dpHydrateOrderDisclosure(orderId)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ── 订单动作(mark_paid/confirm/confirm_in_person):前端先确保披露 ack,再取一次性 Passkey token 带进 order-action 端点;后端(#93)仍强制两次披露+Passkey。
|
|
161
|
+
window.dpHandleAction = async (orderId, action) => {
|
|
162
|
+
const msgEl = document.getElementById('action-msg')
|
|
163
|
+
const show = (type, m) => { if (msgEl) msgEl.innerHTML = alert$(type, m); else if (typeof toast$ === 'function') toast$(m, type) }
|
|
164
|
+
// mark_paid 前必须两次披露都 ack(confirm/confirm_in_person 通常已在 mark_paid 阶段 ack 过,这里兜底再查)。
|
|
165
|
+
const acked = await window.dpEnsureAcks(orderId)
|
|
166
|
+
if (!acked) { show('error', window.dpErrorText('DISCLOSURE_NOT_ACKED')); return }
|
|
167
|
+
let token
|
|
168
|
+
try { token = await requestPasskeyGate('direct_pay_order_action', { order_id: orderId, action }) }
|
|
169
|
+
catch (e) { await window.dpPromptRegisterPasskey(e && e.message); return }
|
|
170
|
+
show('info', `<span class="spinner"></span>${t('处理中...')}`)
|
|
171
|
+
const path = action === 'confirm_in_person' ? `/orders/${orderId}/confirm-in-person` : `/orders/${orderId}/action`
|
|
172
|
+
const body = action === 'confirm_in_person' ? { webauthn_token: token } : { action, webauthn_token: token }
|
|
173
|
+
const r = await POST(path, body)
|
|
174
|
+
if (r.error) { show('error', window.dpErrorText(r.error_code, r.error)); return }
|
|
175
|
+
show('success', t('操作成功'))
|
|
176
|
+
setTimeout(() => renderOrderDetail(document.getElementById('app'), orderId), 1000)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ── 卖家:收款说明设置(GET / PUT / DELETE,仅展示文本)─────────────────────────────────────────
|
|
180
|
+
window.dpSellerInstructionSection = () => `
|
|
181
|
+
<div class="card" style="margin-bottom:12px">
|
|
182
|
+
<div style="font-size:14px;font-weight:700;margin-bottom:6px">💳 ${t('直付收款说明')}</div>
|
|
183
|
+
<div style="font-size:12px;color:#6b7280;line-height:1.6;margin-bottom:10px">${t('这是你自填的收款展示文本(场外结算用);WebAZ 不验证付款方式或币种,不路由支付,不托管资金。')}</div>
|
|
184
|
+
<div id="dp-instr-box">${loading$()}</div>
|
|
185
|
+
</div>`
|
|
186
|
+
|
|
187
|
+
window.dpHydrateInstruction = async () => {
|
|
188
|
+
const box = document.getElementById('dp-instr-box')
|
|
189
|
+
if (!box) return
|
|
190
|
+
const r = await GET('/direct-receive/payment-instruction')
|
|
191
|
+
if (r.error) { box.innerHTML = alert$('error', window.dpErrorText(r.error_code, r.error)); return }
|
|
192
|
+
const cur = r.instruction
|
|
193
|
+
box.innerHTML = `
|
|
194
|
+
${cur ? `<div style="font-size:12px;color:#374151;background:#f9fafb;border:1px solid #e5e7eb;border-radius:8px;padding:8px 10px;margin-bottom:10px">
|
|
195
|
+
<div style="font-size:11px;color:#9ca3af;margin-bottom:2px">${t('当前生效')}${cur.label ? ` · ${escHtml(cur.label)}` : ''}</div>${escHtml(cur.instruction)}</div>` :
|
|
196
|
+
`<div style="font-size:12px;color:#9ca3af;margin-bottom:10px">${t('尚未设置收款说明')}</div>`}
|
|
197
|
+
<div id="dp-instr-msg"></div>
|
|
198
|
+
<div class="form-group"><label class="form-label">${t('收款说明')} <span style="font-size:11px;color:#9ca3af">${t('(展示给买家,如 PayNow / 银行转账 等)')}</span></label>
|
|
199
|
+
<textarea class="form-control" id="dp-instr-text" rows="3" maxlength="500" placeholder="${t('例:PayNow +65 9xxx(场外结算)')}">${cur ? escHtml(cur.instruction) : ''}</textarea></div>
|
|
200
|
+
<div class="form-group"><label class="form-label">${t('标签(可选)')}</label>
|
|
201
|
+
<input class="form-control" id="dp-instr-label" maxlength="40" value="${cur && cur.label ? escHtml(cur.label) : ''}" placeholder="${t('如 PayNow')}"></div>
|
|
202
|
+
<div style="display:flex;gap:8px">
|
|
203
|
+
<button class="btn btn-primary btn-sm" onclick="dpSaveInstruction()">${t('保存')}</button>
|
|
204
|
+
${cur ? `<button class="btn btn-outline btn-sm" style="color:#dc2626;border-color:#dc2626" onclick="dpDeleteInstruction()">${t('停用')}</button>` : ''}
|
|
205
|
+
</div>`
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
window.dpSaveInstruction = async () => {
|
|
209
|
+
const msg = document.getElementById('dp-instr-msg')
|
|
210
|
+
const instruction = document.getElementById('dp-instr-text')?.value?.trim() || ''
|
|
211
|
+
const label = document.getElementById('dp-instr-label')?.value?.trim() || ''
|
|
212
|
+
if (!instruction) { if (msg) msg.innerHTML = alert$('error', t('收款说明不能为空')); return }
|
|
213
|
+
if (msg) msg.innerHTML = `<div class="alert alert-info"><span class="spinner"></span>${t('保存中...')}</div>`
|
|
214
|
+
const r = await PUT('/direct-receive/payment-instruction', { instruction, label })
|
|
215
|
+
if (r.error) { if (msg) msg.innerHTML = alert$('error', window.dpErrorText(r.error_code, r.error)); return }
|
|
216
|
+
if (typeof toast$ === 'function') toast$(t('已保存'), 'success')
|
|
217
|
+
window.dpHydrateInstruction()
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
window.dpDeleteInstruction = async () => {
|
|
221
|
+
const go = await confirmModal(t('停用后买家将无法对你发起直付订单,确定停用?'), t('停用'), { danger: true })
|
|
222
|
+
if (!go) return
|
|
223
|
+
const r = await api('DELETE', '/direct-receive/payment-instruction')
|
|
224
|
+
if (r.error) { if (typeof toast$ === 'function') toast$(window.dpErrorText(r.error_code, r.error), 'error'); return }
|
|
225
|
+
if (typeof toast$ === 'function') toast$(t('已停用'), 'success')
|
|
226
|
+
window.dpHydrateInstruction()
|
|
227
|
+
}
|
|
@@ -32,7 +32,7 @@ async function renderShop(app, opts = {}) {
|
|
|
32
32
|
<div class="product-img">${getCategoryIcon(p.category)}</div>
|
|
33
33
|
<div class="product-body">
|
|
34
34
|
<div class="product-name">${escHtml(p.title)}</div>
|
|
35
|
-
<div class="product-price">${p.price}
|
|
35
|
+
<div class="product-price">${window.fmtPrice(p.price)}</div>
|
|
36
36
|
<div class="product-seller">${repBadge(p.rep_level)}@${escHtml(p.seller_name)}</div>
|
|
37
37
|
</div>
|
|
38
38
|
</div>`).join('')}
|
|
@@ -553,7 +553,7 @@ function renderAgentBuyResultInto(container, res) {
|
|
|
553
553
|
const bestCard = res.best_product ? `
|
|
554
554
|
<div style="background:#f0fdf4;border:1px solid #bbf7d0;border-radius:10px;padding:12px;margin:12px 0">
|
|
555
555
|
<div style="font-weight:600;font-size:14px;margin-bottom:4px">${escHtml(res.best_product.title)}</div>
|
|
556
|
-
<div style="font-size:18px;font-weight:700;color:#16a34a;margin-bottom:4px">${res.best_product.price}
|
|
556
|
+
<div style="font-size:18px;font-weight:700;color:#16a34a;margin-bottom:4px">${window.fmtPrice(res.best_product.price)}</div>
|
|
557
557
|
<div style="font-size:12px;color:#6b7280;margin-bottom:8px">${escHtml(res.best_product.agent_summary || '')}</div>
|
|
558
558
|
${!res.auto_bought ? `<button class="btn btn-primary btn-sm" style="width:auto" onclick="navigate('#order-product/${res.best_product.id}')">${t('查看并下单')}</button>` : ''}
|
|
559
559
|
</div>` : ''
|
|
@@ -571,7 +571,7 @@ function renderAgentBuyResultInto(container, res) {
|
|
|
571
571
|
<div style="font-size:13px;font-weight:500">${p.url_match ? '🎯 ' : ''}${escHtml(p.title)}</div>
|
|
572
572
|
<div style="font-size:11px;color:#6b7280">${escHtml(p.agent_summary || '')}${p.url_match ? ` · <span style="color:#16a34a">${t('同款商品')}</span>` : ''}</div>
|
|
573
573
|
</div>
|
|
574
|
-
<div style="font-weight:700;color:#1d4ed8;white-space:nowrap;margin-left:8px">${p.price}
|
|
574
|
+
<div style="font-weight:700;color:#1d4ed8;white-space:nowrap;margin-left:8px">${window.fmtPrice(p.price)}</div>
|
|
575
575
|
</div>`).join('')}
|
|
576
576
|
</div>` : ''
|
|
577
577
|
container.innerHTML = `
|
|
@@ -675,7 +675,7 @@ function buyResultCardHtml(p) {
|
|
|
675
675
|
<div style="font-size:14px;font-weight:600;color:#111827;line-height:1.4;margin-bottom:4px">${escHtml(p.title)}${typeBadge}${valueBadgeChip}${lowStockChip}</div>
|
|
676
676
|
<div style="display:flex;align-items:baseline;gap:6px;margin-bottom:4px">
|
|
677
677
|
<span style="font-size:20px;font-weight:700;color:#4f46e5">${p.price}</span>
|
|
678
|
-
<span style="font-size:11px;color:#6b7280">
|
|
678
|
+
<span style="font-size:11px;color:#6b7280" data-usdc-local="${p.price}">USDC${window._fxLocal(p.price) ? ' ≈ ' + window._fxLocal(p.price) : ''}</span>
|
|
679
679
|
</div>
|
|
680
680
|
<div style="font-size:11px;color:#6b7280">
|
|
681
681
|
${repBadge(p.rep_level)} @${escHtml(p.seller_name)} · ${p.sales_count || 0} ${t('单完成')}
|
|
@@ -768,8 +768,8 @@ async function renderDiscover(app) {
|
|
|
768
768
|
const sectionStrip = ''
|
|
769
769
|
|
|
770
770
|
// sort + type 合并为可折叠 filter(默认折叠,sort 名 + type 名作摘要)
|
|
771
|
-
const sortLabel = ({trending:'🔥 热门', recommended:'📣 推荐多', seller_win_rate:'⚖️ 胜诉率', newest:'🆕 最新', rating:'⭐ 信誉', price_asc:'💰 价格 ↑', random:'🎲 随机'}[sort]) || sort
|
|
772
|
-
const typeLabel = ({retail:'🛍️ 零售', wholesale:'📦 批发', service:'🛠️ 服务', digital:'💾 数字'}[ptype]) || ptype
|
|
771
|
+
const sortLabel = ({trending:'🔥 '+t('热门'), recommended:'📣 '+t('推荐多'), seller_win_rate:'⚖️ '+t('胜诉率'), newest:'🆕 '+t('最新'), rating:'⭐ '+t('信誉'), price_asc:'💰 '+t('价格 ↑'), random:'🎲 '+t('随机')}[sort]) || sort
|
|
772
|
+
const typeLabel = ({retail:'🛍️ '+t('零售'), wholesale:'📦 '+t('批发'), service:'🛠️ '+t('服务'), digital:'💾 '+t('数字')}[ptype]) || ptype
|
|
773
773
|
const filterPanel = `
|
|
774
774
|
<details style="margin-bottom:10px;background:#fff;border:1px solid #e5e7eb;border-radius:8px">
|
|
775
775
|
<summary style="padding:8px 12px;cursor:pointer;font-size:12px;color:#374151;display:flex;justify-content:space-between;align-items:center">
|
|
@@ -824,7 +824,7 @@ async function renderDiscover(app) {
|
|
|
824
824
|
name: nameField,
|
|
825
825
|
url: location.origin + '/#order-product/' + pp.id,
|
|
826
826
|
...(img ? { image: img } : {}),
|
|
827
|
-
offers: { '@type': 'Offer', price: pp.price, priceCurrency:
|
|
827
|
+
offers: { '@type': 'Offer', price: pp.price, priceCurrency: 'USDC' }, // machine-readable price matches the USDC display (no human/agent drift); WAZ was simulated
|
|
828
828
|
},
|
|
829
829
|
}
|
|
830
830
|
}),
|
|
@@ -945,7 +945,7 @@ function productCardHtml(p, showSales) {
|
|
|
945
945
|
<div class="product-img">${getCategoryIcon(p.category)}</div>
|
|
946
946
|
<div class="product-body">
|
|
947
947
|
<div class="product-name">${escHtml(p.title)}${typeBadge}</div>
|
|
948
|
-
<div class="product-price">${p.price}
|
|
948
|
+
<div class="product-price">${window.fmtPrice(p.price)}</div>
|
|
949
949
|
<div class="product-seller">${t('卖家')}:@${escHtml(p.seller_name)}</div>
|
|
950
950
|
${p.seller_created_at ? `<div style="font-size:10px;color:#9ca3af;margin-top:1px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${t('入驻时长')}:${joinDuration(p.seller_created_at)}</div>` : ''}
|
|
951
951
|
${trust ? `<div style="font-size:10px;color:#6b7280;margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${trust}</div>` : ''}
|
|
@@ -1062,7 +1062,7 @@ async function renderFeedView() {
|
|
|
1062
1062
|
const actor = `<button onclick="toggleFollow('${e.actor_id}', this)" class="feed-actor" style="background:none;border:none;color:#4f46e5;font-weight:600;cursor:pointer;padding:0">${escHtml(e.actor_name || '—')}</button>`
|
|
1063
1063
|
let body = ''
|
|
1064
1064
|
if (e.kind === 'purchase') {
|
|
1065
|
-
body = `${actor} ${t('购买了')} <a href="#order-product/${e.product_id}" style="color:#111">${escHtml(e.product_title)}</a> · ${e.price}
|
|
1065
|
+
body = `${actor} ${t('购买了')} <a href="#order-product/${e.product_id}" style="color:#111">${escHtml(e.product_title)}</a> · ${window.fmtPrice(e.price)}`
|
|
1066
1066
|
} else if (e.kind === 'commission') {
|
|
1067
1067
|
const amount = Number(extra.amount || 0).toFixed(2)
|
|
1068
1068
|
body = `${actor} ${t('因推广')} <a href="#order-product/${e.product_id}" style="color:#111">${escHtml(e.product_title)}</a> ${t('获得 L')}${extra.level} ${t('佣金')} <strong style="color:#059669">+${amount} WAZ</strong>`
|
|
@@ -1093,7 +1093,7 @@ async function renderFeedRanks() {
|
|
|
1093
1093
|
GET('/leaderboard?kind=buyers&limit=5').catch(() => ({ items: [] })),
|
|
1094
1094
|
])
|
|
1095
1095
|
const rankLine = (rank, label, sub, hash) => `
|
|
1096
|
-
<div onclick="location.hash='${hash}'" style="display:flex;gap:8px;align-items:center;padding:6px 0
|
|
1096
|
+
<div ${hash ? `onclick="location.hash='${hash}'"` : ''} style="display:flex;gap:8px;align-items:center;padding:6px 0;${hash ? 'cursor:pointer;' : ''}border-bottom:1px solid #f3f4f6">
|
|
1097
1097
|
<div style="font-size:11px;font-weight:700;color:${rank <= 3 ? '#dc2626' : '#9ca3af'};width:18px;text-align:center;flex-shrink:0">${rank}</div>
|
|
1098
1098
|
<div style="flex:1;min-width:0">
|
|
1099
1099
|
<div style="font-size:12px;font-weight:600;color:#1f2937;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${escHtml(label)}</div>
|
|
@@ -1111,13 +1111,13 @@ async function renderFeedRanks() {
|
|
|
1111
1111
|
|
|
1112
1112
|
body.innerHTML = ''
|
|
1113
1113
|
+ miniCard('🔥', t('热门商品'), 'products', products.items || [], (it, r) =>
|
|
1114
|
-
rankLine(r, it.title, `${Number(it.completion_count||0)} ${t('单')} · ${Number(it.recommend_count||0)} ${t('人推荐')} · ${it.price}
|
|
1114
|
+
rankLine(r, it.title, `${Number(it.completion_count||0)} ${t('单')} · ${Number(it.recommend_count||0)} ${t('人推荐')} · ${window.fmtPrice(it.price)}`, `#order-product/${it.id}`))
|
|
1115
1115
|
+ miniCard('🏪', t('卖家榜'), 'sellers', sellers.items || [], (it, r) =>
|
|
1116
|
-
rankLine(r, '@' + (it.handle || it.name || ''), `${it.rating_count > 0 ? '⭐ ' + Number(it.avg_rating).toFixed(1) + ' (' + it.rating_count + ')' : t('暂无评价')} · ${Number(it.orders_count||0)} ${t('单')}`, `#shop/${it.
|
|
1116
|
+
rankLine(r, '@' + (it.handle || it.name || ''), `${it.rating_count > 0 ? '⭐ ' + Number(it.avg_rating).toFixed(1) + ' (' + it.rating_count + ')' : t('暂无评价')} · ${Number(it.orders_count||0)} ${t('单')}`, it.handle ? `#shop/${it.handle}` : ''))
|
|
1117
1117
|
+ miniCard('📣', t('创作者榜'), 'creators', creators.items || [], (it, r) =>
|
|
1118
|
-
rankLine(r, '@' + (it.handle || it.name || ''), `${Number(it.products_shared||0)} ${t('个商品')} · ${Number(it.total_likes||0)} ${t('赞')}`, `#u/${it.
|
|
1118
|
+
rankLine(r, '@' + (it.handle || it.name || ''), `${Number(it.products_shared||0)} ${t('个商品')} · ${Number(it.total_likes||0)} ${t('赞')}`, it.handle ? `#u/${it.handle}` : ''))
|
|
1119
1119
|
+ miniCard('🛍', t('买家榜'), 'buyers', buyers.items || [], (it, r) =>
|
|
1120
|
-
rankLine(r, '@' + (it.handle || it.name || ''), `${Number(it.orders_count||0)} ${t('单')}`, `#u/${it.
|
|
1120
|
+
rankLine(r, '@' + (it.handle || it.name || ''), `${Number(it.orders_count||0)} ${t('单')}`, it.handle ? `#u/${it.handle}` : ''))
|
|
1121
1121
|
}
|
|
1122
1122
|
|
|
1123
1123
|
window.setFeedScope = (k) => {
|
|
@@ -1176,7 +1176,7 @@ async function renderNewArrivals(app) {
|
|
|
1176
1176
|
<div class="product-img">${getCategoryIcon(p.category)}</div>
|
|
1177
1177
|
<div class="product-body">
|
|
1178
1178
|
<div class="product-name">${escHtml(p.title)}</div>
|
|
1179
|
-
<div class="product-price">${p.price}
|
|
1179
|
+
<div class="product-price">${window.fmtPrice(p.price)}</div>
|
|
1180
1180
|
<div class="product-seller">${repBadge(p.rep_level)}@${escHtml(p.seller_name)}</div>
|
|
1181
1181
|
${p.trial_quota_remaining > 0
|
|
1182
1182
|
? `<div style="font-size:11px;color:#9333ea;margin-top:4px;font-weight:600">🎁 ${t('测评免单 剩')} ${p.trial_quota_remaining} ${t('名额')}</div>`
|
|
@@ -1205,14 +1205,14 @@ async function renderNewArrivalsFeed(app) {
|
|
|
1205
1205
|
? feedEmpty('🆕', t('暂无新品动态'), t('看看 推荐好物'), '#discover')
|
|
1206
1206
|
: products.map(p => {
|
|
1207
1207
|
const ts = fmtTime(p.created_at)
|
|
1208
|
-
const img =
|
|
1208
|
+
const img = window.productThumbSrc(p.images)
|
|
1209
1209
|
return `<div class="card" style="margin-bottom:8px;padding:12px;display:flex;gap:10px;align-items:flex-start;cursor:pointer" onclick="navigate('#order-product/${p.id}')">
|
|
1210
|
-
${img ? `<img src="${escAttr(img)}" style="width:56px;height:56px;border-radius:6px;object-fit:cover;flex-shrink:0">` : `<div style="width:56px;height:56px;border-radius:6px;background:#f3f4f6;display:flex;align-items:center;justify-content:center;font-size:24px;flex-shrink:0">${getCategoryIcon(p.category)}</div>`}
|
|
1210
|
+
${img ? `<img src="${escAttr(img)}" onerror="this.outerHTML='📦'" style="width:56px;height:56px;border-radius:6px;object-fit:cover;flex-shrink:0">` : `<div style="width:56px;height:56px;border-radius:6px;background:#f3f4f6;display:flex;align-items:center;justify-content:center;font-size:24px;flex-shrink:0">${getCategoryIcon(p.category)}</div>`}
|
|
1211
1211
|
<div style="flex:1;min-width:0">
|
|
1212
1212
|
<div style="font-size:13px;line-height:1.5">
|
|
1213
1213
|
${feedActor(p.seller_id, p.seller_name, p.seller_handle)} ${t('上架了')} <strong>${escHtml(p.title)}</strong>
|
|
1214
1214
|
</div>
|
|
1215
|
-
<div style="font-size:11px;color:#9ca3af;margin-top:4px">${p.price}
|
|
1215
|
+
<div style="font-size:11px;color:#9ca3af;margin-top:4px">${window.fmtPrice(p.price)} · ${ts}</div>
|
|
1216
1216
|
</div>
|
|
1217
1217
|
</div>`
|
|
1218
1218
|
}).join('')
|
|
@@ -1249,7 +1249,7 @@ function renderSearchResults(products, banner, q) {
|
|
|
1249
1249
|
<div class="product-img">${getCategoryIcon(p.category)}</div>
|
|
1250
1250
|
<div class="product-body">
|
|
1251
1251
|
<div class="product-name">${escHtml(p.title)}</div>
|
|
1252
|
-
<div class="product-price">${p.price}
|
|
1252
|
+
<div class="product-price">${window.fmtPrice(p.price)}</div>
|
|
1253
1253
|
<div class="product-seller">${repBadge(p.rep_level)}@${escHtml(p.seller_name)}</div>
|
|
1254
1254
|
</div>
|
|
1255
1255
|
</div>`).join('')}
|
|
@@ -1267,7 +1267,7 @@ window.doSearch = async () => {
|
|
|
1267
1267
|
const products = resp.products || []
|
|
1268
1268
|
const m = resp.matched_by
|
|
1269
1269
|
const ext = resp.extracted || {}
|
|
1270
|
-
const plat = ext.platform ? `${ext.platform}` : '外部平台'
|
|
1270
|
+
const plat = ext.platform ? `${ext.platform}` : t('外部平台')
|
|
1271
1271
|
let banner = ''
|
|
1272
1272
|
if (m === 'external_id') banner = `<div class="alert alert-success" style="margin-bottom:12px">✓ ${t('通过')} ${plat} ${t('商品 ID 精确匹配到')} ${products.length} ${t('件')}</div>`
|
|
1273
1273
|
else if (m === 'external_title_exact') banner = `<div class="alert alert-success" style="margin-bottom:12px">✓ ${t('通过外链标题完全匹配到')} ${products.length} ${t('件')}</div>`
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// External source-platform links on the product detail page (renderBuyPage in app.js). UI ONLY.
|
|
2
|
+
// Shows a "前往源平台查看详情" jump button. Verified links only; unverified links are hidden until verified.
|
|
3
|
+
// SECURITY: only http/https URLs are ever made clickable (blocks javascript:/data:/relative); the target
|
|
4
|
+
// domain is shown so the buyer sees where they go; links open with target=_blank rel="noopener noreferrer".
|
|
5
|
+
// Data: GET /api/products/:id/external-links (buyer-facing; NOT owner-gated; returns { links } already
|
|
6
|
+
// filtered to verified + non-revoked). Read-only; no writes.
|
|
7
|
+
|
|
8
|
+
// Return a URL object only for http/https, else null (blocks javascript:/data:/mailto:/relative).
|
|
9
|
+
window.safeExternalUrl = (u) => {
|
|
10
|
+
try { const url = new URL(String(u)); return (url.protocol === 'http:' || url.protocol === 'https:') ? url : null } catch { return null }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Placeholder + self-scheduled hydrate (mirrors productImageGallery's setTimeout pattern in app.js).
|
|
14
|
+
window.extLinksBarHtml = (productId) => {
|
|
15
|
+
setTimeout(() => { if (window.hydrateExtLinks) window.hydrateExtLinks(productId) }, 0)
|
|
16
|
+
return `<div id="ext-links-${productId}"></div>`
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
window.hydrateExtLinks = async (productId) => {
|
|
20
|
+
const box = document.getElementById('ext-links-' + productId)
|
|
21
|
+
if (!box) return
|
|
22
|
+
// Buyer-facing endpoint returns { links } = VERIFIED + non-revoked only (server-enforced, public-safe subset).
|
|
23
|
+
const r = await GET(`/products/${productId}/external-links`).catch(() => null)
|
|
24
|
+
const links = (r && r.links) || []
|
|
25
|
+
const rows = links.map(l => {
|
|
26
|
+
const url = window.safeExternalUrl(l.url)
|
|
27
|
+
if (!url) return '' // non-http(s) → never render as a link
|
|
28
|
+
const host = escHtml(url.hostname)
|
|
29
|
+
return `<a href="${escHtml(url.href)}" target="_blank" rel="noopener noreferrer" style="display:inline-flex;align-items:center;gap:6px;font-size:12px;color:#2563eb;background:#eff6ff;border:1px solid #bfdbfe;border-radius:8px;padding:6px 10px;text-decoration:none">🔗 ${t('前往源平台查看详情')} · ${host}</a>`
|
|
30
|
+
}).filter(Boolean)
|
|
31
|
+
if (rows.length) box.innerHTML = `<div style="display:flex;flex-wrap:wrap;gap:6px;margin:8px 0">${rows.join('')}</div>`
|
|
32
|
+
}
|
|
@@ -53,7 +53,7 @@ async function renderListingsHome(app) {
|
|
|
53
53
|
</div>
|
|
54
54
|
</div>
|
|
55
55
|
<div style="text-align:right">
|
|
56
|
-
${it.min_price != null ? `<div style="color:#dc2626;font-weight:700;font-size:15px">${
|
|
56
|
+
${it.min_price != null ? `<div style="color:#dc2626;font-weight:700;font-size:15px">${window.fmtPrice(it.min_price)}</div><div style="font-size:10px;color:#9ca3af">${t('起')}</div>` : `<div style="font-size:11px;color:#9ca3af">${t('暂无报价')}</div>`}
|
|
57
57
|
</div>
|
|
58
58
|
</div>
|
|
59
59
|
</div>
|
|
@@ -114,11 +114,11 @@ async function renderListingsMine(app) {
|
|
|
114
114
|
</div>
|
|
115
115
|
<div style="display:flex;justify-content:space-between;align-items:center;gap:8px;padding-top:8px;border-top:1px solid #f3f4f6">
|
|
116
116
|
<div style="font-size:11px;color:#6b7280">
|
|
117
|
-
${t('我的报价')} <strong style="color:#374151">${
|
|
117
|
+
${t('我的报价')} <strong style="color:#374151">${window.fmtPrice(myMin)}</strong>
|
|
118
118
|
<span style="color:#9ca3af">· ${it.my_offer_count} ${t('个规格')}</span>
|
|
119
119
|
</div>
|
|
120
120
|
<div style="font-size:11px;color:#6b7280">
|
|
121
|
-
${t('全网最低')} <strong style="color:#dc2626">${
|
|
121
|
+
${t('全网最低')} <strong style="color:#dc2626">${window.fmtPrice(globalMin)}</strong>
|
|
122
122
|
</div>
|
|
123
123
|
</div>
|
|
124
124
|
</div>
|
|
@@ -212,7 +212,7 @@ async function renderListingDetail(app, id) {
|
|
|
212
212
|
${isMine ? `<div style="margin-top:6px"><button class="btn btn-outline btn-sm" style="padding:3px 8px;font-size:10px;color:#4f46e5;border-color:#c7d2fe" onclick="refreshOfferFreshness('${o.id}','${id}')">🔄 ${t('现货确认')}</button></div>` : ''}
|
|
213
213
|
</div>
|
|
214
214
|
<div style="text-align:right">
|
|
215
|
-
<div style="color:#dc2626;font-weight:700;font-size:16px">${
|
|
215
|
+
<div style="color:#dc2626;font-weight:700;font-size:16px">${window.fmtPrice(o.price)}</div>
|
|
216
216
|
<div style="font-size:10px;color:#9ca3af">${t('库存')} ${o.stock}</div>
|
|
217
217
|
${state.user?.role === 'buyer' && o.stock > 0 ? `<button class="btn btn-primary btn-sm" style="margin-top:6px;padding:4px 12px;font-size:11px" onclick="location.hash='#order-product/${o.id}'">${t('购买')}</button>` : ''}
|
|
218
218
|
</div>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// [PRELAUNCH-WAZ-SIM] 预发布期 escrow 模拟币提醒。预发布期 escrow 用模拟 WAZ(非真钱),
|
|
2
|
+
// 为防买家把测试币当真实价值,做三件事 —— 全部门控在 window._wazSimulated:
|
|
3
|
+
// 1) 支付方式不再默认预选 escrow(app-direct-pay.js: dpRailSelectorHtml 的 checked + dpSelectedRail 兜底)
|
|
4
|
+
// 2) 买家切到 escrow 时内联提醒(wazEscrowRailNote 注入 #dp-rail-block,app-direct-pay.js 无需扩容)
|
|
5
|
+
// 3) 生成的 escrow 订单在买家端订单详情顶部强提醒(wazEscrowOrderBanner,app.js renderOrderDetail 调用)
|
|
6
|
+
// 真实结算上线时:把下面 _wazSimulated 置 false(一处)→ 全部 no-op、escrow 预选行为恢复。
|
|
7
|
+
// 彻底移除:grep '[PRELAUNCH-WAZ-SIM]' / '_wazSimulated'(本文件 + app.js + app-direct-pay.js + index.html
|
|
8
|
+
// + package.json check:pwa-syntax + .github/workflows/ci.yml + complexity-ratchet-guard.ts + i18n.js)。
|
|
9
|
+
// UI-only:不碰 wallet/escrow/settlement/订单钱路;纯展示 + 一个"必须显式选 rail"的前端门(后端不变)。
|
|
10
|
+
window._wazSimulated = true
|
|
11
|
+
|
|
12
|
+
// 内联提醒文案(单一真相,两处复用)。
|
|
13
|
+
const WAZ_SIM_NOTE = () => '⚠️ ' + t('测试模式:WAZ 为模拟货币,不代表真实价值。此为预发布演示,你"托管"的并非真实资金,确认收货后释放的也是测试币。')
|
|
14
|
+
|
|
15
|
+
// 买家在"支付方式"里选中 Escrow 时的内联提醒。selectedRail = 当前选中的 rail 值('escrow' / 'direct_p2p' / undefined)。
|
|
16
|
+
// 注入到 #dp-rail-block 内,避免扩容 app-direct-pay.js(其已到 LOC 上限)。非模拟期直接 no-op。
|
|
17
|
+
window.wazEscrowRailNote = (selectedRail) => {
|
|
18
|
+
if (!window._wazSimulated) return
|
|
19
|
+
const block = document.getElementById('dp-rail-block'); if (!block) return
|
|
20
|
+
const host = block.querySelector('div') || block
|
|
21
|
+
let el = document.getElementById('dp-rail-escrow-sim')
|
|
22
|
+
if (selectedRail !== 'escrow') { if (el) el.style.display = 'none'; return }
|
|
23
|
+
if (!el) {
|
|
24
|
+
el = document.createElement('div'); el.id = 'dp-rail-escrow-sim'
|
|
25
|
+
el.style.cssText = 'margin-top:8px;font-size:11px;line-height:1.6;color:#92400e;background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:8px 10px'
|
|
26
|
+
host.appendChild(el)
|
|
27
|
+
}
|
|
28
|
+
el.textContent = WAZ_SIM_NOTE()
|
|
29
|
+
el.style.display = ''
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 生成 escrow 订单后,买家端订单详情顶部的强提醒横幅。非 escrow / 非买家 / 非模拟 → 空串(不渲染)。
|
|
33
|
+
window.wazEscrowOrderBanner = (order, isBuyer) => {
|
|
34
|
+
if (!window._wazSimulated || !isBuyer || !order || order.payment_rail === 'direct_p2p') return ''
|
|
35
|
+
return `<div style="border:1px solid #fde68a;background:linear-gradient(135deg,#fffbeb,#fef3c7);border-radius:12px;padding:12px 14px;margin-bottom:10px">
|
|
36
|
+
<div style="font-size:13px;font-weight:700;color:#92400e;margin-bottom:4px">🧪 ${t('测试托管订单')}</div>
|
|
37
|
+
<div style="font-size:12px;line-height:1.6;color:#374151">${t('本单为预发布演示:WAZ 是模拟货币,不代表真实价值,本协议尚未进行真实资金结算。请勿据此向任何人支付真实款项。')}</div>
|
|
38
|
+
</div>`
|
|
39
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Buyer price display (DISPLAY-ONLY) — render a product price in USDC with a real-time local-currency hint.
|
|
2
|
+
// fmtPrice(usdc) → a <span data-usdc-price> wrapper whose content is "<amount> USDC" + (when live rates + a
|
|
3
|
+
// non-USD buyer currency are available) a muted "≈ <local>". USDC is the display/pricing unit (money.ts:
|
|
4
|
+
// 1 WAZ = 1 USDC = 1e6 base-units, USDC-aligned); this NEVER implies WebAZ holds/settles real USDC —
|
|
5
|
+
// escrow is still simulated WAZ, direct-pay is off-platform.
|
|
6
|
+
// Local currency is derived from the buyer's account region (no manual picker). Rates come from /api/fx/rates
|
|
7
|
+
// (cached + fallback server-side). If rates are unavailable the price simply shows USDC — display never breaks.
|
|
8
|
+
// First-paint race: rates load async; when they arrive, refreshFxPrices() re-renders already-painted price
|
|
9
|
+
// nodes ([data-usdc-price] / [data-usdc-local]) so the "≈ local" hint appears without a full re-render.
|
|
10
|
+
window._fxRates = null
|
|
11
|
+
window.loadFxRates = async () => {
|
|
12
|
+
try {
|
|
13
|
+
const res = await fetch('/api/fx/rates', { signal: AbortSignal.timeout(6000) })
|
|
14
|
+
if (res.ok) { window._fxRates = await res.json(); window.refreshFxPrices() }
|
|
15
|
+
} catch { /* leave null → prices show USDC-only */ }
|
|
16
|
+
}
|
|
17
|
+
window.buyerCurrency = () => {
|
|
18
|
+
const map = { china: 'CNY', us: 'USD', eu: 'EUR', india: 'INR', singapore: 'SGD', global_north: 'USD', global: 'USD' }
|
|
19
|
+
const region = (window.state && window.state.user && window.state.user.region) || ''
|
|
20
|
+
return map[String(region).toLowerCase()] || 'USD'
|
|
21
|
+
}
|
|
22
|
+
// usdc amount → localized "≈ ¥X" string, or '' (USD buyer / no rates / unusable rate → show USDC only).
|
|
23
|
+
window._fxLocal = (usdc) => {
|
|
24
|
+
const r = window._fxRates
|
|
25
|
+
const cur = window.buyerCurrency()
|
|
26
|
+
if (!r || !r.rates || cur === 'USD') return ''
|
|
27
|
+
const rate = Number(r.rates[cur])
|
|
28
|
+
const n = Number(usdc)
|
|
29
|
+
if (!(rate > 0) || !Number.isFinite(n)) return ''
|
|
30
|
+
const local = n * rate
|
|
31
|
+
const sym = { CNY: '¥', EUR: '€', INR: '₹', SGD: 'S$', USD: '$', IDR: 'Rp', MYR: 'RM', PHP: '₱', VND: '₫', THB: '฿' }[cur] || (cur + ' ')
|
|
32
|
+
return sym + (local >= 100 ? String(Math.round(local)) : local.toFixed(2))
|
|
33
|
+
}
|
|
34
|
+
// visible price content (no wrapper): "<amount> USDC" + optional muted "≈ local".
|
|
35
|
+
window._fxPriceInner = (usdc) => {
|
|
36
|
+
const n = Number(usdc)
|
|
37
|
+
const disp = Number.isFinite(n) ? (Number.isInteger(n) ? String(n) : n.toFixed(2)) : ''
|
|
38
|
+
const base = disp + ' USDC'
|
|
39
|
+
const loc = window._fxLocal(usdc)
|
|
40
|
+
return loc ? base + ` <span style="opacity:.6;font-weight:400;font-size:.85em;white-space:nowrap">≈ ${loc}</span>` : base
|
|
41
|
+
}
|
|
42
|
+
// price (USDC decimal) → display HTML. data-usdc-price lets refreshFxPrices() update it once live rates arrive.
|
|
43
|
+
window.fmtPrice = (usdc) => `<span data-usdc-price="${Number(usdc)}">${window._fxPriceInner(usdc)}</span>`
|
|
44
|
+
// re-render already-painted price nodes after rates load (fixes the first-paint race). Safe no-op if no nodes.
|
|
45
|
+
window.refreshFxPrices = () => {
|
|
46
|
+
const doc = window.document
|
|
47
|
+
if (!doc || !doc.querySelectorAll) return
|
|
48
|
+
doc.querySelectorAll('[data-usdc-price]').forEach((el) => { el.innerHTML = window._fxPriceInner(el.getAttribute('data-usdc-price')) })
|
|
49
|
+
doc.querySelectorAll('[data-usdc-local]').forEach((el) => {
|
|
50
|
+
const p = el.getAttribute('data-usdc-local'); const loc = window._fxLocal(p)
|
|
51
|
+
el.innerHTML = 'USDC' + (loc ? ' ≈ ' + loc : '')
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
// fire-and-forget at load; rates are global (no auth), so they can load before the user object is known.
|
|
55
|
+
window.loadFxRates()
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Shared product-image resolver for cards / lists (paired with the public thumb endpoint).
|
|
2
|
+
// productThumbSrc(images): a stored image ref → a loadable <img src>. A 64-hex content hash → the public
|
|
3
|
+
// thumbnail endpoint /api/manifests/<hash>/thumb; an already-usable data:/http(s)/root-relative URL →
|
|
4
|
+
// passthrough; anything else / empty → '' (no image). `images` may be a JSON-array string, a real array,
|
|
5
|
+
// or a legacy CSV string. Card <img>s pair this with onerror="this.outerHTML='📦'" for the 404 (missing
|
|
6
|
+
// thumbnail manifest) fallback, so a bad ref degrades to the icon instead of a broken image.
|
|
7
|
+
window.productThumbSrc = (images) => {
|
|
8
|
+
let arr = images
|
|
9
|
+
if (typeof images === 'string') { try { arr = JSON.parse(images) } catch { arr = images.split(',') } }
|
|
10
|
+
const first = (Array.isArray(arr) ? arr : []).map(s => String(s).trim()).filter(Boolean)[0] || ''
|
|
11
|
+
if (!first) return ''
|
|
12
|
+
if (/^[0-9a-f]{64}$/i.test(first)) return '/api/manifests/' + first + '/thumb'
|
|
13
|
+
if (/^(https?:|data:|\/)/i.test(first)) return first
|
|
14
|
+
return ''
|
|
15
|
+
}
|