minitest2.0 0.0.5 → 0.0.7
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/public/images/dashboard/01-rmb-great-wall-bg.jpg +0 -0
- package/public/images/dashboard/02-city-clearing-skyline-bg.jpg +0 -0
- package/public/images/dashboard/03-cross-border-rmb-world-map-bg.jpg +0 -0
- package/public/images/dashboard/04-digital-currency-fintech-bg.jpg +0 -0
- package/public/images/dashboard/05-usd-liberty-bg.jpg +0 -0
- package/public/images/dashboard/06-eur-europe-architecture-bg.jpg +0 -0
- package/public/images/dashboard/07-hkd-hong-kong-skyline-bg.jpg +0 -0
- package/public/images/dashboard/08-jpy-mount-fuji-bg.jpg +0 -0
- package/src/api/announcement.ts +153 -0
- package/src/api/foreign-position.ts +213 -0
- package/src/router/index.ts +14 -2
- package/src/stores/user.ts +1 -1
- package/src/utils/foreign-forecast.ts +134 -0
- package/src/views/article-detail/index.vue +44 -1
- package/src/views/article-edit/index.vue +9 -0
- package/src/views/dashboard/index.vue +410 -2
- package/src/views/foreign-position/index.vue +1208 -11
- package/src/views/foreign-position-create/index.vue +363 -0
- package/src/views/foreign-position-detail/index.vue +390 -0
- package/src/views/opening-reserve-estimate/index.vue +135 -44
- package/src/views/warning/index.vue +566 -12
- package/vite.config.ts +3 -1
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
buildForeignForecastDetailPayload,
|
|
4
|
+
queryForeignForecastDetail,
|
|
5
|
+
type ForeignForecastDetailRecord,
|
|
6
|
+
} from '@/api/foreign-position'
|
|
7
|
+
import {
|
|
8
|
+
formatForeignForecastAmountText,
|
|
9
|
+
formatForeignForecastAmountYuanText,
|
|
10
|
+
formatForeignForecastDate,
|
|
11
|
+
resolveForeignBusinessType,
|
|
12
|
+
resolveForeignCurrency,
|
|
13
|
+
resolveForeignStatus,
|
|
14
|
+
} from '@/utils/foreign-forecast'
|
|
15
|
+
import { showAppToast } from '@/utils/toast'
|
|
16
|
+
|
|
17
|
+
defineOptions({ name: 'ForeignPositionDetailPage' })
|
|
18
|
+
|
|
19
|
+
type DetailField = {
|
|
20
|
+
label: string
|
|
21
|
+
value: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type DetailSection = {
|
|
25
|
+
title: string
|
|
26
|
+
fields: DetailField[]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const route = useRoute()
|
|
30
|
+
const router = useRouter()
|
|
31
|
+
|
|
32
|
+
const predRefNo = computed(() => String(route.params.predRefNo || ''))
|
|
33
|
+
const detail = ref<ForeignForecastDetailRecord>()
|
|
34
|
+
const loading = ref(false)
|
|
35
|
+
const loadError = ref(false)
|
|
36
|
+
const currencyValue = computed(() => getRecordCurrency(detail.value))
|
|
37
|
+
const currencyOption = computed(() => resolveForeignCurrency(currencyValue.value))
|
|
38
|
+
const statusOption = computed(() => resolveForeignStatus(detail.value?.predStatus))
|
|
39
|
+
const heroAmount = computed(() =>
|
|
40
|
+
formatForeignForecastAmountText(detail.value?.predAmt, currencyValue.value),
|
|
41
|
+
)
|
|
42
|
+
const heroAmountYuan = computed(() => formatForeignForecastAmountYuanText(detail.value?.predAmt))
|
|
43
|
+
const heroDate = computed(() => formatForeignForecastDate(detail.value?.tranDate))
|
|
44
|
+
|
|
45
|
+
const detailSections = computed<DetailSection[]>(() => {
|
|
46
|
+
const item = detail.value
|
|
47
|
+
|
|
48
|
+
if (!item) return []
|
|
49
|
+
|
|
50
|
+
return [
|
|
51
|
+
{
|
|
52
|
+
title: '基础信息',
|
|
53
|
+
fields: [
|
|
54
|
+
{ label: '预报流水号', value: fieldText(item.predRefNo) },
|
|
55
|
+
{ label: '交易日期', value: formatForeignForecastDate(item.tranDate) },
|
|
56
|
+
{ label: '预报日期', value: formatForeignForecastDate(item.predDate) },
|
|
57
|
+
{ label: '预报状态', value: statusOption.value.label },
|
|
58
|
+
{ label: '核销状态', value: fieldText(item.verifyStatus) },
|
|
59
|
+
{ label: '额度标志', value: fieldText(item.eduFlag) },
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
title: '机构与人员',
|
|
64
|
+
fields: [
|
|
65
|
+
{ label: '预报机构', value: fieldText(item.predBranchName || item.predBranch) },
|
|
66
|
+
{ label: '预报机构代码', value: fieldText(item.predBranch) },
|
|
67
|
+
{ label: '预报人员编号', value: fieldText(item.predCode) },
|
|
68
|
+
{ label: '预报人员姓名', value: fieldText(item.predName) },
|
|
69
|
+
{ label: '落地机构名称', value: fieldText(item.groundBranch) },
|
|
70
|
+
{ label: '落地机构代码', value: fieldText(item.groundBranchCode) },
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
title: '业务与币种',
|
|
75
|
+
fields: [
|
|
76
|
+
{ label: '币种', value: currencyOption.value.label },
|
|
77
|
+
{ label: '业务类型', value: resolveForeignBusinessType(item.busiType) },
|
|
78
|
+
{ label: '客户名称', value: fieldText(item.clientName) },
|
|
79
|
+
{ label: '交易对手名称', value: fieldText(item.competName) },
|
|
80
|
+
{ label: '交易对手账号', value: fieldText(item.competAcct) },
|
|
81
|
+
{
|
|
82
|
+
label: '清算行 SWIFT',
|
|
83
|
+
value: fieldText(item.bankSwift || item.swiftCode || item.bicCode),
|
|
84
|
+
},
|
|
85
|
+
{ label: '标准名称', value: fieldText(item.standardName) },
|
|
86
|
+
{ label: '用途', value: fieldText(item.purpose) },
|
|
87
|
+
{ label: '备注', value: fieldText(item.remark) },
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
title: '支付与金额',
|
|
92
|
+
fields: [
|
|
93
|
+
{
|
|
94
|
+
label: '预报金额',
|
|
95
|
+
value: formatForeignForecastAmountText(item.predAmt, currencyValue.value),
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
label: '已核销金额',
|
|
99
|
+
value: formatForeignForecastAmountText(item.verifyAmt, currencyValue.value),
|
|
100
|
+
},
|
|
101
|
+
{ label: '支付渠道', value: fieldText(item.payChannel) },
|
|
102
|
+
{ label: '收支方向', value: fieldText(item.payDirec) },
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
title: '票据与联系',
|
|
107
|
+
fields: [
|
|
108
|
+
{ label: '出票日', value: formatForeignForecastDate(item.billDate) },
|
|
109
|
+
{ label: '票号', value: fieldText(item.billNo) },
|
|
110
|
+
{ label: '联系人', value: fieldText(item.linkMan) },
|
|
111
|
+
{ label: '联系电话', value: fieldText(item.phoneNo) },
|
|
112
|
+
],
|
|
113
|
+
},
|
|
114
|
+
]
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
onMounted(() => {
|
|
118
|
+
loadDetail()
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
async function loadDetail() {
|
|
122
|
+
if (!predRefNo.value) {
|
|
123
|
+
loadError.value = true
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
loading.value = true
|
|
128
|
+
loadError.value = false
|
|
129
|
+
|
|
130
|
+
const payload = buildForeignForecastDetailPayload(predRefNo.value)
|
|
131
|
+
console.log('[外币预报详情入参]', payload)
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const response = await queryForeignForecastDetail(payload)
|
|
135
|
+
detail.value = response.body
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.error('[外币预报详情加载失败]', error)
|
|
138
|
+
loadError.value = true
|
|
139
|
+
showAppToast('详情加载失败,请重试')
|
|
140
|
+
} finally {
|
|
141
|
+
loading.value = false
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function getRecordCurrency(record?: ForeignForecastDetailRecord) {
|
|
146
|
+
return (
|
|
147
|
+
record?.ccy ||
|
|
148
|
+
record?.currency ||
|
|
149
|
+
record?.currencyCode ||
|
|
150
|
+
record?.currCode ||
|
|
151
|
+
record?.curType ||
|
|
152
|
+
record?.tranCcy ||
|
|
153
|
+
''
|
|
154
|
+
)
|
|
155
|
+
.trim()
|
|
156
|
+
.toUpperCase()
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function fieldText(value?: string) {
|
|
160
|
+
const text = value?.trim()
|
|
161
|
+
return text || '-'
|
|
162
|
+
}
|
|
163
|
+
</script>
|
|
164
|
+
|
|
165
|
+
<template>
|
|
166
|
+
<main class="detail-page">
|
|
167
|
+
<section v-if="loading" class="state-section">
|
|
168
|
+
<van-loading vertical>加载中...</van-loading>
|
|
169
|
+
</section>
|
|
170
|
+
|
|
171
|
+
<template v-else-if="detail">
|
|
172
|
+
<section class="hero-card" aria-label="外币预报详情概要">
|
|
173
|
+
<div class="hero-topline">
|
|
174
|
+
<span class="status-tag" :class="statusOption.className">{{ statusOption.label }}</span>
|
|
175
|
+
<span class="hero-date">{{ heroDate }}</span>
|
|
176
|
+
</div>
|
|
177
|
+
<strong>{{ heroAmount }}</strong>
|
|
178
|
+
<span v-if="heroAmountYuan !== '-'" class="hero-amount-yuan">{{ heroAmountYuan }}</span>
|
|
179
|
+
<p>{{ detail.predRefNo || predRefNo }}</p>
|
|
180
|
+
</section>
|
|
181
|
+
|
|
182
|
+
<section
|
|
183
|
+
v-for="section in detailSections"
|
|
184
|
+
:key="section.title"
|
|
185
|
+
class="detail-card"
|
|
186
|
+
:aria-label="section.title"
|
|
187
|
+
>
|
|
188
|
+
<h2>{{ section.title }}</h2>
|
|
189
|
+
<dl class="detail-list">
|
|
190
|
+
<div v-for="field in section.fields" :key="`${section.title}-${field.label}`">
|
|
191
|
+
<dt>{{ field.label }}</dt>
|
|
192
|
+
<dd>{{ field.value }}</dd>
|
|
193
|
+
</div>
|
|
194
|
+
</dl>
|
|
195
|
+
</section>
|
|
196
|
+
</template>
|
|
197
|
+
|
|
198
|
+
<van-empty v-else image="error" :description="loadError ? '详情加载失败' : '详情不存在'">
|
|
199
|
+
<van-button v-if="loadError" round type="danger" @click="loadDetail">重试</van-button>
|
|
200
|
+
<van-button v-else round type="danger" @click="router.replace('/foreign-position')">
|
|
201
|
+
返回列表
|
|
202
|
+
</van-button>
|
|
203
|
+
</van-empty>
|
|
204
|
+
</main>
|
|
205
|
+
</template>
|
|
206
|
+
|
|
207
|
+
<style scoped>
|
|
208
|
+
.detail-page {
|
|
209
|
+
width: 100%;
|
|
210
|
+
max-width: 750px;
|
|
211
|
+
min-height: var(--app-page-min-height, 100vh);
|
|
212
|
+
margin: 0 auto;
|
|
213
|
+
padding: 13px 13px calc(env(safe-area-inset-bottom, 0px) + 24px);
|
|
214
|
+
color: #080d1f;
|
|
215
|
+
background:
|
|
216
|
+
radial-gradient(circle at 50% 0%, rgba(255, 242, 243, 0.92), transparent 118px),
|
|
217
|
+
linear-gradient(180deg, #ffffff 0%, #fbfcfd 54%, #ffffff 100%);
|
|
218
|
+
box-sizing: border-box;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
button {
|
|
222
|
+
border: 0;
|
|
223
|
+
padding: 0;
|
|
224
|
+
background: transparent;
|
|
225
|
+
appearance: none;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.state-section {
|
|
229
|
+
display: flex;
|
|
230
|
+
justify-content: center;
|
|
231
|
+
padding-top: 120px;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.hero-card {
|
|
235
|
+
margin-top: 15px;
|
|
236
|
+
border-radius: 13px;
|
|
237
|
+
padding: 16px;
|
|
238
|
+
color: #ffffff;
|
|
239
|
+
background: linear-gradient(135deg, #f22f3d 0%, #ff5660 100%);
|
|
240
|
+
box-shadow: 0 13px 28px rgba(244, 29, 48, 0.18);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.hero-topline {
|
|
244
|
+
display: flex;
|
|
245
|
+
align-items: center;
|
|
246
|
+
justify-content: space-between;
|
|
247
|
+
gap: 10px;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.hero-card strong {
|
|
251
|
+
display: block;
|
|
252
|
+
margin-top: 18px;
|
|
253
|
+
font-size: 27px;
|
|
254
|
+
font-weight: 800;
|
|
255
|
+
line-height: 1.1;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.hero-amount-yuan {
|
|
259
|
+
display: block;
|
|
260
|
+
margin-top: 7px;
|
|
261
|
+
overflow-wrap: anywhere;
|
|
262
|
+
color: rgba(255, 255, 255, 0.74);
|
|
263
|
+
font-size: 12px;
|
|
264
|
+
font-weight: 500;
|
|
265
|
+
line-height: 1.35;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.hero-card p {
|
|
269
|
+
overflow: hidden;
|
|
270
|
+
margin: 10px 0 0;
|
|
271
|
+
color: rgba(255, 255, 255, 0.82);
|
|
272
|
+
font-size: 12px;
|
|
273
|
+
line-height: 1.3;
|
|
274
|
+
text-overflow: ellipsis;
|
|
275
|
+
white-space: nowrap;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.hero-date {
|
|
279
|
+
color: rgba(255, 255, 255, 0.88);
|
|
280
|
+
font-size: 13px;
|
|
281
|
+
font-weight: 650;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.detail-card {
|
|
285
|
+
margin-top: 10px;
|
|
286
|
+
border-radius: 13px;
|
|
287
|
+
padding: 15px;
|
|
288
|
+
background: rgba(255, 255, 255, 0.96);
|
|
289
|
+
box-shadow: 0 7px 22px rgba(21, 28, 45, 0.055);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.detail-card h2 {
|
|
293
|
+
margin: 0;
|
|
294
|
+
color: #111728;
|
|
295
|
+
font-size: 15px;
|
|
296
|
+
font-weight: 760;
|
|
297
|
+
line-height: 1.2;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.detail-list {
|
|
301
|
+
margin: 12px 0 0;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.detail-list div {
|
|
305
|
+
display: grid;
|
|
306
|
+
grid-template-columns: 96px minmax(0, 1fr);
|
|
307
|
+
gap: 12px;
|
|
308
|
+
padding: 10px 0;
|
|
309
|
+
border-top: 1px solid #eef0f4;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.detail-list div:first-child {
|
|
313
|
+
border-top: 0;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.detail-list dt,
|
|
317
|
+
.detail-list dd {
|
|
318
|
+
margin: 0;
|
|
319
|
+
font-size: 13px;
|
|
320
|
+
line-height: 1.42;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.detail-list dt {
|
|
324
|
+
color: #7f8798;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
.detail-list dd {
|
|
328
|
+
overflow-wrap: anywhere;
|
|
329
|
+
color: #151827;
|
|
330
|
+
font-weight: 650;
|
|
331
|
+
text-align: right;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.status-tag {
|
|
335
|
+
display: inline-flex;
|
|
336
|
+
align-items: center;
|
|
337
|
+
justify-content: center;
|
|
338
|
+
min-height: 20px;
|
|
339
|
+
border: 1px solid transparent;
|
|
340
|
+
border-radius: 4px;
|
|
341
|
+
padding: 2px 7px;
|
|
342
|
+
font-size: 11px;
|
|
343
|
+
font-weight: 650;
|
|
344
|
+
line-height: 1.25;
|
|
345
|
+
white-space: nowrap;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.status-tag-confirmed {
|
|
349
|
+
border-color: #d8e1ee;
|
|
350
|
+
color: #687083;
|
|
351
|
+
background: #ffffff;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.status-tag-pending {
|
|
355
|
+
border-color: #ffd99a;
|
|
356
|
+
color: #a05a00;
|
|
357
|
+
background: #fff4d8;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.status-tag-cancelled {
|
|
361
|
+
border-color: #d6dbe3;
|
|
362
|
+
color: #687083;
|
|
363
|
+
background: #f3f5f8;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
.status-tag-verified {
|
|
367
|
+
border-color: #b7ebc9;
|
|
368
|
+
color: #0b8f3a;
|
|
369
|
+
background: #e4f8eb;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
.status-tag-rejected {
|
|
373
|
+
border-color: #ffc0cb;
|
|
374
|
+
color: #bf2549;
|
|
375
|
+
background: #ffe8ed;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
.status-tag-unknown {
|
|
379
|
+
border-color: #d6dbe3;
|
|
380
|
+
color: #687083;
|
|
381
|
+
background: #f3f5f8;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
@media (max-width: 340px) {
|
|
385
|
+
.detail-list div {
|
|
386
|
+
grid-template-columns: 84px minmax(0, 1fr);
|
|
387
|
+
gap: 8px;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
</style>
|
|
@@ -14,22 +14,46 @@ type ResultItem = {
|
|
|
14
14
|
icon: string
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
type AmountUnitKey = 'hundredMillion' | 'tenMillion' | 'million' | 'tenThousand' | 'yuan'
|
|
18
|
+
|
|
19
|
+
type AmountUnit = {
|
|
20
|
+
key: AmountUnitKey
|
|
21
|
+
label: string
|
|
22
|
+
divisor: bigint
|
|
23
|
+
}
|
|
24
|
+
|
|
17
25
|
const estimateDetail = ref<OpeningReserveEstimateBody | null>(null)
|
|
18
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
|
+
]
|
|
19
38
|
|
|
20
39
|
const resultConfig: ResultItem[] = [
|
|
21
|
-
{ key: 'rhAmt', label: '
|
|
22
|
-
{ key: 'unverifyAmtOut', label: '
|
|
23
|
-
{ key: 'unverifyAmtIn', label: '
|
|
24
|
-
{ key: 'pdAmt', label: '
|
|
25
|
-
{ key: 'fzAmt', label: '
|
|
26
|
-
{ key: 'preAmt', label: '
|
|
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' },
|
|
27
46
|
]
|
|
28
47
|
|
|
48
|
+
const selectedUnit = computed(
|
|
49
|
+
() => amountUnits.find((unit) => unit.key === selectedUnitKey.value) || defaultAmountUnit,
|
|
50
|
+
)
|
|
51
|
+
|
|
29
52
|
const resultItems = computed(() =>
|
|
30
53
|
resultConfig.map((item) => ({
|
|
31
54
|
...item,
|
|
32
|
-
|
|
55
|
+
label: `${item.label}(${selectedUnit.value.label})`,
|
|
56
|
+
value: formatAmountByUnit(estimateDetail.value?.[item.key], selectedUnit.value),
|
|
33
57
|
})),
|
|
34
58
|
)
|
|
35
59
|
|
|
@@ -62,64 +86,65 @@ async function handleQuery(options: { showSuccessToast?: boolean } = {}) {
|
|
|
62
86
|
}
|
|
63
87
|
}
|
|
64
88
|
|
|
65
|
-
function
|
|
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) {
|
|
66
98
|
const text = value?.trim()
|
|
67
99
|
|
|
68
|
-
if (!text) return
|
|
100
|
+
if (!text) return null
|
|
69
101
|
|
|
70
102
|
const normalizedText = text.replaceAll(',', '')
|
|
71
103
|
|
|
72
104
|
if (!/^[-+]?\d+(\.\d+)?$/.test(normalizedText)) {
|
|
73
|
-
return
|
|
105
|
+
return null
|
|
74
106
|
}
|
|
75
107
|
|
|
76
|
-
const
|
|
108
|
+
const isNegative = normalizedText.startsWith('-')
|
|
77
109
|
const unsignedText =
|
|
78
110
|
normalizedText.startsWith('-') || normalizedText.startsWith('+')
|
|
79
111
|
? normalizedText.slice(1)
|
|
80
112
|
: normalizedText
|
|
81
113
|
const [rawInteger = '0', rawDecimal = ''] = unsignedText.split('.')
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
89
122
|
}
|
|
90
123
|
|
|
91
|
-
return
|
|
124
|
+
return isNegative ? -cents : cents
|
|
92
125
|
}
|
|
93
126
|
|
|
94
|
-
function
|
|
95
|
-
const
|
|
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
|
|
96
132
|
|
|
97
|
-
if (
|
|
98
|
-
|
|
99
|
-
integer,
|
|
100
|
-
decimal: String(centAmount).padStart(2, '0'),
|
|
101
|
-
}
|
|
133
|
+
if (remainder * 2n >= divisor) {
|
|
134
|
+
quotient += 1n
|
|
102
135
|
}
|
|
103
136
|
|
|
104
|
-
return
|
|
105
|
-
integer: incrementIntegerText(integer),
|
|
106
|
-
decimal: '00',
|
|
107
|
-
}
|
|
137
|
+
return isNegative ? -quotient : quotient
|
|
108
138
|
}
|
|
109
139
|
|
|
110
|
-
function
|
|
111
|
-
const
|
|
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, ',')
|
|
112
146
|
|
|
113
|
-
|
|
114
|
-
if (digits[index] !== '9') {
|
|
115
|
-
digits[index] = String(Number(digits[index]) + 1)
|
|
116
|
-
return digits.join('')
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
digits[index] = '0'
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return `1${digits.join('')}`
|
|
147
|
+
return `${isNegative ? '-' : ''}${groupedInteger}.${decimal}`
|
|
123
148
|
}
|
|
124
149
|
|
|
125
150
|
function getErrorMessage(error: unknown, fallback: string) {
|
|
@@ -131,8 +156,21 @@ function getErrorMessage(error: unknown, fallback: string) {
|
|
|
131
156
|
<main class="opening-reserve-page">
|
|
132
157
|
<section class="result-section" aria-label="日初备款数匡算查询结果">
|
|
133
158
|
<div class="section-heading">
|
|
134
|
-
<
|
|
135
|
-
|
|
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>
|
|
136
174
|
</div>
|
|
137
175
|
|
|
138
176
|
<div class="result-stack">
|
|
@@ -185,9 +223,17 @@ button {
|
|
|
185
223
|
.section-heading {
|
|
186
224
|
display: flex;
|
|
187
225
|
align-items: center;
|
|
226
|
+
justify-content: space-between;
|
|
227
|
+
gap: 12px;
|
|
188
228
|
margin-bottom: 21px;
|
|
189
229
|
}
|
|
190
230
|
|
|
231
|
+
.section-heading__title {
|
|
232
|
+
display: flex;
|
|
233
|
+
align-items: center;
|
|
234
|
+
min-width: 0;
|
|
235
|
+
}
|
|
236
|
+
|
|
191
237
|
.section-heading__mark {
|
|
192
238
|
width: 7px;
|
|
193
239
|
height: 24px;
|
|
@@ -206,6 +252,51 @@ button {
|
|
|
206
252
|
letter-spacing: 0;
|
|
207
253
|
}
|
|
208
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
|
+
|
|
209
300
|
.result-stack {
|
|
210
301
|
display: block;
|
|
211
302
|
}
|