Commit b52ff11f authored by David Yang's avatar David Yang

fix: bugs

parent 40f35588
......@@ -27,6 +27,10 @@ function asSaveValue(value, fieldDef) {
return value;
}
function getClearedValue(fieldDef) {
return fieldDef?.multiple ? [] : null;
}
function Pill({ children, tone = 'zinc' }) {
const toneClass =
tone === 'rose'
......@@ -241,6 +245,12 @@ export default function TailwindEditableField({
}
};
const clearValue = async () => {
const clearedValue = getClearedValue(fieldDef);
setDraft(clearedValue);
await save(clearedValue);
};
const cancel = () => {
setDraft(value);
setError('');
......@@ -259,7 +269,8 @@ export default function TailwindEditableField({
try {
const uploaded = await uploadFile(file);
const fileValue = uploaded?.file || uploaded?.data || uploaded;
const nextValue = fieldDef?.multiple ? [...toArray(value), fileValue] : fileValue;
const nextValue = fieldDef?.multiple ? [...toArray(draft), fileValue] : fileValue;
setDraft(nextValue);
await onSave(nextValue);
setEditing(false);
} catch (uploadError) {
......@@ -269,6 +280,25 @@ export default function TailwindEditableField({
}
};
const handleRemoveFile = async (indexToRemove) => {
setError('');
const nextValue = fieldDef?.multiple
? toArray(draft).filter((_, index) => index !== indexToRemove)
: null;
try {
setDraft(nextValue);
await onSave(nextValue);
if (!fieldDef?.multiple) {
setEditing(false);
}
} catch (saveError) {
setDraft(draft);
setError(saveError?.message || '删除附件失败,请稍后重试。');
}
};
if (!editing) {
return (
<button
......@@ -300,7 +330,10 @@ export default function TailwindEditableField({
<div className="flex items-center gap-3 rounded-2xl border border-zinc-200 bg-white px-3 py-2">
<Checkbox checked={Boolean(draft)} onChange={(checked) => setDraft(checked)} color="rose" />
<span className="text-sm text-zinc-700">{draft ? '是' : '否'}</span>
<button type="button" onClick={() => save(Boolean(draft))} className="ml-auto text-sm font-medium text-rose-700">
<button type="button" onClick={clearValue} className="ml-auto text-sm font-medium text-rose-600">
清空
</button>
<button type="button" onClick={() => save(Boolean(draft))} className="text-sm font-medium text-rose-700">
保存
</button>
<button type="button" onClick={cancel} className="text-sm text-zinc-500">
......@@ -334,6 +367,7 @@ export default function TailwindEditableField({
))}
</Select>
<div className="flex justify-end gap-2">
<button type="button" onClick={clearValue} className="text-sm font-medium text-rose-600">清空</button>
<button type="button" onClick={cancel} className="text-sm text-zinc-500">取消</button>
<button type="button" onClick={() => save()} className="text-sm font-medium text-rose-700">保存</button>
</div>
......@@ -347,6 +381,7 @@ export default function TailwindEditableField({
<div className="space-y-3">
<UserSelector value={draft} onChange={setDraft} multiple={fieldDef?.multiple} />
<div className="flex justify-end gap-2">
<button type="button" onClick={clearValue} className="text-sm font-medium text-rose-600">清空</button>
<button type="button" onClick={cancel} className="text-sm text-zinc-500">取消</button>
<button type="button" onClick={() => save()} className="text-sm font-medium text-rose-700">保存</button>
</div>
......@@ -364,6 +399,7 @@ export default function TailwindEditableField({
onChange={setDraft}
/>
<div className="flex justify-end gap-2">
<button type="button" onClick={clearValue} className="text-sm font-medium text-rose-600">清空</button>
<button type="button" onClick={cancel} className="text-sm text-zinc-500">取消</button>
<button type="button" onClick={() => save()} className="text-sm font-medium text-rose-700">保存</button>
</div>
......@@ -375,10 +411,47 @@ export default function TailwindEditableField({
if (fieldType === 'file') {
return (
<div className="space-y-3 rounded-2xl border border-zinc-200 bg-white p-3">
<FieldValue value={value} fieldDef={fieldDef} />
<div className="space-y-2">
{toArray(draft).length === 0 ? (
<span className="text-zinc-400">未上传</span>
) : (
toArray(draft).map((file, index) => {
const label = file?.name || file?.title || file?.url || `附件 ${index + 1}`;
return (
<div
key={`${label}-${index}`}
className="flex items-center justify-between gap-3 rounded-2xl border border-zinc-100 bg-zinc-50 px-3 py-2"
>
<div className="min-w-0 flex-1">
{file?.url ? (
<a
href={file.url}
target="_blank"
rel="noreferrer"
className="text-sm font-medium text-sky-700 transition hover:text-sky-800"
>
{label}
</a>
) : (
<div className="text-sm font-medium text-zinc-700">{label}</div>
)}
</div>
<button
type="button"
onClick={() => handleRemoveFile(index)}
className="shrink-0 text-sm font-medium text-rose-600"
>
删除
</button>
</div>
);
})
)}
</div>
<input type="file" onChange={handleFileUpload} disabled={uploading} className="block w-full text-sm text-zinc-600" />
{error ? <div className="text-sm text-rose-600">{error}</div> : null}
<div className="flex justify-end gap-2">
<button type="button" onClick={clearValue} className="text-sm font-medium text-rose-600">清空</button>
<button type="button" onClick={cancel} className="text-sm text-zinc-500">取消</button>
<span className="text-sm text-zinc-400">{uploading ? '上传中...' : '选择文件后自动保存'}</span>
</div>
......@@ -393,6 +466,7 @@ export default function TailwindEditableField({
<Textarea rows={8} value={jsonValue} onChange={(event) => setDraft(event.target.value)} resizable />
{error ? <div className="text-sm text-rose-600">{error}</div> : null}
<div className="flex justify-end gap-2">
<button type="button" onClick={clearValue} className="text-sm font-medium text-rose-600">清空</button>
<button type="button" onClick={cancel} className="text-sm text-zinc-500">取消</button>
<button
type="button"
......@@ -411,6 +485,7 @@ export default function TailwindEditableField({
<div className="space-y-3">
<Textarea rows={5} value={draft || ''} onChange={(event) => setDraft(event.target.value)} autoFocus />
<div className="flex justify-end gap-2">
<button type="button" onClick={clearValue} className="text-sm font-medium text-rose-600">清空</button>
<button type="button" onClick={cancel} className="text-sm text-zinc-500">取消</button>
<button type="button" onClick={() => save()} className="text-sm font-medium text-rose-700">保存</button>
</div>
......@@ -433,6 +508,7 @@ export default function TailwindEditableField({
autoFocus
/>
<div className="flex justify-end gap-2">
<button type="button" onClick={clearValue} className="text-sm font-medium text-rose-600">清空</button>
<button type="button" onClick={cancel} className="text-sm text-zinc-500">取消</button>
<button type="button" onClick={() => save()} className="text-sm font-medium text-rose-700">保存</button>
</div>
......
......@@ -63,6 +63,7 @@ const RecordDetailDrawer = (props, ref) => {
onDataChange,
readonly = false,
filterView = [],
preferredViewName,
} = props;
const [refreshKey, setRefreshKey] = useState(() => Date.now());
......@@ -112,11 +113,21 @@ const RecordDetailDrawer = (props, ref) => {
return;
}
if (preferredViewName) {
const matchedView = detailViews.find(
(view) => view.name === preferredViewName || view.title === preferredViewName,
);
if (matchedView && matchedView.name !== activeTabKey) {
setActiveTabKey(matchedView.name);
return;
}
}
const currentTab = detailViews.find((view) => view.name === activeTabKey);
if (!currentTab) {
setActiveTabKey(detailViews[0].name);
}
}, [detailViews, activeTabKey]);
}, [detailViews, activeTabKey, preferredViewName]);
useImperativeHandle(
ref,
......
import xyLogo from '@/assets/xy-logo.png';
import xyLogoSmall from '@/assets/xy-logo_small.png';
import { UserContext } from '@/wrapper/Auth';
import {
......@@ -244,16 +243,13 @@ export default function ProblemRectificationLayout() {
sidebarCollapsed ? 'justify-center' : 'gap-3 px-1',
)}
>
{sidebarCollapsed ? (
<img
src={sidebarCollapsed ? xyLogoSmall : xyLogo}
src={xyLogoSmall}
alt="XY Logo"
className={clsx(
'shrink-0 object-contain',
sidebarCollapsed
? 'h-8 w-auto max-w-8'
: 'h-8 w-auto max-w-[140px]',
)}
className="h-8 w-auto max-w-8 shrink-0 object-contain"
/>
) : null}
{!sidebarCollapsed ? (
<div className="min-w-0">
<h1 className="truncate text-sm font-semibold tracking-[0.02em] text-white/95">
......@@ -327,12 +323,7 @@ export default function ProblemRectificationLayout() {
close={() => setIsMobileSidebarOpen(false)}
>
<div className="px-5 pt-6">
<div className="flex items-center gap-2">
<img
src={xyLogo}
alt="XY Logo"
className="h-8 w-auto max-w-[140px] shrink-0 object-contain"
/>
<div>
<div>
<h1 className="text-base font-semibold tracking-[0.02em] text-white">
{APP_NAME}
......
import { Button } from '@/catalyst-ui-kit/javascript/button';
import { Checkbox } from '@/catalyst-ui-kit/javascript/checkbox';
import {
Dialog,
DialogActions,
......@@ -8,14 +9,22 @@ import {
} from '@/catalyst-ui-kit/javascript/dialog';
import { Input } from '@/catalyst-ui-kit/javascript/input';
import { Select } from '@/catalyst-ui-kit/javascript/select';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/catalyst-ui-kit/javascript/table';
import { Textarea } from '@/catalyst-ui-kit/javascript/textarea';
import RecordDetailDrawer from '@/pages/RecordDetailDrawer';
import {
batchRemoveRecords,
batchUpdateRecords,
createNotice,
createRecord,
executeAction,
loadSubject,
loadUser,
queryUsers,
sendNotice,
......@@ -24,37 +33,28 @@ import {
import * as Headless from '@headlessui/react';
import clsx from 'clsx';
import dayjs from 'dayjs';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import {
emptyValueForField,
formatFieldValue,
getFieldNameFromPath,
getFieldValue,
getSelectOptions,
normalizeText,
} from '@/utils/subjectView';
import { DEPARTMENT_GROUPS } from '../problemRectificationDashboardService';
import {
PROBLEM_RECTIFICATION_SUBJECT,
buildGridViewCountMap,
buildGridViews,
buildInitialCreateValues,
buildProblemRectificationConfiguredListView,
buildProblemRectificationCreateFormFields,
buildProblemRectificationCreatePayload,
buildProblemRectificationListView,
loadProblemRectificationListData,
getRecordCellText,
loadProblemRectificationListResources,
} from '../problemRectificationListService';
const CREATE_FIELD_ORDER = [
'project_name',
'inspect_the_implementing_unit',
'check_the_implementation_time',
'inspection_report_date',
'rectification_progress',
'problem_from',
'problem_type',
'feedback_unit',
'related_issue_domain',
'the_time_of_rectification',
'responsible_department',
'problem_brief',
'problem_discovery_situation',
'rectification_situation',
'remark',
];
const SELECTION_COLUMN_WIDTH = 56;
const ACTION_COLUMN_WIDTH = 132;
const FIELD_HINTS = {
inspect_the_implementing_unit: '按对象配置的实施单位选项选择',
......@@ -66,20 +66,86 @@ const FIELD_HINTS = {
the_time_of_rectification: '提交时会自动转成 ISO 8601 时间',
};
function getPinnedSide(column) {
const pinnedValue = column?.lockPosition || column?.pinned;
if (pinnedValue === 'left' || pinnedValue === 'right') {
return pinnedValue;
}
return null;
}
function buildStickyColumnMeta(columns = []) {
const leftColumns = columns.filter((column) => getPinnedSide(column) === 'left');
const centerColumns = columns.filter((column) => !getPinnedSide(column));
const rightColumns = columns.filter((column) => getPinnedSide(column) === 'right');
const orderedColumns = [...leftColumns, ...centerColumns, ...rightColumns];
let leftOffset = SELECTION_COLUMN_WIDTH;
let rightOffset = ACTION_COLUMN_WIDTH;
const stickyMeta = {};
leftColumns.forEach((column) => {
stickyMeta[column.field] = {
side: 'left',
offset: leftOffset,
isEdge: column.field === leftColumns[leftColumns.length - 1]?.field,
};
leftOffset += column.width || 180;
});
[...rightColumns].reverse().forEach((column) => {
stickyMeta[column.field] = {
side: 'right',
offset: rightOffset,
isEdge: column.field === rightColumns[0]?.field,
};
rightOffset += column.width || 180;
});
return {
orderedColumns,
stickyMeta,
};
}
function getStickyCellClass(meta, kind = 'body') {
if (!meta) {
return '';
}
const baseClass =
kind === 'header'
? 'sticky z-30 bg-zinc-50/95 backdrop-blur'
: 'sticky z-20 bg-white';
const sideClass = meta.side === 'left' ? 'left-0' : 'right-0';
const edgeShadow =
meta.side === 'left' && meta.isEdge
? 'shadow-[10px_0_20px_rgba(15,23,42,0.06)]'
: meta.side === 'right' && meta.isEdge
? 'shadow-[-10px_0_20px_rgba(15,23,42,0.06)]'
: '';
return clsx(baseClass, sideClass, edgeShadow);
}
function SummaryCard({ label, value, hint, tone = 'default' }) {
const toneClass =
tone === 'rose'
? 'border-rose-200 bg-rose-50/80'
? 'border-rose-200/80 bg-rose-50/85'
: tone === 'amber'
? 'border-amber-200 bg-amber-50/80'
? 'border-amber-200/80 bg-amber-50/85'
: tone === 'emerald'
? 'border-emerald-200 bg-emerald-50/80'
: 'border-zinc-200 bg-zinc-50/80';
? 'border-emerald-200/80 bg-emerald-50/85'
: 'border-zinc-200/80 bg-white/88';
return (
<div className={clsx('rounded-[24px] border p-4 shadow-sm', toneClass)}>
<div className="text-sm font-medium text-slate-500">{label}</div>
<div className="mt-3 text-3xl font-semibold tracking-tight text-slate-900">
<div className={clsx('rounded-[26px] border p-5 shadow-sm ring-1 ring-zinc-950/5', toneClass)}>
<div className="text-xs font-semibold uppercase tracking-[0.22em] text-slate-400">
{label}
</div>
<div className="mt-3 text-3xl font-semibold tracking-tight text-slate-950">
{value}
</div>
<div className="mt-2 text-sm text-slate-500">{hint}</div>
......@@ -89,7 +155,7 @@ function SummaryCard({ label, value, hint, tone = 'default' }) {
function EmptyState({ title, description }) {
return (
<div className="rounded-[28px] border border-dashed border-zinc-300 bg-white/70 px-6 py-12 text-center">
<div className="rounded-[28px] border border-dashed border-zinc-300 bg-white/80 px-6 py-14 text-center">
<div className="text-xl font-semibold text-slate-900">{title}</div>
<div className="mt-3 text-sm leading-6 text-slate-500">{description}</div>
</div>
......@@ -99,20 +165,15 @@ function EmptyState({ title, description }) {
function StatusPill({ text, tone = 'default' }) {
const toneClass =
tone === 'rose'
? 'bg-rose-50 text-rose-700'
? 'bg-rose-50 text-rose-700 ring-rose-100'
: tone === 'emerald'
? 'bg-emerald-50 text-emerald-700'
? 'bg-emerald-50 text-emerald-700 ring-emerald-100'
: tone === 'amber'
? 'bg-amber-50 text-amber-700'
: 'bg-slate-100 text-slate-600';
? 'bg-amber-50 text-amber-700 ring-amber-100'
: 'bg-slate-100 text-slate-600 ring-slate-200';
return (
<span
className={clsx(
'inline-flex rounded-full px-3 py-1 text-xs font-medium',
toneClass,
)}
>
<span className={clsx('inline-flex rounded-full px-3 py-1 text-xs font-medium ring-1', toneClass)}>
{text || '-'}
</span>
);
......@@ -141,10 +202,6 @@ function resolveAuditTone(status = '') {
return 'default';
}
function formatUsers(users) {
return users && users.length > 0 ? users.join('、') : '-';
}
function SelectedUsers({ users, onRemove }) {
if (users.length === 0) {
return (
......@@ -176,37 +233,28 @@ function SelectedUsers({ users, onRemove }) {
);
}
function RemoteUserSelect({
label,
value,
onChange,
placeholder,
onSearch,
}) {
function RemoteUserSelect({ label, value, onChange, placeholder, onSearch }) {
const [keyword, setKeyword] = useState('');
const [options, setOptions] = useState([]);
const [loading, setLoading] = useState(false);
const requestIdRef = useRef(0);
useEffect(() => {
let cancelled = false;
const requestId = requestIdRef.current + 1;
requestIdRef.current = requestId;
const loadOptions = async () => {
setLoading(true);
try {
const users = await onSearch(keyword);
if (!cancelled && requestIdRef.current === requestId) {
if (!cancelled) {
setOptions(users);
}
} catch (error) {
if (!cancelled && requestIdRef.current === requestId) {
if (!cancelled) {
setOptions([]);
}
} finally {
if (!cancelled && requestIdRef.current === requestId) {
if (!cancelled) {
setLoading(false);
}
}
......@@ -219,18 +267,12 @@ function RemoteUserSelect({
};
}, [keyword, onSearch]);
const selectedUsers = useMemo(() => value.filter(Boolean), [value]);
const selectedIds = useMemo(
() => new Set(selectedUsers.map((user) => user._id)),
[selectedUsers],
);
const selectedIds = useMemo(() => new Set(value.map((item) => item._id)), [value]);
const availableOptions = options.filter((user) => !selectedIds.has(user._id));
return (
<div className="space-y-3">
<label className="mb-2 block text-sm font-medium text-slate-700">
{label}
</label>
<label className="block text-sm font-medium text-slate-700">{label}</label>
<Headless.Combobox
value={null}
nullable
......@@ -238,7 +280,7 @@ function RemoteUserSelect({
if (!user || selectedIds.has(user._id)) {
return;
}
onChange([...selectedUsers, user]);
onChange([...value, user]);
setKeyword('');
}}
>
......@@ -271,9 +313,7 @@ function RemoteUserSelect({
value={user}
className="cursor-pointer rounded-xl px-3 py-2 text-sm text-slate-700 data-focus:bg-rose-50 data-focus:text-slate-900"
>
<div className="font-medium text-slate-900">
{user.display_name || user.username}
</div>
<div className="font-medium text-slate-900">{user.display_name || user.username}</div>
<div className="text-xs text-slate-500">{user.username}</div>
</Headless.ComboboxOption>
))
......@@ -286,30 +326,13 @@ function RemoteUserSelect({
</div>
</Headless.Combobox>
<SelectedUsers
users={selectedUsers}
onRemove={(userId) =>
onChange(selectedUsers.filter((user) => user._id !== userId))
}
users={value}
onRemove={(userId) => onChange(value.filter((user) => user._id !== userId))}
/>
</div>
);
}
function buildInitialCreateValues(subject) {
return (subject?.fields || [])
.filter((field) => CREATE_FIELD_ORDER.includes(field.name))
.reduce((result, field) => {
result[field.name] = emptyValueForField(field);
return result;
}, {});
}
function normalizeFieldList(subject) {
const fieldMap = new Map((subject?.fields || []).map((field) => [field.name, field]));
return CREATE_FIELD_ORDER.map((fieldName) => fieldMap.get(fieldName)).filter(Boolean);
}
function CreateField({ fieldDef, value, onChange }) {
const options = getSelectOptions(fieldDef);
const isMultiple = Boolean(fieldDef?.multiple);
......@@ -328,13 +351,7 @@ function CreateField({ fieldDef, value, onChange }) {
}
if (fieldType === 'date') {
return (
<Input
type="date"
value={value || ''}
onChange={(event) => onChange(event.target.value)}
/>
);
return <Input type="date" value={value || ''} onChange={(event) => onChange(event.target.value)} />;
}
if (fieldType === 'select') {
......@@ -344,9 +361,7 @@ function CreateField({ fieldDef, value, onChange }) {
value={isMultiple ? value || [] : value || ''}
onChange={(event) => {
if (isMultiple) {
onChange(
Array.from(event.target.selectedOptions).map((option) => option.value),
);
onChange(Array.from(event.target.selectedOptions).map((option) => option.value));
return;
}
onChange(event.target.value);
......@@ -364,6 +379,7 @@ function CreateField({ fieldDef, value, onChange }) {
return (
<Input
type={fieldType === 'number' ? 'number' : 'text'}
value={value || ''}
onChange={(event) => onChange(event.target.value)}
placeholder={placeholder}
......@@ -371,89 +387,178 @@ function CreateField({ fieldDef, value, onChange }) {
);
}
function FieldValuePreview({ column, record }) {
const fieldName = getFieldNameFromPath(column.field);
const rawRecord = record.raw || record;
const value = getFieldValue(rawRecord, column.field);
if (fieldName === 'rectification_progress') {
return <StatusPill text={formatFieldValue(value, column.fieldDef) || '未填写'} tone={resolveProgressTone(formatFieldValue(value, column.fieldDef) || '')} />;
}
if (fieldName === 'audit_status' || fieldName === 'final_audit_status') {
return <StatusPill text={formatFieldValue(value, column.fieldDef) || '未填写'} tone={resolveAuditTone(formatFieldValue(value, column.fieldDef) || '')} />;
}
if (fieldName === 'fill_in_the_people' || fieldName === 'review_people') {
const text = getRecordCellText(record, column);
return <div className="whitespace-normal break-words text-sm text-slate-700">{text}</div>;
}
if (column.fieldDef?.type === 'file') {
const fileValue = Array.isArray(value) ? value.filter(Boolean) : value ? [value] : [];
if (fileValue.length === 0) {
return <span className="text-slate-400">未上传</span>;
}
return (
<div className="flex flex-wrap gap-2">
{fileValue.map((file, index) => {
const label = file?.name || file?.title || `附件 ${index + 1}`;
if (!file?.url) {
return (
<span key={`${label}-${index}`} className="inline-flex rounded-full bg-zinc-100 px-2.5 py-1 text-xs font-medium text-zinc-700">
{label}
</span>
);
}
return (
<a
key={`${label}-${index}`}
href={file.url}
target="_blank"
rel="noreferrer"
className="inline-flex rounded-full bg-sky-50 px-2.5 py-1 text-xs font-medium text-sky-700 transition hover:bg-sky-100"
>
{label}
</a>
);
})}
</div>
);
}
const text = getRecordCellText(record, column);
const isLongText = ['long_text', 'rich_text'].includes(column.fieldDef?.type);
return (
<div className={clsx('whitespace-normal break-words text-sm text-slate-700', isLongText && 'line-clamp-3')}>
{text}
</div>
);
}
export default function ProblemRectificationListPage() {
const [records, setRecords] = useState([]);
const [createSubject, setCreateSubject] = useState(null);
const [subject, setSubject] = useState(null);
const [views, setViews] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [actionLoading, setActionLoading] = useState(false);
const [filters, setFilters] = useState({
groupKey: 'all',
keyword: '',
progress: '',
auditStatus: '',
});
const [activeViewName, setActiveViewName] = useState('');
const [selectedIds, setSelectedIds] = useState([]);
const [showCreateModal, setShowCreateModal] = useState(false);
const [showPushModal, setShowPushModal] = useState(false);
const [formValues, setFormValues] = useState({});
const [fillerUsers, setFillerUsers] = useState([]);
const [auditorUsers, setAuditorUsers] = useState([]);
const [drawerRecordName, setDrawerRecordName] = useState('');
const [drawerDirty, setDrawerDirty] = useState(false);
const reloadList = async () => {
const reloadList = useCallback(async () => {
setLoading(true);
setError('');
try {
const nextRecords = await loadProblemRectificationListData();
setRecords(nextRecords);
const resources = await loadProblemRectificationListResources();
setRecords(resources.records || []);
setSubject(resources.subject || null);
setViews(resources.views || []);
} catch (requestError) {
setRecords([]);
setSubject(null);
setViews([]);
setError(requestError?.message || '问题整改列表加载失败,请稍后重试。');
} finally {
setLoading(false);
}
};
}, []);
useEffect(() => {
reloadList();
}, []);
}, [reloadList]);
useEffect(() => {
let cancelled = false;
const gridViews = useMemo(() => buildGridViews(views), [views]);
(async () => {
try {
const subject = await loadSubject(PROBLEM_RECTIFICATION_SUBJECT);
if (!cancelled) {
setCreateSubject(subject);
setFormValues(buildInitialCreateValues(subject));
}
} catch (requestError) {
if (!cancelled) {
setCreateSubject(null);
useEffect(() => {
if (gridViews.length === 0) {
setActiveViewName('');
return;
}
const currentView = gridViews.find((view) => view.name === activeViewName);
if (!currentView) {
setActiveViewName(gridViews[0].name);
}
})();
}, [gridViews, activeViewName]);
return () => {
cancelled = true;
};
}, []);
const activeGridView = useMemo(
() => gridViews.find((view) => view.name === activeViewName) || gridViews[0] || null,
[gridViews, activeViewName],
);
const configuredView = useMemo(
() => buildProblemRectificationConfiguredListView(records, filters, activeGridView, subject),
[records, filters, activeGridView, subject],
);
const view = useMemo(
() => buildProblemRectificationListView(records, filters),
[records, filters],
const stickyColumns = useMemo(
() => buildStickyColumnMeta(configuredView.columns || []),
[configuredView.columns],
);
const gridViewCounts = useMemo(
() => buildGridViewCountMap(records, filters, gridViews, subject),
[records, filters, gridViews, subject],
);
const createFields = useMemo(
() => normalizeFieldList(createSubject),
[createSubject],
() => buildProblemRectificationCreateFormFields(activeGridView, subject),
[activeGridView, subject],
);
const initialCreateValues = useMemo(
() => buildInitialCreateValues(createSubject),
[createSubject],
() => buildInitialCreateValues(createFields),
[createFields],
);
const filteredRecords = view.filteredRecords || [];
const groupCounts = view.groupCounts || {};
useEffect(() => {
if (!showCreateModal) {
setFormValues(initialCreateValues);
}
}, [initialCreateValues, showCreateModal]);
const filteredRecords = configuredView.filteredRecords || [];
const selectedRecords = useMemo(
() => filteredRecords.filter((item) => selectedIds.includes(item.id)),
[filteredRecords, selectedIds],
);
useEffect(() => {
const visibleIds = new Set(filteredRecords.map((item) => item.id));
setSelectedIds((current) => current.filter((id) => visibleIds.has(id)));
}, [filteredRecords]);
const fixedCount = filteredRecords.filter(
(item) => item.progress === '已整改' || item.progress === '已优化',
).length;
const pendingAuditCount = filteredRecords.filter(
(item) => item.auditStatus && item.auditStatus !== '已审批',
).length;
......@@ -474,6 +579,20 @@ export default function ProblemRectificationListPage() {
setSelectedIds((current) => current.filter((item) => item !== recordId));
};
const handleOpenDrawer = (record) => {
setDrawerDirty(false);
setDrawerRecordName(record.name || record.id);
};
const handleCloseDrawer = async () => {
setDrawerRecordName('');
if (drawerDirty) {
setDrawerDirty(false);
await reloadList();
}
};
const handleCreate = async () => {
if (
!normalizeText(formValues.project_name) &&
......@@ -510,9 +629,7 @@ export default function ProblemRectificationListPage() {
return;
}
const confirmed = window.confirm(
`确认删除 ${targetRecords.length} 条问题整改记录吗?此操作不可撤销。`,
);
const confirmed = window.confirm(`确认删除 ${targetRecords.length} 条问题整改记录吗?此操作不可撤销。`);
if (!confirmed) {
return;
}
......@@ -527,14 +644,13 @@ export default function ProblemRectificationListPage() {
}
await batchRemoveRecords({
subject: 'problem_rectification',
subject: PROBLEM_RECTIFICATION_SUBJECT,
filter: {
name: { $in: names },
},
});
setSelectedIds((current) =>
current.filter((id) => !targetRecords.some((item) => item.id === id)),
);
setSelectedIds((current) => current.filter((id) => !targetRecords.some((item) => item.id === id)));
await reloadList();
} catch (requestError) {
setError(requestError?.message || '删除问题整改记录失败,请稍后重试。');
......@@ -565,6 +681,7 @@ export default function ProblemRectificationListPage() {
setError('请先勾选需要推送的问题整改记录。');
return;
}
if (fillerIds.length === 0 || auditorIds.length === 0) {
setError('请先选择填报人和复核人。');
return;
......@@ -574,10 +691,8 @@ export default function ProblemRectificationListPage() {
setError('');
try {
const fillerUsers = await Promise.all(fillerIds.map((id) => loadUser(id)));
const auditorUsers = await Promise.all(auditorIds.map((id) => loadUser(id)));
const currentUserInfo =
JSON.parse(window.localStorage.getItem('current_user') || '{}') || {};
const fillerProfiles = await Promise.all(fillerIds.map((id) => loadUser(id)));
const currentUserInfo = JSON.parse(window.localStorage.getItem('current_user') || '{}') || {};
const messageRecord = await createRecord({
subject: 'message_reference_id',
......@@ -589,8 +704,8 @@ export default function ProblemRectificationListPage() {
restriction_key: '_id',
restriction_value: selectedRecords.map((item) => item.id),
reference_record_id: selectedRecords.map((item) => item.id),
view_name: filters.groupKey,
subject_name: 'problem_rectification',
view_name: activeGridView?.name || '',
subject_name: PROBLEM_RECTIFICATION_SUBJECT,
editor_status: '未完成',
auditor_status: '未完成',
sender_status: '未完成',
......@@ -598,14 +713,14 @@ export default function ProblemRectificationListPage() {
});
const progressIdEditor = [];
for (const fillerUser of fillerUsers) {
for (const fillerProfile of fillerProfiles) {
const oaProcessResult = await executeAction('oa_notice:createProgress', {
title: `${dayjs().format('YYYY-MM-DD')} 填写问题整改表`,
classify: '流程待办',
contentType: '流程待办',
receiveUserId: fillerUser._id,
receiveUserName: fillerUser.username,
receiveDisplayName: fillerUser.display_name,
receiveUserId: fillerProfile._id,
receiveUserName: fillerProfile.username,
receiveDisplayName: fillerProfile.display_name,
sendUserId: currentUserInfo?._id || currentUserInfo?.id || '',
sendUserName: currentUserInfo?.username || '',
sendDisplayName: currentUserInfo?.display_name || '',
......@@ -621,7 +736,7 @@ export default function ProblemRectificationListPage() {
});
await batchUpdateRecords({
subject: 'problem_rectification',
subject: PROBLEM_RECTIFICATION_SUBJECT,
filter: {
_id: { $in: selectedRecords.map((item) => item.id) },
},
......@@ -654,337 +769,266 @@ export default function ProblemRectificationListPage() {
};
return (
<div className="space-y-6">
<div className="space-y-6 pb-4">
<section className="grid gap-4 md:grid-cols-3">
<SummaryCard
label="当前分组问题数"
label="当前视图记录数"
value={filteredRecords.length}
hint="筛选结果"
hint={activeGridView?.title ? `${activeGridView.title} 视图` : '筛选结果'}
tone="rose"
/>
<SummaryCard
label="已整改 / 已优化"
value={fixedCount}
hint="已完成数量"
hint="按当前视图与筛选统计"
tone="emerald"
/>
<SummaryCard
label="待复核项"
value={pendingAuditCount}
hint="待处理记录"
hint="审批状态未通过的记录"
tone="amber"
/>
</section>
<section className="overflow-x-auto">
<div className="inline-flex min-w-full gap-3 rounded-[28px] border border-zinc-200 bg-white/90 p-2 shadow-sm">
{DEPARTMENT_GROUPS.map((group) => (
<section className="rounded-[30px] border border-white/80 bg-[linear-gradient(180deg,rgba(255,255,255,0.96),rgba(250,250,250,0.92))] p-3 shadow-sm ring-1 ring-zinc-950/5">
<div className="flex flex-wrap gap-2">
{gridViews.map((view) => {
const active = view.name === activeGridView?.name;
return (
<button
key={group.key}
key={view.name}
type="button"
onClick={() =>
setFilters((current) => ({ ...current, groupKey: group.key }))
}
onClick={() => {
setActiveViewName(view.name);
setSelectedIds([]);
}}
className={clsx(
'rounded-[18px] px-4 py-3 text-sm font-medium transition-all duration-200',
filters.groupKey === group.key
? 'bg-[linear-gradient(135deg,#7f1d1d,#be123c)] text-white shadow-lg'
: 'text-slate-600 hover:bg-rose-50 hover:text-rose-700',
'inline-flex min-h-11 items-center gap-2 rounded-full px-4 py-2.5 text-sm font-medium transition',
active
? 'bg-[linear-gradient(135deg,#7f1d1d,#be123c)] text-white shadow-lg shadow-rose-200/60'
: 'bg-white text-slate-600 ring-1 ring-zinc-200 hover:bg-rose-50 hover:text-rose-700',
)}
>
{group.label}
<span>{view.title || view.name}</span>
<span
className={clsx(
'ml-2 rounded-full px-2 py-0.5 text-xs',
filters.groupKey === group.key
? 'bg-white/18 text-white'
: 'bg-zinc-100 text-slate-500',
'rounded-full px-2 py-0.5 text-xs',
active ? 'bg-white/20 text-white' : 'bg-zinc-100 text-slate-500',
)}
>
{groupCounts[group.key] || 0}
{gridViewCounts[view.name] || 0}
</span>
</button>
))}
);
})}
</div>
</section>
<section className="rounded-[30px] border border-zinc-200 bg-white/95 p-5 shadow-sm">
<section className="rounded-[30px] border border-zinc-200 bg-white/95 p-5 shadow-sm ring-1 ring-zinc-950/5">
<div className="grid gap-4 xl:grid-cols-[1.4fr_0.8fr_0.8fr_auto]">
<div>
<label className="mb-2 block text-sm font-medium text-slate-700">
关键词搜索
</label>
<input
<label className="mb-2 block text-sm font-medium text-slate-700">关键词搜索</label>
<Input
value={filters.keyword}
onChange={(event) =>
setFilters((current) => ({
...current,
keyword: event.target.value,
}))
}
placeholder="搜索项目名称、问题简述、问题类型、反馈单位..."
className="w-full rounded-2xl border border-zinc-200 bg-zinc-50 px-4 py-3 text-sm text-slate-900 outline-none transition focus:border-rose-300 focus:bg-white"
onChange={(event) => setFilters((current) => ({ ...current, keyword: event.target.value }))}
placeholder="搜索当前视图中的项目名称、状态、问题简述、责任人..."
/>
</div>
<div>
<label className="mb-2 block text-sm font-medium text-slate-700">
整改进度
</label>
<select
<label className="mb-2 block text-sm font-medium text-slate-700">整改进度</label>
<Select
value={filters.progress}
onChange={(event) =>
setFilters((current) => ({
...current,
progress: event.target.value,
}))
}
className="w-full rounded-2xl border border-zinc-200 bg-zinc-50 px-4 py-3 text-sm text-slate-900 outline-none transition focus:border-rose-300 focus:bg-white"
onChange={(event) => setFilters((current) => ({ ...current, progress: event.target.value }))}
>
<option value="">全部进度</option>
{view.progressOptions.map((option) => (
{configuredView.progressOptions.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</Select>
</div>
<div>
<label className="mb-2 block text-sm font-medium text-slate-700">
审批状态
</label>
<select
<label className="mb-2 block text-sm font-medium text-slate-700">审批状态</label>
<Select
value={filters.auditStatus}
onChange={(event) =>
setFilters((current) => ({
...current,
auditStatus: event.target.value,
}))
}
className="w-full rounded-2xl border border-zinc-200 bg-zinc-50 px-4 py-3 text-sm text-slate-900 outline-none transition focus:border-rose-300 focus:bg-white"
onChange={(event) => setFilters((current) => ({ ...current, auditStatus: event.target.value }))}
>
<option value="">全部状态</option>
{view.auditStatusOptions.map((option) => (
{configuredView.auditStatusOptions.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</Select>
</div>
<div className="flex items-end">
<button
type="button"
<Button
outline
className="w-full justify-center"
onClick={() => {
setFilters({
groupKey: 'all',
keyword: '',
progress: '',
auditStatus: '',
});
setSelectedIds([]);
}}
className="w-full rounded-2xl border border-zinc-200 bg-white px-4 py-3 text-sm font-medium text-slate-600 transition hover:border-rose-200 hover:text-rose-700"
>
重置筛选
</button>
清空筛选
</Button>
</div>
</div>
</section>
<section className="rounded-[30px] border border-zinc-200 bg-white/95 shadow-sm">
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-zinc-100 px-6 py-5">
<section className="overflow-hidden rounded-[32px] border border-white/80 bg-white/96 shadow-[0_18px_48px_rgba(15,23,42,0.08)] ring-1 ring-zinc-950/5">
<div className="flex flex-wrap items-start justify-between gap-4 border-b border-zinc-100 px-6 py-5">
<div>
<div className="text-lg font-semibold text-slate-900">问题清单</div>
<div className="mt-1 text-sm text-slate-500">
当前选中 {selectedIds.length} 条记录。
当前视图显示 {filteredRecords.length} 条记录,已选中 {selectedIds.length} 条。
</div>
</div>
<div className="flex flex-wrap gap-2">
<button
type="button"
onClick={() => setShowCreateModal(true)}
className="rounded-full bg-[linear-gradient(135deg,#7f1d1d,#be123c)] px-4 py-2 text-sm font-medium text-white shadow-lg transition hover:brightness-105"
>
<Button color="rose" onClick={() => setShowCreateModal(true)}>
新增记录
</button>
<button
type="button"
disabled={selectedIds.length === 0 || actionLoading}
onClick={() => setShowPushModal(true)}
className="rounded-full border border-zinc-200 px-4 py-2 text-sm font-medium text-slate-600 transition hover:border-rose-200 hover:text-rose-700 disabled:cursor-not-allowed disabled:opacity-50"
>
</Button>
<Button outline disabled={selectedIds.length === 0 || actionLoading} onClick={() => setShowPushModal(true)}>
推送整改表
</button>
<button
type="button"
className="rounded-full border border-zinc-200 px-4 py-2 text-sm font-medium text-slate-600 transition hover:border-rose-200 hover:text-rose-700"
>
</Button>
<Button outline disabled>
导出
</button>
<button
type="button"
className="rounded-full border border-zinc-200 px-4 py-2 text-sm font-medium text-slate-600 transition hover:border-rose-200 hover:text-rose-700"
>
</Button>
<Button outline disabled>
一键归档
</button>
<button
type="button"
disabled={selectedIds.length === 0 || actionLoading}
onClick={() => handleDelete(selectedRecords)}
className="rounded-full border border-rose-200 bg-rose-50 px-4 py-2 text-sm font-medium text-rose-700 transition hover:bg-rose-100 disabled:cursor-not-allowed disabled:opacity-50"
>
</Button>
<Button color="red" disabled={selectedIds.length === 0 || actionLoading} onClick={() => handleDelete(selectedRecords)}>
删除选中
</button>
</Button>
</div>
</div>
{error ? (
<div className="border-b border-zinc-100 px-6 py-4 text-sm text-rose-600">
<div className="border-b border-zinc-100 bg-rose-50/70 px-6 py-4 text-sm text-rose-700">
{error}
</div>
) : null}
{loading ? (
<div className="px-6 py-5">
<EmptyState
title="正在加载问题整改列表"
description="请稍候。"
/>
<div className="px-6 py-6">
<EmptyState title="正在加载问题整改列表" description="正在读取对象结构、视图配置和记录数据,请稍候。" />
</div>
) : filteredRecords.length === 0 ? (
<div className="px-6 py-5">
<div className="px-6 py-6">
<EmptyState
title="暂无数据"
description="未查询到记录。"
title="当前视图暂无数据"
description="可以切换其他视图,或调整关键词、审批状态和整改进度筛选。"
/>
</div>
) : (
<div className="overflow-x-auto">
<table className="min-w-full border-separate border-spacing-0">
<thead>
<tr className="text-left text-sm text-slate-500">
<th className="border-b border-zinc-100 bg-zinc-50/80 px-4 py-4 font-medium">
<input
type="checkbox"
checked={
filteredRecords.length > 0 &&
selectedIds.length === filteredRecords.length
}
onChange={(event) => toggleAll(event.target.checked)}
/>
</th>
{[
'检查项目名称',
'反馈单位',
'整改进度',
'审批状态',
'最终审批',
'预计整改完成时间',
'问题类型',
'填写人 / 复核人',
'问题简述',
'操作',
].map((header) => (
<th
key={header}
className="border-b border-zinc-100 bg-zinc-50/80 px-6 py-4 font-medium"
<div className="px-3 pb-3 pt-2">
<Table bleed dense className="[--gutter:--spacing(3)]">
<TableHead>
<TableRow className="bg-zinc-50/80">
<TableHeader
className="w-14 sticky left-0 z-40 bg-zinc-50/95 backdrop-blur shadow-[10px_0_20px_rgba(15,23,42,0.06)]"
style={{ width: SELECTION_COLUMN_WIDTH, minWidth: SELECTION_COLUMN_WIDTH }}
>
{header}
</th>
))}
</tr>
</thead>
<tbody>
{filteredRecords.map((record, index) => (
<tr
key={record.id}
<div className="flex justify-center">
<Checkbox
checked={filteredRecords.length > 0 && selectedIds.length === filteredRecords.length}
onChange={toggleAll}
color="rose"
aria-label="全选当前视图记录"
/>
</div>
</TableHeader>
{stickyColumns.orderedColumns.map((column) => {
const stickyMeta = stickyColumns.stickyMeta[column.field];
return (
<TableHeader
key={column.field}
style={{
width: column.width || 180,
minWidth: Math.min(Math.max(column.width || 180, 150), 360),
left: stickyMeta?.side === 'left' ? `${stickyMeta.offset}px` : undefined,
right: stickyMeta?.side === 'right' ? `${stickyMeta.offset}px` : undefined,
}}
className={clsx(
'align-top text-sm text-slate-700',
index % 2 === 0 ? 'bg-white' : 'bg-zinc-50/45',
'text-xs font-semibold uppercase tracking-[0.18em] text-slate-400',
getStickyCellClass(stickyMeta, 'header'),
)}
>
<td className="border-b border-zinc-100 px-4 py-5">
<input
type="checkbox"
{column.label}
</TableHeader>
);
})}
<TableHeader
className="sticky right-0 z-40 bg-zinc-50/95 text-xs font-semibold uppercase tracking-[0.18em] text-slate-400 shadow-[-10px_0_20px_rgba(15,23,42,0.06)] backdrop-blur"
style={{ width: ACTION_COLUMN_WIDTH, minWidth: ACTION_COLUMN_WIDTH }}
>
操作
</TableHeader>
</TableRow>
</TableHead>
<TableBody>
{filteredRecords.map((record) => (
<TableRow key={record.id} className="align-top">
<TableCell
className="align-top sticky left-0 z-30 bg-white shadow-[10px_0_20px_rgba(15,23,42,0.05)]"
style={{ width: SELECTION_COLUMN_WIDTH, minWidth: SELECTION_COLUMN_WIDTH }}
>
<div className="flex justify-center pt-1">
<Checkbox
checked={selectedIds.includes(record.id)}
onChange={(event) =>
toggleRow(record.id, event.target.checked)
}
/>
</td>
<td className="max-w-[220px] border-b border-zinc-100 px-6 py-5">
<div className="font-semibold text-slate-900">
{record.projectName}
</div>
<div className="mt-2 text-xs leading-5 text-slate-500">
{record.inspectUnit || '未填写实施单位'}
</div>
</td>
<td className="max-w-[180px] border-b border-zinc-100 px-6 py-5">
{record.feedbackUnit || '-'}
</td>
<td className="border-b border-zinc-100 px-6 py-5">
<StatusPill
text={record.progress || '未填写'}
tone={resolveProgressTone(record.progress)}
/>
</td>
<td className="border-b border-zinc-100 px-6 py-5">
<StatusPill
text={record.auditStatus || '未填写'}
tone={resolveAuditTone(record.auditStatus)}
/>
</td>
<td className="border-b border-zinc-100 px-6 py-5">
<StatusPill
text={record.finalAuditStatus || '未填写'}
tone={resolveAuditTone(record.finalAuditStatus)}
onChange={(checked) => toggleRow(record.id, checked)}
color="rose"
aria-label={`选择 ${record.title || record.projectName || '当前记录'}`}
/>
</td>
<td className="border-b border-zinc-100 px-6 py-5">
{record.dueDateLabel}
</td>
<td className="max-w-[180px] border-b border-zinc-100 px-6 py-5">
<div>{record.problemType || '-'}</div>
<div className="mt-2 text-xs text-slate-500">
{record.problemFrom || '未填写来源'}
</div>
</td>
<td className="max-w-[220px] border-b border-zinc-100 px-6 py-5">
<div className="text-xs uppercase tracking-[0.18em] text-slate-400">
填写人
</div>
<div className="mt-1">{formatUsers(record.fillUsers)}</div>
<div className="mt-3 text-xs uppercase tracking-[0.18em] text-slate-400">
复核人
</div>
<div className="mt-1">{formatUsers(record.reviewUsers)}</div>
</td>
<td className="max-w-[340px] border-b border-zinc-100 px-6 py-5">
<div className="line-clamp-2 text-slate-700">
{record.brief || '暂无问题简述'}
</div>
<div className="mt-2 text-xs leading-5 text-slate-500">
{record.relatedIssueDomain || '未填写业务领域'}
</div>
</td>
<td className="border-b border-zinc-100 px-6 py-5">
<div className="flex flex-col gap-2">
<button
type="button"
onClick={() => handleDelete([record])}
className="rounded-full border border-rose-200 px-3 py-1 text-xs font-medium text-rose-700 transition hover:bg-rose-50"
</TableCell>
{stickyColumns.orderedColumns.map((column) => {
const stickyMeta = stickyColumns.stickyMeta[column.field];
return (
<TableCell
key={column.field}
style={{
width: column.width || 180,
minWidth: Math.min(Math.max(column.width || 180, 150), 360),
left: stickyMeta?.side === 'left' ? `${stickyMeta.offset}px` : undefined,
right: stickyMeta?.side === 'right' ? `${stickyMeta.offset}px` : undefined,
}}
className={clsx('align-top', getStickyCellClass(stickyMeta))}
>
<FieldValuePreview column={column} record={record} />
</TableCell>
);
})}
<TableCell
className="align-top sticky right-0 z-30 bg-white shadow-[-10px_0_20px_rgba(15,23,42,0.05)]"
style={{ width: ACTION_COLUMN_WIDTH, minWidth: ACTION_COLUMN_WIDTH }}
>
<div className="flex min-w-28 flex-col gap-2">
<Button plain onClick={() => handleOpenDrawer(record)}>
查看详情
</Button>
<Button outline color="red" onClick={() => handleDelete([record])}>
删除
</button>
</Button>
</div>
</td>
</tr>
</TableCell>
</TableRow>
))}
</tbody>
</table>
</TableBody>
</Table>
</div>
)}
</section>
......@@ -992,29 +1036,30 @@ export default function ProblemRectificationListPage() {
<Dialog open={showCreateModal} onClose={handleCloseCreateModal} size="5xl">
<DialogTitle>新增问题整改记录</DialogTitle>
<DialogDescription>
表单字段已按 <code className="rounded bg-zinc-100 px-1.5 py-0.5 text-xs">problem_rectification</code>{' '}
对象定义渲染,`select` 选项直接读取对象配置,日期字段会在提交时自动转成 ISO 8601。
当前新增表单按视图 <code className="rounded bg-zinc-100 px-1.5 py-0.5 text-xs">{activeGridView?.title || activeGridView?.name || '默认视图'}</code>{' '}
的字段顺序渲染,`select` 选项来自 Subject 配置,日期提交时会自动转为 ISO 8601。
</DialogDescription>
<DialogBody>
<div className="rounded-[24px] border border-zinc-100 bg-zinc-50/50 px-4 py-3 text-sm text-slate-500">
至少填写“检查项目名称”或“问题简述”之一。长文本字段已展开为更适合录入的输入区域。
<div className="rounded-[24px] border border-zinc-100 bg-zinc-50/60 px-4 py-3 text-sm text-slate-500">
至少填写“检查项目名称”或“问题简述”之一。当前只展示本视图所关心的字段,减少一次性录入负担。
</div>
<div className="mt-6 grid gap-5 md:grid-cols-2">
{createFields.map((fieldDef) => {
const isLongText = fieldDef.type === 'long_text';
const isSingleColumn = !isLongText && fieldDef.name !== 'responsible_department';
{createFields.map((field) => {
const fieldDef = field.fieldDef;
const isLongText = fieldDef?.type === 'long_text';
const fullWidth = isLongText || fieldDef?.multiple;
return (
<div
key={fieldDef.name}
key={field.field}
className={clsx(
'rounded-[24px] border border-zinc-200 bg-white/90 p-4 shadow-sm',
isSingleColumn ? '' : 'md:col-span-2',
'rounded-[24px] border border-zinc-200 bg-white/92 p-4 shadow-sm',
fullWidth && 'md:col-span-2',
)}
>
<label className="mb-2 block text-sm font-medium text-slate-700">
{fieldDef.label || fieldDef.name}
{field.label}
</label>
<CreateField
fieldDef={fieldDef}
......@@ -1027,9 +1072,7 @@ export default function ProblemRectificationListPage() {
}
/>
{FIELD_HINTS[fieldDef.name] ? (
<div className="mt-2 text-xs leading-5 text-slate-500">
{FIELD_HINTS[fieldDef.name]}
</div>
<div className="mt-2 text-xs leading-5 text-slate-500">{FIELD_HINTS[fieldDef.name]}</div>
) : null}
</div>
);
......@@ -1040,7 +1083,7 @@ export default function ProblemRectificationListPage() {
<Button outline onClick={handleCloseCreateModal}>
取消
</Button>
<Button color="blue" disabled={actionLoading || !createSubject} onClick={handleCreate}>
<Button color="blue" disabled={actionLoading || !subject} onClick={handleCreate}>
{actionLoading ? '提交中...' : '确认新增'}
</Button>
</DialogActions>
......@@ -1058,7 +1101,8 @@ export default function ProblemRectificationListPage() {
>
<DialogTitle>推送问题整改表</DialogTitle>
<DialogDescription>
已选择 {selectedRecords.length} 条记录。请通过远程搜索选择填报人和复核人。
已从视图 <code className="rounded bg-zinc-100 px-1.5 py-0.5 text-xs">{activeGridView?.title || activeGridView?.name || '-'}</code>{' '}
中选择 {selectedRecords.length} 条记录,请为本次流程配置填报人和复核人。
</DialogDescription>
<DialogBody>
<div className="grid gap-6 md:grid-cols-2">
......@@ -1077,6 +1121,7 @@ export default function ProblemRectificationListPage() {
placeholder="搜索复核人用户名或显示名"
/>
</div>
<div className="mt-6 rounded-[24px] border border-zinc-200 bg-zinc-50/80 p-4">
<div className="text-sm font-medium text-slate-900">本次推送记录</div>
<div className="mt-3 flex flex-wrap gap-2">
......@@ -1085,7 +1130,7 @@ export default function ProblemRectificationListPage() {
key={record.id}
className="rounded-full bg-white px-3 py-1 text-xs font-medium text-slate-700 shadow-sm ring-1 ring-zinc-200"
>
{record.projectName}
{record.title || record.projectName || record.name}
</span>
))}
</div>
......@@ -1108,6 +1153,16 @@ export default function ProblemRectificationListPage() {
</DialogActions>
</Dialog>
) : null}
{drawerRecordName ? (
<RecordDetailDrawer
open={Boolean(drawerRecordName)}
recordName={drawerRecordName}
setCustomComponentClose={handleCloseDrawer}
onDataChange={() => setDrawerDirty(true)}
preferredViewName={activeGridView?.title || activeGridView?.name}
/>
) : null}
</div>
);
}
......@@ -51,6 +51,19 @@
- 新增、编辑、查看详情优先采用抽屉或弹窗,不走重型嵌套表格交互
- 批量操作放在顶部工具栏,不再依赖右键菜单作为唯一入口
### 3.3 视图配置驱动约定
问题整改主列表与详情抽屉统一以 `getSubjectView(subjectName)` 返回的视图配置驱动,不再在页面内写死部门标签和字段顺序:
- `type = "grid"` 的视图用于主列表顶部视图切换,替代原先写死的 tab
- `settings.columns` 决定列表列顺序、宽度和排序优先级
- `settings.restrictions` 与 `settings.filters.conditions` 共同决定该视图下可见的数据范围
- `settings.viewAuth` 决定当前用户是否能看到该视图
- `type = "detail"` 的视图用于详情抽屉字段编排,按 `columns` 顺序渲染字段
- `settings.auth` 与 `disableColumns` 用于控制详情抽屉内字段是否允许编辑
因此前端新增字段展示、部门视图调整、列顺序变化时,应优先改 Subject View 配置,而不是直接修改页面常量。
### 3.1 目标页面拆分
为便于当前项目直接重构,建议把模块拆成以下 4 个页面入口:
......@@ -179,6 +192,13 @@
- 表格内分组拖拽
- 全屏容器能力
替代实现约定:
- 列表页保留表格浏览、搜索、筛选、批量选择
- 字段编辑统一进入右侧详情抽屉
- 长文本、附件、状态类字段在详情抽屉中直接维护
- 详情抽屉内字段顺序与显示权限跟随 `detail view` 配置
---
## 6. 流程角色
......@@ -624,6 +644,7 @@ const result = await queryUsers(0, 20, {
布局层负责:
- 左侧菜单
- 桌面展开态仅展示模块名称,去掉大 Logo;仅在桌面折叠态展示小 Logo
- 顶部用户区
- 内容容器
- 移动端侧栏开关
......
......@@ -7,6 +7,7 @@ import {
import { DEPARTMENT_GROUPS } from './problemRectificationDashboardService';
import {
buildViewColumns,
checkViewCondition,
emptyValueForField,
formatFieldValue,
getFieldNameFromPath,
......@@ -162,6 +163,18 @@ export function buildProblemRectificationConfiguredListView(records, filters, gr
return false;
}
if (
!checkViewCondition(
{
relationship: gridView?.settings?.filters?.relationship || 'all',
rules: gridView?.settings?.filters?.conditions || [],
},
rawRecord,
)
) {
return false;
}
if (nextProgress && record.progress !== nextProgress) {
return false;
}
......@@ -200,6 +213,18 @@ export function buildProblemRectificationConfiguredListView(records, filters, gr
};
}
export function buildGridViewCountMap(records, filters, views, subject) {
return views.reduce((counts, view) => {
counts[view.name] = buildProblemRectificationConfiguredListView(
records,
filters,
view,
subject,
).filteredRecords.length;
return counts;
}, {});
}
function matchesDepartmentGroup(record, groupKey) {
if (!groupKey || groupKey === 'all') {
return true;
......
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