@seasonkoh/webaz 0.1.27 → 0.1.28

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.
@@ -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">${Number(it.min_price).toFixed(2)} <span style="font-size:11px;color:#9ca3af">WAZ</span></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">${myMin.toFixed(2)} WAZ</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">${globalMin.toFixed(2)} WAZ</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">${Number(o.price).toFixed(2)} <span style="font-size:11px;color:#9ca3af">WAZ</span></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) }