项目管理页添加快速新建项目功能

This commit is contained in:
a273825743
2026-05-17 01:04:44 +08:00
parent 735d238acb
commit 27cdfc03c6
+361 -319
View File
@@ -1,319 +1,361 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { Card, Typography, Button, Space, Table, Tag, message, Spin, Modal, Input } from 'antd'; import { Card, Typography, Button, Space, Table, Tag, message, Spin, Modal, Input, Form, Select, InputNumber, DatePicker, Row, Col } from 'antd';
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons'; import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
import apiClient from '../../utils/request'; import apiClient from '../../utils/request';
import { useAuthStore } from '../../store/authStore'; import { useAuthStore } from '../../store/authStore';
import dayjs from 'dayjs';
const { Title, Paragraph } = Typography;
const { Title, Paragraph } = Typography;
interface Project {
id: number interface Project {
project_code: string id: number
name: string project_code: string
customer_id: number name: string
customer_name?: string customer_id: number
status: string customer_name?: string
budget: string status: string
spent: string budget: string
start_date: string spent: string
end_date: string start_date: string
description: string end_date: string
manager_name?: string description: string
progress?: number manager_name?: string
} progress?: number
contract_amount?: number
const ProjectsPage: React.FC = () => { location?: string
const navigate = useNavigate(); }
const [isMobile, setIsMobile] = useState(false);
const [projects, setProjects] = useState<Project[]>([]); const ProjectsPage: React.FC = () => {
const [loading, setLoading] = useState(true); const navigate = useNavigate();
const [deleteModalVisible, setDeleteModalVisible] = useState(false); const [isMobile, setIsMobile] = useState(false);
const [deleteProjectId, setDeleteProjectId] = useState<number | null>(null); const [projects, setProjects] = useState<Project[]>([]);
const [deletePassword, setDeletePassword] = useState(''); const [loading, setLoading] = useState(true);
const [deleteLoading, setDeleteLoading] = useState(false); const [deleteModalVisible, setDeleteModalVisible] = useState(false);
const [deleteProjectId, setDeleteProjectId] = useState<number | null>(null);
const { user: currentUser } = useAuthStore(); const [deletePassword, setDeletePassword] = useState('');
const isAdmin = currentUser?.role === 'admin' || false; const [deleteLoading, setDeleteLoading] = useState(false);
useEffect(() => { const [createModalVisible, setCreateModalVisible] = useState(false);
const checkMobile = () => { const [createLoading, setCreateLoading] = useState(false);
setIsMobile(window.innerWidth <= 768); const [form] = Form.useForm();
}; const [customers, setCustomers] = useState<any[]>([]);
const [users, setUsers] = useState<any[]>([]);
checkMobile();
window.addEventListener('resize', checkMobile); const { user: currentUser } = useAuthStore();
return () => window.removeEventListener('resize', checkMobile); const isAdmin = currentUser?.role === 'admin' || false;
}, []);
useEffect(() => {
useEffect(() => { const checkMobile = () => setIsMobile(window.innerWidth <= 768);
fetchProjects(); checkMobile();
}, []); window.addEventListener('resize', checkMobile);
return () => window.removeEventListener('resize', checkMobile);
const fetchProjects = async () => { }, []);
try {
const response = await apiClient.get('/projects'); useEffect(() => { fetchProjects(); }, []);
if (response.data.success) {
setProjects(response.data.data.map((p: Project) => ({ const fetchProjects = async () => {
...p, try {
key: p.id.toString(), const response = await apiClient.get('/projects');
progress: Math.floor(Math.random() * 100), // 临时模拟进度 if (response.data.success) {
manager_name: p.manager_name || '未分配' setProjects(response.data.data.map((p: Project) => ({
}))); ...p,
} else { key: p.id.toString(),
console.error('API返回失败:', response.data.message); progress: Math.floor(Math.random() * 100),
message.error(`获取项目列表失败: ${response.data.message}`); manager_name: p.manager_name || '未分配'
} })));
} catch (error) { } else {
console.error('获取项目列表错误:', error); message.error(`获取项目列表失败: ${response.data.message}`);
message.error(`获取项目列表失败: ${error instanceof Error ? error.message : '网络错误'}`); }
} finally { } catch (error) {
setLoading(false); message.error('获取项目列表失败');
} } finally {
}; setLoading(false);
}
// 处理删除项目 };
const handleDeleteProject = (projectId: number) => {
setDeleteProjectId(projectId); const fetchCreateOptions = async () => {
setDeletePassword(''); try {
setDeleteModalVisible(true); const [custRes, userRes] = await Promise.all([
}; apiClient.get('/customers'),
apiClient.get('/users'),
// 确认删除项目 ]);
const handleDeleteConfirm = async () => { if (custRes.data.success) setCustomers(custRes.data.data || []);
if (!deleteProjectId) return; if (userRes.data.success) setUsers(userRes.data.data || []);
} catch (e) { /* ignore */ }
// 验证密码 };
if (deletePassword !== 'X123c321@') {
message.error('密码错误'); const handleCreateClick = () => {
return; form.resetFields();
} form.setFieldsValue({ status: 'in_progress', start_date: dayjs() });
fetchCreateOptions();
setDeleteLoading(true); setCreateModalVisible(true);
try { };
const response = await apiClient.delete(`/projects/${deleteProjectId}`, {
headers: { const handleCreateSubmit = async () => {
'x-user-role': 'admin' try {
} const values = await form.validateFields();
}); setCreateLoading(true);
if (response.data.success) { const payload = {
message.success('项目删除成功'); ...values,
setDeleteModalVisible(false); manager_id: values.project_manager_id,
fetchProjects(); start_date: values.start_date?.format('YYYY-MM-DD'),
} else { end_date: values.end_date?.format('YYYY-MM-DD') || null,
message.error(response.data.message || '删除失败'); contract_amount: values.contract_amount || 0,
} };
} catch (error) { const res = await apiClient.post('/projects', payload);
message.error('删除项目失败'); if (res.data.success) {
} finally { message.success('项目创建成功');
setDeleteLoading(false); setCreateModalVisible(false);
} fetchProjects();
}; } else {
message.error(res.data.message || '创建失败');
// 桌面端表格列 }
const desktopColumns = [ } catch (e: any) {
{ if (e.response?.data?.message) message.error(e.response.data.message);
title: '项目名称', }
dataIndex: 'name', setCreateLoading(false);
key: 'name', };
width: 250,
ellipsis: true, const handleDeleteProject = (projectId: number) => {
render: (text: string, record: Project) => ( setDeleteProjectId(projectId);
<a onClick={() => navigate(`/projects/${record.id}`)} style={{ cursor: 'pointer' }}> setDeletePassword('');
{text} setDeleteModalVisible(true);
</a> };
),
}, const handleDeleteConfirm = async () => {
{ if (!deleteProjectId) return;
title: '项目经理', if (deletePassword !== 'X123c321@') {
dataIndex: 'manager_name', message.error('密码错误');
key: 'manager_name', return;
width: 100, }
}, setDeleteLoading(true);
{ try {
title: '预算', const response = await apiClient.delete(`/projects/${deleteProjectId}`, {
dataIndex: 'budget', headers: { 'x-user-role': 'admin' }
key: 'budget', });
width: 120, if (response.data.success) {
render: (amount: string) => { message.success('项目删除成功');
const val = parseFloat(amount || '0'); setDeleteModalVisible(false);
return val > 0 ? `¥${(val / 10000).toFixed(1)}` : '-'; fetchProjects();
}, } else {
}, message.error(response.data.message || '删除失败');
{ }
title: '进度', } catch (error) {
dataIndex: 'progress', message.error('删除项目失败');
key: 'progress', } finally {
width: 120, setDeleteLoading(false);
render: (progress: number) => ( }
<div style={{ width: 100 }}> };
<div style={{ background: '#f0f0f0', borderRadius: 10, height: 8 }}>
<div const desktopColumns = [
style={{ {
background: progress > 80 ? '#52c41a' : progress > 50 ? '#1890ff' : '#faad14', title: '项目名称', dataIndex: 'name', key: 'name', width: 250, ellipsis: true,
borderRadius: 10, render: (text: string, record: Project) => (
height: 8, <a onClick={() => navigate(`/projects/${record.id}`)}>{text}</a>
width: `${progress}%`, ),
}} },
/> { title: '项目经理', dataIndex: 'manager_name', key: 'manager_name', width: 100 },
</div> {
<span style={{ fontSize: 12, color: '#888' }}>{progress}%</span> title: '预算', dataIndex: 'budget', key: 'budget', width: 120,
</div> render: (amount: string) => {
), const val = parseFloat(amount || '0');
}, return val > 0 ? `¥${(val / 10000).toFixed(1)}` : '-';
{ },
title: '状态', },
dataIndex: 'status', {
key: 'status', title: '进度', dataIndex: 'progress', key: 'progress', width: 120,
width: 100, render: (progress: number) => (
render: (status: string) => { <div style={{ width: 100 }}>
const statusMap: Record<string, { color: string; text: string }> = { <div style={{ background: '#f0f0f0', borderRadius: 10, height: 8 }}>
planning: { color: 'blue', text: '规划中' }, <div style={{
in_progress: { color: 'processing', text: '进行中' }, background: progress > 80 ? '#52c41a' : progress > 50 ? '#1890ff' : '#faad14',
completed: { color: 'success', text: '已完成' }, borderRadius: 10, height: 8, width: `${progress}%`,
suspended: { color: 'warning', text: '已暂停' }, }} />
}; </div>
const config = statusMap[status] || { color: 'default', text: status }; <span style={{ fontSize: 12, color: '#888' }}>{progress}%</span>
return <Tag color={config.color}>{config.text}</Tag>; </div>
}, ),
}, },
{ {
title: '操作', title: '状态', dataIndex: 'status', key: 'status', width: 100,
key: 'action', render: (status: string) => {
width: 140, const statusMap: Record<string, { color: string; text: string }> = {
render: (_: unknown, record: Project) => ( planning: { color: 'blue', text: '规划中' }, in_progress: { color: 'processing', text: '进行中' },
<Space> completed: { color: 'success', text: '已完成' }, suspended: { color: 'warning', text: '已暂停' },
<Button size="small" onClick={() => navigate(`/projects/${record.id}`)}></Button> active: { color: 'processing', text: '进行中' },
{isAdmin && ( };
<Button const config = statusMap[status] || { color: 'default', text: status };
size="small" return <Tag color={config.color}>{config.text}</Tag>;
danger },
icon={<DeleteOutlined />} },
onClick={() => handleDeleteProject(record.id)} {
> title: '操作', key: 'action', width: 140,
render: (_: unknown, record: Project) => (
</Button> <Space>
)} <Button size="small" onClick={() => navigate(`/projects/${record.id}`)}></Button>
</Space> {isAdmin && <Button size="small" danger icon={<DeleteOutlined />} onClick={() => handleDeleteProject(record.id)}></Button>}
), </Space>
}, ),
]; },
];
// 移动端简化表格列
const mobileColumns = [ const mobileColumns = [
{ {
title: '项目', title: '项目', dataIndex: 'name', key: 'name', ellipsis: true,
dataIndex: 'name', render: (text: string, record: Project) => (
key: 'name', <a onClick={() => navigate(`/projects/${record.id}`)}>{text}</a>
ellipsis: true, ),
render: (text: string, record: Project) => ( },
<a onClick={() => navigate(`/projects/${record.id}`)} style={{ cursor: 'pointer' }}> {
{text} title: '进度', dataIndex: 'progress', key: 'progress', width: 80,
</a> render: (progress: number) => (
), <div style={{ width: 60 }}>
}, <div style={{ background: '#f0f0f0', borderRadius: 4, height: 6 }}>
{ <div style={{
title: '进度', background: progress > 80 ? '#52c41a' : progress > 50 ? '#1890ff' : '#faad14',
dataIndex: 'progress', borderRadius: 4, height: 6, width: `${progress}%`,
key: 'progress', }} />
width: 80, </div>
render: (progress: number) => ( <span style={{ fontSize: 10, color: '#888' }}>{progress}%</span>
<div style={{ width: 60 }}> </div>
<div style={{ background: '#f0f0f0', borderRadius: 4, height: 6 }}> ),
<div },
style={{ {
background: progress > 80 ? '#52c41a' : progress > 50 ? '#1890ff' : '#faad14', title: '状态', dataIndex: 'status', key: 'status', width: 70,
borderRadius: 4, render: (status: string) => {
height: 6, const statusMap: Record<string, { color: string; text: string }> = {
width: `${progress}%`, planning: { color: 'blue', text: '规划' }, in_progress: { color: 'processing', text: '进行中' },
}} completed: { color: 'success', text: '完成' }, suspended: { color: 'warning', text: '暂停' },
/> active: { color: 'processing', text: '进行中' },
</div> };
<span style={{ fontSize: 10, color: '#888' }}>{progress}%</span> const config = statusMap[status] || { color: 'default', text: status };
</div> return <Tag color={config.color} style={{ fontSize: 10 }}>{config.text}</Tag>;
), },
}, },
{ {
title: '状态', title: '操作', key: 'action', width: 60,
dataIndex: 'status', render: (_: unknown, record: Project) => (
key: 'status', <Button size="small" onClick={() => navigate(`/projects/${record.id}`)}></Button>
width: 70, ),
render: (status: string) => { },
const statusMap: Record<string, { color: string; text: string }> = { ];
planning: { color: 'blue', text: '规划' },
in_progress: { color: 'processing', text: '进行中' }, return (
completed: { color: 'success', text: '完成' }, <div style={{ padding: isMobile ? 8 : 24 }}>
suspended: { color: 'warning', text: '暂停' }, <div style={{ marginBottom: isMobile ? 12 : 24, display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 8 }}>
}; <div>
const config = statusMap[status] || { color: 'default', text: status }; <Title level={isMobile ? 3 : 2} style={{ marginBottom: 8 }}></Title>
return <Tag color={config.color} style={{ fontSize: 10 }}>{config.text}</Tag>; <Paragraph type="secondary" style={{ marginBottom: 0 }}></Paragraph>
}, </div>
}, <Button type="primary" icon={<PlusOutlined />} onClick={handleCreateClick}>
{ {isMobile ? '新建' : '新建项目'}
title: '操作', </Button>
key: 'action', </div>
width: 60,
render: (_: unknown, record: Project) => ( <Card title="项目列表" size="small" styles={{ body: { padding: isMobile ? 8 : 24 } }}>
<Button size="small" onClick={() => navigate(`/projects/${record.id}`)}></Button> {loading ? (
), <div style={{ textAlign: 'center', padding: 40 }}><Spin /></div>
}, ) : (
]; <Table
dataSource={projects}
return ( columns={isMobile ? mobileColumns : desktopColumns}
<div style={{ padding: isMobile ? 8 : 24 }}> pagination={{ pageSize: isMobile ? 5 : 10, size: isMobile ? 'small' : 'default' }}
<div style={{ marginBottom: isMobile ? 12 : 24 }}> scroll={isMobile ? { x: 350 } : undefined}
<Title level={isMobile ? 3 : 2} style={{ marginBottom: 8 }}></Title> size={isMobile ? 'small' : 'middle'}
<Paragraph type="secondary" style={{ marginBottom: 0 }}> />
)}
</Paragraph> </Card>
</div>
<Modal
<Card title="快速新建项目"
title="项目列表" open={createModalVisible}
size="small" onOk={handleCreateSubmit}
styles={{ body: { padding: isMobile ? 8 : 24 } }} onCancel={() => setCreateModalVisible(false)}
> confirmLoading={createLoading}
{loading ? ( okText="创建"
<div style={{ textAlign: 'center', padding: 40 }}> cancelText="取消"
<Spin /> width={isMobile ? '95%' : 560}
</div> style={{ top: isMobile ? 10 : 40 }}
) : ( destroyOnClose
<Table >
dataSource={projects} <Form form={form} layout="vertical" size="middle">
columns={isMobile ? mobileColumns : desktopColumns} <Form.Item name="name" label="项目名称" rules={[{ required: true, message: '请输入项目名称' }]}>
pagination={{ <Input placeholder="如:万象省赛塔尼县22kV线路工程" />
pageSize: isMobile ? 5 : 10, </Form.Item>
size: isMobile ? 'small' : 'default'
}} <Row gutter={8}>
scroll={isMobile ? { x: 350 } : undefined} <Col span={12}>
size={isMobile ? 'small' : 'middle'} <Form.Item name="customer_id" label="客户">
/> <Select showSearch optionFilterProp="label" allowClear placeholder="选择客户"
)} options={customers.map((c: any) => ({ value: c.id, label: c.name }))} />
</Card> </Form.Item>
</Col>
{/* 删除确认模态框 */} <Col span={12}>
<Modal <Form.Item name="project_manager_id" label="项目经理">
title="删除确认" <Select showSearch optionFilterProp="label" allowClear placeholder="选择项目经理"
open={deleteModalVisible} options={users.map((u: any) => ({ value: u.id, label: u.name || u.username }))} />
onOk={handleDeleteConfirm} </Form.Item>
onCancel={() => setDeleteModalVisible(false)} </Col>
confirmLoading={deleteLoading} </Row>
okText="确认删除"
cancelText="取消" <Row gutter={8}>
> <Col span={12}>
<div style={{ marginBottom: 16 }}> <Form.Item name="contract_amount" label="合同金额">
<p></p> <InputNumber style={{ width: '100%' }} min={0} placeholder="0" />
<p></p> </Form.Item>
</div> </Col>
<Input.Password <Col span={12}>
placeholder="请输入管理员密码" <Form.Item name="status" label="项目状态">
value={deletePassword} <Select options={[
onChange={(e) => setDeletePassword(e.target.value)} { value: 'planning', label: '规划中' },
size="large" { value: 'in_progress', label: '进行中' },
/> { value: 'completed', label: '已完成' },
</Modal> ]} />
</div> </Form.Item>
); </Col>
}; </Row>
export default ProjectsPage; <Row gutter={8}>
<Col span={12}>
<Form.Item name="start_date" label="开始日期">
<DatePicker style={{ width: '100%' }} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="end_date" label="结束日期">
<DatePicker style={{ width: '100%' }} />
</Form.Item>
</Col>
</Row>
<Form.Item name="location" label="项目地点">
<Input placeholder="如:老挝万象省" />
</Form.Item>
<Form.Item name="description" label="项目描述">
<Input.TextArea rows={2} placeholder="简要描述项目内容" />
</Form.Item>
</Form>
</Modal>
<Modal
title="删除确认"
open={deleteModalVisible}
onOk={handleDeleteConfirm}
onCancel={() => setDeleteModalVisible(false)}
confirmLoading={deleteLoading}
okText="确认删除"
cancelText="取消"
>
<div style={{ marginBottom: 16 }}>
<p></p>
<p></p>
</div>
<Input.Password placeholder="请输入管理员密码" value={deletePassword} onChange={(e) => setDeletePassword(e.target.value)} size="large" />
</Modal>
</div>
);
};
export default ProjectsPage;