Commit 6f750b02 authored by David Yang's avatar David Yang

fix: 优化用户字段回显和选中项排序

parent 2482eb15
......@@ -2,12 +2,18 @@ import XinyuanAutocomplete from '@/components/XinyuanAutocomplete';
import { Label } from '@heroui/react';
import { loadUser, queryUsers } from '@/services/DataService';
import { normalizeUserLabel, toArray } from '@/utils/subjectView';
import { useEffect, useMemo, useState } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
function getUserId(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({
value,
onChange,
......@@ -20,9 +26,23 @@ export default function UserSearchSelect({
const [keyword, setKeyword] = useState('');
const [users, setUsers] = useState([]);
const [profiles, setProfiles] = useState({});
const [failedLookupIds, setFailedLookupIds] = useState(new Set());
const [loading, setLoading] = useState(false);
const lookedUpIdsRef = useRef(new Set());
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(() => {
let cancelled = false;
setLoading(true);
......@@ -52,30 +72,77 @@ export default function UserSearchSelect({
}, [keyword]);
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;
let cancelled = false;
Promise.all(missing.map((id) => loadUser(id).catch(() => null))).then((items) => {
if (cancelled) return;
missing.forEach((id) => lookedUpIdsRef.current.add(id));
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) => ({
...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; };
}, [profiles, selectedIds]);
const options = useMemo(() => {
const merged = new Map(users.map((user) => [getUserId(user), user]));
selectedIds.forEach((id) => merged.set(id, profiles[id] || { _id: id, username: id }));
return [...merged.values()].map((user) => ({
const usersById = new Map(users.map((user) => [getUserId(user), user]));
const selectedIdSet = new Set(selectedIds);
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),
label: normalizeUserLabel(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
.map((id) => normalizeUserLabel(profiles[id] || users.find((user) => getUserId(user) === id) || id))
.map((id) => optionById.get(id)?.label)
.filter(Boolean)
.join('、');
return (
......
import { useEffect, useMemo, useState } from 'react';
import { useEffect, useState } from 'react';
import clsx from 'clsx';
import {
Alert,
......@@ -7,12 +7,12 @@ import {
Checkbox,
Chip,
Input,
Label,
TextArea,
} from '@heroui/react';
import FormDatePicker from '@/components/FormDatePicker';
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 XinyuanAutocomplete from '@/components/XinyuanAutocomplete';
import IntegrityRiskMaterialField from '@/pages/integrity_risk_prevention/IntegrityRiskMaterialField';
......@@ -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 }) {
const values = multiple ? toArray(value).map(String) : [String(value || '')];
const optionValueMap = new Map(
......@@ -370,10 +279,11 @@ function ControlledFieldEditor({
if (fieldType === 'user') {
return (
<UserSelector
<UserSearchSelect
value={value}
onChange={onChange}
multiple={Boolean(fieldDef?.multiple)}
placeholder="请选择用户"
variant={fieldVariant}
/>
);
......@@ -707,10 +617,11 @@ export default function TailwindEditableField({
if (fieldType === 'user') {
return (
<div className="space-y-3">
<UserSelector
<UserSearchSelect
value={draft}
onChange={setDraft}
multiple={fieldDef?.multiple}
placeholder="请选择用户"
variant={fieldVariant}
/>
<div className="flex justify-end gap-2">
......
......@@ -70,6 +70,9 @@
`reference_record_id` 与当前页面兼容写入的 `reference_id` 需保持注意。
19. 过程表列表不直接展示内部嵌套字段 `department_control_items`;部门展开行中的用户和引用
字段仍保存 ID,但列表批量解析并显示用户名称、记录标题,解析失败时才回退显示 ID。
20. 详情编辑中的用户字段会按已保存的用户 ID 补查用户信息,不受首屏用户列表范围限制;
展开下拉框时已选用户始终置顶并保持原选中顺序。补查失败时保留原 ID 选择值并显示不可用状态,
不因回显失败自动清空;保存时仍只写入 `_id` / `_id[]`。
### 2.1 Subject View 授权
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment