minitest2.0 0.0.6 → 0.0.8

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.
Files changed (32) hide show
  1. package/package.json +1 -1
  2. package/public/images/dashboard/01-rmb-great-wall-bg.jpg +0 -0
  3. package/public/images/dashboard/02-city-clearing-skyline-bg.jpg +0 -0
  4. package/public/images/dashboard/03-cross-border-rmb-world-map-bg.jpg +0 -0
  5. package/public/images/dashboard/04-digital-currency-fintech-bg.jpg +0 -0
  6. package/public/images/dashboard/05-usd-liberty-bg.jpg +0 -0
  7. package/public/images/dashboard/06-eur-europe-architecture-bg.jpg +0 -0
  8. package/public/images/dashboard/07-hkd-hong-kong-skyline-bg.jpg +0 -0
  9. package/public/images/dashboard/08-jpy-mount-fuji-bg.jpg +0 -0
  10. package/src/api/dashboard.ts +44 -0
  11. package/src/api/foreign-position.ts +71 -0
  12. package/src/router/index.ts +29 -17
  13. package/src/utils/foreign-forecast.ts +37 -5
  14. package/src/views/{article-detail → announcement/detail}/index.vue +35 -1
  15. package/src/views/dashboard/index.vue +535 -2
  16. package/src/views/foreign/position/create/index.vue +363 -0
  17. package/src/views/foreign/position/detail/index.vue +390 -0
  18. package/src/views/foreign/position/index.vue +1223 -0
  19. package/src/views/foreign-position/index.vue +0 -26
  20. /package/src/views/{article-edit → announcement/edit}/index.vue +0 -0
  21. /package/src/views/{articles → announcement}/index.vue +0 -0
  22. /package/src/views/{settings → mine/settings}/index.vue +0 -0
  23. /package/src/views/{clearing-detail → rmb/clearing-detail}/index.vue +0 -0
  24. /package/src/views/{net-debit → rmb/net-debit}/index.vue +0 -0
  25. /package/src/views/{pbc-position → rmb/pbc-position}/index.vue +0 -0
  26. /package/src/views/{rmb-position-create → rmb/position/create}/index.vue +0 -0
  27. /package/src/views/{rmb-position-detail → rmb/position/detail}/index.vue +0 -0
  28. /package/src/views/{rmb-position → rmb/position}/index.vue +0 -0
  29. /package/src/views/{position-estimate → rmb/position-estimate}/index.vue +0 -0
  30. /package/src/views/{opening-reserve-estimate → rmb/position-estimate/opening-reserve}/index.vue +0 -0
  31. /package/src/views/{position-estimate-report → rmb/position-estimate/report}/index.vue +0 -0
  32. /package/src/views/{warning → rmb/warning}/index.vue +0 -0
@@ -1,9 +1,306 @@
1
1
  <script setup lang="ts">
2
+ import { queryForeignDashboard, queryRmbDashboard } from '@/api/dashboard'
3
+
2
4
  defineOptions({ name: 'DashboardPage' })
