minitest2.0 0.0.10 → 0.0.11

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minitest2.0",
3
- "version": "0.0.10",
3
+ "version": "0.0.11",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "repository": {
@@ -26,15 +26,21 @@ export type ForeignForecastRecord = {
26
26
  /** 交易对手。 */
27
27
  competName?: string
28
28
  /** 单证业务编号。 */
29
+ docBusinessno?: string
30
+ /** 兼容列表接口可能返回的旧字段名。 */
29
31
  docbusinessno?: string
30
32
  /** 落地机构。 */
31
33
  groundBranch?: string
34
+ /** 落地机构名称。 */
35
+ groundBranchName?: string
32
36
  /** 中间行 Swift Code。 */
33
37
  midBankSc?: string
34
38
  /** 结汇、购汇或现汇。 */
35
39
  originalForeign?: string
36
40
  /** 收支方向。 */
37
41
  payDirec?: string
42
+ /** 汇款路径。 */
43
+ paymentRoute?: string
38
44
  /** 联系电话。 */
39
45
  phoneNo?: string
40
46
  /** 预报金额,接口按元级金额返回,页面转换为万元级展示。 */
@@ -57,6 +63,10 @@ export type ForeignForecastRecord = {
57
63
  predWay?: string
58
64
  /** 交易对手行名称。 */
59
65
  rcvBank?: string
66
+ /** 收款账号。 */
67
+ rcvAcct?: string
68
+ /** 收款人名称。 */
69
+ rcvName?: string
60
70
  /** 交易对手行 Swift Code。 */
61
71
  rcvBankSc?: string
62
72
  /** 备注。 */
@@ -206,7 +216,7 @@ type QueryOptions = {
206
216
  }
207
217
 
208
218
  const QUERY_TRAN_CODE = '/TAM/0001/140201001'
209
- const DETAIL_TRAN_CODE = '/TAM/0001/140101002'
219
+ const DETAIL_TRAN_CODE = '/TAM/0001/140201004'
210
220
 
211
221
  export function buildForeignForecastQueryPayload({
212
222
  form,
@@ -56,6 +56,26 @@ export const foreignBusinessOptions: ForeignOption[] = [
56
56
  { label: '资金调拨', value: '13' },
57
57
  ]
58
58
 
59
+ export const foreignPayDirectionMap: Record<string, string> = {
60
+ I: '收入',
61
+ O: '支出',
62
+ }
63
+
64
+ export const foreignPaymentRouteMap: Record<string, string> = {
65
+ '01': '人行境内外币支付',
66
+ }
67
+
68
+ export const foreignClientTypeMap: Record<string, string> = {
69
+ CT01: '对公客户',
70
+ CT02: '个人客户',
71
+ }
72
+
73
+ export const foreignOriginalForeignMap: Record<string, string> = {
74
+ '01': '结汇',
75
+ '02': '购汇',
76
+ '03': '现汇',
77
+ }
78
+
59
79
  export const allForeignStatusOption: ForeignStatusOption = {
60
80
  label: '全部预报状态',
61
81
  value: '',
@@ -100,6 +120,22 @@ export function resolveForeignBusinessType(value?: string) {
100
120
  return foreignBusinessOptions.find((item) => item.value === value)?.label || value
101
121
  }
102
122
 
123
+ export function resolveForeignPayDirection(value?: string) {
124
+ return resolveForeignCode(foreignPayDirectionMap, value)
125
+ }
126
+
127
+ export function resolveForeignPaymentRoute(value?: string) {
128
+ return resolveForeignCode(foreignPaymentRouteMap, value)
129
+ }
130
+
131
+ export function resolveForeignClientType(value?: string) {
132
+ return resolveForeignCode(foreignClientTypeMap, value)
133
+ }
134
+
135
+ export function resolveForeignOriginalForeign(value?: string) {
136
+ return resolveForeignCode(foreignOriginalForeignMap, value)
137
+ }
138
+
103
139
  export function resolveForeignStatus(value?: string) {
104
140
  if (!value?.trim()) {
105
141
  return unknownForeignStatus
@@ -141,3 +177,8 @@ export function formatForeignForecastAmountYuanText(value?: string) {
141
177
  minimumFractionDigits: 2,
142
178
  })} 元`
143
179
  }
180
+
181
+ function resolveForeignCode(map: Record<string, string>, value?: string) {
182
+ const code = value?.trim()
183
+ return code ? map[code] || code : '-'
184
+ }
@@ -5,12 +5,13 @@ import {
5
5
  type ForeignForecastDetailRecord,
6
6
  } from '@/api/foreign-position'
7
7
  import {
8
- formatForeignForecastAmountText,
9
- formatForeignForecastAmountYuanText,
10
8
  formatForeignForecastDate,
11
9
  resolveForeignBusinessType,
10
+ resolveForeignClientType,
12
11
  resolveForeignCurrency,
13
- resolveForeignStatus,
12
+ resolveForeignOriginalForeign,
13
+ resolveForeignPayDirection,
14
+ resolveForeignPaymentRoute,
14
15
  } from '@/utils/foreign-forecast'
15
16
  import { showAppToast } from '@/utils/toast'
16
17
 
@@ -23,6 +24,7 @@ type DetailField = {
23
24
 
24
25
  type DetailSection = {
25
26
  title: string
27
+ icon: string
26
28
  fields: DetailField[]
27
29
  }
28
30
 
@@ -35,12 +37,11 @@ const loading = ref(false)
35
37
  const loadError = ref(false)
36
38
  const currencyValue = computed(() => getRecordCurrency(detail.value))
37
39
  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),
40
+ const heroAmount = computed(() => fieldText(detail.value?.predAmt))
41
+ const businessTypeText = computed(() =>
42
+ resolveForeignBusinessType(detail.value?.tranType || detail.value?.busiType),
41
43
  )
42
- const heroAmountYuan = computed(() => formatForeignForecastAmountYuanText(detail.value?.predAmt))
43
- const heroDate = computed(() => formatForeignForecastDate(detail.value?.tranDate))
44
+ const payDirectionText = computed(() => resolveForeignPayDirection(detail.value?.payDirec))
44
45
 
45
46
  const detailSections = computed<DetailSection[]>(() => {
46
47
  const item = detail.value
@@ -50,65 +51,63 @@ const detailSections = computed<DetailSection[]>(() => {
50
51
  return [
51
52
  {
52
53
  title: '基础信息',
54
+ icon: 'orders-o',
53
55
  fields: [
54
- { label: '预报流水号', value: fieldText(item.predRefNo) },
55
- { label: '交易日期', value: formatForeignForecastDate(item.tranDate) },
56
+ { label: '预报人', value: fieldText(item.predCode || item.predName) },
57
+ {
58
+ label: '预报机构',
59
+ value: fieldText(item.predDept || item.predBranchName || item.predBranch),
60
+ },
61
+ { label: '联系电话', value: fieldText(item.phoneNo) },
62
+ {
63
+ label: '落地机构',
64
+ value: fieldText(item.groundBranchName || item.groundBranch),
65
+ },
56
66
  { 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) },
67
+ { label: '起息日', value: formatForeignForecastDate(item.tranDate) },
68
+ { label: '业务类型', value: businessTypeText.value },
69
+ { label: '预报金额(元)', value: heroAmount.value },
70
+ { label: '币种', value: currencyOption.value.label },
71
+ { label: '汇款路径', value: resolveForeignPaymentRoute(item.paymentRoute) },
72
+ { label: '收支方向', value: payDirectionText.value },
73
+ { label: '-', value: '-' },
71
74
  ],
72
75
  },
73
76
  {
74
- title: '业务与币种',
77
+ title: '交易/收款信息',
78
+ icon: 'records-o',
75
79
  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) },
80
+ { label: '交易对手SwiftCode', value: fieldText(item.competAcctSc) },
81
81
  {
82
- label: '清算行 SWIFT',
83
- value: fieldText(item.bankSwift || item.swiftCode || item.bicCode),
82
+ label: '单证业务编号',
83
+ value: fieldText(item.docBusinessno || item.docbusinessno),
84
84
  },
85
- { label: '标准名称', value: fieldText(item.standardName) },
86
- { label: '用途', value: fieldText(item.purpose) },
87
- { label: '备注', value: fieldText(item.remark) },
85
+ { label: '收款人名称', value: fieldText(item.rcvName) },
86
+ { label: '收款账号', value: fieldText(item.rcvAcct) },
87
+ { label: '收款行名称', value: fieldText(item.rcvBank) },
88
+ { label: '收款行SwiftCode', value: fieldText(item.rcvBankSc) },
89
+ { label: '中间行SwiftCode', value: fieldText(item.midBankSc) },
90
+ { label: '-', value: '-' },
88
91
  ],
89
92
  },
90
93
  {
91
- title: '支付与金额',
94
+ title: '客户信息',
95
+ icon: 'manager-o',
92
96
  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) },
97
+ { label: '客户账号', value: fieldText(item.clientAcct) },
98
+ { label: '客户名称', value: fieldText(item.clientName) },
99
+ { label: '客户类型', value: resolveForeignClientType(item.clientType) },
100
+ { label: '账户行', value: fieldText(item.bankAcct) },
101
+ { label: '结汇/购汇/现汇', value: resolveForeignOriginalForeign(item.originalForeign) },
102
+ { label: '-', value: '-' },
103
103
  ],
104
104
  },
105
105
  {
106
- title: '票据与联系',
106
+ title: '附加信息',
107
+ icon: 'notes-o',
107
108
  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) },
109
+ { label: '用途/原因', value: fieldText(item.purpose) },
110
+ { label: '备注', value: fieldText(item.remark) },
112
111
  ],
113
112
  },
114
113
  ]
@@ -160,6 +159,10 @@ function fieldText(value?: string) {
160
159
  const text = value?.trim()
161
160
  return text || '-'
162
161
  }
162
+
163
+ function handleEdit() {
164
+ showAppToast('编辑功能暂未开放')
165
+ }
163
166
  </script>
164
167
 
165
168
  <template>
@@ -170,13 +173,37 @@ function fieldText(value?: string) {
170
173
 
171
174
  <template v-else-if="detail">
172
175
  <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 class="hero-primary">
177
+ <div class="hero-amount">
178
+ <span>预报金额(元)</span>
179
+ <strong>{{ heroAmount }}</strong>
180
+ </div>
181
+
182
+ <div class="hero-currency">
183
+ <span class="hero-icon"><van-icon name="gold-coin-o" /></span>
184
+ <span class="hero-copy">
185
+ <small>币种</small>
186
+ <strong>{{ currencyOption.label }}</strong>
187
+ </span>
188
+ </div>
189
+ </div>
190
+
191
+ <div class="hero-secondary">
192
+ <div class="hero-summary">
193
+ <span class="hero-icon"><van-icon name="exchange" /></span>
194
+ <span class="hero-copy">
195
+ <small>收支方向</small>
196
+ <strong>{{ payDirectionText }}</strong>
197
+ </span>
198
+ </div>
199
+ <div class="hero-summary">
200
+ <span class="hero-icon"><van-icon name="orders-o" /></span>
201
+ <span class="hero-copy">
202
+ <small>业务类型</small>
203
+ <strong>{{ businessTypeText }}</strong>
204
+ </span>
205
+ </div>
176
206
  </div>
177
- <strong>{{ heroAmount }}</strong>
178
- <span v-if="heroAmountYuan !== '-'" class="hero-amount-yuan">{{ heroAmountYuan }}</span>
179
- <p>{{ detail.predRefNo || predRefNo }}</p>
180
207
  </section>
181
208
 
182
209
  <section
@@ -185,14 +212,26 @@ function fieldText(value?: string) {
185
212
  class="detail-card"
186
213
  :aria-label="section.title"
187
214
  >
188
- <h2>{{ section.title }}</h2>
189
- <dl class="detail-list">
190
- <div v-for="field in section.fields" :key="`${section.title}-${field.label}`">
215
+ <header class="detail-card__header">
216
+ <van-icon :name="section.icon" />
217
+ <h2>{{ section.title }}</h2>
218
+ </header>
219
+ <dl class="detail-grid">
220
+ <div
221
+ v-for="field in section.fields"
222
+ :key="`${section.title}-${field.label}`"
223
+ class="detail-item"
224
+ >
191
225
  <dt>{{ field.label }}</dt>
192
- <dd>{{ field.value }}</dd>
226
+ <dd :title="field.value">{{ field.value }}</dd>
193
227
  </div>
194
228
  </dl>
195
229
  </section>
230
+
231
+ <footer class="action-bar">
232
+ <button class="back-btn" type="button" @click="router.back()">返回</button>
233
+ <button class="edit-btn" type="button" @click="handleEdit">编辑</button>
234
+ </footer>
196
235
  </template>
197
236
 
198
237
  <van-empty v-else image="error" :description="loadError ? '详情加载失败' : '详情不存在'">
@@ -210,17 +249,16 @@ function fieldText(value?: string) {
210
249
  max-width: 750px;
211
250
  min-height: var(--app-page-min-height, 100vh);
212
251
  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%);
252
+ padding: 13px 13px calc(env(safe-area-inset-bottom, 0px) + 84px);
253
+ color: #1c2029;
254
+ background: #f8f9fb;
218
255
  box-sizing: border-box;
219
256
  }
220
257
 
221
258
  button {
222
259
  border: 0;
223
260
  padding: 0;
261
+ font: inherit;
224
262
  background: transparent;
225
263
  appearance: none;
226
264
  }
@@ -232,159 +270,245 @@ button {
232
270
  }
233
271
 
234
272
  .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);
273
+ overflow: hidden;
274
+ border: 1px solid #ffd1d5;
275
+ border-radius: 8px;
276
+ background: #fffafa;
277
+ box-shadow: 0 5px 14px rgba(38, 45, 61, 0.05);
241
278
  }
242
279
 
243
- .hero-topline {
244
- display: flex;
280
+ .hero-primary {
281
+ display: grid;
282
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1.08fr);
245
283
  align-items: center;
246
- justify-content: space-between;
247
- gap: 10px;
284
+ min-height: 112px;
285
+ padding: 18px 16px 16px;
286
+ box-sizing: border-box;
248
287
  }
249
288
 
250
- .hero-card strong {
289
+ .hero-amount span,
290
+ .hero-copy small {
251
291
  display: block;
252
- margin-top: 18px;
253
- font-size: 27px;
254
- font-weight: 800;
255
- line-height: 1.1;
292
+ color: #4f535c;
293
+ font-size: 13px;
294
+ font-weight: 500;
295
+ line-height: 1.35;
256
296
  }
257
297
 
258
- .hero-amount-yuan {
298
+ .hero-amount strong {
259
299
  display: block;
260
300
  margin-top: 7px;
261
301
  overflow-wrap: anywhere;
262
- color: rgba(255, 255, 255, 0.74);
263
- font-size: 12px;
264
- font-weight: 500;
265
- line-height: 1.35;
302
+ color: #f12535;
303
+ font-size: 29px;
304
+ font-weight: 760;
305
+ line-height: 1.05;
266
306
  }
267
307
 
268
- .hero-card p {
308
+ .hero-currency,
309
+ .hero-summary {
310
+ display: flex;
311
+ align-items: center;
312
+ min-width: 0;
313
+ }
314
+
315
+ .hero-icon {
316
+ display: flex;
317
+ flex: 0 0 36px;
318
+ align-items: center;
319
+ justify-content: center;
320
+ width: 36px;
321
+ height: 36px;
322
+ border: 1px solid #ffd2d6;
323
+ border-radius: 50%;
324
+ color: #f22d3c;
325
+ background: #fff0f1;
326
+ font-size: 20px;
327
+ box-sizing: border-box;
328
+ }
329
+
330
+ .hero-copy {
331
+ min-width: 0;
332
+ margin-left: 10px;
333
+ }
334
+
335
+ .hero-copy strong {
336
+ display: block;
337
+ margin-top: 3px;
269
338
  overflow: hidden;
270
- margin: 10px 0 0;
271
- color: rgba(255, 255, 255, 0.82);
272
- font-size: 12px;
273
- line-height: 1.3;
339
+ color: #20242c;
340
+ font-size: 14px;
341
+ font-weight: 700;
342
+ line-height: 1.35;
274
343
  text-overflow: ellipsis;
275
344
  white-space: nowrap;
276
345
  }
277
346
 
278
- .hero-date {
279
- color: rgba(255, 255, 255, 0.88);
280
- font-size: 13px;
281
- font-weight: 650;
347
+ .hero-secondary {
348
+ display: grid;
349
+ grid-template-columns: repeat(2, minmax(0, 1fr));
350
+ min-height: 72px;
351
+ border-top: 1px solid #e8e8ea;
352
+ padding: 12px 16px;
353
+ box-sizing: border-box;
354
+ }
355
+
356
+ .hero-summary:nth-child(2) {
357
+ border-left: 1px solid #e4e5e8;
358
+ padding-left: 18px;
282
359
  }
283
360
 
284
361
  .detail-card {
285
362
  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);
363
+ overflow: hidden;
364
+ border: 1px solid #e0e3e8;
365
+ border-radius: 8px;
366
+ background: #ffffff;
367
+ box-shadow: 0 4px 12px rgba(38, 45, 61, 0.04);
368
+ }
369
+
370
+ .detail-card__header {
371
+ display: flex;
372
+ align-items: center;
373
+ min-height: 42px;
374
+ border-bottom: 1px solid #e2e4e8;
375
+ padding: 0 13px;
376
+ box-sizing: border-box;
377
+ }
378
+
379
+ .detail-card__header .van-icon {
380
+ color: #f52b3b;
381
+ font-size: 20px;
290
382
  }
291
383
 
292
384
  .detail-card h2 {
293
- margin: 0;
294
- color: #111728;
385
+ margin: 0 0 0 8px;
386
+ color: #252932;
295
387
  font-size: 15px;
296
- font-weight: 760;
388
+ font-weight: 700;
297
389
  line-height: 1.2;
298
390
  }
299
391
 
300
- .detail-list {
301
- margin: 12px 0 0;
392
+ .detail-grid {
393
+ display: grid;
394
+ grid-template-columns: repeat(2, minmax(0, 1fr));
395
+ margin: 0;
396
+ padding: 0 12px;
302
397
  }
303
398
 
304
- .detail-list div {
399
+ .detail-item {
305
400
  display: grid;
306
- grid-template-columns: 96px minmax(0, 1fr);
307
- gap: 12px;
308
- padding: 10px 0;
309
- border-top: 1px solid #eef0f4;
401
+ grid-template-columns: max-content minmax(0, 1fr);
402
+ align-items: center;
403
+ min-width: 0;
404
+ min-height: 45px;
405
+ border-top: 1px solid #e7e8eb;
406
+ column-gap: 9px;
407
+ box-sizing: border-box;
310
408
  }
311
409
 
312
- .detail-list div:first-child {
410
+ .detail-item:nth-child(-n + 2) {
313
411
  border-top: 0;
314
412
  }
315
413
 
316
- .detail-list dt,
317
- .detail-list dd {
414
+ .detail-item:nth-child(odd) {
415
+ padding-right: 12px;
416
+ }
417
+
418
+ .detail-item:nth-child(even) {
419
+ padding-left: 12px;
420
+ }
421
+
422
+ .detail-item dt,
423
+ .detail-item dd {
424
+ min-width: 0;
318
425
  margin: 0;
319
426
  font-size: 13px;
320
- line-height: 1.42;
427
+ line-height: 1.35;
321
428
  }
322
429
 
323
- .detail-list dt {
324
- color: #7f8798;
430
+ .detail-item dt {
431
+ color: #555b66;
432
+ white-space: nowrap;
325
433
  }
326
434
 
327
- .detail-list dd {
328
- overflow-wrap: anywhere;
329
- color: #151827;
330
- font-weight: 650;
331
- text-align: right;
435
+ .detail-item dd {
436
+ overflow: hidden;
437
+ color: #292e38;
438
+ font-weight: 500;
439
+ text-overflow: ellipsis;
440
+ white-space: nowrap;
332
441
  }
333
442
 
334
- .status-tag {
335
- display: inline-flex;
443
+ .action-bar {
444
+ position: fixed;
445
+ right: max(13px, calc((100vw - 750px) / 2 + 13px));
446
+ bottom: calc(env(safe-area-inset-bottom, 0px) + 10px);
447
+ left: max(13px, calc((100vw - 750px) / 2 + 13px));
448
+ z-index: 18;
449
+ display: grid;
450
+ grid-template-columns: minmax(0, 0.8fr) minmax(0, 1fr);
451
+ gap: 14px;
452
+ }
453
+
454
+ .back-btn,
455
+ .edit-btn {
456
+ display: flex;
336
457
  align-items: center;
337
458
  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;
459
+ height: 46px;
460
+ border-radius: 8px;
461
+ font-size: 15px;
462
+ font-weight: 700;
346
463
  }
347
464
 
348
- .status-tag-confirmed {
349
- border-color: #d8e1ee;
350
- color: #687083;
465
+ .back-btn {
466
+ border: 1px solid #e1e4e9;
467
+ color: #313640;
351
468
  background: #ffffff;
352
469
  }
353
470
 
354
- .status-tag-pending {
355
- border-color: #ffd99a;
356
- color: #a05a00;
357
- background: #fff4d8;
471
+ .edit-btn {
472
+ color: #ffffff;
473
+ background: #fa2838;
474
+ box-shadow: 0 7px 16px rgba(250, 40, 56, 0.18);
358
475
  }
359
476
 
360
- .status-tag-cancelled {
361
- border-color: #d6dbe3;
362
- color: #687083;
363
- background: #f3f5f8;
364
- }
477
+ @media (max-width: 360px) {
478
+ .detail-page {
479
+ padding-right: 10px;
480
+ padding-left: 10px;
481
+ }
365
482
 
366
- .status-tag-verified {
367
- border-color: #b7ebc9;
368
- color: #0b8f3a;
369
- background: #e4f8eb;
370
- }
483
+ .hero-primary,
484
+ .hero-secondary {
485
+ padding-right: 12px;
486
+ padding-left: 12px;
487
+ }
371
488
 
372
- .status-tag-rejected {
373
- border-color: #ffc0cb;
374
- color: #bf2549;
375
- background: #ffe8ed;
376
- }
489
+ .hero-summary:nth-child(2) {
490
+ padding-left: 12px;
491
+ }
377
492
 
378
- .status-tag-unknown {
379
- border-color: #d6dbe3;
380
- color: #687083;
381
- background: #f3f5f8;
382
- }
493
+ .detail-grid {
494
+ padding: 0 9px;
495
+ }
496
+
497
+ .detail-item {
498
+ column-gap: 6px;
499
+ }
500
+
501
+ .detail-item:nth-child(odd) {
502
+ padding-right: 7px;
503
+ }
504
+
505
+ .detail-item:nth-child(even) {
506
+ padding-left: 7px;
507
+ }
383
508
 
384
- @media (max-width: 340px) {
385
- .detail-list div {
386
- grid-template-columns: 84px minmax(0, 1fr);
387
- gap: 8px;
509
+ .detail-item dt,
510
+ .detail-item dd {
511
+ font-size: 12px;
388
512
  }
389
513
  }
390
514
  </style>
@@ -0,0 +1,367 @@
1
+ # mini2.0 项目开发规范
2
+
3
+ > 适用范围:本仓库所有前端页面、组件、状态、接口和构建配置。
4
+ >
5
+ > 最后更新:2026-07-10。
6
+
7
+ ## 1. 技术栈与运行环境
8
+
9
+ | 类别 | 项目选型 |
10
+ | --- | --- |
11
+ | 核心框架 | Vue 3、TypeScript、`<script setup>` |
12
+ | 构建工具 | Vite 8 |
13
+ | UI 组件 | Vant 4、Iconify |
14
+ | 路由 | Vue Router,Hash 路由 |
15
+ | 状态管理 | Pinia、pinia-plugin-persistedstate |
16
+ | 网络请求 | Axios、敏行宿主 `MXCommon.ajax` / `NXCommon.ajax` |
17
+ | 样式处理 | CSS、Less、PostCSS、px-to-viewport |
18
+ | 代码质量 | ESLint、OxcLint、Prettier、vue-tsc |
19
+
20
+ - Node.js 必须满足 `^20.19.0 || >=22.12.0`。
21
+ - 统一使用 npm 和仓库内的 `package-lock.json`,不要混用 pnpm、Yarn 或 Bun。
22
+ - 全局源码使用 UTF-8、LF 换行、2 空格缩进。
23
+
24
+ ## 2. 常用命令
25
+
26
+ ```bash
27
+ # 安装依赖
28
+ npm ci
29
+
30
+ # 本地开发,默认使用 development 环境并监听 8080 端口
31
+ npm run dev
32
+
33
+ # 使用 test 环境启动
34
+ npm run dev:test
35
+
36
+ # 自动修复 ESLint 和 OxcLint 问题
37
+ npm run lint
38
+
39
+ # TypeScript / Vue 类型检查
40
+ npm run type-check
41
+
42
+ # 格式化 src 目录
43
+ npm run format
44
+ ```
45
+
46
+ 构建命令仅在发布或明确需要构建验证时执行:
47
+
48
+ ```bash
49
+ npm run build
50
+ npm run build:test
51
+ npm run build:prod
52
+ ```
53
+
54
+ ### 2.1 构建副作用
55
+
56
+ 项目的 `plugins/bump-version.ts` 会在构建时执行以下操作:
57
+
58
+ 1. 自动修改 `src/config/plugin.properties.test` 或 `src/config/plugin.properties.pro` 的版本号;
59
+ 2. 输出 `dist/www`、`dist/plugin.properties`;
60
+ 3. 生成 `dist/dist_<version_code>.zip`。
61
+
62
+ 因此不要把构建产生的版本号变化混入普通功能提交。构建后必须检查 `git diff`,只保留本次任务确实需要的版本文件变更。
63
+
64
+ ## 3. 环境配置
65
+
66
+ 项目按 Vite mode 读取配置:
67
+
68
+ - `.env.development`:本地开发;
69
+ - `.env.test`:测试环境;
70
+ - `.env.production`:生产环境。
71
+
72
+ 当前公共变量:
73
+
74
+ | 变量 | 说明 |
75
+ | --- | --- |
76
+ | `VITE_APP_ENV` | 当前业务环境标识 |
77
+ | `VITE_API_BASE_URL` | 接口基础地址 |
78
+ | `VITE_API_TIMEOUT` | 请求超时时间,单位毫秒 |
79
+ | `VITE_ENABLE_VUE_DEVTOOLS` | 是否启用 Vue DevTools,设置为 `false` 时关闭 |
80
+
81
+ 规范要求:
82
+
83
+ - 只有允许暴露到浏览器的变量才使用 `VITE_` 前缀;
84
+ - 不得在源码、环境文件、日志或提交记录中写入密码、Token、私钥等敏感信息;
85
+ - 新增环境变量时,同时补充 `env.d.ts` 类型和本文档说明;
86
+ - 本地开发接口统一经过 Vite `/oms` 代理,不在页面中硬编码服务地址。
87
+
88
+ ## 4. 目录职责
89
+
90
+ ```text
91
+ src/
92
+ ├── api/ # 业务接口、接口入参与响应类型、后端数据归一化
93
+ ├── components/ # 跨页面复用的通用组件
94
+ ├── config/ # 环境、宿主应用和打包配置
95
+ ├── core/ # 基础设施:敏行 JSAPI、双端请求适配等
96
+ ├── router/ # 路由表和全局路由守卫
97
+ ├── stores/ # Pinia 全局或业务域共享状态
98
+ ├── styles/ # 全局样式及第三方组件覆盖
99
+ ├── types/ # 跨模块公共类型声明
100
+ ├── utils/ # 无页面依赖的通用工具和业务格式化函数
101
+ └── views/ # 页面,按业务域组织
102
+ ├── announcement/
103
+ ├── dashboard/
104
+ ├── foreign/
105
+ ├── home/
106
+ ├── login/
107
+ ├── mine/
108
+ └── rmb/
109
+ ```
110
+
111
+ ### 4.1 页面目录规则
112
+
113
+ - 页面必须按业务域归档,人民币业务放在 `views/rmb`,外币业务放在 `views/foreign`。
114
+ - 列表、详情、新增等同一业务的页面放在同一模块下,例如:
115
+
116
+ ```text
117
+ views/rmb/position/
118
+ ├── index.vue
119
+ ├── create/index.vue
120
+ └── detail/index.vue
121
+ ```
122
+
123
+ - 移动页面文件时,默认只调整目录和路由 import,不改变已对外使用的 URL。
124
+ - `src/views/home/index.vue` 是首页业务入口清单;新增、删除或调整首页模块时需要同步检查它。
125
+ - 只在多个页面实际复用时抽取到 `components`;页面私有组件应放在对应页面模块内,避免过早全局化。
126
+
127
+ ## 5. 命名规范
128
+
129
+ | 对象 | 规范 | 示例 |
130
+ | --- | --- | --- |
131
+ | 业务目录 | kebab-case | `position-estimate` |
132
+ | 页面入口 | `index.vue` | `views/rmb/position/index.vue` |
133
+ | 通用组件 | PascalCase | `AppTitleBar.vue` |
134
+ | 变量、函数 | camelCase | `systemWorkDate`、`handleQuery` |
135
+ | 常量 | UPPER_SNAKE_CASE | `DETAIL_TRAN_CODE` |
136
+ | 类型、接口 | PascalCase | `PbcPositionBalance` |
137
+ | Store Hook | `useXxxStore` | `usePositionFilterStore` |
138
+ | 事件处理函数 | `handleXxx` 或 `onXxx` | `handleSubmit`、`onMounted` |
139
+ | CSS 类 | kebab-case,组件内建议 BEM | `app-title-bar__back` |
140
+
141
+ - 布尔值使用 `is`、`has`、`show`、`can`、`should` 等可读前缀。
142
+ - API 字段必须保留后端原始命名时,在 API 层声明类型并集中映射,不把含义不明的缩写继续扩散到视图层。
143
+ - import 优先使用 `@/` 别名,避免多层 `../../../` 相对路径。
144
+
145
+ ## 6. TypeScript 规范
146
+
147
+ - 禁止无理由使用 `any`;外部未知数据先使用 `unknown`,再通过类型守卫收窄。
148
+ - 请求入参、响应体、组件 Props、Emits 和 Store 状态必须声明类型。
149
+ - 类型仅用于编译时,应使用 `import type`。
150
+ - 项目启用了 `noUncheckedIndexedAccess`,数组或对象索引结果必须处理 `undefined`。
151
+ - 可选字段在进入计算和展示前应提供明确兜底,不使用非空断言掩盖真实问题。
152
+ - 优先使用联合类型表达有限状态,不用散落的魔法字符串。
153
+ - 共用类型放在 `src/types`;仅被一个 API 使用的类型与对应 API 文件放在一起。
154
+ - 金额等高精度后端字段优先保留为字符串,在格式化或计算边界再显式转换。
155
+
156
+ ## 7. Vue 组件规范
157
+
158
+ ### 7.1 脚本
159
+
160
+ - 新组件统一使用 `<script setup lang="ts">` 和 Composition API。
161
+ - 单文件组件按“依赖导入 → Props/Emits → 响应式状态 → 计算属性 → 生命周期 → 事件方法”的顺序组织。
162
+ - Props 和 Emits 使用类型声明;有默认值时使用 `withDefaults`。
163
+ - 派生数据使用 `computed`,不要用 `watch` 手动维护可计算状态。
164
+ - 注册了原生事件或定时器时,必须在 `onBeforeUnmount` / `onUnmounted` 中清理。
165
+ - Vue、Vue Router、Pinia、VueUse 常用 API 已配置自动导入;业务模块、工具函数和类型仍需显式 import。
166
+ - Vant 组件已按需自动注册,直接使用 `van-*` 组件,不在页面重复做全量注册。
167
+
168
+ ### 7.2 模板
169
+
170
+ - 模板只负责声明展示,不放复杂数据转换或多层业务判断;复杂逻辑放到 `computed` 或函数中。
171
+ - 列表渲染必须提供稳定且唯一的 `:key`,不得默认使用数组下标。
172
+ - 原生 `<button>` 必须设置 `type="button"` 或正确的提交类型。
173
+ - 图标按钮、返回按钮和自定义交互控件必须提供 `aria-label`,并保留键盘可操作性。
174
+ - “隐藏”和“禁用”是不同语义:需求要求隐藏时使用 `v-if`,不要仅设置 `disabled`。
175
+ - 请求中的按钮应绑定 loading 状态,避免重复提交。
176
+
177
+ ### 7.3 样式
178
+
179
+ - 页面和组件样式默认使用 `<style scoped>`;真正全局的样式才放入 `src/styles`。
180
+ - 覆盖 Vant 内部样式时使用 `:deep()`,并把影响范围限制在当前页面或组件。
181
+ - 全局 Vant 覆盖统一放在 `src/styles/vant-overrides.css`。
182
+ - 页面最小高度优先使用 `var(--app-page-min-height)`,不要各自重复计算标题栏和 Tabbar 高度。
183
+ - 必须考虑 `env(safe-area-inset-top)` 和 `env(safe-area-inset-bottom)`,避免内容被刘海、标题栏或底部导航遮挡。
184
+ - 主内容在大屏环境下应遵守项目现有的 `750px` 最大宽度约束。
185
+ - PostCSS 以 `375px` 设计稿宽度把大于等于 `2px` 的 px 转为 vw;`1px` 边框会保留。编写尺寸时必须意识到这一转换规则。
186
+ - 优先复用 Vant CSS 变量和项目现有色彩,不随意增加新的全局颜色或层级值。
187
+
188
+ ## 8. 路由规范
189
+
190
+ 所有页面路由集中在 `src/router/index.ts`:
191
+
192
+ - 页面组件使用动态 import,保持路由级懒加载;
193
+ - `name` 使用唯一的 PascalCase 名称;
194
+ - 需要登录的页面必须配置 `meta.requiresAuth: true`;
195
+ - 标题统一配置在 `meta.title`;
196
+ - 只有需要底部导航的一级页面才设置 `meta.showTabbar: true`;
197
+ - 新增一级页面时,同步检查 `src/App.vue` 中的 Tabbar、无返回按钮路由和标题栏行为;
198
+ - 路由参数使用语义化名称,例如 `:predRefNo`、`:id`;
199
+ - 不在单个页面重复实现登录跳转,统一交给全局路由守卫。
200
+
201
+ 示例:
202
+
203
+ ```ts
204
+ {
205
+ path: '/example',
206
+ name: 'Example',
207
+ component: () => import('@/views/rmb/example/index.vue'),
208
+ meta: { requiresAuth: true, title: '示例页面' },
209
+ }
210
+ ```
211
+
212
+ ## 9. Pinia 状态规范
213
+
214
+ 按状态作用域决定存放位置:
215
+
216
+ 1. 仅当前组件使用、离开页面即可丢失:使用组件内 `ref` / `reactive`;
217
+ 2. 同一业务的多个页面或组件共享:新建业务 Store;
218
+ 3. 需要刷新后保留:在对应业务 Store 中配置持久化;
219
+ 4. 全应用通用运行状态和应用设置:放在 `src/stores/app.ts`;
220
+ 5. 登录态和用户信息:放在 `src/stores/user.ts`。
221
+
222
+ 具体要求:
223
+
224
+ - 统一使用 Setup Store 写法;
225
+ - 页面专属状态不得为了省事全部塞入 `app.ts`,应创建独立的 feature store;
226
+ - 共享业务状态通过 Store action 修改,不让页面到处直接拼装同一业务规则;
227
+ - 持久化必须设置唯一、稳定的 key,并用 `pick` 只保存必要字段;
228
+ - 新增持久化字段时考虑旧数据兼容、默认值和退出登录后的清理策略;
229
+ - 除明确设计的登录态外,不持久化敏感数据、临时响应和可重新计算的数据;
230
+ - `piniaPluginPersistedstate` 已在 `src/main.ts` 注册,不要重复注册。
231
+
232
+ ## 10. 接口与请求规范
233
+
234
+ ### 10.1 分层
235
+
236
+ ```text
237
+ 页面 / Store
238
+
239
+ src/api/* 业务接口与数据映射
240
+
241
+ src/core/request 双端请求适配
242
+ ├── 浏览器:Axios
243
+ └── 敏行宿主:MXCommon.ajax / NXCommon.ajax
244
+ ```
245
+
246
+ - 页面不得直接调用 Axios 或 `MXCommon.ajax`,统一调用 `src/api` 暴露的业务函数。
247
+ - 业务 API 必须从 `@/core/request` 引入 `request`,确保浏览器和敏行宿主行为一致。
248
+ - GET 查询参数放 `params`,POST/PUT 请求体放 `data`。
249
+ - 登录、刷新 Token 等无需鉴权的接口才允许设置 `skipAuth: true`。
250
+ - 敏行原生请求仅支持 GET、POST、PUT、DELETE;新增 PATCH 接口前必须先确认宿主兼容方案。
251
+ - TAM 交易统一复用 `buildTamRequestPayload` 和 `tamRequest`,交易码定义为带业务说明的常量。
252
+
253
+ 示例:
254
+
255
+ ```ts
256
+ import { request } from '@/core/request'
257
+
258
+ export type ExampleQuery = {
259
+ date: string
260
+ }
261
+
262
+ export type ExampleResult = {
263
+ amount: string
264
+ }
265
+
266
+ export function queryExample(params: ExampleQuery) {
267
+ return request<ExampleResult>({
268
+ url: '/example',
269
+ method: 'GET',
270
+ params,
271
+ })
272
+ }
273
+ ```
274
+
275
+ ### 10.2 响应与错误处理
276
+
277
+ - Axios 请求层统一处理进度条、鉴权头、Token 刷新、HTTP 错误和通用业务 code。
278
+ - API 层负责兼容后端包装结构、字段别名和数据归一化,页面只消费稳定模型。
279
+ - 页面异步操作使用 `try/catch/finally`,并在 `finally` 中恢复 loading 状态。
280
+ - 用户提示统一使用 `showAppToast` / `showAppLoadingToast`,不直接散落 Vant Toast 配置。
281
+ - 不吞掉异常;无法在当前层恢复时继续抛出,让调用方决定页面提示和交互。
282
+ - 日志必须包含可定位的业务上下文,但不得输出密码、完整 Token、敏感账户信息或整段隐私报文。
283
+
284
+ ## 11. 业务数据规范
285
+
286
+ ### 11.1 金额
287
+
288
+ - 后端返回的人民币金额默认按“元”理解,视图主展示统一通过共享工具转换为“亿元”。
289
+ - 转换前先去除千分位逗号,使用 `src/utils/rmb-forecast.ts` 中的共享函数,避免页面各写一套算法。
290
+ - `0`、`0.00` 是有效业务值,不得用普通真值判断当作空数据。
291
+ - 只有产品需求明确要求时,才在“亿元”下补充完整“元”金额。
292
+ - 保留正负号、精度和异常值兜底,不直接在模板中执行 `Number(...).toFixed(...)`。
293
+
294
+ ### 11.2 日期
295
+
296
+ - 页面日期控件使用 `YYYY-MM-DD`;TAM 等接口按约定转换为 `YYYYMMDD`。
297
+ - 系统工作日优先复用 `useAppStore()` 的 `systemWorkDate` / `systemWorkDateCompact`。
298
+ - 日期格式化集中到 API 或 utils 层,不在多个页面重复切片拼接。
299
+
300
+ ### 11.3 富文本
301
+
302
+ - 使用 `v-html` 前必须经过白名单清洗,统一复用公告模块的 `sanitizeArticleHtml` 思路。
303
+ - 图片协议、Base64、链接协议和危险属性都必须显式校验。
304
+ - 富文本点击、图片预览等行为优先使用容器事件委托,避免给动态 HTML 注入脚本处理器。
305
+
306
+ ## 12. 注释与日志规范
307
+
308
+ - 注释使用中文,重点解释“为什么”和业务约束,不复述代码字面含义。
309
+ - 交易码、字段匹配、跨接口关联、单位转换和兼容逻辑必须补充注释。
310
+ - 公共函数和复杂类型优先使用 JSDoc,说明参数、返回值、单位和异常情况。
311
+ - 临时调试日志在提交前删除;确需保留的联调日志使用稳定前缀,例如 `[人行头寸余额查询]`。
312
+ - 禁止在生产日志中打印认证凭证或完整敏感报文。
313
+
314
+ ## 13. 格式化与质量检查
315
+
316
+ 代码风格以仓库配置为准:
317
+
318
+ - 不使用分号;
319
+ - 字符串使用单引号;
320
+ - 单行宽度尽量不超过 100;
321
+ - 2 空格缩进;
322
+ - 文件末尾保留换行;
323
+ - 删除行尾空格。
324
+
325
+ 注意:`npm run lint` 和 `npm run format` 都会直接修改文件。执行后必须重新检查 diff,避免把无关格式化混入当前任务。
326
+
327
+ 项目当前没有统一的自动化测试脚本。功能改动至少应覆盖以下检查:
328
+
329
+ 1. 检查修改文件的 ESLint / TypeScript 问题;
330
+ 2. 在目标环境手动验证成功、空数据、错误和重复点击场景;
331
+ 3. 涉及移动端布局时检查安全区、长文本、小屏和 750px 宽屏表现;
332
+ 4. 涉及双端请求时分别考虑浏览器 Axios 与敏行宿主 AJAX 的差异;
333
+ 5. 只有明确需要时再执行构建,并处理版本文件副作用。
334
+
335
+ ## 14. Git 与发布规范
336
+
337
+ - 一次提交只处理一个明确目标,不夹带无关重构、格式化或生成文件。
338
+ - 提交信息遵循仓库现有 Conventional Commits 风格:
339
+
340
+ ```text
341
+ feat(position-filter): 持久化筛选区展开状态
342
+ fix(announcement): 修复详情图片预览
343
+ refactor(router): 调整业务页面目录
344
+ chore(ci): 固定 npm 发布版本
345
+ ```
346
+
347
+ - 常用类型:`feat`、`fix`、`refactor`、`docs`、`style`、`chore`。
348
+ - 不提交 `dist`、`.playwright-cli`、日志、缓存和 IDE 私有配置。
349
+ - 移动文件后必须检查 `src/router/index.ts` 和全仓旧 import,确保没有残留路径。
350
+ - 推送前检查 `git status` 和 `git diff`,保留用户已有且与任务无关的修改。
351
+ - `main` 分支每次 push 都会触发 `.github/workflows/publish.yml`:自动提升 npm patch 版本、发布 npm 包并回推版本提交与 tag。普通开发不得把“推送 main”当作无副作用操作。
352
+ - 发布流程使用 npm Trusted Publishing;不要在仓库中新增长期 npm Token。
353
+
354
+ ## 15. 提交前检查清单
355
+
356
+ - [ ] 修改范围与需求一致,没有顺手改动无关功能。
357
+ - [ ] 页面、API、Store、工具函数放在正确目录。
358
+ - [ ] 新路由已配置唯一名称、标题、鉴权和正确的懒加载路径。
359
+ - [ ] 页面共享或持久化状态已放入独立、职责清晰的 Pinia Store。
360
+ - [ ] 请求通过 `src/api` 和 `@/core/request` 发出,没有绕过双端适配层。
361
+ - [ ] 入参、响应、Props、Emits 和 Store 状态具备 TypeScript 类型。
362
+ - [ ] 金额、日期、空值和 `0` 值按业务约定处理。
363
+ - [ ] 用户提示、loading、异常和重复提交状态完整。
364
+ - [ ] 移动端安全区、标题栏、Tabbar 和响应式宽度未被破坏。
365
+ - [ ] 没有敏感信息、临时日志、生成文件或无关格式化进入 diff。
366
+ - [ ] 如果执行过构建,已确认版本配置文件是否发生非预期变化。
367
+ - [ ] 已查看最终 `git status` 和 `git diff`。