@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,1296 @@
1
+ // WebAZ — Discover / Search / Feed domain (classic multi-script split, slice F / app-discover.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; these pages run only on
6
+ // route/click (after app.js loads), so cross-file globals (GET/POST/state/shell/
7
+ // escHtml/navigate/t/toast$/skeleton$/productImageGallery/feedActor/feedEmpty/
8
+ // pageHotFeedToggle/ensureProfileMini/...) resolve at call time. No import/export.
9
+ //
10
+ // Pure relocation of the browse/discover surfaces: shop entry + smart-buy header,
11
+ // the intent-driven #buy search flow, #discover + type/sort chips + feed views,
12
+ // #new arrivals, and #search results. Domain-only helpers move with them
13
+ // (buyResultCardHtml/computeBuyReasons/smartRecognitionLine/saveRecentSearch/
14
+ // searchByKeyword/productCardHtml). Platform-wide helpers (toast$/skeleton$/
15
+ // feedActor/feedEmpty/pageHotFeedToggle/ensureProfileMini/productImageGallery/…)
16
+ // and the profile/nearby/product-detail/cart/order surfaces stay in app.js.
17
+ //
18
+ // No money/order/payment/wallet/cart/dispute/status path. No UI/behavior change.
19
+
20
+ // ─── 商店页 ───────────────────────────────────────────────────
21
+
22
+ async function renderShop(app, opts = {}) {
23
+ const activeTab = opts.expand ? 'agent-buy' : 'shop'
24
+ app.innerHTML = shell(loading$(), activeTab)
25
+ const products = await GET('/products')
26
+
27
+ const grid = products.length === 0
28
+ ? `<div class="empty"><div class="empty-icon">🛍️</div><div class="empty-text">${t('暂无商品')}</div></div>`
29
+ : `<div class="product-grid">
30
+ ${products.map(p => `
31
+ <div class="product-card" onclick="navigate('#order-product/${p.id}')">
32
+ <div class="product-img">${getCategoryIcon(p.category)}</div>
33
+ <div class="product-body">
34
+ <div class="product-name">${escHtml(p.title)}</div>
35
+ <div class="product-price">${p.price} <span style="font-size:11px;font-weight:400">WAZ</span></div>
36
+ <div class="product-seller">${repBadge(p.rep_level)}@${escHtml(p.seller_name)}</div>
37
+ </div>
38
+ </div>`).join('')}
39
+ </div>`
40
+
41
+ const compactBanner = `
42
+ <div onclick="navigate('#shop/agent')" style="background:linear-gradient(135deg,#6366f1,#8b5cf6);border-radius:12px;padding:14px 16px;margin-bottom:16px;cursor:pointer;display:flex;align-items:center;gap:12px">
43
+ <span style="font-size:28px">🤖</span>
44
+ <div>
45
+ <div style="color:#fff;font-weight:600;font-size:14px">${t('智能下单')}</div>
46
+ <div style="color:rgba(255,255,255,0.8);font-size:12px">${t('粘贴任意平台链接,AI 帮你找更优方案')}</div>
47
+ </div>
48
+ <span style="margin-left:auto;color:rgba(255,255,255,0.7);font-size:18px">›</span>
49
+ </div>`
50
+
51
+ const expandedAgentBuy = `
52
+ <div class="card" style="margin-bottom:16px;background:linear-gradient(135deg,#eef2ff,#faf5ff);border-color:#c7d2fe">
53
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px">
54
+ <div style="display:flex;align-items:center;gap:8px">
55
+ <span style="font-size:22px">🤖</span>
56
+ <strong style="font-size:15px">${t('智能下单')}</strong>
57
+ </div>
58
+ <button class="btn btn-outline btn-sm" style="width:auto;padding:4px 10px" onclick="navigate('#shop')">${t('收起')}</button>
59
+ </div>
60
+ <p style="color:#6b7280;font-size:12px;margin-bottom:10px">${t('粘贴商品链接,AI 自动搜索 WebAZ 更优方案,可一键下单')}</p>
61
+ <div class="form-group">
62
+ <label class="form-label" style="font-size:12px">${t('商品链接')}</label>
63
+ <input class="form-control" id="ab-url" placeholder="${t('粘贴淘宝 / 京东 / 亚马逊等链接')}" style="font-size:13px">
64
+ </div>
65
+ <div class="form-group">
66
+ <label class="form-label" style="font-size:12px">${t('收货地址')}</label>
67
+ <input class="form-control" id="ab-addr" placeholder="${t('省市区街道,用于自动下单')}" style="font-size:13px">
68
+ </div>
69
+ <div style="display:flex;align-items:center;gap:8px;margin-bottom:12px">
70
+ <input type="checkbox" id="ab-auto" style="width:16px;height:16px">
71
+ <label for="ab-auto" style="font-size:12px;cursor:pointer">${t('找到更优方案后自动下单(否则仅展示比价结果)')}</label>
72
+ </div>
73
+ <button class="btn btn-primary" id="ab-btn" onclick="doAgentBuy()">${t('开始分析')}</button>
74
+ <div id="ab-result"></div>
75
+ </div>`
76
+
77
+ const agentBuyBanner = state.user?.role === 'buyer'
78
+ ? (opts.expand ? expandedAgentBuy : compactBanner)
79
+ : ''
80
+
81
+ app.innerHTML = shell(`
82
+ <h1 class="page-title">${t('发现好物')}</h1>
83
+ ${agentBuyBanner}
84
+ <div class="search-bar">
85
+ <div class="search-input-wrap" id="search-wrap">
86
+ <input class="search-input" id="search-inp" placeholder="${t('搜索 / 粘贴外链或分享文本')}" onkeydown="if(event.key==='Enter')doSearch()" oninput="toggleSearchClear()">
87
+ <button type="button" class="search-clear" onclick="clearSearchInput()" aria-label="${t('清空')}">×</button>
88
+ </div>
89
+ <button class="btn btn-primary btn-sm" style="width:auto;padding:10px 12px" onclick="doSearch()" title="${t('一字不差完全匹配')}">${t('精准')}</button>
90
+ <button class="btn btn-outline btn-sm" style="width:auto;padding:10px 12px" onclick="doFuzzySearch()" title="${t('部分匹配(命中≥50%)')}">${t('模糊')}</button>
91
+ </div>
92
+ <div style="margin-bottom:16px;display:flex;gap:8px;flex-wrap:wrap">
93
+ <button class="btn btn-outline btn-sm" style="width:auto" onclick="navigate('#skills')">${t('⚡ Skill 市场')}</button>
94
+ <button class="btn btn-outline btn-sm" style="width:auto" onclick="navigate('#verify-tasks')">${t('🛡️ 验证任务')}</button>
95
+ </div>
96
+ <div id="product-list">${grid}</div>
97
+ `, activeTab)
98
+ }
99
+
100
+ // ─── P10:智能购买 / 发现好物 / 新品发现 ─────────────────────
101
+
102
+ // 统一头部:输入框(左侧扫码 + 粘贴气泡 + 右侧清空 ×)+ 主搜索按钮 + 拍照搜图
103
+ // active='discover'/'new'/'nearby' → 发现页群组,placeholder 提示"模糊匹配",预填上次搜索词
104
+ // active='buy' (default) → 智能下单页,placeholder 提示"精确匹配"
105
+ function renderSmartBuyHeader(active) {
106
+ // 2026-05-24 #975:6 子页 + #buy 各自独立 scope(state key + placeholder + 输入 name)
107
+ // autocomplete=off + 每个 scope 的 input name 唯一,阻断浏览器跨页 autofill / 历史合并
108
+ const SCOPE = {
109
+ buy: { ph: '精确匹配:商品标题 / 外链 / 口令 / hash', key: null, name: 'webaz-q-buy' },
110
+ discover: { ph: '模糊搜索:标题 / 描述 / 类目 | 精准匹配', key: '_discoverQ', name: 'webaz-q-discover' },
111
+ new: { ph: '搜新品:未成交的最新上架(标题 / 描述)', key: '_newQ', name: 'webaz-q-new' },
112
+ nearby: { ph: '在你 11km 雷达内搜:top 热门商品标题', key: '_nearbyQ', name: 'webaz-q-nearby' },
113
+ auctions: { ph: '在拍卖 + 二手内搜:标题 / 备注', key: '_aucQ', name: 'webaz-q-auctions' },
114
+ rfq: { ph: '在求购单内搜:标题 / 备注', key: '_rfqQ', name: 'webaz-q-rfq' },
115
+ wishes: { ph: '在许愿池内搜:标题 / 内容', key: '_wishQ', name: 'webaz-q-wishes' },
116
+ }
117
+ const sc = SCOPE[active] || SCOPE.buy
118
+ const placeholder = t(sc.ph)
119
+ const prefill = sc.key ? (state[sc.key] || '') : ''
120
+ const inputName = sc.name
121
+ const isDiscover = active === 'discover' || active === 'new' || active === 'nearby'
122
+ return `
123
+ <div class="smart-buy-header" style="margin-bottom:14px">
124
+ <div class="search-bar">
125
+ <div class="search-input-wrap sbh-with-leading${prefill ? ' has-value' : ''}" id="sbh-search-wrap">
126
+ <button type="button" class="sbh-leading-scan" onclick="startQrScan()" aria-label="${t('扫码')}" title="${t('扫码(二维码 / 条码)')}">${SVG_SCAN}</button>
127
+ <input id="sbh-search-inp" class="search-input"
128
+ name="${inputName}"
129
+ autocomplete="off"
130
+ autocorrect="off"
131
+ autocapitalize="off"
132
+ spellcheck="false"
133
+ data-form-type="search"
134
+ data-1p-ignore="true"
135
+ placeholder="${placeholder}"
136
+ value="${escAttr(prefill)}"
137
+ onkeydown="if(event.key==='Enter')smartHeaderSearch()"
138
+ oninput="toggleSbhClear()"
139
+ onfocus="checkClipboardForSmartBuy()"
140
+ onblur="setTimeout(hidePasteFloat, 200)">
141
+ <button type="button" id="sbh-paste-float" class="sbh-paste-float" onclick="usePasteHint()" style="display:none">
142
+ ${SVG_PASTE}
143
+ <span>${t('粘贴')}</span>
144
+ </button>
145
+ <button type="button" class="search-clear" onclick="clearSbhInput()" aria-label="${t('清空')}">×</button>
146
+ </div>
147
+ <button class="btn btn-primary btn-sm" style="width:auto;padding:10px 14px;display:inline-flex;align-items:center;justify-content:center" onclick="smartHeaderSearch()" title="${t('搜索')}">${SVG_SEARCH}</button>
148
+ <button class="btn btn-outline btn-sm" style="width:auto;padding:10px 12px;display:inline-flex;align-items:center;justify-content:center" onclick="startVisualSearch()" title="${t('拍照搜图')}">${SVG_CAMERA}</button>
149
+ <input type="file" id="sbh-visual-file" accept="image/*" capture="environment" style="display:none" onchange="onVisualSearchPick(event)">
150
+ </div>
151
+ ${isDiscover && prefill ? `<div style="margin-top:6px;font-size:11px;color:var(--gray-500);display:flex;align-items:center;gap:6px;flex-wrap:wrap"><span>🔎 ${t('正在筛选')}: <strong style="color:var(--gray-700)">${escHtml(prefill)}</strong>${state._discoverMatchMode === 'fuzzy' ? ` <span style="background:#fef3c7;color:#92400e;padding:1px 6px;border-radius:99px;font-size:10px;font-weight:600;margin-left:4px">${t('模糊匹配')}</span>` : state._discoverMatchMode === 'strict' ? ` <span style="background:#dcfce7;color:#166534;padding:1px 6px;border-radius:99px;font-size:10px;font-weight:600;margin-left:4px">${t('精确匹配')}</span>` : ''}</span><button onclick="clearDiscoverQuery()" style="background:none;border:none;color:var(--primary);font-size:11px;cursor:pointer;padding:0">${t('清除筛选')}</button></div>` : ''}
152
+ </div>`
153
+ }
154
+
155
+ // 拍照搜图 — 调原生相机或相册选图;后端 /api/ai/image-search 尚未实现,先把图片预填到 input 并提示
156
+ window.startVisualSearch = () => {
157
+ const f = document.getElementById('sbh-visual-file')
158
+ if (f) f.click()
159
+ }
160
+
161
+ window.onVisualSearchPick = async (e) => {
162
+ const file = e.target.files?.[0]
163
+ e.target.value = '' // 允许同一文件再次选择
164
+ if (!file) return
165
+ // P0 占位:图像搜索后端待接入;先告知用户
166
+ // 后续可改为 POST /api/ai/image-search (multipart) → 返回 candidates,渲染到 smart-results
167
+ toast$(t('拍照搜图功能即将上线 — 已暂存图片:') + file.name)
168
+ }
169
+
170
+ // 剪贴板智能识别:输入框 focus 时尝试读 clipboard;命中则在输入框上方浮出"粘贴链接"按钮(iOS 风格)
171
+ window.checkClipboardForSmartBuy = async () => {
172
+ if (window.__sbhPasteChecked) return
173
+ window.__sbhPasteChecked = true
174
+ if (!navigator.clipboard?.readText) return
175
+ let txt = ''
176
+ try { txt = (await navigator.clipboard.readText()).trim() } catch { return }
177
+ if (!txt) return
178
+ if (txt.length < 4 || txt.length > 500) return
179
+ if (txt.split('\n').length > 3) return
180
+ const inp = document.getElementById('sbh-search-inp')
181
+ if (!inp) return
182
+ if (inp.value.trim() === txt || inp.value.trim()) return // 已有输入不打扰
183
+ const isUrl = /^https?:\/\//i.test(txt) || /https?:\/\/\S+/i.test(txt)
184
+ const looksLikeProduct = isUrl || /^[^\s,,。!?!?;;][一-龥\w\s\-+().×\/]{3,80}$/.test(txt)
185
+ if (!looksLikeProduct) return
186
+ window.__sbhPasteText = txt
187
+ const fb = document.getElementById('sbh-paste-float')
188
+ if (fb) {
189
+ fb.title = (txt.length > 60 ? txt.slice(0, 60) + '…' : txt)
190
+ fb.style.display = 'inline-flex'
191
+ fb.classList.add('sbh-paste-float-in')
192
+ }
193
+ }
194
+
195
+ window.hidePasteFloat = () => {
196
+ const fb = document.getElementById('sbh-paste-float')
197
+ if (fb) fb.style.display = 'none'
198
+ }
199
+
200
+ window.usePasteHint = () => {
201
+ const txt = window.__sbhPasteText
202
+ const inp = document.getElementById('sbh-search-inp')
203
+ if (!inp || !txt) { hidePasteFloat(); return }
204
+ inp.value = txt
205
+ inp.dispatchEvent(new Event('input'))
206
+ hidePasteFloat()
207
+ window.__sbhPasteText = null
208
+ smartHeaderSearch()
209
+ }
210
+
211
+ // 通用语音输入 helper — 复用 WebSpeech API,UI 通用化
212
+ window.startVoiceInput = (inputId, onResult) => {
213
+ const SR = window.webkitSpeechRecognition || window.SpeechRecognition
214
+ if (!SR) {
215
+ // iOS Safari 没有 Web Speech API — 引导用户用系统键盘麦克风键(输入法栏)
216
+ const inp = document.getElementById(inputId)
217
+ if (inp) inp.focus()
218
+ alert(t('此浏览器无内置语音 — 请点击输入框后用键盘上的 🎤 键说话'))
219
+ return
220
+ }
221
+ const inp = document.getElementById(inputId)
222
+ if (!inp) return
223
+ const btn = document.getElementById('sbh-voice-btn') || document.querySelector(`button[onclick*="${inputId}"]`)
224
+ const rec = new SR()
225
+ rec.lang = (window._lang === 'en' ? 'en-US' : 'zh-CN')
226
+ rec.interimResults = true
227
+ rec.continuous = false
228
+ if (btn) { btn.style.background = '#dc2626'; btn.style.color = '#fff'; btn.innerHTML = SVG_MIC_REC }
229
+ rec.onresult = (e) => {
230
+ let txt = ''
231
+ for (let i = 0; i < e.results.length; i++) txt += e.results[i][0].transcript
232
+ inp.value = txt
233
+ inp.dispatchEvent(new Event('input'))
234
+ }
235
+ rec.onend = () => {
236
+ if (btn) { btn.style.background = ''; btn.style.color = ''; btn.innerHTML = SVG_MIC }
237
+ if (typeof onResult === 'function' && inp.value.trim()) setTimeout(onResult, 100)
238
+ }
239
+ rec.onerror = (e) => {
240
+ if (btn) { btn.style.background = ''; btn.style.color = ''; btn.innerHTML = SVG_MIC }
241
+ if (e.error === 'not-allowed') alert(t('请允许麦克风权限'))
242
+ }
243
+ try { rec.start() } catch (e) { console.warn('SR start failed', e) }
244
+ }
245
+
246
+ window.toggleSbhClear = () => {
247
+ const inp = document.getElementById('sbh-search-inp')
248
+ const wrap = document.getElementById('sbh-search-wrap')
249
+ if (inp && wrap) wrap.classList.toggle('has-value', !!inp.value)
250
+ // 用户开始输入 → 隐藏剪贴板悬浮按钮(不再打扰)
251
+ if (inp && inp.value) hidePasteFloat()
252
+ }
253
+ window.clearSbhInput = () => {
254
+ const inp = document.getElementById('sbh-search-inp')
255
+ if (inp) { inp.value = ''; inp.focus(); toggleSbhClear() }
256
+ // 发现/新品/附近:清掉 DOM 输入的同时也清掉 state 里的查询,否则切换 banner 会被 prefill 回来
257
+ if (state._discoverQ) {
258
+ state._discoverQ = ''
259
+ const h = location.hash
260
+ if (h.startsWith('#discover/new')) renderNewArrivals(document.getElementById('app'))
261
+ else if (h.startsWith('#discover')) renderDiscover(document.getElementById('app'))
262
+ else if (h.startsWith('#nearby')) { try { renderNearby(document.getElementById('app')) } catch {} }
263
+ }
264
+ }
265
+
266
+ // 清除当前发现页的模糊查询并重渲
267
+ window.clearDiscoverQuery = () => {
268
+ state._discoverQ = ''
269
+ if (location.hash.startsWith('#discover/new')) renderNewArrivals(document.getElementById('app'))
270
+ else renderDiscover(document.getElementById('app'))
271
+ }
272
+
273
+ // 6-pill 顶部 banner 切换:换 tab 时清掉旧的模糊查询,避免不同板块互相串味
274
+ window.switchDiscoverBanner = (hash) => {
275
+ state._discoverQ = ''
276
+ location.hash = hash
277
+ }
278
+
279
+ // 从智能下单 "无精确匹配" → 一键带 query 跳发现页并触发模糊搜索
280
+ window.goDiscoverWithQuery = (q) => {
281
+ state._discoverQ = String(q || '').trim()
282
+ navigate('#discover')
283
+ }
284
+ window.smartHeaderSearch = () => {
285
+ const raw = document.getElementById('sbh-search-inp')?.value?.trim() || ''
286
+ const h = location.hash
287
+ const app = document.getElementById('app')
288
+ // 2026-05-24 #974:scoped 搜索路由表 — 每个域用各自 query state
289
+ const scopeMap = [
290
+ { test: h => h.startsWith('#discover/new'), key: '_newQ', render: () => renderNewArrivals(app) },
291
+ { test: h => h.startsWith('#discover'), key: '_discoverQ', render: () => renderDiscover(app) },
292
+ { test: h => h.startsWith('#nearby'), key: '_nearbyQ', render: () => { try { renderNearby(app) } catch {} } },
293
+ { test: h => h.startsWith('#auctions/feed'),key: '_aucQ', render: () => renderAuctionsFeed(app) },
294
+ { test: h => h.startsWith('#auctions'), key: '_aucQ', render: () => renderAuctionBoard(app) },
295
+ { test: h => h.startsWith('#secondhand'), key: '_aucQ', render: () => { _shFilters.q = state._aucQ || ''; renderSecondhandMarket(app) } },
296
+ { test: h => h.startsWith('#rfq/new/feed'), key: '_rfqQ', render: () => renderRfqFeed(app) },
297
+ { test: h => h.startsWith('#rfq/new'), key: '_rfqQ', render: () => renderRfqCreate(app) },
298
+ { test: h => h.startsWith('#rfqs'), key: '_rfqQ', render: () => renderRfqBoard(app) },
299
+ { test: h => h.startsWith('#wishes/feed'), key: '_wishQ', render: () => renderWishesFeed(app) },
300
+ { test: h => h.startsWith('#wishes'), key: '_wishQ', render: () => renderWishBoard(app) },
301
+ ]
302
+ const scope = scopeMap.find(s => s.test(h))
303
+
304
+ // 空输入:清掉旧 query 重渲
305
+ if (!raw) {
306
+ if (scope) {
307
+ const hadQuery = !!state[scope.key]
308
+ state[scope.key] = ''
309
+ if (hadQuery) scope.render()
310
+ }
311
+ return
312
+ }
313
+ // 智能下单 (#buy) → 协议级精确匹配
314
+ if (h === '#buy' || h === '#' || h === '') {
315
+ smartSearchExec(raw)
316
+ return
317
+ }
318
+ // 域内搜索 — 留在本页过滤
319
+ if (scope) {
320
+ state[scope.key] = raw
321
+ scope.render()
322
+ return
323
+ }
324
+ // 其他页面 → 暂存 + 跳转 #buy
325
+ sessionStorage.setItem('webaz_pending_search', raw)
326
+ navigate('#buy')
327
+ }
328
+
329
+ // M7.1:智能下单 = 意图驱动 (intent-driven) 购买入口
330
+ // 删除 filter 面板(filter 是浏览工具,与 intent-driven 冲突)
331
+ // 删除批量粘贴 details(批量场景去 MCP / agent SDK)
332
+ // 页面只展示:搜索框 + 空状态引导 / 搜索结果
333
+ async function renderBuy(app) {
334
+ app.innerHTML = shell(loading$(), 'buy')
335
+ await ensureProfileMini()
336
+ // 每次进 #buy 都重置剪贴板检查(不主动 read — 等输入框 focus 触发,符合浏览器手势策略)
337
+ window.__sbhPasteChecked = false
338
+
339
+ // 未登录 + 有分享 ctx → 顶部 hero banner(引导注册)
340
+ const heroBanner = !state.user ? renderShareBanner('hero') : ''
341
+
342
+ // 最近搜过(localStorage,最多 8 条)
343
+ const recent = (() => {
344
+ try { return JSON.parse(localStorage.getItem('webaz_recent_searches') || '[]').slice(0, 8) } catch { return [] }
345
+ })()
346
+ const recentChips = state.user && recent.length > 0 ? `
347
+ <div style="margin-bottom:14px">
348
+ <div style="font-size:11px;color:#9ca3af;margin-bottom:6px">${t('最近搜过')}</div>
349
+ <div style="display:flex;flex-wrap:wrap;gap:6px">
350
+ ${recent.map(q => `<button onclick="repeatSearch(${JSON.stringify(q).replace(/"/g, '&quot;')})" style="padding:4px 10px;border-radius:99px;background:#f3f4f6;border:1px solid #e5e7eb;font-size:11px;color:#374151;cursor:pointer;max-width:160px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${escHtml(q)}</button>`).join('')}
351
+ <button onclick="clearRecentSearches()" style="padding:4px 10px;border-radius:99px;background:transparent;border:1px dashed #d1d5db;font-size:11px;color:#9ca3af;cursor:pointer">${t('清空')}</button>
352
+ </div>
353
+ </div>
354
+ ` : ''
355
+
356
+ // AI 助手快捷入口(仅登录)— 跳转 #ai-recommend 全屏页(深度对话/任务)
357
+ // "让 AI 帮我搜" 入口已撤除(保留悬浮 🤖 FAB + 我的页 AI 推荐 tile 作为入口)
358
+ const aiShortcut = ''
359
+
360
+ app.innerHTML = shell(`
361
+ ${heroBanner}
362
+ ${notePromptPlaceholder('buy')}
363
+ ${!state.user && readShareCtx()?.sponsor_id ? `<div style="margin-bottom:12px;text-align:center"><button class="btn btn-primary" onclick="location.hash='#login'">📝 ${t('注册解锁全部功能')}</button></div>` : ''}
364
+ ${renderSmartBuyHeader('buy')}
365
+ ${aiShortcut}
366
+ ${recentChips}
367
+ <div id="smart-results">${renderBuyEmptyState()}</div>
368
+ `, 'buy')
369
+ hydrateNotePrompt('buy')
370
+
371
+ // 跨页搜索接续:上一页的 header 搜索 → 跳到 #buy 后取出执行
372
+ const pending = sessionStorage.getItem('webaz_pending_search')
373
+ if (pending) {
374
+ sessionStorage.removeItem('webaz_pending_search')
375
+ setTimeout(() => {
376
+ const hdr = document.getElementById('sbh-search-inp')
377
+ if (hdr) { hdr.value = pending; smartSearchExec(pending) }
378
+ }, 80)
379
+ }
380
+ }
381
+
382
+ // 识别回显行:让"系统把你的输入判定为哪种表达式"对用户可见(易懂)
383
+ // kind: keyword | url | anchor | hash;detail = 关键词 / 口令 code(会被转义)
384
+ function smartRecognitionLine(kind, detail) {
385
+ const d = escHtml(String(detail || '').trim())
386
+ const map = {
387
+ keyword: '🔍 ' + t('按商品标题精确匹配') + (d ? ` 「${d}」` : ''),
388
+ url: '🔗 ' + t('识别为外部链接 · 正为你比价 WebAZ 同款'),
389
+ anchor: '🎫 ' + t('识别为达人口令') + (d ? ` @${d}` : '') + ' · ' + t('正在跳转 TA 推荐'),
390
+ hash: '🔖 ' + t('识别为内容指纹 · 正在打开来源验证'),
391
+ }
392
+ const txt = map[kind]
393
+ if (!txt) return ''
394
+ return `<div style="font-size:12px;color:#4338ca;background:#eef2ff;border:1px solid #e0e7ff;border-radius:8px;padding:8px 12px;margin-bottom:10px;line-height:1.5">${txt}</div>`
395
+ }
396
+
397
+ // 空状态:引导 intent + 求购单引导 + 隐性卖家入口(不主动推荐分发)
398
+ function renderBuyEmptyState() {
399
+ const rfqTarget = !state.user ? '#login' : '#rfq/new'
400
+ return `
401
+ <div style="margin-top:20px">
402
+ <div class="card" style="background:linear-gradient(135deg,#eef2ff 0%,#faf5ff 100%);border-color:#c7d2fe;padding:18px">
403
+ <div style="font-size:15px;font-weight:700;color:#4338ca;margin-bottom:10px">🎯 ${t('智能下单 = 知道要买什么,帮你买')}</div>
404
+ <div style="font-size:12px;color:#4b5563;line-height:1.8;margin-bottom:12px">
405
+ · ${t('输入商品标题 → 直达对应商品下单')}<br>
406
+ · ${t('粘贴外部链接 → WebAZ 同款推荐')}<br>
407
+ · ${t('输入口令 @xxx → 跳到达人推荐的商品')}<br>
408
+ · ${t('输入 P2P 内容 hash → 验证内容来源')}
409
+ </div>
410
+ <div style="font-size:11px;color:#9ca3af">${t('协议级承诺:不做模糊推测,不主动推荐分发')}</div>
411
+ </div>
412
+
413
+ <div style="margin-top:14px;padding:12px 14px;background:#fff;border:1px dashed #bfdbfe;border-radius:8px;display:flex;align-items:center;justify-content:space-between;gap:10px">
414
+ <div style="font-size:12px;color:#1e40af;line-height:1.5">
415
+ 💬 ${t('找不到合适商品?发个求购单让卖家来抢')}
416
+ </div>
417
+ <a href="${rfqTarget}" style="font-size:12px;color:#1d4ed8;font-weight:600;text-decoration:none;white-space:nowrap">${t('发求购单 →')}</a>
418
+ </div>
419
+
420
+ <div style="margin-top:10px;padding:12px 14px;background:#fff;border:1px dashed #fde68a;border-radius:8px;display:flex;align-items:center;justify-content:space-between;gap:10px">
421
+ <div style="font-size:12px;color:#78350f;line-height:1.5">
422
+ 🛒 ${t('你也想让你的商品出现在这里?')}
423
+ </div>
424
+ <a href="javascript:void(0)" onclick="goCreateListingFromBuy('')" style="font-size:12px;color:#92400e;font-weight:600;text-decoration:none;white-space:nowrap;cursor:pointer">${t('上架商品 →')}</a>
425
+ </div>
426
+ </div>`
427
+ }
428
+
429
+ // 无匹配状态:iOS 风极简 — 三层卡片:诚实告知 + 模糊搜索引导 + 上架 CTA
430
+ // 上架卡片只露最简引导,详细原因 / 步骤折叠到 <details> 里
431
+ function renderBuyNoMatchState(query) {
432
+ const ctaLabel = !state.user ? t('注册并上架商品') : t('我也要上架商品')
433
+ const safeQAttr = escAttr(String(query || '').trim())
434
+ return `
435
+ <div style="margin-top:18px;display:flex;flex-direction:column;gap:10px">
436
+
437
+ ${smartRecognitionLine('keyword', query)}
438
+
439
+ <!-- 1. 诚实告知(回显输入 + 友好解释)-->
440
+ <div style="padding:14px 16px;background:#fff;border:0.5px solid #e5e7eb;border-radius:12px">
441
+ <div style="font-size:15px;font-weight:600;color:#1f2937;margin-bottom:4px">${t('没找到完全一致的商品')}</div>
442
+ <div style="font-size:12px;color:#8e8e93;line-height:1.5">${t('智能下单按商品标题精确匹配,该商品可能还没上架 — 试试下面两种方式。')}</div>
443
+ </div>
444
+
445
+ <!-- 2. 模糊搜索(次要 CTA) -->
446
+ <button data-q="${safeQAttr}" onclick="goDiscoverWithQuery(this.dataset.q)" style="padding:14px 16px;background:#fff;border:0.5px solid #e5e7eb;border-radius:12px;display:flex;justify-content:space-between;align-items:center;width:100%;cursor:pointer;font:inherit;text-align:left">
447
+ <div>
448
+ <div style="font-size:14px;font-weight:500;color:#1f2937">${t('换个方式搜')}</div>
449
+ <div style="font-size:12px;color:#8e8e93;margin-top:2px">${t('发现页支持模糊匹配')}</div>
450
+ </div>
451
+ <span style="color:#007aff;font-size:14px;font-weight:500">${t('去模糊搜索')} →</span>
452
+ </button>
453
+
454
+ <!-- 3. 上架 CTA(主推) — 极简化:标题 + 一句话 + 大按钮 + 折叠详情 -->
455
+ <div style="padding:18px 16px 14px;background:#fff;border:0.5px solid #e5e7eb;border-radius:12px">
456
+ <div style="font-size:15px;font-weight:600;color:#1f2937;margin-bottom:4px">${t('让你的商品也出现在这里')}</div>
457
+ <div style="font-size:12px;color:#8e8e93;line-height:1.5;margin-bottom:14px">${t('上架后买家精准搜索时即可命中。')}</div>
458
+
459
+ <button data-q="${safeQAttr}" onclick="goCreateListingFromBuy(this.dataset.q)" style="width:100%;padding:13px;background:#007aff;color:#fff;border:none;border-radius:10px;font-size:15px;font-weight:600;cursor:pointer;letter-spacing:0.1px">
460
+ ${ctaLabel}
461
+ </button>
462
+
463
+ <details style="margin-top:12px">
464
+ <summary style="font-size:12px;color:#007aff;cursor:pointer;list-style:none;text-align:center;padding:4px">${t('了解详情')}</summary>
465
+ <div style="margin-top:12px;padding-top:12px;border-top:0.5px solid #f3f4f6">
466
+ <div style="font-size:12px;font-weight:600;color:#1f2937;margin-bottom:6px">${t('为什么上架到 WebAZ')}</div>
467
+ <div style="font-size:12px;color:#3c3c43;line-height:1.7;margin-bottom:12px">
468
+ ${t('协议费仅 2% · 分享成交拿 commission · 链上稳定币直达 · Agent 自动比价命中')}
469
+ </div>
470
+ <div style="font-size:12px;font-weight:600;color:#1f2937;margin-bottom:6px">${t('上架只需 3 步')}</div>
471
+ <div style="font-size:12px;color:#3c3c43;line-height:1.7">
472
+ ${t('① 注册或登录(已登录可直接进卖家中心)')}<br>
473
+ ${t('② 粘贴外部链接 → 系统自动提取标题、价格、alias')}<br>
474
+ ${t('③ 设你的 WebAZ 价 → 一键上架(首单成交时自动锁定 stake)')}
475
+ </div>
476
+ <div style="font-size:11px;color:#8e8e93;margin-top:12px">${t('零月费 · 零上架成本 · 协议级买家保护(托管 + 仲裁 + 卖家信誉公开)')}</div>
477
+ </div>
478
+ </details>
479
+ </div>
480
+
481
+ ${query ? `<div style="font-size:11px;color:#8e8e93;text-align:center;margin-top:6px">${t('当前搜索')}:「${escHtml(query)}」 · ${t('换个关键词重试')}</div>` : ''}
482
+ </div>`
483
+ }
484
+
485
+ // M7.1:智能识别 + 路由(URL → 比价;hex hash → P2P;关键词 → 精准搜索)
486
+ // 入口统一为 header 搜索框(id sbh-search-inp);不再从 #smart-search textarea 读
487
+ window.repeatSearch = (q) => {
488
+ const inp = document.getElementById('sbh-search-inp')
489
+ if (inp) inp.value = q
490
+ smartSearchExec(q)
491
+ }
492
+ window.clearRecentSearches = () => {
493
+ localStorage.removeItem('webaz_recent_searches')
494
+ renderBuy(document.getElementById('app'))
495
+ }
496
+ function saveRecentSearch(q) {
497
+ if (!q || q.length < 2 || q.length > 100) return
498
+ try {
499
+ const arr = JSON.parse(localStorage.getItem('webaz_recent_searches') || '[]')
500
+ const filtered = arr.filter(x => x !== q)
501
+ filtered.unshift(q)
502
+ localStorage.setItem('webaz_recent_searches', JSON.stringify(filtered.slice(0, 20)))
503
+ } catch {}
504
+ }
505
+
506
+ window.smartSearchExec = async (overrideQuery) => {
507
+ const raw = (overrideQuery || document.getElementById('sbh-search-inp')?.value || '').trim()
508
+ const results = document.getElementById('smart-results')
509
+ if (!results) return
510
+ if (!raw) { results.innerHTML = renderBuyEmptyState(); return }
511
+ saveRecentSearch(raw)
512
+
513
+ const urls = extractUrls(raw)
514
+ const addr = state.profileMini?.default_address_text
515
+
516
+ // URL 比价(保留 agent-buy 流程;auto_buy 已删除 — 走默认 non-auto)
517
+ if (urls.length > 0) {
518
+ results.innerHTML = loading$()
519
+ if (urls.length > 1) {
520
+ results.innerHTML = `<div id="ab-result"></div>`
521
+ await doBatchBuy(urls, addr || '', false)
522
+ } else {
523
+ const res = await POST('/agent-buy', { source_url: urls[0], shipping_address: addr || undefined, auto_buy: false })
524
+ if (res.error) results.innerHTML = alert$('error', res.error)
525
+ else { addPasteHistory(urls[0]); renderAgentBuyResultInto(results, res) }
526
+ }
527
+ return
528
+ }
529
+
530
+ // P2P 内容 hash
531
+ if (/^[a-f0-9]{64}$/.test(raw)) {
532
+ results.innerHTML = smartRecognitionLine('hash')
533
+ toast$(t('识别为内容指纹 · 正在打开来源验证'))
534
+ openNativeReview(raw)
535
+ return
536
+ }
537
+
538
+ // 口令 anchor:以 @ 开头或长度 7-20 全字母数字(含数字)→ 跳 #anchor 走 lookup
539
+ if (/^@[a-z0-9._]{6,20}$/i.test(raw)) {
540
+ const code = raw.slice(1).toLowerCase()
541
+ toast$(t('识别为达人口令') + ` @${code} · ` + t('正在跳转 TA 推荐'))
542
+ navigate('#anchor?code=' + code)
543
+ return
544
+ }
545
+
546
+ // 关键字搜索
547
+ await searchByKeyword(raw)
548
+ }
549
+
550
+ function renderAgentBuyResultInto(container, res) {
551
+ const recColor = { buy_webaz: '#16a34a', buy_source: '#2563eb', no_match: '#6b7280' }[res.recommendation] || '#6b7280'
552
+ const recLabel = { buy_webaz: t('✅ 推荐 WebAZ 方案'), buy_source: t('🔗 建议继续在原平台购买'), no_match: t('😕 暂未找到合适替代') }[res.recommendation] || ''
553
+ const bestCard = res.best_product ? `
554
+ <div style="background:#f0fdf4;border:1px solid #bbf7d0;border-radius:10px;padding:12px;margin:12px 0">
555
+ <div style="font-weight:600;font-size:14px;margin-bottom:4px">${escHtml(res.best_product.title)}</div>
556
+ <div style="font-size:18px;font-weight:700;color:#16a34a;margin-bottom:4px">${res.best_product.price} WAZ</div>
557
+ <div style="font-size:12px;color:#6b7280;margin-bottom:8px">${escHtml(res.best_product.agent_summary || '')}</div>
558
+ ${!res.auto_bought ? `<button class="btn btn-primary btn-sm" style="width:auto" onclick="navigate('#order-product/${res.best_product.id}')">${t('查看并下单')}</button>` : ''}
559
+ </div>` : ''
560
+ const orderCard = res.auto_bought ? `
561
+ <div class="alert alert-success" style="margin-top:12px">
562
+ <strong>${t('已自动下单!')}</strong> ${t('订单号')}:<a href="#order/${res.order_id}" style="color:#16a34a;font-weight:600">${res.order_id}</a><br>
563
+ <span style="font-size:12px">${t('金额')}:${res.verified_price} WAZ ${t('(已从钱包托管)')}</span>
564
+ </div>` : ''
565
+ const altList = res.webaz_products?.length > 0 ? `
566
+ <div style="margin-top:16px">
567
+ <div style="font-size:12px;color:#9ca3af;margin-bottom:8px">${t('WebAZ 上的相关商品')}</div>
568
+ ${res.webaz_products.map(p => `
569
+ <div onclick="navigate('#order-product/${p.id}')" style="background:${p.url_match ? '#f0fdf4' : '#f9fafb'};border:1px solid ${p.url_match ? '#bbf7d0' : '#f3f4f6'};border-radius:8px;padding:10px 12px;margin-bottom:8px;cursor:pointer;display:flex;justify-content:space-between;align-items:center">
570
+ <div>
571
+ <div style="font-size:13px;font-weight:500">${p.url_match ? '🎯 ' : ''}${escHtml(p.title)}</div>
572
+ <div style="font-size:11px;color:#6b7280">${escHtml(p.agent_summary || '')}${p.url_match ? ` · <span style="color:#16a34a">${t('同款商品')}</span>` : ''}</div>
573
+ </div>
574
+ <div style="font-weight:700;color:#1d4ed8;white-space:nowrap;margin-left:8px">${p.price} WAZ</div>
575
+ </div>`).join('')}
576
+ </div>` : ''
577
+ container.innerHTML = `
578
+ ${smartRecognitionLine('url')}
579
+ <div class="card" style="margin-top:12px">
580
+ <div style="font-size:13px;color:#6b7280;margin-bottom:4px">${t('原商品')}:${escHtml(res.source?.title || '')}${res.source?.price_cny ? ` · ¥${res.source.price_cny}` : ''}</div>
581
+ <div style="font-weight:700;font-size:15px;color:${recColor};margin-bottom:8px">${recLabel}</div>
582
+ <div style="font-size:14px;line-height:1.5;color:#374151">${escHtml(res.reason || '')}</div>
583
+ ${res.savings_note ? `<div style="font-size:12px;color:#16a34a;margin-top:4px">💰 ${escHtml(res.savings_note)}</div>` : ''}
584
+ ${bestCard}${orderCard}${altList}
585
+ </div>`
586
+ }
587
+
588
+ async function searchByKeyword(q) {
589
+ state._lastSearchQ = q
590
+ const results = document.getElementById('smart-results')
591
+ results.innerHTML = loading$()
592
+ // M7.1:精准匹配 — 不再带 category / max_price / handling 等 filter;仅 ship_to 仍 honor 地址(保证可派送)
593
+ const ship_to = state.profileMini?.default_address_region || ''
594
+ const qs = new URLSearchParams({ q })
595
+ if (ship_to) qs.set('ship_to', ship_to)
596
+ const filtered = await GET('/products?' + qs.toString())
597
+
598
+ if (filtered.length === 0 && ship_to) {
599
+ const all = await GET(`/products?q=${encodeURIComponent(q)}`)
600
+ if (all.length > 0) {
601
+ results.innerHTML = `
602
+ ${smartRecognitionLine('keyword', q)}
603
+ <div class="alert alert-warn" style="margin-top:12px">
604
+ <strong>${t('找到')} ${all.length} ${t('个相关商品,但都无法派送到')} ${escHtml(ship_to)}</strong>
605
+ <div style="font-size:11px;margin-top:8px;display:flex;gap:8px;align-items:center;flex-wrap:wrap">
606
+ <button class="btn btn-outline btn-sm" style="width:auto;padding:4px 10px;font-size:11px" onclick="searchByKeywordNoFilterFromState()">${t('查看全部(含不可派送)')}</button>
607
+ <a href="#profile" style="color:#4f46e5">${t('改默认地址')}</a>
608
+ </div>
609
+ </div>`
610
+ return
611
+ }
612
+ }
613
+ if (filtered.length === 0) {
614
+ results.innerHTML = renderBuyNoMatchState(q)
615
+ return
616
+ }
617
+ renderKeywordProducts(results, filtered, ship_to)
618
+ }
619
+
620
+ window.searchByKeywordNoFilter = async (q) => {
621
+ state._lastSearchQ = q
622
+ const results = document.getElementById('smart-results')
623
+ results.innerHTML = loading$()
624
+ const all = await GET(`/products?q=${encodeURIComponent(q)}`)
625
+ if (all.length === 0) { results.innerHTML = renderBuyNoMatchState(q); return }
626
+ renderKeywordProducts(results, all, null)
627
+ }
628
+
629
+ // M-1 fix:不再把 q 串入 onclick HTML(避免反射型 XSS),改读 state 里的最近搜索词
630
+ window.searchByKeywordNoFilterFromState = () => {
631
+ const q = state._lastSearchQ || ''
632
+ if (!q) return
633
+ return window.searchByKeywordNoFilter(q)
634
+ }
635
+
636
+ // M7.1:三段式 result card — 商品信息 + 推荐理由(核心 + 折叠)+ 操作区
637
+ function renderKeywordProducts(container, products, ship_to) {
638
+ container.innerHTML = `
639
+ ${smartRecognitionLine('keyword', state._lastSearchQ || '')}
640
+ <div style="margin-top:8px">
641
+ <div style="font-size:11px;color:#6b7280;margin-bottom:10px">
642
+ 🎯 ${t('找到')} ${products.length} ${t('个精准匹配的商品')}${ship_to ? ` · ${t('可派送到')} ${escHtml(ship_to)}` : ''}
643
+ </div>
644
+ ${products.map(buyResultCardHtml).join('')}
645
+ </div>`
646
+ }
647
+
648
+ // 单张三段式结果卡:商品基础信息 + 推荐理由(核心 3 条永显 + 折叠详情)+ 操作
649
+ function buyResultCardHtml(p) {
650
+ // ── 推荐理由计算(M7.1 仅基于现有字段;M7.2 接 insights endpoint)──
651
+ const reasons = computeBuyReasons(p)
652
+ // 核心 3 条(永显)
653
+ const coreReasons = reasons.slice(0, 3)
654
+ // 折叠详情(其余)
655
+ const moreReasons = reasons.slice(3)
656
+ // 商品类型 badge
657
+ const typeBadge = p.product_type && p.product_type !== 'retail'
658
+ ? `<span style="display:inline-block;font-size:10px;background:#e0e7ff;color:#4338ca;padding:1px 7px;border-radius:99px;margin-left:6px">${t({ wholesale:'批发', service:'服务', digital:'数字' }[p.product_type] || p.product_type)}</span>`
659
+ : ''
660
+ // 稀缺
661
+ const lowStockChip = p.low_stock > 0
662
+ ? `<span style="display:inline-block;font-size:10px;background:#fee2e2;color:#dc2626;padding:1px 7px;border-radius:99px;margin-left:6px;font-weight:600">⚡ ${t('仅剩')} ${p.low_stock} ${t('件')}</span>`
663
+ : ''
664
+ // S5 性价比认证 chip
665
+ const valueBadgeChip = Number(p.value_badge) === 1
666
+ ? `<span style="display:inline-block;font-size:10px;background:linear-gradient(135deg,#fef9c3,#fde68a);color:#854d0e;padding:1px 7px;border-radius:99px;margin-left:6px;font-weight:600;border:1px solid #fcd34d" title="${t('极致性价比认证')} · ${t('类目第')} ${p.value_badge_rank || '?'} ${t('名')}">💎 ${t('性价比')}</span>`
667
+ : ''
668
+
669
+ return `
670
+ <div class="card" style="margin-bottom:12px;padding:14px">
671
+ <!-- ① 商品基础信息 -->
672
+ <div style="display:flex;gap:12px;align-items:flex-start;margin-bottom:10px">
673
+ <div style="font-size:36px;flex-shrink:0;line-height:1">${getCategoryIcon(p.category)}</div>
674
+ <div style="flex:1;min-width:0">
675
+ <div style="font-size:14px;font-weight:600;color:#111827;line-height:1.4;margin-bottom:4px">${escHtml(p.title)}${typeBadge}${valueBadgeChip}${lowStockChip}</div>
676
+ <div style="display:flex;align-items:baseline;gap:6px;margin-bottom:4px">
677
+ <span style="font-size:20px;font-weight:700;color:#4f46e5">${p.price}</span>
678
+ <span style="font-size:11px;color:#6b7280">WAZ</span>
679
+ </div>
680
+ <div style="font-size:11px;color:#6b7280">
681
+ ${repBadge(p.rep_level)} @${escHtml(p.seller_name)} · ${p.sales_count || 0} ${t('单完成')}
682
+ </div>
683
+ </div>
684
+ </div>
685
+
686
+ <!-- ② 推荐理由(核心永显 + 折叠详情)-->
687
+ <div style="background:#f9fafb;border-radius:8px;padding:10px 12px;margin-bottom:10px">
688
+ <div style="font-size:11px;color:#6b7280;margin-bottom:6px;font-weight:600">🎯 ${t('推荐理由')}</div>
689
+ ${coreReasons.length > 0
690
+ ? coreReasons.map(r => `<div style="font-size:12px;color:${r.color || '#374151'};margin-bottom:4px;line-height:1.5">${r.icon || '✓'} ${r.text}</div>`).join('')
691
+ : `<div style="font-size:11px;color:#9ca3af">${t('暂无明显推荐理由 — 协议级公平展示')}</div>`}
692
+ ${moreReasons.length > 0 ? `
693
+ <details style="margin-top:6px">
694
+ <summary style="font-size:11px;color:#6366f1;cursor:pointer;list-style:none">▸ ${t('更多理由')} (${moreReasons.length})</summary>
695
+ <div style="padding-top:6px;border-top:1px solid #e5e7eb;margin-top:6px">
696
+ ${moreReasons.map(r => `<div style="font-size:11px;color:${r.color || '#6b7280'};margin-bottom:3px;line-height:1.5">${r.icon || '·'} ${r.text}</div>`).join('')}
697
+ </div>
698
+ </details>` : ''}
699
+ <button disabled title="${t('M7.3 即将上线')}" style="margin-top:8px;font-size:11px;color:#9ca3af;background:#fff;border:1px dashed #d1d5db;border-radius:6px;padding:4px 10px;cursor:not-allowed">🔍 ${t('对推荐理由发起验证')} <span style="font-size:9px">(${t('即将上线')})</span></button>
700
+ </div>
701
+
702
+ <!-- ③ 操作区 -->
703
+ <div style="display:flex;gap:8px">
704
+ <button onclick="navigate('#order-product/${p.id}')" style="flex:1;padding:10px;background:#fff;color:#4f46e5;border:1.5px solid #4f46e5;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer">👁 ${t('详情')}</button>
705
+ <button onclick="navigate('#order-product/${p.id}')" style="flex:1;padding:10px;background:#4f46e5;color:#fff;border:none;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer">🛒 ${t('立即下单')}</button>
706
+ </div>
707
+ </div>`
708
+ }
709
+
710
+ // M7.1 客户端推荐理由计算(基于现有 API 字段)
711
+ // M7.2 将接 /api/products/:id/insights,拿到更准确的对标 / 估算
712
+ function computeBuyReasons(p) {
713
+ const reasons = []
714
+ // ── 核心:价格优势(vs 外链 source_price)──
715
+ if (p.source_price && p.source_price > p.price) {
716
+ const save = Math.round((p.source_price - p.price) * 100) / 100
717
+ const pct = Math.round((save / p.source_price) * 100)
718
+ reasons.push({ icon: '💰', color: '#16a34a', text: `${t('比外部平台省')} ${save} WAZ (${pct}% ${t('优惠')})` })
719
+ }
720
+ // ── 核心:分享佣金估算 ──
721
+ if (p.commission_rate && Number(p.commission_rate) > 0) {
722
+ const l1 = Math.round(p.price * Number(p.commission_rate) * 0.70 * 100) / 100
723
+ if (l1 > 0) reasons.push({ icon: '🔗', color: '#7c3aed', text: `${t('分享后可得 L1 佣金 ≈')} ${l1} WAZ` })
724
+ }
725
+ // ── 核心:协议保障 ──
726
+ reasons.push({ icon: '🛡', text: t('资金托管 + 仲裁 + 卖家质押保障') })
727
+ // ── 详情:商品成交 ──
728
+ if (p.sales_count > 0) reasons.push({ icon: '🔥', text: `${p.sales_count} ${t('人真实购买')}` })
729
+ // ── 详情:卖家信誉 ──
730
+ if (p.rep_level && p.rep_level !== 'new') {
731
+ const levelLabel = { trusted: t('可信'), quality: t('优质'), star: t('明星'), legend: t('传奇') }[p.rep_level] || p.rep_level
732
+ reasons.push({ icon: '⭐', text: `${t('卖家信誉')}: ${levelLabel}` })
733
+ }
734
+ // ── 详情:退货 / 质保 ──
735
+ if (p.return_days) reasons.push({ icon: '↩️', text: `${p.return_days} ${t('天无理由退货')}` })
736
+ if (p.warranty_days && p.warranty_days > 0) reasons.push({ icon: '🔧', text: `${p.warranty_days} ${t('天质保')}` })
737
+ // ── 详情:发货时效 ──
738
+ if (p.handling_hours) reasons.push({ icon: '⏱', text: `${p.handling_hours} ${t('小时内发货')}` })
739
+ // ── 详情:稀缺 ──
740
+ if (p.low_stock > 0) reasons.push({ icon: '⚡', color: '#dc2626', text: `${t('仅剩')} ${p.low_stock} ${t('件 · 快速决策')}` })
741
+ // [M7.2 接入]:creator unique_sharer_count / 历史最低价 / 同款卖家数 / 验证状态 / 个人化
742
+ return reasons
743
+ }
744
+
745
+ async function renderDiscover(app) {
746
+ app.innerHTML = shell(loading$(), 'discover')
747
+ // 2026-05-24 迁移到 hash 路由(#discover = 好物 / #discover/feed = 动态)
748
+ // 与其他 5 个子页一致,支持 URL 分享 + 浏览器后退
749
+ const subTabs = pageHotFeedToggle('#discover', '#discover/feed')
750
+
751
+ // goods — 里程碑 2:cursor 分页 + 加载更多 / 里程碑 5:sort chip / 里程碑 6:type chip
752
+ // D4 智能默认:先用 state,再回退到 localStorage,再回退到默认
753
+ const sort = state._discoverSort || (() => { try { return localStorage.getItem('webaz_pref_discover_sort') } catch { return null } })() || 'trending'
754
+ const ptype = state._discoverType || (() => { try { return localStorage.getItem('webaz_pref_discover_type') } catch { return null } })() || 'retail'
755
+ state._discoverSort = sort
756
+ state._discoverType = ptype
757
+ const dq = state._discoverQ || ''
758
+ const qsBase = `has_sales=true&sort=${sort}&product_type=${ptype}${dq ? '&fuzzy=true&q=' + encodeURIComponent(dq) : ''}`
759
+ const { items: products, cursor, matchMode } = await GET_WITH_CURSOR(`/products?${qsBase}`)
760
+ state._discoverMatchMode = matchMode
761
+ state._discoverCursor = cursor
762
+ const grid = products.length === 0
763
+ ? `<div class="empty"><div class="empty-icon">🌱</div><div class="empty-text">${t('还没有商品被成交')}</div><button class="btn btn-outline btn-sm" style="margin-top:12px;width:auto" onclick="navigate('#discover/new')">${t('看新品发现 →')}</button></div>`
764
+ : `<div class="product-grid" id="discover-grid">
765
+ ${products.map(p => productCardHtml(p, true)).join('')}
766
+ </div>` + (cursor ? `<div id="discover-more" style="text-align:center;margin-top:16px"><button class="btn btn-outline" style="width:auto;padding:8px 24px" onclick="loadMoreDiscover('${qsBase}','discover-grid','discover-more')">${t('加载更多')}</button></div>` : '')
767
+ // sectionStrip 已并入 discoverNavTabs 顶部 6-pill 横滑条,此处不再渲染独立第二行
768
+ const sectionStrip = ''
769
+
770
+ // sort + type 合并为可折叠 filter(默认折叠,sort 名 + type 名作摘要)
771
+ const sortLabel = ({trending:'🔥 热门', recommended:'📣 推荐多', seller_win_rate:'⚖️ 胜诉率', newest:'🆕 最新', rating:'⭐ 信誉', price_asc:'💰 价格 ↑', random:'🎲 随机'}[sort]) || sort
772
+ const typeLabel = ({retail:'🛍️ 零售', wholesale:'📦 批发', service:'🛠️ 服务', digital:'💾 数字'}[ptype]) || ptype
773
+ const filterPanel = `
774
+ <details style="margin-bottom:10px;background:#fff;border:1px solid #e5e7eb;border-radius:8px">
775
+ <summary style="padding:8px 12px;cursor:pointer;font-size:12px;color:#374151;display:flex;justify-content:space-between;align-items:center">
776
+ <span>🔧 ${t('筛选')}</span>
777
+ <span style="color:#6b7280;font-size:11px">${sortLabel} · ${typeLabel}</span>
778
+ </summary>
779
+ <div style="padding:8px 12px;border-top:1px solid #f3f4f6">
780
+ ${renderSortChips(sort, 'discover')}
781
+ ${renderTypeChips(ptype, 'discover')}
782
+ </div>
783
+ </details>
784
+ `
785
+
786
+ app.innerHTML = shell(`
787
+ ${preLaunchBannerHTML()}
788
+ ${renderSmartBuyHeader('discover')}
789
+ ${discoverGoodsTabs('recommend')}
790
+ ${subTabs}
791
+ ${sectionStrip}
792
+ ${filterPanel}
793
+ <div style="font-size:11px;color:#9ca3af;margin-bottom:10px">
794
+ ${t('只展示真实成交且好评推荐的内容,用户共建非平台算法推荐')}
795
+ </div>
796
+ <div id="product-list">${grid}</div>
797
+ <div id="discover-sh-strip" style="margin-top:24px"></div>
798
+ `, 'discover')
799
+ // M8 非主流引入:在主商品 feed 之后,社交化呈现"邻里闲置"
800
+ shInjectStrip('discover-sh-strip', { limit: 5 })
801
+
802
+ // #1051 Schema.org ItemList — 搜索引擎可取作 SERP / 购物 agent 可一次读完前 20 个商品
803
+ // #1053 每个 Product 加 inLanguage + name 多语言数组(i18n_titles 有别名时)
804
+ try {
805
+ setJsonLd({
806
+ '@context': 'https://schema.org',
807
+ '@type': 'ItemList',
808
+ name: 'WebAZ Discover — verified-sales products',
809
+ numberOfItems: products.length,
810
+ itemListElement: products.slice(0, 20).map((pp, idx) => {
811
+ const img = (pp.images || '').split(',').map(s => s.trim()).filter(s => /^(https?:|\/|data:)/.test(s))[0]
812
+ const lang = pp._lang || 'zh'
813
+ const titles = pp.i18n_titles && typeof pp.i18n_titles === 'object' ? pp.i18n_titles : {}
814
+ const altCount = Object.entries(titles).filter(([k, v]) => k !== lang && v).length
815
+ const nameField = altCount > 0
816
+ ? [{ '@value': String(pp.title || ''), '@language': lang },
817
+ ...Object.entries(titles).filter(([k, v]) => k !== lang && v).map(([k, v]) => ({ '@value': String(v), '@language': k }))]
818
+ : pp.title
819
+ return {
820
+ '@type': 'ListItem',
821
+ position: idx + 1,
822
+ item: {
823
+ '@type': 'Product',
824
+ name: nameField,
825
+ url: location.origin + '/#order-product/' + pp.id,
826
+ ...(img ? { image: img } : {}),
827
+ offers: { '@type': 'Offer', price: pp.price, priceCurrency: pp.currency || 'WAZ' },
828
+ },
829
+ }
830
+ }),
831
+ })
832
+ } catch { /* never break the page */ }
833
+ }
834
+
835
+ // 里程碑 6:product_type chip
836
+ function renderTypeChips(active, ctx) {
837
+ const opts = [
838
+ { k: 'retail', label: t('零售'), icon: '🛍️' },
839
+ { k: 'wholesale', label: t('批发'), icon: '📦' },
840
+ { k: 'service', label: t('服务'), icon: '🛠️' },
841
+ { k: 'digital', label: t('数字'), icon: '💾' },
842
+ ]
843
+ return `
844
+ <div style="display:flex;gap:6px;margin-bottom:10px;overflow-x:auto;padding:2px 0;-webkit-overflow-scrolling:touch">
845
+ ${opts.map(o => `
846
+ <button onclick="setTypeChip('${ctx}','${o.k}')"
847
+ style="white-space:nowrap;border:1px solid ${active===o.k?'#ea580c':'#e5e7eb'};background:${active===o.k?'#fff7ed':'#fff'};color:${active===o.k?'#ea580c':'#374151'};padding:5px 11px;border-radius:999px;font-size:12px;cursor:pointer;font-weight:${active===o.k?'600':'400'}">
848
+ <span style="margin-right:3px">${o.icon}</span>${o.label}
849
+ </button>
850
+ `).join('')}
851
+ </div>`
852
+ }
853
+
854
+ window.setTypeChip = (ctx, ptype) => {
855
+ if (ctx === 'discover') state._discoverType = ptype
856
+ else if (ctx === 'new') state._newType = ptype
857
+ // D4 智能默认:持久化
858
+ try { localStorage.setItem('webaz_pref_' + ctx + '_type', ptype) } catch {}
859
+ const app = document.getElementById('app')
860
+ if (ctx === 'discover') renderDiscover(app)
861
+ else if (ctx === 'new') renderNewArrivals(app)
862
+ }
863
+
864
+ // 里程碑 5-d/e:sort chip row(横向)
865
+ // 2026-05-24 #977:ctx='new' 时 chip 标签 + hint 切换到卖家维度(新品本身无数据累计)
866
+ function renderSortChips(active, ctx) {
867
+ const isNew = ctx === 'new'
868
+ const opts = isNew ? [
869
+ { k: 'trending', label: t('热门卖家'), icon: '🔥' },
870
+ { k: 'recommended', label: t('推荐卖家'), icon: '📣' },
871
+ { k: 'seller_win_rate', label: t('胜诉率'), icon: '⚖️' },
872
+ { k: 'newest', label: t('最新'), icon: '🆕' },
873
+ { k: 'rating', label: t('卖家信誉'), icon: '⭐' },
874
+ { k: 'price_asc', label: t('价格 ↑'), icon: '💰' },
875
+ { k: 'random', label: t('随机探索'), icon: '🎲' },
876
+ ] : [
877
+ { k: 'trending', label: t('热门'), icon: '🔥' },
878
+ { k: 'recommended', label: t('推荐多'), icon: '📣' },
879
+ { k: 'seller_win_rate', label: t('胜诉率'), icon: '⚖️' },
880
+ { k: 'newest', label: t('最新'), icon: '🆕' },
881
+ { k: 'rating', label: t('信誉'), icon: '⭐' },
882
+ { k: 'price_asc', label: t('价格 ↑'), icon: '💰' },
883
+ { k: 'random', label: t('随机探索'), icon: '🎲' },
884
+ ]
885
+ // 2026-05-24 P1-3:sort 解释文案 —— 让用户知道当前排序的依据
886
+ const sortHint = isNew ? {
887
+ trending: t('卖家累计成交量降序(新品无产品级数据,按卖家维度)'),
888
+ recommended: t('卖家累计被推荐买家数(新品无产品级数据,按卖家维度)'),
889
+ seller_win_rate: t('卖家历史争议胜诉率(越高越靠谱)'),
890
+ newest: t('按上架时间倒序'),
891
+ rating: t('卖家平均星级'),
892
+ price_asc: t('价格从低到高'),
893
+ random: t('完全随机 · 探索冷门新品'),
894
+ }[active] : {
895
+ trending: t('近 7 天成交量 × 好评率,社区共建非平台算法'),
896
+ recommended: t('被推荐次数最多(达人 + 普通买家)'),
897
+ seller_win_rate: t('卖家历史争议胜诉率(越高越靠谱)'),
898
+ newest: t('按上架时间倒序'),
899
+ rating: t('卖家平均星级'),
900
+ price_asc: t('价格从低到高'),
901
+ random: t('完全随机 · 探索冷门好物'),
902
+ }[active]
903
+ return `
904
+ <div style="display:flex;gap:6px;margin-bottom:6px;overflow-x:auto;padding:2px 0;-webkit-overflow-scrolling:touch">
905
+ ${opts.map(o => `
906
+ <button onclick="setSortChip('${ctx}','${o.k}')"
907
+ class="sort-chip"
908
+ style="white-space:nowrap;border:1px solid ${active===o.k?'#4f46e5':'#e5e7eb'};background:${active===o.k?'#eef2ff':'#fff'};color:${active===o.k?'#4f46e5':'#374151'};padding:5px 11px;border-radius:999px;font-size:12px;cursor:pointer;font-weight:${active===o.k?'600':'400'}">
909
+ <span style="margin-right:3px">${o.icon}</span>${o.label}
910
+ </button>
911
+ `).join('')}
912
+ ${active === 'random' ? `<button onclick="renderDiscover(document.getElementById('app'))" style="margin-left:4px;border:none;background:transparent;color:#4f46e5;font-size:12px;cursor:pointer">🔄 ${t('换一批')}</button>` : ''}
913
+ </div>
914
+ ${sortHint ? `<div style="font-size:10px;color:#9ca3af;margin-bottom:10px;padding-left:2px">ℹ️ ${sortHint}</div>` : ''}`
915
+ }
916
+
917
+ window.setSortChip = (ctx, sort) => {
918
+ if (ctx === 'discover') state._discoverSort = sort
919
+ else if (ctx === 'new') state._newSort = sort
920
+ else if (ctx === 'search') state._searchSort = sort
921
+ // D4 智能默认:持久化
922
+ try { localStorage.setItem('webaz_pref_' + ctx + '_sort', sort) } catch {}
923
+ const app = document.getElementById('app')
924
+ if (ctx === 'discover') renderDiscover(app)
925
+ else if (ctx === 'new') renderNewArrivals(app)
926
+ else if (ctx === 'search' && state._lastSearchQ) searchByKeyword(state._lastSearchQ)
927
+ }
928
+
929
+ function productCardHtml(p, showSales) {
930
+ // 里程碑 6: 类型标签 + 库存稀缺
931
+ const typeBadge = p.product_type && p.product_type !== 'retail'
932
+ ? `<span style="display:inline-block;font-size:9px;background:#e0e7ff;color:#4338ca;padding:1px 6px;border-radius:99px;margin-left:4px">${t({ wholesale:'批发', service:'服务', digital:'数字' }[p.product_type] || p.product_type)}</span>`
933
+ : ''
934
+ const lowStockBadge = p.low_stock > 0
935
+ ? `<div style="font-size:11px;color:#dc2626;margin-top:3px;font-weight:600">⚡ ${t('仅剩')} ${p.low_stock} ${t('件')}</div>`
936
+ : ''
937
+ const trust = sellerTrustLine(p)
938
+ // 排版规则:每行独立、可单独 ellipsis,长字符不会挤压相邻信息
939
+ // L1 title (CSS 已限 2 行 ellipsis)
940
+ // L2 price + WAZ
941
+ // L3 @sellerName ← 单独一行,ellipsis 保护
942
+ // L4 ⭐可信 · 12 单 ← 信誉行,浅灰
943
+ // L5 🔥 3 已购 · 📣 3 推荐 ← 销量信号,绿字
944
+ return `<div class="product-card" onclick="navigate('#order-product/${p.id}')">
945
+ <div class="product-img">${getCategoryIcon(p.category)}</div>
946
+ <div class="product-body">
947
+ <div class="product-name">${escHtml(p.title)}${typeBadge}</div>
948
+ <div class="product-price">${p.price} <span style="font-size:11px;font-weight:400">WAZ</span></div>
949
+ <div class="product-seller">${t('卖家')}:@${escHtml(p.seller_name)}</div>
950
+ ${p.seller_created_at ? `<div style="font-size:10px;color:#9ca3af;margin-top:1px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${t('入驻时长')}:${joinDuration(p.seller_created_at)}</div>` : ''}
951
+ ${trust ? `<div style="font-size:10px;color:#6b7280;margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${trust}</div>` : ''}
952
+ ${lowStockBadge}
953
+ ${p.trial_quota_remaining > 0 ? `<div style="font-size:11px;color:#9333ea;margin-top:3px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">🎁 ${t('测评免单 剩')} ${p.trial_quota_remaining} ${t('名额')}</div>` : ''}
954
+ ${showSales ? (() => {
955
+ const sales = Number(p.sales_count) || 0
956
+ const rec = Number(p.recommend_count) || 0
957
+ const pct = sales > 0 ? Math.min(100, Math.round((rec / sales) * 100)) : null
958
+ return `<div style="font-size:11px;color:#16a34a;margin-top:3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">🔥 ${sales} ${t('已购')}${rec > 0 ? ` · 📣 ${rec} ${t('推荐')}` : ''}${pct !== null ? ` · ${t('推荐比例')}:${pct}%` : ''}</div>`
959
+ })() : ''}
960
+ </div>
961
+ </div>`
962
+ }
963
+
964
+ window.loadMoreDiscover = async (qsBase, gridId, moreId) => {
965
+ const grid = document.getElementById(gridId)
966
+ const moreWrap = document.getElementById(moreId)
967
+ if (!grid || !state._discoverCursor) return
968
+ if (moreWrap) moreWrap.innerHTML = `<span style="font-size:12px;color:#9ca3af">${t('加载中…')}</span>`
969
+ const { items, cursor } = await GET_WITH_CURSOR(`/products?${qsBase}&cursor=${encodeURIComponent(state._discoverCursor)}`)
970
+ state._discoverCursor = cursor
971
+ const showSales = qsBase.includes('has_sales=true')
972
+ grid.insertAdjacentHTML('beforeend', items.map(p => productCardHtml(p, showSales)).join(''))
973
+ if (cursor) {
974
+ if (moreWrap) moreWrap.innerHTML = `<button class="btn btn-outline" style="width:auto;padding:8px 24px" onclick="loadMoreDiscover('${qsBase}','${gridId}','${moreId}')">${t('加载更多')}</button>`
975
+ } else if (moreWrap) {
976
+ moreWrap.innerHTML = `<span style="font-size:12px;color:#9ca3af">${t('没有更多了')}</span>`
977
+ }
978
+ }
979
+
980
+ // 2026-05-24 setDiscoverTab 兼容旧调用 — 改路由跳转
981
+ window.setDiscoverTab = (k) => {
982
+ navigate(k === 'feed' ? '#discover/feed' : '#discover')
983
+ }
984
+
985
+ // 2026-05-24 推荐好物 · 动态 view(独立 renderer,与其他子页一致)
986
+ async function renderDiscoverFeed(app) {
987
+ state.feedScope = state.feedScope || 'all'
988
+ app.innerHTML = shell(`
989
+ ${renderSmartBuyHeader('discover')}
990
+ ${discoverGoodsTabs('recommend')}
991
+ ${pageHotFeedToggle('#discover', '#discover/feed')}
992
+ <div id="feed-view">${loading$()}</div>
993
+ `, 'discover')
994
+ await renderFeedView()
995
+ }
996
+
997
+ // 📡 动态 — 3 sub: 全网事件流 / 关注事件流 / 排行榜(多维度聚合)
998
+ async function renderFeedView() {
999
+ const scope = state.feedScope || 'all' // 'all' | 'following' | 'rank'
1000
+ const view = document.getElementById('feed-view')
1001
+ if (!view) return
1002
+ const scopePill = (k, icon, label) => {
1003
+ const active = scope === k
1004
+ return `<button class="btn ${active ? 'btn-primary' : 'btn-outline'} btn-sm" style="width:auto;padding:5px 14px;font-size:12px" onclick="setFeedScope('${k}')">${icon} ${label}</button>`
1005
+ }
1006
+ const scopeTabs = `
1007
+ <div style="display:flex;gap:6px;margin-bottom:12px;flex-wrap:wrap">
1008
+ ${scopePill('all', '🌐', t('全网'))}
1009
+ ${scopePill('following', '👥', t('我关注的'))}
1010
+ ${scopePill('rank', '🏆', t('排行榜'))}
1011
+ </div>`
1012
+
1013
+ // 排行榜 sub:4 mini 榜单聚合渲染
1014
+ if (scope === 'rank') {
1015
+ view.innerHTML = scopeTabs + `<div id="feed-rank-body">${loading$()}</div>`
1016
+ await renderFeedRanks()
1017
+ return
1018
+ }
1019
+
1020
+ // D3 笔记 strip — 顶部显示热门笔记(点 navigate 跳 #note/<id>)
1021
+ // following 模式拉关注的人的笔记;其它模式拉 trending
1022
+ const noteSort = scope === 'following' ? 'following' : 'trending'
1023
+ let notesStrip = ''
1024
+ try {
1025
+ const nr = await fetch('/api/notes?sort=' + noteSort + '&limit=8', {
1026
+ headers: state.apiKey ? { Authorization: `Bearer ${state.apiKey}` } : {},
1027
+ }).then(x => x.json())
1028
+ if (Array.isArray(nr?.items) && nr.items.length > 0) {
1029
+ notesStrip = `
1030
+ <div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:6px">
1031
+ <div style="font-size:13px;font-weight:600;color:#1f2937">📝 ${t('笔记')}</div>
1032
+ <button class="btn btn-link" style="background:none;border:none;font-size:11px;color:#4f46e5;padding:0;cursor:pointer" onclick="navigate('#shares')">${t('全部 →')}</button>
1033
+ </div>
1034
+ <div style="display:flex;gap:8px;overflow-x:auto;padding-bottom:8px;margin-bottom:14px;scroll-snap-type:x mandatory">
1035
+ ${nr.items.map(n => `
1036
+ <div style="flex:0 0 140px;scroll-snap-align:start;cursor:pointer" onclick="navigate('#note/${n.id}')">
1037
+ ${n.first_photo
1038
+ ? `<img src="/api/notes/photo/${n.first_photo}" style="width:140px;height:140px;border-radius:8px;object-fit:cover">`
1039
+ : `<div style="width:140px;height:140px;border-radius:8px;background:#f3f4f6;display:flex;align-items:center;justify-content:center;font-size:36px">📝</div>`}
1040
+ <div style="font-size:11px;color:#374151;margin-top:4px;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;line-height:1.3">${escHtml(n.title || n.body_excerpt)}</div>
1041
+ <div style="font-size:10px;color:#9ca3af;margin-top:2px">@${escHtml(n.owner_handle || 'anon')} · ❤️ ${n.stats.likes || 0}</div>
1042
+ </div>`).join('')}
1043
+ </div>`
1044
+ }
1045
+ } catch {}
1046
+
1047
+ // 事件流 sub(全网 / 关注)
1048
+ const data = await GET(`/feed?scope=${scope}`)
1049
+ const events = data.events || []
1050
+ if (events.length === 0 && !notesStrip) {
1051
+ view.innerHTML = `${scopeTabs}<div class="empty"><div class="empty-icon">📭</div><div class="empty-text">${scope === 'following' ? t('你关注的人还没有动态 — 去关注一些活跃用户') : t('暂无动态')}</div></div>`
1052
+ return
1053
+ }
1054
+ if (events.length === 0) {
1055
+ view.innerHTML = `${scopeTabs}${notesStrip}<div style="text-align:center;color:#9ca3af;font-size:12px;padding:14px">${t('暂无其他动态')}</div>`
1056
+ return
1057
+ }
1058
+ const html = events.map(e => {
1059
+ let extra = {}
1060
+ try { extra = e.extra ? JSON.parse(e.extra) : {} } catch (_) {}
1061
+ const ts = fmtTime(e.ts)
1062
+ const actor = `<button onclick="toggleFollow('${e.actor_id}', this)" class="feed-actor" style="background:none;border:none;color:#4f46e5;font-weight:600;cursor:pointer;padding:0">${escHtml(e.actor_name || '—')}</button>`
1063
+ let body = ''
1064
+ if (e.kind === 'purchase') {
1065
+ body = `${actor} ${t('购买了')} <a href="#order-product/${e.product_id}" style="color:#111">${escHtml(e.product_title)}</a> · ${e.price} WAZ`
1066
+ } else if (e.kind === 'commission') {
1067
+ const amount = Number(extra.amount || 0).toFixed(2)
1068
+ body = `${actor} ${t('因推广')} <a href="#order-product/${e.product_id}" style="color:#111">${escHtml(e.product_title)}</a> ${t('获得 L')}${extra.level} ${t('佣金')} <strong style="color:#059669">+${amount} WAZ</strong>`
1069
+ } else if (e.kind === 'join_binary') {
1070
+ // pre-public 去左右码:活动流不再广播左/右区,只显示加入了某人的积分树
1071
+ body = `${actor} ${t('加入了')} ${escHtml(extra.placement_name || '—')} ${t('的积分树')}`
1072
+ }
1073
+ const icon = e.kind === 'purchase' ? '🛒' : e.kind === 'commission' ? '💰' : '⚛'
1074
+ return `<div class="card" style="margin-bottom:8px;padding:10px 12px;display:flex;gap:10px;align-items:flex-start">
1075
+ <div style="font-size:20px">${icon}</div>
1076
+ <div style="flex:1;font-size:13px;line-height:1.5">
1077
+ ${body}
1078
+ <div style="font-size:11px;color:#9ca3af;margin-top:2px">${ts}</div>
1079
+ </div>
1080
+ </div>`
1081
+ }).join('')
1082
+ view.innerHTML = scopeTabs + notesStrip + html
1083
+ }
1084
+
1085
+ // 排行榜 sub:4 mini 板块(商品 / 卖家 / 创作者 / 买家),每个 Top 5 + 查看完整 →
1086
+ async function renderFeedRanks() {
1087
+ const body = document.getElementById('feed-rank-body')
1088
+ if (!body) return
1089
+ const [products, sellers, creators, buyers] = await Promise.all([
1090
+ GET('/leaderboard?kind=products&limit=5').catch(() => ({ items: [] })),
1091
+ GET('/leaderboard?kind=sellers&limit=5').catch(() => ({ items: [] })),
1092
+ GET('/leaderboard?kind=creators&limit=5').catch(() => ({ items: [] })),
1093
+ GET('/leaderboard?kind=buyers&limit=5').catch(() => ({ items: [] })),
1094
+ ])
1095
+ const rankLine = (rank, label, sub, hash) => `
1096
+ <div onclick="location.hash='${hash}'" style="display:flex;gap:8px;align-items:center;padding:6px 0;cursor:pointer;border-bottom:1px solid #f3f4f6">
1097
+ <div style="font-size:11px;font-weight:700;color:${rank <= 3 ? '#dc2626' : '#9ca3af'};width:18px;text-align:center;flex-shrink:0">${rank}</div>
1098
+ <div style="flex:1;min-width:0">
1099
+ <div style="font-size:12px;font-weight:600;color:#1f2937;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${escHtml(label)}</div>
1100
+ <div style="font-size:10px;color:#9ca3af;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${sub}</div>
1101
+ </div>
1102
+ </div>`
1103
+ const miniCard = (icon, title, kind, items, lineMaker) => `
1104
+ <div class="card" style="padding:12px 14px;margin-bottom:10px">
1105
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
1106
+ <div style="font-size:13px;font-weight:700">${icon} ${title}</div>
1107
+ <a href="#leaderboard?kind=${kind}" style="font-size:11px;color:#6366f1;text-decoration:none">${t('完整榜单')} →</a>
1108
+ </div>
1109
+ ${items.length === 0 ? `<div style="text-align:center;color:#9ca3af;font-size:12px;padding:12px 0">${t('暂无数据')}</div>` : items.map((it, i) => lineMaker(it, i + 1)).join('')}
1110
+ </div>`
1111
+
1112
+ body.innerHTML = ''
1113
+ + miniCard('🔥', t('热门商品'), 'products', products.items || [], (it, r) =>
1114
+ rankLine(r, it.title, `${Number(it.completion_count||0)} ${t('单')} · ${Number(it.recommend_count||0)} ${t('人推荐')} · ${it.price} WAZ`, `#order-product/${it.id}`))
1115
+ + miniCard('🏪', t('卖家榜'), 'sellers', sellers.items || [], (it, r) =>
1116
+ rankLine(r, '@' + (it.handle || it.name || ''), `${it.rating_count > 0 ? '⭐ ' + Number(it.avg_rating).toFixed(1) + ' (' + it.rating_count + ')' : t('暂无评价')} · ${Number(it.orders_count||0)} ${t('单')}`, `#shop/${it.id}`))
1117
+ + miniCard('📣', t('创作者榜'), 'creators', creators.items || [], (it, r) =>
1118
+ rankLine(r, '@' + (it.handle || it.name || ''), `${Number(it.products_shared||0)} ${t('个商品')} · ${Number(it.total_likes||0)} ${t('赞')}`, `#u/${it.id}`))
1119
+ + miniCard('🛍', t('买家榜'), 'buyers', buyers.items || [], (it, r) =>
1120
+ rankLine(r, '@' + (it.handle || it.name || ''), `${Number(it.orders_count||0)} ${t('单')}`, `#u/${it.id}`))
1121
+ }
1122
+
1123
+ window.setFeedScope = (k) => {
1124
+ state.feedScope = k
1125
+ renderFeedView()
1126
+ }
1127
+
1128
+ // 2026-05-24 P1-4:新品发现时段 chip + #987:测评免单 chip(并列,可叠加筛选)
1129
+ function renderNewDaysChips(active, trialOnly) {
1130
+ const opts = [
1131
+ { k: '', label: t('全部') },
1132
+ { k: 1, label: t('今日') },
1133
+ { k: 3, label: t('3 天内') },
1134
+ { k: 7, label: t('7 天内') },
1135
+ ]
1136
+ return `
1137
+ <div style="display:flex;gap:6px;margin-bottom:10px;overflow-x:auto;padding:2px 0;-webkit-overflow-scrolling:touch">
1138
+ ${opts.map(o => `
1139
+ <button onclick="setNewDays('${o.k}')"
1140
+ style="flex:0 0 auto;white-space:nowrap;border:1px solid ${active===o.k?'#0891b2':'#e5e7eb'};background:${active===o.k?'#ecfeff':'#fff'};color:${active===o.k?'#0891b2':'#374151'};padding:5px 11px;border-radius:999px;font-size:12px;cursor:pointer;font-weight:${active===o.k?'600':'400'}">
1141
+ ${o.label}
1142
+ </button>
1143
+ `).join('')}
1144
+ <button onclick="setNewTrialOnly(${!trialOnly})"
1145
+ style="flex:0 0 auto;white-space:nowrap;border:1px solid ${trialOnly?'#9333ea':'#e5e7eb'};background:${trialOnly?'#faf5ff':'#fff'};color:${trialOnly?'#7e22ce':'#374151'};padding:5px 11px;border-radius:999px;font-size:12px;cursor:pointer;font-weight:${trialOnly?'600':'400'}">
1146
+ 🎁 ${t('测评免单')}
1147
+ </button>
1148
+ </div>`
1149
+ }
1150
+ window.setNewDays = (val) => {
1151
+ state._newDays = val === '' ? '' : Number(val)
1152
+ renderNewArrivals(document.getElementById('app'))
1153
+ }
1154
+ window.setNewTrialOnly = (val) => {
1155
+ state._newTrialOnly = !!val
1156
+ renderNewArrivals(document.getElementById('app'))
1157
+ }
1158
+
1159
+ async function renderNewArrivals(app) {
1160
+ app.innerHTML = shell(loading$(), 'discover')
1161
+ const sort = state._newSort || 'newest'
1162
+ const ptype = state._newType || 'retail'
1163
+ // 2026-05-24 P1-4:时段 chip — '' = 全部 / 1 / 3 / 7
1164
+ const days = state._newDays != null ? state._newDays : ''
1165
+ const trialOnly = !!state._newTrialOnly
1166
+ // 2026-05-24 #975:独立 scope state(_newQ),不再共用 _discoverQ
1167
+ const dq = state._newQ || ''
1168
+ const qsBase = `has_sales=false&sort=${sort}&product_type=${ptype}${days ? '&since_days=' + days : ''}${trialOnly ? '&has_trial=true' : ''}${dq ? '&fuzzy=true&q=' + encodeURIComponent(dq) : ''}`
1169
+ const { items: products, cursor } = await GET_WITH_CURSOR(`/products?${qsBase}`)
1170
+ state._discoverCursor = cursor
1171
+ const grid = products.length === 0
1172
+ ? emptyState('📦', t('暂无新品'))
1173
+ : `<div class="product-grid" id="new-grid">
1174
+ ${products.map(p => `
1175
+ <div class="product-card" onclick="navigate('#order-product/${p.id}')">
1176
+ <div class="product-img">${getCategoryIcon(p.category)}</div>
1177
+ <div class="product-body">
1178
+ <div class="product-name">${escHtml(p.title)}</div>
1179
+ <div class="product-price">${p.price} <span style="font-size:11px;font-weight:400">WAZ</span></div>
1180
+ <div class="product-seller">${repBadge(p.rep_level)}@${escHtml(p.seller_name)}</div>
1181
+ ${p.trial_quota_remaining > 0
1182
+ ? `<div style="font-size:11px;color:#9333ea;margin-top:4px;font-weight:600">🎁 ${t('测评免单 剩')} ${p.trial_quota_remaining} ${t('名额')}</div>`
1183
+ : `<div style="font-size:11px;color:#f59e0b;margin-top:4px">🆕 ${t('等待第一位买家')}</div>`}
1184
+ </div>
1185
+ </div>`).join('')}
1186
+ </div>` + (cursor ? `<div id="new-more" style="text-align:center;margin-top:16px"><button class="btn btn-outline" style="width:auto;padding:8px 24px" onclick="loadMoreDiscover('${qsBase}','new-grid','new-more')">${t('加载更多')}</button></div>` : '')
1187
+ app.innerHTML = shell(`
1188
+ ${renderSmartBuyHeader('new')}
1189
+ ${discoverGoodsTabs('new')}
1190
+ ${pageHotFeedToggle('#discover/new', '#discover/new/feed', { hotIcon: '🆕', hotLabel: t('新品') })}
1191
+ <div style="font-size:12px;color:var(--gray-500);margin-bottom:14px;line-height:1.5">${t('卖家最新上架、尚无成交 — 成为第一位发现者和传播者')}</div>
1192
+ ${renderNewDaysChips(days, trialOnly)}
1193
+ ${renderSortChips(sort, 'new')}
1194
+ ${renderTypeChips(ptype, 'new')}
1195
+ <div id="product-list">${grid}</div>
1196
+ `, 'discover')
1197
+ }
1198
+
1199
+ // 2026-05-24 新品发现 · 动态:最近上架时间线(@user 上架了 [商品] X 分钟前)
1200
+ async function renderNewArrivalsFeed(app) {
1201
+ app.innerHTML = shell(loading$(), 'discover')
1202
+ const { items } = await GET_WITH_CURSOR('/products?sort=newest&has_sales=false&product_type=retail&limit=30')
1203
+ const products = items || []
1204
+ const body = products.length === 0
1205
+ ? feedEmpty('🆕', t('暂无新品动态'), t('看看 推荐好物'), '#discover')
1206
+ : products.map(p => {
1207
+ const ts = fmtTime(p.created_at)
1208
+ const img = (() => { try { const i = typeof p.images === 'string' ? JSON.parse(p.images) : p.images; return Array.isArray(i) && i[0] ? i[0] : '' } catch { return '' } })()
1209
+ return `<div class="card" style="margin-bottom:8px;padding:12px;display:flex;gap:10px;align-items:flex-start;cursor:pointer" onclick="navigate('#order-product/${p.id}')">
1210
+ ${img ? `<img src="${escAttr(img)}" style="width:56px;height:56px;border-radius:6px;object-fit:cover;flex-shrink:0">` : `<div style="width:56px;height:56px;border-radius:6px;background:#f3f4f6;display:flex;align-items:center;justify-content:center;font-size:24px;flex-shrink:0">${getCategoryIcon(p.category)}</div>`}
1211
+ <div style="flex:1;min-width:0">
1212
+ <div style="font-size:13px;line-height:1.5">
1213
+ ${feedActor(p.seller_id, p.seller_name, p.seller_handle)} ${t('上架了')} <strong>${escHtml(p.title)}</strong>
1214
+ </div>
1215
+ <div style="font-size:11px;color:#9ca3af;margin-top:4px">${p.price} WAZ · ${ts}</div>
1216
+ </div>
1217
+ </div>`
1218
+ }).join('')
1219
+ app.innerHTML = shell(`
1220
+ ${renderSmartBuyHeader('new')}
1221
+ ${discoverGoodsTabs('new')}
1222
+ ${pageHotFeedToggle('#discover/new', '#discover/new/feed', { hotIcon: '🆕', hotLabel: t('新品') })}
1223
+ <h2 style="font-size:16px;font-weight:700;margin:14px 0 10px">📡 ${t('新品动态')}</h2>
1224
+ <div style="font-size:11px;color:#6b7280;margin-bottom:14px">${t('卖家最新上架按时间倒序 · 点击进入商品')}</div>
1225
+ ${body}
1226
+ `, 'discover')
1227
+ }
1228
+
1229
+ window.toggleSearchClear = () => {
1230
+ const inp = document.getElementById('search-inp')
1231
+ const wrap = document.getElementById('search-wrap')
1232
+ if (!inp || !wrap) return
1233
+ wrap.classList.toggle('has-value', !!inp.value)
1234
+ }
1235
+ window.clearSearchInput = () => {
1236
+ const inp = document.getElementById('search-inp')
1237
+ if (!inp) return
1238
+ inp.value = ''
1239
+ inp.focus()
1240
+ window.toggleSearchClear()
1241
+ }
1242
+
1243
+ function renderSearchResults(products, banner, q) {
1244
+ const grid = products.length === 0
1245
+ ? `<div class="empty"><div class="empty-icon">🔍</div><div class="empty-text">${t('没有找到"')}${q.slice(0, 30)}"</div></div>`
1246
+ : `<div class="product-grid">
1247
+ ${products.map(p => `
1248
+ <div class="product-card" onclick="navigate('#order-product/${p.id}')">
1249
+ <div class="product-img">${getCategoryIcon(p.category)}</div>
1250
+ <div class="product-body">
1251
+ <div class="product-name">${escHtml(p.title)}</div>
1252
+ <div class="product-price">${p.price} WAZ</div>
1253
+ <div class="product-seller">${repBadge(p.rep_level)}@${escHtml(p.seller_name)}</div>
1254
+ </div>
1255
+ </div>`).join('')}
1256
+ </div>`
1257
+ document.getElementById('product-list').innerHTML = banner + grid
1258
+ }
1259
+
1260
+ window.doSearch = async () => {
1261
+ const q = document.getElementById('search-inp').value.trim()
1262
+ if (!q) return
1263
+ document.getElementById('product-list').innerHTML = loading$()
1264
+
1265
+ // 精准搜索:所有输入统一走 /search-by-link,由后端按分享精准链路(external_id / external_title / product_title)判定。
1266
+ const resp = await POST('/search-by-link', { text: q })
1267
+ const products = resp.products || []
1268
+ const m = resp.matched_by
1269
+ const ext = resp.extracted || {}
1270
+ const plat = ext.platform ? `${ext.platform}` : '外部平台'
1271
+ let banner = ''
1272
+ if (m === 'external_id') banner = `<div class="alert alert-success" style="margin-bottom:12px">✓ ${t('通过')} ${plat} ${t('商品 ID 精确匹配到')} ${products.length} ${t('件')}</div>`
1273
+ else if (m === 'external_title_exact') banner = `<div class="alert alert-success" style="margin-bottom:12px">✓ ${t('通过外链标题完全匹配到')} ${products.length} ${t('件')}</div>`
1274
+ else if (m === 'product_title_exact') banner = `<div class="alert alert-success" style="margin-bottom:12px">✓ ${t('通过商品标题完全匹配到')} ${products.length} ${t('件')}</div>`
1275
+ else if (resp.unsupported_format) banner = `<div class="alert alert-warn" style="margin-bottom:12px">⚠️ ${resp.hint}</div>`
1276
+ else banner = `<div class="alert" style="margin-bottom:12px">${t('精准搜索未命中(需一字不差)。可改用「模糊」按钮做部分匹配。')}</div>`
1277
+ renderSearchResults(products, banner, q)
1278
+ }
1279
+
1280
+ window.doFuzzySearch = async () => {
1281
+ const q = document.getElementById('search-inp').value.trim()
1282
+ if (!q) return
1283
+ document.getElementById('product-list').innerHTML = loading$()
1284
+ let data = {}
1285
+ try {
1286
+ const r = await fetch('/api/search-fuzzy?q=' + encodeURIComponent(q))
1287
+ data = await r.json()
1288
+ } catch (e) {
1289
+ data = { products: [] }
1290
+ }
1291
+ const products = Array.isArray(data.products) ? data.products : []
1292
+ const banner = products.length
1293
+ ? `<div class="alert" style="margin-bottom:12px">🔍 ${t('模糊匹配到')} ${products.length} ${t('件')}(${t('命中≥')}${Math.round((data.score_threshold || 0.5) * 100)}%)</div>`
1294
+ : `<div class="alert" style="margin-bottom:12px">${t('模糊搜索也没找到。试试更短的关键词。')}</div>`
1295
+ renderSearchResults(products, banner, q)
1296
+ }