Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in / Register
Toggle navigation
P
problem_rectification
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
David Yang
problem_rectification
Commits
426a2639
Commit
426a2639
authored
Aug 25, 2026
by
David Yang
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: support single and multiple audit materials
parent
5206d42b
Show whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
208 additions
and
114 deletions
+208
-114
FileUploadField.jsx
src/components/FileUploadField.jsx
+7
-3
TailwindEditableField.jsx
...s/RecordDetailDrawer/components/TailwindEditableField.jsx
+1
-0
index.jsx
src/pages/RecordDetailDrawer/index.jsx
+33
-8
ProblemRectificationMaterialField.jsx
...fication/components/ProblemRectificationMaterialField.jsx
+151
-87
problem-rectification.business-flow.md
...blem_rectification/problem-rectification.business-flow.md
+6
-6
record-api.md
src/services/record-api.md
+7
-7
subjectsDefinition.md
src/services/subjectsDefinition.md
+3
-3
No files found.
src/components/FileUploadField.jsx
View file @
426a2639
...
...
@@ -9,6 +9,7 @@ export default function FileUploadField({
description
=
'将文件拖到此处,或从本地选择。'
,
isDisabled
=
false
,
fileName
=
''
,
multiple
=
false
,
onFilesChange
,
triggerLabel
=
'选择文件'
,
})
{
...
...
@@ -29,9 +30,9 @@ export default function FileUploadField({
if
(
fileItems
.
length
===
0
)
return
;
const
files
=
await
Promise
.
all
(
fileItems
.
map
((
item
)
=>
item
.
getFile
()));
emitFiles
(
files
.
slice
(
0
,
1
));
emitFiles
(
multiple
?
files
:
files
.
slice
(
0
,
1
));
},
[
emitFiles
],
[
emitFiles
,
multiple
],
);
return
(
...
...
@@ -44,6 +45,7 @@ export default function FileUploadField({
<
DropZone
.
Input
accept=
{
accept
}
disabled=
{
isDisabled
}
multiple=
{
multiple
}
onSelect=
{
emitFiles
}
/>
<
DropZone
.
Icon
>
...
...
@@ -51,7 +53,9 @@ export default function FileUploadField({
</
DropZone
.
Icon
>
<
DropZone
.
Label
>
{
fileName
||
'将文件拖到此处'
}
</
DropZone
.
Label
>
<
DropZone
.
Description
>
{
fileName
?
'已选择文件,可重新选择进行替换。'
:
description
}
{
fileName
?
`已选择${multiple ? '文件,可继续添加。' : '文件,可重新选择进行替换。'}`
:
description
}
</
DropZone
.
Description
>
<
DropZone
.
Trigger
isDisabled=
{
isDisabled
}
>
<
Upload
aria
-
hidden=
"true"
/>
...
...
src/pages/RecordDetailDrawer/components/TailwindEditableField.jsx
View file @
426a2639
...
...
@@ -399,6 +399,7 @@ function ControlledFieldEditor({
<
ProblemRectificationMaterialField
editSessionId=
{
editSessionId
}
fieldPath=
{
fieldPath
}
multiple=
{
Boolean
(
fieldDef
?.
multiple
)
}
onChange=
{
onChange
}
onPendingLocalMaterialChange=
{
onPendingLocalMaterialChange
}
onValidationChange=
{
onValidationChange
}
...
...
src/pages/RecordDetailDrawer/index.jsx
View file @
426a2639
...
...
@@ -268,12 +268,33 @@ const RecordDetailDrawer = (props, ref) => {
},
[]);
const
handlePendingLocalMaterialChange
=
useCallback
(
(
fieldPath
,
pendingMaterial
,
editSessionId
)
=>
{
(
fieldPath
,
change
,
editSessionId
)
=>
{
if
(
editSessionId
!==
editSessionRef
.
current
)
return
;
const
nextPendingMaterials
=
{
...
pendingLocalMaterialsRef
.
current
};
if
(
pendingMaterial
)
{
nextPendingMaterials
[
fieldPath
]
=
pendingMaterial
;
const
currentFieldMaterials
=
Array
.
isArray
(
nextPendingMaterials
[
fieldPath
])
?
nextPendingMaterials
[
fieldPath
]
:
[];
let
nextFieldMaterials
=
currentFieldMaterials
;
if
(
change
?.
type
===
'replace'
)
{
nextFieldMaterials
=
change
.
materials
||
[];
}
else
if
(
change
?.
type
===
'add'
)
{
const
materialsById
=
new
Map
(
currentFieldMaterials
.
map
((
material
)
=>
[
material
.
clientId
,
material
]),
);
(
change
.
materials
||
[]).
forEach
((
material
)
=>
{
materialsById
.
set
(
material
.
clientId
,
material
);
});
nextFieldMaterials
=
[...
materialsById
.
values
()];
}
else
if
(
change
?.
type
===
'remove'
)
{
nextFieldMaterials
=
currentFieldMaterials
.
filter
(
(
material
)
=>
material
.
clientId
!==
change
.
clientId
,
);
}
if
(
nextFieldMaterials
.
length
>
0
)
{
nextPendingMaterials
[
fieldPath
]
=
nextFieldMaterials
;
}
else
{
delete
nextPendingMaterials
[
fieldPath
];
}
...
...
@@ -292,21 +313,25 @@ const RecordDetailDrawer = (props, ref) => {
const
pendingMaterials
=
Object
.
entries
(
pendingLocalMaterialsRef
.
current
)
.
filter
(([
fieldPath
])
=>
dirtyFields
[
fieldPath
]);
for
(
const
[
fieldPath
,
pendingMaterial
]
of
pendingMaterials
)
{
for
(
const
[
fieldPath
,
fieldMaterials
]
of
pendingMaterials
)
{
for
(
const
pendingMaterial
of
fieldMaterials
)
{
if
(
pendingMaterial
.
materialRecordId
)
continue
;
const
createdMaterial
=
await
createProblemRectificationMaterial
({
fileAsset
:
pendingMaterial
.
fileAsset
,
currentUser
,
});
const
currentFieldMaterials
=
pendingLocalMaterialsRef
.
current
[
fieldPath
]
||
[];
pendingLocalMaterialsRef
.
current
=
{
...
pendingLocalMaterialsRef
.
current
,
[
fieldPath
]:
{
...
pendingMaterial
,
materialRecordId
:
createdMaterial
.
id
,
},
[
fieldPath
]:
currentFieldMaterials
.
map
((
material
)
=>
material
.
clientId
===
pendingMaterial
.
clientId
?
{
...
material
,
materialRecordId
:
createdMaterial
.
id
}
:
material
,
),
};
}
}
await
handleFieldsUpdate
(
Object
.
values
(
dirtyFields
));
setIsEditing
(
false
);
...
...
src/pages/problem_rectification/components/ProblemRectificationMaterialField.jsx
View file @
426a2639
import
{
ArrowPathIcon
,
ChevronDown
,
Close
,
Delete
,
FIELD_SUFFIX_ICON_STYLE
,
Search
,
}
from
'@/components/AppIcons'
;
import
FileUploadField
from
'@/components/FileUploadField'
;
import
XinyuanAutocomplete
from
'@/components/XinyuanAutocomplete'
;
import
{
queryAuditMaterials
,
uploadProblemRectificationFile
,
}
from
'../problemRectificationMaterialService'
;
import
{
Alert
,
Autocomplete
,
Button
,
Label
,
ListBox
,
SearchField
,
Spinner
,
Tabs
,
Tooltip
,
}
from
'@heroui/react'
;
...
...
@@ -27,6 +20,24 @@ import { useEffect, useMemo, useState } from 'react';
const
MATERIAL_CENTER_TAB
=
'material-center'
;
const
LOCAL_UPLOAD_TAB
=
'local-upload'
;
function
toFileArray
(
value
)
{
if
(
Array
.
isArray
(
value
))
return
value
.
filter
(
Boolean
);
return
value
?
[
value
]
:
[];
}
function
getFileKey
(
file
=
{})
{
return
file
.
url
||
[
file
.
name
,
file
.
type
,
file
.
size
].
filter
(
Boolean
).
join
(
'|'
);
}
function
appendUniqueFiles
(
currentFiles
,
nextFiles
)
{
const
filesByKey
=
new
Map
();
[...
currentFiles
,
...
nextFiles
].
forEach
((
file
)
=>
{
const
key
=
getFileKey
(
file
);
if
(
key
)
filesByKey
.
set
(
key
,
file
);
});
return
[...
filesByKey
.
values
()];
}
function
formatMaterialDescription
(
material
)
{
return
[
material
.
fileAsset
?.
name
,
...
...
@@ -38,6 +49,7 @@ function formatMaterialDescription(material) {
export
default
function
ProblemRectificationMaterialField
({
editSessionId
,
fieldPath
,
multiple
=
false
,
onChange
,
onPendingLocalMaterialChange
,
onValidationChange
,
...
...
@@ -53,7 +65,11 @@ export default function ProblemRectificationMaterialField({
const
[
retryKey
,
setRetryKey
]
=
useState
(
0
);
const
[
uploading
,
setUploading
]
=
useState
(
false
);
const
[
uploadError
,
setUploadError
]
=
useState
(
''
);
const
[
localFileAsset
,
setLocalFileAsset
]
=
useState
(
null
);
const
[
localFileKeys
,
setLocalFileKeys
]
=
useState
(()
=>
new
Set
());
const
fileValues
=
useMemo
(
()
=>
(
multiple
?
toFileArray
(
value
)
:
toFileArray
(
value
).
slice
(
0
,
1
)),
[
multiple
,
value
],
);
useEffect
(()
=>
{
setActiveTab
(
MATERIAL_CENTER_TAB
);
...
...
@@ -62,7 +78,7 @@ export default function ProblemRectificationMaterialField({
setSelectedMaterialId
(
null
);
setMaterialsError
(
''
);
setUploadError
(
''
);
setLocalFile
Asset
(
null
);
setLocalFile
Keys
(
new
Set
()
);
},
[
editSessionId
]);
useEffect
(()
=>
{
...
...
@@ -99,88 +115,167 @@ export default function ProblemRectificationMaterialField({
};
},
[
activeTab
,
debouncedKeyword
,
retryKey
]);
const
materialOptions
=
useMemo
(
()
=>
materials
.
map
((
material
)
=>
({
...
material
,
id
:
material
.
id
,
label
:
material
.
materialTitle
,
textValue
:
`
${
material
.
materialTitle
}
${
material
.
fileAsset
?.
name
||
''
}
`,
})),
[materials],
);
const selectedMaterial = useMemo(
() => materials.find((material) => material.id === selectedMaterialId),
[materials, selectedMaterialId],
);
const
selectedValueText
=
selectedMaterial
?.
materialTitle
||
value
?.
name
||
'请选择审计材料'
;
const selectedValueText = multiple
? fileValues.length > 0
? `
已选
$
{
fileValues
.
length
}
个附件,可继续添加
`
: '请选择审计材料'
: selectedMaterial?.materialTitle || fileValues[0]?.name || '请选择审计材料';
const handleMaterialSelection = (key) => {
const materialId = key ? String(key) : '';
const material = materials.find((item) => String(item.id) === materialId);
if (!material?.fileAsset?.url) return;
setSelectedMaterialId
(
material
.
id
);
setLocalFileAsset
(
null
);
setUploadError
(
''
);
onPendingLocalMaterialChange
?.(
fieldPath
,
null
,
editSessionId
);
onChange
?.({
const fileAsset = {
name: material.fileAsset.name,
type: material.fileAsset.type || 'application/octet-stream',
size: Number(material.fileAsset.size || 0),
url: material.fileAsset.url,
});
};
setUploadError('');
if (multiple) {
onChange?.(appendUniqueFiles(fileValues, [fileAsset]));
setSelectedMaterialId(null);
return;
}
setSelectedMaterialId(material.id);
setLocalFileKeys(new Set());
onPendingLocalMaterialChange?.(
fieldPath,
{ type: 'replace', materials: [] },
editSessionId,
);
onChange?.(fileAsset);
};
const handleLocalUpload = async (files) => {
const
file
=
files
?.[
0
]
;
if
(
!
file
)
return
;
const
selectedFiles = multiple ? toFileArray(files) : toFileArray(files).slice(0, 1)
;
if (
selectedFiles.length === 0
) return;
setUploading(true);
setUploadError('');
onValidationChange?.(fieldPath, '附件正在上传,请稍候。', editSessionId);
const uploadedAssets = [];
const failedFiles = [];
for (const file of selectedFiles) {
try {
const
fileAsset
=
await
uploadProblemRectificationFile
(
file
);
setLocalFileAsset
(
fileAsset
);
uploadedAssets.push(await uploadProblemRectificationFile(file));
} catch (error) {
failedFiles.push({ file, error });
}
}
if (uploadedAssets.length > 0) {
const pendingMaterials = uploadedAssets.map((fileAsset) => ({
clientId: getFileKey(fileAsset),
fileAsset,
materialRecordId: null,
}));
const uploadedKeys = pendingMaterials.map((item) => item.clientId);
setLocalFileKeys((currentKeys) => new Set(
multiple ? [...currentKeys, ...uploadedKeys] : uploadedKeys,
));
setSelectedMaterialId(null);
onPendingLocalMaterialChange?.(
fieldPath,
{
fileAsset
,
materialRecordId
:
null
},
{
type: multiple ? 'add' : 'replace',
materials: pendingMaterials,
},
editSessionId,
);
onChange
?.(
fileAsset
);
}
catch
(
error
)
{
setUploadError
(
error
?.
message
||
'附件上传失败,请稍后重试。'
);
}
finally
{
onChange?.(multiple
? appendUniqueFiles(fileValues, uploadedAssets)
: uploadedAssets[uploadedAssets.length - 1]);
}
if (failedFiles.length > 0) {
const failedNames = failedFiles.map(({ file }) => file?.name).filter(Boolean).join('、');
setUploadError(
failedFiles.length === selectedFiles.length
? failedFiles[0].error?.message || '附件上传失败,请稍后重试。'
: `
部分附件上传失败:
$
{
failedNames
||
`
${
failedFiles
.
length
}
个文件`
}
,可重新选择后再试。
`,
);
}
setUploading(false);
onValidationChange?.(fieldPath, '', editSessionId);
}
};
const
handleRemove
=
()
=>
{
setSelectedMaterialId
(
null
);
setLocalFileAsset
(
null
);
const handleRemove = (file, fileIndex) => {
const fileKey = getFileKey(file);
setUploadError('');
onPendingLocalMaterialChange
?.(
fieldPath
,
null
,
editSessionId
);
setLocalFileKeys((currentKeys) => {
const nextKeys = new Set(currentKeys);
nextKeys.delete(fileKey);
return nextKeys;
});
onPendingLocalMaterialChange?.(
fieldPath,
{ type: 'remove', clientId: fileKey },
editSessionId,
);
if (multiple) {
onChange?.(fileValues.filter((_, index) => index !== fileIndex));
return;
}
setSelectedMaterialId(null);
onChange?.(null);
};
return (
<div className="space-y-4">
{
value
?.
name
||
value
?.
url
?
(
<
div
className=
"flex min-w-0 items-center justify-between gap-3"
>
{fileValues.length > 0 ? (
<div className="space-y-2">
{fileValues.map((file, index) => {
const fileKey = getFileKey(file);
const fileLabel = file?.name || file?.url || `
附件
$
{
index
+
1
}
`;
return (
<div className="flex min-w-0 items-center justify-between gap-3" key={`
$
{
fileKey
}
-
$
{
index
}
`}>
<div className="min-w-0">
<
div
className=
"truncate text-sm font-medium text-foreground"
title=
{
value
.
name
||
value
.
ur
l
}
>
{
value
.
name
||
value
.
ur
l
}
<div className="truncate text-sm font-medium text-foreground" title={fileLabe
l}>
{fileLabe
l}
</div>
<div className="mt-1 text-xs text-muted">
{
localFileAsset
?
'本地文件,保存后同步至审计材料中心'
:
'已选择附件'
}
{localFileKeys.has(fileKey)
? '本地文件,保存后同步至审计材料中心' : '已选择附件'}
</div>
</div>
<Tooltip>
<Button
aria
-
label=
"删除当前附件"
aria-label={`
删除附件:
$
{
fileLabel
}
`}
isDisabled={uploading}
isIconOnly
size="sm"
variant="danger-soft"
onPress=
{
handleRemove
}
onPress={() => handleRemove(file, index)
}
>
<Delete size={16} />
</Button>
<Tooltip.Content>删除当前附件</Tooltip.Content>
</Tooltip>
</div>
);
})}
</div>
) : (
<span className="text-sm text-muted">未上传</span>
)}
...
...
@@ -207,51 +302,23 @@ export default function ProblemRectificationMaterialField({
<Tabs.Panel className="pt-3" id={MATERIAL_CENTER_TAB}>
<div className="space-y-3">
<
Autocomplete
<
Xinyuan
Autocomplete
aria-label="选择审计材料"
fullWidth
emptyText="没有找到可选择的审计材料"
errorText={materialsError}
inputValue={keyword}
isDisabled={uploading}
isLoading={materialsLoading}
options={materialOptions}
placeholder="请选择审计材料"
searchPlaceholder="搜索材料标题或文件名"
selectedKey={selectedMaterialId}
selectionMode=
"single"
shouldFilter={false}
valueText={selectedValueText}
variant="secondary"
onSelectionChange=
{
handleMaterialSelection
}
>
<
Autocomplete
.
Trigger
>
<
Autocomplete
.
Value
>
<
span
className=
"block min-w-0 truncate"
title=
{
selectedValueText
}
>
{
selectedValueText
}
</
span
>
</
Autocomplete
.
Value
>
<
Autocomplete
.
Indicator
>
<
ChevronDown
size=
{
16
}
style=
{
FIELD_SUFFIX_ICON_STYLE
}
/>
</
Autocomplete
.
Indicator
>
</
Autocomplete
.
Trigger
>
<
Autocomplete
.
Popover
placement=
"bottom start"
>
<
Autocomplete
.
Filter
filter=
{
()
=>
true
}
inputValue=
{
keyword
}
onInputChange={setKeyword}
>
<
SearchField
aria
-
label=
"搜索审计材料"
fullWidth
variant=
"secondary"
>
<
SearchField
.
Group
>
<
SearchField
.
SearchIcon
><
Search
size=
{
16
}
/></
SearchField
.
SearchIcon
>
<
SearchField
.
Input
autoFocus
placeholder=
"搜索材料标题或文件名"
/>
<
SearchField
.
ClearButton
aria
-
label=
"清空搜索"
><
Close
size=
{
14
}
/></
SearchField
.
ClearButton
>
</
SearchField
.
Group
>
</
SearchField
>
<
ListBox
aria
-
label=
"审计材料列表"
items=
{
materialsLoading
||
materialsError
?
[]
:
materials
}
renderEmptyState=
{
()
=>
(
<
div
className=
"grid min-h-24 place-items-center px-4 py-5 text-sm text-muted"
>
{
materialsLoading
?
<
Spinner
size=
"sm"
/>
:
materialsError
||
'没有找到可选择的审计材料'
}
</
div
>
)
}
>
{
(
material
)
=>
(
<
ListBox
.
Item
id=
{
material
.
id
}
textValue=
{
`${material.materialTitle} ${material.fileAsset?.name || ''}`
}
>
onSelectionChange={handleMaterialSelection}
renderOption={(material) => (
<span className="min-w-0">
<Label className="block truncate" title={material.materialTitle}>
{material.materialTitle}
...
...
@@ -260,13 +327,8 @@ export default function ProblemRectificationMaterialField({
{formatMaterialDescription(material)}
</span>
</span>
<
ListBox
.
ItemIndicator
/>
</
ListBox
.
Item
>
)}
</
ListBox
>
</
Autocomplete
.
Filter
>
</
Autocomplete
.
Popover
>
</
Autocomplete
>
/>
{materialsError ? (
<Alert status="danger">
...
...
@@ -285,9 +347,11 @@ export default function ProblemRectificationMaterialField({
<Tabs.Panel className="pt-3" id={LOCAL_UPLOAD_TAB}>
<div className="space-y-3">
<FileUploadField
description=
"上传后先保留为详情草稿,保存详情时同步写入审计材料中心。"
fileName=
{
localFileAsset
?.
name
||
''
}
description={multiple
? '可一次选择多个文件;上传后先保留为详情草稿,保存详情时逐个同步写入审计材料中心。'
: '上传后先保留为详情草稿,保存详情时同步写入审计材料中心。'}
isDisabled={uploading}
multiple={multiple}
triggerLabel={uploading ? '上传中...' : '选择本地文件'}
onFilesChange={handleLocalUpload}
/>
...
...
src/pages/problem_rectification/problem-rectification.business-flow.md
View file @
426a2639
...
...
@@ -146,7 +146,7 @@
-
`am_material`
:审计材料中心共享主档。问题清单详情可查询已有主档;从本地上传证明材料并保存详情时同步创建主档
问题记录仍在
`metadata.rectification_proof_materials`
保存
`{name,type,size,url}`
文件快照,不保存材料主档
`_id`
,不建立自动联动关系
。
问题记录仍在
`metadata.rectification_proof_materials`
保存
文件快照,不保存材料主档
`_id`
,不建立自动联动关系。字段单值时保存
`{name,type,size,url}`
,字段配置为多值时保存上述文件对象数组
。
### 4.4 流程辅助对象
...
...
@@ -706,7 +706,7 @@
-
原主表中的多值选择
`responsible_department`
在旧实现中被转成逗号拼接文本
-
`fill_in_the_people`
、
`review_people`
继续保存为用户
`_id[]`
-
`rectification_proof_materials`
作为文件对象原样保留
-
`rectification_proof_materials`
按线上字段的单值或多值形态保留文件对象或文件对象数组
-
归档是快照,后续原问题记录再变化,不应反向污染归档结果
-
归档明细导入失败时不创建归档批次;归档批次创建失败时应提示用户归档未完整完成,避免出现无法从批次入口追溯的孤立明细
...
...
@@ -756,11 +756,11 @@
整改证明材料交互限定在问题清单详情弹框,不改变整改处理页、整改流程表格和归档详情:
1.
“材料中心”首次读取当前用户可访问的最近 20 条
`am_material`
,支持按材料标题或文件名远程搜索;选中后仅把
`metadata.file_asset`
快照写入详情草稿。
2.
“本地上传”选择文件后立即调用
`/upload`
,上传期间禁止保存
;成功后将文件对象写入详情草稿,并提示保存后同步到材料中心
。
3.
用户点击详情“保存”时,
先创建
`am_material`
,写入
`source_app = issue_rectification`
、当前用户
`_id`
和 ISO 上传时间,再
更新
`problem_rectification.metadata.rectification_proof_materials`
。
1.
“材料中心”首次读取当前用户可访问的最近 20 条
`am_material`
,支持按材料标题或文件名远程搜索;选中后仅把
`metadata.file_asset`
快照写入详情草稿。
控件运行时读取字段
`multiple`
:单值选择会替换原附件,多值选择会追加并按文件地址去重,附件可逐项删除。
2.
“本地上传”选择文件后立即调用
`/upload`
,上传期间禁止保存
及修改当前附件;单值只接收一个文件,多值可一次选择或拖入多个文件。批量上传成功项追加到详情草稿,失败项显示文件级汇总错误并允许重新选择
。
3.
用户点击详情“保存”时,
为每个本次新增且仍保留在草稿中的本地文件分别创建
`am_material`
,写入
`source_app = issue_rectification`
、当前用户
`_id`
和 ISO 上传时间;所有待建档项成功后,再按单值对象或多值对象数组
更新
`problem_rectification.metadata.rectification_proof_materials`
。
4.
用户取消编辑时不创建材料主档;选择已有材料不重复上传、不创建新主档。
5.
材料创建失败时保留编辑草稿且不更新问题;材料创建成功但问题更新失败时保留已建档标记,再次保存只
重试问题更新,避免重复材料。
5.
任一材料创建失败时保留编辑草稿且不更新问题;同一批次已创建成功的材料,以及材料创建成功但问题更新失败的情况,均保留逐项建档标记,再次保存只创建尚未成功的材料并
重试问题更新,避免重复材料。
6.
清空或替换问题附件不删除材料中心主档或底层文件;已有材料后续修改或软删除不联动历史问题快照。
建议字段分组:
...
...
src/services/record-api.md
View file @
426a2639
...
...
@@ -269,7 +269,7 @@ Query:`calculated=true|false`
## 5. 问题详情与审计材料中心接入
问题清单详情弹框中的
`metadata.rectification_proof_materials`
支持从统一材料池选择已有文件,或上传本地文件后同步建档。整改处理页、整改流程表格和归档详情不使用本节流程。
问题清单详情弹框中的
`metadata.rectification_proof_materials`
支持从统一材料池选择已有文件,或上传本地文件后同步建档。
控件必须运行时读取 Subject 字段的
`multiple`
配置:单值字段保存一个文件对象,多值字段保存文件对象数组。
整改处理页、整改流程表格和归档详情不使用本节流程。
### 5.1 查询已有材料
...
...
@@ -301,12 +301,12 @@ Query:`calculated=true|false`
}
```
搜索关键词必须先转义正则特殊字符。选中材料后,只将
`metadata.file_asset`
的
`{name,type,size,url}`
快照写入问题详情草稿,不保存
`am_material`
`_id`
。
搜索关键词必须先转义正则特殊字符。选中材料后,只将
`metadata.file_asset`
的
`{name,type,size,url}`
快照写入问题详情草稿,不保存
`am_material`
`_id`
。
单值字段选择后替换原值;多值字段允许连续选择并追加,按文件地址去重,支持逐项删除。
### 5.2 本地上传并写入材料中心
1.
选择文件后立即调用
`POST /upload`
,将返回结果规范化为
`{name,type,size,url}`
并保留在详情草稿。
2.
用户点击详情“保存”时调用
`POST /record`
创建材料主档:
1.
选择文件后立即调用
`POST /upload`
,将返回结果规范化为
`{name,type,size,url}`
并保留在详情草稿。
单值字段只接收一个文件并替换原值;多值字段允许一次选择或拖入多个文件,每个文件分别上传,成功项追加到草稿,失败项保留错误提示并可重新选择。
2.
用户点击详情“保存”时
,为每个本次本地上传且仍保留在草稿中的文件分别
调用
`POST /record`
创建材料主档:
```
json
{
...
...
@@ -328,6 +328,6 @@ Query:`calculated=true|false`
}
```
3.
材料主档创建成功后,再调用
`PUT /record/:name`
更新问题记录的
`metadata.rectification_proof_materials`
。
4.
材料创建失败时不更新问题记录;材料创建成功但问题更新失败时,当前编辑会话记住已创建的材料,重试保存
不得重复创建。
5.
取消详情编辑不创建
`am_material`
;
已经上传但未建档的二进制文件遵循上传服务自身的清理策略。
3.
所有待建档材料创建成功后,再调用
`PUT /record/:name`
更新问题记录的
`metadata.rectification_proof_materials`
,单值写对象,多值写对象数组
。
4.
任一材料创建失败时不更新问题记录;一批材料中已创建成功的条目和“材料已创建、问题更新失败”的条目都在当前编辑会话中保留建档标记,重试保存只处理尚未建档的条目,
不得重复创建。
5.
用户从草稿逐项删除本地附件后,该附件不再参与材料建档;取消详情编辑不创建
`am_material`
。
已经上传但未建档的二进制文件遵循上传服务自身的清理策略。
src/services/subjectsDefinition.md
View file @
426a2639
...
...
@@ -78,7 +78,7 @@ type Record = {
-
`number`
:数字
-
`boolean`
:
`true / false`
-
`select`
:传选项的存储值,不传显示文案
-
`file`
:
对象,单文件时建议结构为
`{ name, type, size, url }`
-
`file`
:
单值字段保存
`{ name, type, size, url }`
,
`multiple=true`
时保存上述文件对象数组
-
`object`
:对象或对象数组,前端优先使用结构化表单编辑,无现成交互时再退回 JSON 校验方案
### 2.4 `reference`
...
...
@@ -187,7 +187,7 @@ type Record = {
-
新增驾驶舱扩展字段当前在线上 Subject 中大多仍为
`text`
类型,前端工作台按标准值引导录入,用于支持后续严重程度、问题分类、问题原因和整改质效分析
-
其余
`select`
类型的可选值必须以线上 Subject 配置中的
`settings.options`
为准,禁止在页面内硬编码
-
`problem_discovery_situation`
曾在历史文档和归档对象中出现,但本次主对象字段定义未包含;新页面只应在 Subject View 或线上字段配置存在时展示,不能作为
`problem_rectification`
固定字段依赖
-
问题清单详情弹框中的
`rectification_proof_materials`
支持从
`am_material.metadata.file_asset`
复制文件快照,或上传本地文件后创建
`am_material`
。
问题字段仍只保存
`{name,type,size,url}`
,
不保存材料主档
`_id`
,材料后续修改或软删除不反向联动问题记录
-
问题清单详情弹框中的
`rectification_proof_materials`
支持从
`am_material.metadata.file_asset`
复制文件快照,或上传本地文件后创建
`am_material`
。
控件运行时读取线上字段的
`multiple`
:当前定义为单值时保存
`{name,type,size,url}`
;若后续配置为多值则保存文件对象数组,并支持追加、逐项删除及为每个本地文件分别建档。问题字段
不保存材料主档
`_id`
,材料后续修改或软删除不反向联动问题记录
-
`department_rectification_items`
用于新转发流程的部门级整改明细。无论单条还是多条推送,每个问题都按责任部门生成明细;单条问题即使只有 1 个责任部门,也生成 1 条明细。已推送过的问题再次推送时,必须保留原部门明细记录和
`item_id`
,只在本次参与推送的原明细行上更新填写人、审批人、流程状态和待办 ID;未参与本次推送的原明细行保持不变,不因弹窗删除部门配置而从父记录移除。转发后
`rectification_progress`
、
`rectification_situation`
、
`the_time_of_rectification`
、
`rectification_proof_materials`
、
`audit_status`
、
`final_audit_status`
以明细为事实来源,父字段仅做兼容汇总展示;整改进度、审批状态和最终审批状态按部门明细中最落后的状态汇总,例如“整改中”优先于“已整改”,“未审批”优先于“已审批”
-
`problem_rectification_delay_request`
为历史问题级单次延期申请嵌套对象,与
`department_rectification_items`
平级;新发起延期默认按部门写入
`problem_rectification_delay_request_items`
-
`problem_rectification_delay_request_items`
为部门级延期申请明细数组;每个责任部门 1 条延期申请,独立保存填写人、一级审批人、最终审批人、申请完成时间、延期事由、状态和待办 ID
...
...
@@ -308,7 +308,7 @@ type Record = {
| 材料分类 |
`archive_category`
| select | 否 | 问题详情不写入 |
| 所属周期/年份 |
`archive_year`
| text | 否 | 问题详情不写入 |
问题详情选择已有材料时读取
`material_title`
与
`file_asset`
;搜索范围为
`metadata.material_title`
和
`metadata.file_asset.name`
。列表只展示当前登录用户通过 Record 权限可读取且
`file_asset.url`
有效的记录。
问题详情选择已有材料时读取
`material_title`
与
`file_asset`
;搜索范围为
`metadata.material_title`
和
`metadata.file_asset.name`
。列表只展示当前登录用户通过 Record 权限可读取且
`file_asset.url`
有效的记录。
业务文件字段为多值时,每个本地上传文件仍分别创建一条
`am_material`
,业务记录保存对应的文件快照数组。
## 3.3 问题整改归档明细
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment