vue3-element-plus-app-chu 0.0.13 → 0.0.15

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.
Files changed (28) hide show
  1. package/README.md +4 -4
  2. package/package.json +3 -2
  3. package/src/layout/index.vue +21 -36
  4. package/src/router/index.ts +107 -0
  5. package/src/router/menus.ts +16 -0
  6. package/src/utils/request.ts +3 -1
  7. package/src/view/dcps/README.md +52 -0
  8. package/src/view/dcps/auth/index.vue +151 -14
  9. package/src/view/dcps/balance/index.vue +4 -6
  10. package/src/view/dcps/common.css +7 -0
  11. package/src/view/dcps/estimation/api.ts +18 -148
  12. package/src/view/dcps/estimation/index.vue +312 -13
  13. package/src/view/dcps/forecast/api.ts +21 -218
  14. package/src/view/dcps/forecast/index.vue +996 -50
  15. package/src/view/dcps/injection/index.vue +154 -11
  16. package/src/view/dcps/write-off/api.ts +25 -180
  17. package/src/view/dcps/write-off/index.vue +776 -39
  18. package/src/view/dcps/auth/components/.gitkeep +0 -0
  19. package/src/view/dcps/auth/components/AuthorizationDetailDialog.vue +0 -171
  20. package/src/view/dcps/estimation/components/EstimationDetailDialog.vue +0 -222
  21. package/src/view/dcps/forecast/components/ForecastDetailView.vue +0 -145
  22. package/src/view/dcps/forecast/components/ForecastEditorView.vue +0 -320
  23. package/src/view/dcps/forecast/components/ForecastListView.vue +0 -322
  24. package/src/view/dcps/forecast/components/ForecastPageShell.vue +0 -56
  25. package/src/view/dcps/injection/components/InjectionDetailDialog.vue +0 -171
  26. package/src/view/dcps/runtime.ts +0 -38
  27. package/src/view/dcps/write-off/components/FlowWriteOffView.vue +0 -299
  28. package/src/view/dcps/write-off/components/ForecastWriteOffView.vue +0 -437
@@ -1,159 +1,29 @@
1
- import { postDigitalRmb } from '@/utils/digital-rmb-request'
2
- import type { ApiResponse } from '@/utils/request'
3
- import type {
4
- EstimationAmounts, EstimationHistory, EstimationPredRow, EstimationQueryParams, EstimationTranRow,
5
- Tam140503001InBody, Tam140503001OutBody, Tam140503002InBody, Tam140503002OutBody,
6
- Tam140503003InBody, Tam140503003OutBody, Tam140503004InBody, Tam140503004OutBody,
7
- } from './typs'
1
+ import baseHttpClient from "@chic/http"
8
2
 
