save code
This commit is contained in:
@@ -0,0 +1,271 @@
|
||||
.project-list {
|
||||
padding: 20px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.project-list h2 {
|
||||
margin-bottom: 30px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 筛选表单样式 */
|
||||
.filter-form-container {
|
||||
background: #f9fafb;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 30px;
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.filter-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.filter-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.filter-group input {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-group input:focus {
|
||||
outline: none;
|
||||
border-color: #6366f1;
|
||||
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.filter-button, .reset-button {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.filter-button {
|
||||
background: #6366f1;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.filter-button:hover {
|
||||
background: #4f46e5;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.reset-button {
|
||||
background: #f3f4f6;
|
||||
color: #374151;
|
||||
border: 1px solid #d1d5db;
|
||||
}
|
||||
|
||||
.reset-button:hover {
|
||||
background: #e5e7eb;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.projects-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
background: #e5e7eb;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.project-card {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
transition: all 0.2s ease;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.project-card:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.project-card:hover {
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
.project-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.project-header-left {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.project-header h3 {
|
||||
margin: 0 0 5px 0;
|
||||
color: #333;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.project-code {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.project-description {
|
||||
margin: 10px 0;
|
||||
color: #666;
|
||||
line-height: 1.4;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.project-details {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 15px 20px;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.detail-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.detail-group-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
margin-bottom: 5px;
|
||||
padding-bottom: 5px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #6b7280;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.project-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.edit-button, .delete-button {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.edit-button {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.edit-button:hover {
|
||||
background: #2563eb;
|
||||
}
|
||||
|
||||
.delete-button {
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.delete-button:hover {
|
||||
background: #dc2626;
|
||||
}
|
||||
|
||||
.empty-message {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #666;
|
||||
font-size: 16px;
|
||||
background: #f9fafb;
|
||||
border-radius: 8px;
|
||||
border: 2px dashed #e5e7eb;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #666;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.error {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #dc2626;
|
||||
background: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.filter-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.projects-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,27 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { projectApi } from '../services/api';
|
||||
import './ProjectList.css';
|
||||
|
||||
function ProjectList() {
|
||||
const [projects, setProjects] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const [filters, setFilters] = useState({
|
||||
start_date: '',
|
||||
end_date: '',
|
||||
min_budget: '',
|
||||
max_budget: ''
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
fetchProjects();
|
||||
}, []);
|
||||
|
||||
const fetchProjects = async () => {
|
||||
const fetchProjects = async (filterParams = {}) => {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
const data = await projectApi.getProjects();
|
||||
const data = await projectApi.getProjects(filterParams);
|
||||
setProjects(data);
|
||||
} catch (err) {
|
||||
setError('获取项目列表失败');
|
||||
@@ -24,6 +31,34 @@ function ProjectList() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleFilterChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFilters(prev => ({
|
||||
...prev,
|
||||
[name]: value
|
||||
}));
|
||||
};
|
||||
|
||||
const handleFilterSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
const filterParams = {};
|
||||
if (filters.start_date) filterParams.start_date = filters.start_date;
|
||||
if (filters.end_date) filterParams.end_date = filters.end_date;
|
||||
if (filters.min_budget) filterParams.min_budget = parseInt(filters.min_budget);
|
||||
if (filters.max_budget) filterParams.max_budget = parseInt(filters.max_budget);
|
||||
fetchProjects(filterParams);
|
||||
};
|
||||
|
||||
const handleResetFilters = () => {
|
||||
setFilters({
|
||||
start_date: '',
|
||||
end_date: '',
|
||||
min_budget: '',
|
||||
max_budget: ''
|
||||
});
|
||||
fetchProjects();
|
||||
};
|
||||
|
||||
const handleDelete = async (id) => {
|
||||
if (window.confirm('确定要删除这个项目吗?')) {
|
||||
try {
|
||||
@@ -47,19 +82,168 @@ function ProjectList() {
|
||||
return (
|
||||
<div className="project-list">
|
||||
<h2>项目列表</h2>
|
||||
|
||||
{/* 筛选表单 */}
|
||||
<div className="filter-form-container">
|
||||
<form onSubmit={handleFilterSubmit} className="filter-form">
|
||||
<div className="filter-row">
|
||||
<div className="filter-group">
|
||||
<label htmlFor="start_date">开始日期</label>
|
||||
<input
|
||||
type="date"
|
||||
id="start_date"
|
||||
name="start_date"
|
||||
value={filters.start_date}
|
||||
onChange={handleFilterChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="filter-group">
|
||||
<label htmlFor="end_date">结束日期</label>
|
||||
<input
|
||||
type="date"
|
||||
id="end_date"
|
||||
name="end_date"
|
||||
value={filters.end_date}
|
||||
onChange={handleFilterChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="filter-group">
|
||||
<label htmlFor="min_budget">最小预算</label>
|
||||
<input
|
||||
type="number"
|
||||
id="min_budget"
|
||||
name="min_budget"
|
||||
value={filters.min_budget}
|
||||
onChange={handleFilterChange}
|
||||
placeholder="0"
|
||||
/>
|
||||
</div>
|
||||
<div className="filter-group">
|
||||
<label htmlFor="max_budget">最大预算</label>
|
||||
<input
|
||||
type="number"
|
||||
id="max_budget"
|
||||
name="max_budget"
|
||||
value={filters.max_budget}
|
||||
onChange={handleFilterChange}
|
||||
placeholder="999999"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="filter-actions">
|
||||
<button type="submit" className="filter-button">筛选</button>
|
||||
<button type="button" className="reset-button" onClick={handleResetFilters}>重置</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="projects-container">
|
||||
{projects.length === 0 ? (
|
||||
<div className="empty-message">暂无项目</div>
|
||||
) : (
|
||||
projects.map((project) => (
|
||||
<div key={project.id} className="project-card">
|
||||
<h3>{project.name}</h3>
|
||||
<p>{project.description}</p>
|
||||
<div className="project-details">
|
||||
<span>部门: {project.department}</span>
|
||||
<span>预算: {project.budget}</span>
|
||||
<span>状态: {project.status}</span>
|
||||
<div className="project-header">
|
||||
<div className="project-header-left">
|
||||
<h3>{project.name}</h3>
|
||||
<div className="project-code">项目编号: {project.project_code || '-'}</div>
|
||||
{project.description && (
|
||||
<div className="project-description">{project.description}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="project-details">
|
||||
{/* 基本信息 */}
|
||||
<div className="detail-group">
|
||||
<div className="detail-group-title">基本信息</div>
|
||||
<div className="detail-row">
|
||||
<div className="detail-label">工程类别</div>
|
||||
<div className="detail-value">{project.project_type || '-'}</div>
|
||||
</div>
|
||||
<div className="detail-row">
|
||||
<div className="detail-label">所属部门</div>
|
||||
<div className="detail-value">{project.department}</div>
|
||||
</div>
|
||||
<div className="detail-row">
|
||||
<div className="detail-label">状态</div>
|
||||
<div className="detail-value">{project.status || '-'}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 财务信息 */}
|
||||
<div className="detail-group">
|
||||
<div className="detail-group-title">财务信息</div>
|
||||
<div className="detail-row">
|
||||
<div className="detail-label">合同金额</div>
|
||||
<div className="detail-value">{project.contract_amount || 0} 万元</div>
|
||||
</div>
|
||||
<div className="detail-row">
|
||||
<div className="detail-label">实际收款</div>
|
||||
<div className="detail-value">{project.actual_received_amount || 0} 万元</div>
|
||||
</div>
|
||||
<div className="detail-row">
|
||||
<div className="detail-label">回款完成率</div>
|
||||
<div className="detail-value">{project.payment_completion_rate || 0}%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 人员信息 */}
|
||||
<div className="detail-group">
|
||||
<div className="detail-group-title">人员信息</div>
|
||||
<div className="detail-row">
|
||||
<div className="detail-label">项目负责人</div>
|
||||
<div className="detail-value">{project.project_manager || '-'}</div>
|
||||
</div>
|
||||
<div className="detail-row">
|
||||
<div className="detail-label">建设单位</div>
|
||||
<div className="detail-value">{project.client_company || '-'}</div>
|
||||
</div>
|
||||
<div className="detail-row">
|
||||
<div className="detail-label">联系人</div>
|
||||
<div className="detail-value">{project.client_contact || '-'}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 时间信息 */}
|
||||
<div className="detail-group">
|
||||
<div className="detail-group-title">时间信息</div>
|
||||
<div className="detail-row">
|
||||
<div className="detail-label">合同签订</div>
|
||||
<div className="detail-value">{project.contract_date ? new Date(project.contract_date).toLocaleDateString() : '-'}</div>
|
||||
</div>
|
||||
<div className="detail-row">
|
||||
<div className="detail-label">开始日期</div>
|
||||
<div className="detail-value">{project.start_date ? new Date(project.start_date).toLocaleDateString() : '-'}</div>
|
||||
</div>
|
||||
<div className="detail-row">
|
||||
<div className="detail-label">计划结束</div>
|
||||
<div className="detail-value">{project.planned_end_date ? new Date(project.planned_end_date).toLocaleDateString() : '-'}</div>
|
||||
</div>
|
||||
<div className="detail-row">
|
||||
<div className="detail-label">实际结束</div>
|
||||
<div className="detail-value">{project.actual_end_date ? new Date(project.actual_end_date).toLocaleDateString() : '-'}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 进度信息 */}
|
||||
<div className="detail-group">
|
||||
<div className="detail-group-title">进度信息</div>
|
||||
<div className="detail-row">
|
||||
<div className="detail-label">累计进度</div>
|
||||
<div className="detail-value">{project.progress || 0}%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 付款方式 */}
|
||||
<div className="detail-group">
|
||||
<div className="detail-group-title">付款方式</div>
|
||||
<div className="detail-row">
|
||||
<div className="detail-value">{project.payment_terms || '-'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="project-actions">
|
||||
<button className="edit-button">编辑</button>
|
||||
<button className="delete-button" onClick={() => handleDelete(project.id)}>
|
||||
|
||||
@@ -30,9 +30,12 @@ api.interceptors.response.use(
|
||||
},
|
||||
error => {
|
||||
if (error.response && error.response.status === 401) {
|
||||
// 清除token并跳转到登录页
|
||||
// 清除token
|
||||
localStorage.removeItem('token');
|
||||
window.location.href = '/login';
|
||||
// 如果不是在登录页,才跳转到登录页
|
||||
if (!window.location.pathname.includes('/login')) {
|
||||
window.location.href = '/login';
|
||||
}
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
@@ -41,10 +44,11 @@ api.interceptors.response.use(
|
||||
// 认证相关API
|
||||
export const authApi = {
|
||||
login: (username, password) => {
|
||||
const formData = new FormData();
|
||||
formData.append('username', username);
|
||||
formData.append('password', password);
|
||||
return api.post('/auth/login', formData, {
|
||||
// 创建URLSearchParams对象来发送表单数据
|
||||
const params = new URLSearchParams();
|
||||
params.append('username', username);
|
||||
params.append('password', password);
|
||||
return api.post('/auth/login', params, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
@@ -54,8 +58,13 @@ export const authApi = {
|
||||
|
||||
// 项目相关API
|
||||
export const projectApi = {
|
||||
getProjects: () => {
|
||||
return api.get('/projects');
|
||||
getProjects: (filters = {}) => {
|
||||
const params = new URLSearchParams();
|
||||
if (filters.start_date) params.append('start_date', filters.start_date);
|
||||
if (filters.end_date) params.append('end_date', filters.end_date);
|
||||
if (filters.min_budget) params.append('min_budget', filters.min_budget);
|
||||
if (filters.max_budget) params.append('max_budget', filters.max_budget);
|
||||
return api.get(`/projects?${params.toString()}`);
|
||||
},
|
||||
getProject: (id) => {
|
||||
return api.get(`/projects/${id}`);
|
||||
|
||||
Reference in New Issue
Block a user