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.
@@ -1,26 +1,1223 @@
1
+ <script setup lang="ts">
2
+ import {
3
+ buildForeignForecastQueryPayload,
4
+ queryForeignForecastPage,
5
+ type ForeignForecastQueryForm,
6
+ type ForeignForecastRecord,
7
+ } from '@/api/foreign-position'
8
+ import { useAppStore } from '@/stores/app'
9
+ import {
10
+ allForeignBusinessOption,
11
+ allForeignStatusOption,
12
+ foreignBusinessOptions,
13
+ foreignCurrencyOptions,
14
+ foreignStatusOptions,
15
+ formatForeignForecastAmount,
16
+ formatForeignForecastDate,
17
+ resolveForeignBusinessType,
18
+ resolveForeignCurrency,
19
+ resolveForeignStatus,
20
+ toTenThousandAmount,
21
+ type ForeignOption,
22
+ type ForeignStatusOption,
23
+ } from '@/utils/foreign-forecast'
24
+ import { showAppToast } from '@/utils/toast'
25
+
26
+ defineOptions({ name: 'ForeignPositionPage' })
27
+
28
+ type Forecast = {
29
+ id: number
30
+ institution: string
31
+ date: string
32
+ currency: string
33
+ currencyUnit: string
34
+ business: string
35
+ counterparty: string
36
+ amount: number
37
+ predRefNo: string
38
+ status: string
39
+ statusClass: string
40
+ }
41
+
42
+ type PendingForecast = {
43
+ institution: string
44
+ date: string
45
+ currency: string
46
+ business: string
47
+ counterparty: string
48
+ amount: number
49
+ }
50
+
51
+ type SelectorType = 'currency' | 'business' | 'status'
52
+
53
+ type SelectorOption = ForeignOption | ForeignStatusOption
54
+
55
+ const router = useRouter()
56
+ const appStore = useAppStore()
57
+ const defaultTranDate = appStore.systemWorkDate
58
+ const heroImage = `${import.meta.env.BASE_URL}images/12a73787-86a9-4891-a65f-66104746f6a8.png`
59
+ const pageSize = 10
60
+ const backTopThreshold = 520
61
+ const pendingForecastKey = 'foreign-position:pending-create'
62
+
63
+ const queryForm = reactive({
64
+ institution: '',
65
+ currency: '',
66
+ business: '',
67
+ status: '',
68
+ minAmount: '',
69
+ maxAmount: '',
70
+ })
71
+
72
+ const selectedDate = ref(defaultTranDate)
73
+ const showDatePicker = ref(false)
74
+ const datePickerValue = ref(selectedDate.value.split('-'))
75
+ const minDate = new Date(2020, 0, 1)
76
+ const maxDate = new Date(2030, 11, 31)
77
+ const isFilterExpanded = ref(true)
78
+
79
+ const forecastList = ref<Forecast[]>([])
80
+ const forecastTotal = ref(0)
81
+ const currentPage = ref(1)
82
+ const listLoading = ref(false)
83
+ const listFinished = ref(false)
84
+ const listError = ref(false)
85
+ const requesting = ref(false)
86
+ const showBackTop = ref(false)
87
+ const selectorType = ref<SelectorType>('currency')
88
+ const showSelector = ref(false)
89
+
90
+ const selectedCurrency = computed(() => resolveForeignCurrency(queryForm.currency))
91
+ const selectedBusiness = computed(
92
+ () =>
93
+ foreignBusinessOptions.find((item) => item.value === queryForm.business) ||
94
+ allForeignBusinessOption,
95
+ )
96
+ const selectedStatus = computed(
97
+ () =>
98
+ foreignStatusOptions.find((item) => item.value === queryForm.status) || allForeignStatusOption,
99
+ )
100
+ const selectorTitle = computed(() => {
101
+ const titleMap: Record<SelectorType, string> = {
102
+ currency: '选择币种',
103
+ business: '选择业务类型',
104
+ status: '选择预报状态',
105
+ }
106
+
107
+ return titleMap[selectorType.value]
108
+ })
109
+ const selectorOptions = computed<SelectorOption[]>(() => {
110
+ if (selectorType.value === 'currency') return foreignCurrencyOptions
111
+ if (selectorType.value === 'business') return foreignBusinessOptions
112
+
113
+ return foreignStatusOptions
114
+ })
115
+ const filterSummary = computed(() => {
116
+ const institution = queryForm.institution.trim() || '全部机构'
117
+ const currency = selectedCurrency.value.shortLabel
118
+
119
+ return `${selectedDate.value} · ${institution} · ${currency}`
120
+ })
121
+
122
+ onMounted(async () => {
123
+ updateBackTopVisibility()
124
+ window.addEventListener('scroll', updateBackTopVisibility, { passive: true })
125
+ await resetAndLoad()
126
+ consumePendingForecast()
127
+ })
128
+
129
+ onUnmounted(() => {
130
+ window.removeEventListener('scroll', updateBackTopVisibility)
131
+ })
132
+
133
+ function mapForeignRecordToForecast(record: ForeignForecastRecord, index: number): Forecast {
134
+ const currencyValue = getRecordCurrency(record)
135
+ const currency = resolveForeignCurrency(currencyValue)
136
+ const status = resolveForeignStatus(record.predStatus)
137
+ const counterparty =
138
+ record.competName || record.clientName || record.bankSwift || record.swiftCode || '-'
139
+
140
+ return {
141
+ id: index + 1,
142
+ institution: record.groundBranch || '未填写机构',
143
+ date: formatForeignForecastDate(record.tranDate),
144
+ currency: currency.shortLabel,
145
+ currencyUnit: currency.unit,
146
+ business: resolveForeignBusinessType(record.busiType),
147
+ counterparty,
148
+ amount: toTenThousandAmount(record.predAmt),
149
+ predRefNo: record.predRefNo || '',
150
+ status: status.label,
151
+ statusClass: status.className,
152
+ }
153
+ }
154
+
155
+ function buildQueryForm(): ForeignForecastQueryForm {
156
+ return {
157
+ tranDate: selectedDate.value,
158
+ groundBranch: queryForm.institution.trim(),
159
+ currency: queryForm.currency,
160
+ busiType: queryForm.business,
161
+ predStatus: queryForm.status,
162
+ startTranAmt: queryForm.minAmount,
163
+ endTranAmt: queryForm.maxAmount,
164
+ }
165
+ }
166
+
167
+ function parseAmount(value: string) {
168
+ if (!value.trim()) return undefined
169
+
170
+ const amount = Number(value)
171
+ return Number.isFinite(amount) ? amount : undefined
172
+ }
173
+
174
+ function updateBackTopVisibility() {
175
+ showBackTop.value = window.scrollY > backTopThreshold
176
+ }
177
+
178
+ function scrollToTop() {
179
+ showBackTop.value = false
180
+ window.scrollTo({
181
+ top: 0,
182
+ behavior: 'smooth',
183
+ })
184
+ }
185
+
186
+ function openCreatePage() {
187
+ router.push({
188
+ name: 'ForeignPositionCreate',
189
+ query: { tranDate: selectedDate.value },
190
+ })
191
+ }
192
+
193
+ function openSelector(type: SelectorType) {
194
+ selectorType.value = type
195
+ showSelector.value = true
196
+ }
197
+
198
+ function chooseSelectorOption(option: SelectorOption) {
199
+ if (selectorType.value === 'currency') {
200
+ queryForm.currency = option.value
201
+ } else if (selectorType.value === 'business') {
202
+ queryForm.business = option.value
203
+ } else {
204
+ queryForm.status = option.value
205
+ }
206
+
207
+ showSelector.value = false
208
+ }
209
+
210
+ async function handleSearch() {
211
+ const minAmount = parseAmount(queryForm.minAmount)
212
+ const maxAmount = parseAmount(queryForm.maxAmount)
213
+
214
+ if (
215
+ (queryForm.minAmount && minAmount === undefined) ||
216
+ (queryForm.maxAmount && maxAmount === undefined)
217
+ ) {
218
+ showAppToast('请输入正确的金额')
219
+ return
220
+ }
221
+
222
+ if (minAmount !== undefined && maxAmount !== undefined && minAmount > maxAmount) {
223
+ showAppToast('下限金额不能大于上限金额')
224
+ return
225
+ }
226
+
227
+ await resetAndLoad()
228
+
229
+ if (!listError.value) {
230
+ showAppToast(`查询到 ${forecastTotal.value} 笔预报`)
231
+ }
232
+ }
233
+
234
+ async function handleReset() {
235
+ selectedDate.value = defaultTranDate
236
+ datePickerValue.value = selectedDate.value.split('-')
237
+ queryForm.institution = ''
238
+ queryForm.currency = ''
239
+ queryForm.business = ''
240
+ queryForm.status = ''
241
+ queryForm.minAmount = ''
242
+ queryForm.maxAmount = ''
243
+
244
+ await resetAndLoad()
245
+
246
+ if (!listError.value) {
247
+ showAppToast('已重置筛选条件')
248
+ }
249
+ }
250
+
251
+ async function resetAndLoad() {
252
+ currentPage.value = 1
253
+ forecastList.value = []
254
+ forecastTotal.value = 0
255
+ listFinished.value = false
256
+ listError.value = false
257
+
258
+ await loadNextPage()
259
+ }
260
+
261
+ async function loadNextPage() {
262
+ if (requesting.value || listFinished.value) {
263
+ listLoading.value = false
264
+ return
265
+ }
266
+
267
+ requesting.value = true
268
+ listLoading.value = true
269
+ listError.value = false
270
+
271
+ const payload = buildForeignForecastQueryPayload({
272
+ form: buildQueryForm(),
273
+ pageIndex: currentPage.value,
274
+ pageSize,
275
+ })
276
+
277
+ console.log('[外币预报查询入参]', payload)
278
+
279
+ try {
280
+ const response = await queryForeignForecastPage(payload)
281
+ const records = filterRecordsByCurrency(response.body.resultList)
282
+ const nextList = records.map((item, index) =>
283
+ mapForeignRecordToForecast(item, forecastList.value.length + index),
284
+ )
285
+
286
+ forecastList.value.push(...nextList)
287
+ forecastTotal.value = queryForm.currency
288
+ ? forecastList.value.length
289
+ : Number(response.body.totalCount) || forecastList.value.length
290
+ currentPage.value += 1
291
+ listFinished.value = queryForm.currency
292
+ ? response.body.resultList.length < pageSize
293
+ : forecastList.value.length >= forecastTotal.value ||
294
+ response.body.resultList.length < pageSize
295
+ } catch (error) {
296
+ console.error('[外币预报查询失败]', error)
297
+ listError.value = true
298
+ showAppToast('数据加载失败,请重试')
299
+ } finally {
300
+ requesting.value = false
301
+ listLoading.value = false
302
+ }
303
+ }
304
+
305
+ function filterRecordsByCurrency(records: ForeignForecastRecord[]) {
306
+ const currency = queryForm.currency.trim().toUpperCase()
307
+
308
+ if (!currency) return records
309
+
310
+ return records.filter((item) => getRecordCurrency(item) === currency)
311
+ }
312
+
313
+ function getRecordCurrency(record: ForeignForecastRecord) {
314
+ return (
315
+ record.ccy ||
316
+ record.currency ||
317
+ record.currencyCode ||
318
+ record.currCode ||
319
+ record.curType ||
320
+ record.tranCcy ||
321
+ ''
322
+ )
323
+ .trim()
324
+ .toUpperCase()
325
+ }
326
+
327
+ function confirmDate() {
328
+ selectedDate.value = datePickerValue.value.join('-')
329
+ showDatePicker.value = false
330
+ }
331
+
332
+ function openDatePicker() {
333
+ datePickerValue.value = selectedDate.value.split('-')
334
+ showDatePicker.value = true
335
+ }
336
+
337
+ function openDetail(item: Forecast) {
338
+ if (!item.predRefNo) {
339
+ showAppToast('暂无预报流水号')
340
+ return
341
+ }
342
+
343
+ router.push({
344
+ name: 'ForeignPositionDetail',
345
+ params: { predRefNo: item.predRefNo },
346
+ })
347
+ }
348
+
349
+ function consumePendingForecast() {
350
+ const payload = sessionStorage.getItem(pendingForecastKey)
351
+
352
+ if (!payload) return
353
+
354
+ sessionStorage.removeItem(pendingForecastKey)
355
+
356
+ try {
357
+ const forecast = JSON.parse(payload) as Partial<PendingForecast>
358
+
359
+ if (!isPendingForecast(forecast)) return
360
+
361
+ const currency = resolveForeignCurrency(forecast.currency)
362
+ const status = resolveForeignStatus('1')
363
+
364
+ forecastList.value.unshift({
365
+ id: Date.now(),
366
+ institution: forecast.institution,
367
+ date: forecast.date,
368
+ currency: currency.shortLabel,
369
+ currencyUnit: currency.unit,
370
+ business: forecast.business,
371
+ counterparty: forecast.counterparty,
372
+ amount: forecast.amount,
373
+ predRefNo: '',
374
+ status: status.label,
375
+ statusClass: status.className,
376
+ })
377
+ forecastTotal.value += 1
378
+ } catch (error) {
379
+ console.error('[外币预报新增结果解析失败]', error)
380
+ }
381
+ }
382
+
383
+ function isPendingForecast(value: Partial<PendingForecast> | null): value is PendingForecast {
384
+ return Boolean(
385
+ value &&
386
+ typeof value.institution === 'string' &&
387
+ typeof value.date === 'string' &&
388
+ typeof value.currency === 'string' &&
389
+ typeof value.business === 'string' &&
390
+ typeof value.counterparty === 'string' &&
391
+ typeof value.amount === 'number' &&
392
+ Number.isFinite(value.amount),
393
+ )
394
+ }
395
+ </script>
396
+
1
397
  <template>
