vue3-element-plus-app-chu 0.0.24 → 0.0.26

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 CHANGED
@@ -23,15 +23,19 @@ pnpm dev:backend # 只启动后端(backend/ 目录,tsx watch)
23
23
  默认入口为数字人民币头寸预报(`/forecast`),侧边菜单可切换至其他数字人民币页面。
24
24
 
25
25
  - 列表页只保留筛选卡片和表格卡片;筛选条件压在一行,窗口过窄时才换行。
26
- - 新增、修改、详情不使用弹窗,分别对应 `/forecast/create`、`/forecast/:predRefNo/edit`、`/forecast/:predRefNo` 独立路由,页面顶部提供“返回列表”按钮;返回后列表的筛选和分页状态不变。
26
+ - 新增、修改、详情与列表共用 `/forecast` 入口,以当前页面状态切换完整视图,页面顶部提供“返回列表”按钮;返回后列表的筛选和分页状态不变。
27
27
  - 支持交易日期、客户、交易对手、机构、金额范围及业务代码筛选,分页、详情、新增、修改和批量删除。
28
- - 金额、账号及流水号使用字符串处理。业务状态与类型代码保留接口原值,待服务端提供字典后再接入中文选项。
28
+ - 金额、账号及流水号使用字符串处理。业务类型使用文档明确的中文选项,其余未提供字典的代码保留接口原值。
29
29
  - 导入、导出按钮保留在列表工具栏,文件由后端接口生成和解析,页面不再在前端拼装 CSV。
30
- - 模块目录与授权页面保持一致:`api.ts` 只放接口定义,`utils.ts` 放日期、金额、表单校验等共用工具,`typs.ts` 类型,`runtime.ts` 默认交易日期,`index.vue` 为入口,`components/` 放页面外壳和列表、新增修改、详情页面。接口定义只维护在各自模块下,不再集中到 `src/api`。
30
+ - 模块目录与授权页面保持一致:`api.ts` 放接口定义,`typs.ts` 放类型,`index.vue` 包含列表、表单、详情和页面内工具函数。默认交易日期通过共用的 `src/view/dcps/runtime.ts` 获取,接口定义只维护在各自模块下,不再集中到 `src/api`。
31
31
  - 新增、修改、详情页面每行四列展示,窗口变窄后依次降为三列、两列、一列。
32
32
 
33
33
  本地后端已接入 MongoDB 业务模拟,预报增删改查、核销、注资余额、匡算历史和参数均可持久化。JSON 响应统一为 `{ code, data, message }`,TAM 的 `head/body` 位于 `data` 中,前端公共请求层自动解包。服务端导入协议尚未实现时明确返回 `501`。完整运行说明见 [后端 README](backend/README.md)。
34
34
 
35
+ 数字人民币相关接口已按 2026-10-26 最新 Swagger 同步:新增预报的业务类型必填并使用 `201/202/203` 字典,移除 `predName`;核销预报方向使用 `1/2`,待核销查询包含状态 `1/3`,补充交易对手账号;流水增加业务类型筛选并改用 `tranStatus`;注资明细支持起止交易日期;授权页展示业务类型中文名称。匡算明细同步展示部分核销状态。部分核销的继续处理金额语义尚未定义,当前支持查询展示,操作仍沿用原有未核销/已核销流程。字段及本地实现说明见 [数字人民币接口摘录](docs/digital-rmb-apis.md)。
36
+
37
+ 2026-10-27 新增 `/TAM/0001/140504002` 实时流水查询。独立页面 `/flow-query` 改为调用该接口,按 `txnLists` 展示平台流水、交易金额、时间、收付款人、钱包及机构信息,支持分页和刷新。该接口的业务请求体为空,页面不再提交旧核销流水的日期、姓名、金额等筛选;核销页面继续使用 `/140502002`。新接口未声明的业务类型、方向和状态字典保留原值。
38
+
35
39
  ## 城银清头寸参数接口
36
40
 
37
41
  参数管理接口已支持持久化增删改查、关键词筛选和分页。数币页面位于 `src/view/dcps/parameters/`,使用 `moduleCode: '5'`;页面详情直接取表格行数据,不再请求详情接口。同一后端也支持城银清模块 `'3'`。参数名称全局唯一,修改和删除按名称定位;接口字段和示例见 [城银清头寸参数接口说明](docs/city-clearing-parameters-apis.md),Scalar 文档入口为后端 `/docs`。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vue3-element-plus-app-chu",
3
- "version": "0.0.24",
3
+ "version": "0.0.26",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "files": [
@@ -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' {
@@ -57,6 +58,12 @@ const routes: RouteRecordRaw[] = [
57
58
  },
58
59
  ],
59
60
  },
61
+ {
62
+ path: 'flow-query',
63
+ name: 'flow-query',
64
+ component: DigitalRmbFlowQuery,
65
+ meta: { menu: 'flow-query', title: '数字人民币流水查询' },
66
+ },
60
67
  {
61
68
  path: 'auth',
62
69
  name: 'auth',
@@ -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: '参数管理' },
@@ -2,7 +2,7 @@
2
2
 
3
3
  `src/view/dcps` 下每个业务页面统一保持三个文件,不再拆分子组件、工具文件:
4
4
 
5
- 跨页面共用的运行时配置统一放在 `src/view/dcps/runtime.ts`,仅默认导出一个对象,包含 `date`(共用业务日期,格式为 `YYYYMMDD`)、`approverId`(工号)和 `approverName`(姓名)。日期在模块加载时按上海时区生成一次,预报、匡算、核销及授权查询的默认日期均使用同一个 `date`;授权日期范围的开始和结束默认都取该值。页面通过 `import dcpsRuntime from '../runtime'`(分组子页面为 `../../runtime`)引入,不再单独导出日期工具或模块配置。
5
+ 跨页面共用的运行时配置统一放在 `src/view/dcps/runtime.ts`,通过具名对象 `dcpsRuntime` 导出,包含 `date`(共用业务日期,格式为 `YYYYMMDD`)、`approverId`(工号)和 `approverName`(姓名)。日期在模块加载时按上海时区生成一次,预报、匡算、核销及授权查询的默认日期均使用同一个 `date`;授权日期范围的开始和结束默认都取该值。页面通过 `import { dcpsRuntime } from '../runtime'`(分组子页面为 `../../runtime`)引入,不再单独导出日期工具或模块配置。
6
6
 
7
7
  一级菜单为分组时,页面放在该分组目录下。例如“注资/预注资管理”的两个页面分别位于 `funding/injection/` 和 `funding/balance/`,各自保留下面三个文件。
8
8
 
@@ -14,6 +14,12 @@
14
14
 
15
15
  公共样式变量仍放在 `common.css`,由 `src/main.ts` 统一引入。
16
16
 
17
+ ## 当前接口契约
18
+
19
+ 数字人民币接口及递归引用模型以 `docs/digital-rmb.openapi.json` 的 2026-10-27 附件为准。预报新增移除 `predName`,`busiType` 为必填,字典为 `201` 兑回、`202` 兑出、`203` 付现。核销预报查询使用方向 `1/2`,待核销传 `verifyStatusList: ['1', '3']`,已核销传 `['2']`;预报管理和流水方向保持各自原协议。核销页流水查询增加 `busiType`,返回 `tranStatus`(`S/P/F`),核销状态支持 `3` 部分核销。注资明细支持 `txnDtStart`、`txnDtEnd` 单边或双边日期筛选。完整字段及本地业务限制见 `docs/digital-rmb-apis.md`。
20
+
21
+ 独立流水查询页面 `/flow-query` 使用新增的 `140504002`:业务请求体为空,公共请求头分页;读取 `txnLists` 和数字类型的 `totalCount`、`totalPage`,显示新模型的 16 个字符串字段。页面的 `api.ts`、`typs.ts` 独立维护该契约,不再复用核销页的 `140502002` 模型。业务类型、收支方向及交易状态未提供字典,按原值显示。
22
+
17
23
  ## 命名约定
18
24
 
19
25
  - 所有页面组件的 `defineOptions({ name })` 统一以 `Dcps` 开头(PascalCase)。
@@ -29,13 +35,11 @@
29
35
  | 路由路径 | 路由 name | 页面文件 | 页面形态 | 页面标题(meta.title) |
30
36
  | --- | --- | --- | --- | --- |
31
37
  | `/forecast` | `forecast` | `forecast/index.vue` | 列表 | 数字人民币头寸预报 |
32
- | `/forecast/create` | `forecast-create` | `forecast/index.vue` | 新增(`mode: 'create'`) | 新增数字人民币头寸预报 |
33
- | `/forecast/:predRefNo/edit` | `forecast-edit` | `forecast/index.vue` | 修改(`mode: 'edit'`) | 修改数字人民币头寸预报 |
34
- | `/forecast/:predRefNo` | `forecast-detail` | `forecast/index.vue` | 详情(`mode: 'detail'`) | 数字人民币头寸预报详情 |
35
38
  | `/estimation` | `estimation` | `estimation/index.vue` | 页面 | 数字人民币头寸匡算 |
36
39
  | `/funding` | `funding` | — | 重定向到 `injection` | 注资/预注资管理 |
37
40
  | `/funding/injection` | `injection` | `funding/injection/index.vue` | 页面 | 数字人民币注资明细查询 |
38
41
  | `/funding/balance` | `balance` | `funding/balance/index.vue` | 页面 | 数字人民币账户行余额查询申请 |
42
+ | `/flow-query` | `flow-query` | `flow-query/index.vue` | 页面 | 数字人民币流水查询 |
39
43
  | `/auth` | `auth` | `auth/index.vue` | 页面 | 数字人民币授权 |
40
44
  | `/write-off` | `write-off` | `write-off/index.vue` | 页面 | 数字人民币核销 |
41
45
  | `/parameters` | `parameters` | `parameters/index.vue` | 列表、详情、新增和修改弹窗 | 数字人民币参数管理 |
@@ -44,13 +48,13 @@
44
48
 
45
49
  补充说明:
46
50
 
