Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in / Register
Toggle navigation
I
integrity_risk_prevention
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
integrity_risk_prevention
Commits
6f750b02
Commit
6f750b02
authored
Sep 10, 2026
by
David Yang
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix: 优化用户字段回显和选中项排序
parent
2482eb15
Show whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
87 additions
and
106 deletions
+87
-106
UserSearchSelect.jsx
src/components/UserSearchSelect.jsx
+77
-10
TailwindEditableField.jsx
...s/RecordDetailDrawer/components/TailwindEditableField.jsx
+7
-96
integrity-risk-prevention.business-flow.md
...isk_prevention/integrity-risk-prevention.business-flow.md
+3
-0
No files found.
src/components/UserSearchSelect.jsx
View file @
6f750b02
...
@@ -2,12 +2,18 @@ import XinyuanAutocomplete from '@/components/XinyuanAutocomplete';
...
@@ -2,12 +2,18 @@ import XinyuanAutocomplete from '@/components/XinyuanAutocomplete';
import
{
Label
}
from
'@heroui/react'
;
import
{
Label
}
from
'@heroui/react'
;
import
{
loadUser
,
queryUsers
}
from
'@/services/DataService'
;
import
{
loadUser
,
queryUsers
}
from
'@/services/DataService'
;
import
{
normalizeUserLabel
,
toArray
}
from
'@/utils/subjectView'
;
import
{
normalizeUserLabel
,
toArray
}
from
'@/utils/subjectView'
;
import
{
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
{
useEffect
,
useMemo
,
use
Ref
,
use
State
}
from
'react'
;
function
getUserId
(
user
)
{
function
getUserId
(
user
)
{
return
String
(
typeof
user
===
'object'
?
user
?.
_id
||
user
?.
id
||
user
?.
name
||
''
:
user
||
''
);
return
String
(
typeof
user
===
'object'
?
user
?.
_id
||
user
?.
id
||
user
?.
name
||
''
:
user
||
''
);
}
}
function
hasUserPresentation
(
user
)
{
const
id
=
getUserId
(
user
);
const
label
=
normalizeUserLabel
(
user
);
return
Boolean
(
id
&&
label
&&
label
!==
id
);
}
export
default
function
UserSearchSelect
({
export
default
function
UserSearchSelect
({
value
,
value
,
onChange
,
onChange
,
...
@@ -20,9 +26,23 @@ export default function UserSearchSelect({
...
@@ -20,9 +26,23 @@ export default function UserSearchSelect({
const
[
keyword
,
setKeyword
]
=
useState
(
''
);
const
[
keyword
,
setKeyword
]
=
useState
(
''
);
const
[
users
,
setUsers
]
=
useState
([]);
const
[
users
,
setUsers
]
=
useState
([]);
const
[
profiles
,
setProfiles
]
=
useState
({});
const
[
profiles
,
setProfiles
]
=
useState
({});
const
[
failedLookupIds
,
setFailedLookupIds
]
=
useState
(
new
Set
());
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
lookedUpIdsRef
=
useRef
(
new
Set
());
const
selectedIds
=
useMemo
(()
=>
toArray
(
value
).
map
(
getUserId
).
filter
(
Boolean
),
[
value
]);
const
selectedIds
=
useMemo
(()
=>
toArray
(
value
).
map
(
getUserId
).
filter
(
Boolean
),
[
value
]);
useEffect
(()
=>
{
const
selectedProfiles
=
toArray
(
value
).
filter
(
(
item
)
=>
typeof
item
===
'object'
&&
hasUserPresentation
(
item
),
);
if
(
!
selectedProfiles
.
length
)
return
;
setProfiles
((
current
)
=>
({
...
current
,
...
Object
.
fromEntries
(
selectedProfiles
.
map
((
user
)
=>
[
getUserId
(
user
),
user
])),
}));
},
[
value
]);
useEffect
(()
=>
{
useEffect
(()
=>
{
let
cancelled
=
false
;
let
cancelled
=
false
;
setLoading
(
true
);
setLoading
(
true
);
...
@@ -52,30 +72,77 @@ export default function UserSearchSelect({
...
@@ -52,30 +72,77 @@ export default function UserSearchSelect({
},
[
keyword
]);
},
[
keyword
]);
useEffect
(()
=>
{
useEffect
(()
=>
{
const
missing
=
selectedIds
.
filter
((
id
)
=>
!
profiles
[
id
]);
const
missing
=
selectedIds
.
filter
(
(
id
)
=>
!
hasUserPresentation
(
profiles
[
id
])
&&
!
lookedUpIdsRef
.
current
.
has
(
id
),
);
if
(
!
missing
.
length
)
return
undefined
;
if
(
!
missing
.
length
)
return
undefined
;
let
cancelled
=
false
;
let
cancelled
=
false
;
Promise
.
all
(
missing
.
map
((
id
)
=>
loadUser
(
id
).
catch
(()
=>
null
))).
then
((
items
)
=>
{
missing
.
forEach
((
id
)
=>
lookedUpIdsRef
.
current
.
add
(
id
));
if
(
cancelled
)
return
;
Promise
.
all
(
missing
.
map
(
async
(
id
)
=>
{
try
{
const
result
=
await
loadUser
(
id
);
const
user
=
result
?.
user
||
result
;
return
hasUserPresentation
(
user
)
?
{
id
,
user
}
:
{
id
,
user
:
null
};
}
catch
{
return
{
id
,
user
:
null
};
}
})).
then
((
items
)
=>
{
if
(
cancelled
)
{
missing
.
forEach
((
id
)
=>
lookedUpIdsRef
.
current
.
delete
(
id
));
return
;
}
const
loadedUsers
=
items
.
filter
((
item
)
=>
item
.
user
);
if
(
loadedUsers
.
length
)
{
setProfiles
((
current
)
=>
({
setProfiles
((
current
)
=>
({
...
current
,
...
current
,
...
Object
.
fromEntries
(
items
.
filter
(
Boolean
).
map
((
user
)
=>
[
getUserId
(
user
),
user
])),
...
Object
.
fromEntries
(
loadedUsers
.
map
(({
user
}
)
=>
[
getUserId
(
user
),
user
])),
}));
}));
}
const
failedIds
=
items
.
filter
((
item
)
=>
!
item
.
user
).
map
((
item
)
=>
item
.
id
);
if
(
failedIds
.
length
)
{
setFailedLookupIds
((
current
)
=>
new
Set
([...
current
,
...
failedIds
]));
}
});
});
return
()
=>
{
cancelled
=
true
;
};
return
()
=>
{
cancelled
=
true
;
};
},
[
profiles
,
selectedIds
]);
},
[
profiles
,
selectedIds
]);
const
options
=
useMemo
(()
=>
{
const
options
=
useMemo
(()
=>
{
const
merged
=
new
Map
(
users
.
map
((
user
)
=>
[
getUserId
(
user
),
user
]));
const
usersById
=
new
Map
(
users
.
map
((
user
)
=>
[
getUserId
(
user
),
user
]));
selectedIds
.
forEach
((
id
)
=>
merged
.
set
(
id
,
profiles
[
id
]
||
{
_id
:
id
,
username
:
id
}));
const
selectedIdSet
=
new
Set
(
selectedIds
);
return
[...
merged
.
values
()].
map
((
user
)
=>
({
const
selectedOptions
=
selectedIds
.
map
((
id
)
=>
{
const
user
=
[
profiles
[
id
],
usersById
.
get
(
id
)].
find
(
hasUserPresentation
);
if
(
user
)
{
return
{
id
,
label
:
normalizeUserLabel
(
user
),
description
:
user
.
username
||
id
,
};
}
return
{
id
,
label
:
failedLookupIds
.
has
(
id
)
?
'用户信息不可用'
:
'正在加载用户信息…'
,
description
:
id
,
};
});
const
unselectedOptions
=
users
.
filter
((
user
)
=>
!
selectedIdSet
.
has
(
getUserId
(
user
)))
.
map
((
user
)
=>
({
id
:
getUserId
(
user
),
id
:
getUserId
(
user
),
label
:
normalizeUserLabel
(
user
),
label
:
normalizeUserLabel
(
user
),
description
:
user
.
username
||
getUserId
(
user
),
description
:
user
.
username
||
getUserId
(
user
),
}));
}));
},
[
profiles
,
selectedIds
,
users
]);
return
[...
selectedOptions
,
...
unselectedOptions
];
},
[
failedLookupIds
,
profiles
,
selectedIds
,
users
]);
const
optionById
=
new
Map
(
options
.
map
((
option
)
=>
[
option
.
id
,
option
]));
const
valueText
=
selectedIds
const
valueText
=
selectedIds
.
map
((
id
)
=>
normalizeUserLabel
(
profiles
[
id
]
||
users
.
find
((
user
)
=>
getUserId
(
user
)
===
id
)
||
id
))
.
map
((
id
)
=>
optionById
.
get
(
id
)?.
label
)
.
filter
(
Boolean
)
.
join
(
'、'
);
.
join
(
'、'
);
return
(
return
(
...
...
src/pages/RecordDetailDrawer/components/TailwindEditableField.jsx
View file @
6f750b02
import
{
useEffect
,
use
Memo
,
use
State
}
from
'react'
;
import
{
useEffect
,
useState
}
from
'react'
;
import
clsx
from
'clsx'
;
import
clsx
from
'clsx'
;
import
{
import
{
Alert
,
Alert
,
...
@@ -7,12 +7,12 @@ import {
...
@@ -7,12 +7,12 @@ import {
Checkbox
,
Checkbox
,
Chip
,
Chip
,
Input
,
Input
,
Label
,
TextArea
,
TextArea
,
}
from
'@heroui/react'
;
}
from
'@heroui/react'
;
import
FormDatePicker
from
'@/components/FormDatePicker'
;
import
FormDatePicker
from
'@/components/FormDatePicker'
;
import
FileUploadField
from
'@/components/FileUploadField'
;
import
FileUploadField
from
'@/components/FileUploadField'
;
import
{
queryUsers
,
uploadFile
}
from
'@/services/DataService'
;
import
UserSearchSelect
from
'@/components/UserSearchSelect'
;
import
{
uploadFile
}
from
'@/services/DataService'
;
import
ReferenceSearchSelect
from
'@/components/ReferenceSearchSelect'
;
import
ReferenceSearchSelect
from
'@/components/ReferenceSearchSelect'
;
import
XinyuanAutocomplete
from
'@/components/XinyuanAutocomplete'
;
import
XinyuanAutocomplete
from
'@/components/XinyuanAutocomplete'
;
import
IntegrityRiskMaterialField
from
'@/pages/integrity_risk_prevention/IntegrityRiskMaterialField'
;
import
IntegrityRiskMaterialField
from
'@/pages/integrity_risk_prevention/IntegrityRiskMaterialField'
;
...
@@ -165,97 +165,6 @@ function FieldValue({ value, fieldDef }) {
...
@@ -165,97 +165,6 @@ function FieldValue({ value, fieldDef }) {
);
);
}
}
function
UserSelector
({
value
,
onChange
,
multiple
,
variant
})
{
const
[
keyword
,
setKeyword
]
=
useState
(
''
);
const
[
options
,
setOptions
]
=
useState
([]);
const
selectedIds
=
useMemo
(
()
=>
toArray
(
value
).
map
((
item
)
=>
item
?.
_id
||
item
).
filter
(
Boolean
),
[
value
],
);
useEffect
(()
=>
{
let
cancelled
=
false
;
const
loadOptions
=
async
()
=>
{
const
filter
=
keyword
.
trim
()
?
{
$or
:
[
{
username
:
{
$regex
:
keyword
.
trim
(),
$options
:
'i'
}
},
{
display_name
:
{
$regex
:
keyword
.
trim
(),
$options
:
'i'
}
},
],
}
:
{};
try
{
const
result
=
await
queryUsers
(
0
,
20
,
filter
);
if
(
!
cancelled
)
{
setOptions
(
result
?.
users
||
[]);
}
}
catch
(
error
)
{
if
(
!
cancelled
)
{
setOptions
([]);
}
}
};
loadOptions
();
return
()
=>
{
cancelled
=
true
;
};
},
[
keyword
]);
const
handleSelectionChange
=
(
keys
)
=>
{
if
(
!
multiple
)
{
onChange
(
keys
?
String
(
keys
)
:
null
);
return
;
}
const
nextIds
=
keys
===
'all'
?
options
.
map
((
user
)
=>
user
.
_id
).
filter
(
Boolean
)
:
Array
.
from
(
keys
).
map
(
String
);
onChange
(
nextIds
);
};
const
autocompleteOptions
=
options
.
map
((
user
)
=>
({
id
:
user
.
_id
,
label
:
user
.
display_name
||
user
.
username
,
description
:
user
.
username
,
}));
const
selectionProps
=
multiple
?
{
selectedKeys
:
new
Set
(
selectedIds
)
}
:
{
selectedKey
:
selectedIds
[
0
]
||
null
};
const
valueText
=
toArray
(
value
)
.
map
((
item
)
=>
normalizeUserLabel
(
item
))
.
filter
(
Boolean
)
.
join
(
'、'
);
return
(
<
XinyuanAutocomplete
{
...
selectionProps
}
aria
-
label=
"用户"
emptyText=
"暂无用户"
inputValue=
{
keyword
}
options=
{
autocompleteOptions
}
placeholder=
"请选择用户"
searchPlaceholder=
"搜索用户名或显示名"
selectionMode=
{
multiple
?
'multiple'
:
'single'
}
shouldFilter=
{
false
}
valueText=
{
valueText
||
'请选择用户'
}
variant=
{
variant
}
onInputChange=
{
setKeyword
}
onSelectionChange=
{
handleSelectionChange
}
renderOption=
{
(
option
)
=>
(
<
span
className=
"min-w-0"
>
<
Label
className=
"block truncate"
>
{
option
.
label
}
</
Label
>
<
span
className=
"block truncate text-xs text-muted"
>
{
option
.
description
}
</
span
>
</
span
>
)
}
/>
);
}
function
FieldOptionSelect
({
multiple
,
onChange
,
options
,
value
,
variant
})
{
function
FieldOptionSelect
({
multiple
,
onChange
,
options
,
value
,
variant
})
{
const
values
=
multiple
?
toArray
(
value
).
map
(
String
)
:
[
String
(
value
||
''
)];
const
values
=
multiple
?
toArray
(
value
).
map
(
String
)
:
[
String
(
value
||
''
)];
const
optionValueMap
=
new
Map
(
const
optionValueMap
=
new
Map
(
...
@@ -370,10 +279,11 @@ function ControlledFieldEditor({
...
@@ -370,10 +279,11 @@ function ControlledFieldEditor({
if
(
fieldType
===
'user'
)
{
if
(
fieldType
===
'user'
)
{
return
(
return
(
<
UserSe
lector
<
UserSe
archSelect
value=
{
value
}
value=
{
value
}
onChange=
{
onChange
}
onChange=
{
onChange
}
multiple=
{
Boolean
(
fieldDef
?.
multiple
)
}
multiple=
{
Boolean
(
fieldDef
?.
multiple
)
}
placeholder=
"请选择用户"
variant=
{
fieldVariant
}
variant=
{
fieldVariant
}
/>
/>
);
);
...
@@ -707,10 +617,11 @@ export default function TailwindEditableField({
...
@@ -707,10 +617,11 @@ export default function TailwindEditableField({
if
(
fieldType
===
'user'
)
{
if
(
fieldType
===
'user'
)
{
return
(
return
(
<
div
className=
"space-y-3"
>
<
div
className=
"space-y-3"
>
<
UserSe
lector
<
UserSe
archSelect
value=
{
draft
}
value=
{
draft
}
onChange=
{
setDraft
}
onChange=
{
setDraft
}
multiple=
{
fieldDef
?.
multiple
}
multiple=
{
fieldDef
?.
multiple
}
placeholder=
"请选择用户"
variant=
{
fieldVariant
}
variant=
{
fieldVariant
}
/>
/>
<
div
className=
"flex justify-end gap-2"
>
<
div
className=
"flex justify-end gap-2"
>
...
...
src/pages/integrity_risk_prevention/integrity-risk-prevention.business-flow.md
View file @
6f750b02
...
@@ -70,6 +70,9 @@
...
@@ -70,6 +70,9 @@
`reference_record_id`
与当前页面兼容写入的
`reference_id`
需保持注意。
`reference_record_id`
与当前页面兼容写入的
`reference_id`
需保持注意。
19.
过程表列表不直接展示内部嵌套字段
`department_control_items`
;部门展开行中的用户和引用
19.
过程表列表不直接展示内部嵌套字段
`department_control_items`
;部门展开行中的用户和引用
字段仍保存 ID,但列表批量解析并显示用户名称、记录标题,解析失败时才回退显示 ID。
字段仍保存 ID,但列表批量解析并显示用户名称、记录标题,解析失败时才回退显示 ID。
20.
详情编辑中的用户字段会按已保存的用户 ID 补查用户信息,不受首屏用户列表范围限制;
展开下拉框时已选用户始终置顶并保持原选中顺序。补查失败时保留原 ID 选择值并显示不可用状态,
不因回显失败自动清空;保存时仍只写入
`_id`
/
`_id[]`
。
### 2.1 Subject View 授权
### 2.1 Subject View 授权
...
...
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