2
- <main class="page">
3
- <section class="page-body">
4
- <p class="placeholder">页面建设中…</p>
398
+ <main class="foreign-page">
399
+ <section class="query-card">
400
+ <div class="hero-panel">
401
+ <div class="hero-copy">
402
+ <h2>外币预报信息查询</h2>
403
+ <p>查询外币预报及交易明细</p>
404
+ </div>
405
+ <img class="hero-image" :src="heroImage" alt="外币预报信息查询" />
406
+ </div>
5
407
  </section>
408
+
409
+ <section class="filter-sticky">
410
+ <div class="filter-panel" :class="{ 'is-collapsed': !isFilterExpanded }">
411
+ <div class="filter-toolbar">
412
+ <div class="filter-heading">
413
+ <strong>筛选条件</strong>
414
+ <span>{{ filterSummary }}</span>
415
+ </div>
416
+ <button
417
+ class="filter-toggle"
418
+ type="button"
419
+ :aria-expanded="isFilterExpanded"
420
+ @click="isFilterExpanded = !isFilterExpanded"
421
+ >
422
+ <van-icon :name="isFilterExpanded ? 'arrow-up' : 'arrow-down'" />
423
+ <span>{{ isFilterExpanded ? '收起' : '展开' }}</span>
424
+ </button>
425
+ </div>
426
+
427
+ <Transition name="filter-body">
428
+ <div v-show="isFilterExpanded" class="filter-body">
429
+ <button class="filter-row" type="button" @click="openDatePicker">
430
+ <span class="row-icon row-icon-calendar"><van-icon name="calendar-o" /></span>
431
+ <span class="row-label">起息日</span>
432
+ <span class="row-value">{{ selectedDate }}</span>
433
+ <van-icon class="row-calendar" name="calendar-o" />
434
+ <van-icon class="row-arrow" name="arrow" />
435
+ </button>
436
+
437
+ <label class="filter-row">
438
+ <span class="row-icon row-icon-bank"><van-icon name="hotel-o" /></span>
439
+ <span class="row-label">落地机构</span>
440
+ <input
441
+ v-model.trim="queryForm.institution"
442
+ class="row-input"
443
+ type="text"
444
+ inputmode="text"
445
+ placeholder="请输入落地机构关键字"
446
+ />
447
+ <van-icon class="row-arrow" name="arrow" />
448
+ </label>
449
+
450
+ <button class="filter-row" type="button" @click="openSelector('currency')">
451
+ <span class="row-icon row-icon-money"><van-icon name="gold-coin-o" /></span>
452
+ <span class="row-label">币种</span>
453
+ <span class="row-value muted-value">{{ selectedCurrency.label }}</span>
454
+ <van-icon class="row-arrow" name="arrow" />
455
+ </button>
456
+
457
+ <button class="filter-row" type="button" @click="openSelector('business')">
458
+ <span class="row-icon row-icon-briefcase"><van-icon name="bag-o" /></span>
459
+ <span class="row-label">业务类型</span>
460
+ <span class="row-value muted-value">{{ selectedBusiness.label }}</span>
461
+ <van-icon class="row-arrow" name="arrow" />
462
+ </button>
463
+
464
+ <button class="filter-row" type="button" @click="openSelector('status')">
465
+ <span class="row-icon row-icon-status"><van-icon name="orders-o" /></span>
466
+ <span class="row-label">预报状态</span>
467
+ <span class="row-value muted-value">{{ selectedStatus.label }}</span>
468
+ <van-icon class="row-arrow" name="arrow" />
469
+ </button>
470
+
471
+ <div class="filter-row amount-row">
472
+ <span class="row-icon row-icon-money"><van-icon name="gold-coin-o" /></span>
473
+ <span class="row-label">金额区间</span>
474
+ <input
475
+ v-model.trim="queryForm.minAmount"
476
+ class="amount-input"
477
+ type="number"
478
+ inputmode="decimal"
479
+ placeholder="请输入下限金额"
480
+ />
481
+ <span class="amount-divider">-</span>
482
+ <input
483
+ v-model.trim="queryForm.maxAmount"
484
+ class="amount-input"
485
+ type="number"
486
+ inputmode="decimal"
487
+ placeholder="请输入上限金额"
488
+ />
489
+ </div>
490
+
491
+ <div class="filter-actions">
492
+ <button class="reset-btn" type="button" @click="handleReset">
493
+ <van-icon name="replay" />
494
+ <span>重置</span>
495
+ </button>
496
+ <button class="search-btn" type="button" @click="handleSearch">
497
+ <van-icon name="search" />
498
+ <span>查询</span>
499
+ </button>
500
+ </div>
501
+ </div>
502
+ </Transition>
503
+ </div>
504
+ </section>
505
+
506
+ <section class="forecast-card" aria-label="预报列表">
507
+ <van-list
508
+ v-model:loading="listLoading"
509
+ v-model:error="listError"
510
+ :finished="listFinished"
511
+ :immediate-check="false"
512
+ error-text="加载失败,点击重试"
513
+ finished-text="没有更多了"
514
+ loading-text="加载中..."
515
+ @load="loadNextPage"
516
+ >
517
+ <button
518
+ v-for="item in forecastList"
519
+ :key="item.id"
520
+ class="forecast-item"
521
+ type="button"
522
+ @click="openDetail(item)"
523
+ >
524
+ <span class="forecast-avatar"><van-icon name="shop-o" /></span>
525
+ <span class="forecast-main">
526
+ <strong>{{ item.institution }}</strong>
527
+ <span class="forecast-date">{{ item.date }}</span>
528
+ <span class="forecast-business">
529
+ {{ item.business }} | {{ item.currency }} | {{ item.counterparty }}
530
+ </span>
531
+ </span>
532
+ <span class="forecast-side">
533
+ <span class="forecast-amount">
534
+ {{ formatForeignForecastAmount(item.amount) }} <em>{{ item.currencyUnit }}</em>
535
+ </span>
536
+ <span class="status-tag" :class="item.statusClass">{{ item.status }}</span>
537
+ </span>
538
+ <van-icon class="forecast-arrow" name="arrow" />
539
+ </button>
540
+
541
+ <van-empty
542
+ v-if="!listLoading && forecastList.length === 0"
543
+ image-size="72"
544
+ description="暂无外币预报信息"
545
+ />
546
+ </van-list>
547
+ </section>
548
+
549
+ <button class="fab" type="button" aria-label="新增外币预报" @click="openCreatePage">
550
+ <van-icon name="plus" />
551
+ </button>
552
+
553
+ <Transition name="back-top">
554
+ <button
555
+ v-show="showBackTop"
556
+ class="back-top-btn"
557
+ type="button"
558
+ aria-label="返回顶部"
559
+ @click="scrollToTop"
560
+ >
561
+ <van-icon name="arrow-up" />
562
+ </button>
563
+ </Transition>
564
+
565
+ <van-popup v-model:show="showDatePicker" position="bottom" round safe-area-inset-bottom>
566
+ <van-date-picker
567
+ v-model="datePickerValue"
568
+ title="选择起息日"
569
+ :min-date="minDate"
570
+ :max-date="maxDate"
571
+ @cancel="showDatePicker = false"
572
+ @confirm="confirmDate"
573
+ />
574
+ </van-popup>
575
+
576
+ <van-popup v-model:show="showSelector" position="bottom" round safe-area-inset-bottom>
577
+ <section class="selector-panel">
578
+ <header class="selector-header">
579
+ <button type="button" @click="showSelector = false">取消</button>
580
+ <strong>{{ selectorTitle }}</strong>
581
+ <span></span>
582
+ </header>
583
+ <button
584
+ v-for="item in selectorOptions"
585
+ :key="item.value || item.label"
586
+ class="selector-option"
587
+ type="button"
588
+ @click="chooseSelectorOption(item)"
589
+ >
590
+ <span>{{ item.label }}</span>
591
+ <van-icon
592
+ v-if="
593
+ (selectorType === 'currency' && item.value === queryForm.currency) ||
594
+ (selectorType === 'business' && item.value === queryForm.business) ||
595
+ (selectorType === 'status' && item.value === queryForm.status)
596
+ "
597
+ name="success"
598
+ />
599
+ </button>
600
+ </section>
601
+ </van-popup>
6
602
  </main>