47
- - 头寸预报的列表、新增、修改、详情共用 `forecast/index.vue`,通过路由 `props.mode` 切换页面形态;修改和详情通过 `props.predRefNo` 接收路由参数。
48
- - `forecast/index.vue` 顶部使用一个普通 `<script>` 块保存列表记录缓存,跳转修改或详情前暂存列表行,用于补齐详情接口未返回的字段。
51
+ - 头寸预报的列表、新增、修改、详情共用 `/forecast` 与 `forecast/index.vue`,通过当前组件的视图状态切换;修改和详情使用所选记录的 `predRefNo` 请求数据,返回列表保留筛选和分页。
52
+ - `forecast/index.vue` 顶部使用一个普通 `<script>` 块保存列表记录缓存,打开修改或详情前暂存列表行,用于补齐详情接口未返回的字段。
49
53
  - 侧边菜单配置在 `src/router/menus.ts`,其中 `name` 必须与对应路由的 `name` 保持一致;分组菜单通过 `children` 配置二级页面,页面的 `meta.menu` 指向自己的菜单项。
50
54
  - “注资/预注资管理”对应 `funding` 分组,包含注资明细查询和账户行余额查询申请两个页面;访问子页面时自动展开父菜单。旧地址 `/injection`、`/balance` 保留为对应页面的别名。
51
- - 数字人民币参数管理为末尾的独立菜单,页面文件为 `parameters/index.vue`、`parameters/api.ts`、`parameters/typs.ts`,接口与类型均独立定义在该目录,不再依赖或保留 `city-clearing/` 目录。页面复用“城银清算头寸管理-头寸参数”的列表、新增、修改、删除四个接口,详情直接取表格行数据,不再请求详情接口;`api.ts` 参照授权页面直接向 `@chic/http` 平铺传参,由 Axios 公共封装拆分为服务端所需的 `body`、`head` 等字段,不手动包装信封。页面查询调用只传 `keywords`、数字类型的 `pageIndex` 和 `pageSize`,`moduleCode: '5'` 由 API 内部固定添加,分页数字由封装层转换为字符串;新增传六个字符串表单字段,模块代码也由 API 固定添加。本次用户确认以 `paraName` 定位修改和删除:修改时锁定原参数名称、参数标志只读;删除向 API 传名称数组,由 API 转为平铺的 `sysParaList`。页面类型使用 `DigitalRmbParameter` 等独立类型,原始 Swagger 契约及 `docs/city-clearing-parameters*` 文档文件名保留。当前本地后端仍返回模拟结果,不保存参数。
55
+ - 数字人民币参数管理为末尾的独立菜单,页面文件为 `parameters/index.vue`、`parameters/api.ts`、`parameters/typs.ts`,接口与类型均独立定义在该目录,不再依赖或保留 `city-clearing/` 目录。页面复用“城银清算头寸管理-头寸参数”的列表、新增、修改、删除四个接口,详情直接取表格行数据,不再请求详情接口;`api.ts` 参照授权页面直接向 `@chic/http` 平铺传参,由 Axios 公共封装拆分为服务端所需的 `body`、`head` 等字段,不手动包装信封。页面查询调用只传 `keywords`、数字类型的 `pageIndex` 和 `pageSize`,`moduleCode: '5'` 由 API 内部固定添加,分页数字由封装层转换为字符串;新增传六个字符串表单字段,模块代码也由 API 固定添加。本次用户确认以 `paraName` 定位修改和删除:修改时锁定原参数名称、参数标志只读;删除向 API 传名称数组,由 API 转为平铺的 `sysParaList`。页面类型使用 `DigitalRmbParameter` 等独立类型,原始 Swagger 契约及 `docs/city-clearing-parameters*` 文档文件名保留。当前本地后端将业务模拟参数持久化至 MongoDB。
52
56
  - 数字人民币参数管理以数字人民币授权页面为布局参考,统一筛选区、填满剩余高度的表格以及包含摘要和分组字段的详情弹窗样式。
53
- - `src/layout/index.vue` 使用 `<KeepAlive include="DcpsForecastPage">` 缓存头寸预报页面;新增、修改、详情按路由 `key` 分别缓存,返回列表时自动重新查询。
57
+ - `src/layout/index.vue` 使用 `<KeepAlive include="DcpsForecastPage">` 缓存头寸预报页面;新增、修改、详情在同一实例内切换,返回列表时自动重新查询。
54
58
 
55
59
  ## 新增页面检查清单
56
60
 
@@ -34,6 +34,12 @@ const filterFields = [
34
34
  ] as const
35
35
  // 任务状态字典:01 待审批、02 已通过、03 已拒绝。
36
36
  const statusLabels: Record<string, string> = { '01': '待审批', '02': '已通过', '03': '已拒绝' }
37
+ // 业务类型按更新后的接口字典展示,未登记的代码保留原值。
38
+ const businessTypeLabels: Record<string, string> = { '201': '兑回', '202': '兑出', '203': '付现' }
39
+ const formatBusinessType = (value?: string) => {
40
+ const code = value?.trim() || ''
41
+ return code ? businessTypeLabels[code] || code : '—'
42
+ }
37
43
  // 审批按钮配置:value 为操作标识,label 为按钮文字,type 为按钮样式。
