Commit c4324c3a authored by drigle's avatar drigle

fix: show selected audit references

parent c61a6514
...@@ -6,6 +6,7 @@ import ReferenceSearchSelect from './ReferenceSearchSelect'; ...@@ -6,6 +6,7 @@ import ReferenceSearchSelect from './ReferenceSearchSelect';
import { ControlPointPreviewFields } from './ControlPointPreviewDialog'; import { ControlPointPreviewFields } from './ControlPointPreviewDialog';
import { import {
AUDIT_SUBJECTS, AUDIT_SUBJECTS,
getReferenceRecordsByIds,
normalizeRecordId, normalizeRecordId,
searchReferenceRecords, searchReferenceRecords,
searchTags, searchTags,
...@@ -52,6 +53,18 @@ export default function UnifiedControlPointDialog({ ...@@ -52,6 +53,18 @@ export default function UnifiedControlPointDialog({
const loadDepartmentOptions = useCallback((keyword) => searchReferenceRecords('department', keyword), []); const loadDepartmentOptions = useCallback((keyword) => searchReferenceRecords('department', keyword), []);
const loadRegulationOptions = useCallback((keyword) => searchReferenceRecords('internal_regulation', keyword), []); const loadRegulationOptions = useCallback((keyword) => searchReferenceRecords('internal_regulation', keyword), []);
const loadIndicatorOptions = useCallback((keyword) => searchReferenceRecords(AUDIT_SUBJECTS.INDICATOR, keyword), []); const loadIndicatorOptions = useCallback((keyword) => searchReferenceRecords(AUDIT_SUBJECTS.INDICATOR, keyword), []);
const resolveDepartmentIds = useCallback(
(ids) => getReferenceRecordsByIds('department', ids),
[],
);
const resolveRegulationIds = useCallback(
(ids) => getReferenceRecordsByIds('internal_regulation', ids),
[],
);
const resolveIndicatorIds = useCallback(
(ids) => getReferenceRecordsByIds(AUDIT_SUBJECTS.INDICATOR, ids),
[],
);
useEffect(() => { useEffect(() => {
if (!open) return; if (!open) return;
...@@ -130,13 +143,13 @@ export default function UnifiedControlPointDialog({ ...@@ -130,13 +143,13 @@ export default function UnifiedControlPointDialog({
<Modal.Body className="grid gap-5 sm:grid-cols-2"> <Modal.Body className="grid gap-5 sm:grid-cols-2">
<TextField className="rc-form-field sm:col-span-2" isRequired value={form.name} onChange={(value) => change('name', value)}><Label>名称</Label><Input placeholder="请输入控制点名称" variant="secondary" /></TextField> <TextField className="rc-form-field sm:col-span-2" isRequired value={form.name} onChange={(value) => change('name', value)}><Label>名称</Label><Input placeholder="请输入控制点名称" variant="secondary" /></TextField>
<TextField className="rc-form-field sm:col-span-2" value={form.description} onChange={(value) => change('description', value)}><Label>描述信息</Label><TextArea placeholder="请输入控制点描述" rows={4} variant="secondary" /></TextField> <TextField className="rc-form-field sm:col-span-2" value={form.description} onChange={(value) => change('description', value)}><Label>描述信息</Label><TextArea placeholder="请输入控制点描述" rows={4} variant="secondary" /></TextField>
<div className="rc-form-field"><Label>{requiredLabel('责任人', true)}</Label><ReferenceSearchSelect value={form.owner} onChange={(value) => change('owner', value)} loadOptions={loadDepartmentOptions} placeholder="搜索责任部门" /></div> <div className="rc-form-field"><Label>{requiredLabel('责任人', true)}</Label><ReferenceSearchSelect value={form.owner} onChange={(value) => change('owner', value)} loadOptions={loadDepartmentOptions} resolveMissingIds={resolveDepartmentIds} placeholder="搜索责任部门" /></div>
<div className="rc-form-field"><Label>相关标签</Label><ReferenceSearchSelect multiple value={form.related_tags} onChange={(value) => change('related_tags', value)} loadOptions={searchTags} placeholder="搜索并添加标签" /></div> <div className="rc-form-field"><Label>相关标签</Label><ReferenceSearchSelect multiple value={form.related_tags} onChange={(value) => change('related_tags', value)} loadOptions={searchTags} placeholder="搜索并添加标签" /></div>
<div className="rc-form-field sm:col-span-2"><Label>相关制度</Label><ReferenceSearchSelect multiple value={form.related_regulations} onChange={(value) => change('related_regulations', value)} loadOptions={loadRegulationOptions} placeholder="搜索并添加制度" /></div> <div className="rc-form-field sm:col-span-2"><Label>相关制度</Label><ReferenceSearchSelect multiple value={form.related_regulations} onChange={(value) => change('related_regulations', value)} loadOptions={loadRegulationOptions} resolveMissingIds={resolveRegulationIds} placeholder="搜索并添加制度" /></div>
<div className="rc-form-field"><Label>{requiredLabel('控制措施', Boolean(typeField?.required))}</Label><FilterSelect aria-label="控制措施" options={typeOptions} value={form.type} variant="secondary" onChange={(value) => change('type', value)} /></div> <div className="rc-form-field"><Label>{requiredLabel('控制措施', Boolean(typeField?.required))}</Label><FilterSelect aria-label="控制措施" options={typeOptions} value={form.type} variant="secondary" onChange={(value) => change('type', value)} /></div>
{form.type === 'form' ? <TextField className="rc-form-field" value={form.form_label} onChange={(value) => change('form_label', value)}><Label>文本描述</Label><Input placeholder="请输入文本描述" variant="secondary" /></TextField> : null} {form.type === 'form' ? <TextField className="rc-form-field" value={form.form_label} onChange={(value) => change('form_label', value)}><Label>文本描述</Label><Input placeholder="请输入文本描述" variant="secondary" /></TextField> : null}
{form.type === 'attachment' ? <div className="rc-form-field sm:col-span-2"><Label>文件描述</Label><div className="space-y-2">{form.attachment_label.map((label, index) => <div key={index} className="flex items-center gap-2"><TextField aria-label={`文件描述 ${index + 1}`} className="min-w-0 flex-1" value={label} onChange={(value) => { const labels = [...form.attachment_label]; labels[index] = value; change('attachment_label', labels); }}><Input placeholder="请输入文件类别" variant="secondary" /></TextField><Tooltip delay={0}><Button isIconOnly type="button" aria-label="删除文件描述" size="sm" variant="ghost" onPress={() => change('attachment_label', form.attachment_label.filter((_, itemIndex) => itemIndex !== index))}><Delete size={16} /></Button><Tooltip.Content>删除文件描述</Tooltip.Content></Tooltip></div>)}<Button variant="secondary" type="button" onPress={() => change('attachment_label', [...form.attachment_label, ''])}>添加文件描述</Button></div></div> : null} {form.type === 'attachment' ? <div className="rc-form-field sm:col-span-2"><Label>文件描述</Label><div className="space-y-2">{form.attachment_label.map((label, index) => <div key={index} className="flex items-center gap-2"><TextField aria-label={`文件描述 ${index + 1}`} className="min-w-0 flex-1" value={label} onChange={(value) => { const labels = [...form.attachment_label]; labels[index] = value; change('attachment_label', labels); }}><Input placeholder="请输入文件类别" variant="secondary" /></TextField><Tooltip delay={0}><Button isIconOnly type="button" aria-label="删除文件描述" size="sm" variant="ghost" onPress={() => change('attachment_label', form.attachment_label.filter((_, itemIndex) => itemIndex !== index))}><Delete size={16} /></Button><Tooltip.Content>删除文件描述</Tooltip.Content></Tooltip></div>)}<Button variant="secondary" type="button" onPress={() => change('attachment_label', [...form.attachment_label, ''])}>添加文件描述</Button></div></div> : null}
{form.type === 'computed' ? <><div className="rc-form-field"><Label>量化指标</Label><ReferenceSearchSelect value={form.indicator} onChange={(value) => change('indicator', value)} loadOptions={loadIndicatorOptions} placeholder="搜索量化指标" /></div><TextField className="rc-form-field" value={String(form.computed_interval)} onChange={(value) => change('computed_interval', value)}><Label>执行频率(天)</Label><Input type="number" min="1" variant="secondary" /></TextField></> : null} {form.type === 'computed' ? <><div className="rc-form-field"><Label>量化指标</Label><ReferenceSearchSelect value={form.indicator} onChange={(value) => change('indicator', value)} loadOptions={loadIndicatorOptions} resolveMissingIds={resolveIndicatorIds} placeholder="搜索量化指标" /></div><TextField className="rc-form-field" value={String(form.computed_interval)} onChange={(value) => change('computed_interval', value)}><Label>执行频率(天)</Label><Input type="number" min="1" variant="secondary" /></TextField></> : null}
{error ? <FieldError className="sm:col-span-2">{error}</FieldError> : null} {error ? <FieldError className="sm:col-span-2">{error}</FieldError> : null}
</Modal.Body> </Modal.Body>
) : ( ) : (
......
...@@ -15,17 +15,20 @@ export default function ReferenceSearchSelect({ ...@@ -15,17 +15,20 @@ export default function ReferenceSearchSelect({
value, value,
onChange, onChange,
loadOptions, loadOptions,
resolveMissingIds,
multiple = false, multiple = false,
placeholder = '搜索并选择', placeholder = '搜索并选择',
}) { }) {
const [selectedRecordMap, setSelectedRecordMap] = useState(() => new Map());
const valueRecords = useMemo( const valueRecords = useMemo(
() => (Array.isArray(value) ? value : value ? [value] : []) () => (Array.isArray(value) ? value : value ? [value] : [])
.filter((item) => typeof item !== 'string'), .filter((item) => typeof item !== 'string'),
[value], [value],
); );
const [query, setQuery] = useState(''); const [query, setQuery] = useState('');
const [options, setOptions] = useState(valueRecords); const [options, setOptions] = useState([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const selectedIds = useMemo( const selectedIds = useMemo(
() => (Array.isArray(value) ? value : value ? [value] : []) () => (Array.isArray(value) ? value : value ? [value] : [])
.map(normalizeRecordId) .map(normalizeRecordId)
...@@ -33,20 +36,52 @@ export default function ReferenceSearchSelect({ ...@@ -33,20 +36,52 @@ export default function ReferenceSearchSelect({
[value], [value],
); );
const mergeRecords = (records) => {
setSelectedRecordMap((current) => {
const nextMap = new Map(current);
records.filter(Boolean).forEach((record) => {
const id = normalizeRecordId(record);
if (id) nextMap.set(id, record);
});
return nextMap;
});
};
useEffect(() => { useEffect(() => {
if (valueRecords.length > 0) { if (valueRecords.length > 0) {
setOptions((current) => uniqueRecords([...current, ...valueRecords])); mergeRecords(valueRecords);
} }
}, [valueRecords]); }, [valueRecords]);
const missingSelectedIds = selectedIds.filter(
(id) => !selectedRecordMap.get(id),
);
useEffect(() => {
if (!resolveMissingIds || missingSelectedIds.length === 0) return;
let cancelled = false;
resolveMissingIds([...missingSelectedIds]).then((records) => {
if (!cancelled) mergeRecords(records);
}).catch((requestError) => {
if (!cancelled) setError(requestError?.message || '加载已选引用记录失败');
});
return () => {
cancelled = true;
};
}, [missingSelectedIds, resolveMissingIds]);
useEffect(() => { useEffect(() => {
let cancelled = false; let cancelled = false;
const timer = window.setTimeout(async () => { const timer = window.setTimeout(async () => {
setLoading(true); setLoading(true);
setError('');
try { try {
const records = await loadOptions(query); const records = await loadOptions(query);
if (!cancelled) { if (!cancelled) {
setOptions((current) => uniqueRecords([...current, ...(records || [])])); mergeRecords(records);
setOptions(records || []);
} }
} finally { } finally {
if (!cancelled) setLoading(false); if (!cancelled) setLoading(false);
...@@ -59,7 +94,10 @@ export default function ReferenceSearchSelect({ ...@@ -59,7 +94,10 @@ export default function ReferenceSearchSelect({
}; };
}, [loadOptions, query]); }, [loadOptions, query]);
const normalizedOptions = options.map((record) => ({ const normalizedOptions = uniqueRecords([
...selectedIds.map((id) => selectedRecordMap.get(id)).filter(Boolean),
...options,
]).map((record) => ({
id: normalizeRecordId(record), id: normalizeRecordId(record),
label: normalizeRecordLabel(record) || normalizeRecordId(record), label: normalizeRecordLabel(record) || normalizeRecordId(record),
record, record,
...@@ -74,7 +112,9 @@ export default function ReferenceSearchSelect({ ...@@ -74,7 +112,9 @@ export default function ReferenceSearchSelect({
: `已选择 ${selectedIds.length} 项`; : `已选择 ${selectedIds.length} 项`;
return ( return (
<XinyuanAutocomplete <div className="space-y-1">
{error ? <div className="text-sm text-rose-600">{error}</div> : null}
<XinyuanAutocomplete
aria-label={placeholder} aria-label={placeholder}
inputValue={query} inputValue={query}
isLoading={loading} isLoading={loading}
...@@ -100,6 +140,7 @@ export default function ReferenceSearchSelect({ ...@@ -100,6 +140,7 @@ export default function ReferenceSearchSelect({
onChange(multiple ? [] : ''); onChange(multiple ? [] : '');
setQuery(''); setQuery('');
}} }}
/> />
</div>
); );
} }
...@@ -196,6 +196,22 @@ export async function searchReferenceRecords(subject, keyword = '') { ...@@ -196,6 +196,22 @@ export async function searchReferenceRecords(subject, keyword = '') {
return result?.records || []; return result?.records || [];
} }
export async function getReferenceRecordsByIds(subject, ids = []) {
const recordIds = ids.map(normalizeRecordId).filter(Boolean);
if (recordIds.length === 0) return [];
const result = await queryRecords({
subject,
page: 0,
page_size: recordIds.length,
filter: {
_id: { $in: recordIds },
},
mode: 'basic',
});
return result?.records || [];
}
export async function searchTags(keyword = '') { export async function searchTags(keyword = '') {
const result = await queryTags({ const result = await queryTags({
page: 0, page: 0,
......
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