@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,108 @@
|
|
|
1
|
+
// 商家履约保证金 UI(B1 缴纳闭环)。UI ONLY —— 双锁/凭据/幂等全在后端。
|
|
2
|
+
// ① 通知模板(申报/确认/驳回);② 卖家 settings 状态卡(要求额度/状态/缓交/申报表单——通道未放行时隐藏表单,诚实提示);
|
|
3
|
+
// ③ admin 保证金申报队列页(#admin/bond-deposits:核对到账→确认[ROOT+Passkey,当前被 Lock B 挡属预期]/驳回)。
|
|
4
|
+
;(function () {
|
|
5
|
+
const S = window._notifSub
|
|
6
|
+
const P = (emoji, titleZh, bodyZh) => (p) => ({ title: emoji + ' ' + t(titleZh), body: S(t(bodyZh), p) })
|
|
7
|
+
Object.assign(window.NOTIF_TEMPLATES, {
|
|
8
|
+
bond_deposit_submitted: P('🏦', '新保证金缴纳申报待核实', '卖家 {seller} 申报已缴纳履约保证金(T0,凭据 {evidence})。请核对真实到账后在 admin 后台确认(ROOT+Passkey)。'),
|
|
9
|
+
bond_deposit_confirmed: P('✅', '履约保证金已确认锁定', '你的保证金已核实到账并正式锁定,直付入场的保证金门已满足。退出时可申请退还(须无未了结直付责任)。'),
|
|
10
|
+
bond_deposit_rejected: P('❌', '保证金申报未通过核实', '你的保证金缴纳申报未通过核实{note}。请核对付款凭据后重新提交,或联系平台。'),
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
const ST = () => ({ pending: t('待运营核实'), confirmed: t('已核实待锁定'), locked: t('已锁定生效'), insufficient: t('金额不足待补缴'), expired: t('已失效/被驳回'), refunding: t('退还中'), refunded: t('已退还'), slashed: t('已被罚没') })
|
|
14
|
+
|
|
15
|
+
// ② 卖家 settings 状态卡(settings 链尾注入 + hydrate)
|
|
16
|
+
window.bondSellerSection = () => `
|
|
17
|
+
<div class="card" id="bond-card">
|
|
18
|
+
<div style="font-size:14px;font-weight:700;margin-bottom:6px">🏦 ${t('直付履约保证金')}</div>
|
|
19
|
+
<div id="bond-body" style="font-size:12px;color:#6b7280">${loading$()}</div>
|
|
20
|
+
</div>`
|
|
21
|
+
window.bondHydrateSeller = async () => {
|
|
22
|
+
const box = document.getElementById('bond-body'); if (!box) return
|
|
23
|
+
const s = await GET('/direct-receive/bond-status').catch(() => null)
|
|
24
|
+
if (!s) { box.textContent = t('加载失败,请刷新'); return }
|
|
25
|
+
const d = s.deposit
|
|
26
|
+
const statusLine = d
|
|
27
|
+
? `<div style="margin-bottom:6px">${t('当前状态')}:<strong>${ST()[d.status] || d.status}</strong>${d.status === 'pending' ? ` · ${t('凭据')} ${escHtml(d.evidence_ref || '-')}` : ''}${d.reject_note ? `<br><span style="color:#dc2626">${t('驳回说明')}:${escHtml(d.reject_note)}</span>` : ''}</div>`
|
|
28
|
+
: `<div style="margin-bottom:6px">${t('当前状态')}:<strong>${t('未缴纳')}</strong></div>`
|
|
29
|
+
const deferralLine = s.deferral
|
|
30
|
+
? `<div style="margin-bottom:6px;color:#92400e">🕓 ${t('缓交生效中')}${s.deferral.expires_at ? ` · ${t('到期')} ${fmtTime(s.deferral.expires_at)}` : ''}(${t('额度受限;缴清保证金后转正式')})</div>` : ''
|
|
31
|
+
const payBlock = s.rail_cleared
|
|
32
|
+
? `<div style="margin:8px 0;padding:8px;background:#f9fafb;border-radius:8px">
|
|
33
|
+
<div style="font-weight:600;margin-bottom:4px">${t('缴纳方式')}(${t('转账后提交凭据申报')}):</div>
|
|
34
|
+
${window.bondAccountSelector ? window.bondAccountSelector(s) : (s.payment_accounts || []).map(a => `<div style="margin-bottom:4px">· ${escHtml(a.method || '')} ${escHtml(a.currency || '')}:${escHtml(a.instruction || '')}</div>`).join('') || t('暂无收款方式,请联系平台')}
|
|
35
|
+
</div>
|
|
36
|
+
${(!d || ['expired', 'refunded'].includes(d.status)) ? `${window.bondTermsBlock ? window.bondTermsBlock(s) : ''}
|
|
37
|
+
<input class="form-control" id="bond-evidence" maxlength="120" placeholder="${t('付款凭据号(转账单号/链上 tx,必填)')}" style="margin-bottom:8px;font-size:12px">
|
|
38
|
+
<button class="btn btn-primary btn-sm" style="width:auto" onclick="bondSubmitDeposit()">${t('提交缴纳申报')}</button>` : ''}
|
|
39
|
+
${d && d.status === 'pending' ? `<button class="btn btn-outline btn-sm" style="width:auto" onclick="bondCancelDeposit('${d.id}')">${t('撤回申报')}</button>` : ''}`
|
|
40
|
+
: `<div style="margin:8px 0;padding:8px;background:#fffbeb;border:1px solid #fde68a;border-radius:8px;color:#92400e">${escHtml(s.note || '')}</div>`
|
|
41
|
+
box.innerHTML = `
|
|
42
|
+
<div style="margin-bottom:6px">${t('要求额度')}:<strong>${s.required.display} ${s.required.currency}</strong>(${s.required.tier})</div>
|
|
43
|
+
${statusLine}${deferralLine}${payBlock}${window.bondRefundBlock ? window.bondRefundBlock(s) : ''}${window.bondSlashNotice ? window.bondSlashNotice(s) : ''}
|
|
44
|
+
<div id="bond-msg" style="margin-top:6px;font-size:11px"></div>`
|
|
45
|
+
}
|
|
46
|
+
window.bondSubmitDeposit = async () => {
|
|
47
|
+
const msg = document.getElementById('bond-msg')
|
|
48
|
+
const evidence = (document.getElementById('bond-evidence')?.value || '').trim()
|
|
49
|
+
if (!evidence) { msg.innerHTML = `<span style="color:#dc2626">${t('付款凭据号必填')}</span>`; return }
|
|
50
|
+
const r = await POST('/direct-receive/bond-deposit', { evidence_ref: evidence, platform_account_id: (document.querySelector('input[name="bond-acc"]:checked')?.value || ''), agree_terms_version: (document.getElementById('bond-terms-agree')?.checked ? (window._bondTermsVersion || '') : '') })
|
|
51
|
+
if (r.error) { msg.innerHTML = `<span style="color:#dc2626">${r.error}</span>`; return }
|
|
52
|
+
toast$(t('申报已提交,等待运营核实'), 'success'); window.bondHydrateSeller()
|
|
53
|
+
}
|
|
54
|
+
window.bondCancelDeposit = async (id) => {
|
|
55
|
+
if (typeof confirmModal === 'function' && !(await confirmModal(t('确认撤回保证金缴纳申报?'), t('撤回申报'), { danger: true }))) return
|
|
56
|
+
const r = await POST(`/direct-receive/bond-deposit/${id}/cancel`, {})
|
|
57
|
+
if (r.error) return void toast$(r.error, 'error')
|
|
58
|
+
window.bondHydrateSeller()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ③ admin 申报队列页(#admin/bond-deposits;确认走既有 confirm-production 端点 —— Lock B 未放行前 409 属预期,页面明示)
|
|
62
|
+
window.renderAdminBondDeposits = function (app) {
|
|
63
|
+
if (!state.user) { renderLogin(); return }
|
|
64
|
+
if (typeof isAdmin === 'function' && !isAdmin()) { app.innerHTML = shell(`<div class="alert alert-info">${t('仅限管理员')}</div>`, 'admin'); return }
|
|
65
|
+
if ((state.user.admin_type || 'root') !== 'root') { app.innerHTML = shell(`<div class="alert alert-info">${t('仅限根管理员')}</div>`, 'admin'); return }
|
|
66
|
+
app.innerHTML = shell(`
|
|
67
|
+
<h1 class="page-title">🏦 ${t('保证金缴纳申报')}</h1>
|
|
68
|
+
<div style="margin-bottom:8px"><button class="btn btn-outline btn-sm" style="width:auto" onclick="navigate('#admin/dp-ops')">${t('返回 Direct Pay 商户运营')}</button></div>
|
|
69
|
+
<div style="font-size:12px;color:#6b7280;line-height:1.6;margin-bottom:10px">${t('卖家的履约保证金缴纳申报。请【核对凭据对应的真实到账】后确认(ROOT+Passkey,双锁:生产放行 registry 未开时确认会被拒,属预期 fail-closed);核不上则驳回(留说明)。')}</div>
|
|
70
|
+
<div id="bond-adm-box">${loading$()}</div>
|
|
71
|
+
`, 'admin')
|
|
72
|
+
window.bondAdmHydrate()
|
|
73
|
+
}
|
|
74
|
+
window.bondAdmHydrate = async (status) => {
|
|
75
|
+
const box = document.getElementById('bond-adm-box'); if (!box) return
|
|
76
|
+
const r = await GET(`/admin/direct-receive/deposits${status ? `?status=${status}` : ''}`).catch(() => null)
|
|
77
|
+
if (!r || !Array.isArray(r.deposits)) { box.textContent = t('加载失败,请刷新'); return }
|
|
78
|
+
if (r.deposits.length === 0) { box.innerHTML = `<div class="alert alert-info">${t('暂无申报')}</div>`; return }
|
|
79
|
+
box.innerHTML = r.deposits.map(d => `
|
|
80
|
+
<div class="card" style="font-size:12px">
|
|
81
|
+
<div style="display:flex;justify-content:space-between;gap:8px;flex-wrap:wrap">
|
|
82
|
+
<div><strong>${escHtml(d.seller_name || d.user_id)}</strong> ${d.seller_handle ? '@' + escHtml(d.seller_handle) : ''} · ${d.tier} · ${t('要求')} ${d.required_amount} ${String(d.currency).toUpperCase()}</div>
|
|
83
|
+
<div><strong>${ST()[d.status] || d.status}</strong></div>
|
|
84
|
+
</div>
|
|
85
|
+
<div style="color:#6b7280;margin-top:4px">${t('申报单')} ${d.id} · ${t('凭据')}:${escHtml(d.external_ref || '-')} · ${fmtTime(d.created_at)}${d.reject_note ? ` · <span style="color:#dc2626">${escHtml(d.reject_note)}</span>` : ''}</div>${window.bondAdmRefundActions ? window.bondAdmRefundActions(d) : ''}
|
|
86
|
+
${d.status === 'pending' ? `
|
|
87
|
+
<div style="display:flex;gap:8px;margin-top:8px;flex-wrap:wrap">
|
|
88
|
+
<button class="btn btn-primary btn-sm" style="width:auto;font-size:11px" onclick="bondAdmConfirm('${d.id}', ${Math.round(Number(d.required_amount) * 1e6)}, '${escHtml(d.external_ref || '')}')">${t('已核实到账,确认锁定(Passkey)')}</button>
|
|
89
|
+
<button class="btn btn-outline btn-sm" style="width:auto;font-size:11px;color:#dc2626;border-color:#fecaca" onclick="bondAdmReject('${d.id}')">${t('核不上,驳回')}</button>
|
|
90
|
+
</div>` : ''}
|
|
91
|
+
</div>`).join('')
|
|
92
|
+
}
|
|
93
|
+
window.bondAdmConfirm = async (id, amountUnits, receiptRef) => {
|
|
94
|
+
const jurisdiction = prompt(t('法域代码(须在放行白名单内,如 SG)'), 'SG'); if (!jurisdiction) return
|
|
95
|
+
let token
|
|
96
|
+
try { token = await requestPasskeyGate('direct_receive_production_confirm', { deposit_id: id, rail_id: 'operator_attested', amount_units: amountUnits, receipt_ref: receiptRef, jurisdiction }) }
|
|
97
|
+
catch (e) { if (window.dpPromptRegisterPasskey) await window.dpPromptRegisterPasskey(e); return }
|
|
98
|
+
const r = await POST(`/admin/direct-receive/deposits/${id}/confirm-production`, { rail_id: 'operator_attested', expected_amount_units: amountUnits, receipt_ref: receiptRef, jurisdiction, webauthn_token: token })
|
|
99
|
+
if (r.error) return void toast$(r.error_code === 'PRODUCTION_RAIL_NOT_CLEARED' ? t('生产放行 registry 未开(法务清门后翻转)—— 确认被拒属预期 fail-closed') : r.error, 'error')
|
|
100
|
+
toast$(t('已确认锁定,卖家保证金门已满足'), 'success'); window.bondAdmHydrate()
|
|
101
|
+
}
|
|
102
|
+
window.bondAdmReject = async (id) => {
|
|
103
|
+
const note = prompt(t('驳回说明(卖家可见,可空)'), '') ?? ''
|
|
104
|
+
const r = await POST(`/admin/direct-receive/deposits/${id}/reject`, { note })
|
|
105
|
+
if (r.error) return void toast$(r.error, 'error')
|
|
106
|
+
toast$(t('已驳回'), 'success'); window.bondAdmHydrate()
|
|
107
|
+
}
|
|
108
|
+
})()
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// 聊天详情实时轮询。renderChatDetail 只在进入/发消息时拉取消息(无实时推送),对方发来的消息原先停留会话时
|
|
2
|
+
// 看不到(只有通知那条独立轮询能提示)。这里每几秒拉一次会话,就地刷新 #chat-msgs:保持"已在底部则贴底"、
|
|
3
|
+
// 不碰输入框/快捷面板、查看时把新到消息标记已读。自清理:#chat-msgs 离开 DOM 或路由已不是本会话时立即停;
|
|
4
|
+
// 单计时器(window._chatPollTimer),重新进入任一会话先清旧的(不堆叠)。逻辑放独立文件因 app.js 已到 LOC 上限。后台/锁屏暂停,回前台由 app-poll-governor 经 _chatPollNow 补拉。
|
|
5
|
+
// ⚠️ 用【最后一条消息 id】判变化,不能用消息条数——后端默认只返回最近 50 条(滑动窗口),长会话里新消息进来后
|
|
6
|
+
// 条数仍是 50,用 length 比较会永远判"无变化"而不刷新/不标已读(长会话恰是最需要 live update 的场景)。
|
|
7
|
+
window.startChatPoll = (id, initialMsgs) => {
|
|
8
|
+
if (window._chatPollTimer) { clearInterval(window._chatPollTimer); window._chatPollTimer = null }
|
|
9
|
+
const lastOf = (arr) => (arr && arr.length) ? String(arr[arr.length - 1].id || arr[arr.length - 1].created_at || '') : ''
|
|
10
|
+
let last = lastOf(initialMsgs); const stop = () => { if (window._chatPollTimer) { clearInterval(window._chatPollTimer); window._chatPollTimer = null; window._chatPollNow = null } }
|
|
11
|
+
const tickFn = async () => {
|
|
12
|
+
const box = document.getElementById('chat-msgs')
|
|
13
|
+
if (!box || !location.hash.includes('chat/' + id)) return stop() // 已离开本会话 → 自清理
|
|
14
|
+
if (document.hidden) return; let rr = null // 后台/锁屏:暂停(定时器保留,回前台恢复)
|
|
15
|
+
try { rr = await GET('/conversations/' + encodeURIComponent(id)) } catch { return }
|
|
16
|
+
if (!rr || rr.error) return
|
|
17
|
+
const msgs = rr.messages || []
|
|
18
|
+
const newLast = lastOf(msgs)
|
|
19
|
+
if (newLast === last) return // 最后一条没变 → 无新消息
|
|
20
|
+
last = newLast
|
|
21
|
+
const atBottom = box.scrollHeight - box.scrollTop - box.clientHeight < 40
|
|
22
|
+
box.innerHTML = msgs.length === 0
|
|
23
|
+
? `<div style="text-align:center;color:#9ca3af;padding:30px 0;font-size:12px">${t('开始第一句问候')}</div>`
|
|
24
|
+
: msgs.map(m => window.renderChatBubble(m, state.user.id)).join('')
|
|
25
|
+
if (atBottom) box.scrollTop = box.scrollHeight // 原本贴底就继续贴底,否则不打扰阅读位置
|
|
26
|
+
POST('/conversations/' + encodeURIComponent(id) + '/read').catch(() => {}) // fire-and-forget:用 .catch 而非 try/catch(POST 返回 Promise,异步 reject 包不住)
|
|
27
|
+
}
|
|
28
|
+
window._chatPollTimer = setInterval(tickFn, 4000); window._chatPollNow = tickFn
|
|
29
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// 共建运营 admin hub —— 把散落在主 admin 面板的「共建(社区建设)」功能归到一处。UI ONLY,只链接既有路由,
|
|
2
|
+
// 不改任何后端/权限(每个子页自身的鉴权不变)。镜像 Direct Pay 商户运营 hub 的结构。
|
|
3
|
+
// 分区:共建建议收件箱(全 admin) / 建任务治理(root) / 贡献账号归属(自助 + root 审批)。中文 t(),英文 i18n.js _EN。
|
|
4
|
+
|
|
5
|
+
window.renderAdminContributionHub = function (app) {
|
|
6
|
+
if (!state.user) { renderLogin(); return }
|
|
7
|
+
if (!isAdmin()) { app.innerHTML = shell(`<div class="alert alert-info">${t('仅限管理员')}</div>`, 'admin'); return }
|
|
8
|
+
const root = (state.user.admin_type || 'root') === 'root'
|
|
9
|
+
const grp = (title, cards) => `<div style="font-size:13px;font-weight:700;color:#374151;margin:14px 0 6px">${title}</div>${cards}`
|
|
10
|
+
app.innerHTML = shell(`
|
|
11
|
+
<h1 class="page-title">🌱 ${t('共建运营')}</h1>
|
|
12
|
+
<div style="margin-bottom:8px"><button class="btn btn-outline btn-sm" style="width:auto" onclick="navigate('#admin/protocol')">${t('返回协议管理')}</button></div>
|
|
13
|
+
<div style="font-size:12px;color:#6b7280;line-height:1.6;margin-bottom:6px">${t('共建(社区建设)集中入口:外部建议收件箱、建任务治理、贡献账号归属。')}</div>
|
|
14
|
+
${grp(t('共建建议(收件箱)'),
|
|
15
|
+
adminLinkCard('🛠️', t('任务建议收件箱'), t('陌生人 / agent 提交的共建任务建议;审阅 → 转正式任务'), '#admin/task-proposals') +
|
|
16
|
+
adminLinkCard('📨', t('Welcome 提交'), t('#welcome 留下的邮箱订阅 + 建议'), '#admin/public-ideas'))}
|
|
17
|
+
${root ? grp(t('建任务治理'),
|
|
18
|
+
adminLinkCard('🎟️', t('建任务额度审核'), t('非根管理员的建任务扩容申请;批准 = 限时计数授权(仅 root)'), '#admin/quota-requests')) : ''}
|
|
19
|
+
${grp(t('贡献账号归属'),
|
|
20
|
+
adminLinkCard('🔗', t('关联个人贡献账号'), t('把本管理席位的协调贡献归属到你的真实个人账号(需对方确认 + root 审批)'), '#me/operator-claims') +
|
|
21
|
+
(root ? adminLinkCard('🪪', t('操作席位关联审批'), t('管理席位→个人贡献账号的关联申请;确认 + 审批 / 撤销(仅 root)'), '#admin/operator-claims') : ''))}
|
|
22
|
+
`, 'admin')
|
|
23
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Unified create entry — a small "全新商品 ⇄ 二手闲置" kind chooser shown at the top of BOTH create surfaces
|
|
2
|
+
// (the seller new-goods form and the secondhand publish page), so each create flow surfaces the other kind
|
|
3
|
+
// instead of being a dead-end. Mirrors UOMA's single Goods-incl.-secondhand create entry, WITHOUT merging the
|
|
4
|
+
// two backends: /api/products and /api/secondhand keep their own flows, fee rates and role rules.
|
|
5
|
+
// createKindChooserHtml(current): current ∈ {'new','secondhand'}. The active pill is inert; the other pill
|
|
6
|
+
// navigates — 'new' → goCreateListingFromBuy() (the smart seller entry: handles anon / buyer→seller
|
|
7
|
+
// upgrade / role switch), 'secondhand' → #secondhand/publish (open to any logged-in user).
|
|
8
|
+
window.createKindChooserHtml = (current) => {
|
|
9
|
+
const t = window.t || ((s) => s)
|
|
10
|
+
const pill = (active, label, onclick) => active
|
|
11
|
+
? `<span style="flex:1;text-align:center;padding:7px 0;border-radius:8px;font-size:13px;font-weight:600;background:#111827;color:#fff">${label}</span>`
|
|
12
|
+
: `<button type="button" onclick="${onclick}" style="flex:1;text-align:center;padding:7px 0;border-radius:8px;font-size:13px;font-weight:500;background:#f3f4f6;color:#374151;border:1px solid #e5e7eb;cursor:pointer">${label}</button>`
|
|
13
|
+
return `<div style="display:flex;gap:8px;margin-bottom:16px">`
|
|
14
|
+
+ pill(current === 'new', '🆕 ' + t('全新商品'), 'goCreateListingFromBuy()')
|
|
15
|
+
+ pill(current === 'secondhand', '♻️ ' + t('二手闲置'), "location.hash='#secondhand/publish'")
|
|
16
|
+
+ `</div>`
|
|
17
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// Direct Pay (Rail 1) — 卖家多收款账号 + 收款二维码 管理 UI (Phase C2)。UI ONLY。
|
|
2
|
+
// 接 Phase C1 后端 /api/direct-receive/accounts(list / add / update / deactivate + /:id/qr upload·preview)。
|
|
3
|
+
// 写操作(add/update/deactivate/qr)均需现场真人 Passkey(purpose direct_receive_account_manage,
|
|
4
|
+
// action[+account_id] 绑 purpose_data);QR 预览走 Authorization header fetch→blob(<img src> 带不了 header)。
|
|
5
|
+
// WebAZ 只存储/展示卖家自填内容与二维码字节,绝不验证/路由/托管资金,也不解析二维码。双语 parity 由 test-direct-pay-ui.ts 守。
|
|
6
|
+
|
|
7
|
+
// 账号相关 error_code → 双语文案;未命中回落到 dpErrorText(直付通用码)。
|
|
8
|
+
window.draAccountErrorText = (code, fallback) => {
|
|
9
|
+
const M = {
|
|
10
|
+
SELLER_ONLY: t('仅卖家可管理收款账号'),
|
|
11
|
+
ACCOUNT_NOT_FOUND: t('账号不存在'),
|
|
12
|
+
ACCOUNT_INPUT_INVALID: t('账号信息不合法'),
|
|
13
|
+
QR_INVALID: t('二维码图片不合法(仅支持 PNG/WebP,解码后 ≤ 64KB)'),
|
|
14
|
+
HUMAN_PRESENCE_REQUIRED: t('需现场真人 Passkey 确认'),
|
|
15
|
+
PASSKEY_REQUIRED: t('需先注册 Passkey'),
|
|
16
|
+
}
|
|
17
|
+
return M[code] || (window.dpErrorText ? window.dpErrorText(code, fallback) : (fallback || t('操作失败,请重试')))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ── 卖家设置:多收款账号面板(section + hydrate)──────────────────────────────────────────────
|
|
21
|
+
window.draAccountsSection = () => `
|
|
22
|
+
<div class="card" style="margin-bottom:12px">
|
|
23
|
+
<div style="font-size:14px;font-weight:700;margin-bottom:6px">🧾 ${t('直付收款账号')}</div>
|
|
24
|
+
<div style="font-size:12px;color:#6b7280;line-height:1.6;margin-bottom:10px">${t('你可维护多个收款账号(各自币种、可选二维码),买家直付时自选其一。WebAZ 只存储与展示,不验证付款方式或币种,不路由/托管资金,也不解析二维码。')}</div>
|
|
25
|
+
<div id="dra-accounts-box">${loading$()}</div>
|
|
26
|
+
</div>`
|
|
27
|
+
|
|
28
|
+
window.draHydrateAccounts = async () => {
|
|
29
|
+
const box = document.getElementById('dra-accounts-box')
|
|
30
|
+
if (!box) return
|
|
31
|
+
const r = await GET('/direct-receive/accounts')
|
|
32
|
+
if (r.error) { box.innerHTML = alert$('error', window.draAccountErrorText(r.error_code, r.error)); return }
|
|
33
|
+
const accounts = r.accounts || []
|
|
34
|
+
box.innerHTML = `
|
|
35
|
+
<div id="dra-msg"></div>
|
|
36
|
+
${accounts.length ? accounts.map(a => window.draAccountCard(a)).join('') : `<div style="font-size:12px;color:#9ca3af;margin-bottom:10px">${t('尚未添加收款账号')}</div>`}
|
|
37
|
+
<details style="margin-top:8px"><summary style="font-size:13px;font-weight:600;color:#2563eb;cursor:pointer">+ ${t('新增收款账号')}</summary>
|
|
38
|
+
<div style="padding:8px 2px 2px">${window.draAccountForm('new')}</div></details>`
|
|
39
|
+
window.draLoadQrThumbs(accounts)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
window.draAccountCard = (a) => {
|
|
43
|
+
const inactive = a.status !== 'active'
|
|
44
|
+
return `<div style="border:1px solid ${inactive ? '#e5e7eb' : '#d1fae5'};background:${inactive ? '#f9fafb' : '#f0fdf4'};border-radius:10px;padding:10px 12px;margin-bottom:8px;opacity:${inactive ? '0.6' : '1'}">
|
|
45
|
+
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:8px">
|
|
46
|
+
<div style="min-width:0">
|
|
47
|
+
<div style="font-size:13px;font-weight:700;color:#111827">${a.label ? escHtml(a.label) : (a.method ? escHtml(a.method) : t('收款账号'))}${inactive ? ` · <span style="color:#9ca3af;font-weight:400">${t('已停用')}</span>` : ''}</div>
|
|
48
|
+
<div style="font-size:11px;color:#6b7280;margin:2px 0 4px">${a.method ? escHtml(a.method) : ''}${a.method && a.currency ? ' · ' : ''}${a.currency ? escHtml(a.currency) : ''}</div>
|
|
49
|
+
<div style="font-size:12px;color:#374151;white-space:pre-wrap;word-break:break-word">${escHtml(a.instruction)}</div>
|
|
50
|
+
</div>
|
|
51
|
+
<div id="dra-qr-${a.id}" style="flex:0 0 auto;width:72px;height:72px;display:flex;align-items:center;justify-content:center;border:1px dashed #d1d5db;border-radius:8px;font-size:10px;color:#9ca3af;text-align:center">${a.qr_image_ref ? loading$() : t('无二维码')}</div>
|
|
52
|
+
</div>
|
|
53
|
+
${inactive ? '' : `<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:8px;align-items:center">
|
|
54
|
+
<details style="flex:0 0 auto"><summary style="font-size:12px;color:#2563eb;cursor:pointer">${t('编辑')}</summary><div style="padding:8px 2px 2px">${window.draAccountForm(a.id, a)}</div></details>
|
|
55
|
+
<label class="btn btn-outline btn-sm" style="cursor:pointer;margin:0">${a.qr_image_ref ? t('更换二维码') : t('上传二维码')}<input type="file" accept="image/png,image/webp" style="display:none" onchange="draUploadQr('${a.id}', this)"></label>
|
|
56
|
+
<button class="btn btn-outline btn-sm" style="color:#dc2626;border-color:#dc2626" onclick="draDeactivateAccount('${a.id}')">${t('停用')}</button>
|
|
57
|
+
</div>`}
|
|
58
|
+
</div>`
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 文本字段表单(id='new' 为新增;否则为该账号编辑)。QR 不在此处改 —— 由文件上传单独走 /:id/qr。
|
|
62
|
+
window.draAccountForm = (id, a) => {
|
|
63
|
+
const p = id === 'new' ? 'new' : id
|
|
64
|
+
return `
|
|
65
|
+
<div class="form-group"><label class="form-label">${t('收款说明')} <span style="font-size:11px;color:#9ca3af">${t('(展示给买家,如 PayNow / 银行转账 等)')}</span></label>
|
|
66
|
+
<textarea class="form-control" id="dra-instr-${p}" rows="3" maxlength="500" placeholder="${t('例:PayNow +65 9xxx(场外结算)')}">${a ? escHtml(a.instruction) : ''}</textarea></div>
|
|
67
|
+
<div style="display:flex;gap:8px">
|
|
68
|
+
<div class="form-group" style="flex:1"><label class="form-label">${t('收款方式')}</label><input class="form-control" id="dra-method-${p}" maxlength="40" value="${a && a.method ? escHtml(a.method) : ''}" placeholder="${t('如 PayNow')}"></div>
|
|
69
|
+
<div class="form-group" style="flex:1"><label class="form-label">${t('币种')}</label><input class="form-control" id="dra-currency-${p}" maxlength="8" value="${a && a.currency ? escHtml(a.currency) : ''}" placeholder="${t('如 SGD / USDC')}"></div>
|
|
70
|
+
</div>
|
|
71
|
+
<div class="form-group"><label class="form-label">${t('标签(可选)')}</label><input class="form-control" id="dra-label-${p}" maxlength="40" value="${a && a.label ? escHtml(a.label) : ''}" placeholder="${t('如 PayNow')}"></div>
|
|
72
|
+
<button class="btn btn-primary btn-sm" onclick="${id === 'new' ? 'draAddAccount()' : `draUpdateAccount('${id}')`}">${t('保存')}</button>`
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
window.draReadForm = (p) => ({
|
|
76
|
+
instruction: document.getElementById(`dra-instr-${p}`)?.value?.trim() || '',
|
|
77
|
+
method: document.getElementById(`dra-method-${p}`)?.value?.trim() || '',
|
|
78
|
+
currency: document.getElementById(`dra-currency-${p}`)?.value?.trim() || '',
|
|
79
|
+
label: document.getElementById(`dra-label-${p}`)?.value?.trim() || '',
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
// 现场真人 Passkey token;失败(无 Passkey / 取消 / 不支持)→ 提示注册入口并返回 null。
|
|
83
|
+
window.draGate = async (action, accountId) => {
|
|
84
|
+
try { return await requestPasskeyGate('direct_receive_account_manage', accountId ? { action, account_id: accountId } : { action }) }
|
|
85
|
+
catch (e) { if (window.dpPromptRegisterPasskey) await window.dpPromptRegisterPasskey(e); return null }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
window.draAddAccount = async () => {
|
|
89
|
+
const f = window.draReadForm('new')
|
|
90
|
+
if (!f.instruction) { if (typeof toast$ === 'function') toast$(t('收款说明不能为空'), 'error'); return }
|
|
91
|
+
const token = await window.draGate('add'); if (!token) return
|
|
92
|
+
const r = await POST('/direct-receive/accounts', { ...f, webauthn_token: token })
|
|
93
|
+
if (r.error) { if (typeof toast$ === 'function') toast$(window.draAccountErrorText(r.error_code, r.error), 'error'); return }
|
|
94
|
+
if (typeof toast$ === 'function') toast$(t('已保存'), 'success'); window.draHydrateAccounts()
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
window.draUpdateAccount = async (id) => {
|
|
98
|
+
const f = window.draReadForm(id)
|
|
99
|
+
if (!f.instruction) { if (typeof toast$ === 'function') toast$(t('收款说明不能为空'), 'error'); return }
|
|
100
|
+
const token = await window.draGate('update', id); if (!token) return
|
|
101
|
+
const r = await PUT('/direct-receive/accounts/' + id, { ...f, webauthn_token: token })
|
|
102
|
+
if (r.error) { if (typeof toast$ === 'function') toast$(window.draAccountErrorText(r.error_code, r.error), 'error'); return }
|
|
103
|
+
if (typeof toast$ === 'function') toast$(t('已保存'), 'success'); window.draHydrateAccounts()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
window.draDeactivateAccount = async (id) => {
|
|
107
|
+
const go = await confirmModal(t('停用后买家将无法选择该收款账号,确定停用?'), t('停用'), { danger: true })
|
|
108
|
+
if (!go) return
|
|
109
|
+
const token = await window.draGate('deactivate', id); if (!token) return
|
|
110
|
+
const r = await api('DELETE', '/direct-receive/accounts/' + id, { webauthn_token: token })
|
|
111
|
+
if (r.error) { if (typeof toast$ === 'function') toast$(window.draAccountErrorText(r.error_code, r.error), 'error'); return }
|
|
112
|
+
if (typeof toast$ === 'function') toast$(t('已停用'), 'success'); window.draHydrateAccounts()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 上传/更换二维码:客户端先卡类型(png|webp)+大小(≤64KB)→ 读为 data URI → Passkey → PUT。后端仍严格复核。
|
|
116
|
+
window.draUploadQr = async (id, input) => {
|
|
117
|
+
const file = input && input.files && input.files[0]; if (input) input.value = ''
|
|
118
|
+
if (!file) return
|
|
119
|
+
if (file.type !== 'image/png' && file.type !== 'image/webp') { if (typeof toast$ === 'function') toast$(t('二维码仅支持 PNG 或 WebP 图片'), 'error'); return }
|
|
120
|
+
if (file.size > 64 * 1024) { if (typeof toast$ === 'function') toast$(t('二维码图片过大(需 ≤ 64KB)'), 'error'); return }
|
|
121
|
+
let dataUri
|
|
122
|
+
try { dataUri = await new Promise((res, rej) => { const fr = new FileReader(); fr.onload = () => res(fr.result); fr.onerror = rej; fr.readAsDataURL(file) }) }
|
|
123
|
+
catch { if (typeof toast$ === 'function') toast$(t('二维码读取失败'), 'error'); return }
|
|
124
|
+
const token = await window.draGate('qr', id); if (!token) return
|
|
125
|
+
const r = await PUT('/direct-receive/accounts/' + id + '/qr', { qr_data_uri: dataUri, webauthn_token: token })
|
|
126
|
+
if (r.error) { if (typeof toast$ === 'function') toast$(window.draAccountErrorText(r.error_code, r.error), 'error'); return }
|
|
127
|
+
if (typeof toast$ === 'function') toast$(t('二维码已上传'), 'success'); window.draHydrateAccounts()
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 二维码缩略图:owner-only 端点需 Authorization header,<img src> 带不了 → fetch→blob→objectURL。
|
|
131
|
+
window.draLoadQrThumbs = (accounts) => {
|
|
132
|
+
(accounts || []).filter(a => a.qr_image_ref).forEach(a => setTimeout(async () => {
|
|
133
|
+
const el = document.getElementById('dra-qr-' + a.id); if (!el) return
|
|
134
|
+
try {
|
|
135
|
+
const resp = await fetch('/api/direct-receive/accounts/' + a.id + '/qr', { headers: { Authorization: 'Bearer ' + state.apiKey } })
|
|
136
|
+
if (!resp.ok) { el.textContent = '❌'; return }
|
|
137
|
+
const url = URL.createObjectURL(await resp.blob())
|
|
138
|
+
el.innerHTML = `<img src="${url}" alt="${t('收款二维码')}" style="width:72px;height:72px;object-fit:contain;border-radius:6px">`
|
|
139
|
+
} catch { el.textContent = '❌' }
|
|
140
|
+
}, 0))
|
|
141
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Direct Pay (Rail 1) — 买家侧收款账号选择 + 分账号 FX + ack 门后收款码展示 (Phase D3)。UI ONLY。
|
|
2
|
+
// 接 D1(GET /api/direct-receive/selectable-accounts 只读元数据)+ D2(建单带 direct_receive_account_id、
|
|
3
|
+
// GET /api/orders/:id/direct-pay-qr ack 门后取图)。选账号在【下单前】(与建单契约一致);收款目标(instruction 原文 +
|
|
4
|
+
// QR 图)仍只在 D1/D2 both-acked 后经订单快照 / QR 端点展示。WebAZ 不验证/路由/托管,FX 纯展示。双语 parity 由 test-direct-pay-ui.ts 守。
|
|
5
|
+
|
|
6
|
+
// USDC 金额 → 任意币种展示串(用于"卖家按 <币种> 收款 ≈ X");无 rate/币种不支持 → 仅币种码。
|
|
7
|
+
window.dpFxInCurrency = (usdc, currency) => {
|
|
8
|
+
const cur = String(currency || '').toUpperCase()
|
|
9
|
+
const r = window._fxRates
|
|
10
|
+
const n = Number(usdc)
|
|
11
|
+
const sym = { CNY: '¥', EUR: '€', INR: '₹', SGD: 'S$', USD: '$', IDR: 'Rp', MYR: 'RM', PHP: '₱', VND: '₫', THB: '฿' }
|
|
12
|
+
if (!cur) return ''
|
|
13
|
+
if (cur === 'USDC' || cur === 'USD') return Number.isFinite(n) ? `${sym.USD}${n >= 100 ? Math.round(n) : n.toFixed(2)}` : ''
|
|
14
|
+
const rate = r && r.rates ? Number(r.rates[cur]) : NaN
|
|
15
|
+
if (!(rate > 0) || !Number.isFinite(n)) return cur // 币种不在 FX 表:只显示币种码,不臆造换算
|
|
16
|
+
const local = n * rate
|
|
17
|
+
return `${sym[cur] || (cur + ' ')}${local >= 100 ? Math.round(local) : local.toFixed(2)}`
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// 选直付且可用时加载卖家可选收款账号(元数据 only)。0 个 → 清空(legacy 单条说明,无需选);≥1 → 单选列表(默认首个)。
|
|
21
|
+
window.dpLoadBuyerAccounts = async (productId) => {
|
|
22
|
+
const box = document.getElementById('dp-account-picker')
|
|
23
|
+
if (!box) return
|
|
24
|
+
let r
|
|
25
|
+
try { r = await GET('/direct-receive/selectable-accounts?product_id=' + encodeURIComponent(productId || '')) } catch { r = null }
|
|
26
|
+
const opts = (r && r.options) || []
|
|
27
|
+
if (!opts.length) { box.innerHTML = ''; return } // legacy-only 卖家:无多账号,走单条说明,不显示选择器
|
|
28
|
+
box.innerHTML = `
|
|
29
|
+
<div style="font-size:12px;font-weight:600;color:#374151;margin:8px 0 4px">${t('选择卖家收款方式')}</div>
|
|
30
|
+
${opts.map((o, i) => {
|
|
31
|
+
const title = o.label ? escHtml(o.label) : (o.method ? escHtml(o.method) : t('收款账号'))
|
|
32
|
+
const sub = [o.method ? escHtml(o.method) : '', o.currency ? escHtml(o.currency) : ''].filter(Boolean).join(' · ')
|
|
33
|
+
return `<label style="display:flex;gap:8px;align-items:flex-start;font-size:13px;padding:6px 2px;cursor:pointer">
|
|
34
|
+
<input type="radio" name="dp-account" value="${escHtml(o.account_id)}" ${i === 0 ? 'checked' : ''}>
|
|
35
|
+
<span><b>${title}</b>${sub ? ` <span style="color:#6b7280;font-size:11px">${sub}</span>` : ''}<span data-dp-fx-cur="${o.currency ? escHtml(o.currency) : ''}" style="display:block;color:#6b7280;font-size:11px"></span></span>
|
|
36
|
+
</label>`
|
|
37
|
+
}).join('')}
|
|
38
|
+
<div style="font-size:11px;color:#9ca3af;margin-top:2px">${t('收款明细与二维码将在完成风险确认后显示')}</div>`
|
|
39
|
+
window.dpRenderAccountFx() // 按【当前订单总额 = 单价 × 数量】渲染分账号 FX(数量后续变化由 qtyStep/qtyClamp 再刷新)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 当前直付订单总额(USDC)= 单价(data-amt)×【当前数量】(#inp-qty);直付买家场外付这个数,绝不能只按单价显示。
|
|
43
|
+
window.dpAccountTotalUsdc = () => {
|
|
44
|
+
const unit = Number(document.getElementById('dp-rail-block')?.getAttribute('data-amt') || '')
|
|
45
|
+
const q = document.getElementById('inp-qty')
|
|
46
|
+
const qty = q ? Math.max(1, Math.floor(Number(q.value) || 1)) : 1
|
|
47
|
+
return Number.isFinite(unit) ? unit * qty : NaN
|
|
48
|
+
}
|
|
49
|
+
// 用【当前订单总额】渲染/刷新分账号 FX 金额(数量改变后由 qtyStep/qtyClamp 调用)。无该币种汇率 → dpFxInCurrency 回落币种码。
|
|
50
|
+
window.dpRenderAccountFx = () => {
|
|
51
|
+
const total = window.dpAccountTotalUsdc()
|
|
52
|
+
document.querySelectorAll('#dp-account-picker [data-dp-fx-cur]').forEach((el) => {
|
|
53
|
+
const cur = el.getAttribute('data-dp-fx-cur')
|
|
54
|
+
const fx = (cur && Number.isFinite(total)) ? window.dpFxInCurrency(total, cur) : ''
|
|
55
|
+
el.innerHTML = fx ? `${t('卖家按此收款')} ≈ ${fx}` : ''
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 当前选中的收款账号 id(供建单带上);无选择器(legacy)/未选 → '' → 建单省略该字段,后端回落单条说明。
|
|
60
|
+
window.dpSelectedAccountId = () => document.querySelector('input[name="dp-account"]:checked')?.value || ''
|
|
61
|
+
|
|
62
|
+
// ack 门后:取订单收款二维码(owner+both-acked 才 200;Authorization header 走 fetch→blob,<img src> 带不了头)。
|
|
63
|
+
window.dpLoadOrderQr = async (orderId) => {
|
|
64
|
+
const box = document.getElementById('dp-order-qr')
|
|
65
|
+
if (!box) return
|
|
66
|
+
try {
|
|
67
|
+
const resp = await fetch('/api/orders/' + encodeURIComponent(orderId) + '/direct-pay-qr', { headers: { Authorization: 'Bearer ' + (window.state && window.state.apiKey) } }); if (!box.isConnected) return // 切走了(box 离屏)→ 绝不把本单 QR 写进别单面板(纵深防御:box 在 await 前捕获,离屏写入本不可见,此处显式封边)
|
|
68
|
+
if (!resp.ok) { box.innerHTML = ''; return } // 无 QR / 未 ack:静默不显(instruction 文本已单独展示)
|
|
69
|
+
const url = URL.createObjectURL(await resp.blob())
|
|
70
|
+
box.innerHTML = `<div style="font-size:11px;color:#9ca3af;margin:6px 0 2px">${t('收款二维码')}</div><img src="${url}" alt="${t('收款二维码')}" style="width:150px;height:150px;object-fit:contain;border:1px solid #e5e7eb;border-radius:8px">`
|
|
71
|
+
} catch { box.innerHTML = '' }
|
|
72
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// 直付取消退款握手前端(审计项 C)—— 付款后(accepted)·发货前:买家申请取消 → 卖家场外退款并声明 →
|
|
2
|
+
// 买家确认收到(Passkey)→ 系统无责关单。UI ONLY(真正边界在后端 direct-pay-cancel-refund 路由 + 域模块)。
|
|
3
|
+
// 状态来自订单详情 DTO 的 order.cancel_refund(仅 direct_p2p+accepted 计算),同步渲染。中文 t(),英文 i18n.js _EN。
|
|
4
|
+
|
|
5
|
+
window.dpCancelRefundCard = (order, isBuyer, isSeller) => {
|
|
6
|
+
if (!order || order.payment_rail !== 'direct_p2p' || order.status !== 'accepted' || !(isBuyer || isSeller)) return ''
|
|
7
|
+
const cr = order.cancel_refund
|
|
8
|
+
if (!cr) return ''
|
|
9
|
+
const oid = order.id
|
|
10
|
+
const req = cr.request
|
|
11
|
+
const eff = req && req.status
|
|
12
|
+
const head = `<div style="font-size:13px;font-weight:700;color:#92400e;margin-bottom:4px">↩️ ${t('取消订单并退款(直付)')}</div>
|
|
13
|
+
<div style="font-size:12px;color:#4b5563;line-height:1.7;margin-bottom:8px">${t('直付非托管:平台不持货款,退款由卖家在协议外完成,双方在此握手确认,订单无责取消。')}</div>`
|
|
14
|
+
let body = ''
|
|
15
|
+
if (isBuyer && cr.can_confirm) {
|
|
16
|
+
body = `<div style="font-size:12px;color:#374151;margin-bottom:8px">💸 ${t('卖家已声明退款')}${req && req.refund_reference ? '「' + escHtml(req.refund_reference) + '」' : ''}。${t('请先核实退款已到账;未收到请勿确认,可发起争议。')}</div>
|
|
17
|
+
<button class="btn btn-primary btn-sm" style="width:auto" onclick="dpCrConfirm('${oid}')">${t('已收到退款,确认取消订单(需 Passkey)')}</button>`
|
|
18
|
+
} else if (isBuyer && eff === 'requested') {
|
|
19
|
+
body = `<div style="font-size:12px;color:#6b7280;margin-bottom:8px">⏳ ${t('已向卖家发出取消退款请求,等待响应(超期可重新申请或发起争议)。')}</div>
|
|
20
|
+
<button class="btn btn-outline btn-sm" style="width:auto" onclick="dpCrWithdraw('${oid}')">${t('撤回请求')}</button>`
|
|
21
|
+
} else if (isBuyer && cr.can_request) {
|
|
22
|
+
const hint = eff === 'declined' ? `<div style="font-size:12px;color:#b91c1c;margin-bottom:6px">${t('卖家拒绝了上次请求(继续发货)。可再次申请或与卖家沟通。')}</div>` : (eff === 'expired' ? `<div style="font-size:12px;color:#b91c1c;margin-bottom:6px">${t('上次请求卖家未在期限内响应。可重新申请,或发起争议。')}</div>` : '')
|
|
23
|
+
body = `${hint}<textarea id="dp-cr-reason-${oid}" class="form-control" rows="2" placeholder="${t('取消理由(可选,卖家可见)')}" style="margin-bottom:8px;font-size:12px"></textarea>
|
|
24
|
+
<button class="btn btn-outline btn-sm" style="width:auto" onclick="dpCrRequest('${oid}')">${t('申请取消并退款')}</button>`
|
|
25
|
+
} else if (isSeller && cr.can_respond) {
|
|
26
|
+
body = `<div style="font-size:12px;color:#374151;margin-bottom:8px">📩 ${t('买家申请取消订单并退款。')}${req && req.reason ? '「' + escHtml(req.reason) + '」' : ''} ${t('同意:先在协议外退款,再点"我已退款";不同意:拒绝并继续发货。')}</div>
|
|
27
|
+
<input id="dp-cr-ref-${oid}" class="form-control" maxlength="200" placeholder="${t('退款参考(转账单号等,可选,买家可见)')}" style="margin-bottom:8px;font-size:12px">
|
|
28
|
+
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
|
29
|
+
<button class="btn btn-primary btn-sm" style="width:auto" onclick="dpCrMarkRefunded('${oid}')">${t('我已退款')}</button>
|
|
30
|
+
<button class="btn btn-outline btn-sm" style="width:auto" onclick="dpCrDecline('${oid}')">${t('拒绝(继续发货)')}</button>
|
|
31
|
+
</div>`
|
|
32
|
+
} else if (isSeller && eff === 'refund_marked') {
|
|
33
|
+
body = `<div style="font-size:12px;color:#6b7280">⏳ ${t('你已声明退款,等待买家确认收到后订单将无责取消。')}</div>`
|
|
34
|
+
} else return ''
|
|
35
|
+
return `<div class="card" style="border:1px solid #fde68a;background:linear-gradient(135deg,#fffbeb,#fef3c7)">${head}${body}</div>`
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const dpCrToast = (m, kind) => { if (typeof toast$ === 'function') toast$(m, kind) }
|
|
39
|
+
const dpCrReload = (oid) => renderOrderDetail(document.getElementById('app'), oid)
|
|
40
|
+
|
|
41
|
+
window.dpCrRequest = async (oid) => {
|
|
42
|
+
const el = document.getElementById('dp-cr-reason-' + oid)
|
|
43
|
+
const r = await POST('/orders/' + oid + '/cancel-refund/request', { reason: (el && el.value || '').trim() })
|
|
44
|
+
if (r.error) return void dpCrToast(r.error, 'error')
|
|
45
|
+
dpCrToast(t('已发出取消退款请求,等待卖家响应'), 'success'); dpCrReload(oid)
|
|
46
|
+
}
|
|
47
|
+
window.dpCrDecline = async (oid) => {
|
|
48
|
+
const r = await POST('/orders/' + oid + '/cancel-refund/decline', {})
|
|
49
|
+
if (r.error) return void dpCrToast(r.error, 'error')
|
|
50
|
+
dpCrReload(oid)
|
|
51
|
+
}
|
|
52
|
+
window.dpCrMarkRefunded = async (oid) => {
|
|
53
|
+
if (typeof confirmModal === 'function' && !(await confirmModal(t('确认你已在协议外向买家完成退款?声明后买家确认即取消订单;虚假声明将留痕并可被追责。'), t('我已退款'), { danger: true }))) return
|
|
54
|
+
const el = document.getElementById('dp-cr-ref-' + oid)
|
|
55
|
+
const r = await POST('/orders/' + oid + '/cancel-refund/mark-refunded', { refund_reference: (el && el.value || '').trim() })
|
|
56
|
+
if (r.error) return void dpCrToast(r.error, 'error')
|
|
57
|
+
dpCrToast(t('已声明退款,等待买家确认'), 'success'); dpCrReload(oid)
|
|
58
|
+
}
|
|
59
|
+
window.dpCrWithdraw = async (oid) => {
|
|
60
|
+
const r = await POST('/orders/' + oid + '/cancel-refund/withdraw', {})
|
|
61
|
+
if (r.error) return void dpCrToast(r.error, 'error')
|
|
62
|
+
dpCrReload(oid)
|
|
63
|
+
}
|
|
64
|
+
window.dpCrConfirm = async (oid) => {
|
|
65
|
+
if (typeof confirmModal === 'function' && !(await confirmModal(t('确认已收到卖家退款?确认后订单将无责取消,不可撤销。若尚未到账请勿确认。'), t('已收到,取消订单'), { danger: true }))) return
|
|
66
|
+
let token
|
|
67
|
+
try { token = await requestPasskeyGate('direct_pay_order_action', { order_id: oid, action: 'cancel_refund_confirm' }) }
|
|
68
|
+
catch (e) { if (window.dpPromptRegisterPasskey) await window.dpPromptRegisterPasskey(e); return }
|
|
69
|
+
const r = await POST('/orders/' + oid + '/cancel-refund/confirm', { webauthn_token: token })
|
|
70
|
+
if (r.error) return void dpCrToast(window.dpErrorText ? window.dpErrorText(r.error_code, r.error) : r.error, 'error')
|
|
71
|
+
dpCrToast(t('订单已无责取消'), 'success'); dpCrReload(oid)
|
|
72
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Direct Pay (Rail 1) — ADMIN 商户合规录入 (PR-⑧)。UI ONLY。
|
|
2
|
+
// 录入【每个卖家】的 KYB 复核结论 + 制裁筛查结论(直付入场硬门:必须 KYB approved + sanctions clear)。
|
|
3
|
+
// 后端已有受门控端点(admin-direct-receive-deposits):POST /api/admin/direct-receive/kyb-reviews · /sanctions-screenings,
|
|
4
|
+
// 均 ROOT + 真人 Passkey(purpose direct_pay_kyb_ingress / direct_pay_sanctions_ingress;purpose_data 绑
|
|
5
|
+
// user_id+status+provider_ref+expires_at,签 A 写 B 拒)。本屏只接线,不碰资金/状态机。
|
|
6
|
+
// ⚠️ 仅【记录你已实际完成的尽调结论】——不代替真实 KYB/制裁筛查。
|
|
7
|
+
|
|
8
|
+
window.renderAdminDirectReceiveCompliance = async function (app) {
|
|
9
|
+
if (!state.user) { renderLogin(); return }
|
|
10
|
+
if (!isAdmin()) { app.innerHTML = shell(`<div class="alert alert-info">${t('仅限管理员')}</div>`, 'admin'); return }
|
|
11
|
+
const sel = (id, opts) => `<select id="${id}" style="width:100%;padding:7px;border:1px solid #d1d5db;border-radius:6px;font-size:13px">${opts.map(o => `<option value="${o[0]}">${o[1]}</option>`).join('')}</select>`
|
|
12
|
+
const inp = (id, ph) => `<input id="${id}" placeholder="${ph}" style="width:100%;padding:7px;border:1px solid #d1d5db;border-radius:6px;font-size:13px">`
|
|
13
|
+
app.innerHTML = shell(`
|
|
14
|
+
<h1 class="page-title">🧾 ${t('商户合规录入')}</h1>
|
|
15
|
+
<div style="margin-bottom:12px"><button class="btn btn-outline btn-sm" style="width:auto" onclick="navigate('#admin')">${t('返回概览')}</button></div>
|
|
16
|
+
<div style="font-size:12px;color:#6b7280;line-height:1.6;margin-bottom:6px">${t('录入卖家的 KYB / 制裁筛查结论(直付入场硬门)。仅记录你【已实际完成】的尽调结论,不代替真实筛查;均需真人 Passkey。')}</div>
|
|
17
|
+
<div style="font-size:12px;color:#b45309;line-height:1.6;margin-bottom:10px">${t('⚠️ 外部审核编号只填外部审核系统的 case id;切勿填写身份证/护照/住址/证件文件链接 —— 该字段会【明文入库】。')}</div>
|
|
18
|
+
<div class="card" style="margin-bottom:12px;border:1px solid #e5e7eb">
|
|
19
|
+
<div style="font-size:13px;font-weight:700;margin-bottom:8px">${t('卖家用户 ID')}</div>
|
|
20
|
+
${inp('cmp-user', t('卖家用户 ID(seller user id)'))}
|
|
21
|
+
</div>
|
|
22
|
+
<div class="card" style="margin-bottom:12px;border:1px solid #e5e7eb">
|
|
23
|
+
<div style="font-size:13px;font-weight:700;margin-bottom:8px">🪪 ${t('KYB 复核结论')}</div>
|
|
24
|
+
<div style="display:flex;flex-direction:column;gap:6px">
|
|
25
|
+
${sel('cmp-kyb-status', [['approved', t('通过 approved')], ['pending', t('待定 pending')], ['rejected', t('拒绝 rejected')], ['revoked', t('撤销 revoked')]])}
|
|
26
|
+
${inp('cmp-kyb-ref', t('外部审核编号 / vendor case id(选填)'))}
|
|
27
|
+
${inp('cmp-kyb-exp', t('有效期 expires_at(选填,如 2027-01-01)'))}
|
|
28
|
+
<button class="btn btn-primary btn-sm" style="align-self:flex-start;font-size:12px" onclick="window.doIngestKyb()">${t('记录 KYB(真人 Passkey)')}</button>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
<div class="card" style="margin-bottom:12px;border:1px solid #e5e7eb">
|
|
32
|
+
<div style="font-size:13px;font-weight:700;margin-bottom:8px">🛡️ ${t('制裁筛查结论')}</div>
|
|
33
|
+
<div style="display:flex;flex-direction:column;gap:6px">
|
|
34
|
+
${sel('cmp-sanc-status', [['clear', t('通过 clear')], ['flagged', t('命中 flagged')], ['blocked', t('阻断 blocked')]])}
|
|
35
|
+
${inp('cmp-sanc-ref', t('外部审核编号 / vendor case id(选填)'))}
|
|
36
|
+
${inp('cmp-sanc-exp', t('有效期 expires_at(选填)'))}
|
|
37
|
+
<button class="btn btn-primary btn-sm" style="align-self:flex-start;font-size:12px" onclick="window.doIngestSanctions()">${t('记录制裁筛查(真人 Passkey)')}</button>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
<div style="font-size:12px;color:#6b7280">${t('录入后用就绪报告核对:')} <code>npm run direct-pay:readiness</code></div>
|
|
41
|
+
`, 'admin')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 通用:取卖家 id + 组装 body(provider_ref/expires_at 仅在非空时带);purpose_data 必须与请求体逐字一致。
|
|
45
|
+
function _dpComplianceBody(statusId, refId, expId) {
|
|
46
|
+
const get = (id) => { const e = document.getElementById(id); return e ? e.value.trim() : '' }
|
|
47
|
+
const user_id = get('cmp-user')
|
|
48
|
+
if (!user_id) return null
|
|
49
|
+
const body = { user_id, status: get(statusId) }
|
|
50
|
+
const ref = get(refId), exp = get(expId)
|
|
51
|
+
if (ref) body.provider_ref = ref
|
|
52
|
+
if (exp) body.expires_at = exp
|
|
53
|
+
return body
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function _dpComplianceIngest(purpose, path, statusId, refId, expId, okMsg) {
|
|
57
|
+
const body = _dpComplianceBody(statusId, refId, expId)
|
|
58
|
+
if (!body) { if (typeof toast$ === 'function') toast$(t('请填写卖家用户 ID'), 'error'); return }
|
|
59
|
+
const token = await requestPasskeyGate(purpose, body) // purpose_data = body(逐字绑定)
|
|
60
|
+
if (!token) { if (typeof toast$ === 'function') toast$(t('需要真人 Passkey 确认'), 'error'); return }
|
|
61
|
+
const r = await POST(path, { ...body, webauthn_token: token })
|
|
62
|
+
if (!r || r.error) { if (typeof toast$ === 'function') toast$(window.dpErrorText ? window.dpErrorText(r && r.error_code, r && r.error) : t('操作失败,请重试'), 'error'); return }
|
|
63
|
+
if (typeof toast$ === 'function') toast$(okMsg)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
window.doIngestKyb = () => _dpComplianceIngest('direct_pay_kyb_ingress', '/admin/direct-receive/kyb-reviews', 'cmp-kyb-status', 'cmp-kyb-ref', 'cmp-kyb-exp', t('KYB 结论已记录'))
|
|
67
|
+
window.doIngestSanctions = () => _dpComplianceIngest('direct_pay_sanctions_ingress', '/admin/direct-receive/sanctions-screenings', 'cmp-sanc-status', 'cmp-sanc-ref', 'cmp-sanc-exp', t('制裁筛查结论已记录'))
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Direct Pay (Rail 1) — 一键复制按钮(付款说明 / 付款参考号 / 金额)。UI ONLY;不碰任何钱路。
|
|
2
|
+
// 买家场外付款需把收款账号/参考号抄进银行 App —— 手抄是整条轨最易错的一步,复制按钮把出错率降到 0。
|
|
3
|
+
// 文本存 data-dpcopy(escHtml 转义双引号/单引号,属性安全);点按走 app.js 健壮 copyText(clipboard→execCommand 回退),
|
|
4
|
+
// 成功后短暂显示 ✓ 已复制再还原。escHtml/copyText/t 均为运行时(render/click 时)调用,此文件早于 app.js 加载无碍。
|
|
5
|
+
window.dpCopyBtn = (text, label) => `<button type="button" class="btn btn-sm dp-copy-btn" style="padding:2px 8px;font-size:11px;background:#eef2ff;color:#4338ca;border:none;border-radius:6px;cursor:pointer;white-space:nowrap;vertical-align:middle" data-dpcopy="${escHtml(String(text ?? ''))}" onclick="dpDoCopy(this)">📋 ${escHtml(label || t('复制'))}</button>`
|
|
6
|
+
window.dpDoCopy = async (el) => {
|
|
7
|
+
if (!el || el.dataset.busy === '1') return
|
|
8
|
+
const ok = await copyText(el.getAttribute('data-dpcopy') || '')
|
|
9
|
+
if (!ok) return
|
|
10
|
+
const orig = el.innerHTML; el.dataset.busy = '1'; el.innerHTML = '✓ ' + t('已复制')
|
|
11
|
+
setTimeout(() => { el.innerHTML = orig; el.dataset.busy = '' }, 1500)
|
|
12
|
+
}
|