9
- /**
10
- * 数字人民币头寸匡算页面接口与共用工具。
11
- * 接口地址:查询 140503001、导出 140503002、详情 140503003、历史预计调拨金额 140503004;
12
- * 请求报文和接口类型均在本模块内维护。
13
- */
14
- const API = {
3
+ export default {
15
4
  /**
16
- * 匡算查询(140503001)。
17
- * @param params tranDate 为交易日期;predictAllotInAmt、predictAllotOutAmt
18
- * 为今日预计调拨收入、支付金额。返回十个单元格金额,C11、D11 回显本次输入。
5
+ * 查询数字人民币头寸匡算结果。
6
+ * @param params 查询参数:tranDate 为交易日期(YYYYMMDD),
7
+ * predictAllotInAmt、predictAllotOutAmt 为今日预计调拨收入、支出金额。
8
+ * 返回 B15、C3、C6、C7、C8、C11、D6、D7、D8、D11 十个单元格金额。
19
9
  */
20
- queryListApi: (params: EstimationQueryParams) =>
21
- postDigitalRmb<Tam140503001InBody, Tam140503001OutBody>('/TAM/0001/140503001', params),
22
-
10
+ queryListApi: (params: unknown) => baseHttpClient.post<any>("/TAM/0001/140503001", params),
23
11
  /**
24
- * 匡算导出(140503002):入参与查询完全一致,响应只有 fileName。
25
- * 下载协议待文件服务确认,页面导出按钮暂未接线。
12
+ * 导出数字人民币头寸匡算结果。
13
+ * @param params 导出参数与查询一致:tranDate、predictAllotInAmt、predictAllotOutAmt;
14
+ * 响应只返回导出文件名 fileName。
26
15
  */
27
- exportApi: (params: EstimationQueryParams) =>
28
- postDigitalRmb<Tam140503002InBody, Tam140503002OutBody>('/TAM/0001/140503002', params),
29
-
16
+ exportApi: (params: unknown) => baseHttpClient.post<any>("/TAM/0001/140503002", params),
30
17
  /**
31
- * 匡算详情(140503003)。
32
- * @param params position 为报表单元格位置编码,tranDate 为交易日期;
33
- * 返回该单元格对应的预报列表、流水列表和总条数。
18
+ * 查询匡算单元格明细。
19
+ * @param params 查询参数:position 为单元格位置编码,tranDate 为交易日期(YYYYMMDD)。
20
+ * 响应包含预报明细 predList、流水明细 tranList 和总条数 totalCount。
34
21
  */
35
- getDetailApi: (params: Tam140503003InBody) =>
36
- postDigitalRmb<Tam140503003InBody, Tam140503003OutBody>('/TAM/0001/140503003', params),
37
-
22
+ getDetailApi: (params: unknown) => baseHttpClient.post<any>("/TAM/0001/140503003", params),
38
23
  /**
39
- * 历史预计调拨金额查询(140503004)。
40
- * @param tranDate 交易日期。返回该日期的预计调拨收支金额,用于预填查询条件。
24
+ * 查询指定日期的历史预计调拨金额,用于预填查询条件。
25
+ * @param tranDate 交易日期,格式为 YYYYMMDD。
41
26
  */
42
27
  queryHistoryApi: (tranDate: string) =>
43
- postDigitalRmb<Tam140503004InBody, Tam140503004OutBody>('/TAM/0001/140503004', { tranDate }),
44
- }
45
-
46
- /** 校验公共响应头;retCode 不是 00000 时按服务端提示或 fallback 抛错。 */
47
- export function ensureSuccess<T>(response: ApiResponse<T>, fallback: string): T | null {
48
- if (response?.head?.retCode !== '00000') {
49
- throw new Error(response?.head?.retMsg?.trim() || fallback)
50
- }
51
- return response.body
52
- }
53
-
54
- /** 提取可展示的错误信息;无法识别时给出统一提示。 */
55
- export function getErrorMessage(error: unknown): string {
56
- if (error instanceof Error && error.message.trim()) return error.message
57
- if (typeof error === 'string' && error.trim()) return error
58
- return '操作失败,请稍后重试'
59
- }
60
-
61
- /** 业务日期始终按上海时区生成,不依赖浏览器所在时区,格式为 YYYYMMDD。 */
62
- export function getToday(): string {
63
- const parts = new Intl.DateTimeFormat('en-CA', {
64
- timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit',
65
- }).formatToParts(new Date())
66
- const read = (type: Intl.DateTimeFormatPartTypes) => parts.find((part) => part.type === type)?.value || ''
67
- return read('year') + read('month') + read('day')
28
+ baseHttpClient.post<any>("/TAM/0001/140503004", { tranDate })
68
29
  }
69
-
70
- /** 判断是否为合法的 YYYYMMDD 日期。 */
71
- export function isValidDate(value: string): boolean {
72
- if (!/^\d{8}$/.test(value)) return false
73
- const year = Number(value.slice(0, 4))
74
- const month = Number(value.slice(4, 6))
75
- const day = Number(value.slice(6, 8))
76
- if (year < 1 || month < 1 || month > 12 || day < 1) return false
77
- const leap = year % 400 === 0 || (year % 4 === 0 && year % 100 !== 0)
78
- const monthDays = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
79
- return day <= (monthDays[month - 1] || 0)
80
- }
81
-
82
- /** 只接受十进制文本和合法千分位;保留全部小数位,避免改变输入精度。 */
83
- export function normalizeAmount(value: string): string {
84
- let text = value.trim()
85
- if (text.includes(',')) {
86
- if (!/^[+-]?\d{1,3}(?:,\d{3})+(?:\.\d*)?$/.test(text)) return ''
87
- text = text.replace(/,/g, '')
88
- }
89
- const match = /^([+-]?)(\d*)(?:\.(\d*))?$/.exec(text)
90
- if (!match || (!match[2] && !match[3])) return ''
91
- const integer = (match[2] || '0').replace(/^0+(?=\d)/, '')
92
- const fraction = match[3] || ''
93
- const negative = match[1] === '-' && /[1-9]/.test(integer + fraction)
94
- return `${negative ? '-' : ''}${integer}${fraction ? `.${fraction}` : ''}`
95
- }
96
-
97
- /** 千分位展示至少两位小数,超过两位的小数原样保留,不做浮点数转换或舍入。 */
98
- export function formatAmount(value?: string): string {
99
- const original = value?.trim() || ''
100
- if (!original) return '—'
101
- const normalized = normalizeAmount(original)
102
- if (!normalized) return original
103
- const negative = normalized.startsWith('-')
104
- const [integer = '0', fraction = ''] = (negative ? normalized.slice(1) : normalized).split('.')
105
- const grouped = integer.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
106
- return `${negative ? '-' : ''}${grouped}.${fraction.padEnd(2, '0')}`
107
- }
108
-
109
- /**
110
- * 计算轧差(收入减支出);任一金额无法解析时返回空字符串。
111
- * 两侧金额按小数位对齐后用 BigInt 相减,避免浮点误差;
112
- * emptyAsZero 为 true 时空文本按 0 参与计算(用于输入框的实时轧差)。
113
- */
114
- export function calcGapAmount(inAmt: string, outAmt: string, emptyAsZero = false): string {
115
- const parse = (value: string) => {
116
- if (!value?.trim() && emptyAsZero) value = '0'
117
- const normalized = normalizeAmount(value)
118
- if (!normalized) return null
119
- const negative = normalized.startsWith('-')
120
- const [integer = '0', fraction = ''] = (negative ? normalized.slice(1) : normalized).split('.')
121
- return { negative, integer, fraction }
122
- }
123
- const inPart = parse(inAmt)
124
- const outPart = parse(outAmt)
125
- if (!inPart || !outPart) return ''
126
- const scale = Math.max(inPart.fraction.length, outPart.fraction.length)
127
- const toScaled = (part: { negative: boolean; integer: string; fraction: string }): bigint => {
128
- const digits = `${part.integer}${part.fraction.padEnd(scale, '0')}`
129
- return BigInt(digits) * (part.negative ? -1n : 1n)
130
- }
131
- const diff = toScaled(inPart) - toScaled(outPart)
132
- const negative = diff < 0n
133
- const digits = (negative ? -diff : diff).toString().padStart(scale + 1, '0')
134
- const integer = digits.slice(0, digits.length - scale)
135
- const fraction = scale ? digits.slice(digits.length - scale) : ''
136
- return formatAmount(`${negative ? '-' : ''}${integer}${fraction ? `.${fraction}` : ''}`)
137
- }
138
-
139
- /** 收支方向字典:I 来账、O 往账。 */
140
- export const payDirecLabels: Record<string, string> = { I: '来账', O: '往账' }
141
- /** 支付渠道:预报与流水的数字人民币分别为 8、5;其余渠道保留原文。 */
142
- export const payChannelLabels: Record<string, string> = { '5': '数字人民币', '8': '数字人民币' }
143
- /** 核销状态字典:1 未核销、2 已核销。 */
144
- export const verifyStatusLabels: Record<string, string> = { '1': '未核销', '2': '已核销' }
145
- /** 业务类型字典:201 兑回、202 兑出、203 付现;与核销预报使用同一套枚举。 */
146
- export const busiTypeLabels: Record<string, string> = { '201': '兑回', '202': '兑出', '203': '付现' }
147
-
148
- /** 读取字典文案;空值显示横线,未登记的代码保留原文展示。 */
149
- export function dictLabel(labels: Record<string, string>, value?: string): string {
150
- const text = value?.trim() || ''
151
- return text ? labels[text] ?? text : '—'
152
- }
153
-
154
- /** 详情明细展示的统一空值处理;空值显示横线。 */
155
- export function displayText(value?: string): string {
156
- return value?.trim() || '—'
157
- }
158
-
159
- export default API
@@ -1,11 +1,12 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
3
3
  import { ElMessage } from 'element-plus'
4
- import API, {
5
- calcGapAmount, ensureSuccess, formatAmount, getErrorMessage, getToday, isValidDate, normalizeAmount,
6
- } from './api'
7
- import EstimationDetailDialog from './components/EstimationDetailDialog.vue'
8
- import type { EstimationAmounts, EstimationCell, EstimationQueryParams } from './typs'
4
+ import API from './api'
5
+ import type {
6
+ EstimationAmounts, EstimationCell, EstimationPredRow, EstimationQueryParams, EstimationTranRow,
7
+ } from './typs'
8
+
9
+ defineOptions({ name: 'DcpsEstimationPage' })
9
10
 
10
11
  // 数字人民币头寸匡算页面:
11
12
  // 查询条件为交易日期加上今日预计调拨收入、支出金额;交易日期变化后调用
@@ -30,6 +31,112 @@ const cellLabels: Record<string, string> = {
30
31
  B15: '当日实时余额',
31
32
  }
32
33
 
34
+ // ---------- 页面内工具函数 ----------
35
+
36
+ /** 提取可展示的错误信息;无法识别时给出统一提示。 */
37
+ const getErrorMessage = (error: unknown): string => {
38
+ if (error instanceof Error && error.message.trim()) return error.message
39
+ if (typeof error === 'string' && error.trim()) return error
40
+ return '操作失败,请稍后重试'
41
+ }
42
+
43
+ /** 业务日期始终按上海时区生成,不依赖浏览器所在时区,格式为 YYYYMMDD。 */
44
+ const getToday = (): string => {
45
+ const parts = new Intl.DateTimeFormat('en-CA', {
46
+ timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit',
47
+ }).formatToParts(new Date())
48
+ const read = (type: Intl.DateTimeFormatPartTypes) => parts.find((part) => part.type === type)?.value || ''
49
+ return read('year') + read('month') + read('day')
50
+ }
51
+
52
+ /** 判断是否为合法的 YYYYMMDD 日期。 */
53
+ const isValidDate = (value: string): boolean => {
54
+ if (!/^\d{8}$/.test(value)) return false
55
+ const year = Number(value.slice(0, 4))
56
+ const month = Number(value.slice(4, 6))
57
+ const day = Number(value.slice(6, 8))
58
+ if (year < 1 || month < 1 || month > 12 || day < 1) return false
59
+ const leap = year % 400 === 0 || (year % 4 === 0 && year % 100 !== 0)
60
+ const monthDays = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
61
+ return day <= (monthDays[month - 1] || 0)
62
+ }
63
+
64
+ /** 只接受十进制文本和合法千分位;保留全部小数位,避免改变输入精度。 */
65
+ const normalizeAmount = (value: string): string => {
66
+ let text = value.trim()
67
+ if (text.includes(',')) {
68
+ if (!/^[+-]?\d{1,3}(?:,\d{3})+(?:\.\d*)?$/.test(text)) return ''
69
+ text = text.replace(/,/g, '')
70
+ }
71
+ const match = /^([+-]?)(\d*)(?:\.(\d*))?$/.exec(text)
72
+ if (!match || (!match[2] && !match[3])) return ''
73
+ const integer = (match[2] || '0').replace(/^0+(?=\d)/, '')
74
+ const fraction = match[3] || ''
75
+ const negative = match[1] === '-' && /[1-9]/.test(integer + fraction)
76
+ return `${negative ? '-' : ''}${integer}${fraction ? `.${fraction}` : ''}`
77
+ }
78
+
79
+ /** 千分位展示至少两位小数,超过两位的小数原样保留,不做浮点数转换或舍入。 */
80
+ const formatAmount = (value?: string): string => {
81
+ const original = value?.trim() || ''
82
+ if (!original) return '—'
83
+ const normalized = normalizeAmount(original)
84
+ if (!normalized) return original
85
+ const negative = normalized.startsWith('-')
86
+ const [integer = '0', fraction = ''] = (negative ? normalized.slice(1) : normalized).split('.')
87
+ const grouped = integer.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
88
+ return `${negative ? '-' : ''}${grouped}.${fraction.padEnd(2, '0')}`
89
+ }
90
+
91
+ /**
92
+ * 计算轧差(收入减支出);任一金额无法解析时返回空字符串。
93
+ * emptyAsZero 为 true 时空文本按 0 参与计算(用于输入框的实时轧差)。
94
+ */
95
+ const calcGapAmount = (inAmt: string, outAmt: string, emptyAsZero = false): string => {
96
+ const parse = (value: string) => {
97
+ if (!value?.trim() && emptyAsZero) value = '0'
98
+ const normalized = normalizeAmount(value)
99
+ if (!normalized) return null
100
+ const negative = normalized.startsWith('-')
101
+ const [integer = '0', fraction = ''] = (negative ? normalized.slice(1) : normalized).split('.')
102
+ return { negative, integer, fraction }
103
+ }
104
+ const inPart = parse(inAmt)
105
+ const outPart = parse(outAmt)
106
+ if (!inPart || !outPart) return ''
107
+ const scale = Math.max(inPart.fraction.length, outPart.fraction.length)
108
+ const toScaled = (part: { negative: boolean; integer: string; fraction: string }): bigint => {
109
+ const digits = `${part.integer}${part.fraction.padEnd(scale, '0')}`
110
+ return BigInt(digits) * (part.negative ? -1n : 1n)
111
+ }
112
+ const diff = toScaled(inPart) - toScaled(outPart)
113
+ const negative = diff < 0n
114
+ const digits = (negative ? -diff : diff).toString().padStart(scale + 1, '0')
115
+ const integer = digits.slice(0, digits.length - scale)
116
+ const fraction = scale ? digits.slice(digits.length - scale) : ''
117
+ return formatAmount(`${negative ? '-' : ''}${integer}${fraction ? `.${fraction}` : ''}`)
118
+ }
119
+
120
+ /** 收支方向字典:I 来账、O 往账。 */
121
+ const payDirecLabels: Record<string, string> = { I: '来账', O: '往账' }
122
+ /** 支付渠道:预报与流水的数字人民币分别为 8、5;其余渠道保留原文。 */
123
+ const payChannelLabels: Record<string, string> = { '5': '数字人民币', '8': '数字人民币' }
124
+ /** 核销状态字典:1 未核销、2 已核销。 */
125
+ const verifyStatusLabels: Record<string, string> = { '1': '未核销', '2': '已核销' }
126
+ /** 业务类型字典:201 兑回、202 兑出、203 付现;与核销预报使用同一套枚举。 */
127
+ const busiTypeLabels: Record<string, string> = { '201': '兑回', '202': '兑出', '203': '付现' }
128
+
129
+ /** 读取字典文案;空值显示横线,未登记的代码保留原文展示。 */
130
+ const dictLabel = (labels: Record<string, string>, value?: string): string => {
131
+ const text = value?.trim() || ''
132
+ return text ? labels[text] ?? text : '—'
133
+ }
134
+
135
+ /** 详情明细展示的统一空值处理;空值显示横线。 */
136
+ const displayText = (value?: string): string => value?.trim() || '—'
137
+
138
+ // ---------- 匡算报表 ----------
139
+
33
140
  // 读取接口金额文本;未返回的编码保持空字符串,展示为空白。
34
141
  const amountOf = (position: string): string => {
35
142
  const source = amounts.value as unknown as Record<string, unknown> | null
@@ -165,9 +272,57 @@ const submitQuery = async () => {
165
272
  }
166
273
  }
167
274
 
168
- // 详情弹窗状态:cell 为被单击的报表单元格。
275
+ // ---------- 单元格明细弹窗 ----------
276
+
277
+ // 弹窗状态:cell 为被单击的报表单元格;明细请求在弹窗打开时发起。
169
278
  const detailVisible = ref(false)
170
279
  const detailCell = ref<EstimationCell | null>(null)
280
+ const detailPredList = ref<EstimationPredRow[]>([])
281
+ const detailTranList = ref<EstimationTranRow[]>([])
282
+ const detailTotalCount = ref('')
283
+ const detailLoading = ref(false)
284
+ const detailHasError = ref(false)
285
+ let detailRequestId = 0
286
+
287
+ /** 查询单元格明细;cell 或交易日期缺失时不发起请求,直接呈现空态。 */
288
+ const loadDetailInfo = async () => {
289
+ const position = detailCell.value?.position?.trim()
290
+ const date = queriedTranDate.value.trim()
291
+ if (!position || !date) return
292
+ const currentRequestId = ++detailRequestId
293
+ detailLoading.value = true
294
+ try {
295
+ const { head, body } = await API.getDetailApi({ position, tranDate: date }) as any
296
+ if (currentRequestId !== detailRequestId || disposed) return
297
+ if (head?.retCode !== '00000') {
298
+ throw new Error(head?.retMsg || '查询失败')
299
+ }
300
+ detailPredList.value = Array.isArray(body?.predList) ? body.predList : []
301
+ detailTranList.value = Array.isArray(body?.tranList) ? body.tranList : []
302
+ detailTotalCount.value = String(body?.totalCount ?? '').trim()
303
+ detailHasError.value = false
304
+ } catch (error) { // error 为详情请求或响应数据校验异常。
305
+ if (currentRequestId !== detailRequestId || disposed) return
306
+ detailPredList.value = []
307
+ detailTranList.value = []
308
+ detailTotalCount.value = ''
309
+ detailHasError.value = true
310
+ ElMessage.error(getErrorMessage(error))
311
+ } finally {
312
+ if (currentRequestId === detailRequestId) detailLoading.value = false
313
+ }
314
+ }
315
+
316
+ /** 关闭弹窗:重置明细并让未完成的请求失效。 */
317
+ const closeDetail = () => {
318
+ detailVisible.value = false
319
+ detailRequestId += 1
320
+ detailPredList.value = []
321
+ detailTranList.value = []
322
+ detailTotalCount.value = ''
323
+ detailHasError.value = false
324
+ detailLoading.value = false
325
+ }
171
326
 
172
327
  /** 打开单元格明细弹窗;结果就绪前其余单元格为占位横线,不发起详情查询。 */
173
328
  const openDetail = (cell?: EstimationCell) => {
@@ -176,12 +331,19 @@ const openDetail = (cell?: EstimationCell) => {
176
331
  detailVisible.value = true
177
332
  }
178
333
 
334
+ /** 合并预报、流水列表得到明细总条数;接口 totalCount 缺失时按列表长度兜底展示。 */
335
+ const resolveTotal = () => {
336
+ if (/^\d+$/.test(detailTotalCount.value)) return detailTotalCount.value
337
+ return String(detailPredList.value.length + detailTranList.value.length)
338
+ }
339
+
179
340
  // 页面首次显示时按默认日期预填预计调拨收支金额。
180
341
  onMounted(prefillHistory)
181
342
  // 页面离开后,未完成的预填请求不再更新表单或弹出消息。
182
343
  onBeforeUnmount(() => {
183
344
  disposed = true
184
345
  prefillRequestId += 1
346
+ detailRequestId += 1
185
347
  })
186
348
  </script>
187
349
 
@@ -353,23 +515,132 @@ onBeforeUnmount(() => {
353
515
  </el-card>
354
516
 
355
517
  <!-- 单元格明细弹窗:按位置编码查询预报与流水列表。 -->
356
- <EstimationDetailDialog v-model="detailVisible" :cell="detailCell" :tran-date="queriedTranDate" />
518
+ <el-dialog
519
+ v-model="detailVisible"
520
+ :title="detailCell ? `匡算明细 - ${detailCell.label}` : '匡算明细'"
521
+ width="min(1080px, calc(100vw - 32px))"
522
+ top="16px"
523
+ append-to-body
524
+ destroy-on-close
525
+ @open="loadDetailInfo"
526
+ @closed="closeDetail"
527
+ >
528
+ <el-scrollbar class="detail-scrollbar">
529
+ <div class="detail-content">
530
+ <!-- 上下文摘要:被点击单元格的位置、金额与详情查询的交易日期。 -->
531
+ <header class="detail-summary">
532
+ <div>
533
+ <span class="detail-label">位置编码</span>
534
+ <strong class="detail-position">{{ detailCell?.position || '—' }}</strong>
535
+ </div>
536
+ <div>
537
+ <span class="detail-label">单元格金额(元)</span>
538
+ <strong class="detail-amount">{{ formatAmount(detailCell?.amount) }}</strong>
539
+ </div>
540
+ <div>
541
+ <span class="detail-label">交易日期</span>
542
+ <strong>{{ queriedTranDate }}</strong>
543
+ </div>
544
+ <div class="detail-reference">
545
+ <span class="detail-label">明细总条数</span>
546
+ <strong>{{ resolveTotal() }}</strong>
547
+ </div>
548
+ </header>
549
+
550
+ <div v-loading="detailLoading" class="detail-tables">
551
+ <!-- 预报信息明细;列表为空时展示统一空态。 -->
552
+ <div class="dialog-section-title">预报信息</div>
553
+ <el-table :data="detailPredList" size="small" border max-height="240px" empty-text="暂无预报信息">
554
+ <el-table-column label="预报流水号" min-width="150" show-overflow-tooltip>
555
+ <template #default="{ row }">{{ displayText(row.predRefNo) }}</template>
556
+ </el-table-column>
557
+ <el-table-column label="业务类型" width="90" align="center">
558
+ <template #default="{ row }">{{ dictLabel(busiTypeLabels, row.busiType) }}</template>
559
+ </el-table-column>
560
+ <el-table-column label="客户名称" min-width="140" show-overflow-tooltip>
561
+ <template #default="{ row }">{{ displayText(row.clientName) }}</template>
562
+ </el-table-column>
563
+ <el-table-column label="交易对手名称" min-width="140" show-overflow-tooltip>
564
+ <template #default="{ row }">{{ displayText(row.competName) }}</template>
565
+ </el-table-column>
566
+ <el-table-column label="收支方向" width="80" align="center">
567
+ <template #default="{ row }">{{ dictLabel(payDirecLabels, row.payDirec) }}</template>
568
+ </el-table-column>
569
+ <el-table-column label="支付渠道" width="110" align="center">
570
+ <template #default="{ row }">{{ dictLabel(payChannelLabels, row.payChannel) }}</template>
571
+ </el-table-column>
572
+ <el-table-column label="预报金额(元)" width="140" align="right">
573
+ <template #default="{ row }"><span class="amount-cell">{{ formatAmount(row.predAmt) }}</span></template>
574
+ </el-table-column>
575
+ <el-table-column label="已核销金额(元)" width="140" align="right">
576
+ <template #default="{ row }"><span class="amount-cell">{{ formatAmount(row.verifyAmt) }}</span></template>
577
+ </el-table-column>
578
+ <el-table-column label="核销状态" width="90" align="center">
579
+ <template #default="{ row }">{{ dictLabel(verifyStatusLabels, row.verifyStatus) }}</template>
580
+ </el-table-column>
581
+ <el-table-column label="落地支行" min-width="130" show-overflow-tooltip>
582
+ <template #default="{ row }">{{ displayText(row.groundBranch) }}</template>
583
+ </el-table-column>
584
+ <el-table-column label="上级单位" min-width="130" show-overflow-tooltip>
585
+ <template #default="{ row }">{{ displayText(row.upOrg) }}</template>
586
+ </el-table-column>
587
+ </el-table>
588
+
589
+ <!-- 流水信息明细;失败的详情可重新打开弹窗重试。 -->
590
+ <div class="dialog-section-title">流水信息</div>
591
+ <el-table :data="detailTranList" size="small" border max-height="240px" empty-text="暂无流水信息">
592
+ <el-table-column label="交易参考号" min-width="150" show-overflow-tooltip>
593
+ <template #default="{ row }">{{ displayText(row.refNo) }}</template>
594
+ </el-table-column>
595
+ <el-table-column label="业务类型" width="90" align="center">
596
+ <template #default="{ row }">{{ dictLabel(busiTypeLabels, row.busiType) }}</template>
597
+ </el-table-column>
598
+ <el-table-column label="付款人名称" min-width="140" show-overflow-tooltip>
599
+ <template #default="{ row }">{{ displayText(row.payerName) }}</template>
600
+ </el-table-column>
601
+ <el-table-column label="收款人名称" min-width="140" show-overflow-tooltip>
602
+ <template #default="{ row }">{{ displayText(row.payeeName) }}</template>
603
+ </el-table-column>
604
+ <el-table-column label="收支方向" width="80" align="center">
605
+ <template #default="{ row }">{{ dictLabel(payDirecLabels, row.payDirec) }}</template>
606
+ </el-table-column>
607
+ <el-table-column label="支付渠道" width="110" align="center">
608
+ <template #default="{ row }">{{ dictLabel(payChannelLabels, row.payChannel) }}</template>
609
+ </el-table-column>
610
+ <el-table-column label="金额(元)" width="140" align="right">
611
+ <template #default="{ row }"><span class="amount-cell">{{ formatAmount(row.amt) }}</span></template>
612
+ </el-table-column>
613
+ <el-table-column label="核销状态" width="90" align="center">
614
+ <template #default="{ row }">{{ dictLabel(verifyStatusLabels, row.verifyStatus) }}</template>
615
+ </el-table-column>
616
+ <el-table-column label="落地支行" min-width="130" show-overflow-tooltip>
617
+ <template #default="{ row }">{{ displayText(row.groundBranch) }}</template>
618
+ </el-table-column>
619
+ <el-table-column label="上级单位" min-width="130" show-overflow-tooltip>
620
+ <template #default="{ row }">{{ displayText(row.upOrg) }}</template>
621
+ </el-table-column>
622
+ </el-table>
623
+
624
+ <div v-if="detailHasError" class="detail-retry">详情查询失败,关闭弹窗后重新打开可重试。</div>
625
+ </div>
626
+ </div>
627
+ </el-scrollbar>
628
+ <template #footer>
629
+ <el-button @click="closeDetail">关闭</el-button>
630
+ </template>
631
+ </el-dialog>
357
632
  </main>
358
633
  </template>
359
634
 
360
635
  <style scoped>
361
636
  /* 页面先扣除宿主占用高度,再使用 flex 分配剩余高度,内容由报表卡片承载。 */
362
637
  .dcps-page {
363
- /* 手动修改这里:顶部导航/页签 + 底部状态栏 + 宿主上下留白的合计高度。
364
- 独立页面保持 0px;例如顶部 90px、底部 24px、上下留白 6px,填写 120px。
365
- 不包含浏览器标签栏、地址栏,也不包含本页面自身的 padding。 */
366
- --host-layout-height: 0px;
367
638
  box-sizing: border-box;
368
639
  display: flex;
369
640
  flex-direction: column;
370
641
  gap: 5px;
371
- height: calc(100vh - var(--host-layout-height));
372
- height: calc(100dvh - var(--host-layout-height));
642
+ height: calc(100vh - var(--dpcs-host-layout-height));
643
+ height: calc(100dvh - var(--dpcs-host-layout-height));
373
644
  min-height: 0;
374
645
  min-width: 0;
375
646
  padding: 5px;
@@ -481,4 +752,32 @@ td.report-data--input { padding: 5px 8px; }
481
752
  font-size: 10px;
482
753
  letter-spacing: 0.02em;
483
754
  }
755
+
756
+ /* ---------- 单元格明细弹窗 ---------- */
757
+ .detail-scrollbar { max-height: calc(100vh - 240px); }
758
+ .detail-content { padding: 2px 4px 4px; }
759
+
760
+ /* 摘要区:位置编码、金额、日期放在一行,便于确认明细上下文。 */
761
+ .detail-summary {
762
+ display: flex;
763
+ flex-wrap: wrap;
764
+ align-items: center;
765
+ gap: 12px 36px;
766
+ margin-bottom: 12px;
767
+ padding: 12px 14px;
768
+ border-radius: 10px;
769
+ background: var(--el-fill-color-light);
770
+ }
771
+ .detail-label { display: block; margin-bottom: 5px; color: var(--el-text-color-secondary); font-size: 11px; }
772
+ .detail-summary strong { overflow-wrap: anywhere; font-size: 13px; }
773
+ .detail-amount { color: var(--el-color-primary-dark-2); font-variant-numeric: tabular-nums; }
774
+ .detail-reference { margin-left: auto; }
775
+ .detail-reference strong { font-weight: 600; }
776
+
777
+ .dialog-section-title { margin: 12px 0 6px; color: var(--el-text-color-regular); font-size: 13px; font-weight: 600; }
778
+ .detail-tables .el-table + .el-table { margin-top: 2px; }
779
+ .detail-tables { min-height: 160px; }
780
+ .detail-tables .dialog-section-title:first-child { margin-top: 0; }
781
+ .amount-cell { color: var(--el-text-color-primary); font-variant-numeric: tabular-nums; }
782
+ .detail-retry { margin-top: 10px; color: var(--el-color-danger); font-size: 12px; text-align: center; }
484
783
  </style>