5
+
6
+ type DashboardMetric = {
7
+ label: string
8
+ symbol: string
9
+ amount: number
10
+ }
11
+
12
+ type DashboardCard = {
13
+ name: string
14
+ code: string
15
+ symbol?: string
16
+ amount?: number
17
+ metrics?: DashboardMetric[]
18
+ image: string
19
+ accent: string
20
+ position?: string
21
+ }
22
+
23
+ type UnitLabel = '亿' | '千万' | '百万' | '十万' | '万' | '原币'
24
+
25
+ const dashboardImage = (file: string) => `${import.meta.env.BASE_URL}images/dashboard/${file}`
26
+
27
+ const unitOptions: Array<{ label: UnitLabel; factor: number }> = [
28
+ { label: '亿', factor: 100000000 },
29
+ { label: '千万', factor: 10000000 },
30
+ { label: '百万', factor: 1000000 },
31
+ { label: '十万', factor: 100000 },
32
+ { label: '万', factor: 10000 },
33
+ { label: '原币', factor: 1 },
34
+ ]
35
+
36
+ const selectedUnit = ref<UnitLabel>('原币')
37
+
38
+ const selectedUnitFactor = computed(
39
+ () => unitOptions.find((unit) => unit.label === selectedUnit.value)?.factor || 1,
40
+ )
41
+
42
+ const dashboardAmounts = reactive({
43
+ rmb: 0,
44
+ cityClearing: 0,
45
+ crossBorderRmb: 0,
46
+ digitalCurrency: 0,
47
+ usdDomestic: 0,
48
+ usdOverseas: 0,
49
+ eurDomestic: 0,
50
+ eurOverseas: 0,
51
+ hkdDomestic: 0,
52
+ hkdOverseas: 0,
53
+ jpyDomestic: 0,
54
+ jpyOverseas: 0,
55
+ })
56
+
57
+ const isDashboardLoading = ref(false)
58
+ const dashboardLoadError = ref('')
59
+
60
+ const cards = computed<DashboardCard[]>(() => [
61
+ {
62
+ name: '人民币',
63
+ code: 'CNY',
64
+ symbol: '¥',
65
+ amount: dashboardAmounts.rmb,
66
+ image: dashboardImage('01-rmb-great-wall-bg.jpg'),
67
+ accent: '#df2f38',
68
+ position: 'center center',
69
+ },
70
+ {
71
+ name: '城银清',
72
+ code: 'CNY',
73
+ symbol: '¥',
74
+ amount: dashboardAmounts.cityClearing,
75
+ image: dashboardImage('02-city-clearing-skyline-bg.jpg'),
76
+ accent: '#0f4ca7',
77
+ position: 'center center',
78
+ },
79
+ {
80
+ name: '跨境人民币',
81
+ code: 'CNY',
82
+ symbol: '¥',
83
+ amount: dashboardAmounts.crossBorderRmb,
84
+ image: dashboardImage('03-cross-border-rmb-world-map-bg.jpg'),
85
+ accent: '#078b95',
86
+ position: 'center center',
87
+ },
88
+ {
89
+ name: '数字货币',
90
+ code: 'CNY',
91
+ symbol: '¥',
92
+ amount: dashboardAmounts.digitalCurrency,
93
+ image: dashboardImage('04-digital-currency-fintech-bg.jpg'),
94
+ accent: '#4423b7',
95
+ position: 'center center',
96
+ },
97
+ {
98
+ name: '美元',
99
+ code: 'USD',
100
+ metrics: [
101
+ { label: '境内', symbol: '$', amount: dashboardAmounts.usdDomestic },
102
+ { label: '境外', symbol: '$', amount: dashboardAmounts.usdOverseas },
103
+ ],
104
+ image: dashboardImage('05-usd-liberty-bg.jpg'),
105
+ accent: '#0d8b3f',
106
+ position: 'center center',
107
+ },
108
+ {
109
+ name: '欧元',
110
+ code: 'EUR',
111
+ metrics: [
112
+ { label: '境内', symbol: '€', amount: dashboardAmounts.eurDomestic },
113
+ { label: '境外', symbol: '€', amount: dashboardAmounts.eurOverseas },
114
+ ],
115
+ image: dashboardImage('06-eur-europe-architecture-bg.jpg'),
116
+ accent: '#1267d5',
117
+ position: 'center center',
118
+ },
119
+ {
120
+ name: '港元',
121
+ code: 'HKD',
122
+ metrics: [
123
+ { label: '境内', symbol: 'HK$', amount: dashboardAmounts.hkdDomestic },
124
+ { label: '境外', symbol: 'HK$', amount: dashboardAmounts.hkdOverseas },
125
+ ],
126
+ image: dashboardImage('07-hkd-hong-kong-skyline-bg.jpg'),
127
+ accent: '#e06b00',
128
+ position: 'center center',
129
+ },
130
+ {
131
+ name: '日元',
132
+ code: 'JPY',
133
+ metrics: [
134
+ { label: '境内', symbol: '¥', amount: dashboardAmounts.jpyDomestic },
135
+ { label: '境外', symbol: '¥', amount: dashboardAmounts.jpyOverseas },
136
+ ],
137
+ image: dashboardImage('08-jpy-mount-fuji-bg.jpg'),
138
+ accent: '#6125c8',
139
+ position: 'center center',
140
+ },
141
+ ])
142
+
143
+ onMounted(() => {
144
+ void loadDashboard()
145
+ })
146
+
147
+ function getCardStyle(card: DashboardCard) {
148
+ return {
149
+ '--dashboard-accent': card.accent,
150
+ backgroundImage: `url("${card.image}")`,
151
+ backgroundPosition: card.position || 'center center',
152
+ }
153
+ }
154
+
155
+ function getCardAriaLabel(card: DashboardCard) {
156
+ if (card.metrics) {
157
+ const metrics = card.metrics
158
+ .map((metric) => `${metric.label}${formatMoney(metric.amount, metric.symbol)}`)
159
+ .join(',')
160
+
161
+ return `${card.name},${card.code},单位${selectedUnit.value},${metrics}`
162
+ }
163
+
164
+ return `${card.name},${card.code},单位${selectedUnit.value},${formatMoney(card.amount || 0, card.symbol || '')}`
165
+ }
166
+
167
+ function formatMoney(amount: number, symbol: string) {
168
+ const scaledAmount = amount / selectedUnitFactor.value
169
+ const formattedAmount = scaledAmount.toLocaleString('en-US', {
170
+ minimumFractionDigits: 2,
171
+ maximumFractionDigits: 2,
172
+ })
173
+ const unitSuffix = selectedUnit.value === '原币' ? '' : ` ${selectedUnit.value}`
174
+
175
+ return `${symbol} ${formattedAmount}${unitSuffix}`
176
+ }
177
+
178
+ async function loadDashboard() {
179
+ if (isDashboardLoading.value) return
180
+
181
+ isDashboardLoading.value = true
182
+ dashboardLoadError.value = ''
183
+
184
+ try {
185
+ const [rmbResult, foreignResult] = await Promise.allSettled([
186
+ queryRmbDashboard(),
187
+ queryForeignDashboard(),
188
+ ])
189
+ const errorMessages: string[] = []
190
+
191
+ if (rmbResult.status === 'fulfilled') {
192
+ applyRmbDashboardBody(rmbResult.value.body)
193
+ } else {
194
+ errorMessages.push(getErrorMessage(rmbResult.reason))
195
+ }
196
+
197
+ if (foreignResult.status === 'fulfilled') {
198
+ applyForeignDashboardBody(foreignResult.value.body)
199
+ } else {
200
+ errorMessages.push(getErrorMessage(foreignResult.reason))
201
+ }
202
+
203
+ if (errorMessages.length) {
204
+ dashboardLoadError.value = errorMessages.join(';')
205
+ }
206
+ } catch (error) {
207
+ console.error('[看板] 数据加载失败', error)
208
+ dashboardLoadError.value = getErrorMessage(error)
209
+ } finally {
210
+ isDashboardLoading.value = false
211
+ }
212
+ }
213
+
214
+ function applyRmbDashboardBody(body: Awaited<ReturnType<typeof queryRmbDashboard>>['body']) {
215
+ const result = body || {}
216
+
217
+ dashboardAmounts.rmb = toAmount(result.settleAccAmt)
218
+ dashboardAmounts.cityClearing = toAmount(result.availCredit)
219
+ dashboardAmounts.crossBorderRmb = toAmount(result.cipsAvailBal)
220
+ dashboardAmounts.digitalCurrency = toAmount(result.dcpTotalAmt)
221
+ }
222
+
223
+ function applyForeignDashboardBody(
224
+ body: Awaited<ReturnType<typeof queryForeignDashboard>>['body'],
225
+ ) {
226
+ const result = body || {}
227
+
228
+ dashboardAmounts.usdDomestic = toAmount(result.usdDomesticAmt)
229
+ dashboardAmounts.usdOverseas = toAmount(result.usdOverseasAmt)
230
+ dashboardAmounts.eurDomestic = toAmount(result.eurDomesticAmt)
231
+ dashboardAmounts.eurOverseas = toAmount(result.eurOverseasAmt)
232
+ dashboardAmounts.hkdDomestic = toAmount(result.hkdDomesticAmt)
233
+ dashboardAmounts.hkdOverseas = toAmount(result.hkdOverseasAmt)
234
+ dashboardAmounts.jpyDomestic = toAmount(result.jpyDomesticAmt)
235
+ dashboardAmounts.jpyOverseas = toAmount(result.jpyOverseasAmt)
236
+ }
237
+
238
+ function toAmount(value: unknown) {
239
+ const amount = Number(String(value ?? '').replaceAll(',', '').trim())
240
+
241
+ return Number.isFinite(amount) ? amount : 0
242
+ }
243
+
244
+ function getErrorMessage(error: unknown) {
245
+ if (error instanceof Error && error.message) return error.message
246
+
247
+ return '看板数据加载失败'
248
+ }
3
249
  </script>
