Commit 64fcb2ae authored by drigle's avatar drigle

fix: 优化列表分页并移除页面描述

parent ae0fc992
import { Pagination } from '@heroui/react';
import XinyuanAutocomplete from '@/components/XinyuanAutocomplete';
const MAX_PAGE_BUTTONS = 5;
const PAGE_SIZE_OPTIONS = [20, 50, 100];
export default function ListPagination({
page = 0,
pageSize,
total = 0,
totalPages: totalPagesProp,
isDisabled = false,
onPageChange,
onPageSizeChange,
}) {
const computedTotalPages = Math.max(1, Math.ceil(total / pageSize));
const totalPages = Math.max(1, numberOr(totalPagesProp, computedTotalPages));
const currentPage = Math.min(Math.max(0, numberOr(page, 0)), totalPages - 1);
const windowStart = Math.max(0, Math.min(currentPage - 2, totalPages - MAX_PAGE_BUTTONS));
const windowEnd = Math.min(totalPages, windowStart + MAX_PAGE_BUTTONS);
const pageNumbers = Array.from({ length: windowEnd - windowStart }, (_, index) => windowStart + index);
const pageSizeOptions = PAGE_SIZE_OPTIONS.map((value) => ({ value, label: `${value} 条/页` }));
if (total <= 0) return null;
return (
<div className="flex min-w-0 flex-wrap items-center justify-between gap-3 border-t border-separator pt-4">
<div className="flex min-w-0 shrink-0 items-center gap-3">
<Pagination.Summary className="text-sm text-muted">
共 {total} 条,第 {currentPage + 1} / {totalPages} 页
</Pagination.Summary>
<XinyuanAutocomplete
aria-label="每页条数"
className="w-32 shrink-0"
isDisabled={isDisabled}
options={pageSizeOptions}
placeholder="每页条数"
selectedKey={String(pageSize)}
showSearch={false}
valueText={`${pageSize} 条/页`}
variant="secondary"
onSelectionChange={(value) => {
const nextPageSize = Number(value);
if (PAGE_SIZE_OPTIONS.includes(nextPageSize)) onPageSizeChange?.(nextPageSize);
}}
/>
</div>
<Pagination aria-label="列表分页" className="ml-auto w-auto shrink-0" size="lg">
<Pagination.Content>
<Pagination.Item>
<Pagination.Previous
isDisabled={isDisabled || currentPage <= 0}
onPress={() => onPageChange(currentPage - 1)}
>
<Pagination.PreviousIcon />
<span>上一页</span>
</Pagination.Previous>
</Pagination.Item>
{pageNumbers.map((pageNumber) => (
<Pagination.Item key={pageNumber}>
<Pagination.Link
isActive={pageNumber === currentPage}
isDisabled={isDisabled}
onPress={() => onPageChange(pageNumber)}
>
{pageNumber + 1}
</Pagination.Link>
</Pagination.Item>
))}
<Pagination.Item>
<Pagination.Next
isDisabled={isDisabled || currentPage + 1 >= totalPages}
onPress={() => onPageChange(currentPage + 1)}
>
<span>下一页</span>
<Pagination.NextIcon />
</Pagination.Next>
</Pagination.Item>
</Pagination.Content>
</Pagination>
</div>
);
}
function numberOr(value, fallback) {
const number = Number(value);
return Number.isFinite(number) ? number : fallback;
}
import { Button, Input, Label, Pagination, Spinner, TextField } from '@heroui/react';
import { Button, Input, Label, Spinner, TextField } from '@heroui/react';
import { Download, Search } from '@/components/AppIcons';
import ListPagination from '@/components/ListPagination';
import XinyuanAutocomplete from '@/components/XinyuanAutocomplete';
import { useEffect, useState } from 'react';
import { history } from 'umi';
......@@ -12,20 +13,23 @@ export default function ArchiveList() {
const [searchText, setSearchText] = useState('');
const [keyword, setKeyword] = useState('');
const [page, setPage] = useState(0);
const [pageSize, setPageSize] = useState(20);
const [records, setRecords] = useState([]);
const [total, setTotal] = useState(0);
const [totalPages, setTotalPages] = useState(1);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const refresh = async () => {
try {
setLoading(true);
const result = await loadArchivedRegulations({ archive, keyword, page, pageSize: 20 });
setRecords(result.records); setTotal(result.total); setError('');
const result = await loadArchivedRegulations({ archive, keyword, page, pageSize });
setRecords(result.records); setTotal(result.total); setTotalPages(result.totalPages); setError('');
} catch (loadError) { setError(loadError?.message || '归档历史加载失败。'); } finally { setLoading(false); }
};
useEffect(() => { loadArchiveOptions().then((values) => setArchiveOptions(values.map((value) => ({ value, label: value })))).catch(() => setArchiveOptions([])); }, []);
useEffect(() => { refresh(); }, [archive, keyword, page]);
const changePageSize = (nextPageSize) => { setPageSize(nextPageSize); setPage(0); };
useEffect(() => { refresh(); }, [archive, keyword, page, pageSize]);
const submitSearch = () => {
setPage(0);
......@@ -49,5 +53,5 @@ export default function ArchiveList() {
} catch (exportError) { setError(exportError?.message || '导出失败。'); }
};
return <div className="space-y-4"><div className="flex flex-col gap-3 lg:flex-row lg:items-end lg:justify-between"><div className="flex flex-col gap-3 sm:flex-row sm:items-end"><XinyuanAutocomplete className="w-full sm:w-56" label="归档批次" options={[{ value: '', label: '全部归档' }, ...archiveOptions]} selectedKey={archive} onSelectionChange={(value) => { setArchive(String(value || '')); setPage(0); }} /><div className="flex w-full items-end gap-2 sm:w-72"><TextField className="min-w-0 flex-1" fullWidth value={searchText} onChange={setSearchText} onKeyDown={(event) => { if (event.key === 'Enter') { event.preventDefault(); submitSearch(); } }}><Label className="sr-only">搜索归档制度</Label><div className="relative"><Search className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-muted" size={16} /><Input className="pl-9" fullWidth placeholder="搜索制度名称" variant="secondary" /></div></TextField><Button isDisabled={loading} variant="secondary" onPress={submitSearch}>查询</Button></div></div><div className="flex items-center gap-3"><span className="text-sm text-muted">归档记录只读展示,不会修改现行制度</span><Button variant="tertiary" onPress={exportList}><Download size={16} />批量下载</Button></div></div>{error ? <div className="rounded-xl border border-danger/30 bg-danger/5 p-3 text-sm text-danger">{error}</div> : null}{loading ? <div className="flex min-h-48 items-center justify-center"><Spinner label="正在加载归档记录..." /></div> : records.length === 0 ? <div className="rounded-2xl border border-dashed border-separator p-10 text-center text-sm text-muted">暂无归档记录</div> : <div className="overflow-x-auto rounded-2xl border border-separator"><table className="w-full min-w-[900px] text-left text-sm"><thead className="bg-surface-secondary text-muted"><tr>{['制度名称', '时效性', '效力位阶', '审批主体', '制定部门', '发文日期', '归档批次', '操作'].map((title) => <th className="px-4 py-3 font-medium" key={title}>{title}</th>)}</tr></thead><tbody className="divide-y divide-separator">{records.map((record) => { const metadata = record.metadata || {}; return <tr key={record._id || record.name}><td className="px-4 py-3"><button className="font-medium text-accent hover:underline" onClick={() => history.push(`/internal_regulation/${metadata.origin_regulation_id || record.name}`)}>{metadata.regulation_name || record.title || '-'}</button></td><td className="px-4 py-3">{metadata.effective_status || '-'}</td><td className="px-4 py-3">{metadata.level || '-'}</td><td className="max-w-48 truncate px-4 py-3">{Array.isArray(metadata.approve) ? metadata.approve.join('、') : metadata.approve || '-'}</td><td className="max-w-48 truncate px-4 py-3">{Array.isArray(metadata.departments) ? metadata.departments.join('、') : metadata.departments || '-'}</td><td className="px-4 py-3">{metadata.public_date ? new Date(metadata.public_date).toLocaleDateString('zh-CN') : '-'}</td><td className="px-4 py-3">{metadata.archive || '-'}</td><td className="px-4 py-3"><Button size="sm" variant="tertiary" onPress={() => history.push(`/internal_regulation/${metadata.origin_regulation_id || record.name}`)}>查看详情</Button></td></tr>; })}</tbody></table></div>}{total > 20 ? <Pagination className="justify-end" size="sm"><Pagination.Content><Pagination.Item><Pagination.Previous isDisabled={page === 0} onPress={() => setPage((value) => Math.max(0, value - 1))} /></Pagination.Item><Pagination.Item><Pagination.Link isActive>{page + 1}</Pagination.Link></Pagination.Item><Pagination.Item><Pagination.Next isDisabled={(page + 1) * 20 >= total} onPress={() => setPage((value) => value + 1)} /></Pagination.Item></Pagination.Content></Pagination> : null}</div>;
return <div className="space-y-4"><div className="flex flex-col gap-3 lg:flex-row lg:items-end lg:justify-between"><div className="flex flex-col gap-3 sm:flex-row sm:items-end"><XinyuanAutocomplete className="w-full sm:w-56" label="归档批次" options={[{ value: '', label: '全部归档' }, ...archiveOptions]} selectedKey={archive} onSelectionChange={(value) => { setArchive(String(value || '')); setPage(0); }} /><div className="flex w-full items-end gap-2 sm:w-72"><TextField className="min-w-0 flex-1" fullWidth value={searchText} onChange={setSearchText} onKeyDown={(event) => { if (event.key === 'Enter') { event.preventDefault(); submitSearch(); } }}><Label className="sr-only">搜索归档制度</Label><div className="relative"><Search className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-muted" size={16} /><Input className="pl-9" fullWidth placeholder="搜索制度名称" variant="secondary" /></div></TextField><Button isDisabled={loading} variant="secondary" onPress={submitSearch}>查询</Button></div></div><div className="flex items-center gap-3"><span className="text-sm text-muted">归档记录只读展示,不会修改现行制度</span><Button variant="tertiary" onPress={exportList}><Download size={16} />批量下载</Button></div></div>{error ? <div className="rounded-xl border border-danger/30 bg-danger/5 p-3 text-sm text-danger">{error}</div> : null}{loading ? <div className="flex min-h-48 items-center justify-center"><Spinner label="正在加载归档记录..." /></div> : records.length === 0 ? <div className="rounded-2xl border border-dashed border-separator p-10 text-center text-sm text-muted">暂无归档记录</div> : <div className="overflow-x-auto rounded-2xl border border-separator"><table className="w-full min-w-[900px] text-left text-sm"><thead className="bg-surface-secondary text-muted"><tr>{['制度名称', '时效性', '效力位阶', '审批主体', '制定部门', '发文日期', '归档批次', '操作'].map((title) => <th className="px-4 py-3 font-medium" key={title}>{title}</th>)}</tr></thead><tbody className="divide-y divide-separator">{records.map((record) => { const metadata = record.metadata || {}; return <tr key={record._id || record.name}><td className="px-4 py-3"><button className="font-medium text-accent hover:underline" onClick={() => history.push(`/internal_regulation/${metadata.origin_regulation_id || record.name}`)}>{metadata.regulation_name || record.title || '-'}</button></td><td className="px-4 py-3">{metadata.effective_status || '-'}</td><td className="px-4 py-3">{metadata.level || '-'}</td><td className="max-w-48 truncate px-4 py-3">{Array.isArray(metadata.approve) ? metadata.approve.join('、') : metadata.approve || '-'}</td><td className="max-w-48 truncate px-4 py-3">{Array.isArray(metadata.departments) ? metadata.departments.join('、') : metadata.departments || '-'}</td><td className="px-4 py-3">{metadata.public_date ? new Date(metadata.public_date).toLocaleDateString('zh-CN') : '-'}</td><td className="px-4 py-3">{metadata.archive || '-'}</td><td className="px-4 py-3"><Button size="sm" variant="tertiary" onPress={() => history.push(`/internal_regulation/${metadata.origin_regulation_id || record.name}`)}>查看详情</Button></td></tr>; })}</tbody></table></div>}<ListPagination isDisabled={loading} page={page} pageSize={pageSize} total={total} totalPages={totalPages} onPageChange={setPage} onPageSizeChange={changePageSize}></ListPagination></div>;
}
......@@ -64,7 +64,6 @@ export default function InternalRegulationManager() {
<div>
<p className="text-sm text-muted">内部制度管理</p>
<h1 className="mt-1 text-2xl font-semibold tracking-tight">内部制度</h1>
<p className="mt-2 max-w-3xl text-sm leading-6 text-muted">统一管理制度目录、修订计划和归档历史,所有业务字段均通过平台 Record 的 metadata 保存。</p>
</div>
</div>
<Card className="rc-card-surface">
......
......@@ -455,7 +455,7 @@ export default function RegulationGraph() {
if (loading) return <div className="flex min-h-72 items-center justify-center"><Spinner label="正在加载制度拓扑图..." /></div>;
return (
<div className="flex h-[calc(100dvh-7.5rem)] min-h-0 flex-col gap-4 overflow-hidden" onClick={() => setContextMenu(null)}>
<div><h1 className="text-2xl font-semibold">制度拓扑图</h1><p className="mt-1 text-sm text-muted">节点、连线、状态筛选、版本切换和 XMind 流程归类均可直接编辑。</p></div>
<div><h1 className="text-2xl font-semibold">制度拓扑图</h1></div>
{error ? <div role="alert" className="rounded-xl border border-danger/30 bg-danger/5 p-3 text-sm text-danger">{error}</div> : null}
{notice ? <div role="status" className="rounded-xl border border-success/30 bg-success/5 p-3 text-sm text-success">{notice}</div> : null}
<div className="flex min-h-0 flex-1 overflow-hidden rounded-2xl border border-separator bg-background shadow-sm" ref={canvasRef}>
......
import { Button, Chip, Input, Label, Pagination, Spinner, TextArea, TextField } from '@heroui/react';
import { Button, Chip, Input, Label, Spinner, TextArea, TextField } from '@heroui/react';
import { Download, Edit, Eye, PlusIcon, Search } from '@/components/AppIcons';
import ListPagination from '@/components/ListPagination';
import XinyuanAutocomplete from '@/components/XinyuanAutocomplete';
import XinyuanModal from '@/components/XinyuanModal';
import { useEffect, useMemo, useState } from 'react';
......@@ -32,7 +33,9 @@ export default function RegulationList({ options = {} }) {
const [searchText, setSearchText] = useState('');
const [keyword, setKeyword] = useState('');
const [page, setPage] = useState(0);
const [pageSize, setPageSize] = useState(PAGE_SIZE);
const [total, setTotal] = useState(0);
const [totalPages, setTotalPages] = useState(1);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [dialogOpen, setDialogOpen] = useState(false);
......@@ -45,9 +48,10 @@ export default function RegulationList({ options = {} }) {
try {
setLoading(true);
const activeFilters = Object.fromEntries(Object.entries(filters).filter(([, value]) => value).map(([key, value]) => [`metadata.${key}`, ['departments', 'approve'].includes(key) ? { $in: [value] } : value]));
const result = await loadRegulations({ page, pageSize: PAGE_SIZE, keyword, filters: activeFilters });
const result = await loadRegulations({ page, pageSize, keyword, filters: activeFilters });
setRecords(result.records);
setTotal(result.total);
setTotalPages(result.totalPages);
setError('');
} catch (loadError) {
setError(loadError?.message || '制度目录加载失败。');
......@@ -61,7 +65,12 @@ export default function RegulationList({ options = {} }) {
setKeyword(searchText.trim());
};
useEffect(() => { refresh(); }, [page, keyword, filters]);
const changePageSize = (nextPageSize) => {
setPageSize(nextPageSize);
setPage(0);
};
useEffect(() => { refresh(); }, [page, pageSize, keyword, filters]);
useEffect(() => {
const open = () => { setEditing(null); setForm(INITIAL_FORM); setDialogOpen(true); };
window.addEventListener('internal-regulation:create', open);
......@@ -142,7 +151,7 @@ export default function RegulationList({ options = {} }) {
</table>
</div>
)}
{total > PAGE_SIZE ? <Pagination className="justify-end" size="sm"><Pagination.Content><Pagination.Item><Pagination.Previous isDisabled={page === 0} onPress={() => setPage((value) => Math.max(0, value - 1))} /></Pagination.Item><Pagination.Item><Pagination.Link isActive>{page + 1}</Pagination.Link></Pagination.Item><Pagination.Item><Pagination.Next isDisabled={(page + 1) * PAGE_SIZE >= total} onPress={() => setPage((value) => value + 1)} /></Pagination.Item></Pagination.Content></Pagination> : null}
<ListPagination isDisabled={loading} page={page} pageSize={pageSize} total={total} totalPages={totalPages} onPageChange={setPage} onPageSizeChange={changePageSize} />
<XinyuanModal isOpen={dialogOpen} onOpenChange={setDialogOpen} size="2xl" title={editing ? '编辑修订计划' : '创建修订计划'} footer={<div className="flex justify-end gap-2"><Button variant="tertiary" onPress={() => setDialogOpen(false)}>取消</Button><Button isDisabled={saving || !form.regulation_name.trim()} variant="primary" onPress={submit}>{saving ? '保存中...' : '保存'}</Button></div>}>
<div className="grid gap-4 sm:grid-cols-2">
<TextField className="sm:col-span-2" fullWidth isRequired value={form.regulation_name} onChange={(value) => setForm((current) => ({ ...current, regulation_name: value }))}><Label>制度名称</Label><Input fullWidth placeholder="请输入制度名称" variant="secondary" /></TextField>
......
import { Button, Chip, Input, Label, Pagination, Spinner, TextArea, TextField } from '@heroui/react';
import { Button, Chip, Input, Label, Spinner, TextArea, TextField } from '@heroui/react';
import { Archive, Check, Close, Download, Edit, FileText, PaperClipIcon, Search, Upload } from '@/components/AppIcons';
import ListPagination from '@/components/ListPagination';
import XinyuanAutocomplete from '@/components/XinyuanAutocomplete';
import XinyuanModal from '@/components/XinyuanModal';
import { useEffect, useState } from 'react';
......@@ -41,6 +42,8 @@ export default function RevisionList({ options = {} }) {
const [records, setRecords] = useState([]);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(0);
const [pageSize, setPageSize] = useState(20);
const [totalPages, setTotalPages] = useState(1);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [editing, setEditing] = useState(null);
......@@ -55,15 +58,17 @@ export default function RevisionList({ options = {} }) {
const load = async () => {
try {
setLoading(true);
const result = await loadRevisions({ status, keyword, page, pageSize: 20 });
const result = await loadRevisions({ status, keyword, page, pageSize });
setRecords(result.records);
setTotal(result.total);
setTotalPages(result.totalPages);
setError('');
} catch (loadError) {
setError(loadError?.message || '修订计划加载失败。');
} finally { setLoading(false); }
};
useEffect(() => { load(); }, [status, keyword, page]);
const changePageSize = (nextPageSize) => { setPageSize(nextPageSize); setPage(0); };
useEffect(() => { load(); }, [status, keyword, page, pageSize]);
const submitSearch = () => {
setPage(0);
......@@ -131,7 +136,7 @@ export default function RevisionList({ options = {} }) {
{loading ? <div className="flex min-h-48 items-center justify-center"><Spinner label="正在加载修订计划..." /></div> : records.length === 0 ? <div className="rounded-2xl border border-dashed border-separator p-10 text-center text-sm text-muted">当前状态暂无修订计划</div> : (
<div className="overflow-x-auto rounded-2xl border border-separator"><table className="w-full min-w-[1120px] text-left text-sm"><thead className="bg-surface-secondary text-muted"><tr>{['类型', '制度名称', 'OA流程制度', '发起日期', '审批主体', '制定部门', '附件', '状态', '操作'].map((title) => <th className="px-4 py-3 font-medium" key={title}>{title}</th>)}</tr></thead><tbody className="divide-y divide-separator">{records.map((record) => { const metadata = record.metadata || {}; const attachments = Array.isArray(metadata.attachments) ? metadata.attachments : metadata.attachments ? [metadata.attachments] : []; return <tr className="hover:bg-surface-secondary/50" key={record._id || record.name}><td className="px-4 py-3"><Chip color={metadata.type === '修订' ? 'warning' : metadata.type === '废止' ? 'default' : 'accent'} size="sm" variant="soft"><Chip.Label>待{metadata.type || '-'}</Chip.Label></Chip></td><td className="px-4 py-3 font-medium">{metadata.regulation_name || record.title || '-'}</td><td className="max-w-48 truncate px-4 py-3 text-muted">{metadata.regulation_name_oa || metadata.regulation_name_oa_submitted || '-'}</td><td className="px-4 py-3">{formatDate(metadata.created_at)}</td><td className="max-w-48 truncate px-4 py-3">{Array.isArray(metadata.approve) ? metadata.approve.join('、') : metadata.approve || '-'}</td><td className="max-w-48 truncate px-4 py-3">{Array.isArray(metadata.departments) ? metadata.departments.join('、') : metadata.departments || '-'}</td><td className="px-4 py-3">{attachments.length ? <div className="flex flex-col gap-1">{attachments.map((attachment, index) => { const href = typeof attachment === 'string' ? attachment : attachment.url; const title = typeof attachment === 'string' ? `附件 ${index + 1}` : attachment.title || attachment.name || `附件 ${index + 1}`; return href ? <a className="flex items-center gap-1 text-accent hover:underline" href={href} key={attachment._id || attachment.name || index} rel="noreferrer" target="_blank"><PaperClipIcon size={14} />{title}</a> : <span className="text-muted" key={attachment._id || attachment.name || index}>{title}</span>; })}</div> : '-'}</td><td className="px-4 py-3"><Chip color={statusColor(metadata.status)} size="sm" variant="soft"><Chip.Label>{metadata.status}</Chip.Label></Chip></td><td className="px-4 py-3"><div className="flex items-center gap-1">{status === '起草中' ? <><Button isIconOnly aria-label="取消修订" isDisabled={saving} size="sm" variant="tertiary" onPress={() => submitStatus(record, '已取消')}><Close size={16} /></Button><Button isIconOnly aria-label="提交审核" isDisabled={saving} size="sm" variant="tertiary" onPress={() => submitStatus(record, '已提交')}><Check size={16} /></Button><Button isIconOnly aria-label="编辑修订" size="sm" variant="tertiary" onPress={() => openEdit(record)}><Edit size={16} /></Button></> : null}{status === '已提交' ? <><Button isIconOnly aria-label="取消审核" isDisabled={saving} size="sm" variant="tertiary" onPress={() => submitStatus(record, '已取消')}><Close size={16} /></Button><Button isIconOnly aria-label="提交发文" isDisabled={saving} size="sm" variant="tertiary" onPress={() => { setEditing(record); setPublishForm({ public_date: '', attachments: [], content: '' }); setPublishOpen(true); }}><FileText size={16} /></Button></> : null}{status === '已取消' ? <Button isIconOnly aria-label="查看修订" size="sm" variant="tertiary" onPress={() => openEdit(record)}><Edit size={16} /></Button> : null}</div></td></tr>; })}</tbody></table></div>
)}
{total > 20 ? <Pagination className="justify-end" size="sm"><Pagination.Content><Pagination.Item><Pagination.Previous isDisabled={page === 0} onPress={() => setPage((value) => Math.max(0, value - 1))} /></Pagination.Item><Pagination.Item><Pagination.Link isActive>{page + 1}</Pagination.Link></Pagination.Item><Pagination.Item><Pagination.Next isDisabled={(page + 1) * 20 >= total} onPress={() => setPage((value) => value + 1)} /></Pagination.Item></Pagination.Content></Pagination> : null}
<ListPagination isDisabled={loading} page={page} pageSize={pageSize} total={total} totalPages={totalPages} onPageChange={setPage} onPageSizeChange={changePageSize} />
<XinyuanModal isOpen={editOpen} onOpenChange={setEditOpen} size="2xl" title={editing ? '编辑修订计划' : '创建修订计划'} footer={<div className="flex justify-end gap-2"><Button variant="tertiary" onPress={() => setEditOpen(false)}>取消</Button><Button isDisabled={saving || !form.regulation_name.trim()} variant="primary" onPress={saveEdit}>{saving ? '保存中...' : '保存'}</Button></div>}>
<div className="grid gap-4 sm:grid-cols-2"><TextField className="sm:col-span-2" fullWidth isRequired value={form.regulation_name} onChange={(value) => setForm((current) => ({ ...current, regulation_name: value }))}><Label>制度名称</Label><Input fullWidth variant="secondary" /></TextField><XinyuanAutocomplete label="类型" options={['新增', '修订', '废止'].map((value) => ({ value, label: value }))} selectedKey={form.type} onSelectionChange={(value) => setForm((current) => ({ ...current, type: String(value) }))} /><XinyuanAutocomplete label="层级" options={REGULATION_LEVELS.map((value) => ({ value, label: value }))} selectedKey={form.level} onSelectionChange={(value) => setForm((current) => ({ ...current, level: String(value) }))} /><XinyuanAutocomplete label="审批主体" options={optionsFor('approve')} selectedKeys={new Set(form.approve || [])} selectionMode="multiple" onSelectionChange={(values) => setForm((current) => ({ ...current, approve: values === 'all' ? optionsFor('approve').map((item) => item.value) : Array.from(values || []) }))} /><XinyuanAutocomplete label="制定部门" options={optionsFor('departments')} selectedKeys={new Set(form.departments || [])} selectionMode="multiple" onSelectionChange={(values) => setForm((current) => ({ ...current, departments: values === 'all' ? optionsFor('departments').map((item) => item.value) : Array.from(values || []) }))} /><TextField className="sm:col-span-2" fullWidth value={form.comment} onChange={(value) => setForm((current) => ({ ...current, comment: value }))}><Label>备注</Label><TextArea fullWidth rows={4} variant="secondary" /></TextField></div>
......
......@@ -39,5 +39,6 @@
## 加载、错误与限制
- Subject、Record、附件请求均显示 loading、success 或 error 状态;错误保留在当前页面,不使用演示数据或 localStorage 回退。
- 制度目录、修订计划和归档历史列表使用服务端分页;每页条数可选 `20 / 50 / 100`,切换条数后回到第 1 页。页码和页数读取 `/record/query` 返回的 `current_page` 与 `total_pages`,列表总数读取当前筛选条件对应的 `total_records`。
- 保存、提交、发文期间禁用重复提交按钮;失败时保留表单内容。
- 归档记录不修改现行制度;不存在的制度或修订计划显示明确空状态。
......@@ -454,7 +454,6 @@ export default function ForwardApprovals() {
<HeroBanner
eyebrow="模型监控"
title="转发审批"
description="集中处理从模型批次数据发起的转发、填写和两级顺序审批。终审只能在接收人完成填写且一级审批通过后推进。"
/>
{loading ? <NoticeBanner>正在加载转发审批记录...</NoticeBanner> : null}
......
......@@ -223,7 +223,7 @@ export default function ModelBatchData() {
return (
<div className="space-y-5">
<PageHeader eyebrow={batch?.batchCode || '模型批次'} title={batch?.batchName || '批次数据'} description="展示当前批次已同步的全部业务字段。页面不提供字段筛选或图表分析,勾选记录后可隐藏或进入转发审批。" />
<PageHeader eyebrow={batch?.batchCode || '模型批次'} title={batch?.batchName || '批次数据'} />
{loading ? <NoticeBanner>正在加载批次数据和转发状态...</NoticeBanner> : null}
{error ? <NoticeBanner tone="error">{error}</NoticeBanner> : null}
{!error && notice ? <NoticeBanner tone="success">{notice}</NoticeBanner> : null}
......
......@@ -150,7 +150,7 @@ export default function ModelBatches() {
return (
<div className="space-y-5">
<PageHeader eyebrow={model?.modelCode || '模型'} title={model?.modelName || '模型批次'} description="这里仅展示已经同步到本系统的批次。外部未同步批次通过右侧同步入口选择。" />
<PageHeader eyebrow={model?.modelCode || '模型'} title={model?.modelName || '模型批次'} />
{loading ? <NoticeBanner>正在加载模型批次...</NoticeBanner> : null}
{error ? <NoticeBanner tone="error">{error}</NoticeBanner> : null}
......
......@@ -120,7 +120,6 @@ export default function ModelOverview() {
<PageHeader
eyebrow="模型监控"
title="模型运行概览"
description="集中查看已同步模型、批次数据量与最近同步结果。这里仅呈现运行状态,不再提供字段分析或图表配置。"
/>
{loading ? <NoticeBanner>正在初始化模型监控对象并加载概览...</NoticeBanner> : null}
......
......@@ -455,7 +455,7 @@ export default function Models() {
return (
<div>
<PageHeader eyebrow="模型监控" title="全部模型" description="模型由同步自动创建。你可以用无限级分组、标签和说明整理模型,点击模型后进入独立批次页面。" />
<PageHeader eyebrow="模型监控" title="全部模型" />
<div className="space-y-8">
{loading ? <NoticeBanner>正在初始化模型对象并加载列表...</NoticeBanner> : null}
{error ? <NoticeBanner tone="error">{error}</NoticeBanner> : null}
......
......@@ -19,7 +19,38 @@ const REGULATION_LEVELS = ['1级', '2级', '3级', '4级'];
const REVISION_TYPES = ['新增', '修订', '废止'];
function recordsOf(response) {
return Array.isArray(response?.records) ? response.records : [];
if (Array.isArray(response?.records)) return response.records;
if (Array.isArray(response?.data?.records)) return response.data.records;
return Array.isArray(response?.data) ? response.data : [];
}
function totalOf(response, records = []) {
const candidates = [
response?.total_records,
response?.data?.total_records,
response?.total_count,
response?.data?.total_count,
response?.total,
response?.data?.total,
response?.count,
response?.data?.count,
];
const total = candidates.find((value) => value !== null && value !== undefined && Number.isFinite(Number(value)));
return total === undefined ? records.length : Number(total);
}
function numberOr(value, fallback) {
const number = Number(value);
return Number.isFinite(number) ? number : fallback;
}
function paginationOf(response, fallbackPage, fallbackPageSize, total) {
const pageSize = numberOr(response?.page_size, fallbackPageSize);
return {
currentPage: numberOr(response?.current_page, fallbackPage),
pageSize,
totalPages: numberOr(response?.total_pages, Math.max(1, Math.ceil(total / pageSize))),
};
}
function metadataOf(record) {
......@@ -73,7 +104,7 @@ export async function queryAll(subject, { filter = {}, sort = '', select = null
const result = await queryRecords({ subject, page, page_size: 1000, filter, sort, select, mode: 'full' });
const records = recordsOf(result);
all.push(...records);
if (records.length < 1000 || all.length >= Number(result?.total_records || result?.total || 0)) break;
if (records.length < 1000 || all.length >= totalOf(result)) break;
}
return all;
}
......@@ -116,7 +147,8 @@ export async function loadRegulations({ page = 0, pageSize = 20, keyword = '', f
revisions: histories,
};
}),
total: Number(result?.total_records || result?.total || records.length),
total: totalOf(result, records),
...paginationOf(result, page, pageSize, totalOf(result, records)),
};
}
......@@ -130,7 +162,9 @@ export async function loadRevisions({ page = 0, pageSize = 20, status = '起草
const filter = { 'metadata.status': status };
if (keyword.trim()) filter['metadata.regulation_name'] = { $regex: keyword.trim() };
const result = await queryRecords({ subject: REVISION_SUBJECT, page, page_size: pageSize, filter, sort: '-metadata.created_at', mode: 'full' });
return { records: recordsOf(result), total: Number(result?.total_records || result?.total || recordsOf(result).length) };
const records = recordsOf(result);
const total = totalOf(result, records);
return { records, total, ...paginationOf(result, page, pageSize, total) };
}
export async function loadArchiveOptions() {
......@@ -143,7 +177,9 @@ export async function loadArchivedRegulations({ archive = '', page = 0, pageSize
if (archive) filter['metadata.archive'] = archive;
if (keyword.trim()) filter['metadata.regulation_name'] = { $regex: keyword.trim() };
const result = await queryRecords({ subject: ARCHIVE_SUBJECT, page, page_size: pageSize, filter, sort: '-metadata.public_date', mode: 'full' });
return { records: recordsOf(result), total: Number(result?.total_records || result?.total || recordsOf(result).length) };
const records = recordsOf(result);
const total = totalOf(result, records);
return { records, total, ...paginationOf(result, page, pageSize, total) };
}
export async function createRevision(values = {}) {
......
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