minitest2.0 0.0.4 → 0.0.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.
@@ -1,26 +1,617 @@
1
+ <script setup lang="ts">
2
+ import {
3
+ buildClearingDetailPayload,
4
+ queryClearingDetail,
5
+ type ClearingDetailRecord,
6
+ } from '@/api/clearing-detail'
7
+ import { useAppStore } from '@/stores/app'
8
+ import { showAppToast } from '@/utils/toast'
9
+
10
+ defineOptions({ name: 'ClearingDetailPage' })
11
+
12
+ type SummaryItem = {
13
+ label: string
14
+ value: string
15
+ icon: string
16
+ }
17
+
18
+ const appStore = useAppStore()
19
+ const selectedDate = ref(appStore.systemWorkDate)
20
+ const datePickerValue = ref(selectedDate.value.split('-'))
21
+ const showDatePicker = ref(false)
22
+ const minDate = new Date(2020, 0, 1)
23
+ const maxDate = new Date(2030, 11, 31)
24
+
25
+ const records = ref<ClearingDetailRecord[]>([])
26
+ const querying = ref(false)
27
+ const hasQueried = ref(false)
28
+ const queryFailed = ref(false)
29
+
30
+ const summaryItems = computed<SummaryItem[]>(() => [
31
+ { label: '场次数', value: String(records.value.length), icon: 'orders-o' },
32
+ { label: '进款合计', value: formatAmount(sumAmount('secreditLineAmt')), icon: 'gold-coin-o' },
33
+ { label: '出款合计', value: formatAmount(sumAmount('sedebtLineAmt')), icon: 'card' },
34
+ ])
35
+
36
+ onMounted(() => {
37
+ void handleQuery({ showToast: false })
38
+ })
39
+
40
+ async function handleQuery(options: { showToast?: boolean } = {}) {
41
+ if (querying.value) return
42
+
43
+ if (!isValidDate(selectedDate.value)) {
44
+ showAppToast('请选择正确的清算日期')
45
+ return
46
+ }
47
+
48
+ querying.value = true
49
+ const payload = buildClearingDetailPayload({
50
+ queryDate: selectedDate.value,
51
+ })
52
+ console.log('[小额网银清算场次明细查询入参]', payload)
53
+
54
+ try {
55
+ const response = await queryClearingDetail(payload)
56
+ records.value = response.body.result || []
57
+ hasQueried.value = true
58
+ queryFailed.value = false
59
+
60
+ if (options.showToast !== false) {
61
+ showAppToast(`查询到 ${records.value.length} 条明细`)
62
+ }
63
+ } catch (error) {
64
+ console.error('[小额网银清算场次明细查询失败]', error)
65
+ records.value = []
66
+ hasQueried.value = true
67
+ queryFailed.value = true
68
+ showAppToast(getErrorMessage(error, '查询失败,请重试'))
69
+ } finally {
70
+ querying.value = false
71
+ }
72
+ }
73
+
74
+ function handleQueryClick() {
75
+ void handleQuery()
76
+ }
77
+
78
+ function confirmDate() {
79
+ selectedDate.value = datePickerValue.value.join('-')
80
+ showDatePicker.value = false
81
+ }
82
+
83
+ function openDatePicker() {
84
+ datePickerValue.value = selectedDate.value.split('-')
85
+ showDatePicker.value = true
86
+ }
87
+
88
+ function formatDate(value?: string) {
89
+ const text = value?.trim()
90
+
91
+ if (!text) return '-'
92
+
93
+ const compactDate = text.match(/^(\d{4})(\d{2})(\d{2})$/)
94
+ if (compactDate) {
95
+ return `${compactDate[1]}-${compactDate[2]}-${compactDate[3]}`
96
+ }
97
+
98
+ return text
99
+ }
100
+
101
+ function formatSendTime(value?: string) {
102
+ const text = value?.trim()
103
+
104
+ if (!text) return '-'
105
+
106
+ const parsed = new Date(text)
107
+ if (!Number.isNaN(parsed.getTime())) {
108
+ return `${formatDatePart(parsed)} ${formatTimePart(parsed)}`
109
+ }
110
+
111
+ return text.replace('T', ' ')
112
+ }
113
+
114
+ function formatDatePart(value: Date) {
115
+ const year = value.getFullYear()
116
+ const month = String(value.getMonth() + 1).padStart(2, '0')
117
+ const day = String(value.getDate()).padStart(2, '0')
118
+
119
+ return `${year}-${month}-${day}`
120
+ }
121
+
122
+ function formatTimePart(value: Date) {
123
+ const hour = String(value.getHours()).padStart(2, '0')
124
+ const minute = String(value.getMinutes()).padStart(2, '0')
125
+ const second = String(value.getSeconds()).padStart(2, '0')
126
+
127
+ return `${hour}:${minute}:${second}`
128
+ }
129
+
130
+ function formatAmount(value?: string | number) {
131
+ const amount = typeof value === 'number' ? value : parseAmount(value)
132
+
133
+ if (!Number.isFinite(amount)) return '-'
134
+
135
+ return new Intl.NumberFormat('zh-CN', {
136
+ minimumFractionDigits: 2,
137
+ maximumFractionDigits: 2,
138
+ }).format(amount)
139
+ }
140
+
141
+ function formatBalance(record: ClearingDetailRecord) {
142
+ const direction = record.txnetgBalanceDirection?.trim()
143
+ const amountText = formatAmount(record.txnetgBalance)
144
+
145
+ if (amountText === '-') return amountText
146
+ if (!direction || amountText.startsWith('-') || amountText.startsWith('+')) return amountText
147
+
148
+ return `${direction}${amountText}`
149
+ }
150
+
151
+ function fieldText(value?: string) {
152
+ const text = value?.trim()
153
+
154
+ return text || '-'
155
+ }
156
+
157
+ function sumAmount(key: 'secreditLineAmt' | 'sedebtLineAmt') {
158
+ return records.value.reduce((sum, record) => {
159
+ const amount = parseAmount(record[key])
160
+
161
+ return Number.isFinite(amount) ? sum + amount : sum
162
+ }, 0)
163
+ }
164
+
165
+ function parseAmount(value?: string) {
166
+ if (!value?.trim()) return Number.NaN
167
+
168
+ return Number(value.replaceAll(',', ''))
169
+ }
170
+
171
+ function isValidDate(value: string) {
172
+ return /^\d{4}-\d{2}-\d{2}$/.test(value)
173
+ }
174
+
175
+ function getErrorMessage(error: unknown, fallback: string) {
176
+ return error instanceof Error && error.message ? error.message : fallback
177
+ }
178
+ </script>
179
+
1
180
  <template>
