vue3-element-plus-app-chu 0.0.13 → 0.0.14
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/README.md +4 -4
- package/package.json +3 -2
- package/src/layout/index.vue +21 -36
- package/src/router/index.ts +110 -0
- package/src/router/menus.ts +16 -0
- package/src/view/dcps/README.md +59 -0
- package/src/view/dcps/auth/components/AuthorizationDetailDialog.vue +2 -0
- package/src/view/dcps/auth/index.vue +4 -6
- package/src/view/dcps/balance/index.vue +4 -6
- package/src/view/dcps/common.css +7 -0
- package/src/view/dcps/estimation/api.ts +17 -142
- package/src/view/dcps/estimation/components/EstimationDetailDialog.vue +4 -1
- package/src/view/dcps/estimation/index.vue +7 -8
- package/src/view/dcps/estimation/utils.ts +113 -0
- package/src/view/dcps/forecast/api.ts +9 -201
- package/src/view/dcps/forecast/components/ForecastDetailView.vue +19 -9
- package/src/view/dcps/forecast/components/ForecastEditorView.vue +25 -14
- package/src/view/dcps/forecast/components/ForecastListView.vue +8 -11
- package/src/view/dcps/forecast/components/ForecastPageShell.vue +3 -7
- package/src/view/dcps/forecast/index.vue +22 -50
- package/src/view/dcps/forecast/list-refresh.ts +11 -0
- package/src/view/dcps/forecast/row-cache.ts +18 -0
- package/src/view/dcps/forecast/utils.ts +169 -0
- package/src/view/dcps/injection/components/InjectionDetailDialog.vue +2 -0
- package/src/view/dcps/injection/index.vue +4 -6
- package/src/view/dcps/write-off/api.ts +9 -164
- package/src/view/dcps/write-off/components/FlowWriteOffView.vue +4 -3
- package/src/view/dcps/write-off/components/ForecastWriteOffView.vue +4 -3
- package/src/view/dcps/write-off/index.vue +6 -7
- package/src/view/dcps/write-off/utils.ts +131 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { ApiResponse } from '@/utils/request'
|
|
2
|
+
|
|
3
|
+
/** 校验公共响应头;retCode 不是 00000 时按服务端提示或 fallback 抛错。 */
|
|
4
|
+
export function ensureSuccess<T>(response: ApiResponse<T>, fallback: string): T | null {
|
|
5
|
+
if (response?.head?.retCode !== '00000') {
|
|
6
|
+
throw new Error(response?.head?.retMsg?.trim() || fallback)
|
|
7
|
+
}
|
|
8
|
+
return response.body
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** 提取可展示的错误信息;无法识别时给出统一提示。 */
|
|
12
|
+
export function getErrorMessage(error: unknown): string {
|
|
13
|
+
if (error instanceof Error && error.message.trim()) return error.message
|
|
14
|
+
if (typeof error === 'string' && error.trim()) return error
|
|
15
|
+
return '操作失败,请稍后重试'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** 业务日期始终按上海时区生成,不依赖浏览器所在时区,格式为 YYYYMMDD。 */
|
|
19
|
+
export function getToday(): string {
|
|
20
|
+
const parts = new Intl.DateTimeFormat('en-CA', {
|
|
21
|
+
timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit',
|
|
22
|
+
}).formatToParts(new Date())
|
|
23
|
+
const read = (type: Intl.DateTimeFormatPartTypes) => parts.find((part) => part.type === type)?.value || ''
|
|
24
|
+
return read('year') + read('month') + read('day')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** 判断是否为合法的 YYYYMMDD 日期。 */
|
|
28
|
+
export function isValidDate(value: string): boolean {
|
|
29
|
+
if (!/^\d{8}$/.test(value)) return false
|
|
30
|
+
const year = Number(value.slice(0, 4))
|
|
31
|
+
const month = Number(value.slice(4, 6))
|
|
32
|
+
const day = Number(value.slice(6, 8))
|
|
33
|
+
if (year < 1 || month < 1 || month > 12 || day < 1) return false
|
|
34
|
+
const leap = year % 400 === 0 || (year % 4 === 0 && year % 100 !== 0)
|
|
35
|
+
const monthDays = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
|
36
|
+
return day <= (monthDays[month - 1] || 0)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 只接受十进制文本和合法千分位;保留全部小数位,避免改变输入精度。 */
|
|
40
|
+
export function normalizeAmount(value: string): string {
|
|
41
|
+
let text = value.trim()
|
|
42
|
+
if (text.includes(',')) {
|
|
43
|
+
if (!/^[+-]?\d{1,3}(?:,\d{3})+(?:\.\d*)?$/.test(text)) return ''
|
|
44
|
+
text = text.replace(/,/g, '')
|
|
45
|
+
}
|
|
46
|
+
const match = /^([+-]?)(\d*)(?:\.(\d*))?$/.exec(text)
|
|
47
|
+
if (!match || (!match[2] && !match[3])) return ''
|
|
48
|
+
const integer = (match[2] || '0').replace(/^0+(?=\d)/, '')
|
|
49
|
+
const fraction = match[3] || ''
|
|
50
|
+
const negative = match[1] === '-' && /[1-9]/.test(integer + fraction)
|
|
51
|
+
return `${negative ? '-' : ''}${integer}${fraction ? `.${fraction}` : ''}`
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 千分位展示至少两位小数,超过两位的小数原样保留,不做浮点数转换或舍入。 */
|
|
55
|
+
export function formatAmount(value?: string): string {
|
|
56
|
+
const original = value?.trim() || ''
|
|
57
|
+
if (!original) return '—'
|
|
58
|
+
const normalized = normalizeAmount(original)
|
|
59
|
+
if (!normalized) return original
|
|
60
|
+
const negative = normalized.startsWith('-')
|
|
61
|
+
const [integer = '0', fraction = ''] = (negative ? normalized.slice(1) : normalized).split('.')
|
|
62
|
+
const grouped = integer.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
|
63
|
+
return `${negative ? '-' : ''}${grouped}.${fraction.padEnd(2, '0')}`
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 计算轧差(收入减支出);任一金额无法解析时返回空字符串。
|
|
68
|
+
* emptyAsZero 为 true 时空文本按 0 参与计算(用于输入框的实时轧差)。
|
|
69
|
+
*/
|
|
70
|
+
export function calcGapAmount(inAmt: string, outAmt: string, emptyAsZero = false): string {
|
|
71
|
+
const parse = (value: string) => {
|
|
72
|
+
if (!value?.trim() && emptyAsZero) value = '0'
|
|
73
|
+
const normalized = normalizeAmount(value)
|
|
74
|
+
if (!normalized) return null
|
|
75
|
+
const negative = normalized.startsWith('-')
|
|
76
|
+
const [integer = '0', fraction = ''] = (negative ? normalized.slice(1) : normalized).split('.')
|
|
77
|
+
return { negative, integer, fraction }
|
|
78
|
+
}
|
|
79
|
+
const inPart = parse(inAmt)
|
|
80
|
+
const outPart = parse(outAmt)
|
|
81
|
+
if (!inPart || !outPart) return ''
|
|
82
|
+
const scale = Math.max(inPart.fraction.length, outPart.fraction.length)
|
|
83
|
+
const toScaled = (part: { negative: boolean; integer: string; fraction: string }): bigint => {
|
|
84
|
+
const digits = `${part.integer}${part.fraction.padEnd(scale, '0')}`
|
|
85
|
+
return BigInt(digits) * (part.negative ? -1n : 1n)
|
|
86
|
+
}
|
|
87
|
+
const diff = toScaled(inPart) - toScaled(outPart)
|
|
88
|
+
const negative = diff < 0n
|
|
89
|
+
const digits = (negative ? -diff : diff).toString().padStart(scale + 1, '0')
|
|
90
|
+
const integer = digits.slice(0, digits.length - scale)
|
|
91
|
+
const fraction = scale ? digits.slice(digits.length - scale) : ''
|
|
92
|
+
return formatAmount(`${negative ? '-' : ''}${integer}${fraction ? `.${fraction}` : ''}`)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** 收支方向字典:I 来账、O 往账。 */
|
|
96
|
+
export const payDirecLabels: Record<string, string> = { I: '来账', O: '往账' }
|
|
97
|
+
/** 支付渠道:预报与流水的数字人民币分别为 8、5;其余渠道保留原文。 */
|
|
98
|
+
export const payChannelLabels: Record<string, string> = { '5': '数字人民币', '8': '数字人民币' }
|
|
99
|
+
/** 核销状态字典:1 未核销、2 已核销。 */
|
|
100
|
+
export const verifyStatusLabels: Record<string, string> = { '1': '未核销', '2': '已核销' }
|
|
101
|
+
/** 业务类型字典:201 兑回、202 兑出、203 付现;与核销预报使用同一套枚举。 */
|
|
102
|
+
export const busiTypeLabels: Record<string, string> = { '201': '兑回', '202': '兑出', '203': '付现' }
|
|
103
|
+
|
|
104
|
+
/** 读取字典文案;空值显示横线,未登记的代码保留原文展示。 */
|
|
105
|
+
export function dictLabel(labels: Record<string, string>, value?: string): string {
|
|
106
|
+
const text = value?.trim() || ''
|
|
107
|
+
return text ? labels[text] ?? text : '—'
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** 详情明细展示的统一空值处理;空值显示横线。 */
|
|
111
|
+
export function displayText(value?: string): string {
|
|
112
|
+
return value?.trim() || '—'
|
|
113
|
+
}
|
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
import { postDigitalRmb } from '@/utils/digital-rmb-request'
|
|
2
|
-
import type { ApiResponse } from '@/utils/request'
|
|
3
2
|
import type {
|
|
4
3
|
ForecastCreateBody,
|
|
5
|
-
ForecastDetail,
|
|
6
4
|
ForecastDetailParams,
|
|
7
5
|
ForecastEditBody,
|
|
8
|
-
ForecastFormValues,
|
|
9
|
-
ForecastListBody,
|
|
10
6
|
ForecastQueryParams,
|
|
11
|
-
ForecastRow,
|
|
12
7
|
Tam120501001InBody,
|
|
13
8
|
Tam120501002InBody,
|
|
14
9
|
Tam120501003InBody,
|
|
@@ -19,214 +14,27 @@ import type {
|
|
|
19
14
|
} from './typs'
|
|
20
15
|
|
|
21
16
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* 请求报文和接口类型均在本模块内维护。
|
|
17
|
+
* 数字人民币头寸预报接口。
|
|
18
|
+
* 新增 120501001、修改 120501002、删除 120501003、查询 140501001、详情 140501002。
|
|
25
19
|
*/
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* 分页查询头寸预报。
|
|
29
|
-
* @param params 查询参数:opType 固定为 query;tranDate 为必查交易日期,格式为 YYYYMMDD。
|
|
30
|
-
* 其余筛选条件为空字符串表示不限制;金额范围使用 startTransAmt、endTransAmt。
|
|
31
|
-
*/
|
|
20
|
+
export default {
|
|
21
|
+
/** 分页查询头寸预报;tranDate 必填,其余条件为空表示不限制。 */
|
|
32
22
|
queryListApi: (params: ForecastQueryParams) =>
|
|
33
23
|
postDigitalRmb<Tam140501001InBody, Tam140501001OutBody>('/TAM/0001/140501001', params),
|
|
34
24
|
|
|
35
|
-
/**
|
|
36
|
-
* 新增一条头寸预报。
|
|
37
|
-
* @param body 新增业务字段;金额字段由调用方标准化为十进制文本。
|
|
38
|
-
*/
|
|
25
|
+
/** 新增一条头寸预报。 */
|
|
39
26
|
addApi: (body: ForecastCreateBody) =>
|
|
40
27
|
postDigitalRmb<Tam120501001InBody, Record<string, unknown>>('/TAM/0001/120501001', { body }),
|
|
41
28
|
|
|
42
|
-
/**
|
|
43
|
-
* 修改一条头寸预报。
|
|
44
|
-
* @param body 修改业务字段;predRefSeq 为待修改记录的预报流水号。
|
|
45
|
-
*/
|
|
29
|
+
/** 修改一条头寸预报;predRefSeq 为待修改记录的预报流水号。 */
|
|
46
30
|
editApi: (body: ForecastEditBody) =>
|
|
47
31
|
postDigitalRmb<Tam120501002InBody, Record<string, unknown>>('/TAM/0001/120501002', { body }),
|
|
48
32
|
|
|
49
|
-
/**
|
|
50
|
-
* 按预报流水号删除头寸预报。
|
|
51
|
-
* @param predRefSeqs 待删除记录的预报流水号列表。
|
|
52
|
-
*/
|
|
33
|
+
/** 按预报流水号批量删除头寸预报。 */
|
|
53
34
|
deleteApi: (predRefSeqs: string[]) =>
|
|
54
35
|
postDigitalRmb<Tam120501003InBody, Record<string, unknown>>('/TAM/0001/120501003', { predRefSeqs }),
|
|
55
36
|
|
|
56
|
-
/**
|
|
57
|
-
* 读取单条预报详情。
|
|
58
|
-
* @param params 详情参数:predRefNo 为预报流水号。
|
|
59
|
-
*/
|
|
37
|
+
/** 读取单条预报详情;predRefNo 为预报流水号。 */
|
|
60
38
|
getDetailApi: (params: ForecastDetailParams) =>
|
|
61
|
-
postDigitalRmb<Tam140501002InBody, Tam140501002OutBody>('/TAM/0001/140501002', params)
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* 读取详情并与列表记录合并:详情没有的字段由同一条列表记录补充,
|
|
65
|
-
* 不为接口未返回的字段填入空值。
|
|
66
|
-
* @param row 列表中的预报记录。
|
|
67
|
-
*/
|
|
68
|
-
async fetchDetail(row: ForecastRow): Promise<ForecastDetail> {
|
|
69
|
-
if (!row.predRefNo?.trim()) throw new Error('预报流水号缺失,无法读取详情')
|
|
70
|
-
const body = ensureSuccess(await API.getDetailApi({ predRefNo: row.predRefNo }), '读取预报详情失败')
|
|
71
|
-
if (!body || !Object.keys(body).length) throw new Error('服务端未返回预报详情')
|
|
72
|
-
if (body.predRefNo && body.predRefNo !== row.predRefNo) throw new Error('返回的预报流水号与当前记录不一致')
|
|
73
|
-
const definedBody = Object.fromEntries(Object.entries(body).filter(([, value]) => value !== undefined && value !== null))
|
|
74
|
-
return { ...row, ...definedBody } as ForecastDetail
|
|
75
|
-
},
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* 校验列表响应并取出当前页数据;列表缺失或总条数无效时抛出可提示的错误。
|
|
79
|
-
* @param body 列表接口返回的业务数据。
|
|
80
|
-
*/
|
|
81
|
-
readPage(body: ForecastListBody | null): ForecastPage {
|
|
82
|
-
if (!body || !Array.isArray(body.resultList)) throw new Error('查询结果缺少预报列表,请联系服务端确认返回数据')
|
|
83
|
-
const totalText = String(body.totalCount ?? body.resultList.length)
|
|
84
|
-
const total = Number(totalText)
|
|
85
|
-
if (!/^\d+$/.test(totalText) || !Number.isSafeInteger(total)) throw new Error('查询返回的总条数无效')
|
|
86
|
-
return { data: body.resultList, total }
|
|
87
|
-
},
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** 列表查询结果:当前页记录和符合条件的总条数。 */
|
|
91
|
-
export interface ForecastPage {
|
|
92
|
-
data: ForecastRow[] // 当前页的预报记录。
|
|
93
|
-
total: number // 符合查询条件的总记录数。
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/** 校验公共响应头;retCode 不是 00000 时按服务端提示或 fallback 抛错。 */
|
|
97
|
-
export function ensureSuccess<T>(response: ApiResponse<T>, fallback: string): T | null {
|
|
98
|
-
if (response?.head?.retCode !== '00000') {
|
|
99
|
-
throw new Error(response?.head?.retMsg?.trim() || fallback)
|
|
100
|
-
}
|
|
101
|
-
return response.body
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/** 提取可展示的错误信息;无法识别时给出统一提示。 */
|
|
105
|
-
export function getErrorMessage(error: unknown): string {
|
|
106
|
-
if (error instanceof Error && error.message.trim()) return error.message
|
|
107
|
-
if (typeof error === 'string' && error.trim()) return error
|
|
108
|
-
return '操作失败,请稍后重试'
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** 业务日期始终按上海时区生成,不依赖浏览器所在时区,格式为 YYYYMMDD。 */
|
|
112
|
-
export function getToday(): string {
|
|
113
|
-
const parts = new Intl.DateTimeFormat('en-CA', {
|
|
114
|
-
timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit',
|
|
115
|
-
}).formatToParts(new Date())
|
|
116
|
-
const read = (type: Intl.DateTimeFormatPartTypes) => parts.find((part) => part.type === type)?.value || ''
|
|
117
|
-
return read('year') + read('month') + read('day')
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/** 判断是否为合法的 YYYYMMDD 日期。 */
|
|
121
|
-
export function isValidDate(value: string): boolean {
|
|
122
|
-
if (!/^\d{8}$/.test(value)) return false
|
|
123
|
-
const year = Number(value.slice(0, 4))
|
|
124
|
-
const month = Number(value.slice(4, 6))
|
|
125
|
-
const day = Number(value.slice(6, 8))
|
|
126
|
-
if (year < 1 || month < 1 || month > 12 || day < 1) return false
|
|
127
|
-
const leap = year % 400 === 0 || (year % 4 === 0 && year % 100 !== 0)
|
|
128
|
-
const monthDays = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
|
129
|
-
return day <= (monthDays[month - 1] || 0)
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/** 把 YYYYMMDD 格式化为 YYYY-MM-DD 展示;空值显示横线,其他格式保留原文。 */
|
|
133
|
-
export function formatDate(value?: string): string {
|
|
134
|
-
const text = value?.trim() || ''
|
|
135
|
-
if (!text) return '—'
|
|
136
|
-
return isValidDate(text) ? `${text.slice(0, 4)}-${text.slice(4, 6)}-${text.slice(6, 8)}` : text
|
|
39
|
+
postDigitalRmb<Tam140501002InBody, Tam140501002OutBody>('/TAM/0001/140501002', params)
|
|
137
40
|
}
|
|
138
|
-
|
|
139
|
-
/** 只接受十进制文本和合法千分位;保留全部小数位,避免改变输入精度。 */
|
|
140
|
-
export function normalizeAmount(value: string): string {
|
|
141
|
-
let text = value.trim()
|
|
142
|
-
if (text.includes(',')) {
|
|
143
|
-
if (!/^[+-]?\d{1,3}(?:,\d{3})+(?:\.\d*)?$/.test(text)) return ''
|
|
144
|
-
text = text.replace(/,/g, '')
|
|
145
|
-
}
|
|
146
|
-
const match = /^([+-]?)(\d*)(?:\.(\d*))?$/.exec(text)
|
|
147
|
-
if (!match || (!match[2] && !match[3])) return ''
|
|
148
|
-
const integer = (match[2] || '0').replace(/^0+(?=\d)/, '')
|
|
149
|
-
const fraction = match[3] || ''
|
|
150
|
-
const negative = match[1] === '-' && /[1-9]/.test(integer + fraction)
|
|
151
|
-
return `${negative ? '-' : ''}${integer}${fraction ? `.${fraction}` : ''}`
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/** 千分位展示至少两位小数,超过两位的小数原样保留,不做浮点数转换或舍入。 */
|
|
155
|
-
export function formatAmount(value?: string): string {
|
|
156
|
-
const original = value?.trim() || ''
|
|
157
|
-
if (!original) return '—'
|
|
158
|
-
const normalized = normalizeAmount(original)
|
|
159
|
-
if (!normalized) return original
|
|
160
|
-
const negative = normalized.startsWith('-')
|
|
161
|
-
const [integer = '0', fraction = ''] = (negative ? normalized.slice(1) : normalized).split('.')
|
|
162
|
-
const grouped = integer.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
|
163
|
-
return `${negative ? '-' : ''}${grouped}.${fraction.padEnd(2, '0')}`
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/** 按统一小数位数转为 BigInt,任意长度的十进制金额都可精确比较。 */
|
|
167
|
-
export function compareAmounts(left: string, right: string): number {
|
|
168
|
-
const leftText = normalizeAmount(left)
|
|
169
|
-
const rightText = normalizeAmount(right)
|
|
170
|
-
if (!leftText || !rightText) {
|
|
171
|
-
if (!leftText && !rightText) return 0
|
|
172
|
-
return leftText ? -1 : 1
|
|
173
|
-
}
|
|
174
|
-
const parts = (text: string) => {
|
|
175
|
-
const negative = text.startsWith('-')
|
|
176
|
-
const [integer = '0', fraction = ''] = (negative ? text.slice(1) : text).split('.')
|
|
177
|
-
return { negative, integer, fraction }
|
|
178
|
-
}
|
|
179
|
-
const a = parts(leftText)
|
|
180
|
-
const b = parts(rightText)
|
|
181
|
-
const precision = Math.max(a.fraction.length, b.fraction.length)
|
|
182
|
-
const toInteger = (amount: ReturnType<typeof parts>) =>
|
|
183
|
-
BigInt(amount.integer + amount.fraction.padEnd(precision, '0')) * (amount.negative ? -1n : 1n)
|
|
184
|
-
const aValue = toInteger(a)
|
|
185
|
-
const bValue = toInteger(b)
|
|
186
|
-
return aValue < bValue ? -1 : aValue > bValue ? 1 : 0
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/** 新增和导入使用的表单初始值;日期默认取当天。 */
|
|
190
|
-
export function createFormValues(): ForecastFormValues {
|
|
191
|
-
const today = getToday()
|
|
192
|
-
return {
|
|
193
|
-
billDate: '', billNo: '', busiType: '', clientName: '', clientType: '',
|
|
194
|
-
competAcct: '', competName: '', custInfoSeq: '', groundBranch: '', groundBranchName: '',
|
|
195
|
-
linkMan: '', payChannel: '', payDirec: '', phoneNo: '', predAmt: '', predDate: today,
|
|
196
|
-
predName: '', predOrgName: '', purpose: '', remark: '', tranDate: today,
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* 校验表单;新增校验全部必填项,修改只校验已填写的字段。
|
|
202
|
-
* @param values 表单值。
|
|
203
|
-
* @param mode create 为新增,edit 为修改。
|
|
204
|
-
* @returns 错误提示文本,空字符串表示校验通过。
|
|
205
|
-
*/
|
|
206
|
-
export function validateForm(values: ForecastFormValues, mode: 'create' | 'edit' = 'create'): string {
|
|
207
|
-
const requiredFields = [
|
|
208
|
-
['clientName', '客户名称'], ['payDirec', '收支方向'], ['busiType', '业务类型'],
|
|
209
|
-
['payChannel', '支付渠道'], ['groundBranch', '落地机构'],
|
|
210
|
-
] as const
|
|
211
|
-
if (mode === 'create') {
|
|
212
|
-
for (const [key, label] of requiredFields) {
|
|
213
|
-
if (!values[key].trim()) return `请填写${label}`
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
for (const [key, label] of [['tranDate', '交易日期'], ['predDate', '预报日期']] as const) {
|
|
217
|
-
const date = values[key].trim()
|
|
218
|
-
if ((date || mode === 'create') && !isValidDate(date)) return `${label}须为有效日期,格式为 YYYYMMDD`
|
|
219
|
-
}
|
|
220
|
-
if (values.billDate.trim() && !isValidDate(values.billDate.trim())) {
|
|
221
|
-
return '出票日须为有效日期,格式为 YYYYMMDD'
|
|
222
|
-
}
|
|
223
|
-
if (values.predAmt.trim() || mode === 'create') {
|
|
224
|
-
const amount = normalizeAmount(values.predAmt)
|
|
225
|
-
if (!amount || amount.startsWith('-') || (amount.split('.')[1]?.length || 0) > 2) {
|
|
226
|
-
return '预报金额须为非负金额,最多保留两位小数'
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
return ''
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
export default API
|
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
|
3
|
+
import { useRouter } from 'vue-router'
|
|
3
4
|
import ForecastPageShell from './ForecastPageShell.vue'
|
|
4
|
-
import
|
|
5
|
+
import { fetchDetail, formatAmount, formatDate, getErrorMessage } from '../utils'
|
|
5
6
|
import type { ForecastDetail, ForecastRow } from '../typs'
|
|
6
7
|
|
|
7
|
-
defineOptions({ name: '
|
|
8
|
+
defineOptions({ name: 'DcpsForecastDetail' })
|
|
8
9
|
|
|
9
|
-
/**
|
|
10
|
-
const props = defineProps<{
|
|
10
|
+
/** 预报详情页;按路由中的预报流水号读取,列表记录仅用于补充接口未返回的字段。 */
|
|
11
|
+
const props = defineProps<{
|
|
12
|
+
predRefNo: string // 预报流水号,来自路由参数。
|
|
13
|
+
row?: ForecastRow | null // 可选:从列表进入详情时携带的完整记录。
|
|
14
|
+
}>()
|
|
11
15
|
|
|
12
|
-
const
|
|
16
|
+
const router = useRouter()
|
|
17
|
+
const backToList = () => void router.replace({ name: 'forecast' })
|
|
13
18
|
|
|
14
19
|
type DetailField = { key: keyof ForecastDetail; label: string; kind?: 'date' | 'amount' | 'status' }
|
|
15
20
|
|
|
@@ -56,9 +61,14 @@ const detailGroups: { title: string; fields: DetailField[] }[] = [
|
|
|
56
61
|
const detail = ref<ForecastDetail | null>(null)
|
|
57
62
|
const loading = ref(false)
|
|
58
63
|
const error = ref('')
|
|
59
|
-
const predRefNo = computed(() => props.row
|
|
64
|
+
const predRefNo = computed(() => props.predRefNo?.trim() || props.row?.predRefNo?.trim() || '')
|
|
65
|
+
const detailRow = computed<ForecastRow>(() => ({ ...props.row, predRefNo: predRefNo.value }))
|
|
60
66
|
const title = computed(() => predRefNo.value ? `预报详情 · ${predRefNo.value}` : '数字人民币头寸预报详情')
|
|
61
67
|
|
|
68
|
+
const openEdit = () => {
|
|
69
|
+
if (predRefNo.value) void router.push({ name: 'forecast-edit', params: { predRefNo: predRefNo.value } })
|
|
70
|
+
}
|
|
71
|
+
|
|
62
72
|
const displayValue = (field: DetailField) => {
|
|
63
73
|
const value = detail.value?.[field.key]
|
|
64
74
|
if (value === undefined || value === null || value === '') return '—'
|
|
@@ -74,7 +84,7 @@ const loadDetail = async () => {
|
|
|
74
84
|
detail.value = null
|
|
75
85
|
error.value = ''
|
|
76
86
|
try {
|
|
77
|
-
const result = await
|
|
87
|
+
const result = await fetchDetail(detailRow.value)
|
|
78
88
|
if (!disposed) detail.value = result
|
|
79
89
|
} catch (requestError) {
|
|
80
90
|
if (!disposed) error.value = getErrorMessage(requestError)
|
|
@@ -88,9 +98,9 @@ onBeforeUnmount(() => { disposed = true })
|
|
|
88
98
|
</script>
|
|
89
99
|
|
|
90
100
|
<template>
|
|
91
|
-
<ForecastPageShell :title="title" :back-disabled="loading" @back="
|
|
101
|
+
<ForecastPageShell :title="title" :back-disabled="loading" @back="backToList">
|
|
92
102
|
<template #heading-extra>
|
|
93
|
-
<el-button v-if="detail" type="primary" :disabled="loading" @click="
|
|
103
|
+
<el-button v-if="detail" type="primary" :disabled="loading" @click="openEdit">修改预报</el-button>
|
|
94
104
|
</template>
|
|
95
105
|
<el-card class="detail-card" shadow="never" :body-style="{ padding: '12px 16px' }">
|
|
96
106
|
<div v-loading="loading" class="detail-scroll">
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
|
3
|
+
import { useRouter } from 'vue-router'
|
|
3
4
|
import { ElMessage } from 'element-plus'
|
|
4
5
|
import ForecastPageShell from './ForecastPageShell.vue'
|
|
5
|
-
import API
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
import API from '../api'
|
|
7
|
+
import { requestForecastListRefresh } from '../list-refresh'
|
|
8
|
+
import {
|
|
9
|
+
createFormValues, ensureSuccess, fetchDetail, getErrorMessage, getToday, normalizeAmount, validateForm,
|
|
10
|
+
} from '../utils'
|
|
8
11
|
import { forecastRuntime } from '../../runtime'
|
|
9
12
|
import type {
|
|
10
13
|
ForecastCreateBody,
|
|
@@ -16,16 +19,18 @@ import type {
|
|
|
16
19
|
ForecastRow,
|
|
17
20
|
} from '../typs'
|
|
18
21
|
|
|
19
|
-
defineOptions({ name: '
|
|
22
|
+
defineOptions({ name: 'DcpsForecastEditor' })
|
|
20
23
|
|
|
21
|
-
/** 新增和修改共用同一张表单页;row
|
|
24
|
+
/** 新增和修改共用同一张表单页;predRefNo 来自路由,row 为可选的列表记录,用于补齐详情没有返回的字段。 */
|
|
22
25
|
const props = defineProps<{
|
|
23
26
|
mode: ForecastEditorMode // create 为新增,edit 为修改。
|
|
24
|
-
|
|
27
|
+
predRefNo?: string // 修改模式的预报流水号,来自路由参数。
|
|
28
|
+
row?: ForecastRow | null // 可选:从列表进入修改时携带的完整记录。
|
|
25
29
|
tranDate?: string // 新增模式的默认交易日期,格式为 YYYYMMDD。
|
|
26
30
|
}>()
|
|
27
31
|
|
|
28
|
-
const
|
|
32
|
+
const router = useRouter()
|
|
33
|
+
const backToList = () => void router.replace({ name: 'forecast' })
|
|
29
34
|
|
|
30
35
|
type FieldKey = Exclude<keyof ForecastFormValues, 'custRefSeq'>
|
|
31
36
|
type EditorField = {
|
|
@@ -90,7 +95,7 @@ const loadError = ref('')
|
|
|
90
95
|
const initialValues = ref<Partial<ForecastFormValues>>({})
|
|
91
96
|
const requiredFields = new Set<FieldKey>(['clientName', 'payDirec', 'busiType', 'payChannel', 'groundBranch', 'tranDate', 'predDate', 'predAmt'])
|
|
92
97
|
const disabled = computed(() => loading.value || submitting.value)
|
|
93
|
-
const predRefNo = computed(() => props.row?.predRefNo?.trim() || '')
|
|
98
|
+
const predRefNo = computed(() => props.predRefNo?.trim() || props.row?.predRefNo?.trim() || '')
|
|
94
99
|
const title = computed(() => props.mode === 'create' ? '新增数字人民币头寸预报' : '修改数字人民币头寸预报')
|
|
95
100
|
const visibleGroups = computed(() => fieldGroups.map(group => ({
|
|
96
101
|
...group,
|
|
@@ -146,11 +151,16 @@ const buildInitialValues = (detail: ForecastDetail): Partial<ForecastFormValues>
|
|
|
146
151
|
let disposed = false
|
|
147
152
|
// 修改模式先读取原始数据,再回填表单;读取失败时停留在页面提示原因。
|
|
148
153
|
const loadDetail = async () => {
|
|
149
|
-
if (props.mode !== 'edit'
|
|
154
|
+
if (props.mode !== 'edit') return
|
|
155
|
+
if (!predRefNo.value) {
|
|
156
|
+
loadError.value = '预报流水号缺失,无法修改'
|
|
157
|
+
return
|
|
158
|
+
}
|
|
150
159
|
loading.value = true
|
|
151
160
|
loadError.value = ''
|
|
152
161
|
try {
|
|
153
|
-
|
|
162
|
+
const detailRow: ForecastRow = { ...props.row, predRefNo: predRefNo.value }
|
|
163
|
+
initialValues.value = buildInitialValues(await fetchDetail(detailRow))
|
|
154
164
|
if (disposed) return
|
|
155
165
|
initializeForm()
|
|
156
166
|
} catch (error) {
|
|
@@ -206,7 +216,8 @@ const submit = async () => {
|
|
|
206
216
|
}
|
|
207
217
|
if (disposed) return
|
|
208
218
|
ElMessage.success(props.mode === 'create' ? '新增请求已处理' : '修改请求已处理')
|
|
209
|
-
|
|
219
|
+
requestForecastListRefresh()
|
|
220
|
+
await router.replace({ name: 'forecast' })
|
|
210
221
|
} catch (error) {
|
|
211
222
|
if (!disposed) ElMessage.error(getErrorMessage(error))
|
|
212
223
|
} finally {
|
|
@@ -216,13 +227,13 @@ const submit = async () => {
|
|
|
216
227
|
</script>
|
|
217
228
|
|
|
218
229
|
<template>
|
|
219
|
-
<ForecastPageShell :title="title" :back-disabled="submitting" @back="
|
|
230
|
+
<ForecastPageShell :title="title" :back-disabled="submitting" @back="backToList">
|
|
220
231
|
<el-card class="editor-card" shadow="never" :body-style="{ padding: '12px 16px' }">
|
|
221
232
|
<div class="editor-body">
|
|
222
233
|
<div v-loading="loading" class="editor-scroll">
|
|
223
234
|
<el-alert v-if="loadError" class="editor-load-error" :title="loadError" type="error" show-icon :closable="false">
|
|
224
235
|
<el-button link type="primary" :disabled="disabled" @click="loadDetail">重新加载</el-button>
|
|
225
|
-
<el-button link type="primary" :disabled="disabled" @click="
|
|
236
|
+
<el-button link type="primary" :disabled="disabled" @click="backToList">返回列表</el-button>
|
|
226
237
|
</el-alert>
|
|
227
238
|
<template v-else>
|
|
228
239
|
<p class="editor-note">金额单位为元,方向、业务类型、支付渠道和客户类型按业务代码填写。</p>
|
|
@@ -278,7 +289,7 @@ const submit = async () => {
|
|
|
278
289
|
<p v-if="formError" class="editor-error" role="alert" aria-live="polite">{{ formError }}</p>
|
|
279
290
|
<span v-else class="editor-tip">{{ mode === 'create' ? '带 * 的字段为新增必填项。' : '留空的字段表示不修改该字段的原值。' }}</span>
|
|
280
291
|
<el-space :size="8">
|
|
281
|
-
<el-button :disabled="submitting" @click="
|
|
292
|
+
<el-button :disabled="submitting" @click="backToList">返回列表</el-button>
|
|
282
293
|
<el-button type="primary" :disabled="disabled || !!loadError" :loading="submitting" @click="submit">
|
|
283
294
|
{{ mode === 'create' ? '新增预报' : '保存修改' }}
|
|
284
295
|
</el-button>
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
|
|
3
3
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
4
|
-
import API
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
import API from '../api'
|
|
5
|
+
import {
|
|
6
|
+
compareAmounts, ensureSuccess, formatAmount, formatDate, getErrorMessage, normalizeAmount, readPage,
|
|
7
|
+
} from '../utils'
|
|
7
8
|
import { forecastRuntime } from '../../runtime'
|
|
8
9
|
import { ArtDataTable, type ColumnOption, type PaginationOptions, type TableSettings } from '@/components/table'
|
|
9
10
|
import type { ForecastFilters, ForecastRow } from '../typs'
|
|
10
11
|
|
|
11
|
-
defineOptions({ name: '
|
|
12
|
+
defineOptions({ name: 'DcpsForecastList' })
|
|
12
13
|
|
|
13
14
|
/** 列表页只负责查询和展示;新建、修改、详情由外层页面切换。 */
|
|
14
15
|
const emit = defineEmits<{
|
|
@@ -98,7 +99,7 @@ const loadList = async () => {
|
|
|
98
99
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
99
100
|
const response = await API.queryListApi({ ...query, opType: 'query', pageIndex, pageSize })
|
|
100
101
|
if (disposed || currentId !== requestId) return
|
|
101
|
-
const page =
|
|
102
|
+
const page = readPage(ensureSuccess(response, '预报查询失败'))
|
|
102
103
|
const lastPage = Math.max(1, Math.ceil(page.total / pageSize))
|
|
103
104
|
if (pageIndex > lastPage && attempt === 0) {
|
|
104
105
|
pageIndex = lastPage
|
|
@@ -279,16 +280,12 @@ defineExpose({ refresh: loadList, getQueryDate: () => appliedQuery.tranDate })
|
|
|
279
280
|
<style scoped>
|
|
280
281
|
/* 页面先扣除宿主占用高度,再使用 flex 分配剩余高度,让列表内部滚动并保留分页。 */
|
|
281
282
|
.dcps-page {
|
|
282
|
-
/* 手动修改这里:顶部导航/页签 + 底部状态栏 + 宿主上下留白的合计高度。
|
|
283
|
-
独立页面保持 0px;例如顶部 90px、底部 24px、上下留白 6px,填写 120px。
|
|
284
|
-
不包含浏览器标签栏、地址栏,也不包含本页面自身的 padding。 */
|
|
285
|
-
--host-layout-height: 0px;
|
|
286
283
|
box-sizing: border-box;
|
|
287
284
|
display: flex;
|
|
288
285
|
flex-direction: column;
|
|
289
286
|
gap: 5px;
|
|
290
|
-
height: calc(100vh - var(--host-layout-height));
|
|
291
|
-
height: calc(100dvh - var(--host-layout-height));
|
|
287
|
+
height: calc(100vh - var(--dpcs-host-layout-height));
|
|
288
|
+
height: calc(100dvh - var(--dpcs-host-layout-height));
|
|
292
289
|
min-width: 0;
|
|
293
290
|
min-height: 0;
|
|
294
291
|
padding: 5px;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
defineOptions({ name: '
|
|
2
|
+
defineOptions({ name: 'DcpsForecastPageShell' })
|
|
3
3
|
|
|
4
4
|
/** 页面外壳:固定提供返回按钮和页面标题,内容区由各页面自行填充。 */
|
|
5
5
|
defineProps<{
|
|
@@ -32,16 +32,12 @@ const emit = defineEmits<{ back: [] }>()
|
|
|
32
32
|
<style scoped>
|
|
33
33
|
/* 与列表页面一致:扣除宿主占用高度后,把剩余空间分给内容区,内容区内部滚动。 */
|
|
34
34
|
.page-shell {
|
|
35
|
-
/* 手动修改这里:顶部导航/页签 + 底部状态栏 + 宿主上下留白的合计高度。
|
|
36
|
-
独立页面保持 0px;例如顶部 90px、底部 24px、上下留白 6px,填写 120px。
|
|
37
|
-
不包含浏览器标签栏、地址栏,也不包含本页面自身的 padding。 */
|
|
38
|
-
--host-layout-height: 0px;
|
|
39
35
|
box-sizing: border-box;
|
|
40
36
|
display: flex;
|
|
41
37
|
flex-direction: column;
|
|
42
38
|
gap: 5px;
|
|
43
|
-
height: calc(100vh - var(--host-layout-height));
|
|
44
|
-
height: calc(100dvh - var(--host-layout-height));
|
|
39
|
+
height: calc(100vh - var(--dpcs-host-layout-height));
|
|
40
|
+
height: calc(100dvh - var(--dpcs-host-layout-height));
|
|
45
41
|
min-width: 0;
|
|
46
42
|
min-height: 0;
|
|
47
43
|
padding: 5px;
|