308 lines
14 KiB
TypeScript
308 lines
14 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { Card, Button, Table, Space, Modal, Form, Input, Select, InputNumber, Tag, Steps, message, Popconfirm, Drawer, List, Checkbox, Radio, Empty, Spin, Typography, Row, Col } from 'antd';
|
|
import { PlusOutlined, CopyOutlined, EditOutlined, DeleteOutlined, EyeOutlined, ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons';
|
|
import apiClient from '../../utils/request';
|
|
import { useLanguageStore } from '../../store/languageStore';
|
|
|
|
const { Title, Text, Paragraph } = Typography;
|
|
const { Option } = Select;
|
|
const { TextArea } = Input;
|
|
|
|
interface PhaseItem {
|
|
order: number;
|
|
name: string;
|
|
key?: string;
|
|
type: 'serial' | 'parallel';
|
|
depends: number[];
|
|
sub_items: string[];
|
|
}
|
|
|
|
interface Template {
|
|
id: number;
|
|
name: string;
|
|
description: string;
|
|
phases: PhaseItem[];
|
|
is_system: boolean;
|
|
created_at: string;
|
|
}
|
|
|
|
const ProcessTemplates: React.FC = () => {
|
|
const { t, currentLanguage } = useLanguageStore();
|
|
const [templates, setTemplates] = useState<Template[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [createModalVisible, setCreateModalVisible] = useState(false);
|
|
const [viewDrawerVisible, setViewDrawerVisible] = useState(false);
|
|
const [currentTemplate, setCurrentTemplate] = useState<Template | null>(null);
|
|
const [currentStep, setCurrentStep] = useState(0);
|
|
const [form] = Form.useForm();
|
|
const [phases, setPhases] = useState<PhaseItem[]>([]);
|
|
const [phaseDrawerVisible, setPhaseDrawerVisible] = useState(false);
|
|
const [editingPhaseIndex, setEditingPhaseIndex] = useState<number>(-1);
|
|
const [phaseForm] = Form.useForm();
|
|
const [editingTemplateId, setEditingTemplateId] = useState<number | null>(null);
|
|
|
|
useEffect(() => { fetchTemplates(); }, []);
|
|
|
|
const fetchTemplates = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const res = await apiClient.get('/process-templates');
|
|
if (res.data.success) setTemplates(res.data.data);
|
|
} catch (e) { message.error(t('processTemplate.getListFailed')); }
|
|
finally { setLoading(false); }
|
|
};
|
|
|
|
const handleCreate = () => {
|
|
form.resetFields();
|
|
setPhases([]);
|
|
setCurrentStep(0);
|
|
setEditingTemplateId(null);
|
|
setCreateModalVisible(true);
|
|
};
|
|
|
|
const handleEdit = (template: Template) => {
|
|
form.setFieldsValue({ name: template.name, description: template.description });
|
|
setPhases(template.phases || []);
|
|
setCurrentStep(0);
|
|
setEditingTemplateId(template.id);
|
|
setCreateModalVisible(true);
|
|
};
|
|
|
|
const handleCopy = async (template: Template) => {
|
|
try {
|
|
await apiClient.post(`/process-templates/${template.id}/copy`, { name: `${template.name} (副本)` });
|
|
message.success(t('processTemplate.copySuccess'));
|
|
fetchTemplates();
|
|
} catch (e) { message.error(t('processTemplate.copyFailed')); }
|
|
};
|
|
|
|
const handleDelete = async (id: number) => {
|
|
try {
|
|
await apiClient.delete(`/process-templates/${id}`);
|
|
message.success(t('processTemplate.deleteSuccess'));
|
|
fetchTemplates();
|
|
} catch (e) { message.error(t('processTemplate.deleteFailed')); }
|
|
};
|
|
|
|
const handleView = (template: Template) => {
|
|
setCurrentTemplate(template);
|
|
setViewDrawerVisible(true);
|
|
};
|
|
|
|
const handleSaveTemplate = async () => {
|
|
try {
|
|
const name = form.getFieldValue('name');
|
|
const description = form.getFieldValue('description');
|
|
if (!name) { message.warning(t('processTemplate.nameRequired')); setCurrentStep(0); return; }
|
|
if (phases.length === 0) { message.warning(t('processTemplate.phaseRequired')); setCurrentStep(1); return; }
|
|
if (editingTemplateId) {
|
|
await apiClient.put(`/process-templates/${editingTemplateId}`, { name, description, phases });
|
|
message.success(t('processTemplate.updateSuccess'));
|
|
} else {
|
|
await apiClient.post('/process-templates', { name, description, phases });
|
|
message.success(t('processTemplate.createSuccess'));
|
|
}
|
|
setCreateModalVisible(false);
|
|
setEditingTemplateId(null);
|
|
fetchTemplates();
|
|
} catch (e) { message.error(t('common.saveFailed')); }
|
|
};
|
|
|
|
const addPhase = () => {
|
|
setEditingPhaseIndex(-1);
|
|
phaseForm.resetFields();
|
|
phaseForm.setFieldsValue({ name: '', type: 'serial', sub_items_text: '', depends: [] });
|
|
setPhaseDrawerVisible(true);
|
|
};
|
|
|
|
const editPhase = (index: number) => {
|
|
setEditingPhaseIndex(index);
|
|
const p = phases[index];
|
|
phaseForm.setFieldsValue({
|
|
name: p.name,
|
|
type: p.type,
|
|
sub_items_text: (p.sub_items || []).join('\n'),
|
|
depends: p.depends.map(d => d),
|
|
});
|
|
setPhaseDrawerVisible(true);
|
|
};
|
|
|
|
const savePhase = () => {
|
|
const values = phaseForm.getFieldsValue();
|
|
const subItems = (values.sub_items_text || '').split('\n').map((s: string) => s.trim()).filter(Boolean);
|
|
const phaseData: PhaseItem = {
|
|
order: 0,
|
|
name: values.name,
|
|
type: values.type || 'serial',
|
|
depends: values.depends || [],
|
|
sub_items: subItems,
|
|
};
|
|
if (!phaseData.name) { message.warning(t('processTemplate.phaseNameEmpty')); return; }
|
|
const newPhases = [...phases];
|
|
if (editingPhaseIndex >= 0) {
|
|
phaseData.order = newPhases[editingPhaseIndex].order;
|
|
newPhases[editingPhaseIndex] = phaseData;
|
|
} else {
|
|
phaseData.order = newPhases.length + 1;
|
|
newPhases.push(phaseData);
|
|
}
|
|
setPhases(newPhases);
|
|
setPhaseDrawerVisible(false);
|
|
};
|
|
|
|
const removePhase = (index: number) => {
|
|
const newPhases = phases.filter((_, i) => i !== index).map((p, i) => ({ ...p, order: i + 1 }));
|
|
setPhases(newPhases);
|
|
};
|
|
|
|
const movePhase = (index: number, direction: 'up' | 'down') => {
|
|
const newPhases = [...phases];
|
|
const targetIndex = direction === 'up' ? index - 1 : index + 1;
|
|
if (targetIndex < 0 || targetIndex >= newPhases.length) return;
|
|
[newPhases[index], newPhases[targetIndex]] = [newPhases[targetIndex], newPhases[index]];
|
|
newPhases.forEach((p, i) => p.order = i + 1);
|
|
setPhases(newPhases);
|
|
};
|
|
|
|
const getTypeTag = (type: string) => type === 'parallel' ? <Tag color="blue">{t('processTemplate.parallelLabel')}</Tag> : <Tag color="green">{t('processTemplate.serialLabel')}</Tag>;
|
|
|
|
const getDependsNames = (depends: number[]) => {
|
|
return depends.map(d => {
|
|
const p = phases.find(ph => ph.order === d);
|
|
return p ? p.name : `阶段${d}`;
|
|
}).join('、') || '无';
|
|
};
|
|
|
|
const columns = [
|
|
{ title: t('processTemplate.templateName'), dataIndex: 'name', key: 'name', render: (text: string, r: Template) => <Space>{text}{r.is_system && <Tag color="gold">{t('processTemplate.systemPreset')}</Tag>}</Space> },
|
|
{ title: t('processTemplate.phaseCount'), key: 'phases', render: (_: unknown, r: Template) => r.phases?.length || 0 },
|
|
{ title: t('processTemplate.desc'), dataIndex: 'description', key: 'description', ellipsis: true },
|
|
{ title: t('processTemplate.action'), key: 'action', render: (_: unknown, r: Template) => (
|
|
<Space>
|
|
<Button size="small" icon={<EyeOutlined />} onClick={() => handleView(r)}>{t('common.view')}</Button>
|
|
<Button size="small" type="primary" icon={<EditOutlined />} onClick={() => handleEdit(r)}>{t('common.edit')}</Button>
|
|
<Button size="small" icon={<CopyOutlined />} onClick={() => handleCopy(r)}>{t('processTemplate.copy')}</Button>
|
|
{!r.is_system && <Popconfirm title={t('processTemplate.confirmDelete')} onConfirm={() => handleDelete(r.id)}><Button size="small" danger icon={<DeleteOutlined />}>{t('processTemplate.delete')}</Button></Popconfirm>}
|
|
</Space>
|
|
)},
|
|
];
|
|
|
|
return (
|
|
<div style={{ padding: 24 }}>
|
|
<Card title={t('processTemplate.title')} extra={<Button type="primary" icon={<PlusOutlined />} onClick={handleCreate}>{t('processTemplate.newTemplate')}</Button>}>
|
|
<Table columns={columns} dataSource={templates} rowKey="id" loading={loading} pagination={false} />
|
|
</Card>
|
|
|
|
<Modal title={editingTemplateId ? t('processTemplate.editTemplate') : t('processTemplate.newTemplate')} open={createModalVisible} onCancel={() => setCreateModalVisible(false)} width={800} footer={[
|
|
<Button key="cancel" onClick={() => setCreateModalVisible(false)}>{t('processTemplate.cancel')}</Button>,
|
|
currentStep > 0 && <Button key="prev" onClick={() => setCurrentStep(currentStep - 1)}>{t('processTemplate.prev')}</Button>,
|
|
currentStep < 2 && <Button key="next" type="primary" onClick={() => {
|
|
if (currentStep === 0 && !form.getFieldValue('name')) { message.warning(t('processTemplate.nameRequired')); return; }
|
|
setCurrentStep(currentStep + 1);
|
|
}}>{t('processTemplate.next')}</Button>,
|
|
currentStep === 2 && <Button key="save" type="primary" onClick={handleSaveTemplate}>{editingTemplateId ? t('processTemplate.saveEdit') : t('processTemplate.confirmCreate')}</Button>,
|
|
]}>
|
|
<Steps current={currentStep} size="small" style={{ marginBottom: 24 }}>
|
|
<Steps.Step title={t('processTemplate.basicInfo')} />
|
|
<Steps.Step title={t('processTemplate.designPhase')} />
|
|
<Steps.Step title={t('processTemplate.preview')} />
|
|
</Steps>
|
|
|
|
{currentStep === 0 && (
|
|
<Form form={form} layout="vertical">
|
|
<Form.Item name="name" label={t('processTemplate.templateName')} rules={[{ required: true }]}>
|
|
<Input placeholder={t('processTemplate.namePlaceholder')} />
|
|
</Form.Item>
|
|
<Form.Item name="description" label={t('processTemplate.templateDescription')}>
|
|
<TextArea rows={3} placeholder={t('processTemplate.descPlaceholder')} />
|
|
</Form.Item>
|
|
</Form>
|
|
)}
|
|
|
|
{currentStep === 1 && (
|
|
<div>
|
|
{phases.length === 0 ? <Empty description={t('processTemplate.noPhase')} /> : (
|
|
<List bordered dataSource={phases} renderItem={(phase, index) => (
|
|
<List.Item actions={[
|
|
<Button size="small" icon={<EditOutlined />} onClick={() => editPhase(index)}>{t('common.edit')}</Button>,
|
|
<Button size="small" icon={<ArrowUpOutlined />} onClick={() => movePhase(index, 'up')} disabled={index === 0} />,
|
|
<Button size="small" icon={<ArrowDownOutlined />} onClick={() => movePhase(index, 'down')} disabled={index === phases.length - 1} />,
|
|
<Popconfirm title={t('processTemplate.confirmDelete')} onConfirm={() => removePhase(index)}><Button size="small" danger icon={<DeleteOutlined />} /></Popconfirm>,
|
|
]}>
|
|
<List.Item.Meta
|
|
title={<Space>{phase.order}. {phase.name} {getTypeTag(phase.type)} <Text type="secondary">{t('processTemplate.dependencyLabel')}{getDependsNames(phase.depends)}</Text></Space>}
|
|
description={phase.sub_items?.length > 0 ? `子项:${phase.sub_items.join('、')}` : t('processTemplate.emptySubItems')}
|
|
/>
|
|
</List.Item>
|
|
)} />
|
|
)}
|
|
<Button type="dashed" block icon={<PlusOutlined />} style={{ marginTop: 16 }} onClick={addPhase}>{t('processTemplate.addPhase')}</Button>
|
|
</div>
|
|
)}
|
|
|
|
{currentStep === 2 && (
|
|
<div>
|
|
<Title level={5}>{form.getFieldValue('name')}</Title>
|
|
<Paragraph type="secondary">{form.getFieldValue('description')}</Paragraph>
|
|
<List bordered dataSource={phases} renderItem={(phase) => (
|
|
<List.Item>
|
|
<List.Item.Meta
|
|
title={<Space>{phase.order}. {phase.name} {getTypeTag(phase.type)}</Space>}
|
|
description={<>
|
|
<Text type="secondary">{t('processTemplate.dependencyLabel')}{getDependsNames(phase.depends)}</Text><br />
|
|
{phase.sub_items?.length > 0 && <Text>子项:{phase.sub_items.join('、')}</Text>}
|
|
</>}
|
|
/>
|
|
</List.Item>
|
|
)} />
|
|
</div>
|
|
)}
|
|
</Modal>
|
|
|
|
<Drawer title={t('processTemplate.phaseEdit')} open={phaseDrawerVisible} onClose={() => setPhaseDrawerVisible(false)} width={400} extra={<Button type="primary" onClick={savePhase}>{t('processTemplate.save')}</Button>}>
|
|
<Form form={phaseForm} layout="vertical">
|
|
<Form.Item name="name" label={t('processTemplate.phaseName')} rules={[{ required: true }]}>
|
|
<Input placeholder={t('processTemplate.phaseNamePlaceholder')} />
|
|
</Form.Item>
|
|
<Form.Item name="type" label={t('processTemplate.phaseType')}>
|
|
<Radio.Group>
|
|
<Radio value="serial">{t('processTemplate.serial')}</Radio>
|
|
<Radio value="parallel">{t('processTemplate.parallel')}</Radio>
|
|
</Radio.Group>
|
|
</Form.Item>
|
|
<Form.Item name="depends" label={t('processTemplate.dependency')}>
|
|
<Checkbox.Group>
|
|
{phases.filter((_, i) => i !== editingPhaseIndex).map(p => (
|
|
<Checkbox key={p.order} value={p.order} style={{ display: 'block' }}>{p.order}. {p.name}</Checkbox>
|
|
))}
|
|
</Checkbox.Group>
|
|
</Form.Item>
|
|
<Form.Item name="sub_items_text" label={t('processTemplate.subItems')}>
|
|
<TextArea rows={6} placeholder={t('processTemplate.subItemsPlaceholder')} />
|
|
</Form.Item>
|
|
</Form>
|
|
</Drawer>
|
|
|
|
<Drawer title={currentTemplate?.name} open={viewDrawerVisible} onClose={() => setViewDrawerVisible(false)} width={500}>
|
|
{currentTemplate && (
|
|
<div>
|
|
<Paragraph type="secondary">{currentTemplate.description}</Paragraph>
|
|
<List bordered dataSource={currentTemplate.phases || []} renderItem={(phase: PhaseItem) => (
|
|
<List.Item>
|
|
<List.Item.Meta
|
|
title={<Space>{phase.order}. {phase.name} {phase.type === 'parallel' ? <Tag color="blue">{t('processTemplate.parallelLabel')}</Tag> : <Tag color="green">{t('processTemplate.serialLabel')}</Tag>}</Space>}
|
|
description={<>
|
|
<Text type="secondary">{t('processTemplate.dependencyLabelShort')}{'阶段'}{phase.depends?.join('、') || '无'}</Text><br />
|
|
{phase.sub_items?.length > 0 && <Text>子项:{phase.sub_items.join('、')}</Text>}
|
|
</>}
|
|
/>
|
|
</List.Item>
|
|
)} />
|
|
</div>
|
|
)}
|
|
</Drawer>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ProcessTemplates; |