2
- <main class="page">
3
- <section class="page-body">
4
- <p class="placeholder">页面建设中…</p>
181
+ <main class="clearing-page">
182
+ <section class="query-panel" aria-label="清算场次查询条件">
183
+ <button class="date-row" type="button" @click="openDatePicker">
184
+ <span class="date-icon" aria-hidden="true"><van-icon name="calendar-o" /></span>
185
+ <span class="date-label">清算日期</span>
186
+ <span class="date-value">{{ selectedDate }}</span>
187
+ <van-icon class="date-arrow" name="arrow" />
188
+ </button>
5
189
  </section>
190
+
191
+ <section class="summary-grid" aria-label="清算汇总">
192
+ <div v-for="item in summaryItems" :key="item.label" class="summary-card">
193
+ <span class="summary-icon" aria-hidden="true"><van-icon :name="item.icon" /></span>
194
+ <span class="summary-label">{{ item.label }}</span>
195
+ <strong>{{ item.value }}</strong>
196
+ </div>
197
+ </section>
198
+
199
+ <section class="result-section" aria-label="清算场次明细">
200
+ <div class="section-title">
201
+ <span class="section-title-mark" aria-hidden="true"></span>
202
+ <h2>场次明细</h2>
203
+ </div>
204
+
205
+ <van-loading v-if="querying" class="loading-state" color="#f22f3a" size="24px">
206
+ 加载中...
207
+ </van-loading>
208
+
209
+ <div v-else-if="records.length > 0" class="record-list">
210
+ <article
211
+ v-for="(record, index) in records"
212
+ :key="`${record.txnetgType || 'record'}-${index}`"
213
+ class="record-card"
214
+ >
215
+ <header class="record-head">
216
+ <span class="round-badge">场次 {{ fieldText(record.txnetgRnd) }}</span>
217
+ <span
218
+ class="direction-badge"
219
+ :class="{ negative: record.txnetgBalanceDirection === '-' }"
220
+ >
221
+ {{ fieldText(record.txnetgBalanceDirection) }}
222
+ </span>
223
+ </header>
224
+
225
+ <dl class="amount-grid">
226
+ <div>
227
+ <dt>进款</dt>
228
+ <dd>{{ formatAmount(record.secreditLineAmt) }}</dd>
229
+ </div>
230
+ <div>
231
+ <dt>出款</dt>
232
+ <dd>{{ formatAmount(record.sedebtLineAmt) }}</dd>
233
+ </div>
234
+ <div class="balance-cell">
235
+ <dt>轧差</dt>
236
+ <dd>{{ formatBalance(record) }}</dd>
237
+ </div>
238
+ </dl>
239
+
240
+ <dl class="meta-list">
241
+ <div>
242
+ <dt>轧差日期</dt>
243
+ <dd>{{ formatDate(record.txnetgDt) }}</dd>
244
+ </div>
245
+ <div>
246
+ <dt>时间</dt>
247
+ <dd>{{ formatSendTime(record.msgSendDt) }}</dd>
248
+ </div>
249
+ <div>
250
+ <dt>清算报文编号</dt>
251
+ <dd>{{ fieldText(record.txnetgType) }}</dd>
252
+ </div>
253
+ </dl>
254
+ </article>
255
+ </div>
256
+
257
+ <van-empty
258
+ v-else-if="hasQueried && !queryFailed"
259
+ image-size="72"
260
+ description="暂无清算场次明细"
261
+ />
262
+
263
+ <van-empty v-else-if="queryFailed" image-size="72" description="清算场次明细加载失败" />
264
+ </section>
265
+
266
+ <nav class="action-bar" aria-label="清算场次明细操作">
267
+ <button class="query-button" type="button" :disabled="querying" @click="handleQueryClick">
268
+ <van-icon name="search" />
269
+ <span>{{ querying ? '查询中' : '查询' }}</span>
270
+ </button>
271
+ </nav>
272
+
273
+ <van-popup v-model:show="showDatePicker" position="bottom" round safe-area-inset-bottom>
274
+ <van-date-picker
275
+ v-model="datePickerValue"
276
+ title="选择清算日期"
277
+ :min-date="minDate"
278
+ :max-date="maxDate"
279
+ @cancel="showDatePicker = false"
280
+ @confirm="confirmDate"
281
+ />
282
+ </van-popup>
6
283
  </main>
