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
|
@@ -1,66 +1,38 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { ref } from 'vue'
|
|
2
|
+
import { ref, watch } from 'vue'
|
|
3
|
+
import { useRouter } from 'vue-router'
|
|
3
4
|
import ForecastListView from './components/ForecastListView.vue'
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import type {
|
|
5
|
+
import { forecastListRefreshToken } from './list-refresh'
|
|
6
|
+
import { rememberForecastRow } from './row-cache'
|
|
7
|
+
import type { ForecastRow } from './typs'
|
|
7
8
|
|
|
8
|
-
defineOptions({ name: '
|
|
9
|
+
defineOptions({ name: 'DcpsForecastPage' })
|
|
9
10
|
|
|
10
|
-
/**
|
|
11
|
-
|
|
12
|
-
* 项目未引入路由库,切换方式与侧边菜单一致。
|
|
13
|
-
*/
|
|
14
|
-
const view = ref<'list' | 'editor' | 'detail'>('list')
|
|
15
|
-
const editorMode = ref<ForecastEditorMode>('create')
|
|
16
|
-
const activeRow = ref<ForecastRow | null>(null) // 修改和详情使用的列表记录。
|
|
11
|
+
/** 预报列表页入口;新增、修改和详情分别对应独立路由。 */
|
|
12
|
+
const router = useRouter()
|
|
17
13
|
const listRef = ref<InstanceType<typeof ForecastListView> | null>(null)
|
|
18
14
|
|
|
19
15
|
const openCreate = () => {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
view.value = 'editor'
|
|
16
|
+
const tranDate = listRef.value?.getQueryDate()
|
|
17
|
+
void router.push({ name: 'forecast-create', query: tranDate ? { tranDate } : {} })
|
|
23
18
|
}
|
|
24
19
|
const openEdit = (row: ForecastRow) => {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
20
|
+
const predRefNo = row.predRefNo?.trim()
|
|
21
|
+
if (!predRefNo) return
|
|
22
|
+
rememberForecastRow(row)
|
|
23
|
+
void router.push({ name: 'forecast-edit', params: { predRefNo } })
|
|
28
24
|
}
|
|
29
25
|
const openDetail = (row: ForecastRow) => {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const backToList = () => { view.value = 'list' }
|
|
35
|
-
const onSaved = () => {
|
|
36
|
-
backToList()
|
|
37
|
-
void listRef.value?.refresh()
|
|
26
|
+
const predRefNo = row.predRefNo?.trim()
|
|
27
|
+
if (!predRefNo) return
|
|
28
|
+
rememberForecastRow(row)
|
|
29
|
+
void router.push({ name: 'forecast-detail', params: { predRefNo } })
|
|
38
30
|
}
|
|
31
|
+
|
|
32
|
+
// 保存成功返回列表时重新查询;筛选条件和分页由缓存中的列表页保留。
|
|
33
|
+
watch(forecastListRefreshToken, () => void listRef.value?.refresh())
|
|
39
34
|
</script>
|
|
40
35
|
|
|
41
36
|
<template>
|
|
42
|
-
|
|
43
|
-
<ForecastListView
|
|
44
|
-
v-show="view === 'list'"
|
|
45
|
-
ref="listRef"
|
|
46
|
-
@create="openCreate"
|
|
47
|
-
@edit="openEdit"
|
|
48
|
-
@detail="openDetail"
|
|
49
|
-
/>
|
|
50
|
-
<ForecastEditorView
|
|
51
|
-
v-if="view === 'editor'"
|
|
52
|
-
:key="`${editorMode}-${activeRow?.predRefNo ?? 'new'}`"
|
|
53
|
-
:mode="editorMode"
|
|
54
|
-
:row="activeRow"
|
|
55
|
-
:tran-date="listRef?.getQueryDate()"
|
|
56
|
-
@back="backToList"
|
|
57
|
-
@saved="onSaved"
|
|
58
|
-
/>
|
|
59
|
-
<ForecastDetailView
|
|
60
|
-
v-else-if="view === 'detail'"
|
|
61
|
-
:key="activeRow?.predRefNo"
|
|
62
|
-
:row="activeRow!"
|
|
63
|
-
@back="backToList"
|
|
64
|
-
@edit="openEdit"
|
|
65
|
-
/>
|
|
37
|
+
<ForecastListView ref="listRef" @create="openCreate" @edit="openEdit" @detail="openDetail" />
|
|
66
38
|
</template>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ForecastRow } from './typs'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 列表记录缓存。
|
|
5
|
+
* 详情和修改接口没有返回全部字段,路由跳转前暂存列表记录,用于补齐页面展示和表单回填。
|
|
6
|
+
* 直接打开或刷新详情地址时没有缓存,页面会按预报流水号单独请求详情。
|
|
7
|
+
*/
|
|
8
|
+
const rows = new Map<string, ForecastRow>()
|
|
9
|
+
|
|
10
|
+
export const rememberForecastRow = (row: ForecastRow) => {
|
|
11
|
+
const predRefNo = row.predRefNo?.trim()
|
|
12
|
+
if (predRefNo) rows.set(predRefNo, row)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const recallForecastRow = (predRefNo?: string): ForecastRow | null => {
|
|
16
|
+
const key = predRefNo?.trim()
|
|
17
|
+
return key ? rows.get(key) ?? null : null
|
|
18
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import API from './api'
|
|
2
|
+
import type { ApiResponse } from '@/utils/request'
|
|
3
|
+
import type {
|
|
4
|
+
ForecastDetail,
|
|
5
|
+
ForecastFormValues,
|
|
6
|
+
ForecastListBody,
|
|
7
|
+
ForecastRow,
|
|
8
|
+
} from './typs'
|
|
9
|
+
|
|
10
|
+
/** 列表查询结果:当前页记录和符合条件的总条数。 */
|
|
11
|
+
export interface ForecastPage {
|
|
12
|
+
data: ForecastRow[] // 当前页的预报记录。
|
|
13
|
+
total: number // 符合查询条件的总记录数。
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** 校验公共响应头;retCode 不是 00000 时按服务端提示或 fallback 抛错。 */
|
|
17
|
+
export function ensureSuccess<T>(response: ApiResponse<T>, fallback: string): T | null {
|
|
18
|
+
if (response?.head?.retCode !== '00000') {
|
|
19
|
+
throw new Error(response?.head?.retMsg?.trim() || fallback)
|
|
20
|
+
}
|
|
21
|
+
return response.body
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 提取可展示的错误信息;无法识别时给出统一提示。 */
|
|
25
|
+
export function getErrorMessage(error: unknown): string {
|
|
26
|
+
if (error instanceof Error && error.message.trim()) return error.message
|
|
27
|
+
if (typeof error === 'string' && error.trim()) return error
|
|
28
|
+
return '操作失败,请稍后重试'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 业务日期始终按上海时区生成,不依赖浏览器所在时区,格式为 YYYYMMDD。 */
|
|
32
|
+
export function getToday(): string {
|
|
33
|
+
const parts = new Intl.DateTimeFormat('en-CA', {
|
|
34
|
+
timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit',
|
|
35
|
+
}).formatToParts(new Date())
|
|
36
|
+
const read = (type: Intl.DateTimeFormatPartTypes) => parts.find((part) => part.type === type)?.value || ''
|
|
37
|
+
return read('year') + read('month') + read('day')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 判断是否为合法的 YYYYMMDD 日期。 */
|
|
41
|
+
export function isValidDate(value: string): boolean {
|
|
42
|
+
if (!/^\d{8}$/.test(value)) return false
|
|
43
|
+
const year = Number(value.slice(0, 4))
|
|
44
|
+
const month = Number(value.slice(4, 6))
|
|
45
|
+
const day = Number(value.slice(6, 8))
|
|
46
|
+
if (year < 1 || month < 1 || month > 12 || day < 1) return false
|
|
47
|
+
const leap = year % 400 === 0 || (year % 4 === 0 && year % 100 !== 0)
|
|
48
|
+
const monthDays = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
|
49
|
+
return day <= (monthDays[month - 1] || 0)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** 把 YYYYMMDD 格式化为 YYYY-MM-DD 展示;空值显示横线,其他格式保留原文。 */
|
|
53
|
+
export function formatDate(value?: string): string {
|
|
54
|
+
const text = value?.trim() || ''
|
|
55
|
+
if (!text) return '—'
|
|
56
|
+
return isValidDate(text) ? `${text.slice(0, 4)}-${text.slice(4, 6)}-${text.slice(6, 8)}` : text
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 只接受十进制文本和合法千分位;保留全部小数位,避免改变输入精度。 */
|
|
60
|
+
export function normalizeAmount(value: string): string {
|
|
61
|
+
let text = value.trim()
|
|
62
|
+
if (text.includes(',')) {
|
|
63
|
+
if (!/^[+-]?\d{1,3}(?:,\d{3})+(?:\.\d*)?$/.test(text)) return ''
|
|
64
|
+
text = text.replace(/,/g, '')
|
|
65
|
+
}
|
|
66
|
+
const match = /^([+-]?)(\d*)(?:\.(\d*))?$/.exec(text)
|
|
67
|
+
if (!match || (!match[2] && !match[3])) return ''
|
|
68
|
+
const integer = (match[2] || '0').replace(/^0+(?=\d)/, '')
|
|
69
|
+
const fraction = match[3] || ''
|
|
70
|
+
const negative = match[1] === '-' && /[1-9]/.test(integer + fraction)
|
|
71
|
+
return `${negative ? '-' : ''}${integer}${fraction ? `.${fraction}` : ''}`
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 千分位展示至少两位小数,超过两位的小数原样保留,不做浮点数转换或舍入。 */
|
|
75
|
+
export function formatAmount(value?: string): string {
|
|
76
|
+
const original = value?.trim() || ''
|
|
77
|
+
if (!original) return '—'
|
|
78
|
+
const normalized = normalizeAmount(original)
|
|
79
|
+
if (!normalized) return original
|
|
80
|
+
const negative = normalized.startsWith('-')
|
|
81
|
+
const [integer = '0', fraction = ''] = (negative ? normalized.slice(1) : normalized).split('.')
|
|
82
|
+
const grouped = integer.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
|
83
|
+
return `${negative ? '-' : ''}${grouped}.${fraction.padEnd(2, '0')}`
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** 按统一小数位数转为 BigInt,任意长度的十进制金额都可精确比较。 */
|
|
87
|
+
export function compareAmounts(left: string, right: string): number {
|
|
88
|
+
const leftText = normalizeAmount(left)
|
|
89
|
+
const rightText = normalizeAmount(right)
|
|
90
|
+
if (!leftText || !rightText) {
|
|
91
|
+
if (!leftText && !rightText) return 0
|
|
92
|
+
return leftText ? -1 : 1
|
|
93
|
+
}
|
|
94
|
+
const parts = (text: string) => {
|
|
95
|
+
const negative = text.startsWith('-')
|
|
96
|
+
const [integer = '0', fraction = ''] = (negative ? text.slice(1) : text).split('.')
|
|
97
|
+
return { negative, integer, fraction }
|
|
98
|
+
}
|
|
99
|
+
const a = parts(leftText)
|
|
100
|
+
const b = parts(rightText)
|
|
101
|
+
const precision = Math.max(a.fraction.length, b.fraction.length)
|
|
102
|
+
const toInteger = (amount: ReturnType<typeof parts>) =>
|
|
103
|
+
BigInt(amount.integer + amount.fraction.padEnd(precision, '0')) * (amount.negative ? -1n : 1n)
|
|
104
|
+
const aValue = toInteger(a)
|
|
105
|
+
const bValue = toInteger(b)
|
|
106
|
+
return aValue < bValue ? -1 : aValue > bValue ? 1 : 0
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** 新增和导入使用的表单初始值;日期默认取当天。 */
|
|
110
|
+
export function createFormValues(): ForecastFormValues {
|
|
111
|
+
const today = getToday()
|
|
112
|
+
return {
|
|
113
|
+
billDate: '', billNo: '', busiType: '', clientName: '', clientType: '',
|
|
114
|
+
competAcct: '', competName: '', custInfoSeq: '', groundBranch: '', groundBranchName: '',
|
|
115
|
+
linkMan: '', payChannel: '', payDirec: '', phoneNo: '', predAmt: '', predDate: today,
|
|
116
|
+
predName: '', predOrgName: '', purpose: '', remark: '', tranDate: today,
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 校验表单;新增校验全部必填项,修改只校验已填写的字段。
|
|
122
|
+
* @param values 表单值。
|
|
123
|
+
* @param mode create 为新增,edit 为修改。
|
|
124
|
+
* @returns 错误提示文本,空字符串表示校验通过。
|
|
125
|
+
*/
|
|
126
|
+
export function validateForm(values: ForecastFormValues, mode: 'create' | 'edit' = 'create'): string {
|
|
127
|
+
const requiredFields = [
|
|
128
|
+
['clientName', '客户名称'], ['payDirec', '收支方向'], ['busiType', '业务类型'],
|
|
129
|
+
['payChannel', '支付渠道'], ['groundBranch', '落地机构'],
|
|
130
|
+
] as const
|
|
131
|
+
if (mode === 'create') {
|
|
132
|
+
for (const [key, label] of requiredFields) {
|
|
133
|
+
if (!values[key].trim()) return `请填写${label}`
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
for (const [key, label] of [['tranDate', '交易日期'], ['predDate', '预报日期']] as const) {
|
|
137
|
+
const date = values[key].trim()
|
|
138
|
+
if ((date || mode === 'create') && !isValidDate(date)) return `${label}须为有效日期,格式为 YYYYMMDD`
|
|
139
|
+
}
|
|
140
|
+
if (values.billDate.trim() && !isValidDate(values.billDate.trim())) {
|
|
141
|
+
return '出票日须为有效日期,格式为 YYYYMMDD'
|
|
142
|
+
}
|
|
143
|
+
if (values.predAmt.trim() || mode === 'create') {
|
|
144
|
+
const amount = normalizeAmount(values.predAmt)
|
|
145
|
+
if (!amount || amount.startsWith('-') || (amount.split('.')[1]?.length || 0) > 2) {
|
|
146
|
+
return '预报金额须为非负金额,最多保留两位小数'
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return ''
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** 读取详情并与列表记录合并:详情没有的字段由同一条列表记录补充。 */
|
|
153
|
+
export async function fetchDetail(row: ForecastRow): Promise<ForecastDetail> {
|
|
154
|
+
if (!row.predRefNo?.trim()) throw new Error('预报流水号缺失,无法读取详情')
|
|
155
|
+
const body = ensureSuccess(await API.getDetailApi({ predRefNo: row.predRefNo }), '读取预报详情失败')
|
|
156
|
+
if (!body || !Object.keys(body).length) throw new Error('服务端未返回预报详情')
|
|
157
|
+
if (body.predRefNo && body.predRefNo !== row.predRefNo) throw new Error('返回的预报流水号与当前记录不一致')
|
|
158
|
+
const definedBody = Object.fromEntries(Object.entries(body).filter(([, value]) => value !== undefined && value !== null))
|
|
159
|
+
return { ...row, ...definedBody } as ForecastDetail
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** 校验列表响应并取出当前页数据;列表缺失或总条数无效时抛出可提示的错误。 */
|
|
163
|
+
export function readPage(body: ForecastListBody | null): ForecastPage {
|
|
164
|
+
if (!body || !Array.isArray(body.resultList)) throw new Error('查询结果缺少预报列表,请联系服务端确认返回数据')
|
|
165
|
+
const totalText = String(body.totalCount ?? body.resultList.length)
|
|
166
|
+
const total = Number(totalText)
|
|
167
|
+
if (!/^\d+$/.test(totalText) || !Number.isSafeInteger(total)) throw new Error('查询返回的总条数无效')
|
|
168
|
+
return { data: body.resultList, total }
|
|
169
|
+
}
|
|
@@ -6,6 +6,8 @@ import InjectionDetailDialog from './components/InjectionDetailDialog.vue'
|
|
|
6
6
|
import API from './api'
|
|
7
7
|
import type { InjectionFilters, InjectionQueryParams, InjectionRecord } from './typs'
|
|
8
8
|
|
|
9
|
+
defineOptions({ name: 'DcpsInjectionPage' })
|
|
10
|
+
|
|
9
11
|
type AmountSortOrder = 'ascending' | 'descending' | null
|
|
10
12
|
|
|
11
13
|
// 初始查询条件;空字符串表示不限制该条件,重置时恢复此配置。
|
|
@@ -327,16 +329,12 @@ onBeforeUnmount(() => {
|
|
|
327
329
|
<style scoped>
|
|
328
330
|
/* 页面先扣除宿主占用高度,再使用 flex 分配剩余高度,让列表内部滚动并保留分页。 */
|
|
329
331
|
.dcps-page {
|
|
330
|
-
/* 手动修改这里:顶部导航/页签 + 底部状态栏 + 宿主上下留白的合计高度。
|
|
331
|
-
独立页面保持 0px;例如顶部 90px、底部 24px、上下留白 6px,填写 120px。
|
|
332
|
-
不包含浏览器标签栏、地址栏,也不包含本页面自身的 padding。 */
|
|
333
|
-
--host-layout-height: 0px;
|
|
334
332
|
box-sizing: border-box;
|
|
335
333
|
display: flex;
|
|
336
334
|
flex-direction: column;
|
|
337
335
|
gap: 5px;
|
|
338
|
-
height: calc(100vh - var(--host-layout-height));
|
|
339
|
-
height: calc(100dvh - var(--host-layout-height));
|
|
336
|
+
height: calc(100vh - var(--dpcs-host-layout-height));
|
|
337
|
+
height: calc(100dvh - var(--dpcs-host-layout-height));
|
|
340
338
|
min-height: 0;
|
|
341
339
|
min-width: 0;
|
|
342
340
|
padding: 5px;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { postDigitalRmb, type DigitalRmbParams } from '@/utils/digital-rmb-request'
|
|
2
|
-
import type { ApiResponse } from '@/utils/request'
|
|
3
2
|
import type {
|
|
4
3
|
Tam120502001InBody,
|
|
5
4
|
Tam120502002InBody,
|
|
@@ -11,182 +10,28 @@ import type {
|
|
|
11
10
|
} from './typs'
|
|
12
11
|
|
|
13
12
|
/**
|
|
14
|
-
*
|
|
13
|
+
* 数字人民币头寸核销接口。
|
|
14
|
+
* 修改 120502001、核销 120502002、撤销核销 120502003、预报查询 140502001、流水查询 140502002。
|
|
15
15
|
* 支持平铺参数和 { appHead, head, body } 报文;业务 tranDate 始终保留在 body。
|
|
16
|
-
* 金额、日期、账号及流水号使用字符串,响应保留 TAM 的 head、body 结构。
|
|
17
16
|
*/
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* 头寸核销-修改(120502001)。
|
|
21
|
-
* @param params predRefNo 为预报流水号,predAmt 为预报金额,
|
|
22
|
-
* beforeVerifyAmt 为修改前核销金额,verifyAmt 为已核销金额。
|
|
23
|
-
*/
|
|
17
|
+
export default {
|
|
18
|
+
/** 头寸核销-修改。 */
|
|
24
19
|
editApi: (params: DigitalRmbParams<Tam120502001InBody>) =>
|
|
25
20
|
postDigitalRmb<Tam120502001InBody, Record<string, unknown>>('/TAM/0001/120502001', params),
|
|
26
21
|
|
|
27
|
-
/**
|
|
28
|
-
* 头寸核销-核销(120502002)。
|
|
29
|
-
* @param params predList 为必填预报集合,每项包含 predRefNo、predAmt、verifyAmt、verifyStatus;
|
|
30
|
-
* tranList 为可选流水集合,传入时每项必须包含 refNo、tranAmt、verifyStatus。
|
|
31
|
-
*/
|
|
22
|
+
/** 头寸核销-核销;predList 必填,tranList 可选。 */
|
|
32
23
|
writeOffApi: (params: DigitalRmbParams<Tam120502002InBody>) =>
|
|
33
24
|
postDigitalRmb<Tam120502002InBody, Record<string, unknown>>('/TAM/0001/120502002', params),
|
|
34
25
|
|
|
35
|
-
/**
|
|
36
|
-
* 头寸核销-撤销核销(120502003)。
|
|
37
|
-
* @param params predList 为预报集合,tranList 为流水集合;Swagger 未声明集合必填。
|
|
38
|
-
* 流水项必须包含 refNo、tranAmt、verifyStatus;预报项字段的必填约束与核销接口不同。
|
|
39
|
-
*/
|
|
26
|
+
/** 头寸核销-撤销核销。 */
|
|
40
27
|
cancelWriteOffApi: (params: DigitalRmbParams<Tam120502003InBody>) =>
|
|
41
28
|
postDigitalRmb<Tam120502003InBody, Record<string, unknown>>('/TAM/0001/120502003', params),
|
|
42
29
|
|
|
43
|
-
/**
|
|
44
|
-
* 头寸核销-预报查询(140502001)。
|
|
45
|
-
* @param params tranDate 为必填交易日期;支持账号、机构、客户、交易对手、金额范围和核销状态列表筛选。
|
|
46
|
-
* busiType:201 兑回、202 兑出、203 付现;payChannel:8 数字人民币;payDirec:I 来账、O 往账。
|
|
47
|
-
* 分页使用公共头 pageIndex、pageSize;返回 body.resultList、body.totalCount(字符串)。
|
|
48
|
-
*/
|
|
30
|
+
/** 头寸核销-预报查询;tranDate 必填,分页使用 pageIndex、pageSize。 */
|
|
49
31
|
queryForecastApi: (params: DigitalRmbParams<Tam140502001InBody>) =>
|
|
50
32
|
postDigitalRmb<Tam140502001InBody, Tam140502001OutBody>('/TAM/0001/140502001', params),
|
|
51
33
|
|
|
52
|
-
/**
|
|
53
|
-
* 头寸核销-流水查询(140502002)。
|
|
54
|
-
* @param params tranDate 为必填交易日期;支持 orgName、payerName、payeeName、payDirec、
|
|
55
|
-
* startTransAmt、endTransAmt、verifyStatus(1 未核销、2 已核销)筛选。
|
|
56
|
-
* 返回 body.resultList、body.totalCount 和可选 fileName;流水的渠道说明为 5 数字人民币。
|
|
57
|
-
*/
|
|
34
|
+
/** 头寸核销-流水查询;tranDate 必填,分页使用 pageIndex、pageSize。 */
|
|
58
35
|
queryFlowApi: (params: DigitalRmbParams<Tam140502002InBody>) =>
|
|
59
|
-
postDigitalRmb<Tam140502002InBody, Tam140502002OutBody>('/TAM/0001/140502002', params)
|
|
36
|
+
postDigitalRmb<Tam140502002InBody, Tam140502002OutBody>('/TAM/0001/140502002', params)
|
|
60
37
|
}
|
|
61
|
-
|
|
62
|
-
/** 列表查询结果:当前页记录和符合条件的总条数。 */
|
|
63
|
-
export interface WriteOffPage<T> {
|
|
64
|
-
data: T[] // 当前页的记录列表。
|
|
65
|
-
total: number // 符合查询条件的总记录数。
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** 校验公共响应头;retCode 不是 00000 时按服务端提示或 fallback 抛错。 */
|
|
69
|
-
export function ensureSuccess<T>(response: ApiResponse<T>, fallback: string): T | null {
|
|
70
|
-
if (response?.head?.retCode !== '00000') {
|
|
71
|
-
throw new Error(response?.head?.retMsg?.trim() || fallback)
|
|
72
|
-
}
|
|
73
|
-
return response.body
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* 校验查询响应并取出当前页数据;两个查询接口共享同一种 body 结构。
|
|
78
|
-
* @param body 查询接口返回的业务数据。
|
|
79
|
-
* @param fallback body 缺少 resultList 时的错误提示文本。
|
|
80
|
-
*/
|
|
81
|
-
export function readPage<R extends { resultList?: unknown[]; totalCount?: unknown }>(
|
|
82
|
-
body: R | null,
|
|
83
|
-
fallback: string,
|
|
84
|
-
): WriteOffPage<NonNullable<R['resultList']>[number]> {
|
|
85
|
-
if (!body || !Array.isArray(body.resultList)) throw new Error(fallback)
|
|
86
|
-
const totalText = String(body.totalCount ?? body.resultList.length)
|
|
87
|
-
const total = Number(totalText)
|
|
88
|
-
if (!/^\d+$/.test(totalText) || !Number.isSafeInteger(total)) throw new Error('查询返回的总条数无效')
|
|
89
|
-
return { data: body.resultList, total }
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/** 提取可展示的错误信息;无法识别时给出统一提示。 */
|
|
93
|
-
export function getErrorMessage(error: unknown): string {
|
|
94
|
-
if (error instanceof Error && error.message.trim()) return error.message
|
|
95
|
-
if (typeof error === 'string' && error.trim()) return error
|
|
96
|
-
return '操作失败,请稍后重试'
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/** 业务日期始终按上海时区生成,不依赖浏览器所在时区,格式为 YYYYMMDD。 */
|
|
100
|
-
export function getToday(): string {
|
|
101
|
-
const parts = new Intl.DateTimeFormat('en-CA', {
|
|
102
|
-
timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit',
|
|
103
|
-
}).formatToParts(new Date())
|
|
104
|
-
const read = (type: Intl.DateTimeFormatPartTypes) => parts.find((part) => part.type === type)?.value || ''
|
|
105
|
-
return read('year') + read('month') + read('day')
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/** 判断是否为合法的 YYYYMMDD 日期。 */
|
|
109
|
-
export function isValidDate(value: string): boolean {
|
|
110
|
-
if (!/^\d{8}$/.test(value)) return false
|
|
111
|
-
const year = Number(value.slice(0, 4))
|
|
112
|
-
const month = Number(value.slice(4, 6))
|
|
113
|
-
const day = Number(value.slice(6, 8))
|
|
114
|
-
if (year < 1 || month < 1 || month > 12 || day < 1) return false
|
|
115
|
-
const leap = year % 400 === 0 || (year % 4 === 0 && year % 100 !== 0)
|
|
116
|
-
const monthDays = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
|
117
|
-
return day <= (monthDays[month - 1] || 0)
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/** 把 YYYYMMDD 格式化为 YYYY-MM-DD 展示;空值显示横线,其他格式保留原文。 */
|
|
121
|
-
export function formatDate(value?: string): string {
|
|
122
|
-
const text = value?.trim() || ''
|
|
123
|
-
if (!text) return '—'
|
|
124
|
-
return isValidDate(text) ? `${text.slice(0, 4)}-${text.slice(4, 6)}-${text.slice(6, 8)}` : text
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/** 只接受十进制文本和合法千分位;保留全部小数位,避免改变输入精度。 */
|
|
128
|
-
export function normalizeAmount(value: string): string {
|
|
129
|
-
let text = value.trim()
|
|
130
|
-
if (text.includes(',')) {
|
|
131
|
-
if (!/^[+-]?\d{1,3}(?:,\d{3})+(?:\.\d*)?$/.test(text)) return ''
|
|
132
|
-
text = text.replace(/,/g, '')
|
|
133
|
-
}
|
|
134
|
-
const match = /^([+-]?)(\d*)(?:\.(\d*))?$/.exec(text)
|
|
135
|
-
if (!match || (!match[2] && !match[3])) return ''
|
|
136
|
-
const integer = (match[2] || '0').replace(/^0+(?=\d)/, '')
|
|
137
|
-
const fraction = match[3] || ''
|
|
138
|
-
const negative = match[1] === '-' && /[1-9]/.test(integer + fraction)
|
|
139
|
-
return `${negative ? '-' : ''}${integer}${fraction ? `.${fraction}` : ''}`
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/** 千分位展示至少两位小数,超过两位的小数原样保留,不做浮点数转换或舍入。 */
|
|
143
|
-
export function formatAmount(value?: string): string {
|
|
144
|
-
const original = value?.trim() || ''
|
|
145
|
-
if (!original) return '—'
|
|
146
|
-
const normalized = normalizeAmount(original)
|
|
147
|
-
if (!normalized) return original
|
|
148
|
-
const negative = normalized.startsWith('-')
|
|
149
|
-
const [integer = '0', fraction = ''] = (negative ? normalized.slice(1) : normalized).split('.')
|
|
150
|
-
const grouped = integer.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
|
151
|
-
return `${negative ? '-' : ''}${grouped}.${fraction.padEnd(2, '0')}`
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/** 按统一小数位数转为 BigInt,任意长度的十进制金额都可精确比较。 */
|
|
155
|
-
export function compareAmounts(left: string, right: string): number {
|
|
156
|
-
const leftText = normalizeAmount(left)
|
|
157
|
-
const rightText = normalizeAmount(right)
|
|
158
|
-
if (!leftText || !rightText) {
|
|
159
|
-
if (!leftText && !rightText) return 0
|
|
160
|
-
return leftText ? -1 : 1
|
|
161
|
-
}
|
|
162
|
-
const parts = (text: string) => {
|
|
163
|
-
const negative = text.startsWith('-')
|
|
164
|
-
const [integer = '0', fraction = ''] = (negative ? text.slice(1) : text).split('.')
|
|
165
|
-
return { negative, integer, fraction }
|
|
166
|
-
}
|
|
167
|
-
const a = parts(leftText)
|
|
168
|
-
const b = parts(rightText)
|
|
169
|
-
const precision = Math.max(a.fraction.length, b.fraction.length)
|
|
170
|
-
const toInteger = (amount: ReturnType<typeof parts>) =>
|
|
171
|
-
BigInt(amount.integer + amount.fraction.padEnd(precision, '0')) * (amount.negative ? -1n : 1n)
|
|
172
|
-
const aValue = toInteger(a)
|
|
173
|
-
const bValue = toInteger(b)
|
|
174
|
-
return aValue < bValue ? -1 : aValue > bValue ? 1 : 0
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/** 业务类型字典:201 兑回、202 兑出、203 付现;接口附件已声明,超出字典的值保留原文。 */
|
|
178
|
-
export const busiTypeLabels: Record<string, string> = { '201': '兑回', '202': '兑出', '203': '付现' }
|
|
179
|
-
/** 收支方向字典:I 来账、O 往账。 */
|
|
180
|
-
export const payDirecLabels: Record<string, string> = { I: '来账', O: '往账' }
|
|
181
|
-
/** 支付渠道:核销预报为 8、流水为 5,均表示数字人民币;其余渠道保留原文。 */
|
|
182
|
-
export const payChannelLabels: Record<string, string> = { '5': '数字人民币', '8': '数字人民币' }
|
|
183
|
-
/** 核销状态字典:流水的 1 未核销、2 已核销已在附件声明,预报按同一枚举展示。 */
|
|
184
|
-
export const verifyStatusLabels: Record<string, string> = { '1': '未核销', '2': '已核销' }
|
|
185
|
-
|
|
186
|
-
/** 读取字典文案;空值显示横线,未登记的代码保留原文展示。 */
|
|
187
|
-
export function dictLabel(labels: Record<string, string>, value?: string): string {
|
|
188
|
-
const text = value?.trim() || ''
|
|
189
|
-
return text ? labels[text] ?? text : '—'
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
export default API
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { onBeforeUnmount, onMounted, reactive, ref } from 'vue'
|
|
3
3
|
import { ElMessage } from 'element-plus'
|
|
4
|
-
import API
|
|
4
|
+
import API from '../api'
|
|
5
|
+
import {
|
|
5
6
|
busiTypeLabels, compareAmounts, dictLabel, ensureSuccess, formatAmount, formatDate,
|
|
6
7
|
getErrorMessage, getToday, normalizeAmount, payDirecLabels, readPage, verifyStatusLabels,
|
|
7
|
-
} from '../
|
|
8
|
+
} from '../utils'
|
|
8
9
|
import { ArtDataTable, type ColumnOption, type PaginationOptions, type TableSettings } from '@/components/table'
|
|
9
10
|
import type { FlowRow } from '../typs'
|
|
10
11
|
|
|
11
|
-
defineOptions({ name: '
|
|
12
|
+
defineOptions({ name: 'DcpsFlowWriteOff' })
|
|
12
13
|
|
|
13
14
|
// 流水信息表格只负责查询和勾选;核销、撤销核销由父页面统一提交。
|
|
14
15
|
|
|
@@ -1,15 +1,16 @@
|
|
|
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
|
|
4
|
+
import API from '../api'
|
|
5
|
+
import {
|
|
5
6
|
busiTypeLabels, compareAmounts, dictLabel, ensureSuccess, formatAmount, formatDate,
|
|
6
7
|
getErrorMessage, getToday, normalizeAmount, payChannelLabels, payDirecLabels, readPage,
|
|
7
8
|
verifyStatusLabels,
|
|
8
|
-
} from '../
|
|
9
|
+
} from '../utils'
|
|
9
10
|
import { ArtDataTable, type ColumnOption, type PaginationOptions, type TableSettings } from '@/components/table'
|
|
10
11
|
import type { EditVerifyFormValues, ForecastRow } from '../typs'
|
|
11
12
|
|
|
12
|
-
defineOptions({ name: '
|
|
13
|
+
defineOptions({ name: 'DcpsForecastWriteOff' })
|
|
13
14
|
|
|
14
15
|
/** 预报信息表格负责查询、勾选核销预报和逐条修改已核销金额;
|
|
15
16
|
* 核销、撤销核销按钮在本表格工具栏展示,由父页面读取两个表格的勾选统一提交。 */
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, ref } from 'vue'
|
|
3
3
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
4
|
-
import API
|
|
4
|
+
import API from './api'
|
|
5
|
+
import { ensureSuccess, formatAmount, getErrorMessage, normalizeAmount } from './utils'
|
|
5
6
|
import ForecastWriteOffView from './components/ForecastWriteOffView.vue'
|
|
6
7
|
import FlowWriteOffView from './components/FlowWriteOffView.vue'
|
|
7
8
|
import type {
|
|
8
9
|
CancelWriteOffPredItem, FlowRow, ForecastRow, WriteOffFlowInput, WriteOffForecastItem, WriteOffItemInput,
|
|
9
10
|
} from './typs'
|
|
10
11
|
|
|
12
|
+
defineOptions({ name: 'DcpsWriteOffPage' })
|
|
13
|
+
|
|
11
14
|
// 数字人民币核销页面:上方预报信息表格,下方流水信息表格。
|
|
12
15
|
// 核销、撤销核销由两个表格的勾选行合并提交,按钮统一放在预报信息表格的工具栏。
|
|
13
16
|
|
|
@@ -233,16 +236,12 @@ const refreshBoth = async () => {
|
|
|
233
236
|
<style scoped>
|
|
234
237
|
/* 页面先扣除宿主占用高度,再使用 flex 分配剩余高度,让两个表格各占一半内部滚动。 */
|
|
235
238
|
.dcps-page {
|
|
236
|
-
/* 手动修改这里:顶部导航/页签 + 底部状态栏 + 宿主上下留白的合计高度。
|
|
237
|
-
独立页面保持 0px;例如顶部 90px、底部 24px、上下留白 6px,填写 120px。
|
|
238
|
-
不包含浏览器标签栏、地址栏,也不包含本页面自身的 padding。 */
|
|
239
|
-
--host-layout-height: 0px;
|
|
240
239
|
box-sizing: border-box;
|
|
241
240
|
display: flex;
|
|
242
241
|
flex-direction: column;
|
|
243
242
|
gap: 5px;
|
|
244
|
-
height: calc(100vh - var(--host-layout-height));
|
|
245
|
-
height: calc(100dvh - var(--host-layout-height));
|
|
243
|
+
height: calc(100vh - var(--dpcs-host-layout-height));
|
|
244
|
+
height: calc(100dvh - var(--dpcs-host-layout-height));
|
|
246
245
|
min-width: 0;
|
|
247
246
|
min-height: 0;
|
|
248
247
|
padding: 5px;
|