4
250
 
5
251
  <template>
6
- <main class="dashboard-page"></main>
252
+ <main class="dashboard-page" :aria-busy="isDashboardLoading">
253
+ <div class="dashboard-toolbar" aria-label="看板设置">
254
+ <button
255
+ v-if="dashboardLoadError"
256
+ class="dashboard-retry"
257
+ type="button"
258
+ :disabled="isDashboardLoading"
259
+ @click="loadDashboard"
260
+ >
261
+ 刷新
262
+ </button>
263
+
264
+ <label class="unit-switch">
265
+ <span class="unit-switch__label">单位</span>
266
+ <select v-model="selectedUnit" class="unit-switch__select" aria-label="切换金额单位">
267
+ <option v-for="unit in unitOptions" :key="unit.label" :value="unit.label">
268
+ {{ unit.label }}
269
+ </option>
270
+ </select>
271
+ <van-icon class="unit-switch__icon" name="arrow-down" aria-hidden="true" />
272
+ </label>
273
+ </div>
274
+
275
+ <section class="dashboard-list" aria-label="资金看板">
276
+ <article
277
+ v-for="card in cards"
278
+ :key="card.name"
279
+ class="dashboard-card"
280
+ :class="{ 'dashboard-card--split': card.metrics }"
281
+ :style="getCardStyle(card)"
282
+ :aria-label="getCardAriaLabel(card)"
283
+ >
284
+ <header class="dashboard-card__header">
285
+ <h2 class="dashboard-card__name">{{ card.name }}</h2>
286
+ <span class="dashboard-card__code">{{ card.code }}</span>
287
+ </header>
288
+
289
+ <p v-if="!card.metrics" class="dashboard-card__amount">
290
+ {{ formatMoney(card.amount || 0, card.symbol || '') }}
291
+ </p>
292
+
293
+ <div v-else class="dashboard-card__metrics">
294
+ <div v-for="metric in card.metrics" :key="metric.label" class="dashboard-card__metric">
295
+ <span class="dashboard-card__metric-label">{{ metric.label }}</span>
296
+ <strong class="dashboard-card__metric-value">
297
+ {{ formatMoney(metric.amount, metric.symbol) }}
298
+ </strong>
299
+ </div>
300
+ </div>
301
+ </article>
302
+ </section>
303
+ </main>
7
304
  </template>