7
284
  </template>
8
285
 
9
286
  <style scoped>
10
- .page {
287
+ .clearing-page {
288
+ width: 100%;
11
289
  max-width: 750px;
12
290
  min-height: var(--app-page-min-height, 100vh);
13
291
  margin: 0 auto;
14
- padding: 0 11px;
15
- background: #f7f8fa;
292
+ padding: 13px 13px calc(env(safe-area-inset-bottom, 0px) + 86px);
293
+ color: #171b20;
294
+ background: linear-gradient(180deg, #ffffff 0%, #f8f9fb 52%, #ffffff 100%);
295
+ box-sizing: border-box;
16
296
  }
17
- .page-body {
18
- padding: 24px 0;
297
+
298
+ button {
299
+ border: 0;
300
+ padding: 0;
301
+ font: inherit;
302
+ background: transparent;
303
+ appearance: none;
19
304
  }
20
- .placeholder {
21
- text-align: center;
22
- color: #969799;
305
+
306
+ .query-panel {
307
+ overflow: hidden;
308
+ border: 1px solid #edf0f4;
309
+ border-radius: 8px;
310
+ background: #ffffff;
311
+ box-shadow: 0 8px 22px rgba(34, 40, 56, 0.05);
312
+ }
313
+
314
+ .date-row {
315
+ display: grid;
316
+ grid-template-columns: 34px auto minmax(0, 1fr) 18px;
317
+ align-items: center;
318
+ width: 100%;
319
+ min-height: 54px;
320
+ padding: 0 14px;
321
+ column-gap: 10px;
322
+ color: #151922;
323
+ text-align: left;
324
+ }
325
+
326
+ .date-icon {
327
+ display: inline-flex;
328
+ align-items: center;
329
+ justify-content: center;
330
+ width: 34px;
331
+ height: 34px;
332
+ border-radius: 8px;
333
+ color: #ef252d;
334
+ font-size: 19px;
335
+ background: #fff0f2;
336
+ }
337
+
338
+ .date-label {
339
+ color: #202532;
23
340
  font-size: 14px;
24
- padding-top: 120px;
341
+ font-weight: 620;
342
+ }
343
+
344
+ .date-value {
345
+ justify-self: end;
346
+ color: #323846;
347
+ font-size: 15px;
348
+ font-weight: 650;
349
+ }
350
+
351
+ .date-arrow {
352
+ justify-self: end;
353
+ color: #b3b7c2;
354
+ font-size: 15px;
355
+ }
356
+
357
+ .summary-grid {
358
+ display: grid;
359
+ grid-template-columns: repeat(3, minmax(0, 1fr));
360
+ gap: 8px;
361
+ margin-top: 10px;
362
+ }
363
+
364
+ .summary-card {
365
+ display: grid;
366
+ grid-template-rows: 24px auto auto;
367
+ min-width: 0;
368
+ min-height: 92px;
369
+ padding: 11px 9px 10px;
370
+ border: 1px solid #ffe0e3;
371
+ border-radius: 8px;
372
+ background: #ffffff;
373
+ box-shadow: 0 6px 16px rgba(255, 50, 62, 0.04);
374
+ box-sizing: border-box;
375
+ }
376
+
377
+ .summary-icon {
378
+ display: inline-flex;
379
+ align-items: center;
380
+ justify-content: center;
381
+ width: 24px;
382
+ height: 24px;
383
+ border-radius: 7px;
384
+ color: #f02b35;
385
+ font-size: 16px;
386
+ background: #fff2f3;
387
+ }
388
+
389
+ .summary-label {
390
+ margin-top: 7px;
391
+ color: #7b8290;
392
+ font-size: 12px;
393
+ line-height: 1.2;
394
+ }
395
+
396
+ .summary-card strong {
397
+ min-width: 0;
398
+ margin-top: 5px;
399
+ color: #171b20;
400
+ font-size: 14px;
401
+ font-weight: 760;
402
+ line-height: 1.2;
403
+ overflow-wrap: anywhere;
404
+ }
405
+
406
+ .result-section {
407
+ margin-top: 17px;
408
+ }
409
+
410
+ .section-title {
411
+ display: flex;
412
+ align-items: center;
413
+ gap: 8px;
414
+ margin-bottom: 10px;
415
+ }
416
+
417
+ .section-title-mark {
418
+ width: 5px;
419
+ height: 16px;
420
+ border-radius: 8px;
421
+ background: #f22f3a;
422
+ }
423
+
424
+ .section-title h2 {
425
+ margin: 0;
426
+ color: #171b20;
427
+ font-size: 16px;
428
+ font-weight: 720;
429
+ line-height: 1;
430
+ }
431
+
432
+ .loading-state {
433
+ display: flex;
434
+ justify-content: center;
435
+ padding: 52px 0;
436
+ }
437
+
438
+ .record-list {
439
+ display: flex;
440
+ flex-direction: column;
441
+ gap: 10px;
442
+ }
443
+
444
+ .record-card {
445
+ padding: 13px;
446
+ border: 1px solid #edf0f4;
447
+ border-radius: 8px;
448
+ background: #ffffff;
449
+ box-shadow: 0 8px 20px rgba(30, 36, 52, 0.055);
450
+ }
451
+
452
+ .record-head {
453
+ display: flex;
454
+ align-items: center;
455
+ justify-content: space-between;
456
+ gap: 10px;
457
+ }
458
+
459
+ .round-badge,
460
+ .direction-badge {
461
+ display: inline-flex;
462
+ align-items: center;
463
+ justify-content: center;
464
+ min-width: 0;
465
+ height: 26px;
466
+ padding: 0 10px;
467
+ border-radius: 13px;
468
+ font-size: 12px;
469
+ font-weight: 700;
470
+ line-height: 1;
471
+ }
472
+
473
+ .round-badge {
474
+ color: #ef252d;
475
+ background: #fff1f2;
476
+ }
477
+
478
+ .direction-badge {
479
+ min-width: 26px;
480
+ padding: 0 8px;
481
+ color: #0c8c61;
482
+ background: #eaf8f2;
483
+ }
484
+
485
+ .direction-badge.negative {
486
+ color: #d3212a;
487
+ background: #fff0f2;
488
+ }
489
+
490
+ .amount-grid {
491
+ display: grid;
492
+ grid-template-columns: repeat(3, minmax(0, 1fr));
493
+ gap: 8px;
494
+ margin: 12px 0 0;
495
+ padding: 0;
496
+ }
497
+
498
+ .amount-grid div {
499
+ min-width: 0;
500
+ padding: 10px 8px;
501
+ border-radius: 8px;
502
+ background: #f8f9fb;
503
+ }
504
+
505
+ .amount-grid dt,
506
+ .meta-list dt {
507
+ margin: 0;
508
+ color: #7b8290;
509
+ font-size: 12px;
510
+ line-height: 1.2;
511
+ }
512
+
513
+ .amount-grid dd {
514
+ min-width: 0;
515
+ margin: 6px 0 0;
516
+ color: #171b20;
517
+ font-size: 15px;
518
+ font-weight: 720;
519
+ line-height: 1.2;
520
+ overflow-wrap: anywhere;
521
+ }
522
+
523
+ .amount-grid .balance-cell {
524
+ background: #fff5f6;
525
+ }
526
+
527
+ .amount-grid .balance-cell dd {
528
+ color: #d3212a;
529
+ }
530
+
531
+ .meta-list {
532
+ margin: 12px 0 0;
533
+ padding: 0;
534
+ border-top: 1px solid #edf0f2;
535
+ }
536
+
537
+ .meta-list div {
538
+ display: grid;
539
+ grid-template-columns: 92px minmax(0, 1fr);
540
+ align-items: center;
541
+ min-height: 34px;
542
+ column-gap: 10px;
543
+ }
544
+
545
+ .meta-list div + div {
546
+ border-top: 1px solid #f1f2f4;
547
+ }
548
+
549
+ .meta-list dd {
550
+ min-width: 0;
551
+ margin: 0;
552
+ color: #242a36;
553
+ font-size: 13px;
554
+ font-weight: 520;
555
+ line-height: 1.25;
556
+ text-align: right;
557
+ overflow-wrap: anywhere;
558
+ }
559
+
560
+ .action-bar {
561
+ position: fixed;
562
+ right: 0;
563
+ bottom: calc(env(safe-area-inset-bottom, 0px) + 15px);
564
+ left: 50%;
565
+ z-index: 10;
566
+ width: 100%;
567
+ max-width: 750px;
568
+ padding: 0 13px;
569
+ transform: translateX(-50%);
570
+ box-sizing: border-box;
571
+ pointer-events: none;
572
+ }
573
+
574
+ .query-button {
575
+ display: inline-flex;
576
+ align-items: center;
577
+ justify-content: center;
578
+ gap: 7px;
579
+ width: 100%;
580
+ height: 44px;
581
+ border-radius: 22px;
582
+ color: #ffffff;
583
+ font-size: 18px;
584
+ font-weight: 650;
585
+ line-height: 1;
586
+ background: linear-gradient(135deg, #ff3338 0%, #ff232b 100%);
587
+ box-shadow:
588
+ 0 12px 22px rgba(255, 44, 50, 0.24),
589
+ inset 0 1px 0 rgba(255, 255, 255, 0.22);
590
+ pointer-events: auto;
591
+ }
592
+
593
+ .query-button .van-icon {
594
+ font-size: 18px;
595
+ }
596
+
597
+ .query-button:disabled {
598
+ cursor: not-allowed;
599
+ opacity: 0.68;
600
+ }
601
+
602
+ @media (max-width: 350px) {
603
+ .summary-card {
604
+ padding-right: 7px;
605
+ padding-left: 7px;
606
+ }
607
+
608
+ .summary-card strong,
609
+ .amount-grid dd {
610
+ font-size: 13px;
611
+ }
612
+
613
+ .meta-list div {
614
+ grid-template-columns: 82px minmax(0, 1fr);
615
+ }
25
616
  }
26
617
  </style>