vue3-element-plus-app-chu 0.0.23 → 0.0.25
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
package/src/router/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ import DigitalRmbForecast from '@/view/dcps/forecast/index.vue'
|
|
|
6
6
|
import DigitalRmbInjection from '@/view/dcps/funding/injection/index.vue'
|
|
7
7
|
import DigitalRmbWriteOff from '@/view/dcps/write-off/index.vue'
|
|
8
8
|
import DigitalRmbPositionEstimation from '@/view/dcps/estimation/index.vue'
|
|
9
|
+
import DigitalRmbFlowQuery from '@/view/dcps/flow-query/index.vue'
|
|
9
10
|
import DigitalRmbParameters from '@/view/dcps/parameters/index.vue'
|
|
10
11
|
|
|
11
12
|
declare module 'vue-router' {
|
|
@@ -29,36 +30,6 @@ const routes: RouteRecordRaw[] = [
|
|
|
29
30
|
component: DigitalRmbForecast,
|
|
30
31
|
meta: { menu: 'forecast', title: '数字人民币头寸预报' },
|
|
31
32
|
},
|
|
32
|
-
{
|
|
33
|
-
path: 'forecast/create',
|
|
34
|
-
name: 'forecast-create',
|
|
35
|
-
component: DigitalRmbForecast,
|
|
36
|
-
meta: { menu: 'forecast', title: '新增数字人民币头寸预报' },
|
|
37
|
-
props: (route) => ({
|
|
38
|
-
mode: 'create',
|
|
39
|
-
tranDate: typeof route.query.tranDate === 'string' ? route.query.tranDate : undefined,
|
|
40
|
-
}),
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
path: 'forecast/:predRefNo/edit',
|
|
44
|
-
name: 'forecast-edit',
|
|
45
|
-
component: DigitalRmbForecast,
|
|
46
|
-
meta: { menu: 'forecast', title: '修改数字人民币头寸预报' },
|
|
47
|
-
props: (route) => ({
|
|
48
|
-
mode: 'edit',
|
|
49
|
-
predRefNo: String(route.params.predRefNo ?? ''),
|
|
50
|
-
}),
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
path: 'forecast/:predRefNo',
|
|
54
|
-
name: 'forecast-detail',
|
|
55
|
-
component: DigitalRmbForecast,
|
|
56
|
-
meta: { menu: 'forecast', title: '数字人民币头寸预报详情' },
|
|
57
|
-
props: (route) => ({
|
|
58
|
-
mode: 'detail',
|
|
59
|
-
predRefNo: String(route.params.predRefNo ?? ''),
|
|
60
|
-
}),
|
|
61
|
-
},
|
|
62
33
|
{
|
|
63
34
|
path: 'estimation',
|
|
64
35
|
name: 'estimation',
|
|
@@ -87,6 +58,12 @@ const routes: RouteRecordRaw[] = [
|
|
|
87
58
|
},
|
|
88
59
|
],
|
|
89
60
|
},
|
|
61
|
+
{
|
|
62
|
+
path: 'flow-query',
|
|
63
|
+
name: 'flow-query',
|
|
64
|
+
component: DigitalRmbFlowQuery,
|
|
65
|
+
meta: { menu: 'flow-query', title: '数字人民币流水查询' },
|
|
66
|
+
},
|
|
90
67
|
{
|
|
91
68
|
path: 'auth',
|
|
92
69
|
name: 'auth',
|
package/src/router/menus.ts
CHANGED
|
@@ -23,6 +23,7 @@ export const layoutMenus: LayoutMenu[] = [
|
|
|
23
23
|
{ name: 'balance', title: '数字人民币账户行余额查询申请', compactTitle: '余额查询' },
|
|
24
24
|
],
|
|
25
25
|
},
|
|
26
|
+
{ name: 'flow-query', title: '数字人民币流水查询', compactTitle: '流水查询' },
|
|
26
27
|
{ name: 'auth', title: '数字人民币授权', compactTitle: '授权' },
|
|
27
28
|
{ name: 'write-off', title: '数字人民币核销', compactTitle: '核销' },
|
|
28
29
|
{ name: 'parameters', title: '数字人民币参数管理', compactTitle: '参数管理' },
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import baseHttpClient from '@chic/http'
|
|
2
|
+
import type { FlowQueryParams } from './typs'
|
|
3
|
+
import type { Tam140502002OutBody } from './typs'
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
/**
|
|
7
|
+
* 分页查询数字人民币流水信息。
|
|
8
|
+
* 接口:140502002 头寸核销-流水查询(/TAM/0001/140502002)。
|
|
9
|
+
* @param params tranDate 交易日期(必填,YYYYMMDD);pageIndex、pageSize 分页;
|
|
10
|
+
* orgName 我方机构,payerName/payeeName 付款人/收款人名称,
|
|
11
|
+
* payDirec 收支方向,startTransAmt/endTransAmt 金额范围,verifyStatus 核销状态。
|
|
12
|
+
*/
|
|
13
|
+
queryFlowApi: (params: FlowQueryParams) =>
|
|
14
|
+
baseHttpClient.post<Tam140502002OutBody>('/TAM/0001/140502002', params),
|
|
15
|
+
}
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { onBeforeUnmount, onMounted, reactive, ref } from 'vue'
|
|
3
|
+
import { ElMessage } from 'element-plus'
|
|
4
|
+
import API from './api'
|
|
5
|
+
import { dcpsRuntime } from '../runtime'
|
|
6
|
+
import { ArtDataTable, type ColumnOption, type PaginationOptions, type TableSettings } from '@/components/table'
|
|
7
|
+
import type { FlowRow, FlowFilters, FlowQueryParams } from './typs'
|
|
8
|
+
import type { Tam140502002OutBody } from './typs'
|
|
9
|
+
|
|
10
|
+
defineOptions({ name: 'DcpsFlowQueryPage' })
|
|
11
|
+
|
|
12
|
+
// ---------- 页面内工具函数 ----------
|
|
13
|
+
|
|
14
|
+
/** 列表查询结果:当前页记录和符合条件的总条数。 */
|
|
15
|
+
interface FlowPage<T> {
|
|
16
|
+
data: T[] // 当前页的记录列表。
|
|
17
|
+
total: number // 符合查询条件的总记录数。
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** 校验公共响应头;retCode 不是 00000 时按服务端提示或 fallback 抛错。 */
|
|
21
|
+
const ensureSuccess = <T,>(response: any, fallback: string): T | null => {
|
|
22
|
+
if (response?.head?.retCode !== '00000') {
|
|
23
|
+
throw new Error(response?.head?.retMsg?.trim() || fallback)
|
|
24
|
+
}
|
|
25
|
+
return response.body
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 校验查询响应并取出当前页数据。 */
|
|
29
|
+
const readPage = <R extends { resultList?: unknown[]; totalCount?: unknown }>(
|
|
30
|
+
body: R | null,
|
|
31
|
+
fallback: string,
|
|
32
|
+
): FlowPage<NonNullable<R['resultList']>[number]> => {
|
|
33
|
+
if (!body || !Array.isArray(body.resultList)) throw new Error(fallback)
|
|
34
|
+
const totalText = String(body.totalCount ?? body.resultList.length)
|
|
35
|
+
const total = Number(totalText)
|
|
36
|
+
if (!/^\d+$/.test(totalText) || !Number.isSafeInteger(total)) throw new Error('查询返回的总条数无效')
|
|
37
|
+
return { data: body.resultList, total }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 提取可展示的错误信息;无法识别时给出统一提示。 */
|
|
41
|
+
const getErrorMessage = (error: unknown): string => {
|
|
42
|
+
if (error instanceof Error && error.message.trim()) return error.message
|
|
43
|
+
if (typeof error === 'string' && error.trim()) return error
|
|
44
|
+
return '操作失败,请稍后重试'
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 判断是否为合法的 YYYYMMDD 日期。 */
|
|
48
|
+
const isValidDate = (value: string): boolean => {
|
|
49
|
+
if (!/^\d{8}$/.test(value)) return false
|
|
50
|
+
const year = Number(value.slice(0, 4))
|
|
51
|
+
const month = Number(value.slice(4, 6))
|
|
52
|
+
const day = Number(value.slice(6, 8))
|
|
53
|
+
if (year < 1 || month < 1 || month > 12 || day < 1) return false
|
|
54
|
+
const leap = year % 400 === 0 || (year % 4 === 0 && year % 100 !== 0)
|
|
55
|
+
const monthDays = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
|
56
|
+
return day <= (monthDays[month - 1] || 0)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 把 YYYYMMDD 格式化为 YYYY-MM-DD 展示;空值显示横线,其他格式保留原文。 */
|
|
60
|
+
const formatDate = (value?: string): string => {
|
|
61
|
+
const text = value?.trim() || ''
|
|
62
|
+
if (!text) return '—'
|
|
63
|
+
return isValidDate(text) ? `${text.slice(0, 4)}-${text.slice(4, 6)}-${text.slice(6, 8)}` : text
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 只接受十进制文本和合法千分位;保留全部小数位,避免改变输入精度。 */
|
|
67
|
+
const normalizeAmount = (value: string): string => {
|
|
68
|
+
let text = value.trim()
|
|
69
|
+
if (text.includes(',')) {
|
|
70
|
+
if (!/^[+-]?\d{1,3}(?:,\d{3})+(?:\.\d*)?$/.test(text)) return ''
|
|
71
|
+
text = text.replace(/,/g, '')
|
|
72
|
+
}
|
|
73
|
+
const match = /^([+-]?)(\d*)(?:\.(\d*))?$/.exec(text)
|
|
74
|
+
if (!match || (!match[2] && !match[3])) return ''
|
|
75
|
+
const integer = (match[2] || '0').replace(/^0+(?=\d)/, '')
|
|
76
|
+
const fraction = match[3] || ''
|
|
77
|
+
const negative = match[1] === '-' && /[1-9]/.test(integer + fraction)
|
|
78
|
+
return `${negative ? '-' : ''}${integer}${fraction ? `.${fraction}` : ''}`
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** 千分位展示至少两位小数,超过两位的小数原样保留,不做浮点数转换或舍入。 */
|
|
82
|
+
const formatAmount = (value?: string): string => {
|
|
83
|
+
const original = value?.trim() || ''
|
|
84
|
+
if (!original) return '—'
|
|
85
|
+
const normalized = normalizeAmount(original)
|
|
86
|
+
if (!normalized) return original
|
|
87
|
+
const negative = normalized.startsWith('-')
|
|
88
|
+
const [integer = '0', fraction = ''] = (negative ? normalized.slice(1) : normalized).split('.')
|
|
89
|
+
const grouped = integer.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
|
90
|
+
return `${negative ? '-' : ''}${grouped}.${fraction.padEnd(2, '0')}`
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** 比较两个标准化后的金额字符串,返回 -1/0/1。 */
|
|
94
|
+
const compareAmounts = (left: string, right: string): number => {
|
|
95
|
+
const leftText = normalizeAmount(left)
|
|
96
|
+
const rightText = normalizeAmount(right)
|
|
97
|
+
if (!leftText || !rightText) {
|
|
98
|
+
if (!leftText && !rightText) return 0
|
|
99
|
+
return leftText ? -1 : 1
|
|
100
|
+
}
|
|
101
|
+
const parts = (text: string) => {
|
|
102
|
+
const negative = text.startsWith('-')
|
|
103
|
+
const [integer = '0', fraction = ''] = (negative ? text.slice(1) : text).split('.')
|
|
104
|
+
return { negative, integer, fraction }
|
|
105
|
+
}
|
|
106
|
+
const a = parts(leftText)
|
|
107
|
+
const b = parts(rightText)
|
|
108
|
+
const precision = Math.max(a.fraction.length, b.fraction.length)
|
|
109
|
+
const toInteger = (amount: ReturnType<typeof parts>) =>
|
|
110
|
+
BigInt(amount.integer + amount.fraction.padEnd(precision, '0')) * (amount.negative ? -1n : 1n)
|
|
111
|
+
const aValue = toInteger(a)
|
|
112
|
+
const bValue = toInteger(b)
|
|
113
|
+
return aValue < bValue ? -1 : aValue > bValue ? 1 : 0
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** 业务类型字典:201 兑回、202 兑出、203 付现;超出字典的值保留原文。 */
|
|
117
|
+
const busiTypeLabels: Record<string, string> = { '201': '兑回', '202': '兑出', '203': '付现' }
|
|
118
|
+
/** 收支方向字典:I 来账、O 往账。 */
|
|
119
|
+
const payDirecLabels: Record<string, string> = { I: '来账', O: '往账' }
|
|
120
|
+
/** 支付渠道:5、8 均表示数字人民币,其余渠道保留原文。 */
|
|
121
|
+
const payChannelLabels: Record<string, string> = { '5': '数字人民币', '8': '数字人民币' }
|
|
122
|
+
/** 核销状态字典:1 未核销、2 已核销。 */
|
|
123
|
+
const verifyStatusLabels: Record<string, string> = { '1': '未核销', '2': '已核销' }
|
|
124
|
+
|
|
125
|
+
/** 读取字典文案;空值显示横线,未登记的代码保留原文展示。 */
|
|
126
|
+
const dictLabel = (labels: Record<string, string>, value?: string): string => {
|
|
127
|
+
const text = value?.trim() || ''
|
|
128
|
+
return text ? labels[text] ?? text : '—'
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** 校验金额文本:非负十进制,最多两位小数;返回标准化后的文本。 */
|
|
132
|
+
const validAmount = (value: string) => {
|
|
133
|
+
const normalized = normalizeAmount(value)
|
|
134
|
+
if (!normalized || normalized.startsWith('-') || (normalized.split('.')[1]?.length || 0) > 2) return ''
|
|
135
|
+
return normalized
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ---------- 查询条件与列表状态 ----------
|
|
139
|
+
|
|
140
|
+
let disposed = false // 页面是否已卸载,避免未完成请求继续更新界面。
|
|
141
|
+
|
|
142
|
+
// 默认查询条件;空字符串表示不限制该条件,重置时恢复此配置。
|
|
143
|
+
const createFlowFilters = () => ({
|
|
144
|
+
tranDate: dcpsRuntime.date, // 必填交易日期,格式为 YYYYMMDD。
|
|
145
|
+
orgName: '', // 我方机构。
|
|
146
|
+
payerName: '', // 付款人名称。
|
|
147
|
+
payeeName: '', // 收款人名称。
|
|
148
|
+
payDirec: '', // 收支方向:I 来账、O 往账。
|
|
149
|
+
startTransAmt: '', // 起始金额。
|
|
150
|
+
endTransAmt: '', // 截止金额。
|
|
151
|
+
verifyStatus: '', // 核销状态:1 未核销、2 已核销。
|
|
152
|
+
})
|
|
153
|
+
const flowFilters = ref<FlowFilters>(createFlowFilters())
|
|
154
|
+
let flowAppliedQuery: FlowQueryParams = { ...createFlowFilters(), pageIndex: 1, pageSize: 15 }
|
|
155
|
+
|
|
156
|
+
// 重复输入项共用一份配置,依次为字段、名称和输入框长度限制。
|
|
157
|
+
const flowTextFilters = [
|
|
158
|
+
['orgName', '我方机构', 160],
|
|
159
|
+
['payerName', '付款人名称', 180],
|
|
160
|
+
['payeeName', '收款人名称', 180],
|
|
161
|
+
] as const
|
|
162
|
+
|
|
163
|
+
const busiTypeOptions = Object.entries(busiTypeLabels).map(([value, label]) => ({ value, label }))
|
|
164
|
+
const payDirecOptions = Object.entries(payDirecLabels).map(([value, label]) => ({ value, label }))
|
|
165
|
+
const verifyStatusOptions = Object.entries(verifyStatusLabels).map(([value, label]) => ({ value, label }))
|
|
166
|
+
const payChannelOptions = Object.entries(payChannelLabels).map(([value, label]) => ({ value, label }))
|
|
167
|
+
|
|
168
|
+
// 列表、分页与请求状态;请求序号防止旧响应覆盖新结果。
|
|
169
|
+
const flowRows = ref<FlowRow[]>([])
|
|
170
|
+
const flowPagination = reactive({ current: 1, size: 15, total: 0 })
|
|
171
|
+
const flowPaginationOptions: PaginationOptions = { align: 'right', pageSizes: [15, 30, 50, 100], background: false }
|
|
172
|
+
const flowTableSettings = ref<TableSettings>({ size: 'small', stripe: true, border: true, headerBackground: true })
|
|
173
|
+
const flowLoading = ref(false)
|
|
174
|
+
const flowQueryError = ref('')
|
|
175
|
+
const flowTableRef = ref<InstanceType<typeof ArtDataTable> | null>(null)
|
|
176
|
+
let flowRequestId = 0
|
|
177
|
+
|
|
178
|
+
/** 生成普通文本列,默认将空值显示为横线。 */
|
|
179
|
+
const flowTextColumn = (prop: keyof FlowRow, label: string, width: number, visible = true): ColumnOption<FlowRow> => ({
|
|
180
|
+
prop, label, width, visible, showOverflowTooltip: true,
|
|
181
|
+
formatter: (row) => row[prop]?.trim() || '—',
|
|
182
|
+
})
|
|
183
|
+
/** 生成字典翻译列;未登记的代码保留原文展示。 */
|
|
184
|
+
const flowDictColumn = (prop: keyof FlowRow, label: string, width: number, labels: Record<string, string>): ColumnOption<FlowRow> => ({
|
|
185
|
+
...flowTextColumn(prop, label, width),
|
|
186
|
+
formatter: (row) => dictLabel(labels, row[prop]),
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
// 表格完整列配置;字段顺序即展示顺序,辅助字段默认隐藏。
|
|
190
|
+
const flowColumns: ColumnOption<FlowRow>[] = [
|
|
191
|
+
{ type: 'globalIndex', label: '序号', width: 60, align: 'center' },
|
|
192
|
+
flowTextColumn('refNo', '流水号', 190),
|
|
193
|
+
{ ...flowTextColumn('tranDate', '交易日期', 110), formatter: (row) => formatDate(row.tranDate) },
|
|
194
|
+
flowTextColumn('tranTime', '交易时间', 110),
|
|
195
|
+
flowDictColumn('payDirec', '收支方向', 90, payDirecLabels),
|
|
196
|
+
flowTextColumn('payerName', '付款人名称', 170),
|
|
197
|
+
flowTextColumn('payeeName', '收款人名称', 170),
|
|
198
|
+
{ prop: 'tranAmt', label: '交易金额(元)', width: 160, align: 'right', useSlot: true, slotName: 'tranAmount' },
|
|
199
|
+
flowDictColumn('payChannel', '支付渠道', 110, payChannelLabels),
|
|
200
|
+
flowDictColumn('busiType', '业务类型', 95, busiTypeLabels),
|
|
201
|
+
flowTextColumn('busiStatus', '业务状态', 110),
|
|
202
|
+
flowDictColumn('verifyStatus', '核销状态', 95, verifyStatusLabels),
|
|
203
|
+
flowTextColumn('branchName', '我方机构', 150),
|
|
204
|
+
flowTextColumn('collBankName', '收款行名称', 160, false),
|
|
205
|
+
flowTextColumn('payBankName', '付款行名称', 160, false),
|
|
206
|
+
flowTextColumn('branchCode', '机构行号', 150, false),
|
|
207
|
+
flowTextColumn('collBankNo', '收款行行号', 170, false),
|
|
208
|
+
flowTextColumn('payBankNo', '付款行行号', 170, false),
|
|
209
|
+
flowTextColumn('collWltId', '收款钱包ID', 190, false),
|
|
210
|
+
flowTextColumn('payWltId', '付款钱包ID', 190, false),
|
|
211
|
+
flowTextColumn('remark', '备注', 160, false),
|
|
212
|
+
]
|
|
213
|
+
|
|
214
|
+
// 请求序号防止旧响应覆盖新结果;页码越界时最多补查一次。
|
|
215
|
+
const loadFlowList = async () => {
|
|
216
|
+
if (disposed) return
|
|
217
|
+
const currentId = ++flowRequestId
|
|
218
|
+
const query = { ...flowAppliedQuery }
|
|
219
|
+
const pageSize = flowPagination.size
|
|
220
|
+
let pageIndex = flowPagination.current
|
|
221
|
+
flowLoading.value = true
|
|
222
|
+
flowQueryError.value = ''
|
|
223
|
+
flowTableRef.value?.getElTable()?.clearSelection()
|
|
224
|
+
try {
|
|
225
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
226
|
+
const response = await API.queryFlowApi({ ...query, pageIndex, pageSize })
|
|
227
|
+
if (currentId !== flowRequestId) return
|
|
228
|
+
const page = readPage<Tam140502002OutBody>(
|
|
229
|
+
ensureSuccess<Tam140502002OutBody>(response, '流水查询失败'),
|
|
230
|
+
'查询结果缺少流水列表,请联系服务端确认返回数据',
|
|
231
|
+
)
|
|
232
|
+
const lastPage = Math.max(1, Math.ceil(page.total / pageSize))
|
|
233
|
+
if (pageIndex > lastPage && attempt === 0) {
|
|
234
|
+
pageIndex = lastPage
|
|
235
|
+
continue
|
|
236
|
+
}
|
|
237
|
+
flowPagination.current = pageIndex
|
|
238
|
+
flowPagination.total = page.total
|
|
239
|
+
flowRows.value = page.data
|
|
240
|
+
return
|
|
241
|
+
}
|
|
242
|
+
} catch (error) {
|
|
243
|
+
if (currentId !== flowRequestId) return
|
|
244
|
+
flowRows.value = []
|
|
245
|
+
flowPagination.total = 0
|
|
246
|
+
flowQueryError.value = getErrorMessage(error)
|
|
247
|
+
} finally {
|
|
248
|
+
if (currentId === flowRequestId) flowLoading.value = false
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// 新查询从第一页开始;重置后复用同一查询流程。
|
|
253
|
+
const searchFlow = () => {
|
|
254
|
+
if (disposed) return
|
|
255
|
+
const source = flowFilters.value
|
|
256
|
+
if (!/^\d{8}$/.test(source.tranDate.trim())) return void ElMessage.warning('请选择交易日期')
|
|
257
|
+
const startTransAmt = validAmount(source.startTransAmt)
|
|
258
|
+
if (source.startTransAmt.trim() && !startTransAmt) {
|
|
259
|
+
return void ElMessage.warning('起始金额请输入非负数,最多保留两位小数')
|
|
260
|
+
}
|
|
261
|
+
const endTransAmt = validAmount(source.endTransAmt)
|
|
262
|
+
if (source.endTransAmt.trim() && !endTransAmt) {
|
|
263
|
+
return void ElMessage.warning('截止金额请输入非负数,最多保留两位小数')
|
|
264
|
+
}
|
|
265
|
+
if (startTransAmt && endTransAmt && compareAmounts(startTransAmt, endTransAmt) > 0) {
|
|
266
|
+
return void ElMessage.warning('起始金额不能大于截止金额')
|
|
267
|
+
}
|
|
268
|
+
// 只保留已填写的筛选条件;空字符串不发送,表示不限制该条件。
|
|
269
|
+
const query: FlowQueryParams = { tranDate: source.tranDate.trim(), pageIndex: 1, pageSize: flowPagination.size }
|
|
270
|
+
for (const [key] of flowTextFilters) {
|
|
271
|
+
const value = source[key].trim()
|
|
272
|
+
if (value) query[key] = value
|
|
273
|
+
}
|
|
274
|
+
if (source.payDirec.trim()) query.payDirec = source.payDirec.trim()
|
|
275
|
+
if (source.verifyStatus.trim()) query.verifyStatus = source.verifyStatus.trim()
|
|
276
|
+
if (startTransAmt) query.startTransAmt = startTransAmt
|
|
277
|
+
if (endTransAmt) query.endTransAmt = endTransAmt
|
|
278
|
+
flowAppliedQuery = query
|
|
279
|
+
void loadFlowList()
|
|
280
|
+
}
|
|
281
|
+
const resetFlowFilters = () => {
|
|
282
|
+
if (disposed) return
|
|
283
|
+
flowFilters.value = createFlowFilters()
|
|
284
|
+
searchFlow()
|
|
285
|
+
}
|
|
286
|
+
// 更新分页;current 为目标页码,size 为每页条数,切换条数时 current 传入 1。
|
|
287
|
+
const changeFlowPage = (current: number, size = flowPagination.size) => {
|
|
288
|
+
if (disposed) return
|
|
289
|
+
flowPagination.current = current
|
|
290
|
+
flowPagination.size = size
|
|
291
|
+
void loadFlowList()
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// 页面首次显示时,按默认条件加载流水列表。
|
|
295
|
+
onMounted(() => {
|
|
296
|
+
void loadFlowList()
|
|
297
|
+
})
|
|
298
|
+
// 页面离开后,未完成的请求不再更新界面或弹出消息。
|
|
299
|
+
onBeforeUnmount(() => {
|
|
300
|
+
disposed = true
|
|
301
|
+
flowRequestId += 1
|
|
302
|
+
})
|
|
303
|
+
</script>
|
|
304
|
+
|
|
305
|
+
<template>
|
|
306
|
+
<main class="dcps-page">
|
|
307
|
+
<section class="view-region">
|
|
308
|
+
<!-- 查询条件:控件尺寸跟随表格设置,卡片内边距为 10px,点击查询生效。 -->
|
|
309
|
+
<el-card class="flow-search" shadow="never" :body-style="{ padding: '10px' }">
|
|
310
|
+
<el-form class="filter-form" :model="flowFilters" :size="flowTableSettings.size" label-position="top" @submit.prevent="searchFlow">
|
|
311
|
+
<el-form-item class="filter-date" label="交易日期" required>
|
|
312
|
+
<el-date-picker v-model="flowFilters.tranDate" type="date" format="YYYY-MM-DD" value-format="YYYYMMDD" :clearable="false" :editable="false" />
|
|
313
|
+
</el-form-item>
|
|
314
|
+
<el-form-item class="filter-status" label="核销状态">
|
|
315
|
+
<el-select v-model="flowFilters.verifyStatus" clearable placeholder="不限">
|
|
316
|
+
<el-option v-for="option in verifyStatusOptions" :key="option.value" :label="option.label" :value="option.value" />
|
|
317
|
+
</el-select>
|
|
318
|
+
</el-form-item>
|
|
319
|
+
<el-form-item class="filter-status" label="收支方向">
|
|
320
|
+
<el-select v-model="flowFilters.payDirec" clearable placeholder="不限">
|
|
321
|
+
<el-option v-for="option in payDirecOptions" :key="option.value" :label="option.label" :value="option.value" />
|
|
322
|
+
</el-select>
|
|
323
|
+
</el-form-item>
|
|
324
|
+
<!-- prop 为查询字段,label 为字段名称,maxlength 为允许输入的最大字符数。 -->
|
|
325
|
+
<el-form-item v-for="[key, label, maxlength] in flowTextFilters" :key="key" :label="label">
|
|
326
|
+
<el-input v-model="flowFilters[key]" :placeholder="`输入${label}`" :maxlength="maxlength" clearable />
|
|
327
|
+
</el-form-item>
|
|
328
|
+
<el-form-item class="filter-amount" label="起始金额(元)">
|
|
329
|
+
<el-input v-model="flowFilters.startTransAmt" placeholder="不限制" inputmode="decimal" :maxlength="40" clearable />
|
|
330
|
+
</el-form-item>
|
|
331
|
+
<el-form-item class="filter-amount" label="截止金额(元)">
|
|
332
|
+
<el-input v-model="flowFilters.endTransAmt" placeholder="不限制" inputmode="decimal" :maxlength="40" clearable />
|
|
333
|
+
</el-form-item>
|
|
334
|
+
<el-space class="filter-actions" :size="8">
|
|
335
|
+
<el-button @click="resetFlowFilters">重置</el-button>
|
|
336
|
+
<el-button type="primary" native-type="submit" :loading="flowLoading">查询</el-button>
|
|
337
|
+
</el-space>
|
|
338
|
+
</el-form>
|
|
339
|
+
</el-card>
|
|
340
|
+
|
|
341
|
+
<el-alert v-if="flowQueryError" :title="flowQueryError" type="error" show-icon :closable="false">
|
|
342
|
+
<el-button link type="primary" :disabled="flowLoading" @click="void loadFlowList()">重新查询</el-button>
|
|
343
|
+
</el-alert>
|
|
344
|
+
|
|
345
|
+
<!-- 流水信息表格:分页由表格内置分页组件承载。 -->
|
|
346
|
+
<div class="table-region">
|
|
347
|
+
<ArtDataTable
|
|
348
|
+
ref="flowTableRef"
|
|
349
|
+
v-model:settings="flowTableSettings"
|
|
350
|
+
:show-search-bar="false"
|
|
351
|
+
class="flow-table"
|
|
352
|
+
fill
|
|
353
|
+
title="流水信息"
|
|
354
|
+
toolbar-layout="search,refresh,size,fullscreen,columns,settings"
|
|
355
|
+
:data="flowRows"
|
|
356
|
+
:columns="flowColumns"
|
|
357
|
+
:loading="flowLoading"
|
|
358
|
+
:pagination="flowPagination"
|
|
359
|
+
:pagination-options="flowPaginationOptions"
|
|
360
|
+
row-key="refNo"
|
|
361
|
+
:empty-text="flowQueryError ? '查询失败,请重新查询' : '暂无符合条件的流水信息'"
|
|
362
|
+
@pagination:current-change="changeFlowPage"
|
|
363
|
+
@pagination:size-change="(size: number) => changeFlowPage(1, size)"
|
|
364
|
+
@refresh="void loadFlowList()"
|
|
365
|
+
>
|
|
366
|
+
<!-- row 为当前行原始流水记录,金额字段单位为元。 -->
|
|
367
|
+
<template #tranAmount="{ row }"><span class="amount-cell">{{ formatAmount(row.tranAmt) }}</span></template>
|
|
368
|
+
</ArtDataTable>
|
|
369
|
+
</div>
|
|
370
|
+
</section>
|
|
371
|
+
</main>
|
|
372
|
+
</template>
|
|
373
|
+
|
|
374
|
+
<style scoped>
|
|
375
|
+
/* 页面先扣除宿主占用高度,再使用 flex 分配剩余高度,列表内部滚动并保留分页。 */
|
|
376
|
+
.dcps-page {
|
|
377
|
+
box-sizing: border-box;
|
|
378
|
+
display: flex;
|
|
379
|
+
width: 100%;
|
|
380
|
+
max-width: 100%;
|
|
381
|
+
flex-direction: column;
|
|
382
|
+
gap: 5px;
|
|
383
|
+
height: calc(100vh - var(--dpcs-host-layout-height));
|
|
384
|
+
height: calc(100dvh - var(--dpcs-host-layout-height));
|
|
385
|
+
min-height: 0;
|
|
386
|
+
min-width: 0;
|
|
387
|
+
padding: 5px;
|
|
388
|
+
overflow: hidden;
|
|
389
|
+
background: var(--el-bg-color-page, #f7f8fa);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/* 视图区域:纵向占父页面剩余高度,列表内部滚动并保留分页。 */
|
|
393
|
+
.view-region {
|
|
394
|
+
display: flex;
|
|
395
|
+
flex: 1;
|
|
396
|
+
flex-direction: column;
|
|
397
|
+
gap: 5px;
|
|
398
|
+
min-height: 0;
|
|
399
|
+
min-width: 0;
|
|
400
|
+
}
|
|
401
|
+
/* 筛选项压成一行,等分剩余宽度,窗口过窄时才换行并限制高度,为列表保留空间。 */
|
|
402
|
+
.flow-search {
|
|
403
|
+
flex: 0 0 auto;
|
|
404
|
+
max-height: min(38dvh, 320px);
|
|
405
|
+
overflow-y: auto;
|
|
406
|
+
}
|
|
407
|
+
.filter-form { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px 6px; }
|
|
408
|
+
.filter-form :deep(.el-form-item) { flex: 1 1 76px; min-width: 76px; margin: 0; }
|
|
409
|
+
.filter-form :deep(.el-form-item__label) { height: auto; margin-bottom: 4px; padding: 0; font-size: 12px; line-height: 16px; }
|
|
410
|
+
/* 下拉和日期控件撑满筛选格,避免宽度塌缩。 */
|
|
411
|
+
.filter-form :deep(.el-select), .filter-form :deep(.el-date-editor) { width: 100%; min-width: 0; }
|
|
412
|
+
.filter-form :deep(.filter-date) { flex: 1.5 0 116px; }
|
|
413
|
+
.filter-form :deep(.filter-status) { flex: 1.1 0 96px; }
|
|
414
|
+
.filter-form :deep(.filter-amount) { flex: 1.2 0 92px; }
|
|
415
|
+
.filter-actions { flex: 0 0 auto; }
|
|
416
|
+
.table-region { flex: 1; min-width: 0; min-height: 0; }
|
|
417
|
+
.flow-table { padding: 10px; }
|
|
418
|
+
.amount-cell { color: var(--el-text-color-primary); font-variant-numeric: tabular-nums; }
|
|
419
|
+
|
|
420
|
+
@media (max-width: 640px) {
|
|
421
|
+
.filter-form :deep(.el-form-item) { flex-basis: 100%; }
|
|
422
|
+
.filter-actions { margin-left: 0; }
|
|
423
|
+
}
|
|
424
|
+
</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** 数字人民币流水查询(140502002)的类型定义,复用核销页面的协议类型。 */
|
|
2
|
+
export type {
|
|
3
|
+
/** 140502002 请求体业务字段。 */
|
|
4
|
+
Tam140502002InBody,
|
|
5
|
+
/** 140502002 响应体。 */
|
|
6
|
+
Tam140502002OutBody,
|
|
7
|
+
/** 140502002 返回的单条流水记录。 */
|
|
8
|
+
Tam140502002OutBodyResultList,
|
|
9
|
+
/** 流水查询条件;空字符串表示不限制该条件。 */
|
|
10
|
+
FlowFilters,
|
|
11
|
+
/** 流水查询参数:必填 tranDate,分页从第 1 页开始。 */
|
|
12
|
+
FlowQueryParams,
|
|
13
|
+
/** 流水查询(140502002)返回的单条流水记录。 */
|
|
14
|
+
FlowRow,
|
|
15
|
+
} from '../write-off/typs'
|
|
@@ -21,7 +21,6 @@ const recallForecastRow = (predRefNo?: string): ForecastRowType | null => {
|
|
|
21
21
|
|
|
22
22
|
<script setup lang="ts">
|
|
23
23
|
import { computed, onActivated, onBeforeUnmount, reactive, ref, watch } from 'vue'
|
|
24
|
-
import { useRouter } from 'vue-router'
|
|
25
24
|
import { ElConfigProvider, ElMessage, ElMessageBox } from 'element-plus'
|
|
26
25
|
import API from './api'
|
|
27
26
|
import { dcpsRuntime } from '../runtime'
|
|
@@ -41,17 +40,8 @@ defineOptions({ name: 'DcpsForecastPage' })
|
|
|
41
40
|
|
|
42
41
|
/**
|
|
43
42
|
* 预报页面:列表、新增/修改、详情共用一个入口文件。
|
|
44
|
-
*
|
|
43
|
+
* 新增/修改/详情直接在当前页面以对话框打开,不跳转新页面。
|
|
45
44
|
*/
|
|
46
|
-
const props = defineProps<{
|
|
47
|
-
mode?: 'create' | 'edit' | 'detail' // 页面形态;缺省为列表页。
|
|
48
|
-
predRefNo?: string // 修改、详情模式的预报流水号,来自路由参数。
|
|
49
|
-
tranDate?: string // 新增模式的默认交易日期,格式为 YYYYMMDD。
|
|
50
|
-
}>()
|
|
51
|
-
|
|
52
|
-
const router = useRouter()
|
|
53
|
-
const isEditor = computed(() => props.mode === 'create' || props.mode === 'edit')
|
|
54
|
-
const isDetail = computed(() => props.mode === 'detail')
|
|
55
45
|
|
|
56
46
|
// ---------- 页面内工具函数 ----------
|
|
57
47
|
|
|
@@ -243,6 +233,13 @@ const listBusy = computed(() => listMutation.value !== null)
|
|
|
243
233
|
const listTableRef = ref<InstanceType<typeof ArtDataTable> | null>(null)
|
|
244
234
|
let listRequestId = 0
|
|
245
235
|
|
|
236
|
+
// ---------- 对话框状态 ----------
|
|
237
|
+
const editorDialogVisible = ref(false) // 新增/修改对话框是否显示。
|
|
238
|
+
const editorDialogMode = ref<'create' | 'edit'>('create') // 当前对话框形态。
|
|
239
|
+
const editorDialogPredRefNo = ref('') // 修改模式的预报流水号。
|
|
240
|
+
const detailDialogVisible = ref(false) // 详情对话框是否显示。
|
|
241
|
+
const detailDialogPredRefNo = ref('') // 详情模式的预报流水号。
|
|
242
|
+
|
|
246
243
|
const listHasReference = (row: ForecastRow) => Boolean(row.predRefNo?.trim())
|
|
247
244
|
const listTextColumn = (prop: keyof ForecastRow, label: string, width: number, visible = true): ColumnOption<ForecastRow> => ({
|
|
248
245
|
prop, label, width, visible, showOverflowTooltip: true,
|
|
@@ -342,25 +339,29 @@ const changeListPage = (current: number, size = listPagination.size) => {
|
|
|
342
339
|
void loadList()
|
|
343
340
|
}
|
|
344
341
|
|
|
345
|
-
//
|
|
342
|
+
// 行内操作交给对话框处理,整行记录用于补齐详情接口没有返回的字段。
|
|
346
343
|
const openEdit = (row: ForecastRow) => {
|
|
347
344
|
if (listBusy.value || !listHasReference(row)) return
|
|
348
345
|
const predRefNo = row.predRefNo?.trim()
|
|
349
346
|
if (!predRefNo) return
|
|
350
347
|
rememberForecastRow(row)
|
|
351
|
-
|
|
348
|
+
editorDialogPredRefNo.value = predRefNo
|
|
349
|
+
editorDialogMode.value = 'edit'
|
|
350
|
+
editorDialogVisible.value = true
|
|
352
351
|
}
|
|
353
352
|
const openDetail = (row: ForecastRow) => {
|
|
354
353
|
if (listBusy.value || !listHasReference(row)) return
|
|
355
|
-
const predRefNo = row.predRefNo?.trim()
|
|
354
|
+
const predRefNo = row.prefRefNo ?? row.predRefNo?.trim()
|
|
356
355
|
if (!predRefNo) return
|
|
357
356
|
rememberForecastRow(row)
|
|
358
|
-
|
|
357
|
+
detailDialogPredRefNo.value = predRefNo
|
|
358
|
+
detailDialogVisible.value = true
|
|
359
359
|
}
|
|
360
360
|
const openCreate = () => {
|
|
361
361
|
if (listBusy.value) return
|
|
362
|
-
|
|
363
|
-
|
|
362
|
+
editorDialogPredRefNo.value = ''
|
|
363
|
+
editorDialogMode.value = 'create'
|
|
364
|
+
editorDialogVisible.value = true
|
|
364
365
|
}
|
|
365
366
|
|
|
366
367
|
async function deleteRecords(records: ForecastRow[]) {
|
|
@@ -387,7 +388,7 @@ async function deleteRecords(records: ForecastRow[]) {
|
|
|
387
388
|
|
|
388
389
|
// 列表页由 KeepAlive 缓存;每次回到列表时重新查询,保留筛选条件和分页。
|
|
389
390
|
onActivated(() => {
|
|
390
|
-
|
|
391
|
+
void loadList()
|
|
391
392
|
})
|
|
392
393
|
|
|
393
394
|
// ---------- 新增 / 修改页 ----------
|
|
@@ -401,7 +402,8 @@ type EditorField = {
|
|
|
401
402
|
createOnly?: boolean
|
|
402
403
|
}
|
|
403
404
|
|
|
404
|
-
const editorMode = computed<'create' | 'edit'>(() =>
|
|
405
|
+
const editorMode = computed<'create' | 'edit'>(() => editorDialogMode.value)
|
|
406
|
+
const editorPredRefNo = computed(() => editorDialogPredRefNo.value?.trim() || '')
|
|
405
407
|
|
|
406
408
|
/** 修改时允许提交的字段;与新增接口保持同一组业务字段。 */
|
|
407
409
|
const editableFields = [
|
|
@@ -457,7 +459,6 @@ const loadError = ref('')
|
|
|
457
459
|
const initialValues = ref<Partial<ForecastFormValues>>({})
|
|
458
460
|
const requiredFields = new Set<FieldKey>(['clientName', 'payDirec', 'busiType', 'payChannel', 'groundBranch', 'tranDate', 'predDate', 'predAmt'])
|
|
459
461
|
const editorDisabled = computed(() => editorLoading.value || submitting.value)
|
|
460
|
-
const editorPredRefNo = computed(() => props.predRefNo?.trim() || '')
|
|
461
462
|
const editorTitle = computed(() => editorMode.value === 'create' ? '新增数字人民币头寸预报' : '修改数字人民币头寸预报')
|
|
462
463
|
const visibleGroups = computed(() => fieldGroups.map(group => ({
|
|
463
464
|
...group,
|
|
@@ -482,7 +483,7 @@ const fieldPlaceholder = (field: EditorField) => {
|
|
|
482
483
|
const initializeForm = () => {
|
|
483
484
|
const nextValues = createFormValues()
|
|
484
485
|
if (editorMode.value === 'create') {
|
|
485
|
-
nextValues.tranDate =
|
|
486
|
+
nextValues.tranDate = dcpsRuntime.date
|
|
486
487
|
nextValues.predDate = dcpsRuntime.date
|
|
487
488
|
} else {
|
|
488
489
|
for (const key of Object.keys(nextValues) as FieldKey[]) {
|
|
@@ -532,12 +533,7 @@ const loadEditorDetail = async () => {
|
|
|
532
533
|
}
|
|
533
534
|
}
|
|
534
535
|
|
|
535
|
-
//
|
|
536
|
-
onActivated(() => {
|
|
537
|
-
if (!isEditor.value) return
|
|
538
|
-
if (editorMode.value === 'create') initializeForm()
|
|
539
|
-
else void loadEditorDetail()
|
|
540
|
-
})
|
|
536
|
+
// 新增或修改每次打开对话框时重新初始化。
|
|
541
537
|
|
|
542
538
|
watch(form, () => { formError.value = '' }, { deep: true })
|
|
543
539
|
|
|
@@ -636,7 +632,7 @@ const detailGroups: { title: string; fields: DetailField[] }[] = [
|
|
|
636
632
|
const detail = ref<ForecastDetail | null>(null)
|
|
637
633
|
const detailLoading = ref(false)
|
|
638
634
|
const detailError = ref('')
|
|
639
|
-
const detailPredRefNo = computed(() =>
|
|
635
|
+
const detailPredRefNo = computed(() => detailDialogPredRefNo.value?.trim() || '')
|
|
640
636
|
const detailTitle = computed(() => detailPredRefNo.value ? `预报详情 · ${detailPredRefNo.value}` : '数字人民币头寸预报详情')
|
|
641
637
|
const detailRow = computed<ForecastRow>(() => ({ ...recallForecastRow(detailPredRefNo.value), predRefNo: detailPredRefNo.value }))
|
|
642
638
|
|