minitest2.0 0.0.4 → 0.0.6
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 +1 -1
- package/src/App.vue +1 -1
- package/src/api/announcement.ts +153 -0
- package/src/api/clearing-detail.ts +66 -0
- package/src/api/foreign-position.ts +142 -0
- package/src/api/net-debit.ts +90 -0
- package/src/api/opening-reserve-estimate.ts +42 -0
- package/src/api/pbc-position.ts +63 -5
- package/src/router/index.ts +16 -16
- package/src/stores/user.ts +1 -1
- package/src/utils/foreign-forecast.ts +102 -0
- package/src/utils/rmb-forecast.ts +25 -1
- package/src/views/article-detail/index.vue +9 -0
- package/src/views/article-edit/index.vue +9 -0
- package/src/views/clearing-detail/index.vue +603 -12
- package/src/views/net-debit/index.vue +502 -12
- package/src/views/opening-reserve-estimate/index.vue +424 -12
- package/src/views/pbc-position/index.vue +65 -1
- package/src/views/rmb-position-detail/index.vue +14 -1
- package/src/views/warning/index.vue +566 -12
- package/vite.config.ts +3 -1
|
@@ -1,28 +1,440 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
buildOpeningReserveEstimatePayload,
|
|
4
|
+
queryOpeningReserveEstimate,
|
|
5
|
+
type OpeningReserveEstimateBody,
|
|
6
|
+
} from '@/api/opening-reserve-estimate'
|
|
7
|
+
import { showAppToast } from '@/utils/toast'
|
|
8
|
+
|
|
9
|
+
defineOptions({ name: 'OpeningReserveEstimatePage' })
|
|
10
|
+
|
|
11
|
+
type ResultItem = {
|
|
12
|
+
key: keyof OpeningReserveEstimateBody
|
|
13
|
+
label: string
|
|
14
|
+
icon: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type AmountUnitKey = 'hundredMillion' | 'tenMillion' | 'million' | 'tenThousand' | 'yuan'
|
|
18
|
+
|
|
19
|
+
type AmountUnit = {
|
|
20
|
+
key: AmountUnitKey
|
|
21
|
+
label: string
|
|
22
|
+
divisor: bigint
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const estimateDetail = ref<OpeningReserveEstimateBody | null>(null)
|
|
26
|
+
const querying = ref(false)
|
|
27
|
+
const selectedUnitKey = ref<AmountUnitKey>('yuan')
|
|
28
|
+
|
|
29
|
+
const defaultAmountUnit: AmountUnit = { key: 'yuan', label: '元', divisor: 1n }
|
|
30
|
+
|
|
31
|
+
const amountUnits: AmountUnit[] = [
|
|
32
|
+
{ key: 'hundredMillion', label: '亿元', divisor: 100000000n },
|
|
33
|
+
{ key: 'tenMillion', label: '千万元', divisor: 10000000n },
|
|
34
|
+
{ key: 'million', label: '百万元', divisor: 1000000n },
|
|
35
|
+
{ key: 'tenThousand', label: '万元', divisor: 10000n },
|
|
36
|
+
defaultAmountUnit,
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
const resultConfig: ResultItem[] = [
|
|
40
|
+
{ key: 'rhAmt', label: '人行时点头寸', icon: 'balance-o' },
|
|
41
|
+
{ key: 'unverifyAmtOut', label: '预报未核销支出', icon: 'card' },
|
|
42
|
+
{ key: 'unverifyAmtIn', label: '预报未核销收入', icon: 'gold-coin-o' },
|
|
43
|
+
{ key: 'pdAmt', label: '总分行铺底资金之和', icon: 'paid' },
|
|
44
|
+
{ key: 'fzAmt', label: '法定准备金', icon: 'shield-o' },
|
|
45
|
+
{ key: 'preAmt', label: '日初备款', icon: 'chart-trending-o' },
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
const selectedUnit = computed(
|
|
49
|
+
() => amountUnits.find((unit) => unit.key === selectedUnitKey.value) || defaultAmountUnit,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
const resultItems = computed(() =>
|
|
53
|
+
resultConfig.map((item) => ({
|
|
54
|
+
...item,
|
|
55
|
+
label: `${item.label}(${selectedUnit.value.label})`,
|
|
56
|
+
value: formatAmountByUnit(estimateDetail.value?.[item.key], selectedUnit.value),
|
|
57
|
+
})),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
onMounted(() => {
|
|
61
|
+
void handleQuery({ showSuccessToast: false })
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
function handleRefresh() {
|
|
65
|
+
void handleQuery()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function handleQuery(options: { showSuccessToast?: boolean } = {}) {
|
|
69
|
+
if (querying.value) return
|
|
70
|
+
|
|
71
|
+
querying.value = true
|
|
72
|
+
const payload = buildOpeningReserveEstimatePayload()
|
|
73
|
+
console.log('[日初备款数匡算入参]', payload)
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const response = await queryOpeningReserveEstimate(payload)
|
|
77
|
+
estimateDetail.value = response.body || {}
|
|
78
|
+
if (options.showSuccessToast !== false) {
|
|
79
|
+
showAppToast('刷新完成')
|
|
80
|
+
}
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error('[日初备款数匡算失败]', error)
|
|
83
|
+
showAppToast(getErrorMessage(error, '查询失败,请重试'))
|
|
84
|
+
} finally {
|
|
85
|
+
querying.value = false
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function formatAmountByUnit(value: string | undefined, unit: AmountUnit) {
|
|
90
|
+
const cents = parseYuanToCents(value)
|
|
91
|
+
|
|
92
|
+
if (cents === null) return '-'
|
|
93
|
+
|
|
94
|
+
return formatMinorAmount(divideRounded(cents, unit.divisor))
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function parseYuanToCents(value?: string) {
|
|
98
|
+
const text = value?.trim()
|
|
99
|
+
|
|
100
|
+
if (!text) return null
|
|
101
|
+
|
|
102
|
+
const normalizedText = text.replaceAll(',', '')
|
|
103
|
+
|
|
104
|
+
if (!/^[-+]?\d+(\.\d+)?$/.test(normalizedText)) {
|
|
105
|
+
return null
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const isNegative = normalizedText.startsWith('-')
|
|
109
|
+
const unsignedText =
|
|
110
|
+
normalizedText.startsWith('-') || normalizedText.startsWith('+')
|
|
111
|
+
? normalizedText.slice(1)
|
|
112
|
+
: normalizedText
|
|
113
|
+
const [rawInteger = '0', rawDecimal = ''] = unsignedText.split('.')
|
|
114
|
+
const integer = rawInteger.replace(/^0+(?=\d)/, '') || '0'
|
|
115
|
+
const decimalText = `${rawDecimal}000`
|
|
116
|
+
const decimal = decimalText.slice(0, 2)
|
|
117
|
+
const needsRound = Number(decimalText[2]) >= 5
|
|
118
|
+
let cents = BigInt(integer) * 100n + BigInt(decimal)
|
|
119
|
+
|
|
120
|
+
if (needsRound) {
|
|
121
|
+
cents += 1n
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return isNegative ? -cents : cents
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function divideRounded(value: bigint, divisor: bigint) {
|
|
128
|
+
const isNegative = value < 0n
|
|
129
|
+
const absoluteValue = isNegative ? -value : value
|
|
130
|
+
let quotient = absoluteValue / divisor
|
|
131
|
+
const remainder = absoluteValue % divisor
|
|
132
|
+
|
|
133
|
+
if (remainder * 2n >= divisor) {
|
|
134
|
+
quotient += 1n
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return isNegative ? -quotient : quotient
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function formatMinorAmount(value: bigint) {
|
|
141
|
+
const isNegative = value < 0n
|
|
142
|
+
const absoluteValue = isNegative ? -value : value
|
|
143
|
+
const integer = absoluteValue / 100n
|
|
144
|
+
const decimal = String(absoluteValue % 100n).padStart(2, '0')
|
|
145
|
+
const groupedInteger = integer.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
|
146
|
+
|
|
147
|
+
return `${isNegative ? '-' : ''}${groupedInteger}.${decimal}`
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function getErrorMessage(error: unknown, fallback: string) {
|
|
151
|
+
return error instanceof Error && error.message ? error.message : fallback
|
|
152
|
+
}
|
|
153
|
+
</script>
|
|
154
|
+
|
|
1
155
|
<template>
|
|
2
|
-
<main class="page">
|
|
3
|
-
<section class="
|
|
4
|
-
<
|
|
156
|
+
<main class="opening-reserve-page">
|
|
157
|
+
<section class="result-section" aria-label="日初备款数匡算查询结果">
|
|
158
|
+
<div class="section-heading">
|
|
159
|
+
<div class="section-heading__title">
|
|
160
|
+
<span class="section-heading__mark" aria-hidden="true"></span>
|
|
161
|
+
<h2>查询结果</h2>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
<label class="unit-switch">
|
|
165
|
+
<span>单位</span>
|
|
166
|
+
<em>{{ selectedUnit.label }}</em>
|
|
167
|
+
<select v-model="selectedUnitKey" aria-label="选择金额单位">
|
|
168
|
+
<option v-for="unit in amountUnits" :key="unit.key" :value="unit.key">
|
|
169
|
+
{{ unit.label }}
|
|
170
|
+
</option>
|
|
171
|
+
</select>
|
|
172
|
+
<van-icon name="arrow-down" aria-hidden="true" />
|
|
173
|
+
</label>
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
<div class="result-stack">
|
|
177
|
+
<dl class="result-list">
|
|
178
|
+
<div v-for="item in resultItems" :key="item.key" class="result-row">
|
|
179
|
+
<dt>
|
|
180
|
+
<span class="result-icon" aria-hidden="true">
|
|
181
|
+
<van-icon :name="item.icon" />
|
|
182
|
+
</span>
|
|
183
|
+
<span class="result-label">{{ item.label }}</span>
|
|
184
|
+
</dt>
|
|
185
|
+
<dd>{{ item.value }}</dd>
|
|
186
|
+
</div>
|
|
187
|
+
</dl>
|
|
188
|
+
</div>
|
|
5
189
|
</section>
|
|
190
|
+
|
|
191
|
+
<nav class="action-bar" aria-label="日初备款数匡算操作">
|
|
192
|
+
<button class="query-button" type="button" :disabled="querying" @click="handleRefresh">
|
|
193
|
+
<span>{{ querying ? '刷新中' : '刷新' }}</span>
|
|
194
|
+
</button>
|
|
195
|
+
</nav>
|
|
6
196
|
</main>
|
|
7
197
|
</template>
|
|
8
198
|
|
|
9
199
|
<style scoped>
|
|
10
|
-
.page {
|
|
200
|
+
.opening-reserve-page {
|
|
201
|
+
width: 100%;
|
|
11
202
|
max-width: 750px;
|
|
12
203
|
min-height: var(--app-page-min-height, 100vh);
|
|
13
204
|
margin: 0 auto;
|
|
14
|
-
padding:
|
|
15
|
-
|
|
205
|
+
padding: 31px 15px calc(env(safe-area-inset-bottom, 0px) + 94px);
|
|
206
|
+
color: #171b20;
|
|
207
|
+
background: #ffffff;
|
|
208
|
+
box-sizing: border-box;
|
|
16
209
|
}
|
|
17
210
|
|
|
18
|
-
|
|
19
|
-
|
|
211
|
+
button {
|
|
212
|
+
border: 0;
|
|
213
|
+
padding: 0;
|
|
214
|
+
font: inherit;
|
|
215
|
+
background: transparent;
|
|
216
|
+
appearance: none;
|
|
20
217
|
}
|
|
21
218
|
|
|
22
|
-
.
|
|
23
|
-
|
|
24
|
-
|
|
219
|
+
.result-section {
|
|
220
|
+
display: block;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.section-heading {
|
|
224
|
+
display: flex;
|
|
225
|
+
align-items: center;
|
|
226
|
+
justify-content: space-between;
|
|
227
|
+
gap: 12px;
|
|
228
|
+
margin-bottom: 21px;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.section-heading__title {
|
|
232
|
+
display: flex;
|
|
233
|
+
align-items: center;
|
|
234
|
+
min-width: 0;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.section-heading__mark {
|
|
238
|
+
width: 7px;
|
|
239
|
+
height: 24px;
|
|
240
|
+
flex: 0 0 7px;
|
|
241
|
+
border-radius: 999px;
|
|
242
|
+
background: linear-gradient(180deg, #ff3b44 0%, #ff1f2d 100%);
|
|
243
|
+
box-shadow: 0 6px 12px rgba(255, 39, 48, 0.22);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.section-heading h2 {
|
|
247
|
+
margin: 0 0 0 13px;
|
|
248
|
+
color: #151922;
|
|
249
|
+
font-size: 20px;
|
|
250
|
+
font-weight: 760;
|
|
251
|
+
line-height: 1.2;
|
|
252
|
+
letter-spacing: 0;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.unit-switch {
|
|
256
|
+
position: relative;
|
|
257
|
+
display: inline-flex;
|
|
258
|
+
align-items: center;
|
|
259
|
+
flex: 0 0 auto;
|
|
260
|
+
height: 30px;
|
|
261
|
+
padding: 0 28px 0 11px;
|
|
262
|
+
border: 1px solid #ffd5da;
|
|
263
|
+
border-radius: 999px;
|
|
264
|
+
color: #ef252d;
|
|
265
|
+
font-size: 12px;
|
|
266
|
+
font-weight: 560;
|
|
267
|
+
line-height: 1;
|
|
268
|
+
background: #fff7f8;
|
|
269
|
+
box-shadow: 0 5px 14px rgba(255, 43, 50, 0.08);
|
|
270
|
+
box-sizing: border-box;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.unit-switch span {
|
|
274
|
+
margin-right: 4px;
|
|
275
|
+
color: #8c8f98;
|
|
276
|
+
font-weight: 500;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.unit-switch em {
|
|
280
|
+
font-style: normal;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.unit-switch select {
|
|
284
|
+
position: absolute;
|
|
285
|
+
inset: 0;
|
|
286
|
+
z-index: 2;
|
|
287
|
+
width: 100%;
|
|
288
|
+
height: 100%;
|
|
289
|
+
opacity: 0;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.unit-switch :deep(.van-icon) {
|
|
293
|
+
position: absolute;
|
|
294
|
+
right: 10px;
|
|
295
|
+
color: #ef252d;
|
|
296
|
+
font-size: 12px;
|
|
297
|
+
pointer-events: none;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.result-stack {
|
|
301
|
+
display: block;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.result-list {
|
|
305
|
+
display: flex;
|
|
306
|
+
flex-direction: column;
|
|
307
|
+
gap: 10px;
|
|
308
|
+
margin: 0;
|
|
309
|
+
padding: 0;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.result-row {
|
|
313
|
+
display: grid;
|
|
314
|
+
grid-template-columns: 34px minmax(0, 1fr);
|
|
315
|
+
grid-template-rows: auto auto;
|
|
316
|
+
align-items: center;
|
|
317
|
+
min-height: 74px;
|
|
318
|
+
padding: 13px 16px;
|
|
319
|
+
column-gap: 13px;
|
|
320
|
+
border: 1px solid #ffd9dd;
|
|
321
|
+
border-radius: 12px;
|
|
322
|
+
background:
|
|
323
|
+
linear-gradient(180deg, rgba(255, 255, 255, 0.99) 0%, rgba(255, 252, 252, 0.97) 100%), #ffffff;
|
|
324
|
+
box-shadow: 0 7px 18px rgba(255, 43, 50, 0.06);
|
|
325
|
+
box-sizing: border-box;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.result-row dt {
|
|
329
|
+
display: contents;
|
|
330
|
+
margin: 0;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.result-icon {
|
|
334
|
+
display: inline-flex;
|
|
335
|
+
align-items: center;
|
|
336
|
+
justify-content: center;
|
|
337
|
+
grid-row: 1 / 3;
|
|
338
|
+
grid-column: 1;
|
|
339
|
+
width: 34px;
|
|
340
|
+
height: 34px;
|
|
341
|
+
border-radius: 50%;
|
|
342
|
+
color: #f52932;
|
|
343
|
+
font-size: 19px;
|
|
344
|
+
background:
|
|
345
|
+
radial-gradient(circle at 35% 24%, rgba(255, 255, 255, 0.9) 0%, rgba(255, 255, 255, 0) 42%),
|
|
346
|
+
linear-gradient(135deg, #fff2f3 0%, #ffe6e9 100%);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.result-label {
|
|
350
|
+
grid-row: 1;
|
|
351
|
+
grid-column: 2;
|
|
352
|
+
align-self: end;
|
|
353
|
+
min-width: 0;
|
|
354
|
+
margin: 0 0 5px;
|
|
355
|
+
color: #20242d;
|
|
25
356
|
font-size: 14px;
|
|
26
|
-
|
|
357
|
+
font-weight: 500;
|
|
358
|
+
line-height: 1.25;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
.result-row dd {
|
|
362
|
+
grid-row: 2;
|
|
363
|
+
grid-column: 2;
|
|
364
|
+
align-self: start;
|
|
365
|
+
min-width: 0;
|
|
366
|
+
margin: 0;
|
|
367
|
+
color: #f20f19;
|
|
368
|
+
font-size: 18px;
|
|
369
|
+
font-weight: 650;
|
|
370
|
+
line-height: 1.28;
|
|
371
|
+
text-align: left;
|
|
372
|
+
overflow-wrap: anywhere;
|
|
373
|
+
letter-spacing: 0;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.action-bar {
|
|
377
|
+
position: fixed;
|
|
378
|
+
right: 0;
|
|
379
|
+
bottom: calc(env(safe-area-inset-bottom, 0px) + 15px);
|
|
380
|
+
left: 50%;
|
|
381
|
+
z-index: 10;
|
|
382
|
+
width: 100%;
|
|
383
|
+
max-width: 750px;
|
|
384
|
+
padding: 0 15px;
|
|
385
|
+
transform: translateX(-50%);
|
|
386
|
+
box-sizing: border-box;
|
|
387
|
+
pointer-events: none;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.query-button {
|
|
391
|
+
display: inline-flex;
|
|
392
|
+
align-items: center;
|
|
393
|
+
justify-content: center;
|
|
394
|
+
width: 100%;
|
|
395
|
+
height: 48px;
|
|
396
|
+
border-radius: 14px;
|
|
397
|
+
color: #ffffff;
|
|
398
|
+
font-size: 18px;
|
|
399
|
+
font-weight: 680;
|
|
400
|
+
line-height: 1;
|
|
401
|
+
background: linear-gradient(135deg, #ff3d43 0%, #ff1825 100%);
|
|
402
|
+
box-shadow:
|
|
403
|
+
0 13px 24px rgba(255, 35, 43, 0.24),
|
|
404
|
+
inset 0 1px 0 rgba(255, 255, 255, 0.28);
|
|
405
|
+
pointer-events: auto;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
.query-button:disabled {
|
|
409
|
+
cursor: not-allowed;
|
|
410
|
+
opacity: 0.72;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
@media (max-width: 360px) {
|
|
414
|
+
.opening-reserve-page {
|
|
415
|
+
padding-right: 15px;
|
|
416
|
+
padding-left: 15px;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
.result-row {
|
|
420
|
+
grid-template-columns: 32px minmax(0, 1fr);
|
|
421
|
+
min-height: 70px;
|
|
422
|
+
padding: 12px 14px;
|
|
423
|
+
column-gap: 12px;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
.result-icon {
|
|
427
|
+
width: 32px;
|
|
428
|
+
height: 32px;
|
|
429
|
+
font-size: 18px;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
.result-label {
|
|
433
|
+
font-size: 13px;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
.result-row dd {
|
|
437
|
+
font-size: 16px;
|
|
438
|
+
}
|
|
27
439
|
}
|
|
28
440
|
</style>
|
|
@@ -9,30 +9,50 @@ import { showAppToast } from '@/utils/toast'
|
|
|
9
9
|
|
|
10
10
|
defineOptions({ name: 'PbcPositionPage' })
|
|
11
11
|
|
|
12
|
+
/** 页面信息行的展示模型,icon 对应 Vant Icon 的图标名称。 */
|
|
12
13
|
type PositionItem = {
|
|
14
|
+
/** 字段中文名,直接渲染到左侧标题。 */
|
|
13
15
|
label: string
|
|
16
|
+
/** 字段展示值,接口空值统一转换为占位符。 */
|
|
14
17
|
value: string
|
|
18
|
+
/** Vant Icon 名称,用于增强字段识别度。 */
|
|
15
19
|
icon: string
|
|
20
|
+
/** 是否按关键金额样式突出展示。 */
|
|
16
21
|
highlight?: boolean
|
|
17
22
|
}
|
|
18
23
|
|
|
24
|
+
/** 页面按业务含义分组展示字段,避免模板中直接堆业务判断。 */
|
|
19
25
|
type PositionSection = {
|
|
26
|
+
/** 分组唯一标识,用于 v-for key。 */
|
|
20
27
|
key: string
|
|
28
|
+
/** 当前分组下的信息行。 */
|
|
21
29
|
items: PositionItem[]
|
|
22
30
|
}
|
|
23
31
|
|
|
32
|
+
/** 当前申请或最终查询到的人行头寸余额明细。 */
|
|
24
33
|
const balanceDetail = ref<PbcPositionBalance | null>(null)
|
|
34
|
+
/** “发送”按钮加载态,防止重复发起申请。 */
|
|
25
35
|
const sending = ref(false)
|
|
36
|
+
/** “查询”按钮加载态,防止查询链路重复执行。 */
|
|
26
37
|
const querying = ref(false)
|
|
27
38
|
|
|
39
|
+
/** 任一交易进行中时禁用底部操作按钮。 */
|
|
28
40
|
const actionDisabled = computed(() => sending.value || querying.value)
|
|
41
|
+
/** 发送交易返回的申请平台流水号;存在该值时才展示“查询”按钮。 */
|
|
29
42
|
const requestRefNo = computed(() => balanceDetail.value?.requestRefNo || '')
|
|
30
43
|
|
|
44
|
+
/**
|
|
45
|
+
* 将接口明细转换为页面展示分组。
|
|
46
|
+
*
|
|
47
|
+
* 发送成功但尚未查询时,balanceDetail 只包含申请流水号;其余字段会显示
|
|
48
|
+
* 占位符。查询成功后,列表字段和明细字段合并进同一个对象并刷新展示。
|
|
49
|
+
*/
|
|
31
50
|
const positionSections = computed<PositionSection[]>(() => {
|
|
32
51
|
const detail = balanceDetail.value
|
|
33
52
|
|
|
34
53
|
return [
|
|
35
54
|
{
|
|
55
|
+
// 基础信息:流水号、账号、更新时间等用于定位本次查询结果的字段。
|
|
36
56
|
key: 'basic',
|
|
37
57
|
items: [
|
|
38
58
|
{ label: '清算账户账号', value: fieldText(detail?.settleBankNo), icon: 'manager' },
|
|
@@ -47,6 +67,7 @@ const positionSections = computed<PositionSection[]>(() => {
|
|
|
47
67
|
],
|
|
48
68
|
},
|
|
49
69
|
{
|
|
70
|
+
// 余额信息:接口返回单位为元,页面统一换算为亿元展示。
|
|
50
71
|
key: 'balance',
|
|
51
72
|
items: [
|
|
52
73
|
{
|
|
@@ -64,6 +85,7 @@ const positionSections = computed<PositionSection[]>(() => {
|
|
|
64
85
|
],
|
|
65
86
|
},
|
|
66
87
|
{
|
|
88
|
+
// 排队和辖内非清算账户信息:由明细交易补充返回。
|
|
67
89
|
key: 'queue',
|
|
68
90
|
items: [
|
|
69
91
|
{
|
|
@@ -87,7 +109,14 @@ const positionSections = computed<PositionSection[]>(() => {
|
|
|
87
109
|
]
|
|
88
110
|
})
|
|
89
111
|
|
|
112
|
+
/**
|
|
113
|
+
* 发起人行头寸余额查询申请。
|
|
114
|
+
*
|
|
115
|
+
* 成功后仅能得到申请平台流水号,页面先保存该流水号并展示“查询”按钮;
|
|
116
|
+
* 最终余额数据需要用户继续点击“查询”触发列表加明细的组合查询。
|
|
117
|
+
*/
|
|
90
118
|
async function handleSend() {
|
|
119
|
+
// 交易进行中直接忽略重复点击,避免产生多笔申请或状态交叉覆盖。
|
|
91
120
|
if (actionDisabled.value) return
|
|
92
121
|
|
|
93
122
|
sending.value = true
|
|
@@ -98,6 +127,7 @@ async function handleSend() {
|
|
|
98
127
|
const response = await sendPbcPositionRequest(payload)
|
|
99
128
|
const refNo = response.body.refNo || ''
|
|
100
129
|
|
|
130
|
+
// 保存申请流水号作为下一步查询条件,同时让页面立即展示当前编号。
|
|
101
131
|
balanceDetail.value = {
|
|
102
132
|
requestRefNo: refNo,
|
|
103
133
|
refNo,
|
|
@@ -111,7 +141,14 @@ async function handleSend() {
|
|
|
111
141
|
}
|
|
112
142
|
}
|
|
113
143
|
|
|
144
|
+
/**
|
|
145
|
+
* 根据发送返回的申请平台流水号查询人行头寸余额。
|
|
146
|
+
*
|
|
147
|
+
* queryPbcPositionBalance 内部会先查候选列表,再逐条查询明细并匹配
|
|
148
|
+
* requestRefNo;页面只负责校验是否已有申请编号和维护加载态。
|
|
149
|
+
*/
|
|
114
150
|
async function handleQuery() {
|
|
151
|
+
// 和发送共用禁用条件,避免发送、查询两个异步流程并发写入页面状态。
|
|
115
152
|
if (actionDisabled.value) return
|
|
116
153
|
|
|
117
154
|
if (!requestRefNo.value) {
|
|
@@ -136,11 +173,18 @@ async function handleQuery() {
|
|
|
136
173
|
}
|
|
137
174
|
}
|
|
138
175
|
|
|
176
|
+
/** 将接口字符串字段标准化为可展示文本,空值统一显示占位符。 */
|
|
139
177
|
function fieldText(value?: string) {
|
|
140
178
|
const text = value?.trim()
|
|
141
179
|
return text || '-'
|
|
142
180
|
}
|
|
143
181
|
|
|
182
|
+
/**
|
|
183
|
+
* 将接口返回的元级金额转换为亿元文本。
|
|
184
|
+
*
|
|
185
|
+
* 后端金额字段可能带千分位逗号;转换前先移除逗号,非法数字或空值
|
|
186
|
+
* 都按占位符展示,避免页面出现 NaN。
|
|
187
|
+
*/
|
|
144
188
|
function formatYuanToBillionText(value?: string) {
|
|
145
189
|
if (!value?.trim()) return '-'
|
|
146
190
|
|
|
@@ -153,6 +197,7 @@ function formatYuanToBillionText(value?: string) {
|
|
|
153
197
|
}).format(amount / 100000000)
|
|
154
198
|
}
|
|
155
199
|
|
|
200
|
+
/** 从异常对象中提取可读错误信息,兜底展示当前操作的默认失败文案。 */
|
|
156
201
|
function getErrorMessage(error: unknown, fallback: string) {
|
|
157
202
|
return error instanceof Error && error.message ? error.message : fallback
|
|
158
203
|
}
|
|
@@ -160,6 +205,7 @@ function getErrorMessage(error: unknown, fallback: string) {
|
|
|
160
205
|
|
|
161
206
|
<template>
|
|
162
207
|
<main class="pbc-page">
|
|
208
|
+
<!-- 余额信息区:发送前显示占位符,查询成功后展示真实余额和明细字段。 -->
|
|
163
209
|
<section class="balance-card" aria-label="人行头寸余额">
|
|
164
210
|
<div v-for="section in positionSections" :key="section.key" class="info-section">
|
|
165
211
|
<dl class="info-list">
|
|
@@ -176,7 +222,8 @@ function getErrorMessage(error: unknown, fallback: string) {
|
|
|
176
222
|
</div>
|
|
177
223
|
</section>
|
|
178
224
|
|
|
179
|
-
|
|
225
|
+
<!-- 底部操作区:未获得申请流水号时只展示“发送”,发送成功后再展示“查询”。 -->
|
|
226
|
+
<nav class="action-bar" :class="{ 'single-action': !requestRefNo }" aria-label="人行头寸操作">
|
|
180
227
|
<button
|
|
181
228
|
class="action-btn send-btn"
|
|
182
229
|
type="button"
|
|
@@ -186,6 +233,7 @@ function getErrorMessage(error: unknown, fallback: string) {
|
|
|
186
233
|
<span>{{ sending ? '发送中' : '发送' }}</span>
|
|
187
234
|
</button>
|
|
188
235
|
<button
|
|
236
|
+
v-if="requestRefNo"
|
|
189
237
|
class="action-btn query-btn"
|
|
190
238
|
type="button"
|
|
191
239
|
:disabled="actionDisabled"
|
|
@@ -198,6 +246,7 @@ function getErrorMessage(error: unknown, fallback: string) {
|
|
|
198
246
|
</template>
|
|
199
247
|
|
|
200
248
|
<style scoped>
|
|
249
|
+
/* 页面主体宽度与移动端安全区留白,适配 750px 设计稿容器。 */
|
|
201
250
|
.pbc-page {
|
|
202
251
|
width: 100%;
|
|
203
252
|
max-width: 750px;
|
|
@@ -208,6 +257,7 @@ function getErrorMessage(error: unknown, fallback: string) {
|
|
|
208
257
|
box-sizing: border-box;
|
|
209
258
|
}
|
|
210
259
|
|
|
260
|
+
/* 重置原生按钮样式,避免不同终端默认外观影响底部操作按钮。 */
|
|
211
261
|
button {
|
|
212
262
|
border: 0;
|
|
213
263
|
padding: 0;
|
|
@@ -216,6 +266,7 @@ button {
|
|
|
216
266
|
appearance: none;
|
|
217
267
|
}
|
|
218
268
|
|
|
269
|
+
/* 信息卡片承载全部余额字段,多个业务分组用浅色分隔条区分。 */
|
|
219
270
|
.balance-card {
|
|
220
271
|
overflow: hidden;
|
|
221
272
|
margin: 17px 13px 0;
|
|
@@ -229,6 +280,7 @@ button {
|
|
|
229
280
|
border-top: 8px solid #f7f7f8;
|
|
230
281
|
}
|
|
231
282
|
|
|
283
|
+
/* 字段列表使用 dl 语义结构,dt 为字段名,dd 为字段值。 */
|
|
232
284
|
.info-list {
|
|
233
285
|
margin: 0;
|
|
234
286
|
padding: 8px 13px;
|
|
@@ -246,6 +298,7 @@ button {
|
|
|
246
298
|
border-top: 1px solid #edf0f2;
|
|
247
299
|
}
|
|
248
300
|
|
|
301
|
+
/* 字段图标和标题保持左侧固定结构,右侧值按内容宽度右对齐。 */
|
|
249
302
|
.info-row dt {
|
|
250
303
|
display: flex;
|
|
251
304
|
align-items: center;
|
|
@@ -288,6 +341,7 @@ button {
|
|
|
288
341
|
white-space: nowrap;
|
|
289
342
|
}
|
|
290
343
|
|
|
344
|
+
/* 关键余额金额使用更高权重,便于柜员快速定位清算账户余额。 */
|
|
291
345
|
.info-row dd.highlight {
|
|
292
346
|
color: #c91b20;
|
|
293
347
|
font-size: 22px;
|
|
@@ -295,6 +349,7 @@ button {
|
|
|
295
349
|
letter-spacing: 0;
|
|
296
350
|
}
|
|
297
351
|
|
|
352
|
+
/* 固定底部操作栏,pointer-events 只开放给按钮,避免遮挡页面其他区域点击。 */
|
|
298
353
|
.action-bar {
|
|
299
354
|
position: fixed;
|
|
300
355
|
right: 0;
|
|
@@ -312,6 +367,12 @@ button {
|
|
|
312
367
|
pointer-events: none;
|
|
313
368
|
}
|
|
314
369
|
|
|
370
|
+
/* 未获取申请流水号时只有一个发送按钮,按钮占满整行。 */
|
|
371
|
+
.action-bar.single-action {
|
|
372
|
+
grid-template-columns: 1fr;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/* 发送和查询按钮共享基础尺寸,具体颜色由各自业务类控制。 */
|
|
315
376
|
.action-btn {
|
|
316
377
|
display: inline-flex;
|
|
317
378
|
align-items: center;
|
|
@@ -329,6 +390,7 @@ button {
|
|
|
329
390
|
opacity: 0.68;
|
|
330
391
|
}
|
|
331
392
|
|
|
393
|
+
/* 发送是主操作,使用实心红色按钮。 */
|
|
332
394
|
.send-btn {
|
|
333
395
|
color: #ffffff;
|
|
334
396
|
background: linear-gradient(135deg, #ff3338 0%, #ff232b 100%);
|
|
@@ -337,6 +399,7 @@ button {
|
|
|
337
399
|
inset 0 1px 0 rgba(255, 255, 255, 0.22);
|
|
338
400
|
}
|
|
339
401
|
|
|
402
|
+
/* 查询是发送后的次级操作,使用描边按钮以弱化视觉层级。 */
|
|
340
403
|
.query-btn {
|
|
341
404
|
border: 1px solid #ff3038;
|
|
342
405
|
color: #ef252d;
|
|
@@ -344,6 +407,7 @@ button {
|
|
|
344
407
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.9);
|
|
345
408
|
}
|
|
346
409
|
|
|
410
|
+
/* 窄屏下压缩字段和值字号,避免长字段名与金额互相挤压。 */
|
|
347
411
|
@media (max-width: 350px) {
|
|
348
412
|
.field-label,
|
|
349
413
|
.info-row dd {
|