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.
@@ -5,43 +5,65 @@ import {
5
5
  type TamResponse,
6
6
  } from '@/api/tam'
7
7
 
8
+ /**
9
+ * 人行头寸余额“发送”交易请求报文。
10
+ *
11
+ * 该交易不携带业务 body,仅用于向后端发起一次余额查询申请,
12
+ * 后端返回的申请平台流水号会作为后续“查询”动作的匹配条件。
13
+ */
8
14
  export type PbcPositionSendPayload = {
9
15
  appHead: TamRequestPayload<PbcPositionEmptyBody>['appHead']
10
16
  body: PbcPositionEmptyBody
11
17
  head: TamRequestPayload<PbcPositionEmptyBody>['head']
12
18
  }
13
19
 
20
+ /**
21
+ * 人行头寸余额列表查询请求报文。
22
+ *
23
+ * 列表查询同样为空 body,后端按当前登录用户、机构、工作日等报文头信息
24
+ * 返回可继续查询明细的记录集合。
25
+ */
14
26
  export type PbcPositionQueryPayload = {
15
27
  appHead: TamRequestPayload<PbcPositionEmptyBody>['appHead']
16
28
  body: PbcPositionEmptyBody
17
29
  head: TamRequestPayload<PbcPositionEmptyBody>['head']
18
30
  }
19
31
 
32
+ /**
33
+ * 人行头寸余额明细查询请求报文。
34
+ *
35
+ * 明细交易需要列表记录中的应答平台流水号和清算账户行号,
36
+ * 查询结果再与发送交易返回的申请平台流水号进行匹配。
37
+ */
20
38
  export type PbcPositionDetailPayload = {
21
39
  appHead: TamRequestPayload<PbcPositionDetailRequestBody>['appHead']
22
40
  body: PbcPositionDetailRequestBody
23
41
  head: TamRequestPayload<PbcPositionDetailRequestBody>['head']
24
42
  }
25
43
 
44
+ /** “发送”交易响应,body 中主要使用后端返回的申请平台流水号。 */
26
45
  export type PbcPositionSendResult = {
27
46
  body: PbcPositionSendBody
28
47
  head: TamResponse<PbcPositionSendBody>['head']
29
48
  }
30
49
 
50
+ /** “列表查询”交易响应,body 可能是数组,也可能被 resultList 包裹。 */
31
51
  export type PbcPositionQueryResult = {
32
52
  body: PbcPositionQueryBody
33
53
  head: TamResponse<PbcPositionQueryBody>['head']
34
54
  }
35
55
 
56
+ /** “明细查询”交易响应,body 为页面最终展示的扩展余额信息。 */
36
57
  export type PbcPositionDetailResult = {
37
58
  body: PbcPositionDetailBody
38
59
  head: TamResponse<PbcPositionDetailBody>['head']
39
60
  }
40
61
 
