trustbase-seller-backend 0.7.3 → 0.7.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trustbase-seller-backend",
3
- "version": "0.7.3",
3
+ "version": "0.7.5",
4
4
  "description": "TrustBase Seller Backend (merchant edition) — 商品上链/订单/支付/质押 write API for TrustBase 保障链生态. 单卖家自托管形态: 签名 key 即卖家身份, 私钥不出门.",
5
5
  "main": "run-unified.js",
6
6
  "bin": {
package/run-unified.js CHANGED
@@ -923,7 +923,13 @@ async function startUnifiedServer() {
923
923
  }));
924
924
  console.log(` ✅ Chain RPC/LCD proxy mounted: /rpc -> ${CHAIN_RPC_PROXY_TARGET}, /lcd -> ${CHAIN_LCD_PROXY_TARGET}`);
925
925
  // 7.5 静态文件托管(PRD-E §5.1)
926
- const webDist = node_path_1.default.resolve(__dirname, '../../web/dist');
926
+ // 2026-10-17 kimi: 买家 PWA 已内嵌到包内 web/dist;优先包内路径,
927
+ // 包外 ../../web/dist 仅作老部署兼容,避免“文件装上了却打不开页面”。
928
+ const webDistCandidates = [
929
+ node_path_1.default.resolve(__dirname, 'web/dist'),
930
+ node_path_1.default.resolve(__dirname, '../../web/dist'),
931
+ ];
932
+ const webDist = webDistCandidates.find((p) => node_fs_1.default.existsSync(p)) || webDistCandidates[0];
927
933
  if (node_fs_1.default.existsSync(webDist)) {
928
934
  // index.html 不缓存 (baobei 15442): index.html 引用带 hash 的 assets, 缓存旧版会引旧 hash → 404/旧内容.
929
935
  // assets 本身 (带 content hash) 走 maxAge 长缓存.
@@ -0,0 +1,398 @@
1
+ /**
2
+ * pwa-shop-demo — 极简店铺 PWA (pi 2026-09-29)
3
+ *
4
+ * 需求: prd-docs/开发文档/示例店铺-pwa-shop-demo-需求骨架-2026-09-29.md
5
+ * 红线: 买家 UI 零链上术语 (不说链上/助记词/gas/钱包/区块)
6
+ * 技术: 纯静态 vanilla JS, 无框架无构建, clone 挂静态服务器即跑
7
+ */
8
+
9
+ const API = window.location.origin + '/api';
10
+ const app = document.getElementById('app');
11
+ const offlineBar = document.getElementById('offline-bar');
12
+
13
+ // 2026-10-17 kimi: 不再静默白屏 — 把启动/脚本/异步错误直接显示在页面上。
14
+ function showFatalError(where, err) {
15
+ const msg = err && err.message ? err.message : String(err || '未知错误');
16
+ app.innerHTML = `<div class="empty"><div class="empty-icon">⚠️</div><p>${esc(where)}: ${esc(msg)}</p></div>`;
17
+ }
18
+ window.addEventListener('error', (e) => showFatalError('脚本错误', e.error || e.message));
19
+ window.addEventListener('unhandledrejection', (e) => showFatalError('异步错误', e.reason));
20
+
21
+ // ============================================================
22
+ // 状态管理 (极简)
23
+ // ============================================================
24
+ let state = {
25
+ view: 'list', // list | detail | pay | order
26
+ products: [],
27
+ currentProduct: null,
28
+ currentOrder: null,
29
+ payTimer: null,
30
+ };
31
+
32
+ // ============================================================
33
+ // 安全卡 (买家身份, 本地生成, 静默)
34
+ // ============================================================
35
+ function getCardId() {
36
+ let id = localStorage.getItem('tb_card_id');
37
+ if (!id) {
38
+ id = 'card_' + crypto.randomUUID().replace(/-/g, '').slice(0, 16);
39
+ localStorage.setItem('tb_card_id', id);
40
+ }
41
+ return id;
42
+ }
43
+
44
+ // ============================================================
45
+ // API 调用
46
+ // ============================================================
47
+ async function fetchProducts() {
48
+ const res = await fetch(`${API}/products`);
49
+ if (!res.ok) throw new Error('获取商品失败');
50
+ return res.json();
51
+ }
52
+
53
+ async function fetchProduct(id) {
54
+ const res = await fetch(`${API}/products/${id}`);
55
+ if (!res.ok) throw new Error('获取商品详情失败');
56
+ return res.json();
57
+ }
58
+
59
+ async function createOrder(productId, quantity, shippingAddress) {
60
+ const body = {
61
+ product_id: productId,
62
+ quantity: quantity,
63
+ buyer_card: getCardId(),
64
+ };
65
+ // A1: physical 商品必传收货地址; virtual 不传 (后端 buyer-shop.js:385-389)
66
+ if (shippingAddress) body.shipping_address = shippingAddress;
67
+ const res = await fetch(`${API}/orders`, {
68
+ method: 'POST',
69
+ headers: { 'Content-Type': 'application/json' },
70
+ body: JSON.stringify(body),
71
+ });
72
+ if (!res.ok) throw new Error('下单失败');
73
+ return res.json();
74
+ }
75
+
76
+ async function fetchOrder(orderId) {
77
+ const res = await fetch(`${API}/orders/${orderId}`);
78
+ if (!res.ok) throw new Error('获取订单失败');
79
+ return res.json();
80
+ }
81
+
82
+ // ============================================================
83
+ // 视图: 商品列表
84
+ // ============================================================
85
+ function renderList() {
86
+ state.view = 'list';
87
+ app.innerHTML = '<div class="loading">加载中…</div>';
88
+ fetchProducts()
89
+ .then(products => {
90
+ state.products = products;
91
+ if (!products.length) {
92
+ app.innerHTML = '<div class="empty"><div class="empty-icon">🏪</div><p>暂无商品</p></div>';
93
+ return;
94
+ }
95
+ app.innerHTML = `
96
+ <div style="padding:16px 0;font-size:20px;font-weight:700">商品</div>
97
+ <div class="product-grid">
98
+ ${products.map(p => `
99
+ <div class="product-card" onclick="location.hash='#product/${p.id}'">
100
+ <img class="product-img" src="${p.image_url || '/placeholder.svg'}" alt="${esc(p.name)}" loading="lazy">
101
+ <div class="product-info">
102
+ <div class="product-name">${esc(p.name)}</div>
103
+ <div class="product-price">¥${fmt(p.price)}</div>
104
+ <div class="product-stock">${p.stock > 0 ? '库存 ' + p.stock : '暂时缺货'}</div>
105
+ </div>
106
+ </div>
107
+ `).join('')}
108
+ </div>
109
+ `;
110
+ })
111
+ .catch(() => {
112
+ app.innerHTML = '<div class="empty"><div class="empty-icon">📡</div><p>无法连接店铺</p></div>';
113
+ });
114
+ }
115
+
116
+ // ============================================================
117
+ // 视图: 商品详情
118
+ // ============================================================
119
+ function renderDetail(productId) {
120
+ state.view = 'detail';
121
+ app.innerHTML = '<div class="loading">加载中…</div>';
122
+ fetchProduct(productId)
123
+ .then(p => {
124
+ state.currentProduct = p;
125
+ app.innerHTML = `
126
+ <div style="padding:12px 0">
127
+ <button class="btn btn-secondary" style="width:auto;padding:8px 16px;font-size:14px" onclick="location.hash=''">← 返回</button>
128
+ </div>
129
+ <img class="detail-img" src="${p.image_url || '/placeholder.svg'}" alt="${esc(p.name)}">
130
+ <div class="detail-name">${esc(p.name)}</div>
131
+ <div class="detail-desc">${esc(p.description || '')}</div>
132
+ <div class="detail-price">¥${fmt(p.price)}</div>
133
+ <div style="font-size:13px;color:var(--muted);margin-bottom:16px">
134
+ ${p.stock > 0 ? '库存 ' + p.stock + ' 件' : '暂时缺货'}
135
+ </div>
136
+ ${(p.product_type || 'physical') === 'physical' ? `
137
+ <div class="field" style="margin-bottom:16px">
138
+ <label style="font-size:13px;color:var(--muted)">收货地址(实物商品必填)</label>
139
+ <textarea id="shipping-addr" rows="2" maxlength="500" placeholder="省市区 + 详细地址 + 收件人 + 电话" style="width:100%;padding:10px;border:1px solid var(--line);border-radius:8px;font-size:14px;resize:vertical"></textarea>
140
+ </div>` : ''}
141
+ <button class="btn" ${p.stock <= 0 || p.price <= 0 ? 'disabled' : ''} onclick="handleBuy('${p.id}')">
142
+ ${p.stock > 0 && p.price > 0 ? '立即购买' : (p.price <= 0 ? '暂未定价' : '暂时缺货')}
143
+ </button>
144
+ `;
145
+ })
146
+ .catch(() => {
147
+ app.innerHTML = '<div class="empty"><div class="empty-icon">📦</div><p>商品不存在</p></div>';
148
+ });
149
+ }
150
+
151
+ // ============================================================
152
+ // 视图: 支付 (扫码)
153
+ // ============================================================
154
+ function renderPay(order) {
155
+ state.view = 'pay';
156
+ state.currentOrder = order;
157
+ app.innerHTML = `
158
+ <div style="padding:12px 0">
159
+ <button class="btn btn-secondary" style="width:auto;padding:8px 16px;font-size:14px" onclick="location.hash='#product/${order.product_id}'">← 返回商品</button>
160
+ </div>
161
+ <div class="qr-container">
162
+ <div style="font-size:17px;font-weight:600;margin-bottom:20px">微信扫码支付</div>
163
+ <div class="qr-box"><canvas id="qr-canvas"></canvas></div>
164
+ <div class="qr-hint">¥${fmt(order.amount)} · 请使用微信扫一扫</div>
165
+ <div id="pay-code-url" style="font-size:11px;color:var(--muted);margin-top:8px;word-break:break-all;max-width:260px"></div>
166
+ <div style="margin-top:24px;font-size:13px;color:var(--muted)">
167
+ 订单号: ${esc(order.id.slice(0, 12))}…<br>
168
+ <span id="pay-status">等待支付中…</span>
169
+ </div>
170
+ </div>
171
+ `;
172
+
173
+ // 二维码: 改由后端渲染 (2026-10-03 kimi)
174
+ // 原因: 包内 qrcode.min.js (kazuhiko arase qrcode-generator) 的 UMD 只处理 AMD/CommonJS,
175
+ // 浏览器里既没有 toCanvas, window.qrcode 也不可靠 → 支付页二维码长期空白。
176
+ // 后端 GET /api/qr?text=<urlencoded> 返回 SVG (只认 weixin://wxpay/ 前缀)。
177
+ if (order.code_url) {
178
+ const box = document.getElementById('qr-canvas');
179
+ if (box) {
180
+ const img = document.createElement('img');
181
+ img.src = '/api/qr?text=' + encodeURIComponent(order.code_url);
182
+ img.alt = '微信支付二维码';
183
+ img.width = 220;
184
+ img.height = 220;
185
+ img.style.display = 'block';
186
+ img.style.margin = '0 auto';
187
+ img.onerror = function () {
188
+ const d = document.createElement('div');
189
+ d.style.cssText = 'padding:24px;color:var(--muted);font-size:13px;word-break:break-all';
190
+ d.innerHTML = '二维码加载失败<br>可在微信里直接打开下面的支付链接';
191
+ img.replaceWith(d);
192
+ };
193
+ // 注意: #qr-canvas 是 <canvas>, 其子元素不渲染 —— 必须整体替换, 不能 append 进去
194
+ box.replaceWith(img);
195
+ }
196
+ // 兜底: 支付链接原文 (二维码不可用时可复制到微信打开)
197
+ const hint = document.getElementById('pay-code-url');
198
+ if (hint) hint.textContent = order.code_url;
199
+ }
200
+
201
+ // 轮询订单状态
202
+ startPayPolling(order.id);
203
+ }
204
+
205
+ function startPayPolling(orderId) {
206
+ stopPayPolling();
207
+ let pollCount = 0;
208
+ state.payTimer = setInterval(() => {
209
+ pollCount++;
210
+ if (pollCount > 300) { stopPayPolling(); return; }
211
+ fetchOrder(orderId)
212
+ .then(order => {
213
+ if (order.status === 'paid' || order.status === 'completed') {
214
+ stopPayPolling();
215
+ location.hash = `#order/${orderId}`;
216
+ }
217
+ })
218
+ .catch(() => {}); // 静默重试
219
+ }, 3000);
220
+ }
221
+
222
+ function stopPayPolling() {
223
+ if (state.payTimer) {
224
+ clearInterval(state.payTimer);
225
+ state.payTimer = null;
226
+ }
227
+ }
228
+
229
+ // ============================================================
230
+ // 视图: 订单状态
231
+ // ============================================================
232
+ function renderOrder(orderId) {
233
+ state.view = 'order';
234
+ app.innerHTML = '<div class="loading">加载中…</div>';
235
+ fetchOrder(orderId)
236
+ .then(order => {
237
+ const statusMap = {
238
+ 'pending': ['待支付', 'status-pending'],
239
+ 'paid': ['已支付', 'status-paid'],
240
+ 'shipped': ['已发货', 'status-paid'],
241
+ 'completed': ['已完成', 'status-done'],
242
+ };
243
+ const [statusText, statusClass] = statusMap[order.status] || ['处理中', 'status-pending'];
244
+ app.innerHTML = `
245
+ <div style="padding:24px 0">
246
+ <span class="status-badge ${statusClass}">${statusText}</span>
247
+ ${order.status === 'pending' ? '<button class="btn" style="margin-top:16px" onclick="handlePay(\'' + order.id + '\')">继续支付</button>' : ''}
248
+ ${order.status === 'paid' || order.status === 'completed' ? '<div style="margin-top:16px;font-size:14px;color:var(--ok)">✓ 交易已存证</div>' : ''}
249
+ </div>
250
+ <div class="product-card" style="cursor:default">
251
+ <div class="product-info">
252
+ <div class="product-name">${esc(order.product_name || '商品')}</div>
253
+ <div class="product-price">¥${fmt(order.amount)}</div>
254
+ <div style="font-size:12px;color:var(--muted);margin-top:8px">
255
+ 订单号: ${esc(order.id)}<br>
256
+ 数量: ${order.quantity || 1}<br>
257
+ ${order.tx_hash ? '凭证: ' + esc(order.tx_hash.slice(0, 16)) + '…' : ''}
258
+ </div>
259
+ </div>
260
+ </div>
261
+ <button class="btn btn-secondary" style="margin-top:24px" onclick="location.hash=''">返回首页</button>
262
+ `;
263
+ })
264
+ .catch(() => {
265
+ app.innerHTML = '<div class="empty"><div class="empty-icon">📋</div><p>订单不存在</p></div>';
266
+ });
267
+ }
268
+
269
+
270
+ function loadQrLib(cb) {
271
+ if (window.qrcode) return cb();
272
+ var script = document.createElement('script');
273
+ script.src = '/qrcode.min.js';
274
+ script.onload = cb;
275
+ script.onerror = cb;
276
+ document.head.appendChild(script);
277
+ }
278
+
279
+ // kazuhiko qrcode-generator API 直绘 canvas (typeNumber 0 = 自动, 'M' 纠错)
280
+ function drawQrToCanvas(canvas, text, size) {
281
+ const qr = window.qrcode(0, 'M');
282
+ qr.addData(text);
283
+ qr.make();
284
+ const count = qr.getModuleCount();
285
+ canvas.width = size;
286
+ canvas.height = size;
287
+ const ctx = canvas.getContext('2d');
288
+ ctx.fillStyle = '#FFFFFF';
289
+ ctx.fillRect(0, 0, size, size);
290
+ ctx.fillStyle = '#000000';
291
+ const quiet = 2; // 白边 (模块数)
292
+ const cell = size / (count + quiet);
293
+ for (let r = 0; r < count; r++) {
294
+ for (let c = 0; c < count; c++) {
295
+ if (qr.isDark(r, c)) {
296
+ ctx.fillRect(
297
+ Math.floor((c + quiet / 2) * cell),
298
+ Math.floor((r + quiet / 2) * cell),
299
+ Math.ceil(cell), Math.ceil(cell)
300
+ );
301
+ }
302
+ }
303
+ }
304
+ }
305
+
306
+ // ============================================================
307
+ // 事件处理
308
+ // ============================================================
309
+ window.handleBuy = function(productId) {
310
+ // A1: physical 商品前端先拦收货地址 (后端 buyer-shop.js:387 也拦, 双重保证)
311
+ const p = state.currentProduct;
312
+ let shippingAddress = '';
313
+ if (p && (p.product_type || 'physical') === 'physical') {
314
+ const ta = document.getElementById('shipping-addr');
315
+ shippingAddress = ta ? ta.value.trim() : '';
316
+ if (!shippingAddress) {
317
+ alert('请填写收货地址(实物商品必需)');
318
+ if (ta) ta.focus();
319
+ return;
320
+ }
321
+ }
322
+ app.innerHTML = '<div class="loading">正在下单…</div>';
323
+ createOrder(productId, 1, shippingAddress)
324
+ .then(order => {
325
+ if (order.code_url) {
326
+ renderPay(order);
327
+ } else {
328
+ location.hash = `#order/${order.id}`;
329
+ }
330
+ })
331
+ .catch(err => {
332
+ app.innerHTML = `<div class="empty"><div class="empty-icon">⚠️</div><p>${esc(err.message)}</p><button class="btn btn-secondary" style="margin-top:16px" onclick="location.hash='#product/${productId}'">返回</button></div>`;
333
+ });
334
+ };
335
+
336
+ window.handlePay = function(orderId) {
337
+ app.innerHTML = '<div class="loading">正在获取支付信息…</div>';
338
+ fetchOrder(orderId).then(order => {
339
+ if (order.code_url) {
340
+ renderPay(order);
341
+ } else {
342
+ renderOrder(orderId);
343
+ }
344
+ });
345
+ };
346
+
347
+ // ============================================================
348
+ // 路由 (hash-based)
349
+ // ============================================================
350
+ function route() {
351
+ // 注意: location.hash 自带 '#', slice(1) 后已无 '#', 比较时不能再带 '#'
352
+ // (2026-10-02 kimi 修: 原实现 startsWith('#product/') 恒为假 → 详情/下单/支付页全部不可达, 买家流程整条断在这)
353
+ const hash = location.hash.slice(1);
354
+ stopPayPolling();
355
+
356
+ if (hash.startsWith('product/')) {
357
+ renderDetail(hash.replace('product/', ''));
358
+ } else if (hash.startsWith('order/')) {
359
+ renderOrder(hash.replace('order/', ''));
360
+ } else {
361
+ renderList();
362
+ }
363
+ }
364
+
365
+ window.addEventListener('hashchange', route);
366
+
367
+ // ============================================================
368
+ // 离线检测
369
+ // ============================================================
370
+ window.addEventListener('online', () => { offlineBar.style.display = 'none'; });
371
+ window.addEventListener('offline', () => { offlineBar.style.display = 'block'; });
372
+
373
+ // ============================================================
374
+ // 工具函数
375
+ // ============================================================
376
+ function esc(s) {
377
+ return String(s || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
378
+ }
379
+
380
+ function fmt(n) {
381
+ return Number(n || 0).toFixed(2);
382
+ }
383
+
384
+ // ============================================================
385
+ // 启动
386
+ // ============================================================
387
+ getCardId(); // 确保安全卡存在
388
+
389
+ // SW 注册 (安全上下文先判: https 或 localhost 才可用)
390
+ if ('serviceWorker' in navigator) {
391
+ navigator.serviceWorker.register('/sw.js', { scope: '/' })
392
+ .then(function(reg) { console.log('[PWA] SW registered, scope:', reg.scope); })
393
+ .catch(function(err) { console.warn('[PWA] SW not available:', err.message); });
394
+ } else {
395
+ console.warn('[PWA] ServiceWorker not supported or insecure context');
396
+ }
397
+
398
+ route();
@@ -0,0 +1,7 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
2
+ <rect width="64" height="64" rx="14" fill="#F7F3EB"/>
3
+ <path d="M20 24 L28 16 L36 16 L44 24 L44 48 L20 48 Z" fill="#D4753E"/>
4
+ <rect x="24" y="34" width="16" height="14" fill="#F7F3EB"/>
5
+ <rect x="26" y="20" width="4" height="8" fill="#F7F3EB" opacity="0.6"/>
6
+ <rect x="34" y="20" width="4" height="8" fill="#F7F3EB" opacity="0.6"/>
7
+ </svg>
@@ -0,0 +1,126 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
6
+ <meta name="theme-color" content="#F7F3EB">
7
+ <link rel="manifest" href="/manifest.json">
8
+ <link rel="icon" href="/icon.svg" type="image/svg+xml">
9
+ <title>店铺</title>
10
+ <style>
11
+ :root {
12
+ --bg: #F7F3EB;
13
+ --card: #FFFFFF;
14
+ --ink: #1a1a2e;
15
+ --muted: #8a8a9a;
16
+ --accent: #D4753E;
17
+ --accent-hover: #C4682F;
18
+ --ok: #5B8C5A;
19
+ --radius: 12px;
20
+ }
21
+ * { box-sizing: border-box; margin: 0; padding: 0; }
22
+ body {
23
+ font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Noto Sans SC', sans-serif;
24
+ background: var(--bg);
25
+ color: var(--ink);
26
+ min-height: 100vh;
27
+ padding-bottom: env(safe-area-inset-bottom);
28
+ }
29
+ .container { max-width: 640px; margin: 0 auto; padding: 16px; }
30
+
31
+ /* 商品列表 */
32
+ .product-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px; }
33
+ .product-card {
34
+ background: var(--card);
35
+ border-radius: var(--radius);
36
+ overflow: hidden;
37
+ cursor: pointer;
38
+ transition: transform 0.15s ease, box-shadow 0.15s ease;
39
+ }
40
+ .product-card:active { transform: scale(0.98); }
41
+ .product-img { width: 100%; aspect-ratio: 4/3; object-fit: cover; background: #eee; }
42
+ .product-info { padding: 14px; }
43
+ .product-name { font-size: 15px; font-weight: 600; line-height: 1.4; margin-bottom: 6px; }
44
+ .product-price { font-size: 18px; font-weight: 700; color: var(--accent); }
45
+ .product-stock { font-size: 12px; color: var(--muted); margin-top: 4px; }
46
+
47
+ /* 商品详情 */
48
+ .detail-img { width: 100%; aspect-ratio: 4/3; object-fit: cover; border-radius: var(--radius); }
49
+ .detail-name { font-size: 22px; font-weight: 700; margin: 16px 0 8px; }
50
+ .detail-desc { font-size: 14px; color: var(--muted); line-height: 1.6; margin-bottom: 16px; white-space: pre-wrap; }
51
+ .detail-price { font-size: 28px; font-weight: 700; color: var(--accent); margin-bottom: 24px; }
52
+
53
+ /* 按钮 */
54
+ .btn {
55
+ display: block;
56
+ width: 100%;
57
+ padding: 16px;
58
+ border: none;
59
+ border-radius: var(--radius);
60
+ background: var(--accent);
61
+ color: #fff;
62
+ font-size: 17px;
63
+ font-weight: 600;
64
+ cursor: pointer;
65
+ text-align: center;
66
+ transition: background 0.15s;
67
+ }
68
+ .btn:hover { background: var(--accent-hover); }
69
+ .btn:disabled { opacity: 0.5; cursor: not-allowed; }
70
+ .btn-secondary { background: transparent; color: var(--accent); border: 1px solid var(--accent); }
71
+
72
+ /* 支付二维码 */
73
+ .qr-container { text-align: center; padding: 32px 16px; }
74
+ .qr-box {
75
+ display: inline-block;
76
+ background: #fff;
77
+ padding: 24px;
78
+ border-radius: var(--radius);
79
+ box-shadow: 0 4px 24px rgba(0,0,0,0.08);
80
+ }
81
+ .qr-box canvas { display: block; }
82
+ .qr-hint { font-size: 14px; color: var(--muted); margin-top: 16px; }
83
+
84
+ /* 订单状态 */
85
+ .status-badge {
86
+ display: inline-block;
87
+ padding: 6px 16px;
88
+ border-radius: 20px;
89
+ font-size: 13px;
90
+ font-weight: 600;
91
+ margin-bottom: 16px;
92
+ }
93
+ .status-pending { background: #FFF3CD; color: #856404; }
94
+ .status-paid { background: #D4EDDA; color: #155724; }
95
+ .status-done { background: #CCE5FF; color: #004085; }
96
+
97
+ /* 空状态 */
98
+ .empty { text-align: center; padding: 60px 16px; color: var(--muted); }
99
+ .empty-icon { font-size: 48px; margin-bottom: 12px; }
100
+
101
+ /* 离线提示 */
102
+ .offline-bar {
103
+ background: #ff9800;
104
+ color: #fff;
105
+ text-align: center;
106
+ padding: 6px;
107
+ font-size: 12px;
108
+ display: none;
109
+ }
110
+
111
+ /* 加载 */
112
+ .loading { text-align: center; padding: 60px; color: var(--muted); }
113
+
114
+ @media (max-width: 480px) {
115
+ .product-grid { grid-template-columns: 1fr; }
116
+ .container { padding: 12px; }
117
+ }
118
+ </style>
119
+ </head>
120
+ <body>
121
+ <div class="offline-bar" id="offline-bar">⚠ 网络已断开,显示缓存内容</div>
122
+ <div class="container" id="app"></div>
123
+
124
+ <script type="module" src="/app.js?v=20261017a"></script>
125
+ </body>
126
+ </html>
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "TrustBase 示例店铺",
3
+ "short_name": "店铺",
4
+ "description": "TrustBase 极简示例店铺 — 商品·下单·扫码支付",
5
+ "start_url": "/",
6
+ "scope": "/",
7
+ "display": "standalone",
8
+ "background_color": "#F7F3EB",
9
+ "theme_color": "#F7F3EB",
10
+ "icons": [
11
+ { "src": "/icon.svg", "sizes": "any", "type": "image/svg+xml" }
12
+ ]
13
+ }