Commit a6b49d2c authored by drigle's avatar drigle

fix: 制度目录行操作对齐 streams-admin 修订/废止/备注逻辑

parent f82fd5d5
import { Button, Chip, Input, Label, Spinner, TextArea, TextField } from '@heroui/react'; import { Button, Chip, Input, Label, Spinner, TextArea, TextField, toast } from '@heroui/react';
import { Download, Edit, Eye, PlusIcon, Search } from '@/components/AppIcons'; import { Close, Download, Edit, Eye, FileText, PlusIcon, Search } from '@/components/AppIcons';
import ListPagination from '@/components/ListPagination'; import ListPagination from '@/components/ListPagination';
import XinyuanAutocomplete from '@/components/XinyuanAutocomplete'; import XinyuanAutocomplete from '@/components/XinyuanAutocomplete';
import XinyuanModal from '@/components/XinyuanModal'; import XinyuanModal from '@/components/XinyuanModal';
...@@ -11,11 +11,11 @@ import { ...@@ -11,11 +11,11 @@ import {
queryAll, queryAll,
REGULATION_SUBJECT, REGULATION_SUBJECT,
REGULATION_LEVELS, REGULATION_LEVELS,
updateRevision, updateRegulationComment,
} from '@/services/internalRegulationService'; } from '@/services/internalRegulationService';
import { downloadCsv } from './csv'; import { downloadCsv } from './csv';
const INITIAL_FORM = { regulation_name: '', type: '新增', level: '1级', approve: [], departments: [], comment: '' }; const INITIAL_FORM = { regulation_name: '', level: '1级', approve: [], departments: [], comment: '' };
const PAGE_SIZE = 20; const PAGE_SIZE = 20;
function fieldOptions(options, field) { function fieldOptions(options, field) {
...@@ -28,6 +28,10 @@ function dateText(value) { ...@@ -28,6 +28,10 @@ function dateText(value) {
return Number.isNaN(date.getTime()) ? String(value).slice(0, 10) : date.toLocaleDateString('zh-CN'); return Number.isNaN(date.getTime()) ? String(value).slice(0, 10) : date.toLocaleDateString('zh-CN');
} }
function recordTitle(record) {
return record.regulation_name || record.title || '-';
}
export default function RegulationList({ options = {} }) { export default function RegulationList({ options = {} }) {
const [records, setRecords] = useState([]); const [records, setRecords] = useState([]);
const [searchText, setSearchText] = useState(''); const [searchText, setSearchText] = useState('');
...@@ -39,9 +43,10 @@ export default function RegulationList({ options = {} }) { ...@@ -39,9 +43,10 @@ export default function RegulationList({ options = {} }) {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [dialogOpen, setDialogOpen] = useState(false); const [dialogOpen, setDialogOpen] = useState(false);
const [editing, setEditing] = useState(null);
const [form, setForm] = useState(INITIAL_FORM); const [form, setForm] = useState(INITIAL_FORM);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [confirmState, setConfirmState] = useState({ open: false, type: '', record: null });
const [commentState, setCommentState] = useState({ open: false, record: null, comment: '' });
const [filters, setFilters] = useState({ effective_status: '', level: '', departments: '', approve: '' }); const [filters, setFilters] = useState({ effective_status: '', level: '', departments: '', approve: '' });
const refresh = async () => { const refresh = async () => {
...@@ -72,7 +77,7 @@ export default function RegulationList({ options = {} }) { ...@@ -72,7 +77,7 @@ export default function RegulationList({ options = {} }) {
useEffect(() => { refresh(); }, [page, pageSize, keyword, filters]); useEffect(() => { refresh(); }, [page, pageSize, keyword, filters]);
useEffect(() => { useEffect(() => {
const open = () => { setEditing(null); setForm(INITIAL_FORM); setDialogOpen(true); }; const open = () => { setForm(INITIAL_FORM); setDialogOpen(true); };
window.addEventListener('internal-regulation:create', open); window.addEventListener('internal-regulation:create', open);
return () => window.removeEventListener('internal-regulation:create', open); return () => window.removeEventListener('internal-regulation:create', open);
}, []); }, []);
...@@ -103,9 +108,36 @@ export default function RegulationList({ options = {} }) { ...@@ -103,9 +108,36 @@ export default function RegulationList({ options = {} }) {
if (!form.regulation_name.trim()) return; if (!form.regulation_name.trim()) return;
try { try {
setSaving(true); setSaving(true);
if (editing) await updateRevision(editing.name, form); await createRevision(form);
else await createRevision(form);
setDialogOpen(false); setDialogOpen(false);
toast.success('制度创建成功');
await refresh();
} catch (saveError) {
setError(saveError?.message || '保存失败,请检查字段后重试。');
} finally {
setSaving(false);
}
};
const requestRevision = (record, type) => {
setConfirmState({ open: true, type, record });
};
const confirmRevision = async () => {
const { type, record } = confirmState;
if (!record) return;
try {
setSaving(true);
await createRevision({
regulation_name: recordTitle(record),
type,
level: record.level,
departments: record.departments,
approve: record.approve,
origin_regulation: record._id,
});
setConfirmState({ open: false, type: '', record: null });
toast.success(type === '修订' ? '修订制度创建成功' : '废除制度创建成功');
await refresh(); await refresh();
} catch (saveError) { } catch (saveError) {
setError(saveError?.message || '保存失败,请检查字段后重试。'); setError(saveError?.message || '保存失败,请检查字段后重试。');
...@@ -114,6 +146,26 @@ export default function RegulationList({ options = {} }) { ...@@ -114,6 +146,26 @@ export default function RegulationList({ options = {} }) {
} }
}; };
const openComment = (record) => {
setCommentState({ open: true, record, comment: record.comment || '' });
};
const saveComment = async () => {
const { record } = commentState;
if (!record) return;
try {
setSaving(true);
await updateRegulationComment(record, commentState.comment);
setCommentState({ open: false, record: null, comment: '' });
toast.success('已成功添加备注');
await refresh();
} catch (saveError) {
setError(saveError?.message || '备注保存失败,请稍后重试。');
} finally {
setSaving(false);
}
};
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between"> <div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
...@@ -124,7 +176,7 @@ export default function RegulationList({ options = {} }) { ...@@ -124,7 +176,7 @@ export default function RegulationList({ options = {} }) {
</TextField> </TextField>
<Button isDisabled={loading} variant="secondary" onPress={submitSearch}>查询</Button> <Button isDisabled={loading} variant="secondary" onPress={submitSearch}>查询</Button>
</div> </div>
<div className="flex flex-wrap items-center gap-2"><Button variant="tertiary" onPress={exportList}><Download size={16} />批量下载</Button><Button variant="primary" onPress={() => { setEditing(null); setForm(INITIAL_FORM); setDialogOpen(true); }}><PlusIcon size={16} />创建修订计划</Button></div> <div className="flex flex-wrap items-center gap-2"><Button variant="tertiary" onPress={exportList}><Download size={16} />批量下载</Button><Button variant="primary" onPress={() => { setForm(INITIAL_FORM); setDialogOpen(true); }}><PlusIcon size={16} />创建修订计划</Button></div>
</div> </div>
<div className="flex flex-wrap items-end gap-3 rounded-xl border border-separator bg-surface-secondary/50 p-3"> <div className="flex flex-wrap items-end gap-3 rounded-xl border border-separator bg-surface-secondary/50 p-3">
<XinyuanAutocomplete className="w-44" label="时效性" options={[{ value: '', label: '全部' }, ...effectiveStatusOptions]} selectedKey={filters.effective_status} onSelectionChange={(value) => { setFilters((current) => ({ ...current, effective_status: String(value || '') })); setPage(0); }} /> <XinyuanAutocomplete className="w-44" label="时效性" options={[{ value: '', label: '全部' }, ...effectiveStatusOptions]} selectedKey={filters.effective_status} onSelectionChange={(value) => { setFilters((current) => ({ ...current, effective_status: String(value || '') })); setPage(0); }} />
...@@ -135,33 +187,44 @@ export default function RegulationList({ options = {} }) { ...@@ -135,33 +187,44 @@ export default function RegulationList({ options = {} }) {
{error ? <div className="rounded-xl border border-danger/30 bg-danger/5 p-3 text-sm text-danger">{error}</div> : null} {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> : ( {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"> <div className="overflow-x-auto rounded-2xl border border-separator">
<table className="w-full min-w-[980px] text-left text-sm"> <table className="w-full min-w-[1040px] 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> <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"> <tbody className="divide-y divide-separator">
{records.map((record) => <tr className="hover:bg-surface-secondary/50" key={record._id || record.name}> {records.map((record) => <tr className="hover:bg-surface-secondary/50" key={record._id || record.name}>
<td className="px-4 py-3"><button className="max-w-64 truncate font-medium text-accent hover:underline" onClick={() => history.push(`/internal_regulation/${record.name || record._id}`)}>{record.regulation_name || record.title || '-'}</button></td> <td className="px-4 py-3"><button className="max-w-64 truncate font-medium text-accent hover:underline" onClick={() => history.push(`/internal_regulation/${record.name || record._id}`)}>{recordTitle(record)}</button></td>
<td className="px-4 py-3">{record.revision_type ? <Chip color={record.revision_type === '修订' ? 'warning' : 'accent'} size="sm" variant="soft"><Chip.Label>待{record.revision_type}</Chip.Label></Chip> : <span className="text-muted">-</span>}</td> <td className="px-4 py-3">{record.revision_type ? <Chip color={record.revision_type === '修订' ? 'warning' : record.revision_type === '废止' ? 'default' : 'accent'} size="sm" variant="soft"><Chip.Label>待{record.revision_type}</Chip.Label></Chip> : <span className="text-muted">-</span>}</td>
<td className="px-4 py-3">{record.effective_status || '-'}</td><td className="px-4 py-3">{record.level || '-'}</td> <td className="px-4 py-3">{record.effective_status || '-'}</td><td className="px-4 py-3">{record.level || '-'}</td>
<td className="max-w-52 truncate px-4 py-3">{Array.isArray(record.departments) ? record.departments.join('、') : record.departments || '-'}</td> <td className="max-w-52 truncate px-4 py-3">{Array.isArray(record.departments) ? record.departments.join('、') : record.departments || '-'}</td>
<td className="max-w-52 truncate px-4 py-3">{Array.isArray(record.approve) ? record.approve.join('、') : record.approve || '-'}</td> <td className="max-w-52 truncate px-4 py-3">{Array.isArray(record.approve) ? record.approve.join('、') : record.approve || '-'}</td>
<td className="px-4 py-3">{dateText(record.public_date)}</td> <td className="px-4 py-3">{dateText(record.public_date)}</td>
<td className="px-4 py-3"><div className="flex items-center gap-1"><Button isIconOnly aria-label="查看详情" size="sm" variant="tertiary" onPress={() => history.push(`/internal_regulation/${record.name || record._id}`)}><Eye size={16} /></Button><Button isIconOnly aria-label="编辑修订计划" size="sm" variant="tertiary" onPress={() => { setEditing(record); setForm({ ...INITIAL_FORM, ...record }); setDialogOpen(true); }}><Edit size={16} /></Button></div></td> <td className="max-w-40 truncate px-4 py-3 text-muted" title={record.comment || ''}>{record.comment || '-'}</td>
<td className="px-4 py-3"><div className="flex items-center gap-1">
<Button isIconOnly aria-label="查看详情" size="sm" variant="tertiary" onPress={() => history.push(`/internal_regulation/${record.name || record._id}`)}><Eye size={16} /></Button>
<Button isIconOnly aria-label="修订制度" size="sm" variant="tertiary" onPress={() => requestRevision(record, '修订')}><Edit size={16} /></Button>
<Button isIconOnly aria-label="废止制度" size="sm" variant="tertiary" onPress={() => requestRevision(record, '废止')}><Close size={16} /></Button>
<Button isIconOnly aria-label="编辑备注" size="sm" variant="tertiary" onPress={() => openComment(record)}><FileText size={16} /></Button>
</div></td>
</tr>)} </tr>)}
</tbody> </tbody>
</table> </table>
</div> </div>
)} )}
<ListPagination isDisabled={loading} page={page} pageSize={pageSize} total={total} totalPages={totalPages} onPageChange={setPage} onPageSizeChange={changePageSize} /> <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>}> <XinyuanModal isOpen={dialogOpen} onOpenChange={setDialogOpen} size="2xl" title="创建修订计划" 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"> <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> <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>
<XinyuanAutocomplete isRequired 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={REGULATION_LEVELS.map((value) => ({ value, label: value }))} selectedKey={form.level} onSelectionChange={(value) => setForm((current) => ({ ...current, level: String(value) }))} />
<XinyuanAutocomplete label="审批主体" options={approveOptions} selectedKeys={new Set(form.approve || [])} selectionMode="multiple" onSelectionChange={(values) => setForm((current) => ({ ...current, approve: values === 'all' ? approveOptions.map((item) => item.value) : Array.from(values || []) }))} /> <XinyuanAutocomplete label="审批主体" options={approveOptions} selectedKeys={new Set(form.approve || [])} selectionMode="multiple" onSelectionChange={(values) => setForm((current) => ({ ...current, approve: values === 'all' ? approveOptions.map((item) => item.value) : Array.from(values || []) }))} />
<XinyuanAutocomplete label="制定部门" options={departmentOptions} selectedKeys={new Set(form.departments || [])} selectionMode="multiple" onSelectionChange={(values) => setForm((current) => ({ ...current, departments: values === 'all' ? departmentOptions.map((item) => item.value) : Array.from(values || []) }))} /> <XinyuanAutocomplete label="制定部门" options={departmentOptions} selectedKeys={new Set(form.departments || [])} selectionMode="multiple" onSelectionChange={(values) => setForm((current) => ({ ...current, departments: values === 'all' ? departmentOptions.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} placeholder="补充说明" variant="secondary" /></TextField> <TextField className="sm:col-span-2" fullWidth value={form.comment} onChange={(value) => setForm((current) => ({ ...current, comment: value }))}><Label>备注</Label><TextArea fullWidth rows={4} placeholder="补充说明" variant="secondary" /></TextField>
</div> </div>
</XinyuanModal> </XinyuanModal>
<XinyuanModal isOpen={confirmState.open} onOpenChange={(open) => setConfirmState((current) => ({ ...current, open }))} size="md" title={confirmState.type === '修订' ? '确定要修订这条制度吗?' : '确认要废止这条制度吗?'} description={confirmState.record ? `制度名称:${recordTitle(confirmState.record)}` : ''} footer={<div className="flex justify-end gap-2"><Button variant="tertiary" onPress={() => setConfirmState((current) => ({ ...current, open: false }))}>取消</Button><Button isDisabled={saving} variant="primary" onPress={confirmRevision}>{saving ? '创建中...' : '确定'}</Button></div>}>
{confirmState.type === '废止' ? <div className="text-sm text-danger">废止后该制度将在发文时标记为“已失效”,请确认已完成相关审批流程。</div> : null}
</XinyuanModal>
<XinyuanModal isOpen={commentState.open} onOpenChange={(open) => setCommentState((current) => ({ ...current, open }))} size="md" title="编辑备注" description={commentState.record ? `制度名称:${recordTitle(commentState.record)}` : ''} footer={<div className="flex justify-end gap-2"><Button variant="tertiary" onPress={() => setCommentState((current) => ({ ...current, open: false }))}>取消</Button><Button isDisabled={saving} variant="primary" onPress={saveComment}>{saving ? '保存中...' : '保存'}</Button></div>}>
<TextField fullWidth value={commentState.comment} onChange={(value) => setCommentState((current) => ({ ...current, comment: value }))}><Label>备注</Label><TextArea fullWidth rows={5} placeholder="补充说明" variant="secondary" /></TextField>
</XinyuanModal>
</div> </div>
); );
} }
...@@ -11,8 +11,8 @@ ...@@ -11,8 +11,8 @@
1. 页面读取 `internal_regulation` Subject 定义,并从字段 `settings.options` 生成审批主体、制定部门等选项。 1. 页面读取 `internal_regulation` Subject 定义,并从字段 `settings.options` 生成审批主体、制定部门等选项。
2. 列表按 `metadata.public_date` 倒序查询,支持制度名称搜索、时效性/位阶/部门/审批主体筛选与展示;制度名称输入后点击“查询”或按 Enter 才提交搜索,输入过程不实时请求;“批量下载”按当前已提交的搜索和筛选条件导出 CSV。 2. 列表按 `metadata.public_date` 倒序查询,支持制度名称搜索、时效性/位阶/部门/审批主体筛选与展示;制度名称输入后点击“查询”或按 Enter 才提交搜索,输入过程不实时请求;“批量下载”按当前已提交的搜索和筛选条件导出 CSV。
3. 创建修订计划时写入 `internal_regulation_revision`,业务字段全部放入 `metadata`,初始状态为“起草中”。 3. “创建修订计划”固定生成“新增”类型的 `internal_regulation_revision` 记录,业务字段全部放入 `metadata`,初始状态为“起草中”。
4. 制度名称进入详情路由;编辑仅更新制度允许修改的 `metadata` 字段,引用字段只保存 ID。 4. 制度目录行操作与旧版 streams-admin 保持一致:制度名称进入详情路由;行内“修订制度/废止制度”按钮弹出确认框,确认后以当前制度的 `regulation_name/level/departments/approve` 复制生成一条 `internal_regulation_revision` 记录,`type` 为“修订”或“废止”,`origin_regulation` 只保存现行制度 ID,初始状态为“起草中”,列表“修订状态”列刷新为“待修订/待废止”;行内“编辑备注”打开备注弹窗,仅更新现行制度 `metadata.comment`,不覆盖其余字段。
## 修订计划与状态 ## 修订计划与状态
......
...@@ -209,6 +209,18 @@ export async function updateRevision(name, values = {}) { ...@@ -209,6 +209,18 @@ export async function updateRevision(name, values = {}) {
}); });
} }
export async function updateRegulationComment(record, comment) {
const name = record?.name;
if (!name) throw new Error('制度记录缺少标识,无法保存备注。');
const payload = {
name,
title: record.title,
metadata: normalizeMetadata({ ...metadataOf(record), comment }),
};
if (record.content !== undefined) payload.content = record.content;
return updateRecord(name, payload);
}
export async function setRevisionStatus(name, status) { export async function setRevisionStatus(name, status) {
const current = await loadRecord(name); const current = await loadRecord(name);
if (!current) throw new Error('修订计划不存在。'); if (!current) throw new Error('修订计划不存在。');
......
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