62
+ /** 列表交易返回的基础记录,也是明细查询的入参来源。 */
41
63
  export type PbcPositionRecord = {
42
- /** 应答平台流水号,查询明细入参。 */
64
+ /** 应答平台流水号,作为明细查询 body.refNo。 */
43
65
  refNo?: string
44
- /** 清算账户行号。 */
66
+ /** 清算账户行号,作为明细查询 body.settleBankNo。 */
45
67
  settleBankNo?: string
46
68
  /** 清算账户余额,接口按元返回。 */
47
69
  settleBal?: string
@@ -49,14 +71,15 @@ export type PbcPositionRecord = {
49
71
  avaAmt?: string
50
72
  /** 预期头寸,接口按元返回。 */
51
73
  expectAmt?: string
52
- /** 更新时间。 */
74
+ /** 余额数据更新时间,页面原样展示。 */
53
75
  updateTime?: string
54
76
  }
55
77
 
78
+ /** 明细交易返回的完整余额记录,补充排队金额和辖内非清算账户信息。 */
56
79
  export type PbcPositionDetailBody = PbcPositionRecord & {
57
- /** 申请平台流水号。 */
80
+ /** 申请平台流水号,用于与发送交易返回的流水号匹配。 */
58
81
  refNo?: string
59
- /** 应答平台流水号。 */
82
+ /** 明细返回的应答平台流水号,页面展示使用。 */
60
83
  resRefNo?: string
61
84
  /** 排队借记总金额,接口按元返回。 */
62
85
  lineDrAmt?: string
@@ -68,33 +91,50 @@ export type PbcPositionDetailBody = PbcPositionRecord & {
68
91
  subAcctAmt?: string
69
92
  }
70
93
 
94
+ /**
95
+ * 页面使用的归一化余额模型。
96
+ *
97
+ * 列表记录和明细记录会合并到同一个对象中,额外保留发送交易返回的
98
+ * requestRefNo,便于页面判断是否允许继续查询并展示当前申请编号。
99
+ */
71
100
  export type PbcPositionBalance = PbcPositionRecord &
72
101
  PbcPositionDetailBody & {
73
102
  /** 发送交易返回的申请平台流水号。 */
74
103
  requestRefNo?: string
75
104
  }
76
105
 
106
+ /** 人行头寸余额相关交易均没有业务入参,body 按接口约定传空对象。 */
77
107
  type PbcPositionEmptyBody = Record<string, never>
78
108
 
109
+ /** 发送交易响应体,仅返回本次申请的平台流水号。 */
79
110
  type PbcPositionSendBody = {
111
+ /** 申请平台流水号,后续查询用它匹配最终明细。 */
80
112
  refNo?: string
81
113
  }
82
114
 
115
+ /** 列表查询响应体,兼容后端返回数组或 { resultList } 两种结构。 */
83
116
  type PbcPositionQueryBody =
84
117
  | PbcPositionRecord[]
85
118
  | {
86
119
  resultList?: PbcPositionRecord[]
87
120
  }
88
121
 
122
+ /** 明细查询请求体,两个字段均来自列表记录。 */
89
123
  type PbcPositionDetailRequestBody = {
124
+ /** 列表记录中的应答平台流水号。 */
90
125
  refNo: string
126
+ /** 列表记录中的清算账户行号。 */
91
127
  settleBankNo: string
92
128
  }
93
129
 
130
+ /** 140104009:发起人行头寸余额查询申请。 */
94
131
  const SEND_TRAN_CODE = '/TAM/0001/140104009'
132
+ /** 140104010:查询可用于继续取明细的人行头寸余额记录列表。 */
95
133
  const QUERY_TRAN_CODE = '/TAM/0001/140104010'
134
+ /** 140104011:按列表记录查询人行头寸余额明细。 */
96
135
  const DETAIL_TRAN_CODE = '/TAM/0001/140104011'
97
136
 
137
+ /** 构造“发送”交易报文,填充 TAM 公共报文头并传空业务 body。 */
98
138
  export function buildPbcPositionSendPayload(): PbcPositionSendPayload {
99
139
  return buildTamRequestPayload({
100
140
  tranCode: SEND_TRAN_CODE,
@@ -102,12 +142,14 @@ export function buildPbcPositionSendPayload(): PbcPositionSendPayload {
102
142
  })
103
143
  }
104
144
 
145
+ /** 调用“发送”交易,成功后返回申请平台流水号。 */
105
146
  export async function sendPbcPositionRequest(
106
147
  payload: PbcPositionSendPayload,
107
148
  ): Promise<PbcPositionSendResult> {
108
149
  return tamRequest<PbcPositionEmptyBody, PbcPositionSendBody>(payload)
109
150
  }
110
151
 
152
+ /** 构造“列表查询”交易报文,按登录上下文查询当前可见的人行头寸记录。 */
111
153
  export function buildPbcPositionQueryPayload(): PbcPositionQueryPayload {
112
154
  return buildTamRequestPayload({
113
155
  tranCode: QUERY_TRAN_CODE,
@@ -115,12 +157,14 @@ export function buildPbcPositionQueryPayload(): PbcPositionQueryPayload {
115
157
  })
116
158
  }
117
159
 
160
+ /** 调用“列表查询”交易,返回可进一步查询明细的记录集合。 */
118
161
  export async function queryPbcPositionList(
119
162
  payload: PbcPositionQueryPayload,
120
163
  ): Promise<PbcPositionQueryResult> {
121
164
  return tamRequest<PbcPositionEmptyBody, PbcPositionQueryBody>(payload)
122
165
  }
123
166
 
167
+ /** 根据列表记录构造“明细查询”交易报文。 */
124
168
  export function buildPbcPositionDetailPayload(
125
169
  record: Pick<PbcPositionRecord, 'refNo' | 'settleBankNo'>,
126
170
  ): PbcPositionDetailPayload {
@@ -133,13 +177,22 @@ export function buildPbcPositionDetailPayload(
133
177
  })
134
178
  }
135
179
 
180
+ /** 调用“明细查询”交易,返回单条人行头寸余额的完整字段。 */
136
181
  export async function queryPbcPositionDetail(
137
182
  payload: PbcPositionDetailPayload,
138
183
  ): Promise<PbcPositionDetailResult> {
139
184
  return tamRequest<PbcPositionDetailRequestBody, PbcPositionDetailBody>(payload)
140
185
  }
141
186
 
187
+ /**
188
+ * 按发送交易返回的申请平台流水号查询最终余额明细。
189
+ *
190
+ * 后端流程拆成“列表查询”和“逐条明细查询”两步:先取到候选记录列表,
191
+ * 再使用每条记录的应答流水号和清算账户行号查询明细;只有明细中的
192
+ * refNo 与发送返回的 requestRefNo 一致时,才认为命中了本次申请结果。
193
+ */
142
194
  export async function queryPbcPositionBalance(requestRefNo: string): Promise<PbcPositionBalance> {
195
+ // 第一步:获取当前登录上下文下可查询的候选人行头寸记录。
143
196
  const queryPayload = buildPbcPositionQueryPayload()
144
197
  console.log('[人行头寸余额查询1入参]', queryPayload)
145
198
 
@@ -151,15 +204,19 @@ export async function queryPbcPositionBalance(requestRefNo: string): Promise<Pbc
151
204
  }
152
205
 
153
206
  for (const record of records) {
207
+ // 明细交易必须同时传入应答流水号和清算账户行号,缺任一字段时跳过。
154
208
  if (!record.refNo || !record.settleBankNo) continue
155
209
 
210
+ // 第二步:逐条查询明细,因为列表交易不直接返回全部展示字段。
156
211
  const detailPayload = buildPbcPositionDetailPayload(record)
157
212
  console.log('[人行头寸余额查询2入参]', detailPayload)
158
213
 
159
214
  const detailResponse = await queryPbcPositionDetail(detailPayload)
160
215
 
216
+ // 明细 refNo 表示申请平台流水号,用它与发送交易返回值做最终匹配。
161
217
  if (detailResponse.body.refNo !== requestRefNo) continue
162
218
 
219
+ // 保留列表字段,使用明细字段覆盖同名字段,并补齐页面需要的流水号别名。
163
220
  return {
164
221
  ...record,
165
222
  ...detailResponse.body,
@@ -171,6 +228,7 @@ export async function queryPbcPositionBalance(requestRefNo: string): Promise<Pbc
171
228
  throw new Error(`未查询到编号 ${requestRefNo} 对应的余额结果`)
172
229
  }
173
230
 
231
+ /** 统一提取列表记录,屏蔽后端数组和 resultList 包裹结构的差异。 */
174
232
  function getPbcPositionRecords(body: PbcPositionQueryBody) {
175
233
  if (Array.isArray(body)) return body
176
234
 
@@ -15,31 +15,31 @@ const router = createRouter({
15
15
  path: '/',
16
16
  name: 'Home',
17
17
  component: () => import('@/views/home/index.vue'),
18
- meta: { requiresAuth: true, title: '头寸管理平台' },
18
+ meta: { requiresAuth: true, showTabbar: true, title: '头寸管理平台' },
19
19
  },
20
20
  {
21
21
  path: '/login',
22
22
  name: 'Login',
23
23
  component: () => import('@/views/login/index.vue'),
24
- meta: { hideTabbar: true, title: '账号登录' },
24
+ meta: { title: '账号登录' },
25
25
  },
26
26
  {
27
27
  path: '/rmb-position',
28
28
  name: 'RmbPosition',
29
29
  component: () => import('@/views/rmb-position/index.vue'),
30
- meta: { requiresAuth: true, hideTabbar: true, title: '本币预报信息查询' },
30
+ meta: { requiresAuth: true, title: '本币预报信息查询' },
31
31
  },
32
32
  {
33
33
  path: '/rmb-position/new',
34
34
  name: 'RmbPositionCreate',
35
35
  component: () => import('@/views/rmb-position-create/index.vue'),
36
- meta: { requiresAuth: true, hideTabbar: true, title: '新增本币预报' },
36
+ meta: { requiresAuth: true, title: '新增本币预报' },
37
37
  },
38
38
  {
39
39
  path: '/rmb-position/:predRefNo',
40
40
  name: 'RmbPositionDetail',
41
41
  component: () => import('@/views/rmb-position-detail/index.vue'),
42
- meta: { requiresAuth: true, hideTabbar: true, title: '预报详情' },
42
+ meta: { requiresAuth: true, title: '预报详情' },
43
43
  },
44
44
  {
45
45
  path: '/foreign-position',
@@ -51,7 +51,7 @@ const router = createRouter({
51
51
  path: '/pbc-position',
52
52
  name: 'PbcPosition',
53
53
  component: () => import('@/views/pbc-position/index.vue'),
54
- meta: { requiresAuth: true, hideTabbar: true, title: '人行头寸余额查询' },
54
+ meta: { requiresAuth: true, title: '人行头寸余额查询' },
55
55
  },
56
56
  {
57
57
  path: '/position-estimate',
@@ -63,13 +63,13 @@ const router = createRouter({
63
63
  path: '/position-estimate/report',
64
64
  name: 'PositionEstimateReport',
65
65
  component: () => import('@/views/position-estimate-report/index.vue'),
66
- meta: { requiresAuth: true, hideTabbar: true, title: '头寸匡算报表' },
66
+ meta: { requiresAuth: true, title: '头寸匡算报表' },
67
67
  },
68
68
  {
69
69
  path: '/position-estimate/opening-reserve',
70
70
  name: 'OpeningReserveEstimate',
71
71
  component: () => import('@/views/opening-reserve-estimate/index.vue'),
72
- meta: { requiresAuth: true, hideTabbar: true, title: '日初备款数匡算' },
72
+ meta: { requiresAuth: true, title: '日初备款数匡算' },
73
73
  },
74
74
  {
75
75
  path: '/net-debit',
@@ -87,49 +87,49 @@ const router = createRouter({
87
87
  path: '/warning',
88
88
  name: 'Warning',
89
89
  component: () => import('@/views/warning/index.vue'),
90
- meta: { requiresAuth: true, title: '预警信息' },
90
+ meta: { requiresAuth: true, title: '头寸管理平台' },
91
91
  },
92
92
  {
93
93
  path: '/dashboard',
94
94
  name: 'Dashboard',
95
95
  component: () => import('@/views/dashboard/index.vue'),
96
- meta: { requiresAuth: true, title: '看板' },
96
+ meta: { requiresAuth: true, showTabbar: true, title: '看板' },
97
97
  },
98
98
  {
99
99
  path: '/articles',
100
100
  name: 'Articles',
101
101
  component: () => import('@/views/articles/index.vue'),
102
- meta: { requiresAuth: true, title: '公告' },
102
+ meta: { requiresAuth: true, showTabbar: true, title: '公告' },
103
103
  },
104
104
  {
105
105
  path: '/articles/new',
106
106
  name: 'ArticlePublish',
107
107
  component: () => import('@/views/article-edit/index.vue'),
108
- meta: { requiresAuth: true, hideTabbar: true, title: '发布公告' },
108
+ meta: { requiresAuth: true, title: '发布公告' },
109
109
  },
110
110
  {
111
111
  path: '/articles/:id',
112
112
  name: 'ArticleDetail',
113
113
  component: () => import('@/views/article-detail/index.vue'),
114
- meta: { requiresAuth: true, hideTabbar: true, title: '公告详情' },
114
+ meta: { requiresAuth: true, title: '公告详情' },
115
115
  },
116
116
  {
117
117
  path: '/articles/:id/edit',
118
118
  name: 'ArticleEdit',
119
119
  component: () => import('@/views/article-edit/index.vue'),
120
- meta: { requiresAuth: true, hideTabbar: true, title: '编辑公告' },
120
+ meta: { requiresAuth: true, title: '编辑公告' },
121
121
  },
122
122
  {
123
123
  path: '/mine',
124
124
  name: 'Mine',
125
125
  component: () => import('@/views/mine/index.vue'),
126
- meta: { requiresAuth: true, title: '我的' },
126
+ meta: { requiresAuth: true, showTabbar: true, title: '我的' },
127
127
  },
128
128
  {
129
129
  path: '/settings',
130
130
  name: 'Settings',
131
131
  component: () => import('@/views/settings/index.vue'),
132
- meta: { requiresAuth: true, hideTabbar: true, title: '设置' },
132
+ meta: { requiresAuth: true, title: '设置' },
133
133
  },
134
134
  ],
135
135
  })
@@ -20,7 +20,7 @@ export const useUserStore = defineStore(
20
20
  // -------------------------------- State --------------------------------
21
21
 
22
22
  /** 登录凭证,登录成功后由后端返回,后续请求通过请求头携带 */
23
- const token = ref('1s')
23
+ const token = ref('')
24
24
  const refreshToken = ref('')
25
25
  const username = ref('')
26
26
  const realName = ref('')
@@ -0,0 +1,102 @@
1
+ export type ForeignCurrencyOption = {
2
+ label: string
3
+ shortLabel: string
4
+ value: string
5
+ unit: string
6
+ }
7
+
8
+ export type ForeignOption = {
9
+ label: string
10
+ value: string
11
+ }
12
+
13
+ export type ForeignStatusOption = ForeignOption & {
14
+ className: string
15
+ }
16
+
17
+ export const foreignCurrencyOptions: ForeignCurrencyOption[] = [
18
+ { label: '全部币种', shortLabel: '全部币种', value: '', unit: '万元' },
19
+ { label: '美元', shortLabel: '美元', value: 'USD', unit: '万美元' },
20
+ { label: '欧元', shortLabel: '欧元', value: 'EUR', unit: '万欧元' },
21
+ { label: '港币', shortLabel: '港币', value: 'HKD', unit: '万港币' },
22
+ { label: '日元', shortLabel: '日元', value: 'JPY', unit: '万日元' },
23
+ { label: '英镑', shortLabel: '英镑', value: 'GBP', unit: '万英镑' },
24
+ { label: '加拿大元', shortLabel: '加元', value: 'CAD', unit: '万加元' },
25
+ { label: '澳大利亚元', shortLabel: '澳元', value: 'AUD', unit: '万澳元' },
26
+ { label: '新加坡元', shortLabel: '新元', value: 'SGD', unit: '万新元' },
27
+ { label: '瑞士法郎', shortLabel: '瑞郎', value: 'CHF', unit: '万瑞郎' },
28
+ ]
29
+
30
+ export const foreignBusinessOptions: ForeignOption[] = [
31
+ { label: '全部业务类型', value: '' },
32
+ { label: '普通汇款', value: '1' },
33
+ { label: '外汇买卖', value: '2' },
34
+ { label: '结售汇', value: '3' },
35
+ { label: '资金调拨', value: '4' },
36
+ { label: '同业业务', value: '5' },
37
+ { label: '贸易融资', value: '6' },
38
+ ]
39
+
40
+ export const foreignStatusOptions: ForeignStatusOption[] = [
41
+ { label: '全部预报状态', value: '', className: 'status-tag-unknown' },
42
+ { label: '已确认', value: '1', className: 'status-tag-confirmed' },
43
+ { label: '待确认', value: '2', className: 'status-tag-pending' },
44
+ { label: '已撤销', value: '3', className: 'status-tag-cancelled' },
45
+ { label: '已核销', value: '4', className: 'status-tag-verified' },
46
+ { label: '已驳回', value: '5', className: 'status-tag-rejected' },
47
+ ]
48
+
49
+ export const unknownForeignStatus: ForeignStatusOption = {
50
+ label: '未知状态',
51
+ value: '',
52
+ className: 'status-tag-unknown',
53
+ }
54
+
55
+ export function resolveForeignCurrency(value?: string) {
56
+ const normalizedValue = value?.trim().toUpperCase()
57
+
58
+ if (!normalizedValue) {
59
+ return foreignCurrencyOptions[0]
60
+ }
61
+
62
+ return (
63
+ foreignCurrencyOptions.find((item) => item.value === normalizedValue) || {
64
+ label: normalizedValue,
65
+ shortLabel: normalizedValue,
66
+ value: normalizedValue,
67
+ unit: `万${normalizedValue}`,
68
+ }
69
+ )
70
+ }
71
+
72
+ export function resolveForeignBusinessType(value?: string) {
73
+ if (!value?.trim()) return '普通汇款'
74
+
75
+ return foreignBusinessOptions.find((item) => item.value === value)?.label || value
76
+ }
77
+
78
+ export function resolveForeignStatus(value?: string) {
79
+ if (!value?.trim()) {
80
+ return unknownForeignStatus
81
+ }
82
+
83
+ return foreignStatusOptions.find((item) => item.value === value) || unknownForeignStatus
84
+ }
85
+
86
+ export function formatForeignForecastDate(value?: string) {
87
+ if (value && /^\d{8}$/.test(value)) {
88
+ return `${value.slice(0, 4)}-${value.slice(4, 6)}-${value.slice(6, 8)}`
89
+ }
90
+
91
+ return value || '-'
92
+ }
93
+
94
+ export function toTenThousandAmount(value?: string) {
95
+ const amount = Number(value?.replaceAll(',', '') || 0)
96
+ return Number.isFinite(amount) ? amount / 10000 : 0
97
+ }
98
+
99
+ export function formatForeignForecastAmount(value: number) {
100
+ const sign = value >= 0 ? '+' : '-'
101
+ return `${sign}${Math.abs(value).toFixed(2)}`
102
+ }
@@ -68,7 +68,7 @@ export function formatRmbForecastDate(value?: string) {
68
68
  }
69
69
 
70
70
  export function toBillionYuan(value?: string) {
71
- const amount = Number(value || 0)
71
+ const amount = Number(value?.replaceAll(',', '') || 0)
72
72
  return Number.isFinite(amount) ? amount / 100000000 : 0
73
73
  }
74
74
 
@@ -82,3 +82,27 @@ export function formatForecastAmountText(value?: string) {
82
82
 
83
83
  return `${formatForecastAmount(toBillionYuan(value))} 亿元`
84
84
  }
85
+
86
+ export function formatForecastAmountYuanText(value?: string) {
87
+ const text = value?.trim()
88
+
89
+ if (!text) return '-'
90
+
91
+ const normalizedText = text.replaceAll(',', '')
92
+
93
+ if (!/^[-+]?\d+(\.\d+)?$/.test(normalizedText)) {
94
+ return `${text} 元`
95
+ }
96
+
97
+ const sign =
98
+ normalizedText.startsWith('-') || normalizedText.startsWith('+')
99
+ ? normalizedText.slice(0, 1)
100
+ : ''
101
+ const unsignedText = sign ? normalizedText.slice(1) : normalizedText
102
+ const [integerPart = '0', decimalPart] = unsignedText.split('.')
103
+ const normalizedInteger = integerPart.replace(/^0+(?=\d)/, '') || '0'
104
+ const groupedInteger = normalizedInteger.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
105
+ const amountText = decimalPart === undefined ? groupedInteger : `${groupedInteger}.${decimalPart}`
106
+
107
+ return `${sign}${amountText} 元`
108
+ }
@@ -219,6 +219,15 @@ function articleContentToText(html: string) {
219
219
  text-decoration: none;
220
220
  }
221
221
 
222
+ .article-content :deep(img) {
223
+ display: block;
224
+ max-width: 100%;
225
+ height: auto;
226
+ margin: 8px auto 14px;
227
+ border-radius: 6px;
228
+ object-fit: contain;
229
+ }
230
+
222
231
  .article-content :deep(pre) {
223
232
  position: relative;
224
233
  overflow-x: auto;
@@ -514,6 +514,15 @@ function normalizeCodeLanguage(value: string) {
514
514
  background: #f6f7f9;
515
515
  }
516
516
 
517
+ .rich-editor :deep(img) {
518
+ display: block;
519
+ max-width: 100%;
520
+ height: auto;
521
+ margin: 8px auto 12px;
522
+ border-radius: 6px;
523
+ object-fit: contain;
524
+ }
525
+
517
526
  .rich-editor :deep(pre) {
518
527
  overflow-x: auto;
519
528
  margin: 0 0 12px;