@seasonkoh/webaz 0.1.27 → 0.1.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/README.zh-CN.md +7 -6
- package/dist/currency.js +16 -0
- package/dist/deposit-rails.js +52 -0
- package/dist/direct-pay-aml-monitor.js +67 -0
- package/dist/direct-pay-aml-review.js +40 -0
- package/dist/direct-pay-base-bond-entry.js +5 -0
- package/dist/direct-pay-bond-rail-clearance.js +94 -0
- package/dist/direct-pay-compliance-ingress.js +145 -0
- package/dist/direct-pay-controls.js +136 -0
- package/dist/direct-pay-create.js +162 -0
- package/dist/direct-pay-deferral-quota.js +43 -0
- package/dist/direct-pay-disclosures.js +44 -0
- package/dist/direct-pay-eligibility.js +46 -0
- package/dist/direct-pay-fee-ar.js +241 -0
- package/dist/direct-pay-launch-readiness.js +108 -0
- package/dist/direct-pay-launch-summary.js +68 -0
- package/dist/direct-pay-ledger.js +94 -0
- package/dist/direct-receive-account-qr.js +77 -0
- package/dist/direct-receive-accounts.js +82 -0
- package/dist/direct-receive-deferral.js +142 -0
- package/dist/direct-receive-deposits.js +260 -0
- package/dist/direct-receive-payment-instruction.js +26 -0
- package/dist/fx-rates.js +71 -0
- package/dist/layer0-foundation/L0-1-database/schema.js +531 -1
- package/dist/layer0-foundation/L0-2-state-machine/genuine-sale.js +15 -5
- package/dist/layer0-foundation/L0-2-state-machine/transitions.js +41 -0
- package/dist/layer0-foundation/L0-5-manifest/manifest.js +1 -1
- package/dist/layer1-agent/L1-1-mcp-server/cli.js +64 -0
- package/dist/layer1-agent/L1-1-mcp-server/network-mode.js +80 -0
- package/dist/layer1-agent/L1-1-mcp-server/server.js +251 -69
- package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +4 -4
- package/dist/ledger.js +12 -3
- package/dist/mcp.js +23 -4
- package/dist/merchant-bond-domain.js +64 -0
- package/dist/merchant-bond-exposure.js +78 -0
- package/dist/merchant-bond-watcher.js +26 -0
- package/dist/payment-rails.js +29 -0
- package/dist/product-verification.js +93 -0
- package/dist/pwa/acp-feed.js +3 -2
- package/dist/pwa/contract-fingerprint.js +3 -0
- package/dist/pwa/direct-pay-guards.js +20 -0
- package/dist/pwa/direct-pay-order-redaction.js +24 -0
- package/dist/pwa/endpoint-actions.js +3 -0
- package/dist/pwa/public/app-account.js +977 -0
- package/dist/pwa/public/app-admin.js +608 -0
- package/dist/pwa/public/app-agents.js +63 -0
- package/dist/pwa/public/app-ai.js +2162 -0
- package/dist/pwa/public/app-chat-poll.js +29 -0
- package/dist/pwa/public/app-contribution.js +836 -0
- package/dist/pwa/public/app-create-kinds.js +17 -0
- package/dist/pwa/public/app-direct-pay-accounts.js +141 -0
- package/dist/pwa/public/app-direct-pay-buyer.js +72 -0
- package/dist/pwa/public/app-direct-pay-compliance.js +67 -0
- package/dist/pwa/public/app-direct-pay-deferral-admin.js +72 -0
- package/dist/pwa/public/app-direct-pay-deferral.js +61 -0
- package/dist/pwa/public/app-direct-pay-fee-center.js +33 -0
- package/dist/pwa/public/app-direct-pay-fee-ops.js +112 -0
- package/dist/pwa/public/app-direct-pay-product-verify.js +103 -0
- package/dist/pwa/public/app-direct-pay-readiness.js +38 -0
- package/dist/pwa/public/app-direct-pay-store-verify.js +100 -0
- package/dist/pwa/public/app-direct-pay.js +227 -0
- package/dist/pwa/public/app-discover.js +1296 -0
- package/dist/pwa/public/app-external-links.js +32 -0
- package/dist/pwa/public/app-listings.js +226 -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 +1692 -0
- package/dist/pwa/public/app-seller.js +199 -0
- package/dist/pwa/public/app-shop.js +1145 -0
- package/dist/pwa/public/app.js +13388 -22277
- package/dist/pwa/public/i18n.js +413 -196
- package/dist/pwa/public/index.html +27 -0
- package/dist/pwa/public/openapi.json +579 -1
- package/dist/pwa/routes/admin-analytics.js +4 -2
- package/dist/pwa/routes/admin-direct-receive-deposits.js +321 -0
- package/dist/pwa/routes/agent-grants.js +255 -0
- package/dist/pwa/routes/buyer-feeds.js +2 -1
- package/dist/pwa/routes/chat.js +6 -1
- package/dist/pwa/routes/dashboards.js +2 -1
- package/dist/pwa/routes/direct-pay-availability.js +196 -0
- package/dist/pwa/routes/direct-pay-disclosure-acks.js +74 -0
- package/dist/pwa/routes/direct-pay-timeouts.js +71 -0
- package/dist/pwa/routes/direct-receive-accounts.js +155 -0
- package/dist/pwa/routes/direct-receive-payment-instructions.js +45 -0
- package/dist/pwa/routes/fx.js +12 -0
- package/dist/pwa/routes/leaderboard.js +47 -9
- package/dist/pwa/routes/listings.js +2 -2
- package/dist/pwa/routes/logistics.js +4 -0
- package/dist/pwa/routes/manifests.js +38 -0
- package/dist/pwa/routes/me-data.js +5 -2
- package/dist/pwa/routes/orders-action.js +117 -9
- package/dist/pwa/routes/orders-create.js +4 -0
- package/dist/pwa/routes/orders-read.js +36 -0
- package/dist/pwa/routes/p2p-products.js +2 -2
- package/dist/pwa/routes/products-create.js +5 -3
- package/dist/pwa/routes/products-links.js +34 -0
- package/dist/pwa/routes/products-list.js +2 -1
- package/dist/pwa/routes/products-update.js +22 -2
- package/dist/pwa/routes/promoter.js +3 -0
- package/dist/pwa/routes/referral.js +4 -0
- package/dist/pwa/routes/returns.js +26 -1
- package/dist/pwa/routes/rewards-apply.js +10 -5
- package/dist/pwa/routes/rewards-clearing-mature.js +96 -0
- package/dist/pwa/routes/rewards-escrow-expire.js +6 -2
- package/dist/pwa/routes/shops.js +2 -1
- package/dist/pwa/routes/url-claim.js +2 -2
- package/dist/pwa/routes/users-public.js +8 -0
- package/dist/pwa/routes/wallet-read.js +3 -0
- package/dist/pwa/routes/webauthn.js +7 -2
- package/dist/pwa/server-schema.js +9 -0
- package/dist/pwa/server.js +261 -1706
- package/dist/runtime/agent-grant-scopes.js +128 -0
- package/dist/runtime/agent-grant-verifier.js +67 -0
- package/dist/runtime/agent-pairing.js +60 -0
- package/dist/runtime/apply-webaz-runtime-schema.js +15 -0
- package/dist/runtime/webaz-schema-helpers.js +1952 -0
- package/dist/store-verification.js +77 -0
- package/dist/version.js +1 -1
- package/package.json +80 -2
|
@@ -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,226 @@
|
|
|
1
|
+
// WebAZ — Listings (multi-seller follow-sell) read-only display (classic split, slice J / app-listings.js)
|
|
2
|
+
//
|
|
3
|
+
// Loaded as a CLASSIC script in this order (index.html):
|
|
4
|
+
// i18n → app-admin → app-contribution → app-ai → app-discover → app-profile → app-account → app-shop → app-listings → app-seller → app.js (source of truth: index.html)
|
|
5
|
+
// Top-level functions / window.* handlers are global; pages run on route/click
|
|
6
|
+
// (after app.js loads), so cross-file globals (GET/state/shell/escHtml/navigate/t/
|
|
7
|
+
// productCardHtml/LISTING_CATEGORY_NAMES/LISTING_TAG_DEFS/FULFILLMENT_LABELS/...) resolve
|
|
8
|
+
// at call time. No import/export.
|
|
9
|
+
//
|
|
10
|
+
// READ-ONLY display surfaces only — all GET /listings*: renderListingsHome,
|
|
11
|
+
// renderListingsMine, renderListingDetail, + LISTING_SORT_CHIPS/URGENCY_CHIPS
|
|
12
|
+
// (display-only chips) and setListingUrgency/setListingSort (re-render toggles).
|
|
13
|
+
//
|
|
14
|
+
// INTENTIONALLY LEFT in app.js (money/stake/order path — never moved here): listings
|
|
15
|
+
// create/follow + offer handlers LOCK or RELEASE wallet stake (listings.ts requires
|
|
16
|
+
// + locks stake on POST /listings; offers.ts releases stake) — renderListingCreate/
|
|
17
|
+
// submitListingCreate, renderListingFollow/submitFollowListing, refreshOfferFreshness
|
|
18
|
+
// (POST /offers/:id/refresh). They are reached cross-file via the onclick handlers
|
|
19
|
+
// that these read-only pages render. No UI/behavior change.
|
|
20
|
+
|
|
21
|
+
async function renderListingsHome(app) {
|
|
22
|
+
app.innerHTML = `
|
|
23
|
+
<div style="padding:14px;max-width:760px;margin:0 auto">
|
|
24
|
+
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px">
|
|
25
|
+
<h2 style="font-size:18px;font-weight:700;margin:0">${t('多商家跟卖')}</h2>
|
|
26
|
+
<button class="btn btn-primary btn-sm" onclick="location.hash='#listings/new'">+ ${t('创建商品身份')}</button>
|
|
27
|
+
</div>
|
|
28
|
+
<div style="margin-bottom:10px">
|
|
29
|
+
<input id="lst-q" placeholder="${t('搜索:型号 / 关键词')}" style="width:100%;padding:8px 10px;border:1px solid #d1d5db;border-radius:6px">
|
|
30
|
+
</div>
|
|
31
|
+
<div id="lst-list">${loading$()}</div>
|
|
32
|
+
</div>
|
|
33
|
+
`
|
|
34
|
+
const load = async () => {
|
|
35
|
+
const q = document.getElementById('lst-q').value.trim()
|
|
36
|
+
const list = document.getElementById('lst-list')
|
|
37
|
+
list.innerHTML = loading$()
|
|
38
|
+
const r = await GET('/listings' + (q ? '?q=' + encodeURIComponent(q) : ''))
|
|
39
|
+
const items = r?.items || []
|
|
40
|
+
if (!items.length) {
|
|
41
|
+
list.innerHTML = `<div style="text-align:center;color:#9ca3af;padding:40px 0">${t('暂无商品身份,创建第一个')}</div>`
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
list.innerHTML = items.map(it => `
|
|
45
|
+
<div class="card" style="padding:12px;margin-bottom:10px;cursor:pointer" onclick="location.hash='#listings/${it.id}'">
|
|
46
|
+
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px">
|
|
47
|
+
<div style="flex:1;min-width:0">
|
|
48
|
+
<div style="font-weight:600;font-size:14px;margin-bottom:4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${escHtml(it.title)}</div>
|
|
49
|
+
<div style="font-size:11px;color:#6b7280">
|
|
50
|
+
${t(LISTING_CATEGORY_NAMES[it.category] || it.category)}
|
|
51
|
+
${it.external_id ? ' · ' + escHtml(it.external_id) : ''}
|
|
52
|
+
· ${it.offer_count || 0} ${t('个卖家')}
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
<div style="text-align:right">
|
|
56
|
+
${it.min_price != null ? `<div style="color:#dc2626;font-weight:700;font-size:15px">${window.fmtPrice(it.min_price)}</div><div style="font-size:10px;color:#9ca3af">${t('起')}</div>` : `<div style="font-size:11px;color:#9ca3af">${t('暂无报价')}</div>`}
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
`).join('')
|
|
61
|
+
}
|
|
62
|
+
document.getElementById('lst-q').addEventListener('input', () => { clearTimeout(window._lstTimer); window._lstTimer = setTimeout(load, 300) })
|
|
63
|
+
load()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 我的跟卖 — 卖家查看自己在哪些 listings 已上架,含价格竞争位
|
|
67
|
+
async function renderListingsMine(app) {
|
|
68
|
+
if (!state.user) { location.hash = '#login'; return }
|
|
69
|
+
if (state.user.role !== 'seller') {
|
|
70
|
+
app.innerHTML = shell(`<div style="padding:24px">${alert$('warn', t('仅卖家可查看我的跟卖'))}</div>`, 'me')
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
app.innerHTML = shell(`
|
|
74
|
+
<div style="padding:14px;max-width:760px;margin:0 auto">
|
|
75
|
+
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px">
|
|
76
|
+
<h2 style="font-size:18px;font-weight:700;margin:0">📋 ${t('我的跟卖')}</h2>
|
|
77
|
+
<button class="btn btn-outline btn-sm" onclick="location.hash='#listings'">${t('去找新款 →')}</button>
|
|
78
|
+
</div>
|
|
79
|
+
<div id="mine-list">${loading$()}</div>
|
|
80
|
+
</div>
|
|
81
|
+
`, 'me')
|
|
82
|
+
const r = await GET('/listings/mine').catch(() => ({ items: [] }))
|
|
83
|
+
const items = r?.items || []
|
|
84
|
+
const list = document.getElementById('mine-list')
|
|
85
|
+
if (!items.length) {
|
|
86
|
+
list.innerHTML = `<div style="text-align:center;color:#9ca3af;padding:40px 16px;line-height:1.7">
|
|
87
|
+
<div style="font-size:48px;margin-bottom:10px">🏬</div>
|
|
88
|
+
<div>${t('还没有跟卖任何商品')}</div>
|
|
89
|
+
<a href="#listings" style="display:inline-block;margin-top:14px;color:#4f46e5;font-size:13px">${t('去 listings 板块挑款 →')}</a>
|
|
90
|
+
</div>`
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
list.innerHTML = items.map(it => {
|
|
94
|
+
const myMin = Number(it.my_min_price || 0)
|
|
95
|
+
const globalMin = Number(it.global_min_price || 0)
|
|
96
|
+
const isCheapest = myMin > 0 && globalMin > 0 && Math.abs(myMin - globalMin) < 0.001
|
|
97
|
+
const diff = myMin > 0 && globalMin > 0 ? myMin - globalMin : 0
|
|
98
|
+
const positionBadge = isCheapest
|
|
99
|
+
? `<span style="background:#dcfce7;color:#166534;font-size:10px;padding:2px 7px;border-radius:99px;font-weight:600">🏆 ${t('全网最低')}</span>`
|
|
100
|
+
: (diff > 0 ? `<span style="background:#fef3c7;color:#92400e;font-size:10px;padding:2px 7px;border-radius:99px;font-weight:600">+${diff.toFixed(2)} ${t('高于最低')}</span>` : '')
|
|
101
|
+
const creatorBadge = it.is_creator ? `<span style="background:#ede9fe;color:#5b21b6;font-size:10px;padding:2px 7px;border-radius:99px;font-weight:600;margin-left:4px">${t('我创建')}</span>` : ''
|
|
102
|
+
return `
|
|
103
|
+
<div class="card" style="padding:12px;margin-bottom:10px;cursor:pointer" onclick="location.hash='#listings/${it.id}'">
|
|
104
|
+
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px;margin-bottom:6px">
|
|
105
|
+
<div style="flex:1;min-width:0">
|
|
106
|
+
<div style="font-weight:600;font-size:14px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${escHtml(it.title)}${creatorBadge}</div>
|
|
107
|
+
<div style="font-size:11px;color:#6b7280;margin-top:3px">
|
|
108
|
+
${t(LISTING_CATEGORY_NAMES[it.category] || it.category)}
|
|
109
|
+
${it.external_id ? ' · ' + escHtml(it.external_id) : ''}
|
|
110
|
+
· ${t('全网')} ${it.total_offer_count} ${t('个卖家')}
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
${positionBadge}
|
|
114
|
+
</div>
|
|
115
|
+
<div style="display:flex;justify-content:space-between;align-items:center;gap:8px;padding-top:8px;border-top:1px solid #f3f4f6">
|
|
116
|
+
<div style="font-size:11px;color:#6b7280">
|
|
117
|
+
${t('我的报价')} <strong style="color:#374151">${window.fmtPrice(myMin)}</strong>
|
|
118
|
+
<span style="color:#9ca3af">· ${it.my_offer_count} ${t('个规格')}</span>
|
|
119
|
+
</div>
|
|
120
|
+
<div style="font-size:11px;color:#6b7280">
|
|
121
|
+
${t('全网最低')} <strong style="color:#dc2626">${window.fmtPrice(globalMin)}</strong>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
`
|
|
126
|
+
}).join('')
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// P2: 排序 chip + urgency selector state(per listing detail view)
|
|
130
|
+
const LISTING_SORT_CHIPS = [
|
|
131
|
+
{ key: 'smart', emoji: '✨', label: '综合' },
|
|
132
|
+
{ key: 'cheapest', emoji: '💰', label: '最便宜' },
|
|
133
|
+
{ key: 'fastest', emoji: '⚡', label: '最快' },
|
|
134
|
+
{ key: 'trusted', emoji: '🛡', label: '最可靠' },
|
|
135
|
+
{ key: 'nearest', emoji: '📍', label: '最近' },
|
|
136
|
+
{ key: 'clearance', emoji: '🔥', label: '清仓' },
|
|
137
|
+
]
|
|
138
|
+
const URGENCY_CHIPS = [
|
|
139
|
+
{ key: 'now', emoji: '⚡', label: '急要' },
|
|
140
|
+
{ key: 'today', emoji: '📅', label: '今天' },
|
|
141
|
+
{ key: 'flex', emoji: '🌊', label: '宽松' },
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
async function renderListingDetail(app, id) {
|
|
145
|
+
// 读取状态(带默认)
|
|
146
|
+
const urgency = state._listingUrgency || 'flex'
|
|
147
|
+
const sort = state._listingSort || 'smart'
|
|
148
|
+
|
|
149
|
+
app.innerHTML = `<div style="padding:14px;max-width:760px;margin:0 auto" id="lst-detail">${loading$()}</div>`
|
|
150
|
+
const qs = new URLSearchParams({ urgency, sort })
|
|
151
|
+
if (state.user?.region) qs.set('buyer_region', state.user.region)
|
|
152
|
+
const r = await GET('/listings/' + encodeURIComponent(id) + '?' + qs.toString())
|
|
153
|
+
if (r?.error) { document.getElementById('lst-detail').innerHTML = alert$('error', r.error); return }
|
|
154
|
+
const l = r.listing
|
|
155
|
+
const offers = r.offers || []
|
|
156
|
+
const isSeller = state.user?.role === 'seller'
|
|
157
|
+
const myId = state.user?.id
|
|
158
|
+
|
|
159
|
+
const chipBtn = (chip, active, isUrgency) => `
|
|
160
|
+
<button onclick="${isUrgency ? `setListingUrgency('${chip.key}','${id}')` : `setListingSort('${chip.key}','${id}')`}"
|
|
161
|
+
style="display:inline-flex;align-items:center;gap:3px;padding:5px 10px;border-radius:99px;font-size:11px;font-weight:600;cursor:pointer;border:1px solid ${active?'#4f46e5':'#e5e7eb'};background:${active?'#eef2ff':'#fff'};color:${active?'#4338ca':'#6b7280'};white-space:nowrap">
|
|
162
|
+
${chip.emoji} ${t(chip.label)}
|
|
163
|
+
</button>`
|
|
164
|
+
|
|
165
|
+
document.getElementById('lst-detail').innerHTML = `
|
|
166
|
+
<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:14px">
|
|
167
|
+
<div style="flex:1">
|
|
168
|
+
<div style="font-size:11px;color:#9ca3af;margin-bottom:2px">${t(LISTING_CATEGORY_NAMES[l.category] || l.category)}${l.external_id ? ' · ' + escHtml(l.external_id) : ''}</div>
|
|
169
|
+
<h2 style="font-size:18px;font-weight:700;margin:0 0 4px">${escHtml(l.title)}</h2>
|
|
170
|
+
${l.description ? `<div style="font-size:12px;color:#6b7280;margin-top:6px;white-space:pre-wrap">${escHtml(l.description)}</div>` : ''}
|
|
171
|
+
</div>
|
|
172
|
+
<button class="btn btn-sm" onclick="location.hash='#listings'" style="background:#f3f4f6">←</button>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
<div style="margin:10px 0">
|
|
176
|
+
<div style="font-size:11px;color:#6b7280;margin-bottom:4px">${t('紧急程度')}</div>
|
|
177
|
+
<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:8px">
|
|
178
|
+
${URGENCY_CHIPS.map(c => chipBtn(c, urgency === c.key, true)).join('')}
|
|
179
|
+
</div>
|
|
180
|
+
<div style="font-size:11px;color:#6b7280;margin-bottom:4px">${t('排序')}</div>
|
|
181
|
+
<div style="display:flex;gap:6px;flex-wrap:wrap;overflow-x:auto">
|
|
182
|
+
${LISTING_SORT_CHIPS.map(c => chipBtn(c, sort === c.key, false)).join('')}
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
<div style="display:flex;justify-content:space-between;align-items:center;margin:14px 0 10px">
|
|
187
|
+
<div style="font-size:13px;font-weight:600">${offers.length} ${t('个卖家在卖')}</div>
|
|
188
|
+
<div style="display:flex;gap:6px">
|
|
189
|
+
${state.user && state.user.id !== l.created_by ? `<button class="btn btn-sm" style="background:#eef2ff;color:#4338ca;font-size:11px;padding:5px 12px" onclick="openChatForContext('listing_qa','${l.id}','${l.created_by}')">💬 ${t('问商家')}</button>` : ''}
|
|
190
|
+
${isSeller ? `<button class="btn btn-primary btn-sm" onclick="location.hash='#listings/${l.id}/follow'">+ ${t('我也卖(跟卖)')}</button>` : ''}
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
${offers.length === 0 ? `<div style="text-align:center;color:#9ca3af;padding:30px 0">${t('暂无卖家')}</div>` : offers.map(o => {
|
|
195
|
+
const isMine = myId && o.seller_id === myId
|
|
196
|
+
const isStale = (o.tags || []).includes('stale')
|
|
197
|
+
const isColdStart = Number(o.cold_start_remaining || 0) > 0
|
|
198
|
+
return `
|
|
199
|
+
<div class="card" style="padding:12px;margin-bottom:8px${isStale ? ';border-left:3px solid #f59e0b' : ''}">
|
|
200
|
+
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px">
|
|
201
|
+
<div style="flex:1;min-width:0">
|
|
202
|
+
<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:6px">
|
|
203
|
+
${(o.tags || []).filter(tg => tg !== 'stale').map(tg => {
|
|
204
|
+
const def = LISTING_TAG_DEFS[tg]
|
|
205
|
+
return def ? `<span style="display:inline-flex;align-items:center;gap:3px;font-size:10px;background:${def.color}1a;color:${def.color};padding:2px 8px;border-radius:99px;font-weight:600">${def.emoji} ${t(def.label)}</span>` : ''
|
|
206
|
+
}).join('')}
|
|
207
|
+
${isStale ? `<span style="display:inline-flex;align-items:center;gap:3px;font-size:10px;background:#fef3c7;color:#92400e;padding:2px 8px;border-radius:99px;font-weight:600">⚠ ${t('备货')}</span>` : ''}
|
|
208
|
+
${isColdStart ? `<span style="display:inline-flex;align-items:center;gap:3px;font-size:10px;background:#e0e7ff;color:#3730a3;padding:2px 8px;border-radius:99px;font-weight:600">❄ ${t('新卖家')}</span>` : ''}
|
|
209
|
+
</div>
|
|
210
|
+
<div style="font-size:12px;color:#374151;margin-bottom:3px">@${escHtml(o.seller_handle || o.seller_id.slice(0,8))} · ${o.seller_region ? regionLabel(o.seller_region) : '-'} · ${o.seller_sales || 0} ${t('单成交')}</div>
|
|
211
|
+
<div style="font-size:11px;color:#6b7280">${t(FULFILLMENT_LABELS[o.fulfillment_type] || o.fulfillment_type)}${o.eta_hours != null ? ' · ETA ' + o.eta_hours + 'h' : ''}</div>
|
|
212
|
+
${isMine ? `<div style="margin-top:6px"><button class="btn btn-outline btn-sm" style="padding:3px 8px;font-size:10px;color:#4f46e5;border-color:#c7d2fe" onclick="refreshOfferFreshness('${o.id}','${id}')">🔄 ${t('现货确认')}</button></div>` : ''}
|
|
213
|
+
</div>
|
|
214
|
+
<div style="text-align:right">
|
|
215
|
+
<div style="color:#dc2626;font-weight:700;font-size:16px">${window.fmtPrice(o.price)}</div>
|
|
216
|
+
<div style="font-size:10px;color:#9ca3af">${t('库存')} ${o.stock}</div>
|
|
217
|
+
${state.user?.role === 'buyer' && o.stock > 0 ? `<button class="btn btn-primary btn-sm" style="margin-top:6px;padding:4px 12px;font-size:11px" onclick="location.hash='#order-product/${o.id}'">${t('购买')}</button>` : ''}
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
</div>`
|
|
221
|
+
}).join('')}
|
|
222
|
+
`
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
window.setListingUrgency = (key, id) => { state._listingUrgency = key; renderListingDetail(document.getElementById('app'), id) }
|
|
226
|
+
window.setListingSort = (key, id) => { state._listingSort = key; renderListingDetail(document.getElementById('app'), id) }
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// [PRELAUNCH-WAZ-SIM] 预发布期 escrow 模拟币提醒。预发布期 escrow 用模拟 WAZ(非真钱),
|
|
2
|
+
// 为防买家把测试币当真实价值,做三件事 —— 全部门控在 window._wazSimulated:
|
|
3
|
+
// 1) 支付方式不再默认预选 escrow(app-direct-pay.js: dpRailSelectorHtml 的 checked + dpSelectedRail 兜底)
|
|
4
|
+
// 2) 买家切到 escrow 时内联提醒(wazEscrowRailNote 注入 #dp-rail-block,app-direct-pay.js 无需扩容)
|
|
5
|
+
// 3) 生成的 escrow 订单在买家端订单详情顶部强提醒(wazEscrowOrderBanner,app.js renderOrderDetail 调用)
|
|
6
|
+
// 真实结算上线时:把下面 _wazSimulated 置 false(一处)→ 全部 no-op、escrow 预选行为恢复。
|
|
7
|
+
// 彻底移除:grep '[PRELAUNCH-WAZ-SIM]' / '_wazSimulated'(本文件 + app.js + app-direct-pay.js + index.html
|
|
8
|
+
// + package.json check:pwa-syntax + .github/workflows/ci.yml + complexity-ratchet-guard.ts + i18n.js)。
|
|
9
|
+
// UI-only:不碰 wallet/escrow/settlement/订单钱路;纯展示 + 一个"必须显式选 rail"的前端门(后端不变)。
|
|
10
|
+
window._wazSimulated = true
|
|
11
|
+
|
|
12
|
+
// 内联提醒文案(单一真相,两处复用)。
|
|
13
|
+
const WAZ_SIM_NOTE = () => '⚠️ ' + t('测试模式:WAZ 为模拟货币,不代表真实价值。此为预发布演示,你"托管"的并非真实资金,确认收货后释放的也是测试币。')
|
|
14
|
+
|
|
15
|
+
// 买家在"支付方式"里选中 Escrow 时的内联提醒。selectedRail = 当前选中的 rail 值('escrow' / 'direct_p2p' / undefined)。
|
|
16
|
+
// 注入到 #dp-rail-block 内,避免扩容 app-direct-pay.js(其已到 LOC 上限)。非模拟期直接 no-op。
|
|
17
|
+
window.wazEscrowRailNote = (selectedRail) => {
|
|
18
|
+
if (!window._wazSimulated) return
|
|
19
|
+
const block = document.getElementById('dp-rail-block'); if (!block) return
|
|
20
|
+
const host = block.querySelector('div') || block
|
|
21
|
+
let el = document.getElementById('dp-rail-escrow-sim')
|
|
22
|
+
if (selectedRail !== 'escrow') { if (el) el.style.display = 'none'; return }
|
|
23
|
+
if (!el) {
|
|
24
|
+
el = document.createElement('div'); el.id = 'dp-rail-escrow-sim'
|
|
25
|
+
el.style.cssText = 'margin-top:8px;font-size:11px;line-height:1.6;color:#92400e;background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:8px 10px'
|
|
26
|
+
host.appendChild(el)
|
|
27
|
+
}
|
|
28
|
+
el.textContent = WAZ_SIM_NOTE()
|
|
29
|
+
el.style.display = ''
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 生成 escrow 订单后,买家端订单详情顶部的强提醒横幅。非 escrow / 非买家 / 非模拟 → 空串(不渲染)。
|
|
33
|
+
window.wazEscrowOrderBanner = (order, isBuyer) => {
|
|
34
|
+
if (!window._wazSimulated || !isBuyer || !order || order.payment_rail === 'direct_p2p') return ''
|
|
35
|
+
return `<div style="border:1px solid #fde68a;background:linear-gradient(135deg,#fffbeb,#fef3c7);border-radius:12px;padding:12px 14px;margin-bottom:10px">
|
|
36
|
+
<div style="font-size:13px;font-weight:700;color:#92400e;margin-bottom:4px">🧪 ${t('测试托管订单')}</div>
|
|
37
|
+
<div style="font-size:12px;line-height:1.6;color:#374151">${t('本单为预发布演示:WAZ 是模拟货币,不代表真实价值,本协议尚未进行真实资金结算。请勿据此向任何人支付真实款项。')}</div>
|
|
38
|
+
</div>`
|
|
39
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Buyer price display (DISPLAY-ONLY) — render a product price in USDC with a real-time local-currency hint.
|
|
2
|
+
// fmtPrice(usdc) → a <span data-usdc-price> wrapper whose content is "<amount> USDC" + (when live rates + a
|
|
3
|
+
// non-USD buyer currency are available) a muted "≈ <local>". USDC is the display/pricing unit (money.ts:
|
|
4
|
+
// 1 WAZ = 1 USDC = 1e6 base-units, USDC-aligned); this NEVER implies WebAZ holds/settles real USDC —
|
|
5
|
+
// escrow is still simulated WAZ, direct-pay is off-platform.
|
|
6
|
+
// Local currency is derived from the buyer's account region (no manual picker). Rates come from /api/fx/rates
|
|
7
|
+
// (cached + fallback server-side). If rates are unavailable the price simply shows USDC — display never breaks.
|
|
8
|
+
// First-paint race: rates load async; when they arrive, refreshFxPrices() re-renders already-painted price
|
|
9
|
+
// nodes ([data-usdc-price] / [data-usdc-local]) so the "≈ local" hint appears without a full re-render.
|
|
10
|
+
window._fxRates = null
|
|
11
|
+
window.loadFxRates = async () => {
|
|
12
|
+
try {
|
|
13
|
+
const res = await fetch('/api/fx/rates', { signal: AbortSignal.timeout(6000) })
|
|
14
|
+
if (res.ok) { window._fxRates = await res.json(); window.refreshFxPrices() }
|
|
15
|
+
} catch { /* leave null → prices show USDC-only */ }
|
|
16
|
+
}
|
|
17
|
+
window.buyerCurrency = () => {
|
|
18
|
+
const map = { china: 'CNY', us: 'USD', eu: 'EUR', india: 'INR', singapore: 'SGD', global_north: 'USD', global: 'USD' }
|
|
19
|
+
const region = (window.state && window.state.user && window.state.user.region) || ''
|
|
20
|
+
return map[String(region).toLowerCase()] || 'USD'
|
|
21
|
+
}
|
|
22
|
+
// usdc amount → localized "≈ ¥X" string, or '' (USD buyer / no rates / unusable rate → show USDC only).
|
|
23
|
+
window._fxLocal = (usdc) => {
|
|
24
|
+
const r = window._fxRates
|
|
25
|
+
const cur = window.buyerCurrency()
|
|
26
|
+
if (!r || !r.rates || cur === 'USD') return ''
|
|
27
|
+
const rate = Number(r.rates[cur])
|
|
28
|
+
const n = Number(usdc)
|
|
29
|
+
if (!(rate > 0) || !Number.isFinite(n)) return ''
|
|
30
|
+
const local = n * rate
|
|
31
|
+
const sym = { CNY: '¥', EUR: '€', INR: '₹', SGD: 'S$', USD: '$', IDR: 'Rp', MYR: 'RM', PHP: '₱', VND: '₫', THB: '฿' }[cur] || (cur + ' ')
|
|
32
|
+
return sym + (local >= 100 ? String(Math.round(local)) : local.toFixed(2))
|
|
33
|
+
}
|
|
34
|
+
// visible price content (no wrapper): "<amount> USDC" + optional muted "≈ local".
|
|
35
|
+
window._fxPriceInner = (usdc) => {
|
|
36
|
+
const n = Number(usdc)
|
|
37
|
+
const disp = Number.isFinite(n) ? (Number.isInteger(n) ? String(n) : n.toFixed(2)) : ''
|
|
38
|
+
const base = disp + ' USDC'
|
|
39
|
+
const loc = window._fxLocal(usdc)
|
|
40
|
+
return loc ? base + ` <span style="opacity:.6;font-weight:400;font-size:.85em;white-space:nowrap">≈ ${loc}</span>` : base
|
|
41
|
+
}
|
|
42
|
+
// price (USDC decimal) → display HTML. data-usdc-price lets refreshFxPrices() update it once live rates arrive.
|
|
43
|
+
window.fmtPrice = (usdc) => `<span data-usdc-price="${Number(usdc)}">${window._fxPriceInner(usdc)}</span>`
|
|
44
|
+
// re-render already-painted price nodes after rates load (fixes the first-paint race). Safe no-op if no nodes.
|
|
45
|
+
window.refreshFxPrices = () => {
|
|
46
|
+
const doc = window.document
|
|
47
|
+
if (!doc || !doc.querySelectorAll) return
|
|
48
|
+
doc.querySelectorAll('[data-usdc-price]').forEach((el) => { el.innerHTML = window._fxPriceInner(el.getAttribute('data-usdc-price')) })
|
|
49
|
+
doc.querySelectorAll('[data-usdc-local]').forEach((el) => {
|
|
50
|
+
const p = el.getAttribute('data-usdc-local'); const loc = window._fxLocal(p)
|
|
51
|
+
el.innerHTML = 'USDC' + (loc ? ' ≈ ' + loc : '')
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
// fire-and-forget at load; rates are global (no auth), so they can load before the user object is known.
|
|
55
|
+
window.loadFxRates()
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Shared product-image resolver for cards / lists (paired with the public thumb endpoint).
|
|
2
|
+
// productThumbSrc(images): a stored image ref → a loadable <img src>. A 64-hex content hash → the public
|
|
3
|
+
// thumbnail endpoint /api/manifests/<hash>/thumb; an already-usable data:/http(s)/root-relative URL →
|
|
4
|
+
// passthrough; anything else / empty → '' (no image). `images` may be a JSON-array string, a real array,
|
|
5
|
+
// or a legacy CSV string. Card <img>s pair this with onerror="this.outerHTML='📦'" for the 404 (missing
|
|
6
|
+
// thumbnail manifest) fallback, so a bad ref degrades to the icon instead of a broken image.
|
|
7
|
+
window.productThumbSrc = (images) => {
|
|
8
|
+
let arr = images
|
|
9
|
+
if (typeof images === 'string') { try { arr = JSON.parse(images) } catch { arr = images.split(',') } }
|
|
10
|
+
const first = (Array.isArray(arr) ? arr : []).map(s => String(s).trim()).filter(Boolean)[0] || ''
|
|
11
|
+
if (!first) return ''
|
|
12
|
+
if (/^[0-9a-f]{64}$/i.test(first)) return '/api/manifests/' + first + '/thumb'
|
|
13
|
+
if (/^(https?:|data:|\/)/i.test(first)) return first
|
|
14
|
+
return ''
|
|
15
|
+
}
|