@seasonkoh/webaz 0.1.28 → 0.1.30
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/bond-refund-blockers.js +50 -0
- package/dist/bond-slash.js +100 -0
- package/dist/bond-terms.js +31 -0
- 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 +99 -0
- package/dist/direct-pay-cancel-refund.js +160 -0
- package/dist/direct-pay-compliance-ingress.js +145 -0
- package/dist/direct-pay-controls.js +136 -0
- package/dist/direct-pay-create.js +231 -0
- package/dist/direct-pay-deferral-quota.js +43 -0
- package/dist/direct-pay-disclosures.js +50 -0
- package/dist/direct-pay-eligibility.js +46 -0
- package/dist/direct-pay-fee-ar.js +241 -0
- package/dist/direct-pay-fee-prepay-request.js +80 -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-pay-returns.js +104 -0
- package/dist/direct-pay-stock.js +9 -0
- package/dist/direct-receive-account-qr.js +77 -0
- package/dist/direct-receive-accounts.js +82 -0
- package/dist/direct-receive-deferral.js +171 -0
- package/dist/direct-receive-deposits.js +365 -0
- package/dist/direct-receive-payment-instruction.js +26 -0
- package/dist/free-shipping.js +37 -0
- package/dist/fx-rates.js +78 -0
- package/dist/layer0-foundation/L0-1-database/schema.js +777 -1
- package/dist/layer0-foundation/L0-2-state-machine/engine.js +1 -0
- package/dist/layer0-foundation/L0-2-state-machine/genuine-sale.js +15 -5
- package/dist/layer0-foundation/L0-2-state-machine/transitions.js +114 -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 +99 -32
- package/dist/layer2-business/L2-6-notifications/notification-engine.js +110 -41
- package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +133 -31
- package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +16 -4
- package/dist/layer3-trust/L3-1-dispute-engine/mutual-cancel.js +156 -0
- 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/platform-receive-accounts.js +94 -0
- package/dist/product-verification.js +93 -0
- package/dist/pwa/acp-feed.js +3 -2
- package/dist/pwa/arbitration-read-admin.js +37 -0
- package/dist/pwa/arbitrator-lifecycle.js +100 -0
- package/dist/pwa/contract-fingerprint.js +18 -0
- package/dist/pwa/direct-pay-guards.js +20 -0
- package/dist/pwa/direct-pay-order-redaction.js +42 -0
- package/dist/pwa/endpoint-actions.js +3 -0
- package/dist/pwa/human-presence.js +2 -6
- package/dist/pwa/public/app-account.js +9 -9
- package/dist/pwa/public/app-admin-disputes.js +55 -0
- package/dist/pwa/public/app-agent-appeal.js +90 -0
- package/dist/pwa/public/app-agent-approvals.js +93 -0
- package/dist/pwa/public/app-agent-pair.js +127 -0
- package/dist/pwa/public/app-ai.js +10 -10
- package/dist/pwa/public/app-arbitrator-admin.js +87 -0
- package/dist/pwa/public/app-arbitrator-entry.js +9 -0
- package/dist/pwa/public/app-bond-deferral-ui.js +9 -0
- package/dist/pwa/public/app-bond-refund-ui.js +66 -0
- package/dist/pwa/public/app-bond-slash-ui.js +74 -0
- package/dist/pwa/public/app-bond-terms-ui.js +23 -0
- package/dist/pwa/public/app-bond-ui.js +108 -0
- package/dist/pwa/public/app-chat-poll.js +29 -0
- package/dist/pwa/public/app-contribution-hub.js +23 -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-cancel-refund.js +72 -0
- package/dist/pwa/public/app-direct-pay-compliance.js +67 -0
- package/dist/pwa/public/app-direct-pay-copy.js +12 -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-history.js +39 -0
- package/dist/pwa/public/app-direct-pay-fee-ops.js +112 -0
- package/dist/pwa/public/app-direct-pay-fee-request.js +81 -0
- package/dist/pwa/public/app-direct-pay-fee-requests-admin.js +70 -0
- package/dist/pwa/public/app-direct-pay-memo.js +14 -0
- package/dist/pwa/public/app-direct-pay-negotiation.js +35 -0
- package/dist/pwa/public/app-direct-pay-pay.js +15 -0
- package/dist/pwa/public/app-direct-pay-paymodal.js +32 -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-reconcile.js +19 -0
- package/dist/pwa/public/app-direct-pay-returns.js +56 -0
- package/dist/pwa/public/app-direct-pay-reveal.js +82 -0
- package/dist/pwa/public/app-direct-pay-sales-report.js +70 -0
- package/dist/pwa/public/app-direct-pay-store-verify.js +100 -0
- package/dist/pwa/public/app-direct-pay.js +226 -0
- package/dist/pwa/public/app-discover.js +20 -20
- package/dist/pwa/public/app-dispute-close-ui.js +38 -0
- package/dist/pwa/public/app-external-links.js +32 -0
- package/dist/pwa/public/app-free-shipping-ui.js +29 -0
- package/dist/pwa/public/app-gmv-rail-split.js +12 -0
- package/dist/pwa/public/app-listing-commerce-ui.js +72 -0
- package/dist/pwa/public/app-listings.js +4 -4
- package/dist/pwa/public/app-mutual-cancel.js +54 -0
- package/dist/pwa/public/app-notif-templates-orders.js +43 -0
- package/dist/pwa/public/app-notif-templates.js +22 -0
- package/dist/pwa/public/app-order-accept-ui.js +158 -0
- package/dist/pwa/public/app-order-errors.js +50 -0
- package/dist/pwa/public/app-order-labels.js +19 -0
- package/dist/pwa/public/app-order-rail-filter.js +26 -0
- package/dist/pwa/public/app-platform-receive-accounts.js +140 -0
- package/dist/pwa/public/app-poll-governor.js +22 -0
- 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 +10 -10
- package/dist/pwa/public/app-purchase-terms-ui.js +68 -0
- package/dist/pwa/public/app-sale-regions-ui.js +38 -0
- package/dist/pwa/public/app-seller.js +5 -5
- package/dist/pwa/public/app-shop.js +19 -19
- package/dist/pwa/public/app-trade-tax-ui.js +33 -0
- package/dist/pwa/public/app.js +264 -295
- package/dist/pwa/public/i18n.js +1068 -197
- package/dist/pwa/public/index.html +58 -0
- package/dist/pwa/public/openapi.json +1845 -653
- package/dist/pwa/public/style.css +3 -0
- package/dist/pwa/routes/admin-analytics.js +4 -2
- package/dist/pwa/routes/admin-direct-receive-deposits.js +533 -0
- package/dist/pwa/routes/admin-protocol-params.js +16 -0
- package/dist/pwa/routes/admin-reports.js +31 -5
- package/dist/pwa/routes/agent-governance.js +1 -1
- package/dist/pwa/routes/agent-grants.js +253 -32
- package/dist/pwa/routes/analytics.js +2 -0
- package/dist/pwa/routes/arbitrator.js +67 -14
- package/dist/pwa/routes/bond-seller.js +162 -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-cancel-refund.js +111 -0
- package/dist/pwa/routes/direct-pay-disclosure-acks.js +79 -0
- package/dist/pwa/routes/direct-pay-pending-accept.js +222 -0
- package/dist/pwa/routes/direct-pay-returns.js +74 -0
- package/dist/pwa/routes/direct-pay-timeouts.js +248 -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/disputes-read.js +20 -6
- package/dist/pwa/routes/disputes-write.js +91 -33
- package/dist/pwa/routes/external-anchors.js +4 -2
- package/dist/pwa/routes/fee-prepay-requests.js +66 -0
- package/dist/pwa/routes/fx.js +12 -0
- package/dist/pwa/routes/governance-onboarding.js +46 -8
- package/dist/pwa/routes/leaderboard.js +47 -9
- package/dist/pwa/routes/listings.js +2 -2
- package/dist/pwa/routes/logistics.js +6 -2
- package/dist/pwa/routes/manifests.js +38 -0
- package/dist/pwa/routes/me-data.js +5 -2
- package/dist/pwa/routes/mutual-cancel.js +62 -0
- package/dist/pwa/routes/orders-action.js +297 -16
- package/dist/pwa/routes/orders-create.js +21 -6
- package/dist/pwa/routes/orders-read.js +106 -8
- package/dist/pwa/routes/p2p-products.js +2 -2
- package/dist/pwa/routes/platform-receive-accounts.js +111 -0
- package/dist/pwa/routes/products-create.js +15 -4
- 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 +37 -3
- package/dist/pwa/routes/profile-identity.js +4 -2
- package/dist/pwa/routes/promoter.js +3 -0
- package/dist/pwa/routes/referral.js +4 -0
- package/dist/pwa/routes/returns.js +60 -4
- 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/seller-directpay-report.js +110 -0
- package/dist/pwa/routes/seller-quota.js +5 -1
- package/dist/pwa/routes/shipping-templates.js +219 -0
- package/dist/pwa/routes/shops.js +2 -1
- package/dist/pwa/routes/snf.js +4 -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 +3 -3
- package/dist/pwa/server.js +112 -132
- package/dist/runtime/agent-grant-scopes.js +69 -1
- package/dist/runtime/webaz-schema-helpers.js +161 -3
- package/dist/sale-regions.js +116 -0
- package/dist/shipping-templates.js +119 -0
- package/dist/store-verification.js +77 -0
- package/dist/trade-tax.js +99 -0
- package/dist/trade-terms.js +76 -0
- package/dist/version.js +1 -1
- package/package.json +128 -2
|
@@ -0,0 +1,226 @@
|
|
|
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] || (window.orderErrorLookup && window.orderErrorLookup(code)) || fallback || t('操作失败,请重试') // 回退查订单错误码表(NOT_ORDER_BUYER 等披露/动作码)
|
|
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
|
+
// 【先落订单页,融合付款弹窗叠在其上】:订单页本身有收款信息+对账卡+自动隐藏倒计时,所以无论用户点按钮、点遮罩还是
|
|
85
|
+
// 按 ESC 关掉弹窗,都留在订单页而非产品页 —— 杜绝"关弹窗后失联→重复下单"(审计:弹窗被 overlay/ESC 关会 strand)。
|
|
86
|
+
const o = await GET(`/orders/${orderId}`)
|
|
87
|
+
navigate(`#order/${orderId}`)
|
|
88
|
+
if (o && o.order && window.dpShowPaymentModal) setTimeout(() => window.dpShowPaymentModal(o.order), 120) // 等订单页渲染后叠弹窗
|
|
89
|
+
}
|
|
90
|
+
// ── 确保两次披露都已 ack。两屏文本各自展示确认(证据不减);缺两个 → 一次 Passkey ceremony 覆盖(stage:'both',首单
|
|
91
|
+
// 3→2,2026-07-04 决策),缺一个 → 单独 ceremony。会话缓存 _dpAcked:both 过的单热路径(mark_paid/confirm)不再重复 2 GET;
|
|
92
|
+
// 后端 requireBothDisclosuresAcked 仍硬门。懒取金额:仅 D2 将展示才 GET 订单。──
|
|
93
|
+
window.dpEnsureAcks = async (orderId) => {
|
|
94
|
+
if (window._dpAcked && window._dpAcked[orderId]) return true
|
|
95
|
+
const st = await GET(`/direct-pay/disclosure-acks/${orderId}`)
|
|
96
|
+
if (st.error) { if (typeof toast$ === 'function') toast$(window.dpErrorText(st.error_code, st.error), 'error'); return false }
|
|
97
|
+
const cache = () => { (window._dpAcked = window._dpAcked || {})[orderId] = true; return true }
|
|
98
|
+
if (st.both === true) return cache()
|
|
99
|
+
const disc = st.disclosures || {}
|
|
100
|
+
const missing = ['pre_select', 'pre_confirm'].filter(k => !(st.acked && st.acked[k]))
|
|
101
|
+
let _pay = ''
|
|
102
|
+
if (missing.includes('pre_confirm')) { try { const _od = await GET(`/orders/${orderId}`); if (_od && _od.order) _pay = window.dpPayAmountText(_od.order) } catch {} }
|
|
103
|
+
for (let i = 0; i < missing.length; i++) {
|
|
104
|
+
const key = missing[i], tx = key === 'pre_select' ? disc.pre_select : disc.pre_confirm
|
|
105
|
+
const body = (tx ? (window._lang === 'en' ? tx.en : tx.zh) : t('风险披露')) + (key === 'pre_confirm' && _pay ? `\n\n💸 ${_pay}` : '')
|
|
106
|
+
const go = await confirmModal(`${body}\n\n${t('我已阅读并理解上述风险')}`, i === missing.length - 1 ? t('了解直接付款(需 Passkey)') : t('下一步'), { danger: true })
|
|
107
|
+
if (!go) return false
|
|
108
|
+
}
|
|
109
|
+
const ok = await window.dpDoAck(orderId, missing.length === 2 ? 'both' : missing[0])
|
|
110
|
+
return ok ? cache() : false
|
|
111
|
+
}
|
|
112
|
+
// Passkey 门失败:仅【确实未注册】(后端 NO_PASSKEY_REGISTERED,由 requestPasskeyGate 透传到 err.code)才引导注册;
|
|
113
|
+
// 已注册但取消/设备不支持/超时(含 navigator DOMException,其 .code 是 legacy 数字非本值)→ 仅本地化"请重试",
|
|
114
|
+
// 不导注册(修用户反馈:已注册者被重复提醒)、不把英文 DOMException 文案 toast 给中文用户(bilingual)。全部 5 调用点传 err 对象。
|
|
115
|
+
window.dpPromptRegisterPasskey = async (err) => {
|
|
116
|
+
if (!(err && typeof err === 'object' && err.code === 'NO_PASSKEY_REGISTERED')) { if (typeof toast$ === 'function') toast$(t('验证未完成,请重试'), 'info'); return }
|
|
117
|
+
if (await confirmModal(t('直付的风险确认与付款标记需要 Passkey。你还没有注册 Passkey,前往「我的 → 安全与存储」注册一个?'), t('前往注册 Passkey'), {})) navigate('#me')
|
|
118
|
+
}
|
|
119
|
+
// 单次披露 ack:Passkey ceremony(purpose=direct_pay_disclosure_ack,order+stage 绑 purpose_data)→ POST ack。
|
|
120
|
+
window.dpDoAck = async (orderId, stage) => {
|
|
121
|
+
let token
|
|
122
|
+
try { token = await requestPasskeyGate('direct_pay_disclosure_ack', { order_id: orderId, stage }) }
|
|
123
|
+
catch (e) { await window.dpPromptRegisterPasskey(e); return false }
|
|
124
|
+
const r = await POST('/direct-pay/disclosure-acks', { order_id: orderId, stage, webauthn_token: token })
|
|
125
|
+
if (r.error) { if (typeof toast$ === 'function') toast$(window.dpErrorText(r.error_code, r.error), 'error'); return false }
|
|
126
|
+
return true
|
|
127
|
+
}
|
|
128
|
+
// ── 订单详情:direct_p2p 诚实边界始终显示;卖家收款说明快照【不】内联进 HTML,由 dpHydrateOrderDisclosure 在 both-acked 后才另取渲染(未 ack 时 DOM 里也没有快照)。
|
|
129
|
+
window.dpOrderDisclosureHtml = (order) => `
|
|
130
|
+
<div class="card" style="border:1px solid #fde68a;background:linear-gradient(135deg,#fffbeb,#fef3c7)">
|
|
131
|
+
<div style="font-size:13px;font-weight:700;color:#92400e;margin-bottom:8px">💸 ${t('直付订单(非托管)')}</div>
|
|
132
|
+
<ul style="margin:0 0 8px;padding-left:18px;font-size:12px;line-height:1.7;color:#374151">
|
|
133
|
+
<li>${t('本金不经 WebAZ —— 你直接付款给卖家(场外)')}</li>
|
|
134
|
+
<li>${t('非担保交易:WebAZ 不托管本金、无退款能力,仅对卖家有信誉处罚权')}</li>
|
|
135
|
+
<li>${t('卖家收款说明来自卖家自填,WebAZ 不验证付款方式或币种')}</li>
|
|
136
|
+
</ul>
|
|
137
|
+
<div id="dp-order-instr" data-order-id="${escHtml(String(order && order.id || ''))}">${loading$()}</div>
|
|
138
|
+
</div>`
|
|
139
|
+
// ack-gated 收款说明:both-acked → 另取订单快照并展示;否则只显示"先完成 D1/D2 Passkey"的门(快照不入 DOM)。
|
|
140
|
+
window.dpHydrateOrderDisclosure = async (orderId) => {
|
|
141
|
+
if (!window.dpInstrBox(orderId)) return
|
|
142
|
+
const st = await GET(`/direct-pay/disclosure-acks/${orderId}`)
|
|
143
|
+
const box = window.dpInstrBox(orderId); if (!box) return // async 回包后再确认当前页仍是该订单(切页后旧回包不得写 DOM)
|
|
144
|
+
if (st.error) { box.innerHTML = `<div style="font-size:12px;color:#dc2626">${window.dpErrorText(st.error_code, st.error)}</div>`; return }
|
|
145
|
+
if (!st.both) {
|
|
146
|
+
box.innerHTML = `<div style="font-size:12px;color:#92400e;background:#fff;border:1px solid #fde68a;border-radius:8px;padding:8px 10px">
|
|
147
|
+
${t('需先完成 D1/D2 Passkey 风险确认,确认后才显示卖家收款说明。')}
|
|
148
|
+
<div style="margin-top:6px"><button class="btn btn-outline btn-sm" onclick="dpCompleteAcksThenReveal('${orderId}')">${t('完成风险确认')}</button></div></div>`
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
const o = await GET(`/orders/${orderId}`)
|
|
152
|
+
if (window.dpRenderPaymentInfo) window.dpRenderPaymentInfo(box, o && o.order ? o.order : null, orderId) // PR-2:按订单状态渲染收款信息可见性(待支付=5min 自动窗口 / 其它状态=默认隐藏,需 Passkey 二次验证+风险提示)
|
|
153
|
+
}
|
|
154
|
+
window.dpCompleteAcksThenReveal = async (orderId) => {
|
|
155
|
+
const ok = await window.dpEnsureAcks(orderId)
|
|
156
|
+
if (ok) window.dpHydrateOrderDisclosure(orderId)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── 订单动作(mark_paid/confirm/confirm_in_person):前端先确保披露 ack,再取一次性 Passkey token 带进 order-action 端点;后端(#93)仍强制两次披露+Passkey。
|
|
160
|
+
window.dpHandleAction = async (orderId, action) => {
|
|
161
|
+
const msgEl = document.getElementById('action-msg')
|
|
162
|
+
const show = (type, m) => { if (msgEl) msgEl.innerHTML = alert$(type, m); else if (typeof toast$ === 'function') toast$(m, type) }
|
|
163
|
+
// mark_paid 前必须两次披露都 ack(confirm/confirm_in_person 通常已在 mark_paid 阶段 ack 过,这里兜底再查)。
|
|
164
|
+
const acked = await window.dpEnsureAcks(orderId)
|
|
165
|
+
if (!acked) { show('error', window.dpErrorText('DISCLOSURE_NOT_ACKED')); return }
|
|
166
|
+
let token
|
|
167
|
+
try { token = await requestPasskeyGate('direct_pay_order_action', { order_id: orderId, action }) }
|
|
168
|
+
catch (e) { await window.dpPromptRegisterPasskey(e); return }
|
|
169
|
+
show('info', `<span class="spinner"></span>${t('处理中...')}`)
|
|
170
|
+
const path = action === 'confirm_in_person' ? `/orders/${orderId}/confirm-in-person` : `/orders/${orderId}/action`
|
|
171
|
+
const body = action === 'confirm_in_person' ? { webauthn_token: token } : { action, webauthn_token: token, ...(action === 'mark_paid' && window.dpReadMemo ? { notes: window.dpReadMemo(orderId) } : {}) }
|
|
172
|
+
const r = await POST(path, body)
|
|
173
|
+
if (r.error) { show('error', window.dpErrorText(r.error_code, r.error)); return }
|
|
174
|
+
show('success', t('操作成功'))
|
|
175
|
+
setTimeout(() => renderOrderDetail(document.getElementById('app'), orderId), 1000)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ── 卖家:收款说明设置(GET / PUT / DELETE,仅展示文本)─────────────────────────────────────────
|
|
179
|
+
window.dpSellerInstructionSection = () => `
|
|
180
|
+
<div class="card" style="margin-bottom:12px">
|
|
181
|
+
<div style="font-size:14px;font-weight:700;margin-bottom:6px">💳 ${t('直付收款说明')}</div>
|
|
182
|
+
<div style="font-size:12px;color:#6b7280;line-height:1.6;margin-bottom:10px">${t('这是你自填的收款展示文本(场外结算用);WebAZ 不验证付款方式或币种,不路由支付,不托管资金。')}</div>
|
|
183
|
+
<div id="dp-instr-box">${loading$()}</div>
|
|
184
|
+
</div>`
|
|
185
|
+
|
|
186
|
+
window.dpHydrateInstruction = async () => {
|
|
187
|
+
const box = document.getElementById('dp-instr-box')
|
|
188
|
+
if (!box) return
|
|
189
|
+
const r = await GET('/direct-receive/payment-instruction')
|
|
190
|
+
if (r.error) { box.innerHTML = alert$('error', window.dpErrorText(r.error_code, r.error)); return }
|
|
191
|
+
const cur = r.instruction
|
|
192
|
+
box.innerHTML = `
|
|
193
|
+
${cur ? `<div style="font-size:12px;color:#374151;background:#f9fafb;border:1px solid #e5e7eb;border-radius:8px;padding:8px 10px;margin-bottom:10px">
|
|
194
|
+
<div style="font-size:11px;color:#9ca3af;margin-bottom:2px">${t('当前生效')}${cur.label ? ` · ${escHtml(cur.label)}` : ''}</div>${escHtml(cur.instruction)}</div>` :
|
|
195
|
+
`<div style="font-size:12px;color:#9ca3af;margin-bottom:10px">${t('尚未设置收款说明')}</div>`}
|
|
196
|
+
<div id="dp-instr-msg"></div>
|
|
197
|
+
<div class="form-group"><label class="form-label">${t('收款说明')} <span style="font-size:11px;color:#9ca3af">${t('(展示给买家,如 PayNow / 银行转账 等)')}</span></label>
|
|
198
|
+
<textarea class="form-control" id="dp-instr-text" rows="3" maxlength="500" placeholder="${t('例:PayNow +65 9xxx(场外结算)')}">${cur ? escHtml(cur.instruction) : ''}</textarea></div>
|
|
199
|
+
<div class="form-group"><label class="form-label">${t('标签(可选)')}</label>
|
|
200
|
+
<input class="form-control" id="dp-instr-label" maxlength="40" value="${cur && cur.label ? escHtml(cur.label) : ''}" placeholder="${t('如 PayNow')}"></div>
|
|
201
|
+
<div style="display:flex;gap:8px">
|
|
202
|
+
<button class="btn btn-primary btn-sm" onclick="dpSaveInstruction()">${t('保存')}</button>
|
|
203
|
+
${cur ? `<button class="btn btn-outline btn-sm" style="color:#dc2626;border-color:#dc2626" onclick="dpDeleteInstruction()">${t('停用')}</button>` : ''}
|
|
204
|
+
</div>`
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
window.dpSaveInstruction = async () => {
|
|
208
|
+
const msg = document.getElementById('dp-instr-msg')
|
|
209
|
+
const instruction = document.getElementById('dp-instr-text')?.value?.trim() || ''
|
|
210
|
+
const label = document.getElementById('dp-instr-label')?.value?.trim() || ''
|
|
211
|
+
if (!instruction) { if (msg) msg.innerHTML = alert$('error', t('收款说明不能为空')); return }
|
|
212
|
+
if (msg) msg.innerHTML = `<div class="alert alert-info"><span class="spinner"></span>${t('保存中...')}</div>`
|
|
213
|
+
const r = await PUT('/direct-receive/payment-instruction', { instruction, label })
|
|
214
|
+
if (r.error) { if (msg) msg.innerHTML = alert$('error', window.dpErrorText(r.error_code, r.error)); return }
|
|
215
|
+
if (typeof toast$ === 'function') toast$(t('已保存'), 'success')
|
|
216
|
+
window.dpHydrateInstruction()
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
window.dpDeleteInstruction = async () => {
|
|
220
|
+
const go = await confirmModal(t('停用后买家将无法对你发起直付订单,确定停用?'), t('停用'), { danger: true })
|
|
221
|
+
if (!go) return
|
|
222
|
+
const r = await api('DELETE', '/direct-receive/payment-instruction')
|
|
223
|
+
if (r.error) { if (typeof toast$ === 'function') toast$(window.dpErrorText(r.error_code, r.error), 'error'); return }
|
|
224
|
+
if (typeof toast$ === 'function') toast$(t('已停用'), 'success')
|
|
225
|
+
window.dpHydrateInstruction()
|
|
226
|
+
}
|
|
@@ -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,38 @@
|
|
|
1
|
+
// 争议协商收口·买家侧 —— "我已收到货 · 撤诉并确认收货"。UI ONLY(权威门在 orders-action dispute_withdraw_confirm:
|
|
2
|
+
// delivered 来源履约争议 + 发起人本人 + 裁定前;dp 轨 D1/D2+Passkey)。可达性来自订单 DTO can_confirm_receipt_close_dispute。
|
|
3
|
+
// 注入:包装 mutualCancelCard(协商取消卡下追加;卖家侧收口=协商取消全额退款,买家侧=本卡),app.js 净零行。
|
|
4
|
+
;(function () {
|
|
5
|
+
const S = window._notifSub
|
|
6
|
+
Object.assign(window.NOTIF_TEMPLATES, {
|
|
7
|
+
dispute_withdrawn_confirmed: (p) => ({ title: '✅ ' + t('买家已撤诉并确认收货'), body: S(t('买家撤回争议并确认收货,订单已完成结算,双方信誉不受影响。'), p) }),
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
const closeCard = (order) => {
|
|
11
|
+
if (!order || !order.can_confirm_receipt_close_dispute) return ''
|
|
12
|
+
const settleNote = order.payment_rail === 'direct_p2p'
|
|
13
|
+
? t('确认后订单完成:货款你已线下支付,平台服务费照常计提。需 Passkey 确认。')
|
|
14
|
+
: t('确认后订单完成:托管货款将释放给卖家。')
|
|
15
|
+
return `<div class="card" style="border:1px solid #bbf7d0;background:linear-gradient(135deg,#f0fdf4,#dcfce7)">
|
|
16
|
+
<div style="font-size:13px;font-weight:700;color:#166534;margin-bottom:4px">📦 ${t('后来收到货了?')}</div>
|
|
17
|
+
<div style="font-size:12px;color:#4b5563;line-height:1.7;margin-bottom:8px">${t('包裹晚到/在代收点找到了?你可以撤回这条争议并确认收货,订单正常完成,双方信誉都不受影响。')}${settleNote}</div>
|
|
18
|
+
<button class="btn btn-primary btn-sm" style="width:auto" onclick="dcWithdrawConfirm('${order.id}','${order.payment_rail === 'direct_p2p' ? 1 : ''}')">${t('我已收到货 · 撤诉并确认收货')}</button>
|
|
19
|
+
</div>`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
window.dcWithdrawConfirm = async (oid, isDp) => {
|
|
23
|
+
if (typeof confirmModal === 'function' && !(await confirmModal(t('确认撤回争议并确认收货?争议将关闭(不可恢复),订单立即完成结算。'), t('撤诉并确认收货'), { danger: true }))) return
|
|
24
|
+
const body = { action: 'dispute_withdraw_confirm' }
|
|
25
|
+
if (isDp) {
|
|
26
|
+
try { body.webauthn_token = await requestPasskeyGate('direct_pay_order_action', { order_id: oid, action: 'dispute_withdraw_confirm' }) }
|
|
27
|
+
catch (e) { if (window.dpPromptRegisterPasskey) await window.dpPromptRegisterPasskey(e); return }
|
|
28
|
+
}
|
|
29
|
+
const r = await POST('/orders/' + oid + '/action', body)
|
|
30
|
+
if (r.error) return void toast$(r.error, 'error')
|
|
31
|
+
toast$(t('已撤诉并确认收货,订单完成'), 'success')
|
|
32
|
+
renderOrderDetail(document.getElementById('app'), oid)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 包装协商取消卡:disputed 订单页在其后追加买家收口卡(app.js 调用点不变,净零行)
|
|
36
|
+
const _origMcCard = window.mutualCancelCard
|
|
37
|
+
window.mutualCancelCard = (order, isBuyer, isSeller) => (_origMcCard ? _origMcCard(order, isBuyer, isSeller) : '') + (isBuyer ? closeCard(order) : '')
|
|
38
|
+
})()
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// 满额免邮 UI(营销域,S2 返工)。UI ONLY —— 校验/判免全在后端(free-shipping.ts,建单 gate 应用)。入口:卖家后台「营销」tab 卡(app.js 净零 hook);点开就地 sheet 配店铺阈值,单品覆盖走 API。语义:促销非运费结构,券后货款≥阈值运费商家承担;供应商报价期规则不搬家。
|
|
2
|
+
;(function () {
|
|
3
|
+
window.freeShippingMarketingCard = () => `
|
|
4
|
+
<div onclick="fsOpenSheet()" class="card" style="padding:14px;cursor:pointer;background:linear-gradient(135deg,#ecfeff,#cffafe);border-color:#67e8f9">
|
|
5
|
+
<div style="font-size:24px">🚚</div>
|
|
6
|
+
<div style="font-weight:600;font-size:13px;color:#155e75;margin-top:6px">${t('满额免邮')}</div>
|
|
7
|
+
<div style="font-size:10px;color:#0e7490;margin-top:2px">${t('拉客单价 · 运费你承担')}</div>
|
|
8
|
+
</div>`
|
|
9
|
+
window.fsOpenSheet = async () => {
|
|
10
|
+
const s = await GET('/seller/shipping-settings').catch(() => null)
|
|
11
|
+
const cur = s && s.store_free_shipping_threshold
|
|
12
|
+
const html = `
|
|
13
|
+
<div style="font-size:14px;font-weight:700;margin-bottom:6px">🚚 ${t('满额免邮(店铺默认)')}</div>
|
|
14
|
+
<div style="font-size:12px;color:#6b7280;margin-bottom:10px">${t('买家券后货款(不含保险/捐赠)达到阈值时,本单运费免收、由你承担。人工询价订单不适用;单品可经接口单独覆盖;只影响之后的新订单。')}</div>
|
|
15
|
+
<input class="form-control" id="fs-threshold" type="number" min="0" step="0.01" placeholder="${t('阈值金额(留空=关闭)')}" value="${cur ?? ''}" style="margin-bottom:10px">
|
|
16
|
+
<button class="btn btn-primary btn-sm" style="width:auto" onclick="fsSave()">${t('保存免邮设置')}</button>
|
|
17
|
+
<div id="fs-msg" style="margin-top:6px;font-size:11px"></div>`
|
|
18
|
+
if (typeof openSheet === 'function') openSheet(html)
|
|
19
|
+
else { const d = document.createElement('div'); d.className = 'card js-sheet'; d.style.cssText = 'position:fixed;left:50%;top:20%;transform:translateX(-50%);z-index:120;max-width:360px;width:92%'; d.innerHTML = html + `<button class="btn btn-outline btn-sm" style="width:auto;margin-top:8px" onclick="this.closest('.js-sheet').remove()">${t('关闭')}</button>`; document.body.appendChild(d) }
|
|
20
|
+
}
|
|
21
|
+
window.fsSave = async () => {
|
|
22
|
+
const raw = (document.getElementById('fs-threshold')?.value || '').trim()
|
|
23
|
+
const msg = document.getElementById('fs-msg')
|
|
24
|
+
if (raw !== '' && !(Number.isFinite(Number(raw)) && Number(raw) > 0)) { if (msg) msg.innerHTML = `<span style="color:#dc2626">${t('阈值必须是正数')}</span>`; return } // 审计 P3:1e999→Infinity→JSON null→静默清除,前端就地拒
|
|
25
|
+
const r = await POST('/seller/shipping-template', { store_free_shipping_threshold: raw === '' ? null : Number(raw) })
|
|
26
|
+
if (r.error) { if (msg) msg.innerHTML = `<span style="color:#dc2626">${escHtml(r.error)}</span>`; return }
|
|
27
|
+
if (msg) msg.innerHTML = `<span style="color:#16a34a">✓ ${t('已保存')}${raw === '' ? ' · ' + t('已关闭') : ''}</span>`
|
|
28
|
+
}
|
|
29
|
+
})()
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// GMV 按支付轨拆分小注(卖家仪表盘共用)。UI ONLY。
|
|
2
|
+
// 托管(escrow)= 平台真实托管收入;直接收款(direct_p2p)= 场外收款,平台不经手 —— 二者不同性质,不该混成一个 GMV。
|
|
3
|
+
// 仅当【有直接收款】时显示(纯托管卖家不打扰);金额沿用调用方的计价显示。
|
|
4
|
+
;(function () {
|
|
5
|
+
window.gmvRailSplitHtml = (escrow, directPay, fmt) => {
|
|
6
|
+
const d = Number(directPay) || 0
|
|
7
|
+
if (d <= 0) return '' // 无直接收款 → 不显示,GMV 就是纯托管
|
|
8
|
+
const e = Number(escrow) || 0
|
|
9
|
+
const f = typeof fmt === 'function' ? fmt : (n) => Number(n || 0).toFixed(0)
|
|
10
|
+
return `<div style="font-size:10px;color:#6b7280;margin-top:3px;line-height:1.5" title="${t('托管=平台托管收入;直接收款=场外收款,平台不经手')}">🏦 ${t('托管')} ${f(e)} · 🤝 ${t('直接收款')} ${f(d)}</div>`
|
|
11
|
+
}
|
|
12
|
+
})()
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// 单链接商务覆盖(S4)—— 上架/编辑表单里按【单个商品】覆盖店铺默认:接单/运费/询价/可售区/满额免邮/税费进口责任/清关字段。
|
|
2
|
+
// UI ONLY,写全部复用已验证端点(POST /seller/accept-mode + /seller/shipping-template + PUT /products/:id)。
|
|
3
|
+
// "留空=继承店铺默认"(值 null 清除该商品覆盖);编辑态从已取到的 product 行(formatProductForAgent 透传各覆盖列)回填。
|
|
4
|
+
// app.js 净零:add/edit 表单各注入一个 section hook + 保存时调 window.listingCommerceSave(productId)。
|
|
5
|
+
;(function () {
|
|
6
|
+
const J = (x) => { try { return typeof x === 'string' ? JSON.parse(x) : (x || null) } catch { return null } }
|
|
7
|
+
const tplText = (arr) => (Array.isArray(arr) ? arr : []).map(e => `${e.region} ${e.fee}${e.est_days ? ' ' + e.est_days : ''}`).join('\n')
|
|
8
|
+
const taxText = (arr) => (Array.isArray(arr) ? arr : []).map(l => `${l.region} ${l.label}${l.rate_pct != null ? ' ' + l.rate_pct : ''}`).join('\n')
|
|
9
|
+
const srCodes = (r) => r ? (r.mode === 'list' ? (r.include || []) : (r.exclude || [])).join(' ') : ''
|
|
10
|
+
|
|
11
|
+
// p = 商品对象(编辑)或 null(新建)。所有字段 id 前缀 lc-。
|
|
12
|
+
window.listingCommerceSectionHtml = (p) => {
|
|
13
|
+
p = p || {}
|
|
14
|
+
const sr = J(p.sale_regions)
|
|
15
|
+
const sel = (id, cur, opts) => `<select class="form-control" id="${id}" style="font-size:13px">${opts.map(([v, lbl]) => `<option value="${v}" ${String(cur ?? '') === v ? 'selected' : ''}>${lbl}</option>`).join('')}</select>`
|
|
16
|
+
return `<details style="margin-bottom:16px">
|
|
17
|
+
<summary style="font-size:13px;color:#6b7280;cursor:pointer;padding:4px 0">🌍 ${t('本商品:接单 / 运费 / 可售区 / 税费(留空=用店铺默认)')}</summary>
|
|
18
|
+
<div style="margin-top:12px;display:flex;flex-direction:column;gap:10px">
|
|
19
|
+
<div><label class="form-label" style="font-size:12px">${t('接单模式')}</label>${sel('lc-accept', p.accept_mode, [['', t('继承店铺默认')], ['auto', t('自动接单')], ['manual', t('手动接单')]])}</div>
|
|
20
|
+
<div><label class="form-label" style="font-size:12px">${t('运费模板(每行:地区码 运费 [时效];* 兜底;留空=继承)')}</label><textarea class="form-control" id="lc-tpl" rows="2" style="font-size:12px;font-family:monospace" placeholder="SG 5 3-5 * 25 10-20">${escHtml(tplText(J(p.shipping_template)))}</textarea></div>
|
|
21
|
+
<div><label class="form-label" style="font-size:12px">${t('模板外地区询价(直付)')}</label>${sel('lc-quote', p.shipping_quote_ok == null ? '' : (Number(p.shipping_quote_ok) === 1 ? '1' : '0'), [['', t('继承店铺默认')], ['1', t('开')], ['0', t('关')]])}</div>
|
|
22
|
+
<div style="display:flex;gap:8px">
|
|
23
|
+
<div style="flex:1"><label class="form-label" style="font-size:12px">${t('可售区域')}</label>${sel('lc-sr-mode', sr ? sr.mode : '', [['', t('继承/不限')], ['list', t('仅允许')], ['all', t('全球除外')]])}</div>
|
|
24
|
+
<div style="flex:1"><label class="form-label" style="font-size:12px">${t('地区码(空格分隔)')}</label><input class="form-control" id="lc-sr-codes" style="font-size:12px;font-family:monospace" value="${escHtml(srCodes(sr))}"></div>
|
|
25
|
+
</div>
|
|
26
|
+
<div style="display:flex;gap:8px">
|
|
27
|
+
<div style="flex:1"><label class="form-label" style="font-size:12px">${t('满额免邮阈值')}</label><input class="form-control" id="lc-free" type="number" min="0" step="0.01" placeholder="${t('继承')}" value="${p.free_shipping_threshold ?? ''}"></div>
|
|
28
|
+
<div style="flex:1"><label class="form-label" style="font-size:12px">${t('进口责任')}</label>${sel('lc-duty', p.import_duty_terms, [['', t('继承/不声明')], ['ddu', 'DDU'], ['ddp', 'DDP']])}</div>
|
|
29
|
+
</div>
|
|
30
|
+
<div><label class="form-label" style="font-size:12px">${t('价内已含税(每行:地区码 税名 [税率%];留空=继承)')}</label><textarea class="form-control" id="lc-tax" rows="2" style="font-size:12px;font-family:monospace" placeholder="SG GST 9">${escHtml(taxText(J(p.tax_lines)))}</textarea></div>
|
|
31
|
+
<div style="border-top:1px dashed #e5e7eb;padding-top:8px;font-size:11px;color:#9ca3af">${t('跨境清关字段(证据用,可选)')}</div>
|
|
32
|
+
<div style="display:flex;gap:8px">
|
|
33
|
+
<input class="form-control" id="lc-weight" type="number" min="0" step="0.01" placeholder="${t('重量 kg')}" value="${p.weight_kg ?? ''}" style="font-size:12px">
|
|
34
|
+
<input class="form-control" id="lc-pkg" placeholder="${t('尺寸 长x宽x高cm')}" value="${escHtml(p.package_size || '')}" style="font-size:12px">
|
|
35
|
+
</div>
|
|
36
|
+
<div style="display:flex;gap:8px">
|
|
37
|
+
<input class="form-control" id="lc-origin" placeholder="${t('发货国码')}" value="${escHtml(p.origin_country || '')}" style="font-size:12px;text-transform:uppercase">
|
|
38
|
+
<input class="form-control" id="lc-coo" placeholder="${t('原产国码')}" value="${escHtml(p.country_of_origin || '')}" style="font-size:12px;text-transform:uppercase">
|
|
39
|
+
<input class="form-control" id="lc-hs" placeholder="${t('HS 编码')}" value="${escHtml(p.hs_code || '')}" style="font-size:12px">
|
|
40
|
+
</div>
|
|
41
|
+
<input class="form-control" id="lc-customs-desc" placeholder="${t('报关英文品名')}" value="${escHtml(p.customs_description || '')}" style="font-size:12px">
|
|
42
|
+
</div></details>`
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const LC_FIELDS = ['lc-accept', 'lc-tpl', 'lc-quote', 'lc-sr-mode', 'lc-free', 'lc-duty', 'lc-tax', 'lc-weight', 'lc-pkg', 'lc-origin', 'lc-coo', 'lc-hs', 'lc-customs-desc']
|
|
46
|
+
window.listingCommerceHasOverrides = () => LC_FIELDS.some(id => ((document.getElementById(id)?.value) || '').trim() !== '') // 新建时据此决定"仓库优先"(有覆盖→先落仓库,全落定再激活)
|
|
47
|
+
// 收集并保存(新建/编辑同用;productId 必须已存在)。留空=null=清除该商品覆盖。复用已验证端点,任一报错即冒泡。opts.activate=全成功后激活(仓库优先新品的收尾)。
|
|
48
|
+
window.listingCommerceSave = async (productId, opts) => {
|
|
49
|
+
if (!productId || !document.getElementById('lc-accept')) return { ok: true } // section 未渲染 → 跳过
|
|
50
|
+
const v = (id) => (document.getElementById(id)?.value || '').trim()
|
|
51
|
+
const parseTpl = (txt) => txt.split('\n').map(l => l.trim()).filter(Boolean).map(l => { const m = l.split(/\s+/); const est = m.slice(2).join(' '); return { region: m[0], fee: Number(m[1]), ...(est ? { est_days: est } : {}) } })
|
|
52
|
+
const parseTax = (txt) => txt.split('\n').map(l => l.trim()).filter(Boolean).map(l => { const m = l.split(/\s+/); const region = m[0]; const last = m[m.length - 1]; const hasRate = m.length > 2 && /^[0-9.]+$/.test(last); const label = m.slice(1, hasRate ? -1 : undefined).join(' '); return { region, label, ...(hasRate ? { rate_pct: Number(last) } : {}), kind: 'included' } }) // 空格安全:label 可含空格(如 Sales Tax);末 token 为数字才当税率
|
|
53
|
+
const srMode = v('lc-sr-mode'); const srCodesArr = v('lc-sr-codes').split(/\s+/).filter(Boolean)
|
|
54
|
+
const tplTxt = v('lc-tpl'); const taxTxt = v('lc-tax'); const q = v('lc-quote'); const fr = v('lc-free')
|
|
55
|
+
const steps = [ // 顺序 fail-fast:校验最重的运费/税费 body 先行 → 它被拒时接单/清关都还没写,避免部分保存(各字段独立无跨字段不变量)
|
|
56
|
+
() => POST('/seller/shipping-template', {
|
|
57
|
+
product_id: productId,
|
|
58
|
+
template: tplTxt ? parseTpl(tplTxt) : null,
|
|
59
|
+
quote_ok: q === '' ? null : q === '1',
|
|
60
|
+
sale_regions: srMode ? { mode: srMode, ...(srMode === 'list' ? { include: srCodesArr } : { exclude: srCodesArr }) } : null,
|
|
61
|
+
free_shipping_threshold: fr === '' ? null : Number(fr),
|
|
62
|
+
import_duty_terms: v('lc-duty') || null,
|
|
63
|
+
tax_lines: taxTxt ? parseTax(taxTxt) : null,
|
|
64
|
+
}),
|
|
65
|
+
() => POST('/seller/accept-mode', { product_id: productId, accept_mode: v('lc-accept') || null }),
|
|
66
|
+
() => PUT(`/products/${productId}`, { weight_kg: v('lc-weight') ? Number(v('lc-weight')) : null, package_size: v('lc-pkg') || null, origin_country: v('lc-origin') || null, country_of_origin: v('lc-coo') || null, customs_description: v('lc-customs-desc') || null, hs_code: v('lc-hs') || null }),
|
|
67
|
+
]
|
|
68
|
+
for (const step of steps) { const r = await step(); if (r && r.error) return { ok: false, error: r.error } }
|
|
69
|
+
if (opts && opts.activate) { const a = await PATCH(`/products/${productId}/status`, { status: 'active' }); if (a && a.error) return { ok: false, error: a.error } } // 仓库优先新品:覆盖全落定 → 激活;激活失败也算失败(商品留仓库=不可售,安全)
|
|
70
|
+
return { ok: true }
|
|
71
|
+
}
|
|
72
|
+
})()
|