vue3-element-plus-app-chu 0.0.19 → 0.0.21
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 -0
- package/package.json +1 -1
- package/src/components/table/ArtTableHeader.vue +29 -6
- package/src/router/index.ts +7 -0
- package/src/router/menus.ts +1 -0
- package/src/view/dcps/README.md +3 -1
- package/src/view/dcps/auth/index.vue +13 -13
- package/src/view/dcps/city-clearing/parameters/api.ts +36 -0
- package/src/view/dcps/city-clearing/parameters/typs.ts +82 -0
- package/src/view/dcps/estimation/index.vue +9 -5
- package/src/view/dcps/forecast/index.vue +13 -11
- package/src/view/dcps/funding/balance/api.ts +3 -3
- package/src/view/dcps/funding/balance/index.vue +2 -2
- package/src/view/dcps/funding/injection/index.vue +3 -1
- package/src/view/dcps/parameters/api.ts +26 -0
- package/src/view/dcps/parameters/index.vue +506 -0
- package/src/view/dcps/parameters/typs.ts +18 -0
- package/src/view/dcps/runtime.ts +11 -30
- package/src/view/dcps/write-off/index.vue +17 -10
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
|
|
3
|
+
import { ElConfigProvider, ElMessage } from 'element-plus'
|
|
4
|
+
import { ArtDataTable, type ColumnOption, type PaginationOptions, type TableSettings } from '@/components/table'
|
|
5
|
+
import type { ApiResponse } from '@/utils/request'
|
|
6
|
+
import API from './api'
|
|
7
|
+
import type { DigitalRmbParameter, ParameterFormValues, ParameterListBody } from './typs'
|
|
8
|
+
|
|
9
|
+
defineOptions({ name: 'DcpsParametersPage' })
|
|
10
|
+
|
|
11
|
+
type DialogMode = 'create' | 'edit' | 'detail'
|
|
12
|
+
type Mutation = 'create' | 'edit' | 'delete' | null
|
|
13
|
+
|
|
14
|
+
const parameterFields = [
|
|
15
|
+
{ key: 'paraName', label: '参数名称', multiline: false },
|
|
16
|
+
{ key: 'paraType', label: '参数类型', multiline: false },
|
|
17
|
+
{ key: 'paraMark', label: '参数标志', multiline: false },
|
|
18
|
+
{ key: 'paraValue', label: '参数值', multiline: true },
|
|
19
|
+
{ key: 'paraDesc', label: '参数描述', multiline: true },
|
|
20
|
+
{ key: 'remark', label: '备注', multiline: true },
|
|
21
|
+
] as const
|
|
22
|
+
|
|
23
|
+
const createFormValues = (parameter: DigitalRmbParameter = {}): ParameterFormValues => ({
|
|
24
|
+
paraName: parameter.paraName ?? '',
|
|
25
|
+
paraType: parameter.paraType ?? '',
|
|
26
|
+
paraMark: parameter.paraMark ?? '',
|
|
27
|
+
paraValue: parameter.paraValue ?? '',
|
|
28
|
+
paraDesc: parameter.paraDesc ?? '',
|
|
29
|
+
remark: parameter.remark ?? '',
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const errorMessage = (error: unknown): string =>
|
|
33
|
+
error instanceof Error && error.message.trim() ? error.message : '请求失败,请稍后重试'
|
|
34
|
+
|
|
35
|
+
const ensureSuccess = <T,>(response: ApiResponse<T>, fallback: string): T | null => {
|
|
36
|
+
if (response?.head?.retCode !== '00000') {
|
|
37
|
+
throw new Error(response?.head?.retMsg?.trim() || fallback)
|
|
38
|
+
}
|
|
39
|
+
return response.body
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 只读取契约中的字符串字段,参数值不转换成数字,也不去掉首尾空格。 */
|
|
43
|
+
const readParameter = (value: unknown): DigitalRmbParameter => {
|
|
44
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
45
|
+
throw new Error('服务端返回的参数格式异常')
|
|
46
|
+
}
|
|
47
|
+
const source = value as Record<string, unknown>
|
|
48
|
+
const result: DigitalRmbParameter = {}
|
|
49
|
+
for (const { key } of parameterFields) {
|
|
50
|
+
const field = source[key]
|
|
51
|
+
if (field === undefined) continue
|
|
52
|
+
if (typeof field !== 'string') throw new Error('服务端返回的参数字段格式异常')
|
|
53
|
+
result[key] = field
|
|
54
|
+
}
|
|
55
|
+
return result
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const hasName = (parameter: DigitalRmbParameter): boolean => Boolean(parameter.paraName?.trim())
|
|
59
|
+
const displayValue = (value?: string): string => value === undefined || value === '' ? '—' : value
|
|
60
|
+
|
|
61
|
+
const keywords = ref('')
|
|
62
|
+
let appliedKeywords = ''
|
|
63
|
+
const rows = ref<DigitalRmbParameter[]>([])
|
|
64
|
+
const selectedRows = ref<DigitalRmbParameter[]>([])
|
|
65
|
+
const pagination = reactive({ current: 1, size: 15, total: 0 })
|
|
66
|
+
const paginationOptions: PaginationOptions = { align: 'right', pageSizes: [15, 30, 50, 100], background: false }
|
|
67
|
+
const tableSettings = ref<TableSettings>({ size: 'small', stripe: true, border: true, headerBackground: true })
|
|
68
|
+
const tableRef = ref<InstanceType<typeof ArtDataTable> | null>(null)
|
|
69
|
+
const showSearchBar = ref(true)
|
|
70
|
+
const listLoading = ref(false)
|
|
71
|
+
const listError = ref('')
|
|
72
|
+
const pendingMutation = ref<Mutation>(null)
|
|
73
|
+
const writing = computed(() => pendingMutation.value !== null)
|
|
74
|
+
const listBusy = computed(() => listLoading.value || writing.value)
|
|
75
|
+
let listRequestId = 0
|
|
76
|
+
let detailRequestId = 0
|
|
77
|
+
let disposed = false
|
|
78
|
+
|
|
79
|
+
const textColumn = (prop: keyof DigitalRmbParameter, label: string, minWidth: number): ColumnOption<DigitalRmbParameter> => ({
|
|
80
|
+
prop, label, minWidth, showOverflowTooltip: true,
|
|
81
|
+
formatter: (row) => displayValue(row[prop]),
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const tableColumns: ColumnOption<DigitalRmbParameter>[] = [
|
|
85
|
+
{ type: 'selection', width: 42, fixed: 'left', selectable: hasName },
|
|
86
|
+
{ type: 'globalIndex', label: '序号', width: 60, align: 'center' },
|
|
87
|
+
{ ...textColumn('paraName', '参数名称', 180), useSlot: true, slotName: 'name' },
|
|
88
|
+
textColumn('paraType', '参数类型', 110),
|
|
89
|
+
textColumn('paraMark', '参数标志', 110),
|
|
90
|
+
{ ...textColumn('paraValue', '参数值', 190), useSlot: true, slotName: 'value' },
|
|
91
|
+
textColumn('paraDesc', '参数描述', 220),
|
|
92
|
+
textColumn('remark', '备注', 180),
|
|
93
|
+
{ key: 'actions', label: '操作', width: 170, fixed: 'right', align: 'center', useSlot: true },
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
const clearSelection = () => {
|
|
97
|
+
selectedRows.value = []
|
|
98
|
+
tableRef.value?.getElTable()?.clearSelection()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** 查询和翻页使用已提交的关键字,旧请求返回时不覆盖新结果。 */
|
|
102
|
+
const loadList = async () => {
|
|
103
|
+
if (disposed) return
|
|
104
|
+
const requestId = ++listRequestId
|
|
105
|
+
const params = { keywords: appliedKeywords || undefined, pageIndex: pagination.current, pageSize: pagination.size }
|
|
106
|
+
listLoading.value = true
|
|
107
|
+
listError.value = ''
|
|
108
|
+
clearSelection()
|
|
109
|
+
try {
|
|
110
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
111
|
+
const response = await API.queryListApi(params)
|
|
112
|
+
if (disposed || requestId !== listRequestId) return
|
|
113
|
+
const body: ParameterListBody | null = ensureSuccess(response, '参数查询失败')
|
|
114
|
+
if (!body || !Array.isArray(body.resultList)) throw new Error('查询结果缺少参数列表')
|
|
115
|
+
const totalText = body.totalCount ?? String(body.resultList.length)
|
|
116
|
+
const total = Number(totalText)
|
|
117
|
+
if (!/^\d+$/.test(totalText) || !Number.isSafeInteger(total) || total < 0) {
|
|
118
|
+
throw new Error('查询返回的总条数无效')
|
|
119
|
+
}
|
|
120
|
+
const lastPage = Math.max(1, Math.ceil(total / params.pageSize))
|
|
121
|
+
if (params.pageIndex > lastPage) {
|
|
122
|
+
pagination.current = lastPage
|
|
123
|
+
params.pageIndex = lastPage
|
|
124
|
+
if (attempt === 0) continue
|
|
125
|
+
throw new Error('参数列表已变化,请重新查询')
|
|
126
|
+
}
|
|
127
|
+
rows.value = body.resultList.map(readParameter)
|
|
128
|
+
pagination.total = total
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if (disposed || requestId !== listRequestId) return
|
|
133
|
+
rows.value = []
|
|
134
|
+
Object.assign(pagination, { current: 1, total: 0 })
|
|
135
|
+
listError.value = errorMessage(error)
|
|
136
|
+
ElMessage.error(listError.value)
|
|
137
|
+
} finally {
|
|
138
|
+
if (!disposed && requestId === listRequestId) listLoading.value = false
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const search = () => {
|
|
143
|
+
if (listBusy.value) return
|
|
144
|
+
appliedKeywords = keywords.value.trim()
|
|
145
|
+
pagination.current = 1
|
|
146
|
+
void loadList()
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const resetFilters = () => {
|
|
150
|
+
if (listBusy.value) return
|
|
151
|
+
keywords.value = ''
|
|
152
|
+
search()
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const refreshList = () => {
|
|
156
|
+
if (!listBusy.value) void loadList()
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const changePage = (current: number, size = pagination.size) => {
|
|
160
|
+
if (listBusy.value || (current === pagination.current && size === pagination.size)) return
|
|
161
|
+
Object.assign(pagination, { current, size })
|
|
162
|
+
void loadList()
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// 新增、修改和详情共用弹窗;详情失败时禁用保存,避免提交未完整加载的数据。
|
|
166
|
+
const dialogVisible = ref(false)
|
|
167
|
+
const dialogMode = ref<DialogMode>('create')
|
|
168
|
+
const dialogLoading = ref(false)
|
|
169
|
+
const dialogReady = ref(false)
|
|
170
|
+
const dialogError = ref('')
|
|
171
|
+
const dialogSource = ref<DigitalRmbParameter | null>(null)
|
|
172
|
+
const form = ref(createFormValues())
|
|
173
|
+
const formNameError = ref('')
|
|
174
|
+
const dialogTitle = computed(() => ({ create: '新增参数', edit: '修改参数', detail: '参数详情' })[dialogMode.value])
|
|
175
|
+
const dialogBusy = computed(() => dialogLoading.value || writing.value)
|
|
176
|
+
|
|
177
|
+
const openCreateDialog = () => {
|
|
178
|
+
if (listBusy.value) return
|
|
179
|
+
detailRequestId += 1
|
|
180
|
+
dialogMode.value = 'create'
|
|
181
|
+
dialogSource.value = null
|
|
182
|
+
form.value = createFormValues()
|
|
183
|
+
formNameError.value = ''
|
|
184
|
+
dialogError.value = ''
|
|
185
|
+
dialogLoading.value = false
|
|
186
|
+
dialogReady.value = true
|
|
187
|
+
dialogVisible.value = true
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const loadDetail = async () => {
|
|
191
|
+
const source = dialogSource.value
|
|
192
|
+
if (!source || !hasName(source) || disposed || writing.value) return
|
|
193
|
+
const paraName = source.paraName!
|
|
194
|
+
const requestId = ++detailRequestId
|
|
195
|
+
dialogLoading.value = true
|
|
196
|
+
dialogReady.value = false
|
|
197
|
+
dialogError.value = ''
|
|
198
|
+
try {
|
|
199
|
+
const response = await API.getDetailApi(paraName)
|
|
200
|
+
if (disposed || requestId !== detailRequestId) return
|
|
201
|
+
const detail = readParameter(ensureSuccess(response, '读取参数详情失败'))
|
|
202
|
+
if (!Object.keys(detail).length) throw new Error('服务端未返回参数详情')
|
|
203
|
+
if (detail.paraName !== undefined && detail.paraName !== paraName) {
|
|
204
|
+
throw new Error('返回的参数名称与当前记录不一致')
|
|
205
|
+
}
|
|
206
|
+
form.value = createFormValues({ ...source, ...detail, paraName })
|
|
207
|
+
dialogReady.value = true
|
|
208
|
+
} catch (error) {
|
|
209
|
+
if (!disposed && requestId === detailRequestId) dialogError.value = errorMessage(error)
|
|
210
|
+
} finally {
|
|
211
|
+
if (!disposed && requestId === detailRequestId) dialogLoading.value = false
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const openParameterDialog = (parameter: DigitalRmbParameter, mode: 'edit' | 'detail' = 'detail') => {
|
|
216
|
+
if (listBusy.value || !hasName(parameter)) return
|
|
217
|
+
dialogMode.value = mode
|
|
218
|
+
dialogSource.value = { ...parameter }
|
|
219
|
+
form.value = createFormValues(parameter)
|
|
220
|
+
formNameError.value = ''
|
|
221
|
+
dialogError.value = ''
|
|
222
|
+
dialogVisible.value = true
|
|
223
|
+
void loadDetail()
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const closeParameterDialog = () => {
|
|
227
|
+
// 关闭后即使请求继续返回,也不能更新下一次打开的弹窗。
|
|
228
|
+
detailRequestId += 1
|
|
229
|
+
dialogLoading.value = false
|
|
230
|
+
dialogReady.value = false
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const submitParameter = async () => {
|
|
234
|
+
const mode = dialogMode.value
|
|
235
|
+
if (mode === 'detail' || dialogBusy.value || !dialogReady.value || !dialogVisible.value) return
|
|
236
|
+
const values = { ...form.value }
|
|
237
|
+
if (!values.paraName.trim()) {
|
|
238
|
+
formNameError.value = '请填写参数名称'
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
formNameError.value = ''
|
|
242
|
+
dialogError.value = ''
|
|
243
|
+
pendingMutation.value = mode
|
|
244
|
+
try {
|
|
245
|
+
const response = mode === 'create'
|
|
246
|
+
? await API.addApi({ ...values, paraName: values.paraName.trim() })
|
|
247
|
+
: await API.editApi({
|
|
248
|
+
paraName: dialogSource.value!.paraName!,
|
|
249
|
+
paraType: values.paraType,
|
|
250
|
+
paraValue: values.paraValue,
|
|
251
|
+
paraDesc: values.paraDesc,
|
|
252
|
+
remark: values.remark,
|
|
253
|
+
})
|
|
254
|
+
if (disposed) return
|
|
255
|
+
ensureSuccess(response, mode === 'create' ? '新增参数失败' : '修改参数失败')
|
|
256
|
+
ElMessage.success(response.head.retMsg?.trim() || (mode === 'create' ? '新增请求成功' : '修改请求成功'))
|
|
257
|
+
dialogVisible.value = false
|
|
258
|
+
// 写接口不返回记录;重新查询服务端,不在前端伪造保存后的列表。
|
|
259
|
+
await loadList()
|
|
260
|
+
} catch (error) {
|
|
261
|
+
if (!disposed) dialogError.value = errorMessage(error)
|
|
262
|
+
} finally {
|
|
263
|
+
if (!disposed) pendingMutation.value = null
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// 删除名单取操作时的快照,确认后提交参数名称数组,支持单条和批量删除。
|
|
268
|
+
const deleteDialogVisible = ref(false)
|
|
269
|
+
const deleteNames = ref<string[]>([])
|
|
270
|
+
const deleteError = ref('')
|
|
271
|
+
const openDeleteDialog = (parameters: DigitalRmbParameter[]) => {
|
|
272
|
+
if (listBusy.value) return
|
|
273
|
+
const names = parameters.filter(hasName).map((parameter) => parameter.paraName!)
|
|
274
|
+
if (!names.length) return
|
|
275
|
+
deleteNames.value = [...new Set(names)]
|
|
276
|
+
deleteError.value = ''
|
|
277
|
+
deleteDialogVisible.value = true
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const submitDelete = async () => {
|
|
281
|
+
if (writing.value || !deleteDialogVisible.value || !deleteNames.value.length) return
|
|
282
|
+
pendingMutation.value = 'delete'
|
|
283
|
+
deleteError.value = ''
|
|
284
|
+
try {
|
|
285
|
+
const response = await API.deleteApi(deleteNames.value)
|
|
286
|
+
if (disposed) return
|
|
287
|
+
ensureSuccess(response, '删除参数失败')
|
|
288
|
+
ElMessage.success(response.head.retMsg?.trim() || '删除请求成功')
|
|
289
|
+
deleteDialogVisible.value = false
|
|
290
|
+
await loadList()
|
|
291
|
+
} catch (error) {
|
|
292
|
+
if (!disposed) deleteError.value = errorMessage(error)
|
|
293
|
+
} finally {
|
|
294
|
+
if (!disposed) pendingMutation.value = null
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
onMounted(loadList)
|
|
299
|
+
onBeforeUnmount(() => {
|
|
300
|
+
disposed = true
|
|
301
|
+
listRequestId += 1
|
|
302
|
+
detailRequestId += 1
|
|
303
|
+
})
|
|
304
|
+
</script>
|
|
305
|
+
|
|
306
|
+
<template>
|
|
307
|
+
<ElConfigProvider :size="tableSettings.size">
|
|
308
|
+
<main class="dcps-page">
|
|
309
|
+
<el-card class="parameter-header" shadow="never" :body-style="{ padding: '12px 14px' }">
|
|
310
|
+
<h1>数字人民币参数管理</h1>
|
|
311
|
+
<el-form
|
|
312
|
+
v-show="showSearchBar"
|
|
313
|
+
class="filter-form"
|
|
314
|
+
label-position="top"
|
|
315
|
+
:disabled="listBusy"
|
|
316
|
+
@submit.prevent="search()"
|
|
317
|
+
>
|
|
318
|
+
<el-form-item label="关键字">
|
|
319
|
+
<el-input v-model="keywords" placeholder="输入查询关键字" clearable />
|
|
320
|
+
</el-form-item>
|
|
321
|
+
<el-space class="filter-actions" :size="8">
|
|
322
|
+
<el-button @click="resetFilters">重置</el-button>
|
|
323
|
+
<el-button type="primary" native-type="submit" :loading="listLoading">查询</el-button>
|
|
324
|
+
</el-space>
|
|
325
|
+
</el-form>
|
|
326
|
+
</el-card>
|
|
327
|
+
|
|
328
|
+
<el-alert v-if="listError" :title="listError" type="error" show-icon :closable="false" />
|
|
329
|
+
|
|
330
|
+
<div class="parameter-table-region">
|
|
331
|
+
<ArtDataTable
|
|
332
|
+
ref="tableRef"
|
|
333
|
+
v-model:settings="tableSettings"
|
|
334
|
+
v-model:show-search-bar="showSearchBar"
|
|
335
|
+
style="padding: 10px"
|
|
336
|
+
fill
|
|
337
|
+
toolbar-layout="search,refresh,size,fullscreen,columns,settings"
|
|
338
|
+
:data="rows"
|
|
339
|
+
:columns="tableColumns"
|
|
340
|
+
:loading="listBusy"
|
|
341
|
+
:pagination="pagination"
|
|
342
|
+
:pagination-options="paginationOptions"
|
|
343
|
+
:empty-text="listError ? '查询失败,请重新查询' : '暂无符合条件的参数'"
|
|
344
|
+
@refresh="refreshList"
|
|
345
|
+
@pagination:current-change="changePage"
|
|
346
|
+
@pagination:size-change="(size: number) => changePage(1, size)"
|
|
347
|
+
@selection-change="(selection: DigitalRmbParameter[]) => selectedRows = selection"
|
|
348
|
+
@row-dblclick="(row: DigitalRmbParameter) => openParameterDialog(row)"
|
|
349
|
+
>
|
|
350
|
+
<template #toolbar-left>
|
|
351
|
+
<div class="table-actions">
|
|
352
|
+
<el-button type="primary" :disabled="listBusy" @click="openCreateDialog">新增参数</el-button>
|
|
353
|
+
<el-button type="danger" plain :disabled="listBusy || !selectedRows.length" @click="openDeleteDialog(selectedRows)">
|
|
354
|
+
批量删除
|
|
355
|
+
</el-button>
|
|
356
|
+
<span v-if="selectedRows.length" class="selection-count">已选 {{ selectedRows.length }} 项</span>
|
|
357
|
+
</div>
|
|
358
|
+
</template>
|
|
359
|
+
<template #name="{ row }">
|
|
360
|
+
<el-button v-if="hasName(row)" class="parameter-name" link type="primary" :disabled="listBusy" @click.stop="openParameterDialog(row)">
|
|
361
|
+
{{ row.paraName }}
|
|
362
|
+
</el-button>
|
|
363
|
+
<span v-else>—</span>
|
|
364
|
+
</template>
|
|
365
|
+
<template #value="{ row }">
|
|
366
|
+
<span class="parameter-value">{{ displayValue(row.paraValue) }}</span>
|
|
367
|
+
</template>
|
|
368
|
+
<template #actions="{ row }">
|
|
369
|
+
<el-button link type="primary" :disabled="listBusy || !hasName(row)" @click.stop="openParameterDialog(row)">详情</el-button>
|
|
370
|
+
<el-button link type="primary" :disabled="listBusy || !hasName(row)" @click.stop="openParameterDialog(row, 'edit')">修改</el-button>
|
|
371
|
+
<el-button link type="danger" :disabled="listBusy || !hasName(row)" @click.stop="openDeleteDialog([row])">删除</el-button>
|
|
372
|
+
</template>
|
|
373
|
+
</ArtDataTable>
|
|
374
|
+
</div>
|
|
375
|
+
|
|
376
|
+
<el-dialog
|
|
377
|
+
v-model="dialogVisible"
|
|
378
|
+
:title="dialogTitle"
|
|
379
|
+
width="min(660px, calc(100vw - 32px))"
|
|
380
|
+
append-to-body
|
|
381
|
+
destroy-on-close
|
|
382
|
+
:show-close="!writing"
|
|
383
|
+
:close-on-click-modal="false"
|
|
384
|
+
:close-on-press-escape="!writing"
|
|
385
|
+
@close="closeParameterDialog"
|
|
386
|
+
>
|
|
387
|
+
<el-alert v-if="dialogError" class="dialog-error" :title="dialogError" type="error" show-icon :closable="false" />
|
|
388
|
+
<el-scrollbar max-height="min(62vh, 620px)">
|
|
389
|
+
<div v-if="dialogLoading" class="detail-loading" role="status">正在读取参数详情…</div>
|
|
390
|
+
<dl v-else-if="dialogMode === 'detail' && dialogReady" class="detail-fields">
|
|
391
|
+
<div v-for="field in parameterFields" :key="field.key" class="detail-field" :class="{ 'detail-field--wide': field.multiline }">
|
|
392
|
+
<dt>{{ field.label }}</dt>
|
|
393
|
+
<dd :class="{ 'parameter-value': field.key === 'paraValue' }">{{ displayValue(form[field.key]) }}</dd>
|
|
394
|
+
</div>
|
|
395
|
+
</dl>
|
|
396
|
+
<el-form
|
|
397
|
+
v-else-if="dialogMode !== 'detail' && dialogReady"
|
|
398
|
+
id="parameter-editor-form"
|
|
399
|
+
class="parameter-form"
|
|
400
|
+
label-position="top"
|
|
401
|
+
:model="form"
|
|
402
|
+
:disabled="dialogBusy"
|
|
403
|
+
@submit.prevent="submitParameter()"
|
|
404
|
+
>
|
|
405
|
+
<el-form-item
|
|
406
|
+
v-for="field in parameterFields"
|
|
407
|
+
:key="field.key"
|
|
408
|
+
:label="field.label"
|
|
409
|
+
:required="field.key === 'paraName'"
|
|
410
|
+
:error="field.key === 'paraName' ? formNameError : undefined"
|
|
411
|
+
:class="{ 'form-item--wide': field.multiline }"
|
|
412
|
+
>
|
|
413
|
+
<el-input
|
|
414
|
+
v-model="form[field.key]"
|
|
415
|
+
:type="field.multiline ? 'textarea' : 'text'"
|
|
416
|
+
:autosize="field.multiline ? { minRows: 2, maxRows: 5 } : undefined"
|
|
417
|
+
:readonly="dialogMode === 'edit' && (field.key === 'paraName' || field.key === 'paraMark')"
|
|
418
|
+
:placeholder="`请输入${field.label}`"
|
|
419
|
+
@input="field.key === 'paraName' && (formNameError = '')"
|
|
420
|
+
/>
|
|
421
|
+
</el-form-item>
|
|
422
|
+
</el-form>
|
|
423
|
+
</el-scrollbar>
|
|
424
|
+
<template #footer>
|
|
425
|
+
<el-button v-if="dialogError && !dialogReady && dialogSource" :loading="dialogLoading" @click="loadDetail">重新读取</el-button>
|
|
426
|
+
<el-button :disabled="writing" @click="dialogVisible = false">{{ dialogMode === 'detail' ? '关闭' : '取消' }}</el-button>
|
|
427
|
+
<el-button
|
|
428
|
+
v-if="dialogMode !== 'detail'"
|
|
429
|
+
type="primary"
|
|
430
|
+
:loading="pendingMutation === 'create' || pendingMutation === 'edit'"
|
|
431
|
+
:disabled="dialogLoading || !dialogReady || writing"
|
|
432
|
+
@click="submitParameter"
|
|
433
|
+
>
|
|
434
|
+
{{ dialogMode === 'create' ? '新增' : '保存' }}
|
|
435
|
+
</el-button>
|
|
436
|
+
</template>
|
|
437
|
+
</el-dialog>
|
|
438
|
+
|
|
439
|
+
<el-dialog
|
|
440
|
+
v-model="deleteDialogVisible"
|
|
441
|
+
title="删除参数"
|
|
442
|
+
width="min(460px, calc(100vw - 32px))"
|
|
443
|
+
append-to-body
|
|
444
|
+
destroy-on-close
|
|
445
|
+
:show-close="!writing"
|
|
446
|
+
:close-on-click-modal="false"
|
|
447
|
+
:close-on-press-escape="!writing"
|
|
448
|
+
>
|
|
449
|
+
<p class="delete-description">确认删除以下 {{ deleteNames.length }} 项参数?</p>
|
|
450
|
+
<el-scrollbar max-height="240px">
|
|
451
|
+
<ul class="delete-list"><li v-for="name in deleteNames" :key="name">{{ name }}</li></ul>
|
|
452
|
+
</el-scrollbar>
|
|
453
|
+
<el-alert v-if="deleteError" class="dialog-error" :title="deleteError" type="error" show-icon :closable="false" />
|
|
454
|
+
<template #footer>
|
|
455
|
+
<el-button :disabled="writing" @click="deleteDialogVisible = false">取消</el-button>
|
|
456
|
+
<el-button type="danger" :loading="pendingMutation === 'delete'" :disabled="writing" @click="submitDelete">确认删除</el-button>
|
|
457
|
+
</template>
|
|
458
|
+
</el-dialog>
|
|
459
|
+
</main>
|
|
460
|
+
</ElConfigProvider>
|
|
461
|
+
</template>
|
|
462
|
+
|
|
463
|
+
<style scoped>
|
|
464
|
+
.dcps-page {
|
|
465
|
+
display: flex;
|
|
466
|
+
flex-direction: column;
|
|
467
|
+
gap: 5px;
|
|
468
|
+
height: calc(100vh - var(--dpcs-host-layout-height));
|
|
469
|
+
height: calc(100dvh - var(--dpcs-host-layout-height));
|
|
470
|
+
min-width: 0;
|
|
471
|
+
min-height: 0;
|
|
472
|
+
padding: 5px;
|
|
473
|
+
overflow: hidden;
|
|
474
|
+
background: var(--el-bg-color-page, #f7f8fa);
|
|
475
|
+
}
|
|
476
|
+
.parameter-header { flex: 0 0 auto; }
|
|
477
|
+
.parameter-header h1 { margin: 0; font-size: 15px; font-weight: 600; line-height: 24px; }
|
|
478
|
+
.filter-form { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 10px 12px; margin-top: 12px; }
|
|
479
|
+
.filter-form :deep(.el-form-item) { flex: 0 1 360px; min-width: 160px; margin: 0; }
|
|
480
|
+
.filter-form :deep(.el-form-item__label) { margin-bottom: 4px; }
|
|
481
|
+
.filter-actions { flex-shrink: 0; }
|
|
482
|
+
.parameter-table-region { flex: 1; min-width: 0; min-height: 0; }
|
|
483
|
+
.table-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
|
|
484
|
+
.table-actions :deep(.el-button + .el-button) { margin-left: 0; }
|
|
485
|
+
.selection-count { margin-left: 4px; color: var(--el-text-color-secondary); font-size: 12px; white-space: nowrap; }
|
|
486
|
+
.parameter-name { display: inline; max-width: 100%; overflow: hidden; text-overflow: ellipsis; text-align: left; vertical-align: middle; }
|
|
487
|
+
.parameter-value { font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', monospace; font-variant-numeric: tabular-nums; white-space: pre-wrap; }
|
|
488
|
+
.parameter-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 16px; padding: 2px 4px; }
|
|
489
|
+
.form-item--wide { grid-column: 1 / -1; }
|
|
490
|
+
.parameter-form :deep(.el-input__inner[readonly]) { color: var(--el-text-color-secondary); }
|
|
491
|
+
.dialog-error { margin-bottom: 14px; }
|
|
492
|
+
.detail-loading { padding: 40px 0; color: var(--el-text-color-secondary); text-align: center; }
|
|
493
|
+
.detail-fields { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1px; margin: 0; overflow: hidden; border: 1px solid var(--el-border-color-lighter); border-radius: 8px; background: var(--el-border-color-lighter); }
|
|
494
|
+
.detail-field { min-width: 0; padding: 12px 14px; background: var(--el-bg-color); }
|
|
495
|
+
.detail-field--wide { grid-column: 1 / -1; }
|
|
496
|
+
.detail-field dt { margin-bottom: 6px; color: var(--el-text-color-secondary); font-size: 12px; }
|
|
497
|
+
.detail-field dd { margin: 0; overflow-wrap: anywhere; white-space: pre-wrap; line-height: 1.6; }
|
|
498
|
+
.delete-description { margin: 0 0 12px; line-height: 1.6; }
|
|
499
|
+
.delete-list { margin: 0 0 14px; padding: 12px 14px 12px 32px; border-radius: 8px; background: var(--el-fill-color-light); }
|
|
500
|
+
.delete-list li { padding: 3px 0; overflow-wrap: anywhere; }
|
|
501
|
+
@media (max-width: 540px) {
|
|
502
|
+
.filter-form :deep(.el-form-item) { flex-basis: 100%; }
|
|
503
|
+
.parameter-form, .detail-fields { grid-template-columns: minmax(0, 1fr); }
|
|
504
|
+
.selection-count { width: 100%; margin-left: 0; }
|
|
505
|
+
}
|
|
506
|
+
</style>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CityClearingParameter,
|
|
3
|
+
Tam120305003InBody,
|
|
4
|
+
Tam140305001OutBody,
|
|
5
|
+
} from '../city-clearing/parameters/typs'
|
|
6
|
+
|
|
7
|
+
/** 数字人民币复用头寸参数契约,所有参数字段保留字符串。 */
|
|
8
|
+
export type DigitalRmbParameter = CityClearingParameter
|
|
9
|
+
export type ParameterListBody = Tam140305001OutBody
|
|
10
|
+
export type ParameterEditBody = Tam120305003InBody
|
|
11
|
+
export type ParameterFormValues = Required<DigitalRmbParameter>
|
|
12
|
+
|
|
13
|
+
/** 模块由页面接口固定为数币,调用处只提供关键字和公共分页。 */
|
|
14
|
+
export interface ParameterQueryParams {
|
|
15
|
+
keywords?: string
|
|
16
|
+
pageIndex: number
|
|
17
|
+
pageSize: number
|
|
18
|
+
}
|
package/src/view/dcps/runtime.ts
CHANGED
|
@@ -1,35 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* 页面通过
|
|
2
|
+
* DCPS 各页面共用的运行时配置,统一通过具名对象 dcpsRuntime 导出。
|
|
3
|
+
* 页面通过 `import { dcpsRuntime } from '../runtime'` 引入;分组子页面使用 `../../runtime`。
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const parts = new Intl.DateTimeFormat('en-CA', {
|
|
12
|
-
timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit',
|
|
13
|
-
}).formatToParts(new Date())
|
|
14
|
-
const read = (type: Intl.DateTimeFormatPartTypes) => parts.find((part) => part.type === type)?.value || ''
|
|
15
|
-
return read('year') + read('month') + read('day')
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** 预报页面的默认查询日期;日期格式为 YYYYMMDD。 */
|
|
19
|
-
export const forecastRuntime = {
|
|
20
|
-
tranDate: getToday(), // 默认交易日期,查询和重置时使用。
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** 授权页面的审批人和默认创建日期;日期格式为 YYYYMMDD,当前使用固定日期。 */
|
|
24
|
-
export const authRuntime = {
|
|
25
|
-
approverId: '10092', // 审批人工号。
|
|
26
|
-
approverName: '王干事', // 审批人姓名。
|
|
27
|
-
createTmBegin: '20261012', // 默认创建开始日期。
|
|
28
|
-
createTmEnd: '20261012', // 默认创建结束日期。
|
|
29
|
-
}
|
|
6
|
+
// 模块加载时按上海时区生成一次日期,所有页面共用,格式为 YYYYMMDD。
|
|
7
|
+
const dateParts = new Intl.DateTimeFormat('en-CA', {
|
|
8
|
+
timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit',
|
|
9
|
+
}).formatToParts(new Date())
|
|
10
|
+
const readDatePart = (type: Intl.DateTimeFormatPartTypes) => dateParts.find((part) => part.type === type)?.value || ''
|
|
30
11
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
12
|
+
export const dcpsRuntime = {
|
|
13
|
+
date: readDatePart('year') + readDatePart('month') + readDatePart('day'), // 共用业务日期。
|
|
14
|
+
approverId: '10092', // 工号。
|
|
15
|
+
approverName: '王干事', // 姓名。
|
|
35
16
|
}
|