Ethscriptions commited on
Commit
01ec901
·
verified ·
1 Parent(s): 0f4b3b8

Upload 4 files

Browse files
Files changed (4) hide show
  1. icons/icon.svg +13 -0
  2. index.html +1222 -18
  3. manifest.webmanifest +19 -0
  4. sw.js +47 -0
icons/icon.svg ADDED
index.html CHANGED
@@ -1,19 +1,1223 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
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" />
6
+ <meta name="theme-color" content="#4c7dff" />
7
+ <meta name="apple-mobile-web-app-capable" content="yes" />
8
+ <meta name="apple-mobile-web-app-status-bar-style" content="default" />
9
+ <meta name="apple-mobile-web-app-title" content="分摊器" />
10
+ <link rel="manifest" href="./manifest.webmanifest" />
11
+ <link rel="icon" href="./icons/icon.svg" type="image/svg+xml" />
12
+ <title>外卖订单收益分摊器</title>
13
+ <style>
14
+ :root {
15
+ --bg-a: #f7fbff;
16
+ --bg-b: #fff4fb;
17
+ --panel: #ffffff;
18
+ --panel-soft: #f4f8ff;
19
+ --line: #d7e3ff;
20
+ --line-strong: #b8ccff;
21
+ --text-main: #253054;
22
+ --text-sub: #6b7ba8;
23
+ --accent: #4c7dff;
24
+ --accent-2: #28d7a7;
25
+ --accent-3: #ff7ec7;
26
+ --ok: #1fa97f;
27
+ --radius: 14px;
28
+ --shadow: 0 6px 14px rgba(76, 125, 255, 0.10);
29
+ }
30
+
31
+ * { box-sizing: border-box; margin: 0; padding: 0; }
32
+
33
+ body {
34
+ font-family: "Microsoft YaHei", "PingFang SC", sans-serif;
35
+ background: linear-gradient(140deg, var(--bg-a), var(--bg-b));
36
+ color: var(--text-main);
37
+ min-height: 100vh;
38
+ overflow: hidden;
39
+ user-select: none;
40
+ }
41
+
42
+ input, textarea { user-select: text; }
43
+
44
+ .app {
45
+ height: 100vh;
46
+ display: flex;
47
+ flex-direction: column;
48
+ gap: 0;
49
+ padding: 10px;
50
+ }
51
+
52
+ .btn {
53
+ border: 1px solid var(--line-strong);
54
+ background: #f2f6ff;
55
+ color: #2d3f72;
56
+ border-radius: 12px;
57
+ padding: 10px 14px;
58
+ font-size: 16px;
59
+ font-weight: 700;
60
+ cursor: pointer;
61
+ transition: .14s;
62
+ }
63
+
64
+ .btn:hover { box-shadow: 0 3px 8px rgba(76, 125, 255, 0.12); }
65
+ .btn-primary { border: none; background: #4c7dff; color: #fff; }
66
+
67
+ .main {
68
+ flex: 1;
69
+ min-height: 0;
70
+ display: grid;
71
+ grid-template-columns: 1.15fr 1fr 1.25fr;
72
+ gap: 10px;
73
+ height: 100%;
74
+ }
75
+
76
+ .panel {
77
+ min-height: 0;
78
+ overflow: hidden;
79
+ border: 1px solid var(--line);
80
+ border-radius: var(--radius);
81
+ background: var(--panel);
82
+ box-shadow: var(--shadow);
83
+ display: flex;
84
+ flex-direction: column;
85
+ }
86
+
87
+ .panel-title {
88
+ padding: 12px 14px;
89
+ background: linear-gradient(90deg, #edf3ff, #fff2fb);
90
+ border-bottom: 1px solid var(--line);
91
+ display: flex;
92
+ justify-content: space-between;
93
+ align-items: center;
94
+ gap: 10px;
95
+ font-size: 20px;
96
+ font-weight: 800;
97
+ color: #3450a7;
98
+ }
99
+
100
+ .title-actions {
101
+ display: flex;
102
+ align-items: center;
103
+ gap: 8px;
104
+ flex-wrap: wrap;
105
+ justify-content: flex-end;
106
+ }
107
+
108
+ .panel-title small { font-size: 15px; font-weight: 600; color: var(--text-sub); }
109
+
110
+ .controls {
111
+ padding: 12px;
112
+ border-bottom: 1px solid var(--line);
113
+ display: grid;
114
+ grid-template-columns: 1fr 1fr;
115
+ gap: 10px;
116
+ background: var(--panel-soft);
117
+ }
118
+
119
+ .search-row {
120
+ grid-column: span 2;
121
+ display: grid;
122
+ grid-template-columns: 1fr auto;
123
+ gap: 10px;
124
+ }
125
+
126
+ select, .text-input {
127
+ width: 100%;
128
+ border: 1px solid var(--line-strong);
129
+ border-radius: 10px;
130
+ background: #fff;
131
+ color: var(--text-main);
132
+ padding: 10px;
133
+ font-size: 16px;
134
+ outline: none;
135
+ }
136
+
137
+ .product-grid {
138
+ flex: 1;
139
+ min-height: 0;
140
+ overflow: auto;
141
+ padding: 12px;
142
+ display: grid;
143
+ gap: 10px;
144
+ align-content: start;
145
+ grid-template-columns: repeat(auto-fill, minmax(165px, 1fr));
146
+ }
147
+
148
+ .card {
149
+ border: 1px solid #d8e3ff;
150
+ border-radius: 12px;
151
+ padding: 10px;
152
+ min-height: 102px;
153
+ cursor: pointer;
154
+ display: flex;
155
+ flex-direction: column;
156
+ justify-content: space-between;
157
+ background: linear-gradient(140deg, #f7faff, #fff5fb);
158
+ transition: .14s;
159
+ }
160
+
161
+ .card:hover { transform: translateY(-1px); border-color: #9db9ff; box-shadow: 0 8px 16px rgba(76, 125, 255, 0.16); }
162
+ .card .name { font-size: 16px; line-height: 1.35; color: #2f3f72; word-break: break-all; }
163
+ .card .price { margin-top: 8px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-weight: 700; color: #1fbe8d; font-size: 24px; }
164
+
165
+ .order-wrap {
166
+ flex: 1;
167
+ min-height: 0;
168
+ overflow: auto;
169
+ padding: 12px;
170
+ display: flex;
171
+ flex-direction: column;
172
+ gap: 10px;
173
+ }
174
+
175
+ .order-item {
176
+ border: 1px solid #d5e1ff;
177
+ border-radius: 11px;
178
+ padding: 10px;
179
+ display: grid;
180
+ grid-template-columns: 1fr auto;
181
+ gap: 10px;
182
+ align-items: center;
183
+ background: #fbfdff;
184
+ }
185
+
186
+ .order-name { font-size: 17px; line-height: 1.35; color: #2c3b67; font-weight: 700; }
187
+
188
+ .order-prices {
189
+ margin-top: 6px;
190
+ display: grid;
191
+ grid-template-columns: 1fr;
192
+ gap: 8px;
193
+ font-size: 14px;
194
+ color: var(--text-sub);
195
+ }
196
+
197
+ .order-prices .mono { font-size: 17px; color: #3656b4; font-weight: 800; }
198
+
199
+ .order-origin-input {
200
+ width: 100%;
201
+ border: 1px solid #bfd2ff;
202
+ border-radius: 9px;
203
+ background: linear-gradient(135deg, #ffffff, #f6f9ff);
204
+ color: #2f5de0;
205
+ padding: 8px 10px;
206
+ font-size: 18px;
207
+ font-weight: 800;
208
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
209
+ cursor: text;
210
+ outline: none;
211
+ }
212
+
213
+ .order-origin-input:focus {
214
+ border-color: #7ea3ff;
215
+ box-shadow: 0 0 0 3px rgba(76, 125, 255, 0.14);
216
+ }
217
+
218
+ .qty-box { display: flex; align-items: center; gap: 8px; }
219
+
220
+ .mini-btn {
221
+ width: 34px;
222
+ height: 34px;
223
+ border-radius: 8px;
224
+ border: 1px solid #bad0ff;
225
+ background: #f1f6ff;
226
+ color: #3152b2;
227
+ cursor: pointer;
228
+ font-weight: 700;
229
+ font-size: 20px;
230
+ }
231
+
232
+ .qty-text {
233
+ min-width: 30px;
234
+ text-align: center;
235
+ color: #2f437d;
236
+ font-size: 18px;
237
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
238
+ }
239
+
240
+ .sum-line {
241
+ margin-top: auto;
242
+ border-top: 1px dashed #cedcff;
243
+ padding-top: 10px;
244
+ display: flex;
245
+ justify-content: space-between;
246
+ align-items: center;
247
+ gap: 8px;
248
+ color: #4761ad;
249
+ font-size: 24px;
250
+ font-weight: 800;
251
+ position: sticky;
252
+ bottom: 0;
253
+ z-index: 1;
254
+ background: linear-gradient(180deg, rgba(255,255,255,0.95), #fff);
255
+ padding-bottom: 6px;
256
+ }
257
+
258
+ .sum-line strong {
259
+ color: var(--ok);
260
+ font-size: 34px;
261
+ line-height: 1;
262
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
263
+ }
264
+
265
+ .calc-wrap {
266
+ padding: 12px;
267
+ border-bottom: 1px solid var(--line);
268
+ background: var(--panel-soft);
269
+ }
270
+
271
+ .calc-label { color: var(--text-sub); font-size: 15px; margin-bottom: 8px; }
272
+ .calc-row { display: grid; grid-template-columns: 1fr auto; gap: 10px; }
273
+
274
+ .amount-input {
275
+ border: 1px solid #b9ccff;
276
+ border-radius: 10px;
277
+ background: #fff;
278
+ color: #2f5de0;
279
+ font-size: 34px;
280
+ text-align: right;
281
+ padding: 10px;
282
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
283
+ outline: none;
284
+ }
285
+
286
+ .amount-input::placeholder { color: #9fb1df; }
287
+
288
+ .result-wrap {
289
+ flex: 1;
290
+ min-height: 0;
291
+ overflow: auto;
292
+ padding: 12px;
293
+ display: flex;
294
+ flex-direction: column;
295
+ gap: 10px;
296
+ }
297
+
298
+ .result-head {
299
+ border: 1px solid #d4e1ff;
300
+ border-radius: 12px;
301
+ padding: 10px;
302
+ background: #fff;
303
+ font-size: 16px;
304
+ color: #536aa2;
305
+ line-height: 1.6;
306
+ }
307
+
308
+ .result-item,
309
+ .check-box {
310
+ border: 1px solid #d4e1ff;
311
+ border-radius: 12px;
312
+ padding: 12px;
313
+ background: #fbfdff;
314
+ }
315
+
316
+ .result-top {
317
+ font-size: 19px;
318
+ font-weight: 800;
319
+ color: #3455b4;
320
+ margin-bottom: 10px;
321
+ }
322
+
323
+ .price-pair {
324
+ display: grid;
325
+ grid-template-columns: 1fr 1fr;
326
+ gap: 10px;
327
+ }
328
+
329
+ .price-box {
330
+ border: 1px solid #dce6ff;
331
+ border-radius: 10px;
332
+ background: #fff;
333
+ padding: 10px;
334
+ }
335
+
336
+ .price-box .label {
337
+ font-size: 15px;
338
+ color: #5e72a6;
339
+ margin-bottom: 6px;
340
+ display: block;
341
+ font-weight: 700;
342
+ }
343
+
344
+ .price-box .value {
345
+ font-size: 28px;
346
+ color: #2248b7;
347
+ font-weight: 800;
348
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
349
+ line-height: 1.1;
350
+ }
351
+
352
+ .price-box .value.actual { color: var(--ok); }
353
+
354
+ .check-box { font-size: 16px; color: #536aa2; line-height: 1.75; }
355
+ .mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
356
+
357
+ .empty {
358
+ border: 1px dashed #cadbff;
359
+ border-radius: 11px;
360
+ text-align: center;
361
+ padding: 22px 12px;
362
+ color: #8a9bc8;
363
+ font-size: 17px;
364
+ background: #fbfdff;
365
+ line-height: 1.6;
366
+ }
367
+
368
+ .modal {
369
+ position: fixed;
370
+ inset: 0;
371
+ display: none;
372
+ justify-content: center;
373
+ align-items: center;
374
+ z-index: 20;
375
+ padding: 16px;
376
+ background: rgba(94, 121, 190, 0.25);
377
+ backdrop-filter: blur(3px);
378
+ }
379
+
380
+ .modal.active { display: flex; }
381
+
382
+ .modal-box {
383
+ width: 700px;
384
+ max-width: 100%;
385
+ border: 1px solid #d7e2ff;
386
+ border-radius: 12px;
387
+ overflow: hidden;
388
+ background: #fff;
389
+ box-shadow: var(--shadow);
390
+ }
391
+
392
+ .modal-head {
393
+ background: linear-gradient(90deg, #edf3ff, #fff2fb);
394
+ border-bottom: 1px solid #d7e2ff;
395
+ padding: 12px 14px;
396
+ font-size: 20px;
397
+ font-weight: 800;
398
+ color: #3757b8;
399
+ }
400
+
401
+ .modal-body { padding: 14px; }
402
+ .modal-desc { color: #7587b6; font-size: 15px; line-height: 1.6; margin-bottom: 10px; }
403
+
404
+ textarea {
405
+ width: 100%;
406
+ min-height: 300px;
407
+ border: 1px solid #c9d8ff;
408
+ border-radius: 10px;
409
+ background: #fff;
410
+ color: #33457a;
411
+ padding: 12px;
412
+ font-size: 16px;
413
+ line-height: 1.6;
414
+ resize: vertical;
415
+ outline: none;
416
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
417
+ }
418
+
419
+ .modal-foot {
420
+ padding: 12px 14px;
421
+ border-top: 1px solid #d7e2ff;
422
+ display: flex;
423
+ justify-content: flex-end;
424
+ gap: 10px;
425
+ background: #f9fbff;
426
+ }
427
+
428
+ .numpad {
429
+ position: fixed;
430
+ right: 16px;
431
+ bottom: 16px;
432
+ width: 360px;
433
+ border: 1px solid #c5d7ff;
434
+ border-radius: 14px;
435
+ background: #fff;
436
+ box-shadow: var(--shadow);
437
+ padding: 12px;
438
+ z-index: 15;
439
+ display: none;
440
+ }
441
+
442
+ .numpad.active { display: block; }
443
+
444
+ .numpad-grid {
445
+ display: grid;
446
+ grid-template-columns: repeat(3, 1fr);
447
+ gap: 10px;
448
+ }
449
+
450
+ .key {
451
+ border: 1px solid #c5d6ff;
452
+ border-radius: 10px;
453
+ background: #f4f8ff;
454
+ color: #3954a8;
455
+ font-weight: 700;
456
+ font-size: 24px;
457
+ padding: 12px 0;
458
+ cursor: pointer;
459
+ }
460
+
461
+ .key:active { transform: scale(0.98); }
462
+
463
+ .key-fn { font-size: 16px; background: #eef3ff; color: #5a71b0; }
464
+ .key-ac { color: #b84a6a; background: #fff3f7; border-color: #ffd2e1; }
465
+ .key-enter { border: none; color: #fff; background: linear-gradient(135deg, var(--accent), var(--accent-2)); }
466
+
467
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
468
+ ::-webkit-scrollbar-thumb { background: #cfddff; border-radius: 6px; }
469
+
470
+ @media (max-width: 1260px) {
471
+ .main { grid-template-columns: 1fr; }
472
+ body { overflow: auto; }
473
+ .app { height: auto; min-height: 100vh; }
474
+ .numpad { width: calc(100% - 24px); right: 12px; }
475
+ .price-pair { grid-template-columns: 1fr; }
476
+ .order-prices { grid-template-columns: 1fr; }
477
+ }
478
+ </style>
479
+ </head>
480
+ <body>
481
+ <div class="app">
482
+ <main class="main">
483
+ <section class="panel">
484
+ <div class="panel-title">
485
+ <span>商品陈列</span>
486
+ <div class="title-actions">
487
+ <button class="btn btn-primary" id="openImportBtn">📥 导入商品</button>
488
+ <small id="productCountText">0 / 0 个商品</small>
489
+ </div>
490
+ </div>
491
+ <div class="controls">
492
+ <select id="sortSelect">
493
+ <option value="default">默认排序</option>
494
+ <option value="priceAsc">价格从低到高</option>
495
+ <option value="priceDesc">价格从高到低</option>
496
+ <option value="name">按名称排序</option>
497
+ </select>
498
+ <select id="priceFilterSelect">
499
+ <option value="all">全部价格分类</option>
500
+ </select>
501
+ <div class="search-row">
502
+ <input id="searchInput" class="text-input" placeholder="搜索:商品名称" />
503
+ <button class="btn" id="resetFilterBtn">重置筛选</button>
504
+ </div>
505
+ </div>
506
+ <div class="product-grid" id="productGrid"></div>
507
+ </section>
508
+
509
+ <section class="panel">
510
+ <div class="panel-title">
511
+ <span>当前订单</span>
512
+ <button class="btn" id="clearOrderBtn">清空订单</button>
513
+ </div>
514
+ <div class="order-wrap" id="orderWrap"></div>
515
+ </section>
516
+
517
+ <section class="panel">
518
+ <div class="panel-title">
519
+ <span>结算结果</span>
520
+ <button class="btn" id="clearResultBtn">清空结果</button>
521
+ </div>
522
+ <div class="calc-wrap">
523
+ <div class="calc-label">输入外卖平台订单优惠后总金额:</div>
524
+ <div class="calc-row">
525
+ <input id="payInput" class="amount-input" type="text" inputmode="decimal" placeholder="0.00" />
526
+ <button class="btn btn-primary" id="calcBtn">开始计算</button>
527
+ </div>
528
+ </div>
529
+ <div class="result-wrap" id="resultWrap"></div>
530
+ </section>
531
+ </main>
532
+ </div>
533
+
534
+ <div class="modal" id="importModal">
535
+ <div class="modal-box">
536
+ <div class="modal-head">导入商品(支持 Excel 复制粘贴)</div>
537
+ <div class="modal-body">
538
+ <p class="modal-desc">
539
+ 一行一个商品,最后一列为价格,可空格或 Tab 分隔。<br />
540
+ 示例:<code>爆C柠香绿 9.9</code>
541
+ </p>
542
+ <textarea id="importText"></textarea>
543
+ </div>
544
+ <div class="modal-foot">
545
+ <button class="btn" id="closeImportBtn">取消</button>
546
+ <button class="btn btn-primary" id="saveImportBtn">保存并应用</button>
547
+ </div>
548
+ </div>
549
+ </div>
550
+
551
+ <div class="numpad" id="numpad">
552
+ <div class="numpad-grid">
553
+ <button class="key" data-key="7">7</button>
554
+ <button class="key" data-key="8">8</button>
555
+ <button class="key" data-key="9">9</button>
556
+ <button class="key" data-key="4">4</button>
557
+ <button class="key" data-key="5">5</button>
558
+ <button class="key" data-key="6">6</button>
559
+ <button class="key" data-key="1">1</button>
560
+ <button class="key" data-key="2">2</button>
561
+ <button class="key" data-key="3">3</button>
562
+ <button class="key key-ac" data-key="clear">清空</button>
563
+ <button class="key" data-key="0">0</button>
564
+ <button class="key" data-key=".">.</button>
565
+ <button class="key key-fn" data-key="backspace">退格</button>
566
+ <button class="key key-fn" data-key="hide">收起</button>
567
+ <button class="key key-enter" data-key="calc" id="numpadActionBtn">计算</button>
568
+ </div>
569
+ </div>
570
+
571
+ <script>
572
+ const STORAGE_KEYS = { products: 'shop_products_v5' };
573
+
574
+ const state = {
575
+ products: [],
576
+ order: [],
577
+ result: null,
578
+ priceRanges: []
579
+ };
580
+
581
+ let activeNumpadInput = null;
582
+
583
+ const els = {
584
+ productGrid: document.getElementById('productGrid'),
585
+ productCountText: document.getElementById('productCountText'),
586
+ orderWrap: document.getElementById('orderWrap'),
587
+ resultWrap: document.getElementById('resultWrap'),
588
+ sortSelect: document.getElementById('sortSelect'),
589
+ priceFilterSelect: document.getElementById('priceFilterSelect'),
590
+ searchInput: document.getElementById('searchInput'),
591
+ payInput: document.getElementById('payInput'),
592
+ importModal: document.getElementById('importModal'),
593
+ importText: document.getElementById('importText'),
594
+ numpad: document.getElementById('numpad'),
595
+ numpadActionBtn: document.getElementById('numpadActionBtn')
596
+ };
597
+
598
+ function formatMoney(v) { return Number(v).toFixed(2); }
599
+
600
+ function escapeHtml(input) {
601
+ return String(input)
602
+ .replace(/&/g, '&amp;')
603
+ .replace(/</g, '&lt;')
604
+ .replace(/>/g, '&gt;')
605
+ .replace(/"/g, '&quot;')
606
+ .replace(/'/g, '&#39;');
607
+ }
608
+
609
+ function normalizeInputValue(val) {
610
+ const normalized = String(val).replace(/[。.。]/g, '.');
611
+ const only = normalized.replace(/[^\d.]/g, '');
612
+ if (!only) return '';
613
+ const parts = only.split('.');
614
+ const intPart = parts[0] || '0';
615
+ if (parts.length === 1) return intPart;
616
+ return `${intPart}.${parts.slice(1).join('').slice(0, 2)}`;
617
+ }
618
+
619
+ function normalizeProduct(item) {
620
+ const name = String(item.name || '').trim();
621
+ const price = Number(item.price);
622
+ return { name, price };
623
+ }
624
+
625
+ function loadProducts() {
626
+ try {
627
+ const raw = localStorage.getItem(STORAGE_KEYS.products);
628
+ if (!raw) {
629
+ state.products = [];
630
+ return;
631
+ }
632
+ const parsed = JSON.parse(raw);
633
+ const list = Array.isArray(parsed)
634
+ ? parsed
635
+ .map(normalizeProduct)
636
+ .filter((it) => it.name && isFinite(it.price) && it.price > 0)
637
+ : [];
638
+ state.products = list;
639
+ } catch {
640
+ state.products = [];
641
+ }
642
+ }
643
+
644
+ function saveProducts() {
645
+ localStorage.setItem(STORAGE_KEYS.products, JSON.stringify(state.products));
646
+ }
647
+
648
+ function parseProductsText(text) {
649
+ const lines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
650
+ const list = [];
651
+
652
+ for (const line of lines) {
653
+ const m = line.match(/^(.*?)[\s\t]+(-?\d+(?:\.\d+)?)$/);
654
+ if (!m) continue;
655
+ const name = m[1].trim();
656
+ const price = Number(m[2]);
657
+ if (!name || !isFinite(price) || price <= 0) continue;
658
+ list.push({ name, price });
659
+ }
660
+
661
+ return list;
662
+ }
663
+
664
+ function getPriceRanges(products) {
665
+ if (!products.length) return [];
666
+ const prices = products.map((p) => p.price).filter((n) => isFinite(n)).sort((a, b) => a - b);
667
+ if (!prices.length) return [];
668
+
669
+ const min = prices[0];
670
+ const max = prices[prices.length - 1];
671
+ if (min === max) {
672
+ return [{ id: 'r0', min, max, label: `¥${formatMoney(min)}(同一价格)` }];
673
+ }
674
+
675
+ const bucketCount = 4;
676
+ const span = (max - min) / bucketCount;
677
+ const ranges = [];
678
+
679
+ for (let i = 0; i < bucketCount; i += 1) {
680
+ const start = min + span * i;
681
+ const end = i === bucketCount - 1 ? max : min + span * (i + 1);
682
+ const label = i === bucketCount - 1
683
+ ? `¥${formatMoney(start)} ~ ¥${formatMoney(end)}`
684
+ : `¥${formatMoney(start)} ~ ¥${formatMoney(end)}`;
685
+ ranges.push({ id: `r${i}`, min: start, max: end, label });
686
+ }
687
+
688
+ return ranges;
689
+ }
690
+
691
+ function renderPriceFilterOptions() {
692
+ const prev = els.priceFilterSelect.value || 'all';
693
+ state.priceRanges = getPriceRanges(state.products);
694
+
695
+ const options = ['<option value="all">全部价格分类</option>'];
696
+ state.priceRanges.forEach((r) => {
697
+ options.push(`<option value="${r.id}">${r.label}</option>`);
698
+ });
699
+ els.priceFilterSelect.innerHTML = options.join('');
700
+
701
+ const canKeep = prev === 'all' || state.priceRanges.some((r) => r.id === prev);
702
+ els.priceFilterSelect.value = canKeep ? prev : 'all';
703
+ }
704
+
705
+ function openImportModal() {
706
+ els.importText.value = state.products.map((p) => `${p.name}\t${p.price}`).join('\n');
707
+ els.importModal.classList.add('active');
708
+ }
709
+
710
+ function closeImportModal() {
711
+ els.importModal.classList.remove('active');
712
+ }
713
+
714
+ function saveImport() {
715
+ const parsed = parseProductsText(els.importText.value);
716
+ if (!parsed.length) {
717
+ alert('未识别到有效商品,请检查格式:商品名 + 价格。');
718
+ return;
719
+ }
720
+ state.products = parsed;
721
+ renderPriceFilterOptions();
722
+ saveProducts();
723
+ renderProducts();
724
+ closeImportModal();
725
+ alert(`导入成功,共 ${parsed.length} 个商品。`);
726
+ }
727
+
728
+ function filteredProducts() {
729
+ const keyword = els.searchInput.value.trim().toLowerCase();
730
+ const sort = els.sortSelect.value;
731
+ const priceFilter = els.priceFilterSelect.value;
732
+
733
+ let list = state.products.filter((p) => {
734
+ if (keyword) {
735
+ const byName = p.name.toLowerCase().includes(keyword);
736
+ if (!byName) return false;
737
+ }
738
+
739
+ if (priceFilter !== 'all') {
740
+ const range = state.priceRanges.find((r) => r.id === priceFilter);
741
+ if (!range) return true;
742
+ const isLast = range.id === state.priceRanges[state.priceRanges.length - 1]?.id;
743
+ const inRange = isLast
744
+ ? p.price >= range.min && p.price <= range.max
745
+ : p.price >= range.min && p.price < range.max;
746
+ if (!inRange) return false;
747
+ }
748
+
749
+ return true;
750
+ });
751
+
752
+ if (sort === 'priceAsc') list.sort((a, b) => a.price - b.price);
753
+ else if (sort === 'priceDesc') list.sort((a, b) => b.price - a.price);
754
+ else if (sort === 'name') list.sort((a, b) => a.name.localeCompare(b.name, 'zh-CN'));
755
+
756
+ return list;
757
+ }
758
+
759
+ function resetProductFilters() {
760
+ els.sortSelect.value = 'default';
761
+ els.priceFilterSelect.value = 'all';
762
+ els.searchInput.value = '';
763
+ renderProducts();
764
+ }
765
+
766
+ function addProductToOrder(product) {
767
+ const found = state.order.find((it) => it.name === product.name && it.price === product.price);
768
+ if (found) {
769
+ found.qty += 1;
770
+ } else {
771
+ state.order.push({
772
+ name: product.name,
773
+ price: product.price,
774
+ qty: 1
775
+ });
776
+ }
777
+ state.result = null;
778
+ renderOrder();
779
+ }
780
+
781
+ function updateQty(index, delta) {
782
+ const item = state.order[index];
783
+ if (!item) return;
784
+ item.qty += delta;
785
+ if (item.qty <= 0) state.order.splice(index, 1);
786
+ state.result = null;
787
+ renderOrder();
788
+ }
789
+
790
+ function updateOrderPrice(index, value) {
791
+ const item = state.order[index];
792
+ if (!item) return;
793
+ const v = Number(value);
794
+ if (!isFinite(v) || v <= 0) return;
795
+ const nextPrice = Math.round(v * 100) / 100;
796
+ if (item.price === nextPrice) return;
797
+ item.price = nextPrice;
798
+ state.result = null;
799
+ renderOrderFooter();
800
+ renderResult();
801
+ }
802
+
803
+ function clearOrder() {
804
+ state.order = [];
805
+ state.result = null;
806
+ renderOrder();
807
+ }
808
+
809
+ function clearResult() {
810
+ state.result = null;
811
+ renderResult();
812
+ }
813
+
814
+ function getOrderSummary() {
815
+ const count = state.order.reduce((sum, item) => sum + item.qty, 0);
816
+ const total = state.order.reduce((sum, item) => sum + item.price * item.qty, 0);
817
+ return { count, total };
818
+ }
819
+
820
+ function renderOrderFooter() {
821
+ const sumEl = els.orderWrap.querySelector('.sum-line');
822
+ if (!sumEl) return;
823
+ const { count, total } = getOrderSummary();
824
+ sumEl.innerHTML = `<span>${count}件商品原价总和</span><strong>¥ ${formatMoney(total)}</strong>`;
825
+ }
826
+
827
+ function renderProducts() {
828
+ const displayList = filteredProducts();
829
+ els.productCountText.textContent = `${displayList.length} / ${state.products.length} 个商品`;
830
+
831
+ if (!state.products.length) {
832
+ els.productGrid.innerHTML = '<div class="empty">当前没有商品,请点击上方“导入商品”</div>';
833
+ return;
834
+ }
835
+
836
+ if (!displayList.length) {
837
+ els.productGrid.innerHTML = '<div class="empty">没有匹配商品,请按商品名称搜索</div>';
838
+ return;
839
+ }
840
+
841
+ const frag = document.createDocumentFragment();
842
+ displayList.forEach((p) => {
843
+ const card = document.createElement('div');
844
+ card.className = 'card';
845
+ card.innerHTML = `
846
+ <div class="name">${escapeHtml(p.name)}</div>
847
+ <div class="price">¥ ${formatMoney(p.price)}</div>
848
+ `;
849
+ card.addEventListener('click', () => addProductToOrder(p));
850
+ frag.appendChild(card);
851
+ });
852
+
853
+ els.productGrid.innerHTML = '';
854
+ els.productGrid.appendChild(frag);
855
+ }
856
+
857
+ function renderOrder() {
858
+ const frag = document.createDocumentFragment();
859
+
860
+ if (!state.order.length) {
861
+ const empty = document.createElement('div');
862
+ empty.className = 'empty';
863
+ empty.textContent = '请先从左侧商品陈列中点击添加商品';
864
+ frag.appendChild(empty);
865
+ } else {
866
+ state.order.forEach((item, index) => {
867
+ const subtotal = item.price * item.qty;
868
+ const row = document.createElement('div');
869
+ row.className = 'order-item';
870
+ row.innerHTML = `
871
+ <div>
872
+ <div class="order-name">${escapeHtml(item.name)}</div>
873
+ <div class="order-prices">
874
+ <div>原价:<input class="order-origin-input" data-price-index="${index}" value="${formatMoney(item.price)}" /></div>
875
+ <div>小计:<span class="mono">¥ ${formatMoney(subtotal)}</span></div>
876
+ </div>
877
+ </div>
878
+ <div class="qty-box">
879
+ <button class="mini-btn" data-action="minus" data-index="${index}">-</button>
880
+ <span class="qty-text">${item.qty}</span>
881
+ <button class="mini-btn" data-action="plus" data-index="${index}">+</button>
882
+ </div>
883
+ `;
884
+ frag.appendChild(row);
885
+ });
886
+ }
887
+
888
+ const { count, total } = getOrderSummary();
889
+ const sum = document.createElement('div');
890
+ sum.className = 'sum-line';
891
+ sum.innerHTML = `<span>${count}件商品原价总和</span><strong>¥ ${formatMoney(total)}</strong>`;
892
+
893
+ els.orderWrap.innerHTML = '';
894
+ els.orderWrap.appendChild(frag);
895
+ els.orderWrap.appendChild(sum);
896
+ renderResult();
897
+ }
898
+
899
+ function getAllocations(orderItems, payTotal) {
900
+ const totalOriginal = orderItems.reduce((sum, item) => sum + item.price * item.qty, 0);
901
+ const expanded = [];
902
+
903
+ orderItems.forEach((item) => {
904
+ for (let i = 0; i < item.qty; i += 1) {
905
+ expanded.push({ name: item.name, originalPrice: item.price, price: item.price });
906
+ }
907
+ });
908
+
909
+ const totalCount = expanded.length;
910
+ const rawAlloc = expanded.map((it) => (it.price / totalOriginal) * payTotal);
911
+ const rounded = rawAlloc.map((v) => Math.round(v * 100) / 100);
912
+ const roundedSum = rounded.reduce((a, b) => a + b, 0);
913
+ const firstDiff = Math.round((payTotal - roundedSum) * 100) / 100;
914
+ if (rounded.length && firstDiff !== 0) {
915
+ rounded[rounded.length - 1] = Math.round((rounded[rounded.length - 1] + firstDiff) * 100) / 100;
916
+ }
917
+
918
+ const seqMap = new Map();
919
+ const rows = expanded.map((item, idx) => {
920
+ const seq = (seqMap.get(item.name) || 0) + 1;
921
+ seqMap.set(item.name, seq);
922
+ return {
923
+ name: item.name,
924
+ seq,
925
+ originalPrice: item.originalPrice,
926
+ allocatedPrice: rounded[idx]
927
+ };
928
+ });
929
+
930
+ const allocatedTotal = rows.reduce((sum, row) => sum + row.allocatedPrice, 0);
931
+ return {
932
+ rows,
933
+ totalOriginal,
934
+ totalCount,
935
+ allocatedTotal,
936
+ diff: Math.round((payTotal - allocatedTotal) * 100) / 100
937
+ };
938
+ }
939
+
940
+ function calculate() {
941
+ if (!state.order.length) {
942
+ alert('请先添加订单商品。');
943
+ return;
944
+ }
945
+
946
+ const payTotal = Number(els.payInput.value);
947
+ if (!isFinite(payTotal) || payTotal <= 0) {
948
+ alert('请输入有效的订单优惠后总金额。');
949
+ els.payInput.focus();
950
+ openNumpad();
951
+ return;
952
+ }
953
+
954
+ state.result = { payTotal, ...getAllocations(state.order, payTotal) };
955
+ renderResult();
956
+ closeNumpad();
957
+ }
958
+
959
+ function renderResult() {
960
+ if (!state.result || !state.order.length) {
961
+ if (lastResultEmptyState === true) return;
962
+ lastResultEmptyState = true;
963
+ els.resultWrap.innerHTML = '<div class="empty">计算后将完整展示每一件商品:原价 与 订单价格(优惠后)</div>';
964
+ return;
965
+ }
966
+
967
+ lastResultEmptyState = false;
968
+
969
+ const { payTotal, totalOriginal, totalCount, rows, allocatedTotal, diff } = state.result;
970
+ const frag = document.createDocumentFragment();
971
+
972
+ const head = document.createElement('div');
973
+ head.className = 'result-head';
974
+ head.innerHTML = `
975
+ <div>结果条数:<strong class="mono">${rows.length}</strong>(逐件)</div>
976
+ <div>订单优惠后总金额:<strong class="mono">¥ ${formatMoney(payTotal)}</strong> | ${totalCount}件商品原价总和:<strong class="mono">¥ ${formatMoney(totalOriginal)}</strong></div>
977
+ `;
978
+ frag.appendChild(head);
979
+
980
+ rows.forEach((row) => {
981
+ const item = document.createElement('div');
982
+ item.className = 'result-item';
983
+ item.innerHTML = `
984
+ <div class="result-top">${escapeHtml(row.name)}(第${row.seq}件)</div>
985
+ <div class="price-pair">
986
+ <div class="price-box">
987
+ <span class="label">原价</span>
988
+ <strong class="value">¥ ${formatMoney(row.originalPrice)}</strong>
989
+ </div>
990
+ <div class="price-box">
991
+ <span class="label">订单价格(优惠后)</span>
992
+ <strong class="value actual">¥ ${formatMoney(row.allocatedPrice)}</strong>
993
+ </div>
994
+ </div>
995
+ `;
996
+ frag.appendChild(item);
997
+ });
998
+
999
+ const check = document.createElement('div');
1000
+ check.className = 'check-box';
1001
+ check.innerHTML = `
1002
+ <div>校检:分摊后总和 <strong class="mono">¥ ${formatMoney(allocatedTotal)}</strong> | 订单优惠后总金额 <strong class="mono">¥ ${formatMoney(payTotal)}</strong></div>
1003
+ <div>差额:<strong class="mono">¥ ${formatMoney(Math.abs(diff))}</strong></div>
1004
+ `;
1005
+ frag.appendChild(check);
1006
+
1007
+ els.resultWrap.innerHTML = '';
1008
+ els.resultWrap.appendChild(frag);
1009
+ }
1010
+
1011
+ function updateNumpadActionLabel() {
1012
+ if (!els.numpadActionBtn) return;
1013
+ const isPayInput = !activeNumpadInput || activeNumpadInput === els.payInput;
1014
+ els.numpadActionBtn.textContent = isPayInput ? '计算' : '确认';
1015
+ }
1016
+
1017
+ function handleKeypadPress(key) {
1018
+ const targetInput = activeNumpadInput || els.payInput;
1019
+ const current = targetInput.value;
1020
+
1021
+ if (key === 'hide') {
1022
+ closeNumpad();
1023
+ return;
1024
+ }
1025
+
1026
+ if (key === 'calc') {
1027
+ if (targetInput !== els.payInput) {
1028
+ targetInput.blur();
1029
+ closeNumpad();
1030
+ return;
1031
+ }
1032
+ calculate();
1033
+ return;
1034
+ }
1035
+
1036
+ if (key === 'clear') {
1037
+ targetInput.value = '';
1038
+ targetInput.dispatchEvent(new Event('input', { bubbles: true }));
1039
+ return;
1040
+ }
1041
+
1042
+ if (key === 'backspace') {
1043
+ targetInput.value = current.slice(0, -1);
1044
+ targetInput.dispatchEvent(new Event('input', { bubbles: true }));
1045
+ return;
1046
+ }
1047
+
1048
+ if (key === '.') {
1049
+ if (!current) targetInput.value = '0.';
1050
+ else if (!current.includes('.')) targetInput.value += '.';
1051
+ targetInput.dispatchEvent(new Event('input', { bubbles: true }));
1052
+ return;
1053
+ }
1054
+
1055
+ if (/^\d$/.test(key)) {
1056
+ if (current === '0') targetInput.value = key;
1057
+ else targetInput.value = normalizeInputValue(`${current}${key}`);
1058
+ targetInput.dispatchEvent(new Event('input', { bubbles: true }));
1059
+ }
1060
+ }
1061
+
1062
+ function openNumpad(targetInput = els.payInput) {
1063
+ activeNumpadInput = targetInput;
1064
+ updateNumpadActionLabel();
1065
+ els.numpad.classList.add('active');
1066
+ }
1067
+
1068
+ function closeNumpad() {
1069
+ els.numpad.classList.remove('active');
1070
+ activeNumpadInput = null;
1071
+ updateNumpadActionLabel();
1072
+ }
1073
+
1074
+ function bindEvents() {
1075
+ document.getElementById('openImportBtn').addEventListener('click', openImportModal);
1076
+ document.getElementById('closeImportBtn').addEventListener('click', closeImportModal);
1077
+ document.getElementById('saveImportBtn').addEventListener('click', saveImport);
1078
+ document.getElementById('resetFilterBtn').addEventListener('click', resetProductFilters);
1079
+
1080
+ document.getElementById('calcBtn').addEventListener('click', calculate);
1081
+ document.getElementById('clearOrderBtn').addEventListener('click', clearOrder);
1082
+ document.getElementById('clearResultBtn').addEventListener('click', clearResult);
1083
+
1084
+ const scheduleRenderProducts = (() => {
1085
+ let rafId = 0;
1086
+ return () => {
1087
+ if (rafId) cancelAnimationFrame(rafId);
1088
+ rafId = requestAnimationFrame(() => {
1089
+ rafId = 0;
1090
+ renderProducts();
1091
+ });
1092
+ };
1093
+ })();
1094
+
1095
+ els.sortSelect.addEventListener('change', renderProducts);
1096
+ els.priceFilterSelect.addEventListener('change', renderProducts);
1097
+ els.searchInput.addEventListener('input', scheduleRenderProducts);
1098
+
1099
+ els.productGrid.addEventListener('click', (e) => {
1100
+ const target = e.target;
1101
+ if (!(target instanceof HTMLElement)) return;
1102
+ const card = target.closest('.card');
1103
+ if (!(card instanceof HTMLElement)) return;
1104
+ const index = Number(card.dataset.productIndex);
1105
+ if (Number.isNaN(index)) return;
1106
+ const product = lastDisplayedProducts[index];
1107
+ if (!product) return;
1108
+ addProductToOrder(product);
1109
+ });
1110
+
1111
+ els.orderWrap.addEventListener('click', (e) => {
1112
+ const target = e.target;
1113
+ if (!(target instanceof HTMLElement)) return;
1114
+ const action = target.dataset.action;
1115
+ if (!action) return;
1116
+
1117
+ const index = Number(target.dataset.index);
1118
+ if (Number.isNaN(index)) return;
1119
+
1120
+ if (action === 'minus') updateQty(index, -1);
1121
+ if (action === 'plus') updateQty(index, 1);
1122
+ });
1123
+
1124
+ els.orderWrap.addEventListener('focusin', (e) => {
1125
+ const target = e.target;
1126
+ if (!(target instanceof HTMLInputElement)) return;
1127
+ if (!target.dataset.priceIndex) return;
1128
+ openNumpad(target);
1129
+ });
1130
+
1131
+ els.orderWrap.addEventListener('input', (e) => {
1132
+ const target = e.target;
1133
+ if (!(target instanceof HTMLInputElement)) return;
1134
+ if (!target.dataset.priceIndex) return;
1135
+
1136
+ const v = normalizeInputValue(target.value);
1137
+ target.value = v;
1138
+ const index = Number(target.dataset.priceIndex);
1139
+ if (Number.isNaN(index)) return;
1140
+ updateOrderPrice(index, v);
1141
+ });
1142
+
1143
+ els.orderWrap.addEventListener('keydown', (e) => {
1144
+ const target = e.target;
1145
+ if (!(target instanceof HTMLInputElement)) return;
1146
+ if (!target.dataset.priceIndex) return;
1147
+ if (e.key !== 'Enter') return;
1148
+ e.preventDefault();
1149
+ handleKeypadPress('calc');
1150
+ });
1151
+
1152
+ els.payInput.addEventListener('input', () => {
1153
+ els.payInput.value = normalizeInputValue(els.payInput.value);
1154
+ });
1155
+
1156
+ els.payInput.addEventListener('keydown', (e) => {
1157
+ if (e.key !== 'Enter') return;
1158
+ e.preventDefault();
1159
+ handleKeypadPress('calc');
1160
+ });
1161
+
1162
+ els.payInput.addEventListener('focus', () => {
1163
+ openNumpad(els.payInput);
1164
+ });
1165
+
1166
+ document.addEventListener('click', (e) => {
1167
+ const target = e.target;
1168
+ if (!(target instanceof HTMLElement)) return;
1169
+
1170
+ const clickInsidePad = els.numpad.contains(target);
1171
+ const clickInput = target === els.payInput || !!target.closest('.order-origin-input');
1172
+ if (!clickInsidePad && !clickInput) closeNumpad();
1173
+
1174
+ if (target === els.importModal) closeImportModal();
1175
+ });
1176
+
1177
+ document.addEventListener('keydown', (e) => {
1178
+ if (e.key !== 'Enter') return;
1179
+ if (!els.numpad.classList.contains('active')) return;
1180
+ const focused = document.activeElement;
1181
+ if (!(focused instanceof HTMLElement)) return;
1182
+ if (!els.numpad.contains(focused)) return;
1183
+ e.preventDefault();
1184
+ handleKeypadPress('calc');
1185
+ });
1186
+
1187
+ els.numpad.addEventListener('mousedown', (e) => {
1188
+ const target = e.target;
1189
+ if (!(target instanceof HTMLElement)) return;
1190
+ if (!target.closest('.key')) return;
1191
+ e.preventDefault();
1192
+ });
1193
+
1194
+ els.numpad.addEventListener('click', (e) => {
1195
+ const target = e.target;
1196
+ if (!(target instanceof HTMLElement)) return;
1197
+ const key = target.dataset.key;
1198
+ if (!key) return;
1199
+ handleKeypadPress(key);
1200
+ });
1201
+ }
1202
+
1203
+ function registerServiceWorker() {
1204
+ if (!('serviceWorker' in navigator)) return;
1205
+ window.addEventListener('load', () => {
1206
+ navigator.serviceWorker.register('./sw.js').catch(() => {});
1207
+ });
1208
+ }
1209
+
1210
+ function init() {
1211
+ loadProducts();
1212
+ renderPriceFilterOptions();
1213
+ bindEvents();
1214
+ resetProductFilters();
1215
+ renderOrder();
1216
+ renderResult();
1217
+ registerServiceWorker();
1218
+ }
1219
+
1220
+ init();
1221
+ </script>
1222
+ </body>
1223
  </html>
manifest.webmanifest ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "外卖订单收益分摊器",
3
+ "short_name": "分摊器",
4
+ "description": "用于按商品原价分摊外卖订单实付金额的轻量工具。",
5
+ "start_url": "./",
6
+ "scope": "./",
7
+ "display": "standalone",
8
+ "background_color": "#f6f9ff",
9
+ "theme_color": "#4c7dff",
10
+ "lang": "zh-CN",
11
+ "icons": [
12
+ {
13
+ "src": "./icons/icon.svg",
14
+ "sizes": "any",
15
+ "type": "image/svg+xml",
16
+ "purpose": "any maskable"
17
+ }
18
+ ]
19
+ }
sw.js ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const CACHE_NAME = 'shangpin-pwa-v1';
2
+ const CORE_ASSETS = [
3
+ './',
4
+ './index.html',
5
+ './manifest.webmanifest',
6
+ './icons/icon.svg'
7
+ ];
8
+
9
+ self.addEventListener('install', (event) => {
10
+ event.waitUntil(
11
+ caches.open(CACHE_NAME).then((cache) => cache.addAll(CORE_ASSETS))
12
+ );
13
+ self.skipWaiting();
14
+ });
15
+
16
+ self.addEventListener('activate', (event) => {
17
+ event.waitUntil(
18
+ caches.keys().then((keys) =>
19
+ Promise.all(
20
+ keys
21
+ .filter((key) => key !== CACHE_NAME)
22
+ .map((key) => caches.delete(key))
23
+ )
24
+ )
25
+ );
26
+ self.clients.claim();
27
+ });
28
+
29
+ self.addEventListener('fetch', (event) => {
30
+ const { request } = event;
31
+ if (request.method !== 'GET') return;
32
+
33
+ event.respondWith(
34
+ caches.match(request).then((cached) => {
35
+ if (cached) return cached;
36
+ return fetch(request)
37
+ .then((networkResponse) => {
38
+ if (request.url.startsWith(self.location.origin) && networkResponse && networkResponse.status === 200) {
39
+ const responseClone = networkResponse.clone();
40
+ caches.open(CACHE_NAME).then((cache) => cache.put(request, responseClone));
41
+ }
42
+ return networkResponse;
43
+ })
44
+ .catch(() => caches.match('./index.html'));
45
+ })
46
+ );
47
+ });