7
603
  </template>
8
604
 
9
605
  <style scoped>
10
- .page {
606
+ .foreign-page {
607
+ position: relative;
608
+ width: 100%;
11
609
  max-width: 750px;
12
610
  min-height: var(--app-page-min-height, 100vh);
13
611
  margin: 0 auto;
14
- padding: 0 11px;
15
- background: #f7f8fa;
612
+ padding: 13px 13px calc(env(safe-area-inset-bottom, 0px) + 118px);
613
+ color: #080d1f;
614
+ background:
615
+ radial-gradient(circle at 50% 0%, rgba(255, 242, 243, 0.92), transparent 118px),
616
+ linear-gradient(180deg, #ffffff 0%, #fbfcfd 54%, #ffffff 100%);
617
+ box-sizing: border-box;
618
+ }
619
+
620
+ button,
621
+ input {
622
+ font: inherit;
623
+ }
624
+
625
+ button {
626
+ border: 0;
627
+ padding: 0;
628
+ background: transparent;
629
+ appearance: none;
630
+ }
631
+
632
+ .query-card {
633
+ overflow: hidden;
634
+ border-radius: 13px;
635
+ box-shadow: 0 10px 24px rgba(31, 41, 55, 0.06);
636
+ }
637
+
638
+ .hero-panel {
639
+ position: relative;
640
+ height: 143px;
641
+ overflow: hidden;
642
+ border-radius: 13px 13px 0 0;
643
+ background:
644
+ radial-gradient(circle at 72% 80%, rgba(255, 255, 255, 0.16), transparent 74px),
645
+ radial-gradient(circle at 92% 14%, rgba(255, 255, 255, 0.13), transparent 58px),
646
+ linear-gradient(135deg, #ff2736 0%, #ff3a42 42%, #ff454b 100%);
647
+ }
648
+
649
+ .hero-panel::after {
650
+ position: absolute;
651
+ right: 15px;
652
+ bottom: 14px;
653
+ width: 26px;
654
+ height: 64px;
655
+ border-radius: 3px;
656
+ background: rgba(255, 137, 137, 0.34);
657
+ box-shadow: -21px 14px 0 rgba(255, 137, 137, 0.28);
658
+ content: '';
659
+ }
660
+
661
+ .hero-copy {
662
+ position: relative;
663
+ z-index: 2;
664
+ padding: 32px 0 0 15px;
665
+ color: #ffffff;
666
+ }
667
+
668
+ .hero-copy h2 {
669
+ margin: 0;
670
+ font-size: 22px;
671
+ font-weight: 800;
672
+ line-height: 1.2;
673
+ }
674
+
675
+ .hero-copy p {
676
+ margin: 18px 0 0;
677
+ font-size: 13px;
678
+ font-weight: 600;
679
+ line-height: 1.2;
680
+ }
681
+
682
+ .hero-image {
683
+ position: absolute;
684
+ right: 9px;
685
+ bottom: -8px;
686
+ z-index: 1;
687
+ width: 132px;
688
+ height: 132px;
689
+ object-fit: contain;
690
+ filter: drop-shadow(0 12px 20px rgba(165, 0, 18, 0.16));
691
+ }
692
+
693
+ .filter-sticky {
694
+ position: sticky;
695
+ top: calc(50px + env(safe-area-inset-top, 0px));
696
+ z-index: 15;
697
+ margin-top: -18px;
698
+ }
699
+
700
+ .filter-panel {
701
+ position: relative;
702
+ z-index: 1;
703
+ padding: 12px 13px 14px;
704
+ border-radius: 13px;
705
+ background: #ffffff;
706
+ box-shadow: 0 -4px 18px rgba(255, 66, 76, 0.06);
707
+ }
708
+
709
+ .filter-panel.is-collapsed {
710
+ padding-bottom: 12px;
711
+ }
712
+
713
+ .filter-toolbar {
714
+ display: grid;
715
+ grid-template-columns: minmax(0, 1fr) 72px;
716
+ column-gap: 10px;
717
+ align-items: center;
718
+ }
719
+
720
+ .filter-heading {
721
+ min-width: 0;
722
+ }
723
+
724
+ .filter-heading strong,
725
+ .filter-heading span {
726
+ display: block;
727
+ overflow: hidden;
728
+ text-overflow: ellipsis;
729
+ white-space: nowrap;
730
+ }
731
+
732
+ .filter-heading strong {
733
+ color: #101525;
734
+ font-size: 14px;
735
+ font-weight: 750;
736
+ line-height: 1.25;
737
+ }
738
+
739
+ .filter-heading span {
740
+ margin-top: 3px;
741
+ color: #818899;
742
+ font-size: 11px;
743
+ line-height: 1.25;
744
+ }
745
+
746
+ .filter-toggle {
747
+ display: inline-flex;
748
+ align-items: center;
749
+ justify-content: center;
750
+ gap: 4px;
751
+ height: 30px;
752
+ border: 1px solid #ffd5da;
753
+ border-radius: 15px;
754
+ color: #f42b3a;
755
+ font-size: 12px;
756
+ font-weight: 650;
757
+ background: #fff7f8;
758
+ }
759
+
760
+ .filter-toggle .van-icon {
761
+ font-size: 14px;
762
+ }
763
+
764
+ .filter-body {
765
+ overflow: hidden;
766
+ margin-top: 8px;
767
+ border-top: 1px solid #eef0f4;
768
+ }
769
+
770
+ .filter-body-enter-active,
771
+ .filter-body-leave-active {
772
+ transition:
773
+ opacity 0.16s ease,
774
+ transform 0.16s ease;
775
+ }
776
+
777
+ .filter-body-enter-from,
778
+ .filter-body-leave-to {
779
+ opacity: 0;
780
+ transform: translateY(-6px);
781
+ }
782
+
783
+ .filter-row {
784
+ display: flex;
785
+ align-items: center;
786
+ width: 100%;
787
+ min-height: 48px;
788
+ color: #080d1f;
789
+ text-align: left;
790
+ box-sizing: border-box;
791
+ }
792
+
793
+ .filter-row + .filter-row {
794
+ border-top: 1px solid #eef0f4;
795
+ }
796
+
797
+ .row-icon {
798
+ display: flex;
799
+ align-items: center;
800
+ justify-content: center;
801
+ width: 24px;
802
+ height: 24px;
803
+ flex: 0 0 24px;
804
+ border-radius: 6px;
805
+ color: #f42b3a;
806
+ font-size: 16px;
807
+ background: linear-gradient(135deg, #fff2f3 0%, #ffe4e6 100%);
808
+ }
809
+
810
+ .row-label {
811
+ width: 92px;
812
+ flex: 0 0 92px;
813
+ margin-left: 11px;
814
+ color: #151827;
815
+ font-size: 14px;
816
+ font-weight: 650;
817
+ line-height: 1.2;
818
+ }
819
+
820
+ .row-value {
821
+ overflow: hidden;
822
+ margin-left: auto;
823
+ color: #202638;
824
+ font-size: 14px;
825
+ line-height: 1.2;
826
+ text-align: right;
827
+ text-overflow: ellipsis;
828
+ white-space: nowrap;
829
+ }
830
+
831
+ .muted-value {
832
+ color: #8d95a6;
833
+ }
834
+
835
+ .row-input {
836
+ width: 0;
837
+ min-width: 0;
838
+ flex: 1;
839
+ border: 0;
840
+ outline: 0;
841
+ color: #202638;
842
+ font-size: 13px;
843
+ text-align: right;
844
+ background: transparent;
845
+ }
846
+
847
+ .row-input::placeholder,
848
+ .amount-input::placeholder {
849
+ color: #a6adbd;
850
+ }
851
+
852
+ .amount-input::placeholder {
853
+ font-size: 10px;
854
+ }
855
+
856
+ .row-calendar {
857
+ margin-left: 12px;
858
+ color: #a7adba;
859
+ font-size: 16px;
860
+ }
861
+
862
+ .row-arrow {
863
+ margin-left: 12px;
864
+ color: #9fa6b5;
865
+ font-size: 15px;
866
+ }
867
+
868
+ .amount-row {
869
+ gap: 0;
870
+ min-height: 52px;
871
+ }
872
+
873
+ .amount-row .row-label {
874
+ width: 58px;
875
+ flex-basis: 58px;
876
+ margin-right: 8px;
877
+ }
878
+
879
+ .amount-input {
880
+ width: 0;
881
+ min-width: 0;
882
+ flex: 1 1 0;
883
+ height: 28px;
884
+ border: 1px solid #e5e8ef;
885
+ border-radius: 6px;
886
+ padding: 0 4px;
887
+ color: #202638;
888
+ font-size: 11px;
889
+ text-align: center;
890
+ outline: 0;
891
+ background: #ffffff;
892
+ box-sizing: border-box;
893
+ }
894
+
895
+ .amount-divider {
896
+ flex: 0 0 18px;
897
+ color: #8c94a5;
898
+ font-size: 13px;
899
+ text-align: center;
900
+ }
901
+
902
+ .filter-actions {
903
+ display: grid;
904
+ grid-template-columns: 92px minmax(0, 1fr);
905
+ gap: 10px;
906
+ margin-top: 12px;
907
+ }
908
+
909
+ .reset-btn,
910
+ .search-btn {
911
+ display: flex;
912
+ align-items: center;
913
+ justify-content: center;
914
+ gap: 8px;
915
+ width: 100%;
916
+ height: 34px;
917
+ border-radius: 18px;
918
+ font-size: 14px;
919
+ font-weight: 650;
920
+ }
921
+
922
+ .reset-btn {
923
+ border: 1px solid #ffd5da;
924
+ color: #f42b3a;
925
+ background: #fff7f8;
926
+ }
927
+
928
+ .search-btn {
929
+ color: #ffffff;
930
+ background: linear-gradient(135deg, #ff3340 0%, #ff1d34 100%);
931
+ box-shadow: 0 8px 16px rgba(255, 36, 52, 0.18);
932
+ }
933
+
934
+ .reset-btn .van-icon,
935
+ .search-btn .van-icon {
936
+ font-size: 17px;
937
+ }
938
+
939
+ .forecast-card {
940
+ margin-top: 9px;
941
+ overflow: hidden;
942
+ border-radius: 13px;
943
+ background: rgba(255, 255, 255, 0.95);
944
+ box-shadow: 0 7px 22px rgba(21, 28, 45, 0.055);
945
+ }
946
+
947
+ .forecast-item {
948
+ position: relative;
949
+ display: grid;
950
+ grid-template-columns: 42px minmax(0, 1fr) auto 14px;
951
+ column-gap: 10px;
952
+ align-items: center;
953
+ width: 100%;
954
+ min-height: 91px;
955
+ padding: 14px 17px;
956
+ color: #080d1f;
957
+ text-align: left;
958
+ box-sizing: border-box;
959
+ }
960
+
961
+ .forecast-item + .forecast-item {
962
+ border-top: 1px solid #edf0f3;
963
+ }
964
+
965
+ .forecast-avatar {
966
+ display: flex;
967
+ align-items: center;
968
+ justify-content: center;
969
+ width: 36px;
970
+ height: 36px;
971
+ border-radius: 50%;
972
+ color: #111728;
973
+ font-size: 24px;
974
+ background: #fff0f1;
975
+ }
976
+
977
+ .forecast-avatar .van-icon {
978
+ transform: translateY(1px);
979
+ }
980
+
981
+ .forecast-main {
982
+ min-width: 0;
983
+ }
984
+
985
+ .forecast-main strong {
986
+ display: block;
987
+ overflow: hidden;
988
+ color: #080d1f;
989
+ font-size: 14px;
990
+ font-weight: 760;
991
+ line-height: 1.35;
992
+ text-overflow: ellipsis;
993
+ white-space: nowrap;
994
+ }
995
+
996
+ .forecast-date,
997
+ .forecast-business {
998
+ display: block;
999
+ overflow: hidden;
1000
+ margin-top: 7px;
1001
+ font-size: 12px;
1002
+ line-height: 1.2;
1003
+ text-overflow: ellipsis;
1004
+ white-space: nowrap;
1005
+ }
1006
+
1007
+ .forecast-date {
1008
+ color: #8a92a3;
1009
+ }
1010
+
1011
+ .forecast-business {
1012
+ color: #111728;
1013
+ }
1014
+
1015
+ .forecast-side {
1016
+ display: flex;
1017
+ align-items: flex-end;
1018
+ flex-direction: column;
1019
+ min-width: 100px;
1020
+ }
1021
+
1022
+ .forecast-amount {
1023
+ color: #20ad5b;
1024
+ font-size: 15px;
1025
+ font-weight: 650;
1026
+ line-height: 1.2;
1027
+ white-space: nowrap;
1028
+ }
1029
+
1030
+ .forecast-amount em {
1031
+ color: #20ad5b;
1032
+ font-size: 11px;
1033
+ font-style: normal;
1034
+ font-weight: 400;
1035
+ }
1036
+
1037
+ .status-tag {
1038
+ display: inline-flex;
1039
+ align-items: center;
1040
+ justify-content: center;
1041
+ min-height: 20px;
1042
+ max-width: 128px;
1043
+ margin-top: 10px;
1044
+ border: 1px solid transparent;
1045
+ border-radius: 4px;
1046
+ padding: 2px 7px;
1047
+ font-size: 11px;
1048
+ font-weight: 650;
1049
+ line-height: 1.25;
1050
+ text-align: right;
1051
+ white-space: nowrap;
1052
+ }
1053
+
1054
+ .status-tag-confirmed {
1055
+ border-color: #d8e1ee;
1056
+ color: #687083;
1057
+ background: #ffffff;
1058
+ }
1059
+
1060
+ .status-tag-pending {
1061
+ border-color: #ffd99a;
1062
+ color: #a05a00;
1063
+ background: #fff4d8;
1064
+ }
1065
+
1066
+ .status-tag-cancelled {
1067
+ border-color: #d6dbe3;
1068
+ color: #687083;
1069
+ background: #f3f5f8;
1070
+ }
1071
+
1072
+ .status-tag-verified {
1073
+ border-color: #b7ebc9;
1074
+ color: #0b8f3a;
1075
+ background: #e4f8eb;
16
1076
  }
17
- .page-body {
18
- padding: 24px 0;
1077
+
1078
+ .status-tag-rejected {
1079
+ border-color: #ffc0cb;
1080
+ color: #bf2549;
1081
+ background: #ffe8ed;
1082
+ }
1083
+
1084
+ .status-tag-unknown {
1085
+ border-color: #d6dbe3;
1086
+ color: #687083;
1087
+ background: #f3f5f8;
1088
+ }
1089
+
1090
+ .forecast-arrow {
1091
+ color: #9da4b3;
1092
+ font-size: 15px;
1093
+ }
1094
+
1095
+ .fab {
1096
+ position: fixed;
1097
+ right: max(25px, calc((100vw - 750px) / 2 + 25px));
1098
+ bottom: calc(env(safe-area-inset-bottom, 0px) + 35px);
1099
+ z-index: 20;
1100
+ display: flex;
1101
+ align-items: center;
1102
+ justify-content: center;
1103
+ width: 52px;
1104
+ height: 52px;
1105
+ border-radius: 50%;
1106
+ color: #ffffff;
1107
+ font-size: 31px;
1108
+ background: linear-gradient(145deg, #ff4050 0%, #f50f2d 100%);
1109
+ box-shadow: 0 13px 28px rgba(244, 29, 48, 0.34);
1110
+ }
1111
+
1112
+ .back-top-btn {
1113
+ position: fixed;
1114
+ right: max(31px, calc((100vw - 750px) / 2 + 31px));
1115
+ bottom: calc(env(safe-area-inset-bottom, 0px) + 99px);
1116
+ z-index: 20;
1117
+ display: flex;
1118
+ align-items: center;
1119
+ justify-content: center;
1120
+ width: 40px;
1121
+ height: 40px;
1122
+ border: 1px solid rgba(226, 232, 240, 0.9);
1123
+ border-radius: 50%;
1124
+ color: #f22f3d;
1125
+ font-size: 20px;
1126
+ background: rgba(255, 255, 255, 0.94);
1127
+ box-shadow: 0 10px 24px rgba(21, 28, 45, 0.13);
1128
+ backdrop-filter: blur(12px);
1129
+ }
1130
+
1131
+ .back-top-enter-active,
1132
+ .back-top-leave-active {
1133
+ transition:
1134
+ opacity 0.18s ease,
1135
+ transform 0.18s ease;
1136
+ }
1137
+
1138
+ .back-top-enter-from,
1139
+ .back-top-leave-to {
1140
+ opacity: 0;
1141
+ transform: translateY(8px) scale(0.92);
1142
+ }
1143
+
1144
+ .selector-panel {
1145
+ padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 8px);
1146
+ background: #ffffff;
1147
+ }
1148
+
1149
+ .selector-header {
1150
+ display: grid;
1151
+ grid-template-columns: 56px minmax(0, 1fr) 56px;
1152
+ align-items: center;
1153
+ height: 48px;
1154
+ border-bottom: 1px solid #eef0f4;
1155
+ padding: 0 12px;
1156
+ }
1157
+
1158
+ .selector-header button {
1159
+ color: #6f7788;
1160
+ font-size: 14px;
1161
+ text-align: left;
19
1162
  }
20
- .placeholder {
1163
+
1164
+ .selector-header strong {
1165
+ color: #111728;
1166
+ font-size: 15px;
1167
+ font-weight: 700;
21
1168
  text-align: center;
22
- color: #969799;
1169
+ }
1170
+
1171
+ .selector-option {
1172
+ display: flex;
1173
+ align-items: center;
1174
+ justify-content: space-between;
1175
+ width: 100%;
1176
+ min-height: 48px;
1177
+ border-bottom: 1px solid #f1f3f6;
1178
+ padding: 0 18px;
1179
+ color: #151827;
23
1180
  font-size: 14px;
24
- padding-top: 120px;
1181
+ text-align: left;
1182
+ box-sizing: border-box;
1183
+ }
1184
+
1185
+ .selector-option .van-icon {
1186
+ color: #f42b3a;
1187
+ font-size: 18px;
1188
+ }
1189
+
1190
+ @media (max-width: 340px) {
1191
+ .amount-row {
1192
+ gap: 5px;
1193
+ }
1194
+
1195
+ .amount-input {
1196
+ width: 0;
1197
+ padding: 0 5px;
1198
+ font-size: 11px;
1199
+ }
1200
+
1201
+ .row-label {
1202
+ width: 80px;
1203
+ flex-basis: 80px;
1204
+ margin-left: 9px;
1205
+ }
1206
+
1207
+ .amount-row .row-label {
1208
+ width: 52px;
1209
+ flex-basis: 52px;
1210
+ }
1211
+
1212
+ .forecast-item {
1213
+ grid-template-columns: 38px minmax(0, 1fr) auto 12px;
1214
+ column-gap: 8px;
1215
+ padding-right: 12px;
1216
+ padding-left: 12px;
1217
+ }
1218
+
1219
+ .forecast-side {
1220
+ min-width: 88px;
1221
+ }
25
1222
  }
26
1223
  </style>