项目管理页添加快速新建项目功能
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
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 apiClient from '../../utils/request';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const { Title, Paragraph } = Typography;
|
||||
|
||||
@@ -21,6 +22,8 @@ interface Project {
|
||||
description: string
|
||||
manager_name?: string
|
||||
progress?: number
|
||||
contract_amount?: number
|
||||
location?: string
|
||||
}
|
||||
|
||||
const ProjectsPage: React.FC = () => {
|
||||
@@ -33,22 +36,23 @@ const ProjectsPage: React.FC = () => {
|
||||
const [deletePassword, setDeletePassword] = useState('');
|
||||
const [deleteLoading, setDeleteLoading] = useState(false);
|
||||
|
||||
const [createModalVisible, setCreateModalVisible] = useState(false);
|
||||
const [createLoading, setCreateLoading] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const [customers, setCustomers] = useState<any[]>([]);
|
||||
const [users, setUsers] = useState<any[]>([]);
|
||||
|
||||
const { user: currentUser } = useAuthStore();
|
||||
const isAdmin = currentUser?.role === 'admin' || false;
|
||||
|
||||
useEffect(() => {
|
||||
const checkMobile = () => {
|
||||
setIsMobile(window.innerWidth <= 768);
|
||||
};
|
||||
|
||||
const checkMobile = () => setIsMobile(window.innerWidth <= 768);
|
||||
checkMobile();
|
||||
window.addEventListener('resize', checkMobile);
|
||||
return () => window.removeEventListener('resize', checkMobile);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchProjects();
|
||||
}, []);
|
||||
useEffect(() => { fetchProjects(); }, []);
|
||||
|
||||
const fetchProjects = async () => {
|
||||
try {
|
||||
@@ -57,44 +61,78 @@ const ProjectsPage: React.FC = () => {
|
||||
setProjects(response.data.data.map((p: Project) => ({
|
||||
...p,
|
||||
key: p.id.toString(),
|
||||
progress: Math.floor(Math.random() * 100), // 临时模拟进度
|
||||
progress: Math.floor(Math.random() * 100),
|
||||
manager_name: p.manager_name || '未分配'
|
||||
})));
|
||||
} else {
|
||||
console.error('API返回失败:', response.data.message);
|
||||
message.error(`获取项目列表失败: ${response.data.message}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取项目列表错误:', error);
|
||||
message.error(`获取项目列表失败: ${error instanceof Error ? error.message : '网络错误'}`);
|
||||
message.error('获取项目列表失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理删除项目
|
||||
const fetchCreateOptions = async () => {
|
||||
try {
|
||||
const [custRes, userRes] = await Promise.all([
|
||||
apiClient.get('/customers'),
|
||||
apiClient.get('/users'),
|
||||
]);
|
||||
if (custRes.data.success) setCustomers(custRes.data.data || []);
|
||||
if (userRes.data.success) setUsers(userRes.data.data || []);
|
||||
} catch (e) { /* ignore */ }
|
||||
};
|
||||
|
||||
const handleCreateClick = () => {
|
||||
form.resetFields();
|
||||
form.setFieldsValue({ status: 'in_progress', start_date: dayjs() });
|
||||
fetchCreateOptions();
|
||||
setCreateModalVisible(true);
|
||||
};
|
||||
|
||||
const handleCreateSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
setCreateLoading(true);
|
||||
const payload = {
|
||||
...values,
|
||||
manager_id: values.project_manager_id,
|
||||
start_date: values.start_date?.format('YYYY-MM-DD'),
|
||||
end_date: values.end_date?.format('YYYY-MM-DD') || null,
|
||||
contract_amount: values.contract_amount || 0,
|
||||
};
|
||||
const res = await apiClient.post('/projects', payload);
|
||||
if (res.data.success) {
|
||||
message.success('项目创建成功');
|
||||
setCreateModalVisible(false);
|
||||
fetchProjects();
|
||||
} else {
|
||||
message.error(res.data.message || '创建失败');
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (e.response?.data?.message) message.error(e.response.data.message);
|
||||
}
|
||||
setCreateLoading(false);
|
||||
};
|
||||
|
||||
const handleDeleteProject = (projectId: number) => {
|
||||
setDeleteProjectId(projectId);
|
||||
setDeletePassword('');
|
||||
setDeleteModalVisible(true);
|
||||
};
|
||||
|
||||
// 确认删除项目
|
||||
const handleDeleteConfirm = async () => {
|
||||
if (!deleteProjectId) return;
|
||||
|
||||
// 验证密码
|
||||
if (deletePassword !== 'X123c321@') {
|
||||
message.error('密码错误');
|
||||
return;
|
||||
}
|
||||
|
||||
setDeleteLoading(true);
|
||||
try {
|
||||
const response = await apiClient.delete(`/projects/${deleteProjectId}`, {
|
||||
headers: {
|
||||
'x-user-role': 'admin'
|
||||
}
|
||||
headers: { 'x-user-role': 'admin' }
|
||||
});
|
||||
if (response.data.success) {
|
||||
message.success('项目删除成功');
|
||||
@@ -110,149 +148,93 @@ const ProjectsPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 桌面端表格列
|
||||
const desktopColumns = [
|
||||
{
|
||||
title: '项目名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 250,
|
||||
ellipsis: true,
|
||||
title: '项目名称', dataIndex: 'name', key: 'name', width: 250, ellipsis: true,
|
||||
render: (text: string, record: Project) => (
|
||||
<a onClick={() => navigate(`/projects/${record.id}`)} style={{ cursor: 'pointer' }}>
|
||||
{text}
|
||||
</a>
|
||||
<a onClick={() => navigate(`/projects/${record.id}`)}>{text}</a>
|
||||
),
|
||||
},
|
||||
{ title: '项目经理', dataIndex: 'manager_name', key: 'manager_name', width: 100 },
|
||||
{
|
||||
title: '项目经理',
|
||||
dataIndex: 'manager_name',
|
||||
key: 'manager_name',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '预算',
|
||||
dataIndex: 'budget',
|
||||
key: 'budget',
|
||||
width: 120,
|
||||
title: '预算', dataIndex: 'budget', key: 'budget', width: 120,
|
||||
render: (amount: string) => {
|
||||
const val = parseFloat(amount || '0');
|
||||
return val > 0 ? `¥${(val / 10000).toFixed(1)}万` : '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '进度',
|
||||
dataIndex: 'progress',
|
||||
key: 'progress',
|
||||
width: 120,
|
||||
title: '进度', dataIndex: 'progress', key: 'progress', width: 120,
|
||||
render: (progress: number) => (
|
||||
<div style={{ width: 100 }}>
|
||||
<div style={{ background: '#f0f0f0', borderRadius: 10, height: 8 }}>
|
||||
<div
|
||||
style={{
|
||||
<div style={{
|
||||
background: progress > 80 ? '#52c41a' : progress > 50 ? '#1890ff' : '#faad14',
|
||||
borderRadius: 10,
|
||||
height: 8,
|
||||
width: `${progress}%`,
|
||||
}}
|
||||
/>
|
||||
borderRadius: 10, height: 8, width: `${progress}%`,
|
||||
}} />
|
||||
</div>
|
||||
<span style={{ fontSize: 12, color: '#888' }}>{progress}%</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
title: '状态', dataIndex: 'status', key: 'status', width: 100,
|
||||
render: (status: string) => {
|
||||
const statusMap: Record<string, { color: string; text: string }> = {
|
||||
planning: { color: 'blue', text: '规划中' },
|
||||
in_progress: { color: 'processing', text: '进行中' },
|
||||
completed: { color: 'success', text: '已完成' },
|
||||
suspended: { color: 'warning', text: '已暂停' },
|
||||
planning: { color: 'blue', text: '规划中' }, in_progress: { color: 'processing', text: '进行中' },
|
||||
completed: { color: 'success', text: '已完成' }, suspended: { color: 'warning', text: '已暂停' },
|
||||
active: { color: 'processing', text: '进行中' },
|
||||
};
|
||||
const config = statusMap[status] || { color: 'default', text: status };
|
||||
return <Tag color={config.color}>{config.text}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 140,
|
||||
title: '操作', key: 'action', width: 140,
|
||||
render: (_: unknown, record: Project) => (
|
||||
<Space>
|
||||
<Button size="small" onClick={() => navigate(`/projects/${record.id}`)}>查看</Button>
|
||||
{isAdmin && (
|
||||
<Button
|
||||
size="small"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => handleDeleteProject(record.id)}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
)}
|
||||
{isAdmin && <Button size="small" danger icon={<DeleteOutlined />} onClick={() => handleDeleteProject(record.id)}>删除</Button>}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
// 移动端简化表格列
|
||||
const mobileColumns = [
|
||||
{
|
||||
title: '项目',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
ellipsis: true,
|
||||
title: '项目', dataIndex: 'name', key: 'name', ellipsis: true,
|
||||
render: (text: string, record: Project) => (
|
||||
<a onClick={() => navigate(`/projects/${record.id}`)} style={{ cursor: 'pointer' }}>
|
||||
{text}
|
||||
</a>
|
||||
<a onClick={() => navigate(`/projects/${record.id}`)}>{text}</a>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '进度',
|
||||
dataIndex: 'progress',
|
||||
key: 'progress',
|
||||
width: 80,
|
||||
title: '进度', dataIndex: 'progress', key: 'progress', width: 80,
|
||||
render: (progress: number) => (
|
||||
<div style={{ width: 60 }}>
|
||||
<div style={{ background: '#f0f0f0', borderRadius: 4, height: 6 }}>
|
||||
<div
|
||||
style={{
|
||||
<div style={{
|
||||
background: progress > 80 ? '#52c41a' : progress > 50 ? '#1890ff' : '#faad14',
|
||||
borderRadius: 4,
|
||||
height: 6,
|
||||
width: `${progress}%`,
|
||||
}}
|
||||
/>
|
||||
borderRadius: 4, height: 6, width: `${progress}%`,
|
||||
}} />
|
||||
</div>
|
||||
<span style={{ fontSize: 10, color: '#888' }}>{progress}%</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 70,
|
||||
title: '状态', dataIndex: 'status', key: 'status', width: 70,
|
||||
render: (status: string) => {
|
||||
const statusMap: Record<string, { color: string; text: string }> = {
|
||||
planning: { color: 'blue', text: '规划' },
|
||||
in_progress: { color: 'processing', text: '进行中' },
|
||||
completed: { color: 'success', text: '完成' },
|
||||
suspended: { color: 'warning', text: '暂停' },
|
||||
planning: { color: 'blue', text: '规划' }, in_progress: { color: 'processing', text: '进行中' },
|
||||
completed: { color: 'success', text: '完成' }, suspended: { color: 'warning', text: '暂停' },
|
||||
active: { color: 'processing', text: '进行中' },
|
||||
};
|
||||
const config = statusMap[status] || { color: 'default', text: status };
|
||||
return <Tag color={config.color} style={{ fontSize: 10 }}>{config.text}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 60,
|
||||
title: '操作', key: 'action', width: 60,
|
||||
render: (_: unknown, record: Project) => (
|
||||
<Button size="small" onClick={() => navigate(`/projects/${record.id}`)}>查看</Button>
|
||||
),
|
||||
@@ -261,37 +243,102 @@ const ProjectsPage: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div style={{ padding: isMobile ? 8 : 24 }}>
|
||||
<div style={{ marginBottom: isMobile ? 12 : 24 }}>
|
||||
<div style={{ marginBottom: isMobile ? 12 : 24, display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 8 }}>
|
||||
<div>
|
||||
<Title level={isMobile ? 3 : 2} style={{ marginBottom: 8 }}>项目管理</Title>
|
||||
<Paragraph type="secondary" style={{ marginBottom: 0 }}>
|
||||
项目由预算报价签约后自动创建,管理您的项目信息、进度和预算
|
||||
</Paragraph>
|
||||
<Paragraph type="secondary" style={{ marginBottom: 0 }}>管理项目信息、进度和预算</Paragraph>
|
||||
</div>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={handleCreateClick}>
|
||||
{isMobile ? '新建' : '新建项目'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Card
|
||||
title="项目列表"
|
||||
size="small"
|
||||
styles={{ body: { padding: isMobile ? 8 : 24 } }}
|
||||
>
|
||||
<Card title="项目列表" size="small" styles={{ body: { padding: isMobile ? 8 : 24 } }}>
|
||||
{loading ? (
|
||||
<div style={{ textAlign: 'center', padding: 40 }}>
|
||||
<Spin />
|
||||
</div>
|
||||
<div style={{ textAlign: 'center', padding: 40 }}><Spin /></div>
|
||||
) : (
|
||||
<Table
|
||||
dataSource={projects}
|
||||
columns={isMobile ? mobileColumns : desktopColumns}
|
||||
pagination={{
|
||||
pageSize: isMobile ? 5 : 10,
|
||||
size: isMobile ? 'small' : 'default'
|
||||
}}
|
||||
pagination={{ pageSize: isMobile ? 5 : 10, size: isMobile ? 'small' : 'default' }}
|
||||
scroll={isMobile ? { x: 350 } : undefined}
|
||||
size={isMobile ? 'small' : 'middle'}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* 删除确认模态框 */}
|
||||
<Modal
|
||||
title="快速新建项目"
|
||||
open={createModalVisible}
|
||||
onOk={handleCreateSubmit}
|
||||
onCancel={() => setCreateModalVisible(false)}
|
||||
confirmLoading={createLoading}
|
||||
okText="创建"
|
||||
cancelText="取消"
|
||||
width={isMobile ? '95%' : 560}
|
||||
style={{ top: isMobile ? 10 : 40 }}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form form={form} layout="vertical" size="middle">
|
||||
<Form.Item name="name" label="项目名称" rules={[{ required: true, message: '请输入项目名称' }]}>
|
||||
<Input placeholder="如:万象省赛塔尼县22kV线路工程" />
|
||||
</Form.Item>
|
||||
|
||||
<Row gutter={8}>
|
||||
<Col span={12}>
|
||||
<Form.Item name="customer_id" label="客户">
|
||||
<Select showSearch optionFilterProp="label" allowClear placeholder="选择客户"
|
||||
options={customers.map((c: any) => ({ value: c.id, label: c.name }))} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="project_manager_id" label="项目经理">
|
||||
<Select showSearch optionFilterProp="label" allowClear placeholder="选择项目经理"
|
||||
options={users.map((u: any) => ({ value: u.id, label: u.name || u.username }))} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={8}>
|
||||
<Col span={12}>
|
||||
<Form.Item name="contract_amount" label="合同金额">
|
||||
<InputNumber style={{ width: '100%' }} min={0} placeholder="0" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="status" label="项目状态">
|
||||
<Select options={[
|
||||
{ value: 'planning', label: '规划中' },
|
||||
{ value: 'in_progress', label: '进行中' },
|
||||
{ value: 'completed', label: '已完成' },
|
||||
]} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<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}
|
||||
@@ -305,12 +352,7 @@ const ProjectsPage: React.FC = () => {
|
||||
<p>确定要删除这个项目吗?此操作不可恢复。</p>
|
||||
<p>请输入管理员密码确认删除操作:</p>
|
||||
</div>
|
||||
<Input.Password
|
||||
placeholder="请输入管理员密码"
|
||||
value={deletePassword}
|
||||
onChange={(e) => setDeletePassword(e.target.value)}
|
||||
size="large"
|
||||
/>
|
||||
<Input.Password placeholder="请输入管理员密码" value={deletePassword} onChange={(e) => setDeletePassword(e.target.value)} size="large" />
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user