8
305
 
9
306
  <style scoped>
@@ -12,7 +309,243 @@ defineOptions({ name: 'DashboardPage' })
12
309
  max-width: 750px;
13
310
  min-height: var(--app-page-min-height, 100vh);
14
311
  margin: 0 auto;
15
- background: #f7f8fa;
312
+ padding: 8px 12px 10px;
313
+ color: #ffffff;
314
+ background: #ffffff;
16
315
  box-sizing: border-box;
17
316
  }
317
+
318
+ .dashboard-toolbar {
319
+ display: flex;
320
+ align-items: center;
321
+ justify-content: flex-end;
322
+ margin-bottom: 6px;
323
+ }
324
+
325
+ .dashboard-retry {
326
+ height: 24px;
327
+ margin-right: auto;
328
+ border: 1px solid #ffd4d8;
329
+ border-radius: 5px;
330
+ padding: 0 10px;
331
+ color: #dc2735;
332
+ font: inherit;
333
+ font-size: 12px;
334
+ font-weight: 700;
335
+ line-height: 22px;
336
+ background: #fff5f6;
337
+ }
338
+
339
+ .dashboard-retry:disabled {
340
+ opacity: 0.62;
341
+ }
342
+
343
+ .unit-switch {
344
+ position: relative;
345
+ display: inline-flex;
346
+ align-items: center;
347
+ height: 24px;
348
+ overflow: hidden;
349
+ border: 1px solid #e8ebf0;
350
+ border-radius: 5px;
351
+ background: #ffffff;
352
+ box-shadow: 0 2px 7px rgba(31, 45, 62, 0.07);
353
+ }
354
+
355
+ .unit-switch__label {
356
+ padding: 0 6px 0 8px;
357
+ color: #5f6672;
358
+ font-size: 12px;
359
+ font-weight: 700;
360
+ line-height: 1;
361
+ }
362
+
363
+ .unit-switch__select {
364
+ width: 56px;
365
+ height: 100%;
366
+ border: 0;
367
+ border-left: 1px solid #eef0f4;
368
+ padding: 0 21px 0 8px;
369
+ color: #171b20;
370
+ font: inherit;
371
+ font-size: 12px;
372
+ font-weight: 800;
373
+ line-height: 24px;
374
+ background: transparent;
375
+ outline: none;
376
+ appearance: none;
377
+ }
378
+
379
+ .unit-switch__icon {
380
+ position: absolute;
381
+ right: 7px;
382
+ top: 50%;
383
+ color: #7d8591;
384
+ font-size: 11px;
385
+ pointer-events: none;
386
+ transform: translateY(-50%);
387
+ }
388
+
389
+ .dashboard-list {
390
+ display: grid;
391
+ gap: 6px;
392
+ }
393
+
394
+ .dashboard-card {
395
+ position: relative;
396
+ isolation: isolate;
397
+ overflow: hidden;
398
+ aspect-ratio: 351 / 80;
399
+ border-radius: 5px;
400
+ background-repeat: no-repeat;
401
+ background-size: cover;
402
+ box-shadow:
403
+ 0 2px 7px rgba(31, 45, 62, 0.13),
404
+ inset 0 0 0 1px rgba(255, 255, 255, 0.18);
405
+ }
406
+
407
+ .dashboard-card::before {
408
+ content: '';
409
+ position: absolute;
410
+ inset: 0;
411
+ z-index: -1;
412
+ background:
413
+ radial-gradient(circle at 51% 56%, rgba(255, 255, 255, 0.13), transparent 28%),
414
+ linear-gradient(90deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0));
415
+ pointer-events: none;
416
+ }
417
+
418
+ .dashboard-card__header {
419
+ position: absolute;
420
+ inset: 0;
421
+ pointer-events: none;
422
+ }
423
+
424
+ .dashboard-card__name,
425
+ .dashboard-card__code,
426
+ .dashboard-card__amount,
427
+ .dashboard-card__metric-label,
428
+ .dashboard-card__metric-value {
429
+ margin: 0;
430
+ letter-spacing: 0;
431
+ }
432
+
433
+ .dashboard-card__name {
434
+ position: absolute;
435
+ top: 12px;
436
+ left: 12px;
437
+ color: var(--dashboard-accent);
438
+ font-size: 20px;
439
+ font-weight: 800;
440
+ line-height: 1;
441
+ }
442
+
443
+ .dashboard-card__code {
444
+ position: absolute;
445
+ top: 13px;
446
+ right: 13px;
447
+ color: var(--dashboard-accent);
448
+ font-size: 13px;
449
+ font-weight: 700;
450
+ line-height: 1;
451
+ }
452
+
453
+ .dashboard-card__amount {
454
+ position: absolute;
455
+ left: 0;
456
+ right: 0;
457
+ top: 40px;
458
+ overflow: hidden;
459
+ padding: 0 18px;
460
+ color: #ffffff;
461
+ font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Microsoft YaHei', sans-serif;
462
+ font-size: 21px;
463
+ font-weight: 500;
464
+ line-height: 1;
465
+ text-align: center;
466
+ text-overflow: ellipsis;
467
+ white-space: nowrap;
468
+ -webkit-font-smoothing: antialiased;
469
+ text-shadow:
470
+ 0 2px 4px rgba(57, 73, 91, 0.18),
471
+ 0 0 1px rgba(255, 255, 255, 0.28);
472
+ }
473
+
474
+ .dashboard-card__metrics {
475
+ position: absolute;
476
+ left: 52px;
477
+ right: 16px;
478
+ bottom: 13px;
479
+ display: grid;
480
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
481
+ align-items: end;
482
+ column-gap: 29px;
483
+ }
484
+
485
+ .dashboard-card__metrics::before {
486
+ content: '';
487
+ position: absolute;
488
+ left: 50%;
489
+ bottom: 4px;
490
+ width: 1px;
491
+ height: 31px;
492
+ background: rgba(255, 255, 255, 0.78);
493
+ transform: translateX(-15px);
494
+ }
495
+
496
+ .dashboard-card__metric {
497
+ position: relative;
498
+ z-index: 1;
499
+ display: grid;
500
+ min-width: 0;
501
+ gap: 4px;
502
+ }
503
+
504
+ .dashboard-card__metric-label {
505
+ color: var(--dashboard-accent);
506
+ font-size: 12px;
507
+ font-weight: 700;
508
+ line-height: 1;
509
+ }
510
+
511
+ .dashboard-card__metric-value {
512
+ overflow: hidden;
513
+ color: #ffffff;
514
+ font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Microsoft YaHei', sans-serif;
515
+ font-size: 15px;
516
+ font-weight: 500;
517
+ line-height: 1;
518
+ text-overflow: ellipsis;
519
+ white-space: nowrap;
520
+ -webkit-font-smoothing: antialiased;
521
+ text-shadow:
522
+ 0 2px 4px rgba(57, 73, 91, 0.18),
523
+ 0 0 1px rgba(255, 255, 255, 0.28);
524
+ }
525
+
526
+ @media (max-width: 340px) {
527
+ .dashboard-page {
528
+ padding-right: 9px;
529
+ padding-left: 9px;
530
+ }
531
+
532
+ .dashboard-card__name {
533
+ font-size: 18px;
534
+ }
535
+
536
+ .dashboard-card__amount {
537
+ top: 38px;
538
+ font-size: 19px;
539
+ }
540
+
541
+ .dashboard-card__metrics {
542
+ left: 46px;
543
+ right: 11px;
544
+ column-gap: 22px;
545
+ }
546
+
547
+ .dashboard-card__metric-value {
548
+ font-size: 14px;
549
+ }
550
+ }
18
551
  </style>