38
44
  const decisionActions = [
39
45
  { value: 'reject', label: '拒绝', type: 'danger' },
@@ -221,7 +227,7 @@ const tableColumns: ColumnOption[] = [
221
227
  { key: 'sequence', type: 'globalIndex', label: '序号', width: 80, align: 'center' },
222
228
  textColumn('platfSeqNo', '平台流水号', 190),
223
229
  textColumn('id', '任务ID', 180),
224
- textColumn('bizType', '业务类型', 140),
230
+ textColumn('bizType', '业务类型', 140, formatBusinessType),
225
231
  textColumn('collePayerName', '付款人名称', 160),
226
232
  textColumn('payeeName', '收款人名称', 160),
227
233
  {
@@ -254,6 +260,7 @@ const detailValues = computed(() => {
254
260
 
255
261
  return {
256
262
  ...task,
263
+ bizType: formatBusinessType(task.bizType),
257
264
  txnAmt: formatAmount(task.txnAmt),
258
265
  taskSts: formatStatus(task.taskSts),
259
266
  txnTm: formatDateTime(task.txnTm),
@@ -20,7 +20,7 @@ export interface AuthorizationQueryParams extends AuthorizationFilters {
20
20
  export interface AuthorizationTask {
21
21
  id?: string // 任务 ID。
22
22
  platfSeqNo?: string // 平台流水号,用于定位审批任务。
23
- bizType?: string // 业务类型。
23
+ bizType?: string // 业务类型:201 兑回、202 兑出、203 付现;未知代码保留原值。
24
24
  authoOrgName?: string // 授权机构名称。
25
25
  authoOrgNo?: string // 授权机构编号。
26
26
  collePayerName?: string // 付款人名称。
@@ -116,8 +116,8 @@ const calcGapAmount = (inAmt: string, outAmt: string, emptyAsZero = false): stri
116
116
  const payDirecLabels: Record<string, string> = { I: '来账', O: '往账' }
117
117
  /** 支付渠道:预报与流水的数字人民币分别为 8、5;其余渠道保留原文。 */
118
118
  const payChannelLabels: Record<string, string> = { '5': '数字人民币', '8': '数字人民币' }
119
- /** 核销状态字典:1 未核销、2 已核销。 */
120
- const verifyStatusLabels: Record<string, string> = { '1': '未核销', '2': '已核销' }
119
+ /** 与核销页面统一展示核销状态;部分核销记录仍可能出现在匡算明细中。 */
120
+ const verifyStatusLabels: Record<string, string> = { '1': '未核销', '2': '已核销', '3': '部分核销' }
121
121
  /** 业务类型字典:201 兑回、202 兑出、203 付现;与核销预报使用同一套枚举。 */
122
122
  const busiTypeLabels: Record<string, string> = { '201': '兑回', '202': '兑出', '203': '付现' }
123
123
 
@@ -0,0 +1,12 @@
1
+ import baseHttpClient from '@chic/http'
2
+ import type { FlowQueryParams } from './typs'
3
+
4
+ export default {
5
+ /**
6
+ * 分页查询数字人民币实时流水(140504002)。
7
+ * 接口业务 body 为空,不声明日期、姓名、金额或核销状态筛选。
8
+ * @param params 公共分页参数,由请求层放入 head 并转换为字符串。
9
+ */
10
+ queryFlowApi: (params: FlowQueryParams) =>
11
+ baseHttpClient.post<any>('/TAM/0001/140504002', params),
12
+ }
@@ -0,0 +1,249 @@
1
+ <script setup lang="ts">
2
+ import { onBeforeUnmount, onMounted, reactive, ref } from 'vue'
3
+ import { ElMessageBox } from 'element-plus'
4
+ import API from './api'
5
+ import { ArtDataTable, type ColumnOption, type PaginationOptions, type TableSettings } from '@/components/table'
6
+ import type { FlowRow, Tam140504002OutBody } from './typs'
7
+
8
+ defineOptions({ name: 'DcpsFlowQueryPage' })
9
+
10
+ /** 列表查询结果:当前页记录、总记录数和可访问的最后一页。 */
11
+ interface FlowPage {
12
+ data: FlowRow[]
13
+ total: number
14
+ lastPage: number
15
+ }
16
+
17
+ /** 校验 TAM 公共响应头,返回实时流水业务数据。 */
18
+ const ensureSuccess = (response: any): Tam140504002OutBody | null => {
19
+ if (response?.head?.retCode !== '00000') {
20
+ throw new Error(response?.head?.retMsg?.trim() || '实时流水查询失败')
21
+ }
22
+ return response.body
23
+ }
24
+
25
+ /** 计数字段为 integer,明确拒绝字符串、小数、负数和超过安全精度的数字。 */
26
+ const readCount = (value: unknown, label: string): number | undefined => {
27
+ if (value === undefined) return undefined
28
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {
29
+ throw new Error(`查询返回的${label}无效`)
30
+ }
31
+ return value
32
+ }
33
+
34
+ /** 按 txnLists 读取列表,分页数据不完整或相互矛盾时展示明确错误。 */
35
+ const readPage = (body: Tam140504002OutBody | null, pageIndex: number, pageSize: number): FlowPage => {
36
+ if (!body || typeof body !== 'object') throw new Error('查询结果缺少实时流水数据')
37
+ if (body.txnLists !== undefined && !Array.isArray(body.txnLists)) {
38
+ throw new Error('查询返回的实时流水列表格式异常')
39
+ }
40
+ const data = body.txnLists ?? []
41
+ const declaredTotal = readCount(body.totalCount, '总记录数')
42
+ const declaredPages = readCount(body.totalPage, '总页数')
43
+ // 单页且未提供计数时可按实际条数展示,多页结果需要真实总数以保证分页准确。
44
+ if (declaredTotal === undefined && (pageIndex > 1 || (declaredPages !== undefined && declaredPages > 1))) {
45
+ throw new Error('查询结果缺少总记录数,无法读取分页数据')
46
+ }
47
+ const total = declaredTotal ?? data.length
48
+ const pages = Math.ceil(total / pageSize)
49
+ if (total < data.length || (body.txnLists === undefined && total > 0)) {
50
+ throw new Error('查询返回的实时流水列表与总记录数不一致')
51
+ }
52
+ // 无记录时允许服务端返回 0 或 1 页,页面均停留在第 1 页。
53
+ if (declaredPages !== undefined && declaredPages !== pages && !(total === 0 && declaredPages === 1)) {
54
+ throw new Error('查询返回的总页数与总记录数不一致')
55
+ }
56
+ return { data, total, lastPage: Math.max(1, pages) }
57
+ }
58
+
59
+ const getErrorMessage = (error: unknown): string => {
60
+ if (error instanceof Error && error.message.trim()) return error.message
61
+ if (typeof error === 'string' && error.trim()) return error
62
+ return '实时流水查询失败,请稍后重试'
63
+ }
64
+
65
+ /** 仅用字符串处理金额,不做浮点转换或舍入。 */
66
+ const formatAmount = (value?: string): string => {
67
+ const original = value?.trim() || ''
68
+ if (!original) return '—'
69
+ let text = original
70
+ if (text.includes(',')) {
71
+ if (!/^[+-]?\d{1,3}(?:,\d{3})+(?:\.\d*)?$/.test(text)) return original
72
+ text = text.replace(/,/g, '')
73
+ }
74
+ const match = /^([+-]?)(\d*)(?:\.(\d*))?$/.exec(text)
75
+ if (!match || (!match[2] && !match[3])) return original
76
+ const integer = (match[2] || '0').replace(/^0+(?=\d)/, '')
77
+ const fraction = match[3] || ''
78
+ const negative = match[1] === '-' && /[1-9]/.test(integer + fraction)
79
+ return `${negative ? '-' : ''}${integer.replace(/\B(?=(\d{3})+(?!\d))/g, ',')}.${fraction.padEnd(2, '0')}`
80
+ }
81
+
82
+ const flowRows = ref<FlowRow[]>([])
83
+ const flowPagination = reactive({ current: 1, size: 15, total: 0 })
84
+ const flowPaginationOptions: PaginationOptions = { align: 'right', pageSizes: [15, 30, 50, 100], background: false }
85
+ const flowTableSettings = ref<TableSettings>({ size: 'small', stripe: true, border: true, headerBackground: true })
86
+ const flowLoading = ref(false)
87
+ const flowQueryError = ref('')
88
+ let disposed = false
89
+ let flowRequestId = 0 // 仅最新请求可以写入列表;离开页面时使未完成请求失效。
90
+
91
+ /** 所有代码字段保留接口原值,不套用核销接口的方向、业务类型或状态字典。 */
92
+ const flowTextColumn = (prop: keyof FlowRow, label: string, width: number, visible = true): ColumnOption<FlowRow> => ({
93
+ prop, label, width, visible, showOverflowTooltip: true,
94
+ formatter: (row) => row[prop]?.trim() || '—',
95
+ })
96
+
97
+ // 覆盖实时流水的 16 个字段;钱包和机构编号默认在列设置中按需开启。
98
+ const flowColumns: ColumnOption<FlowRow>[] = [
99
+ { type: 'globalIndex', label: '序号', width: 60, align: 'center' },
100
+ flowTextColumn('consumerSeqNo', '平台流水号', 200),
101
+ flowTextColumn('txnTmStamp', '交易时间', 190),
102
+ flowTextColumn('txnDirection', '收支方向', 100),
103
+ flowTextColumn('dbtrNm', '付款人名称', 170),
104
+ flowTextColumn('cdtrNm', '收款人名称', 170),
105
+ { prop: 'txnAmt', label: '交易金额', width: 170, align: 'right', useSlot: true, slotName: 'txnAmount' },
106
+ flowTextColumn('bizType', '业务类型', 100),
107
+ flowTextColumn('txnSts', '交易状态', 100),
108
+ flowTextColumn('payerFinOrgName', '付款机构名称', 170),
109
+ flowTextColumn('payeeFinOrgName', '收款机构名称', 170),
110
+ flowTextColumn('subBrchName', '落地支行名称', 170),
111
+ flowTextColumn('dbtrWltId', '付款钱包 ID', 190, false),
112
+ flowTextColumn('cdtrWltId', '收款钱包 ID', 190, false),
113
+ flowTextColumn('payerFinOrgNo', '付款机构编号', 170, false),
114
+ flowTextColumn('payeeFinOrgNo', '收款机构编号', 170, false),
115
+ flowTextColumn('subBrchBankNo', '落地支行行号', 170, false),
116
+ ]
117
+
118
+ /** 公共 head 承载分页,140504002 的业务 body 不发送旧核销筛选条件。 */
119
+ const loadFlowList = async () => {
120
+ if (disposed) return
121
+ const currentId = ++flowRequestId
122
+ const pageSize = flowPagination.size
123
+ let pageIndex = flowPagination.current
124
+ flowLoading.value = true
125
+ flowQueryError.value = ''
126
+ try {
127
+ for (let attempt = 0; attempt < 2; attempt += 1) {
128
+ const response = await API.queryFlowApi({ pageIndex, pageSize })
129
+ if (disposed || currentId !== flowRequestId) return
130
+ const page = readPage(ensureSuccess(response), pageIndex, pageSize)
131
+ if (pageIndex > page.lastPage) {
132
+ if (attempt === 0) {
133
+ pageIndex = page.lastPage
134
+ continue
135
+ }
136
+ throw new Error('实时流水列表已变化,请重新查询')
137
+ }
138
+ flowPagination.current = pageIndex
139
+ flowPagination.total = page.total
140
+ flowRows.value = page.data
141
+ return
142
+ }
143
+ } catch (error) {
144
+ if (disposed || currentId !== flowRequestId) return
145
+ flowRows.value = []
146
+ flowPagination.total = 0
147
+ flowQueryError.value = getErrorMessage(error)
148
+ } finally {
149
+ if (!disposed && currentId === flowRequestId) flowLoading.value = false
150
+ }
151
+ }
152
+
153
+ /** 查询从第一页开始;工具栏刷新和错误重试保留当前页。 */
154
+ const searchFlow = () => {
155
+ if (disposed || flowLoading.value) return
156
+ flowPagination.current = 1
157
+ void loadFlowList()
158
+ }
159
+
160
+ const realtimeChecking = ref(false)
161
+
162
+ /** 「实时查询流水」仅调用 140504002 确认实时链路;返回成功后弹窗提示,不改动表格与分页。 */
163
+ const checkRealtimeFlow = async () => {
164
+ if (disposed || realtimeChecking.value) return
165
+ realtimeChecking.value = true
166
+ try {
167
+ const response = await API.queryFlowApi({ pageIndex: 1, pageSize: 1 })
168
+ if (disposed) return
169
+ if (response?.head?.retCode !== '00000') {
170
+ throw new Error(response?.head?.retMsg?.trim() || '实时查询流水失败')
171
+ }
172
+ ElMessageBox.alert('操作成功', '提示', { type: 'success', confirmButtonText: '确定' }).catch(() => {})
173
+ } catch (error) {
174
+ if (disposed) return
175
+ ElMessageBox.alert(getErrorMessage(error), '提示', { type: 'error', confirmButtonText: '确定' }).catch(() => {})
176
+ } finally {
177
+ if (!disposed) realtimeChecking.value = false
178
+ }
179
+ }
180
+
181
+ const changeFlowPage = (current: number, size = flowPagination.size) => {
182
+ if (disposed || (current === flowPagination.current && size === flowPagination.size)) return
183
+ if (!Number.isSafeInteger(current) || current < 1 || !Number.isSafeInteger(size) || size < 1) return
184
+ flowPagination.current = current
185
+ flowPagination.size = size
186
+ void loadFlowList()
187
+ }
188
+
189
+ onMounted(() => { void loadFlowList() })
190
+ onBeforeUnmount(() => {
191
+ disposed = true
192
+ flowRequestId += 1
193
+ })
194
+ </script>
195
+
196
+ <template>
197
+ <main class="dcps-page">
198
+ <el-alert v-if="flowQueryError" :title="flowQueryError" type="error" show-icon :closable="false">
199
+ <el-button link type="primary" :disabled="flowLoading" @click="void loadFlowList()">重新查询</el-button>
200
+ </el-alert>
201
+
202
+ <div class="table-region">
203
+ <ArtDataTable
204
+ v-model:settings="flowTableSettings"
205
+ class="flow-table"
206
+ fill
207
+ title="数字人民币实时流水"
208
+ toolbar-layout="refresh,size,fullscreen,columns,settings"
209
+ :data="flowRows"
210
+ :columns="flowColumns"
211
+ :loading="flowLoading"
212
+ :pagination="flowPagination"
213
+ :pagination-options="flowPaginationOptions"
214
+ row-key="consumerSeqNo"
215
+ :empty-text="flowQueryError ? '查询失败,请重新查询' : '暂无实时流水信息'"
216
+ @pagination:current-change="changeFlowPage"
217
+ @pagination:size-change="(size: number) => changeFlowPage(1, size)"
218
+ @refresh="void loadFlowList()"
219
+ >
220
+ <template #toolbar-left>
221
+ <el-button type="primary" :size="flowTableSettings.size" :loading="flowLoading" @click="searchFlow">查询</el-button>
222
+ <el-button type="primary" plain :size="flowTableSettings.size" :loading="realtimeChecking" @click="void checkRealtimeFlow()">实时查询流水</el-button>
223
+ </template>
224
+ <template #txnAmount="{ row }"><span class="amount-cell">{{ formatAmount(row.txnAmt) }}</span></template>
225
+ </ArtDataTable>
226
+ </div>
227
+ </main>
228
+ </template>
229
+
230
+ <style scoped>
231
+ .dcps-page {
232
+ box-sizing: border-box;
233
+ display: flex;
234
+ width: 100%;
235
+ max-width: 100%;
236
+ flex-direction: column;
237
+ gap: 5px;
238
+ height: calc(100vh - var(--dpcs-host-layout-height));
239
+ height: calc(100dvh - var(--dpcs-host-layout-height));
240
+ min-height: 0;
241
+ min-width: 0;
242
+ padding: 5px;
243
+ overflow: hidden;
244
+ background: var(--el-bg-color-page, #f7f8fa);
245
+ }
246
+ .table-region { flex: 1; min-width: 0; min-height: 0; }
247
+ .flow-table { padding: 10px; }
248
+ .amount-cell { color: var(--el-text-color-primary); font-variant-numeric: tabular-nums; }
249
+ </style>
@@ -0,0 +1,38 @@
1
+ /** 数字人民币实时流水查询(140504002)的业务 body 为空。 */
2
+ export type Tam140504002InBody = Record<string, never>
3
+
4
+ /** 平铺的公共分页参数;请求层将其放入 head,业务 body 保持为空。 */
5
+ export interface FlowQueryParams {
6
+ pageIndex: number // 页码,从 1 开始。
7
+ pageSize: number // 每页条数。
8
+ }
9
+
10
+ /** Tam140504002Out.Body:计数字段遵循接口 integer 类型。 */
11
+ export interface Tam140504002OutBody {
12
+ txnLists?: Tam140504002OutBodyTxnLists[] // 当前页的交易流水。
13
+ totalCount?: number // 总记录数。
14
+ totalPage?: number // 总页数。
15
+ }
16
+
17
+ /** 实时流水原始记录;金额、流水号、钱包 ID 和代码均保留字符串。 */
18
+ export interface Tam140504002OutBodyTxnLists {
19
+ bizType?: string // 业务类型,接口未提供枚举。
20
+ cdtrNm?: string // 收款人名称。
21
+ cdtrWltId?: string // 收款钱包 ID。
22
+ consumerSeqNo?: string // 平台流水号。
23
+ dbtrNm?: string // 付款人名称。
24
+ dbtrWltId?: string // 付款钱包 ID。
25
+ payeeFinOrgName?: string // 收款机构名称。
26
+ payeeFinOrgNo?: string // 收款机构编号。
27
+ payerFinOrgName?: string // 付款机构名称。
28
+ payerFinOrgNo?: string // 付款机构编号。
29
+ subBrchBankNo?: string // 落地支行行号。
30
+ subBrchName?: string // 落地支行名称。
31
+ txnAmt?: string // 交易金额。
32
+ txnDirection?: string // 收支方向,接口未提供枚举。
33
+ txnSts?: string // 交易状态,接口未提供枚举。
34
+ txnTmStamp?: string // 交易时间,保留接口原始时间文本。
35
+ }
36
+
37
+ /** 独立实时流水页面的单行记录。 */
38
+ export type FlowRow = Tam140504002OutBodyTxnLists
@@ -12,7 +12,8 @@ export default {
12
12
  queryListApi: (params: unknown) => baseHttpClient.post<any>("/TAM/0001/140501001", params),
13
13
  /**
14
14
  * 新增一条数字人民币头寸预报。
15
- * @param params 新增参数:tranDate 为交易日期,predDate 为预报日期,predAmt 为预报金额;
15
+ * @param params 新增参数:busiType 必填(201 兑回、202 兑出、203 付现);
16
+ * tranDate 为交易日期,predDate 为预报日期,predAmt 为预报金额;
16
17
  * 其余为客户、机构、渠道、用途、备注等业务字段,未填写的字段可不传。
17
18
  */
18
19
  addApi: (params: unknown) => baseHttpClient.post<any>("/TAM/0001/120501001", params),
@@ -40,7 +40,7 @@ defineOptions({ name: 'DcpsForecastPage' })
40
40
 
41
41
  /**
42
42
  * 预报页面:列表、新增/修改、详情共用一个入口文件。
43
- * 新增/修改/详情直接在当前页面以对话框打开,不跳转新页面。
43
+ * 新增、修改和详情在当前入口切换视图,返回列表后保留筛选和分页。
44
44
  */
45
45
 
46
46
  // ---------- 页面内工具函数 ----------
@@ -141,23 +141,24 @@ const createFormValues = (): ForecastFormValues => {
141
141
  billDate: '', billNo: '', busiType: '', clientName: '', clientType: '',
142
142
  competAcct: '', competName: '', custInfoSeq: '', groundBranch: '', groundBranchName: '',
143
143
  linkMan: '', payChannel: '', payDirec: '', phoneNo: '', predAmt: '', predDate: dcpsRuntime.date,
144
- predName: '', predOrgName: '', purpose: '', remark: '', tranDate: dcpsRuntime.date,
144
+ predOrgName: '', purpose: '', remark: '', tranDate: dcpsRuntime.date,
145
145
  }
146
146
  }
147
147
 
148
148
  /**
149
- * 校验表单;新增校验全部必填项,修改只校验已填写的字段。
149
+ * 接口要求新增业务类型必填,同时保留现有预报业务所需的客户、机构、方向、日期与金额校验。
150
150
  * @param values 表单值。
151
151
  * @param mode create 为新增,edit 为修改。
152
152
  * @returns 错误提示文本,空字符串表示校验通过。
153
153
  */
154
154
  const validateForm = (values: ForecastFormValues, mode: 'create' | 'edit' = 'create'): string => {
155
- const requiredFields = [
156
- ['clientName', '客户名称'], ['payDirec', '收支方向'], ['busiType', '业务类型'],
155
+ const businessRequiredFields = [
156
+ ['clientName', '客户名称'], ['payDirec', '收支方向'],
157
157
  ['payChannel', '支付渠道'], ['groundBranch', '落地机构'],
158
158
  ] as const
159
159
  if (mode === 'create') {
160
- for (const [key, label] of requiredFields) {
160
+ if (!values.busiType.trim()) return '请选择业务类型'
161
+ for (const [key, label] of businessRequiredFields) {
161
162
  if (!values[key].trim()) return `请填写${label}`
162
163
  }
163
164
  }
@@ -213,13 +214,15 @@ const listTextFilters = [
213
214
  ['clientName', '客户名称'],
214
215
  ['competName', '交易对手名称'],
215
216
  ['payDirec', '收支方向'],
216
- ['busiType', '业务类型'],
217
217
  ['payChannel', '支付渠道'],
218
218
  ['groundBranch', '落地机构'],
219
219
  ['predStatus', '预报状态'],
220
220
  ['verifyStatus', '核销状态'],
221
221
  ['sortRule', '排序规则'],
222
222
  ] as const
223
+ /** 新接口明确给出的业务类型;未登记代码保留接口原值。 */
224
+ const busiTypeLabels: Record<string, string> = { '201': '兑回', '202': '兑出', '203': '付现' }
225
+ const busiTypeOptions = Object.entries(busiTypeLabels).map(([value, label]) => ({ value, label }))
223
226
  const listRows = ref<ForecastRow[]>([])
224
227
  const listSelectedRows = ref<ForecastRow[]>([])
225
228
  const listPagination = reactive({ current: 1, size: 15, total: 0 })
@@ -233,12 +236,14 @@ const listBusy = computed(() => listMutation.value !== null)
233
236
  const listTableRef = ref<InstanceType<typeof ArtDataTable> | null>(null)
234
237
  let listRequestId = 0
235
238
 
236
- // ---------- 对话框状态 ----------
237
- const editorDialogVisible = ref(false) // 新增/修改对话框是否显示。
238
- const editorDialogMode = ref<'create' | 'edit'>('create') // 当前对话框形态。
239
+ // ---------- 当前视图状态 ----------
240
+ const editorDialogVisible = ref(false) // 新增/修改视图是否显示。
241
+ const editorDialogMode = ref<'create' | 'edit'>('create') // 当前编辑模式。
239
242
  const editorDialogPredRefNo = ref('') // 修改模式的预报流水号。
240
- const detailDialogVisible = ref(false) // 详情对话框是否显示。
243
+ const detailDialogVisible = ref(false) // 详情视图是否显示。
241
244
  const detailDialogPredRefNo = ref('') // 详情模式的预报流水号。
245
+ const isEditor = computed(() => editorDialogVisible.value)
246
+ const isDetail = computed(() => detailDialogVisible.value)
242
247
 
243
248
  const listHasReference = (row: ForecastRow) => Boolean(row.predRefNo?.trim())
244
249
  const listTextColumn = (prop: keyof ForecastRow, label: string, width: number, visible = true): ColumnOption<ForecastRow> => ({
@@ -252,7 +257,7 @@ const listColumns: ColumnOption<ForecastRow>[] = [
252
257
  { ...listTextColumn('tranDate', '交易日期', 110), formatter: (row) => formatDate(row.tranDate) },
253
258
  listTextColumn('clientName', '客户名称', 180),
254
259
  listTextColumn('payDirec', '收支方向', 95),
255
- listTextColumn('busiType', '业务类型', 110),
260
+ { ...listTextColumn('busiType', '业务类型', 110), formatter: (row) => busiTypeLabels[row.busiType ?? ''] ?? row.busiType ?? '—' },
256
261
  listTextColumn('payChannel', '支付渠道', 110),
257
262
  { prop: 'predAmt', label: '预报金额(元)', width: 180, align: 'right', useSlot: true, slotName: 'amount' },
258
263
  { prop: 'verifyAmt', label: '已核销金额(元)', width: 180, align: 'right', useSlot: true, slotName: 'verifiedAmount' },
@@ -339,7 +344,7 @@ const changeListPage = (current: number, size = listPagination.size) => {
339
344
  void loadList()
340
345
  }
341
346
 
342
- // 行内操作交给对话框处理,整行记录用于补齐详情接口没有返回的字段。
347
+ // 行内操作切换当前视图,整行记录用于补齐详情接口没有返回的字段。
343
348
  const openEdit = (row: ForecastRow) => {
344
349
  if (listBusy.value || !listHasReference(row)) return
345
350
  const predRefNo = row.predRefNo?.trim()
@@ -351,7 +356,7 @@ const openEdit = (row: ForecastRow) => {
351
356
  }
352
357
  const openDetail = (row: ForecastRow) => {
353
358
  if (listBusy.value || !listHasReference(row)) return
354
- const predRefNo = row.prefRefNo ?? row.predRefNo?.trim()
359
+ const predRefNo = row.predRefNo?.trim()
355
360
  if (!predRefNo) return
356
361
  rememberForecastRow(row)
357
362
  detailDialogPredRefNo.value = predRefNo
@@ -397,7 +402,7 @@ type FieldKey = Exclude<keyof ForecastFormValues, 'custRefSeq'>
397
402
  type EditorField = {
398
403
  key: FieldKey
399
404
  label: string
400
- kind?: 'date' | 'amount' | 'textarea'
405
+ kind?: 'date' | 'amount' | 'textarea' | 'businessType'
401
406
  placeholder?: string
402
407
  createOnly?: boolean
403
408
  }
@@ -420,7 +425,7 @@ const fieldGroups: { title: string; fields: EditorField[] }[] = [
420
425
  { key: 'predDate', label: '预报日期', kind: 'date' },
421
426
  { key: 'predAmt', label: '预报金额(元)', kind: 'amount', placeholder: '请输入金额,最多两位小数' },
422
427
  { key: 'payDirec', label: '收支方向代码', placeholder: '请输入收支方向代码' },
423
- { key: 'busiType', label: '业务类型代码', placeholder: '请输入业务类型代码' },
428
+ { key: 'busiType', label: '业务类型', kind: 'businessType' },
424
429
  { key: 'payChannel', label: '支付渠道代码', placeholder: '请输入支付渠道代码' },
425
430
  { key: 'clientName', label: '客户名称' },
426
431
  { key: 'clientType', label: '客户类型代码', placeholder: '请输入客户类型代码' },
@@ -441,7 +446,6 @@ const fieldGroups: { title: string; fields: EditorField[] }[] = [
441
446
  {
442
447
  title: '其他信息',
443
448
  fields: [
444
- { key: 'predName', label: '预报人员姓名', createOnly: true },
445
449
  { key: 'predOrgName', label: '预报机构名称', createOnly: true },
446
450
  { key: 'billNo', label: '票号' },
447
451
  { key: 'billDate', label: '出票日期', kind: 'date' },
@@ -454,6 +458,7 @@ const fieldGroups: { title: string; fields: EditorField[] }[] = [
454
458
  const form = reactive<ForecastFormValues>(createFormValues())
455
459
  const formError = ref('')
456
460
  const editorLoading = ref(false) // 读取修改前的原始数据。
461
+ let editorRequestId = 0 // 切换记录或关闭视图后,旧详情请求不再回填表单。
457
462
  const submitting = ref(false) // 提交新增或修改。
458
463
  const loadError = ref('')
459
464
  const initialValues = ref<Partial<ForecastFormValues>>({})
@@ -511,33 +516,55 @@ const buildInitialValues = (detail: ForecastDetail): Partial<ForecastFormValues>
511
516
  return initial
512
517
  }
513
518
 
514
- // 修改模式先读取原始数据,再回填表单;读取失败时停留在页面提示原因。
519
+ // 修改模式先读取原始数据;仅当前视图、当前记录的最新请求可以回填。
515
520
  const loadEditorDetail = async () => {
516
- if (editorMode.value !== 'edit') return
517
- if (!editorPredRefNo.value) {
521
+ if (!editorDialogVisible.value || editorMode.value !== 'edit') return
522
+ const currentId = ++editorRequestId
523
+ const predRefNo = editorPredRefNo.value
524
+ const isCurrentRequest = () => !disposed
525
+ && currentId === editorRequestId
526
+ && editorDialogVisible.value
527
+ && editorMode.value === 'edit'
528
+ && editorPredRefNo.value === predRefNo
529
+ if (!predRefNo) {
530
+ editorLoading.value = false
518
531
  loadError.value = '预报流水号缺失,无法修改'
519
532
  return
520
533
  }
521
534
  editorLoading.value = true
522
535
  loadError.value = ''
523
536
  try {
524
- const cachedRow = recallForecastRow(editorPredRefNo.value)
525
- const detailRow: ForecastRow = { ...cachedRow, predRefNo: editorPredRefNo.value }
526
- initialValues.value = buildInitialValues(await fetchDetail(detailRow))
527
- if (disposed) return
537
+ const cachedRow = recallForecastRow(predRefNo)
538
+ const detailRow: ForecastRow = { ...cachedRow, predRefNo }
539
+ const nextValues = buildInitialValues(await fetchDetail(detailRow))
540
+ if (!isCurrentRequest()) return
541
+ initialValues.value = nextValues
528
542
  initializeForm()
529
543
  } catch (error) {
530
- if (!disposed) loadError.value = getErrorMessage(error)
544
+ if (isCurrentRequest()) loadError.value = getErrorMessage(error)
531
545
  } finally {
532
- if (!disposed) editorLoading.value = false
546
+ if (isCurrentRequest()) editorLoading.value = false
533
547
  }
534
548
  }
535
549
 
536
- // 新增或修改每次打开对话框时重新初始化。
550
+ // 切换状态时同步使旧请求失效;新增表单不受此前修改详情请求影响。
551
+ watch([editorDialogVisible, editorDialogPredRefNo, editorDialogMode], ([visible]) => {
552
+ editorRequestId += 1
553
+ editorLoading.value = false
554
+ if (!visible) return
555
+ initialValues.value = {}
556
+ loadError.value = ''
557
+ initializeForm()
558
+ if (editorMode.value === 'edit') void loadEditorDetail()
559
+ }, { flush: 'sync' })
537
560
 
538
561
  watch(form, () => { formError.value = '' }, { deep: true })
539
562
 
540
- const backToList = () => void router.replace({ name: 'forecast' })
563
+ const backToList = () => {
564
+ editorDialogVisible.value = false
565
+ detailDialogVisible.value = false
566
+ void loadList()
567
+ }
541
568
 
542
569
  // 日期控件清空时可能产生 null,提交前统一回到协议要求的字符串。
543
570
  const submit = async () => {
@@ -555,7 +582,7 @@ const submit = async () => {
555
582
  submitting.value = true
556
583
  try {
557
584
  if (editorMode.value === 'create') {
558
- const body: ForecastCreateBody = {}
585
+ const body: ForecastCreateBody = { busiType: values.busiType }
559
586
  for (const [key, value] of Object.entries(values)) {
560
587
  if (key !== 'custRefSeq' && value?.trim()) Object.assign(body, { [key]: value.trim() })
561
588
  }
@@ -577,7 +604,7 @@ const submit = async () => {
577
604
  }
578
605
  if (disposed) return
579
606
  ElMessage.success(editorMode.value === 'create' ? '新增请求已处理' : '修改请求已处理')
580
- await router.replace({ name: 'forecast' })
607
+ backToList()
581
608
  } catch (error) {
582
609
  if (!disposed) ElMessage.error(getErrorMessage(error))
583
610
  } finally {
@@ -641,7 +668,8 @@ const openEditFromDetail = () => {
641
668
  if (!predRefNo) return
642
669
  const row = recallForecastRow(predRefNo)
643
670
  if (row) rememberForecastRow(row)
644
- void router.push({ name: 'forecast-edit', params: { predRefNo } })
671
+ detailDialogVisible.value = false
672
+ openEdit({ ...row, predRefNo })
645
673
  }
646
674
 
647
675
  const displayValue = (field: DetailField) => {
@@ -649,6 +677,7 @@ const displayValue = (field: DetailField) => {
649
677
  if (value === undefined || value === null || value === '') return '—'
650
678
  if (field.kind === 'amount') return formatAmount(value)
651
679
  if (field.kind === 'date') return formatDate(value)
680
+ if (field.key === 'busiType') return busiTypeLabels[value] ?? value
652
681
  return String(value)
653
682
  }
654
683
 
@@ -667,9 +696,9 @@ const loadDetail = async () => {
667
696
  }
668
697
  }
669
698
 
670
- // 详情页每次进入都按预报流水号读取最新数据(页面实例由 KeepAlive 缓存)。
671
- onActivated(() => {
672
- if (isDetail.value) void loadDetail()
699
+ // 每次进入详情视图都按预报流水号读取最新数据。
700
+ watch([detailDialogVisible, detailDialogPredRefNo], ([visible]) => {
701
+ if (visible) void loadDetail()
673
702
  })
674
703
  </script>
675
704
 
@@ -696,7 +725,7 @@ onActivated(() => {
696
725
  <el-button link type="primary" :disabled="editorDisabled" @click="backToList">返回列表</el-button>
697
726
  </el-alert>
698
727
  <template v-else>
699
- <p class="editor-note">金额单位为元,方向、业务类型、支付渠道和客户类型按业务代码填写。</p>
728
+ <p class="editor-note">金额单位为元;业务类型按接口提供的选项选择,其他未提供字典的字段按业务代码填写。</p>
700
729
  <el-form :model="form" label-position="top" :disabled="editorDisabled" @submit.prevent="submit">
701
730
  <section v-for="group in visibleGroups" :key="group.title" class="editor-group" :aria-label="group.title">
702
731
  <h3>{{ group.title }}</h3>
@@ -722,6 +751,14 @@ onActivated(() => {
722
751
  :editable="false"
723
752
  clearable
724
753
  />
754
+ <el-select
755
+ v-else-if="field.kind === 'businessType'"
756
+ v-model="form.busiType"
757
+ placeholder="请选择业务类型"
758
+ clearable
759
+ >
760
+ <el-option v-for="option in busiTypeOptions" :key="option.value" :label="option.label" :value="option.value" />
761
+ </el-select>
725
762
  <el-input
726
763
  v-else-if="field.kind === 'textarea'"
727
764
  v-model="form[field.key]"
@@ -804,6 +841,11 @@ onActivated(() => {
804
841
  <el-form-item class="filter-date" label="交易日期" required>
805
842
  <el-date-picker v-model="listFilters.tranDate" type="date" format="YYYY-MM-DD" value-format="YYYYMMDD" :clearable="false" :editable="false" />
806
843
  </el-form-item>
844
+ <el-form-item label="业务类型">
845
+ <el-select v-model="listFilters.busiType" clearable placeholder="不限">
846
+ <el-option v-for="option in busiTypeOptions" :key="option.value" :label="option.label" :value="option.value" />
847
+ </el-select>
848
+ </el-form-item>
807
849
  <!-- label 为查询字段名称,输入框较窄,占位文案统一为简短提示。 -->
808
850
  <el-form-item v-for="[key, label] in listTextFilters" :key="key" :label="label">
809
851
  <el-input v-model="listFilters[key]" placeholder="请输入" :maxlength="200" clearable />
@@ -909,7 +951,7 @@ onActivated(() => {
909
951
  .filter-form :deep(.filter-date) { flex: 1.5 0 116px; }
910
952
  .filter-form :deep(.filter-amount) { flex: 1.2 0 92px; }
911
953
  .filter-form :deep(.el-form-item__label) { height: auto; margin-bottom: 4px; padding: 0; font-size: 12px; line-height: 16px; }
912
- .filter-form :deep(.el-date-editor) { width: 100%; min-width: 0; }
954
+ .filter-form :deep(.el-date-editor), .filter-form :deep(.el-select) { width: 100%; min-width: 0; }
913
955
  .filter-actions { flex: 0 0 auto; }
914
956
  .forecast-table-region { flex: 1; min-width: 0; min-height: 0; }
915
957
  .forecast-table { padding: 10px; }
@@ -954,7 +996,7 @@ onActivated(() => {
954
996
  .editor-fields { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); column-gap: 18px; }
955
997
  .editor-fields :deep(.el-form-item) { min-width: 0; margin-bottom: 14px; }
956
998
  .editor-fields :deep(.el-form-item__label) { display: flex; align-items: center; gap: 6px; margin-bottom: 5px; padding: 0; line-height: 20px; }
957
- .editor-fields :deep(.el-date-editor.el-input) { width: 100%; }
999
+ .editor-fields :deep(.el-date-editor.el-input), .editor-fields :deep(.el-select) { width: 100%; }
958
1000
  .editor-field--wide { grid-column: 1 / -1; }
959
1001
  .field-unavailable { color: var(--el-text-color-secondary); font-size: 11px; font-weight: 400; }
960
1002
  .editor-footer { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 8px 12px; padding-top: 10px; border-top: 1px solid var(--el-border-color-lighter); }
@@ -9,8 +9,8 @@ export interface Tam120501001InBody {
9
9
  billDate?: string
10
10
  /** 票号 */
11
11
  billNo?: string
12
- /** 业务类型 */
13
- busiType?: string
12
+ /** 业务类型:201 兑回、202 兑出、203 付现(新增必填)。 */
13
+ busiType: string
14
14
  /** 客户名称 */
15
15
  clientName?: string
16
16
  /** 客户类型 */
@@ -37,8 +37,6 @@ export interface Tam120501001InBody {
37
37
  predAmt?: string
38
38
  /** 预报日期 */
39
39
  predDate?: string
40
- /** 预报人员姓名 */
41
- predName?: string
42
40
  /** 预报机构名称 */
43
41
  predOrgName?: string
44
42
  /** 用途 */
@@ -1,11 +1,13 @@
1
1
  import baseHttpClient from "@chic/http"
2
+ import type { InjectionQueryParams } from './typs'
2
3
 
3
4
  export default {
4
5
  /**
5
6
  * 头寸-数字人民币注资明细查询。
6
7
  * @param params 查询参数:capAdjustType 为调整类型,txnProceSts 为交易处理状态;
8
+ * txnDtStart、txnDtEnd 为交易日期起止范围,格式为 YYYYMMDD,可分别填写。
7
9
  * 空值字段不发送,表示不限制该条件。
8
10
  * 接口未声明分页请求参数,列表在页面内按返回结果本地分页。
9
11
  */
10
- queryListApi: (params: unknown) => baseHttpClient.post<any>("/TAM/0001/140504001", params)
12
+ queryListApi: (params: InjectionQueryParams) => baseHttpClient.post<any>("/TAM/0001/140504001", params)
11
13
  }
@@ -14,6 +14,8 @@ type AmountSortOrder = 'ascending' | 'descending' | null
14
14
  const defaultFilters: InjectionFilters = {
15
15
  capAdjustType: '', // 调整类型。
16
16
  txnProceSts: '', // 交易处理状态。
17
+ txnDtStart: '', // 交易开始日期,不填时不限制。
18
+ txnDtEnd: '', // 交易结束日期,不填时不限制。
17
19
  }
18
20
 
19
21
  // 重复输入项共用一份配置,依次为字段、名称和长度限制。
@@ -241,13 +243,25 @@ const detailValues = computed(() => {
241
243
  }
242
244
  })
243
245
 
246
+ // 校验 YYYYMMDD 日期,避免只检查位数后放过不存在的日期。
247
+ const isValidDate = (value: string): boolean => {
248
+ if (!/^\d{8}$/.test(value)) return false
249
+ const year = Number(value.slice(0, 4))
250
+ const month = Number(value.slice(4, 6))
251
+ const day = Number(value.slice(6, 8))
252
+ if (year < 1 || month < 1 || month > 12 || day < 1) return false
253
+ const leap = year % 400 === 0 || (year % 4 === 0 && year % 100 !== 0)
254
+ const monthDays = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
255
+ return day <= (monthDays[month - 1] || 0)
256
+ }
257
+
244
258
  // 读取表单并组装请求参数;空值字段不发送,表示不限制该条件。
245
259
  const getQueryParams = (): InjectionQueryParams => {
246
- const params: InjectionQueryParams = { ...defaultFilters }
247
- const capAdjustType = filters.value.capAdjustType.trim() // 去除首尾空白后的调整类型。
248
- const txnProceSts = filters.value.txnProceSts.trim() // 去除首尾空白后的交易处理状态。
249
- if (capAdjustType) params.capAdjustType = capAdjustType
250
- if (txnProceSts) params.txnProceSts = txnProceSts
260
+ const params: InjectionQueryParams = {}
261
+ for (const key of Object.keys(defaultFilters) as (keyof InjectionFilters)[]) {
262
+ const value = filters.value[key]?.trim()
263
+ if (value) params[key] = value
264
+ }
251
265
  return params
252
266
  }
253
267
  // 仅查询、重置时更新;翻页和刷新沿用这份条件。
@@ -261,7 +275,7 @@ const loadList = async () => {
261
275
  try {
262
276
  // head 为公共响应头,body 为包含明细列表的业务数据;
263
277
  // body 内的 totalCount、totalPage 供服务端分页时使用,当前接按返回的完整列表本地分页。
264
- const { head, body } = await API.queryListApi(appliedQuery) as any
278
+ const { head, body } = await API.queryListApi(appliedQuery)
265
279
  if (currentRequestId !== requestId) return
266
280
  if (head?.retCode !== '00000') {
267
281
  throw new Error(head?.retMsg || '查询失败')
@@ -285,7 +299,13 @@ const loadList = async () => {
285
299
 
286
300
  // 新查询从第一页开始;重置后复用同一查询流程。
287
301
  const search = () => {
288
- appliedQuery = getQueryParams()
302
+ const query = getQueryParams()
303
+ if (query.txnDtStart && !isValidDate(query.txnDtStart)) return void ElMessage.warning('请选择有效的交易开始日期')
304
+ if (query.txnDtEnd && !isValidDate(query.txnDtEnd)) return void ElMessage.warning('请选择有效的交易结束日期')
305
+ if (query.txnDtStart && query.txnDtEnd && query.txnDtStart > query.txnDtEnd) {
306
+ return void ElMessage.warning('交易开始日期不能晚于结束日期')
307
+ }
308
+ appliedQuery = query
289
309
  pagination.current = 1
290
310
  void loadList()
291
311
  }
@@ -323,6 +343,12 @@ onBeforeUnmount(() => {
323
343
  <!-- 查询条件:控件尺寸跟随表格设置,卡片内边距为 10px,点击查询生效。 -->
324
344
  <el-card v-show="showSearchBar" class="injection-search" shadow="never" :body-style="{ padding: '10px' }">
325
345
  <el-form class="filter-form" :model="filters" :size="tableSettings.size" label-position="top" @submit.prevent="search()">
346
+ <el-form-item class="filter-date" label="交易开始日期">
347
+ <el-date-picker v-model="filters.txnDtStart" type="date" format="YYYY-MM-DD" value-format="YYYYMMDD" placeholder="不限" :editable="false" clearable />
348
+ </el-form-item>
349
+ <el-form-item class="filter-date" label="交易结束日期">
350
+ <el-date-picker v-model="filters.txnDtEnd" type="date" format="YYYY-MM-DD" value-format="YYYYMMDD" placeholder="不限" :editable="false" clearable />
351
+ </el-form-item>
326
352
  <!-- prop 为查询字段,label 为字段名称,maxlength 为允许输入的最大字符数。 -->
327
353
  <el-form-item
328
354
  v-for="[prop, label, maxlength] in filterFields"
@@ -448,7 +474,9 @@ onBeforeUnmount(() => {
448
474
  .filter-form { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 10px 12px; }
449
475
  /* 筛选项较窄时换行,宽屏时封顶 280px,输入框不至于无限拉长。 */
450
476
  .filter-form :deep(.el-form-item) { flex: 1 0 105px; max-width: 280px; min-width: 0; margin: 0; }
477
+ .filter-form :deep(.filter-date) { flex-basis: 160px; }
451
478
  .filter-form :deep(.el-form-item__label) { margin-bottom: 4px; }
479
+ .filter-form :deep(.el-date-editor) { width: 100%; min-width: 0; }
452
480
  .filter-actions { flex-shrink: 0; margin-left: auto; }
453
481
  .amount-cell { color: var(--el-color-primary-dark-2); font-variant-numeric: tabular-nums; font-weight: 600; }
454
482
 
@@ -2,11 +2,12 @@
2
2
  export interface InjectionFilters {
3
3
  capAdjustType: string // 调整类型。
4
4
  txnProceSts: string // 交易处理状态。
5
+ txnDtStart: string // 交易开始日期,格式为 YYYYMMDD。
6
+ txnDtEnd: string // 交易结束日期,格式为 YYYYMMDD。
5
7
  }
6
8
 
7
9
  /** 列表查询参数;空字段在提交前剔除,表示不限制。 */
8
- export interface InjectionQueryParams extends InjectionFilters {
9
- }
10
+ export type InjectionQueryParams = Partial<InjectionFilters>
10
11
 
11
12
  /** 接口返回的数字人民币注资明细字段。 */
12
13
  export interface InjectionRecord {
@@ -23,14 +23,15 @@ export default {
23
23
  * 分页查询核销预报信息。
24
24
  * @param params 查询参数:tranDate 为交易日期(必填,YYYYMMDD);pageIndex、pageSize 为页码和每页条数。
25
25
  * 业务筛选:acctNo 为我方账号,banchPre 为机构关键字,busiType 为业务类型,
26
- * clientName 为客户名称,competName 为交易对手名称,payDirec 为收支方向,
27
- * startTransAmt、endTransAmt 为金额范围,verifyStatusList 为核销状态列表。
26
+ * clientName 为客户名称,competName 为交易对手名称,payDirec 为收支方向(1 收入、2 支出),
27
+ * startTransAmt、endTransAmt 为金额范围,verifyStatusList 待核销传 [1,3]、已核销传 [2]。
28
28
  */
29
29
  queryForecastApi: (params: unknown) => baseHttpClient.post<any>("/TAM/0001/140502001", params),
30
30
  /**
31
31
  * 分页查询核销流水信息。
32
32
  * @param params 查询参数:tranDate 为交易日期(必填,YYYYMMDD);pageIndex、pageSize 为页码和每页条数。
33
- * 业务筛选:orgName 为我方机构,payerName、payeeName 为付款人和收款人名称,
33
+ * 业务筛选:busiType 为业务类型(201 兑回、202 兑出、203 付现),orgName 为我方机构,
34
+ * payerName、payeeName 为付款人和收款人名称,
34
35
  * payDirec 为收支方向,startTransAmt、endTransAmt 为金额范围,verifyStatus 为核销状态。
35
36
  */
36
37
  queryFlowApi: (params: unknown) => baseHttpClient.post<any>("/TAM/0001/140502002", params)
@@ -123,12 +123,14 @@ const compareAmounts = (left: string, right: string): number => {
123
123
 
124
124
  /** 业务类型字典:201 兑回、202 兑出、203 付现;超出字典的值保留原文。 */
125
125
  const busiTypeLabels: Record<string, string> = { '201': '兑回', '202': '兑出', '203': '付现' }
126
- /** 收支方向字典:I 来账、O 往账。 */
127
- const payDirecLabels: Record<string, string> = { I: '来账', O: '往账' }
126
+ /** 核销预报使用 1 收入、2 支出;流水接口仍使用 I 来账、O 往账。 */
127
+ const forecastPayDirecLabels: Record<string, string> = { '1': '收入', '2': '支出' }
128
+ const flowPayDirecLabels: Record<string, string> = { I: '来账', O: '往账' }
128
129
  /** 支付渠道:核销预报为 8、流水为 5,均表示数字人民币;其余渠道保留原文。 */
129
130
  const payChannelLabels: Record<string, string> = { '5': '数字人民币', '8': '数字人民币' }
130
- /** 核销状态字典:1 未核销、2 已核销。 */
131
- const verifyStatusLabels: Record<string, string> = { '1': '未核销', '2': '已核销' }
131
+ /** 响应核销状态字典;流水筛选仍按请求协议仅提供 1、2。 */
132
+ const verifyStatusLabels: Record<string, string> = { '1': '未核销', '2': '已核销', '3': '部分核销' }
133
+ const tranStatusLabels: Record<string, string> = { S: '成功', P: '处理中', F: '失败' }
132
134
 
133
135
  /** 读取字典文案;空值显示横线,未登记的代码保留原文展示。 */
134
136
  const dictLabel = (labels: Record<string, string>, value?: string): string => {
@@ -171,6 +173,9 @@ const openWriteOff = () => {
171
173
  if (busy.value) return
172
174
  const predRows = predSelection.value.filter((row) => row.predRefNo?.trim())
173
175
  if (!predRows.length) return void ElMessage.warning('请先勾选带预报流水号的预报记录')
176
+ const selected = [...predRows, ...flowSelection.value]
177
+ if (selected.some(row => row.verifyStatus === '3')) return void ElMessage.warning('部分核销记录暂不支持继续核销')
178
+ if (selected.some(row => row.verifyStatus !== '1')) return void ElMessage.warning('请仅勾选未核销记录')
174
179
  writeOffPredItems.value = predRows.map((row, index) => ({
175
180
  // 预报流水号可能重复,追加下标保证弹窗表格的渲染键唯一。
176
181
  key: `${row.predRefNo?.trim()}-${index}`,
@@ -239,6 +244,9 @@ const cancelWriteOff = async () => {
239
244
  const predRows = predSelection.value.filter((row) => row.predRefNo?.trim())
240
245
  const flowRows = flowSelection.value.filter(hasRefNo)
241
246
  if (!predRows.length && !flowRows.length) return void ElMessage.warning('请先勾选要撤销核销的预报或流水')
247
+ const selected = [...predRows, ...flowRows]
248
+ if (selected.some(row => row.verifyStatus === '3')) return void ElMessage.warning('部分核销记录暂不支持撤销核销')
249
+ if (selected.some(row => row.verifyStatus !== '2')) return void ElMessage.warning('请仅勾选已核销记录')
242
250
  const summaryParts: string[] = []
243
251
  if (predRows.length) summaryParts.push(`预报 ${predRows.length} 条`)
244
252
  if (flowRows.length) summaryParts.push(`流水 ${flowRows.length} 条`)
@@ -300,7 +308,7 @@ type ForecastAppliedQuery = {
300
308
  busiType?: string // 业务类型:201 兑回、202 兑出、203 付现。
301
309
  clientName?: string // 客户名称。
302
310
  competName?: string // 交易对手名称。
303
- payDirec?: string // 收支方向:I 来账、O 往账。
311
+ payDirec?: string // 收支方向:1 收入、2 支出。
304
312
  startTransAmt?: string // 起始金额。
305
313
  endTransAmt?: string // 截止金额。
306
314
  verifyStatusList?: string[] // 核销状态列表。
@@ -317,7 +325,7 @@ const createForecastFilters = () => ({
317
325
  payDirec: '',
318
326
  startTransAmt: '',
319
327
  endTransAmt: '',
320
- verifyStatusList: [] as string[],
328
+ verifyStatusGroup: '', // 待核销包含未核销和部分核销两种状态。
321
329
  })
322
330
  const forecastFilters = ref(createForecastFilters())
323
331
  let forecastAppliedQuery: ForecastAppliedQuery = { tranDate: dcpsRuntime.date }
@@ -331,8 +339,10 @@ const forecastTextFilters = [
331
339
  ] as const
332
340
  // 已在接口附件声明的枚举,直接生成下拉选项;缺省字典的状态不猜测。
333
341
  const busiTypeOptions = Object.entries(busiTypeLabels).map(([value, label]) => ({ value, label }))
334
- const payDirecOptions = Object.entries(payDirecLabels).map(([value, label]) => ({ value, label }))
335
- const verifyStatusOptions = Object.entries(verifyStatusLabels).map(([value, label]) => ({ value, label }))
342
+ const forecastPayDirecOptions = Object.entries(forecastPayDirecLabels).map(([value, label]) => ({ value, label }))
343
+ const flowPayDirecOptions = Object.entries(flowPayDirecLabels).map(([value, label]) => ({ value, label }))
344
+ const flowVerifyStatusOptions = ['1', '2'].map(value => ({ value, label: verifyStatusLabels[value] }))
345
+ const forecastVerifyStatusOptions = [{ value: 'pending', label: '待核销' }, { value: 'verified', label: '已核销' }]
336
346
 
337
347
  // 列表、分页与请求状态;请求序号防止旧响应覆盖新结果。
338
348
  const forecastRows = ref<ForecastRow[]>([])
@@ -372,7 +382,8 @@ const forecastColumns: ColumnOption<ForecastRow>[] = [
372
382
  forecastDictColumn('busiType', '业务类型', 95, busiTypeLabels),
373
383
  forecastTextColumn('clientName', '客户名称', 170),
374
384
  forecastTextColumn('competName', '交易对手名称', 170),
375
- forecastDictColumn('payDirec', '收支方向', 90, payDirecLabels),
385
+ forecastTextColumn('competAcct', '交易对手账号', 190),
386
+ forecastDictColumn('payDirec', '收支方向', 90, forecastPayDirecLabels),
376
387
  forecastDictColumn('payChannel', '支付渠道', 110, payChannelLabels),
377
388
  { prop: 'predAmt', label: '预报金额(元)', width: 160, align: 'right', useSlot: true, slotName: 'predAmount' },
378
389
  { prop: 'verifyAmt', label: '已核销金额(元)', width: 160, align: 'right', useSlot: true, slotName: 'verifyAmount' },
@@ -445,7 +456,8 @@ const searchForecast = () => {
445
456
  if (source.payDirec.trim()) query.payDirec = source.payDirec.trim()
446
457
  if (startTransAmt) query.startTransAmt = startTransAmt
447
458
  if (endTransAmt) query.endTransAmt = endTransAmt
448
- if (source.verifyStatusList.length) query.verifyStatusList = [...source.verifyStatusList]
459
+ if (source.verifyStatusGroup === 'pending') query.verifyStatusList = ['1', '3']
460
+ if (source.verifyStatusGroup === 'verified') query.verifyStatusList = ['2']
449
461
  forecastAppliedQuery = query
450
462
  forecastPagination.current = 1
451
463
  void loadForecastList()
@@ -468,6 +480,8 @@ const changeForecastPage = (current: number, size = forecastPagination.size) =>
468
480
  */
469
481
  const openEdit = (row: ForecastRow) => {
470
482
  if (busy.value || !forecastHasReference(row)) return
483
+ if (row.verifyStatus === '3') return void ElMessage.warning('部分核销记录暂不支持修改核销金额')
484
+ if (row.verifyStatus !== '1') return void ElMessage.warning('请先撤销核销后再修改核销金额')
471
485
  editForm.value = {
472
486
  predRefNo: row.predRefNo?.trim() || '',
473
487
  predAmt: row.predAmt?.trim() || '',
@@ -507,6 +521,7 @@ const submitEdit = async () => {
507
521
  type FlowAppliedQuery = {
508
522
  tranDate: string // 必填交易日期,格式为 YYYYMMDD。
509
523
  orgName?: string // 我方机构。
524
+ busiType?: string // 业务类型:201 兑回、202 兑出、203 付现。
510
525
  payerName?: string // 付款人名称。
511
526
  payeeName?: string // 收款人名称。
512
527
  payDirec?: string // 收支方向:I 来账、O 往账。
@@ -519,6 +534,7 @@ type FlowAppliedQuery = {
519
534
  const createFlowFilters = () => ({
520
535
  tranDate: dcpsRuntime.date,
521
536
  orgName: '',
537
+ busiType: '',
522
538
  payerName: '',
523
539
  payeeName: '',
524
540
  payDirec: '',
@@ -571,13 +587,13 @@ const flowColumns: ColumnOption<FlowRow>[] = [
571
587
  flowDictColumn('busiType', '业务类型', 95, busiTypeLabels),
572
588
  flowTextColumn('payerName', '付款人名称', 170),
573
589
  flowTextColumn('payeeName', '收款人名称', 170),
574
- flowDictColumn('payDirec', '收支方向', 90, payDirecLabels),
590
+ flowDictColumn('payDirec', '收支方向', 90, flowPayDirecLabels),
575
591
  { prop: 'tranAmt', label: '交易金额(元)', width: 160, align: 'right', useSlot: true, slotName: 'tranAmount' },
576
592
  flowDictColumn('verifyStatus', '核销状态', 95, verifyStatusLabels),
577
593
  flowTextColumn('branchName', '我方机构', 150),
578
594
  flowTextColumn('collBankName', '收款行名称', 160),
579
595
  flowTextColumn('payBankName', '付款行名称', 160),
580
- flowTextColumn('busiStatus', '业务状态', 110),
596
+ flowDictColumn('tranStatus', '交易状态', 110, tranStatusLabels),
581
597
  flowTextColumn('remark', '备注', 160),
582
598
  flowTextColumn('branchCode', '机构行号', 150, false),
583
599
  flowTextColumn('collBankNo', '收款行行号', 170, false),
@@ -643,6 +659,7 @@ const searchFlow = () => {
643
659
  const value = source[key].trim()
644
660
  if (value) query[key] = value
645
661
  }
662
+ if (source.busiType.trim()) query.busiType = source.busiType.trim()
646
663
  if (source.payDirec.trim()) query.payDirec = source.payDirec.trim()
647
664
  if (source.verifyStatus.trim()) query.verifyStatus = source.verifyStatus.trim()
648
665
  if (startTransAmt) query.startTransAmt = startTransAmt
@@ -662,6 +679,11 @@ const changeFlowPage = (current: number, size = flowPagination.size) => {
662
679
  void loadFlowList()
663
680
  }
664
681
 
682
+ /** 未定义部分核销的继续处理契约,操作仅允许原协议的未核销/已核销记录。 */
683
+ const canWriteOff = computed(() => predSelection.value.length > 0 && [...predSelection.value, ...flowSelection.value].every(row => row.verifyStatus === '1'))
684
+ const canCancelWriteOff = computed(() => (predSelection.value.length + flowSelection.value.length) > 0 && [...predSelection.value, ...flowSelection.value].every(row => row.verifyStatus === '2'))
685
+ const hasPartialRows = computed(() => [...forecastRows.value, ...flowRows.value].some(row => row.verifyStatus === '3'))
686
+
665
687
  /** 核销、撤销核销成功后统一刷新两个列表;勾选在各自的刷新流程中清空。 */
666
688
  const refreshBoth = async () => {
667
689
  await Promise.all([loadForecastList(), loadFlowList()])
@@ -683,6 +705,7 @@ onBeforeUnmount(() => {
683
705
  <template>
684
706
  <ElConfigProvider :size="forecastTableSettings.size">
685
707
  <main class="dcps-page">
708
+ <el-alert v-if="hasPartialRows" title="部分核销记录可查询,暂不支持继续核销、修改或撤销操作。" type="info" :closable="false" show-icon />
686
709
  <!-- 上方:预报信息表格,核销、撤销核销按钮统一放在本表格工具栏。 -->
687
710
  <section class="view-region">
688
711
  <!-- 查询条件:控件尺寸跟随表格设置,卡片内边距为 10px,点击查询生效。 -->
@@ -692,8 +715,8 @@ onBeforeUnmount(() => {
692
715
  <el-date-picker v-model="forecastFilters.tranDate" type="date" format="YYYY-MM-DD" value-format="YYYYMMDD" :clearable="false" :editable="false" />
693
716
  </el-form-item>
694
717
  <el-form-item class="filter-status" label="核销状态">
695
- <el-select v-model="forecastFilters.verifyStatusList" multiple collapse-tags collapse-tags-tooltip clearable placeholder="不限">
696
- <el-option v-for="option in verifyStatusOptions" :key="option.value" :label="option.label" :value="option.value" />
718
+ <el-select v-model="forecastFilters.verifyStatusGroup" clearable placeholder="不限">
719
+ <el-option v-for="option in forecastVerifyStatusOptions" :key="option.value" :label="option.label" :value="option.value" />
697
720
  </el-select>
698
721
  </el-form-item>
699
722
  <el-form-item class="filter-status" label="业务类型">
@@ -703,7 +726,7 @@ onBeforeUnmount(() => {
703
726
  </el-form-item>
704
727
  <el-form-item class="filter-status" label="收支方向">
705
728
  <el-select v-model="forecastFilters.payDirec" clearable placeholder="不限">
706
- <el-option v-for="option in payDirecOptions" :key="option.value" :label="option.label" :value="option.value" />
729
+ <el-option v-for="option in forecastPayDirecOptions" :key="option.value" :label="option.label" :value="option.value" />
707
730
  </el-select>
708
731
  </el-form-item>
709
732
  <!-- prop 为查询字段,label 为字段名称,maxlength 为允许输入的最大字符数。 -->
@@ -754,13 +777,15 @@ onBeforeUnmount(() => {
754
777
  <!-- 核销必选预报,单独勾选流水时按钮保持禁用;按钮文案带上两个表格的勾选总数。 -->
755
778
  <el-button
756
779
  type="primary"
757
- :disabled="busy || !predSelection.length"
780
+ :disabled="busy || !canWriteOff"
781
+ title="仅支持未核销记录"
758
782
  @click="openWriteOff"
759
783
  >核销({{ predSelection.length + flowSelection.length }})</el-button>
760
784
  <el-button
761
785
  type="warning"
762
786
  plain
763
- :disabled="busy || !(predSelection.length || flowSelection.length)"
787
+ :disabled="busy || !canCancelWriteOff"
788
+ title="仅支持已核销记录"
764
789
  @click="cancelWriteOff"
765
790
  >撤销核销({{ predSelection.length + flowSelection.length }})</el-button>
766
791
  </div>
@@ -770,7 +795,7 @@ onBeforeUnmount(() => {
770
795
  <template #verifyAmount="{ row }"><span class="amount-cell">{{ formatAmount(row.verifyAmt) }}</span></template>
771
796
  <template #actions="{ row }">
772
797
  <div class="row-actions" @dblclick.stop>
773
- <el-button link type="primary" :disabled="busy || !forecastHasReference(row)" @click.stop="openEdit(row)">修改</el-button>
798
+ <el-button link type="primary" :disabled="busy || !forecastHasReference(row) || row.verifyStatus !== '1'" title="仅未核销记录可修改核销金额" @click.stop="openEdit(row)">修改</el-button>
774
799
  </div>
775
800
  </template>
776
801
  </ArtDataTable>
@@ -824,12 +849,17 @@ onBeforeUnmount(() => {
824
849
  </el-form-item>
825
850
  <el-form-item class="filter-status" label="核销状态">
826
851
  <el-select v-model="flowFilters.verifyStatus" clearable placeholder="不限">
827
- <el-option v-for="option in verifyStatusOptions" :key="option.value" :label="option.label" :value="option.value" />
852
+ <el-option v-for="option in flowVerifyStatusOptions" :key="option.value" :label="option.label" :value="option.value" />
853
+ </el-select>
854
+ </el-form-item>
855
+ <el-form-item class="filter-status" label="业务类型">
856
+ <el-select v-model="flowFilters.busiType" clearable placeholder="不限">
857
+ <el-option v-for="option in busiTypeOptions" :key="option.value" :label="option.label" :value="option.value" />
828
858
  </el-select>
829
859
  </el-form-item>
830
860
  <el-form-item class="filter-status" label="收支方向">
831
861
  <el-select v-model="flowFilters.payDirec" clearable placeholder="不限">
832
- <el-option v-for="option in payDirecOptions" :key="option.value" :label="option.label" :value="option.value" />
862
+ <el-option v-for="option in flowPayDirecOptions" :key="option.value" :label="option.label" :value="option.value" />
833
863
  </el-select>
834
864
  </el-form-item>
835
865
  <!-- prop 为查询字段,label 为字段名称,maxlength 为允许输入的最大字符数。 -->
@@ -93,13 +93,13 @@ export interface Tam140502001InBody {
93
93
  endTransAmt?: string
94
94
  /** 支付渠道,8:数字人民币 */
95
95
  payChannel?: string
96
- /** 收支方向,I=来账 O=往账 */
96
+ /** 收支方向:1 收入、2 支出。 */
97
97
  payDirec?: string
98
98
  /** 起始金额 */
99
99
  startTransAmt?: string
100
100
  /** 交易日期 */
101
101
  tranDate: string
102
- /** 核销状态列表 */
102
+ /** 核销状态列表:待核销 [1, 3]、已核销 [2]。 */
103
103
  verifyStatusList?: Array<string>
104
104
  }
105
105
 
@@ -117,6 +117,8 @@ export interface Tam140502001OutBodyResultList {
117
117
  busiType?: string
118
118
  /** 客户名称 */
119
119
  clientName?: string
120
+ /** 交易对手账号 */
121
+ competAcct?: string
120
122
  /** 交易对手名称 */
121
123
  competName?: string
122
124
  /** 释放状态 */
@@ -125,7 +127,7 @@ export interface Tam140502001OutBodyResultList {
125
127
  groundBranchName?: string
126
128
  /** 支付渠道,8:数字人民币 */
127
129
  payChannel?: string
128
- /** 收支方向 */
130
+ /** 收支方向:1 收入、2 支出。 */
129
131
  payDirec?: string
130
132
  /** 预报金额 */
131
133
  predAmt?: string
@@ -143,6 +145,8 @@ export interface Tam140502001OutBodyResultList {
143
145
 
144
146
  /** Tam140502002In.Body */
145
147
  export interface Tam140502002InBody {
148
+ /** 业务类型:201 兑回、202 兑出、203 付现。 */
149
+ busiType?: string
146
150
  /** 截止金额 */
147
151
  endTransAmt?: string
148
152
  /** 我方机构 */
@@ -177,8 +181,8 @@ export interface Tam140502002OutBodyResultList {
177
181
  branchCode?: string
178
182
  /** 机构名称 */
179
183
  branchName?: string
180
- /** 业务状态 */
181
- busiStatus?: string
184
+ /** 交易状态:S 成功、P 处理中、F 失败。 */
185
+ tranStatus?: string
182
186
  /** 业务类型,201:兑回 ,202:兑出,203:付现 */
183
187
  busiType?: string
184
188
  /** 收款行行名称 */
@@ -210,7 +214,7 @@ export interface Tam140502002OutBodyResultList {
210
214
  tranDate?: string
211
215
  /** 交易时间 */
212
216
  tranTime?: string
213
- /** 核销状态 */
217
+ /** 核销状态:1 未核销、2 已核销、3 部分核销。 */
214
218
  verifyStatus?: string
215
219
  }
216
220