vue3-element-plus-app-chu 0.0.18 → 0.0.19

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": "vue3-element-plus-app-chu",
3
- "version": "0.0.18",
3
+ "version": "0.0.19",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "files": [
@@ -14,7 +14,8 @@
14
14
  "dev": "run-p --print-label dev:frontend dev:backend",
15
15
  "dev:frontend": "vite",
16
16
  "dev:backend": "pnpm -C backend dev",
17
- "build": "run-p type-check \"build-only {@}\" --",
17
+ "build": "run-s type-check build-only",
18
+ "build:fast": "vite build",
18
19
  "preview": "vite preview",
19
20
  "build-only": "vite build",
20
21
  "type-check": "vue-tsc --build",
@@ -26,9 +26,6 @@ import { ElMessage, ElMessageBox } from 'element-plus'
26
26
  import API from './api'
27
27
  import { forecastRuntime, getToday } from '../runtime'
28
28
  import { ArtDataTable, type ColumnOption, type PaginationOptions, type TableSettings } from '@/components/table'
29
- import type {
30
- ApiResponse,
31
- } from '@/utils/request'
32
29
  import type {
33
30
  ForecastCreateBody,
34
31
  ForecastDetail,
@@ -65,7 +62,7 @@ interface ForecastPage {
65
62
  }
66
63
 
67
64
  /** 校验公共响应头;retCode 不是 00000 时按服务端提示或 fallback 抛错。 */
68
- const ensureSuccess = <T,>(response: ApiResponse<T>, fallback: string): T | null => {
65
+ const ensureSuccess = <T,>(response: any, fallback: string): T | null => {
69
66
  if (response?.head?.retCode !== '00000') {
70
67
  throw new Error(response?.head?.retMsg?.trim() || fallback)
71
68
  }
@@ -1,17 +1,11 @@
1
- import baseHttpClient from "@chic/http"
1
+ import baseHttpClient, { type ApiResponse } from '@chic/http'
2
+ import type { AccountBalanceInfo, InjectionCreateParams } from './typs'
2
3
 
3
4
  export default {
4
- /**
5
- * 头寸-账户余额查询申请。
6
- * @param params 请求参数;接口未声明筛选字段,保持空对象即可。
7
- * 页面在进入和点击查询时各发起一次请求,展示返回的单条账户行余额信息。
8
- */
9
- queryBalanceApi: (params: unknown = {}) => baseHttpClient.post<any>("/TAM/0001/140506001", params),
10
- /**
11
- * 注资、预注资管理-注资/预注资发起。
12
- * @param params 提交参数:adjustType 为调整类型(PRFD 注资调增、FDRD 注资调减、
13
- * FFIC 预注资调增、FFRD 预注资调减),tranAmt 为调整金额(元)。
14
- * 提交成功后重新查询账户行余额。
15
- */
16
- createApi: (params: unknown) => baseHttpClient.post<any>("/TAM/0001/120506001", params)
5
+ /** 查询单条账户行余额,无筛选参数。 */
6
+ queryBalanceApi: () =>
7
+ baseHttpClient.post<ApiResponse<AccountBalanceInfo>>('/TAM/0001/140506001', {}),
8
+ /** 注资/预注资发起,响应未声明业务字段。 */
9
+ createApi: (params: InjectionCreateParams) =>
10
+ baseHttpClient.post<ApiResponse>('/TAM/0001/120506001', params),
17
11
  }
@@ -1,155 +1,127 @@
1
1
  <script setup lang="ts">
2
- import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
2
+ import { onBeforeUnmount, onMounted, reactive, ref } from 'vue'
3
3
  import { ElMessage } from 'element-plus'
4
4
  import API from './api'
5
5
  import type { AccountBalanceInfo, InjectionCreateParams } from './typs'
6
6
 
7
7
  defineOptions({ name: 'DcpsBalancePage' })
8
8
 
9
- // 余额信息、请求与加载状态。
10
- const balance = ref<AccountBalanceInfo | null>(null) // 当前展示的账户行余额信息,未查询到时为 null。
11
- const loading = ref(false) // 查询是否进行中,同时控制查询按钮状态。
12
- const lastQueryTime = ref('') // 最近一次成功查询的本地时间,展示在标题区。
13
- let requestId = 0 // 最新请求的序号,用于忽略过期响应。
14
- let disposed = false // 页面是否已卸载,避免未完成请求继续更新界面。
9
+ const balance = ref<AccountBalanceInfo | null>(null)
10
+ const loading = ref(false)
11
+ const lastQueryTime = ref('')
12
+ let requestId = 0 // 只处理最新一次查询的响应。
13
+ let disposed = false
15
14
 
16
- // 提取异常提示;error 为捕获到的请求或处理异常。
17
15
  const getErrorMessage = (error: unknown) => error instanceof Error ? error.message : '请求失败,请稍后重试'
18
16
 
19
- // 将接口时间 value 转为浏览器本地时间,空值显示横线,无效时间保留原文。
17
+ // 时间转为本地显示,无效时间保留原文。
20
18
  const formatDateTime = (value?: string) => {
21
19
  if (!value?.trim()) return '—'
22
- const date = new Date(value) // 解析后的日期对象,用于判断接口时间是否有效。
20
+ const date = new Date(value)
23
21
  return Number.isNaN(date.getTime()) ? value : date.toLocaleString('zh-CN', { hour12: false })
24
22
  }
25
23
 
26
- // 将金额字符串 value 拆成符号、整数和小数,避免转为浮点数后丢失精度。
27
- const parseAmount = (value?: string) => {
24
+ // 使用整数分四舍五入,避免浮点数丢失金额精度。
25
+ const formatAmount = (value?: string) => {
28
26
  const text = String(value ?? '').trim()
29
- // 仅去掉合法千分位分隔符,避免把格式错误的 1,23 当成正常金额。
27
+ // 只移除合法千分位,格式错误时原样显示。
30
28
  const normalized = /^[+-]?\d{1,3}(?:,\d{3})+(?:\.\d*)?$/.test(text) ? text.replace(/,/g, '') : text
31
- const match = /^([+-]?)(\d*)(?:\.(\d*))?$/.exec(normalized) // 匹配结果:1 为符号,2 为整数,3 为小数。
32
- if (!match || (!match[2] && !match[3])) return null
33
-
34
- return {
35
- negative: match[1] === '-', // 是否为负数。
36
- integer: (match[2] || '0').replace(/^0+(?=\d)/, ''), // 去掉前导零的整数部分。
37
- fraction: (match[3] || '').replace(/0+$/, ''), // 去掉末尾零的小数部分。
38
- }
39
- }
29
+ const match = /^([+-]?)(\d*)(?:\.(\d*))?$/.exec(normalized)
30
+ if (!match || (!match[2] && !match[3])) return text || '—'
40
31
 
41
- // 格式化金额 value:先四舍五入到分,再添加千分位,统一展示两位小数。
42
- const formatAmount = (value?: string) => {
43
- const amount = parseAmount(value) // 解析后的十进制金额,无法识别时为 null。
44
- if (!amount) return String(value ?? '').trim() || '—'
45
-
46
- const fraction = amount.fraction.padEnd(3, '0') // 至少保留三位小数,第三位用于判断进位。
47
- let cents = BigInt(amount.integer + fraction.slice(0, 2)) // 金额绝对值对应的整数分,使用 BigInt 保证精度。
48
- if (fraction.charAt(2) >= '5') cents += 1n
49
-
50
- const digits = cents.toString().padStart(3, '0') // 进位后的分字符串,至少三位以保证整数部分存在。
51
- const integer = digits.slice(0, -2).replace(/\B(?=(\d{3})+(?!\d))/g, ',') // 带千分位的元整数部分。
52
- const sign = amount.negative ? '-' : '' // 展示用的金额符号。
53
- return `${sign}${integer}.${digits.slice(-2)}`
32
+ const [, sign, integer, decimals = ''] = match
33
+ const fraction = decimals.padEnd(3, '0')
34
+ const cents = BigInt((integer || '0') + fraction.slice(0, 2)) + (fraction.charAt(2) >= '5' ? 1n : 0n)
35
+ const whole = (cents / 100n).toLocaleString('en-US')
36
+ const decimal = (cents % 100n).toString().padStart(2, '0')
37
+ return `${sign === '-' ? '-' : ''}${whole}.${decimal}`
54
38
  }
55
39
 
56
- // 钱包状态展示:接口暂未提供字典,保留原值;有映射时优先展示中文。
57
- const walletStatusLabels: Record<string, string> = {}
58
- const formatWalletStatus = (value?: string) => value?.trim() ? (walletStatusLabels[value.trim()] ?? value.trim()) : '—'
59
-
60
- // 注资/预注资发起弹窗状态。
61
- const dialogVisible = ref(false) // 发起弹窗是否显示。
62
- const formError = reactive({ adjustType: '', tranAmt: '' }) // 每个表单项的校验提示,空字符串表示没有错误。
63
- const createForm = reactive({ adjustType: '', tranAmt: '' }) // 正在编辑的调整类型与调整金额。
64
- const pendingCreate = ref(false) // 提交是否进行中,避免重复提交。
40
+ const dialogVisible = ref(false)
41
+ const pendingCreate = ref(false)
42
+ const formError = reactive({ adjustType: '', tranAmt: '' })
43
+ const createForm = reactive<{
44
+ adjustType: InjectionCreateParams['adjustType'] | ''
45
+ tranAmt: string
46
+ }>({ adjustType: '', tranAmt: '' })
65
47
 
66
- // 调整类型字典:接口已声明枚举与中文含义。
67
- const adjustTypeOptions = [
48
+ const adjustTypeOptions: { value: InjectionCreateParams['adjustType']; label: string }[] = [
68
49
  { value: 'PRFD', label: '注资调增' },
69
50
  { value: 'FDRD', label: '注资调减' },
70
51
  { value: 'FFIC', label: '预注资调增' },
71
52
  { value: 'FFRD', label: '预注资调减' },
72
- ] as const
53
+ ]
73
54
 
74
55
  // 打开发起弹窗,恢复默认表单。
75
56
  const openCreateDialog = () => {
76
- createForm.adjustType = ''
77
- createForm.tranAmt = ''
78
- formError.adjustType = ''
79
- formError.tranAmt = ''
57
+ Object.assign(createForm, { adjustType: '', tranAmt: '' })
58
+ Object.assign(formError, { adjustType: '', tranAmt: '' })
80
59
  dialogVisible.value = true
81
60
  }
82
61
 
83
- // 校验表单;返回是否通过,同时填写各字段错误提示。
84
- const validateForm = () => {
85
- const adjustType = createForm.adjustType // 调整类型,字典值必选。
86
- const amountText = createForm.tranAmt.trim() // 去除首尾空白后的调整金额。
62
+ // 提交成功后关闭弹窗并刷新余额。
63
+ const submitCreate = async () => {
64
+ if (pendingCreate.value || loading.value || disposed) return
65
+ const { adjustType } = createForm
66
+ const tranAmt = createForm.tranAmt.trim()
87
67
  formError.adjustType = adjustType ? '' : '请选择调整类型'
88
- formError.tranAmt = /^\d+(?:\.\d+)?$/.test(amountText) && /^[1-9]/.test(amountText)
89
- ? '' // 正数金额:整数或小数,不允许负数、前导零以外形态。
68
+ formError.tranAmt = /^\d+(?:\.\d+)?$/.test(tranAmt) && /[1-9]/.test(tranAmt)
69
+ ? ''
90
70
  : '请输入大于 0 的调整金额'
91
- return !formError.adjustType && !formError.tranAmt
92
- }
71
+ if (!adjustType || formError.tranAmt) return
93
72
 
94
- // 提交注资/预注资发起;成功后关闭弹窗,并重新查询账户行余额。
95
- const submitCreate = async () => {
96
- if (pendingCreate.value || loading.value) return
97
- if (!validateForm()) return
98
- const params: InjectionCreateParams = {
99
- adjustType: createForm.adjustType as InjectionCreateParams['adjustType'],
100
- tranAmt: createForm.tranAmt.trim(),
101
- }
102
73
  pendingCreate.value = true
103
74
  try {
104
- // head 为公共响应头,请求是否成功统一以 retCode 判断;接口未声明业务返回字段。
105
- const { head } = await API.createApi(params) as any
75
+ const { head } = await API.createApi({ adjustType, tranAmt })
106
76
  if (disposed) return
107
77
  if (head?.retCode !== '00000') {
108
78
  throw new Error(head?.retMsg || '提交失败')
109
79
  }
110
80
  ElMessage.success('注资/预注资发起已提交')
111
81
  dialogVisible.value = false
112
- void query() // 重新查询账户行余额,展示最新余额。
113
- } catch (error) { // error 为提交请求或业务处理失败信息。
82
+ void query()
83
+ } catch (error) {
114
84
  if (!disposed) ElMessage.error(getErrorMessage(error))
115
85
  } finally {
116
- pendingCreate.value = false
86
+ if (!disposed) pendingCreate.value = false
117
87
  }
118
88
  }
119
89
 
120
- // 摘要金额:接口字段 → 卡片标题,顺序即展示顺序。
121
- const summaryAmounts = [
90
+ type BalanceField = keyof AccountBalanceInfo
91
+ type DisplayField = [field: BalanceField, label: string, isMoney?: boolean]
92
+
93
+ const summaryAmounts: DisplayField[] = [
122
94
  ['bal', '结算钱包余额'],
123
95
  ['amount', '实时余额'],
124
96
  ['enableAmount', '其中:可用余额'],
125
97
  ['yesterdayAmount', '昨日余额'],
126
98
  ['nonBal', '对立账户余额(预注资金额)'],
127
- ] as const
99
+ ]
128
100
 
129
- // 明细分组配置;title 为分组标题,fields 每项为 [字段名, 展示名称, 是否金额]。
130
- const detailGroups = [
101
+ // 字段受接口类型约束,金额标记同时用于格式化和样式。
102
+ const detailGroups: { title: string; fields: DisplayField[] }[] = [
131
103
  {
132
104
  title: '机构与账户',
133
105
  fields: [
134
- ['bankName', '机构名称', false],
135
- ['bankCode', '机构编码', false],
136
- ['innerName', '行内名称', false],
137
- ['innerAccount', '行内账号', false],
106
+ ['bankName', '机构名称'],
107
+ ['bankCode', '机构编码'],
108
+ ['innerName', '行内名称'],
109
+ ['innerAccount', '行内账号'],
138
110
  ],
139
111
  },
140
112
  {
141
113
  title: '结算钱包',
142
114
  fields: [
143
- ['sttlmWlltNm', '结算钱包名称', false],
144
- ['sttlmWlltId', '结算钱包 ID', false],
145
- ['sttlmWlltSts', '结算钱包状态', false],
146
- ['openDate', '开立时间', false],
115
+ ['sttlmWlltNm', '结算钱包名称'],
116
+ ['sttlmWlltId', '结算钱包 ID'],
117
+ ['sttlmWlltSts', '结算钱包状态'],
118
+ ['openDate', '开立时间'],
147
119
  ],
148
120
  },
149
121
  {
150
122
  title: '注资参数',
151
123
  fields: [
152
- ['ciSts', '注资预注资状态', false],
124
+ ['ciSts', '注资预注资状态'],
153
125
  ['ciLmt', '注资最低金额(元)', true],
154
126
  ['realTmTxLmt', '实时业务注资下限(元)', true],
155
127
  ['wltWarnAmt', '钱包预警金额(元)', true],
@@ -166,58 +138,48 @@ const detailGroups = [
166
138
  {
167
139
  title: '报文信息',
168
140
  fields: [
169
- ['refNo', '流水号', false],
170
- ['replyRefNo', '应答流水号', false],
171
- ['ptMsgId', '报文标识号', false],
172
- ['prtryDataType', '报文类型', false],
173
- ['sendOrganizeCode', '发起机构', false],
174
- ['rcvOrganizeCode', '接收机构', false],
175
- ['msgSendTime', '业务处理时间', false],
141
+ ['refNo', '流水号'],
142
+ ['replyRefNo', '应答流水号'],
143
+ ['ptMsgId', '报文标识号'],
144
+ ['prtryDataType', '报文类型'],
145
+ ['sendOrganizeCode', '发起机构'],
146
+ ['rcvOrganizeCode', '接收机构'],
147
+ ['msgSendTime', '业务处理时间'],
176
148
  ],
177
149
  },
178
150
  {
179
151
  title: '备注',
180
152
  fields: [
181
- ['remark', '备注', false],
182
- ['rmk', '交易备注', false],
153
+ ['remark', '备注'],
154
+ ['rmk', '交易备注'],
183
155
  ],
184
156
  },
185
- ] as const
186
-
187
- // 展示数据:金额统一千分位、时间本地化,字段名 → 展示值。
188
- const displayValues = computed<Record<string, string> | null>(() => {
189
- if (!balance.value) return null
190
- const raw = balance.value as unknown as Record<string, string | undefined>
191
- return Object.fromEntries(Object.entries(raw).map(([key, value]) => {
192
- const isAmount = detailGroups.some(group => group.fields.some(([field, , isMoney]) => field === key && isMoney))
193
- || summaryAmounts.some(([field]) => field === key) // 摘要金额同样千分位展示。
194
- const isTime = key === 'msgSendTime'
195
- if (key === 'sttlmWlltSts') return [key, formatWalletStatus(value)] // 钱包状态:先按字典映射,缺字典时保留原值。
196
- return [key, isAmount ? formatAmount(value) : isTime ? formatDateTime(value) : String(value ?? '')]
197
- }))
198
- })
157
+ ]
199
158
 
200
- // 读取展示值,空值显示占位符;field 为字段名。
201
- const displayValue = (field: string) => displayValues.value?.[field]?.trim() || '—'
159
+ const displayValue = (field: BalanceField, isMoney = false) => {
160
+ const value = balance.value?.[field]?.trim()
161
+ if (!value) return '—'
162
+ if (isMoney) return formatAmount(value)
163
+ return field === 'msgSendTime' ? formatDateTime(value) : value
164
+ }
202
165
 
203
166
  // 发起查询申请;接口未声明筛选条件,固定空参数。
204
167
  const query = async () => {
205
168
  if (disposed) return
206
- const currentRequestId = ++requestId // 本次请求序号,仅与最新序号相等时允许更新界面。
169
+ const currentRequestId = ++requestId
207
170
  loading.value = true
208
171
  try {
209
- // head 为公共响应头,body 为单条账户行余额信息。
210
- const { head, body } = await API.queryBalanceApi() as any
172
+ const { head, body } = await API.queryBalanceApi()
211
173
  if (currentRequestId !== requestId) return
212
174
  if (head?.retCode !== '00000') {
213
175
  throw new Error(head?.retMsg || '查询失败')
214
176
  }
215
- if (!body || typeof body !== 'object') {
177
+ if (!body || typeof body !== 'object' || Array.isArray(body)) {
216
178
  throw new Error('查询结果格式异常')
217
179
  }
218
180
  balance.value = body
219
181
  lastQueryTime.value = formatDateTime(new Date().toISOString())
220
- } catch (error) { // error 为接口请求或响应数据校验异常。
182
+ } catch (error) {
221
183
  if (currentRequestId !== requestId) return
222
184
  balance.value = null
223
185
  lastQueryTime.value = ''
@@ -227,7 +189,6 @@ const query = async () => {
227
189
  }
228
190
  }
229
191
 
230
- // 页面首次显示时自动发起查询申请。
231
192
  onMounted(query)
232
193
  // 页面离开后,未完成的请求不再更新界面或弹出消息。
233
194
  onBeforeUnmount(() => {
@@ -261,11 +222,10 @@ onBeforeUnmount(() => {
261
222
  </div>
262
223
  <el-tag class="wallet-status" type="warning" effect="light">{{ displayValue('sttlmWlltSts') }}</el-tag>
263
224
  </header>
264
- <!-- item 为摘要金额配置,value 为格式化后的展示值。 -->
265
225
  <div class="amount-grid">
266
226
  <div v-for="[field, label] in summaryAmounts" :key="field" class="amount-card">
267
227
  <span class="amount-label">{{ label }}</span>
268
- <strong class="amount-value">{{ displayValue(field) }}</strong>
228
+ <strong class="amount-value">{{ displayValue(field, true) }}</strong>
269
229
  <span class="amount-unit">元</span>
270
230
  </div>
271
231
  </div>
@@ -274,14 +234,13 @@ onBeforeUnmount(() => {
274
234
  <!-- 明细分组;group 为当前分组,包含标题和字段配置。无数据时展示空状态。 -->
275
235
  <el-card class="balance-detail" shadow="never" :body-style="{ padding: '12px 14px' }">
276
236
  <template v-if="balance">
277
- <!-- field 为字段名,isMoney 为金额标记,label 为展示名称。 -->
278
237
  <section v-for="group in detailGroups" :key="group.title" class="detail-group" :aria-label="group.title">
279
238
  <h3>{{ group.title }}</h3>
280
239
  <dl class="detail-fields">
281
- <div v-for="[field, label] in group.fields" :key="field" class="detail-field">
240
+ <div v-for="[field, label, isMoney] in group.fields" :key="field" class="detail-field">
282
241
  <dt>{{ label }}</dt>
283
- <dd :class="{ 'detail-money': group.fields.some(([f, , isMoney]) => f === field && isMoney) }">
284
- {{ displayValue(field) }}
242
+ <dd :class="{ 'detail-money': isMoney }">
243
+ {{ displayValue(field, isMoney) }}
285
244
  </dd>
286
245
  </div>
287
246
  </dl>
@@ -305,7 +264,6 @@ onBeforeUnmount(() => {
305
264
  :close-on-press-escape="!pendingCreate"
306
265
  >
307
266
  <el-form :disabled="pendingCreate" label-position="top" @submit.prevent="submitCreate()">
308
- <!-- value 为接口调整类型枚举,label 为中文含义。 -->
309
267
  <el-form-item label="调整类型" :error="formError.adjustType">
310
268
  <el-select v-model="createForm.adjustType" placeholder="请选择调整类型" clearable>
311
269
  <el-option