From 27cdfc03c65cd4a5d88d6b682e4bce5ad4cf7711 Mon Sep 17 00:00:00 2001 From: a273825743 Date: Sun, 17 May 2026 01:04:44 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E7=AE=A1=E7=90=86=E9=A1=B5?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=BF=AB=E9=80=9F=E6=96=B0=E5=BB=BA=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/projects/ProjectsPage.tsx | 680 ++++++++++--------- 1 file changed, 361 insertions(+), 319 deletions(-) diff --git a/frontend/src/pages/projects/ProjectsPage.tsx b/frontend/src/pages/projects/ProjectsPage.tsx index dca7c0a..7961718 100644 --- a/frontend/src/pages/projects/ProjectsPage.tsx +++ b/frontend/src/pages/projects/ProjectsPage.tsx @@ -1,319 +1,361 @@ -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 { PlusOutlined, DeleteOutlined } from '@ant-design/icons'; -import apiClient from '../../utils/request'; -import { useAuthStore } from '../../store/authStore'; - -const { Title, Paragraph } = Typography; - -interface Project { - id: number - project_code: string - name: string - customer_id: number - customer_name?: string - status: string - budget: string - spent: string - start_date: string - end_date: string - description: string - manager_name?: string - progress?: number -} - -const ProjectsPage: React.FC = () => { - const navigate = useNavigate(); - const [isMobile, setIsMobile] = useState(false); - const [projects, setProjects] = useState([]); - const [loading, setLoading] = useState(true); - const [deleteModalVisible, setDeleteModalVisible] = useState(false); - const [deleteProjectId, setDeleteProjectId] = useState(null); - const [deletePassword, setDeletePassword] = useState(''); - const [deleteLoading, setDeleteLoading] = useState(false); - - const { user: currentUser } = useAuthStore(); - const isAdmin = currentUser?.role === 'admin' || false; - - useEffect(() => { - const checkMobile = () => { - setIsMobile(window.innerWidth <= 768); - }; - - checkMobile(); - window.addEventListener('resize', checkMobile); - return () => window.removeEventListener('resize', checkMobile); - }, []); - - useEffect(() => { - fetchProjects(); - }, []); - - const fetchProjects = async () => { - try { - const response = await apiClient.get('/projects'); - if (response.data.success) { - setProjects(response.data.data.map((p: Project) => ({ - ...p, - key: p.id.toString(), - 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 : '网络错误'}`); - } finally { - setLoading(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' - } - }); - if (response.data.success) { - message.success('项目删除成功'); - setDeleteModalVisible(false); - fetchProjects(); - } else { - message.error(response.data.message || '删除失败'); - } - } catch (error) { - message.error('删除项目失败'); - } finally { - setDeleteLoading(false); - } - }; - - // 桌面端表格列 - const desktopColumns = [ - { - title: '项目名称', - dataIndex: 'name', - key: 'name', - width: 250, - ellipsis: true, - render: (text: string, record: Project) => ( - navigate(`/projects/${record.id}`)} style={{ cursor: 'pointer' }}> - {text} - - ), - }, - { - title: '项目经理', - dataIndex: 'manager_name', - key: 'manager_name', - width: 100, - }, - { - 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, - render: (progress: number) => ( -
-
-
80 ? '#52c41a' : progress > 50 ? '#1890ff' : '#faad14', - borderRadius: 10, - height: 8, - width: `${progress}%`, - }} - /> -
- {progress}% -
- ), - }, - { - title: '状态', - dataIndex: 'status', - key: 'status', - width: 100, - render: (status: string) => { - const statusMap: Record = { - planning: { color: 'blue', text: '规划中' }, - in_progress: { color: 'processing', text: '进行中' }, - completed: { color: 'success', text: '已完成' }, - suspended: { color: 'warning', text: '已暂停' }, - }; - const config = statusMap[status] || { color: 'default', text: status }; - return {config.text}; - }, - }, - { - title: '操作', - key: 'action', - width: 140, - render: (_: unknown, record: Project) => ( - - - {isAdmin && ( - - )} - - ), - }, - ]; - - // 移动端简化表格列 - const mobileColumns = [ - { - title: '项目', - dataIndex: 'name', - key: 'name', - ellipsis: true, - render: (text: string, record: Project) => ( - navigate(`/projects/${record.id}`)} style={{ cursor: 'pointer' }}> - {text} - - ), - }, - { - title: '进度', - dataIndex: 'progress', - key: 'progress', - width: 80, - render: (progress: number) => ( -
-
-
80 ? '#52c41a' : progress > 50 ? '#1890ff' : '#faad14', - borderRadius: 4, - height: 6, - width: `${progress}%`, - }} - /> -
- {progress}% -
- ), - }, - { - title: '状态', - dataIndex: 'status', - key: 'status', - width: 70, - render: (status: string) => { - const statusMap: Record = { - planning: { color: 'blue', text: '规划' }, - in_progress: { color: 'processing', text: '进行中' }, - completed: { color: 'success', text: '完成' }, - suspended: { color: 'warning', text: '暂停' }, - }; - const config = statusMap[status] || { color: 'default', text: status }; - return {config.text}; - }, - }, - { - title: '操作', - key: 'action', - width: 60, - render: (_: unknown, record: Project) => ( - - ), - }, - ]; - - return ( -
-
- 项目管理 - - 项目由预算报价签约后自动创建,管理您的项目信息、进度和预算 - -
- - - {loading ? ( -
- -
- ) : ( - - )} - - - {/* 删除确认模态框 */} - setDeleteModalVisible(false)} - confirmLoading={deleteLoading} - okText="确认删除" - cancelText="取消" - > -
-

确定要删除这个项目吗?此操作不可恢复。

-

请输入管理员密码确认删除操作:

-
- setDeletePassword(e.target.value)} - size="large" - /> -
- - ); -}; - -export default ProjectsPage; +import React, { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +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; + +interface Project { + id: number + project_code: string + name: string + customer_id: number + customer_name?: string + status: string + budget: string + spent: string + start_date: string + end_date: string + description: string + manager_name?: string + progress?: number + contract_amount?: number + location?: string +} + +const ProjectsPage: React.FC = () => { + const navigate = useNavigate(); + const [isMobile, setIsMobile] = useState(false); + const [projects, setProjects] = useState([]); + const [loading, setLoading] = useState(true); + const [deleteModalVisible, setDeleteModalVisible] = useState(false); + const [deleteProjectId, setDeleteProjectId] = useState(null); + 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([]); + const [users, setUsers] = useState([]); + + const { user: currentUser } = useAuthStore(); + const isAdmin = currentUser?.role === 'admin' || false; + + useEffect(() => { + const checkMobile = () => setIsMobile(window.innerWidth <= 768); + checkMobile(); + window.addEventListener('resize', checkMobile); + return () => window.removeEventListener('resize', checkMobile); + }, []); + + useEffect(() => { fetchProjects(); }, []); + + const fetchProjects = async () => { + try { + const response = await apiClient.get('/projects'); + if (response.data.success) { + setProjects(response.data.data.map((p: Project) => ({ + ...p, + key: p.id.toString(), + progress: Math.floor(Math.random() * 100), + manager_name: p.manager_name || '未分配' + }))); + } else { + message.error(`获取项目列表失败: ${response.data.message}`); + } + } catch (error) { + 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' } + }); + if (response.data.success) { + message.success('项目删除成功'); + setDeleteModalVisible(false); + fetchProjects(); + } else { + message.error(response.data.message || '删除失败'); + } + } catch (error) { + message.error('删除项目失败'); + } finally { + setDeleteLoading(false); + } + }; + + const desktopColumns = [ + { + title: '项目名称', dataIndex: 'name', key: 'name', width: 250, ellipsis: true, + render: (text: string, record: Project) => ( + navigate(`/projects/${record.id}`)}>{text} + ), + }, + { title: '项目经理', dataIndex: 'manager_name', key: 'manager_name', width: 100 }, + { + 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, + render: (progress: number) => ( +
+
+
80 ? '#52c41a' : progress > 50 ? '#1890ff' : '#faad14', + borderRadius: 10, height: 8, width: `${progress}%`, + }} /> +
+ {progress}% +
+ ), + }, + { + title: '状态', dataIndex: 'status', key: 'status', width: 100, + render: (status: string) => { + const statusMap: Record = { + 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 {config.text}; + }, + }, + { + title: '操作', key: 'action', width: 140, + render: (_: unknown, record: Project) => ( + + + {isAdmin && } + + ), + }, + ]; + + const mobileColumns = [ + { + title: '项目', dataIndex: 'name', key: 'name', ellipsis: true, + render: (text: string, record: Project) => ( + navigate(`/projects/${record.id}`)}>{text} + ), + }, + { + title: '进度', dataIndex: 'progress', key: 'progress', width: 80, + render: (progress: number) => ( +
+
+
80 ? '#52c41a' : progress > 50 ? '#1890ff' : '#faad14', + borderRadius: 4, height: 6, width: `${progress}%`, + }} /> +
+ {progress}% +
+ ), + }, + { + title: '状态', dataIndex: 'status', key: 'status', width: 70, + render: (status: string) => { + const statusMap: Record = { + 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 {config.text}; + }, + }, + { + title: '操作', key: 'action', width: 60, + render: (_: unknown, record: Project) => ( + + ), + }, + ]; + + return ( +
+
+
+ 项目管理 + 管理项目信息、进度和预算 +
+ +
+ + + {loading ? ( +
+ ) : ( +
+ )} + + + setCreateModalVisible(false)} + confirmLoading={createLoading} + okText="创建" + cancelText="取消" + width={isMobile ? '95%' : 560} + style={{ top: isMobile ? 10 : 40 }} + destroyOnClose + > +
+ + + + + +
+ + ({ value: u.id, label: u.name || u.username }))} /> + + + + + + + + + + + + + + + + + + + + + + setDeleteModalVisible(false)} + confirmLoading={deleteLoading} + okText="确认删除" + cancelText="取消" + > +
+

确定要删除这个项目吗?此操作不可恢复。

+

请输入管理员密码确认删除操作:

+
+ setDeletePassword(e.target.value)} size="large" /> +
+ + ); +}; + +export default ProjectsPage;