save code

This commit is contained in:
xsl
2026-01-27 17:28:07 +08:00
parent 989a0227c7
commit 17c458035b
11 changed files with 676 additions and 85 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
> vite
VITE v5.4.21 ready in 131 ms
VITE v5.4.21 ready in 145 ms
➜ Local: http://localhost:8189/
➜ Network: use --host to expose
+45
View File
@@ -0,0 +1,45 @@
<!DOCTYPE html>
<html>
<head>
<title>API测试</title>
</head>
<body>
<h1>API测试页面</h1>
<button onclick="testStatistics()">测试统计API</button>
<div id="result"></div>
<script>
const API_BASE = '/api/v1';
async function testStatistics() {
const resultDiv = document.getElementById('result');
resultDiv.innerHTML = '加载中...';
try {
// 先登录
const loginResponse = await fetch(`${API_BASE}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: 'admin', password: 'admin123' })
});
const loginData = await loginResponse.json();
const token = loginData.data.token;
// 调用统计API
const statsResponse = await fetch(`${API_BASE}/projects/statistics/basic`, {
headers: { 'Authorization': `Bearer ${token}` }
});
const statsData = await statsResponse.json();
resultDiv.innerHTML = `
<h2>统计结果:</h2>
<pre>${JSON.stringify(statsData, null, 2)}</pre>
<h3>项目总数:${statsData.data.total_count}</h3>
`;
} catch (error) {
resultDiv.innerHTML = `<p style="color: red;">错误:${error.message}</p>`;
}
}
</script>
</body>
</html>
+9
View File
@@ -5,6 +5,7 @@ import Layout from './components/Layout/Layout';
import Login from './pages/Login/Login';
import Dashboard from './pages/Dashboard/Dashboard';
import ProjectList from './pages/Projects/ProjectList';
import ProjectDetail from './pages/Projects/ProjectDetail';
import UserList from './pages/Users/UserList';
import Statistics from './pages/Statistics/Statistics';
@@ -48,6 +49,14 @@ function App() {
</PrivateRoute>
}
/>
<Route
path="/projects/:id"
element={
<PrivateRoute>
<ProjectDetail />
</PrivateRoute>
}
/>
<Route
path="/projects/create"
element={
+19 -14
View File
@@ -1,7 +1,9 @@
import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { projectAPI } from '../../services/project';
const Dashboard = () => {
const navigate = useNavigate();
const [statistics, setStatistics] = useState(null);
const [recentProjects, setRecentProjects] = useState([]);
const [loading, setLoading] = useState(true);
@@ -14,13 +16,18 @@ const Dashboard = () => {
try {
setLoading(true);
console.log('开始获取仪表盘数据...');
const [statsResponse, projectsResponse] = await Promise.all([
authAPI.getStatistics(),
projectAPI.getStatistics(),
projectAPI.getList({ page: 1, page_size: 5 }),
]);
setStatistics(statsResponse);
setRecentProjects(projectsResponse.items || []);
console.log('统计API响应:', statsResponse);
console.log('项目列表API响应:', projectsResponse);
setStatistics(statsResponse.data || statsResponse);
setRecentProjects(projectsResponse.data?.items || projectsResponse.items || []);
} catch (error) {
console.error('Failed to fetch dashboard data:', error);
} finally {
@@ -44,17 +51,9 @@ const Dashboard = () => {
<div className="stat-value">{statistics?.total_count || 0}</div>
</div>
<div className="stat-card">
<div className="stat-label">进行中</div>
<div className="stat-label">平均进度%</div>
<div className="stat-value">
{(statistics?.total_count || 0) -
(statistics?.completed_count || 0) -
(statistics?.cancelled_count || 0) || 0}
</div>
</div>
<div className="stat-card">
<div className="stat-label">已完成</div>
<div className="stat-value">
{statistics?.completed_count || 0}
{statistics?.avg_cumulative_progress?.toFixed(1) || '0.0'}
</div>
</div>
<div className="stat-card">
@@ -66,6 +65,12 @@ const Dashboard = () => {
})}
</div>
</div>
<div className="stat-card">
<div className="stat-label">回款完成率%</div>
<div className="stat-value">
{statistics?.avg_receipt_completion_rate?.toFixed(1) || '0.0'}
</div>
</div>
</div>
<div className="card">
@@ -93,7 +98,7 @@ const Dashboard = () => {
onMouseLeave={(e) => {
e.currentTarget.style.background = 'transparent';
}}
onClick={() => window.location.href = `/projects/${project.id}`}
onClick={() => navigate(`/projects/${project.id}`)}
>
<div
style={{
@@ -0,0 +1,212 @@
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { projectAPI } from '../../services/project';
const ProjectDetail = () => {
const { id } = useParams();
const navigate = useNavigate();
const [project, setProject] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchProjectDetail();
}, [id]);
const fetchProjectDetail = async () => {
try {
setLoading(true);
const result = await projectAPI.getDetail(id);
setProject(result.data || result);
} catch (error) {
console.error('Failed to fetch project detail:', error);
alert('获取项目详情失败');
} finally {
setLoading(false);
}
};
if (loading) {
return <div>加载中...</div>;
}
if (!project) {
return <div>项目不存在</div>;
}
return (
<div>
<div style={{ marginBottom: '24px' }}>
<button
className="btn btn-default"
onClick={() => navigate('/projects')}
>
返回列表
</button>
</div>
<div className="card">
<div className="card-header">
项目详情
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '24px' }}>
<div>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 500 }}>项目编号</label>
<div style={{ padding: '8px 12px', background: '#f5f5f5', borderRadius: '4px' }}>
{project.project_no || '-'}
</div>
</div>
<div>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 500 }}>项目名称</label>
<div style={{ padding: '8px 12px', background: '#f5f5f5', borderRadius: '4px' }}>
{project.name || '-'}
</div>
</div>
<div>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 500 }}>工程类别</label>
<div style={{ padding: '8px 12px', background: '#f5f5f5', borderRadius: '4px' }}>
{project.engineering_type || '-'}
</div>
</div>
<div>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 500 }}>所属部门</label>
<div style={{ padding: '8px 12px', background: '#f5f5f5', borderRadius: '4px' }}>
{project.project_department || '-'}
</div>
</div>
<div>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 500 }}>合同金额万元</label>
<div style={{ padding: '8px 12px', background: '#f5f5f5', borderRadius: '4px' }}>
{project.contract_amount ? Number(project.contract_amount).toLocaleString('zh-CN', { minimumFractionDigits: 2 }) : '-'}
</div>
</div>
<div>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 500 }}>项目负责人</label>
<div style={{ padding: '8px 12px', background: '#f5f5f5', borderRadius: '4px' }}>
{project.project_leader || '-'}
</div>
</div>
<div>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 500 }}>建设单位</label>
<div style={{ padding: '8px 12px', background: '#f5f5f5', borderRadius: '4px' }}>
{project.owner_unit || '-'}
</div>
</div>
<div>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 500 }}>建设单位联系人</label>
<div style={{ padding: '8px 12px', background: '#f5f5f5', borderRadius: '4px' }}>
{project.owner_contact || '-'}
</div>
</div>
<div>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 500 }}>合同签订日期</label>
<div style={{ padding: '8px 12px', background: '#f5f5f5', borderRadius: '4px' }}>
{project.signing_date || '-'}
</div>
</div>
<div>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 500 }}>开始日期</label>
<div style={{ padding: '8px 12px', background: '#f5f5f5', borderRadius: '4px' }}>
{project.start_date || '-'}
</div>
</div>
<div>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 500 }}>计划结束日期</label>
<div style={{ padding: '8px 12px', background: '#f5f5f5', borderRadius: '4px' }}>
{project.planned_end_date || '-'}
</div>
</div>
<div>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 500 }}>实际结束日期</label>
<div style={{ padding: '8px 12px', background: '#f5f5f5', borderRadius: '4px' }}>
{project.actual_end_date || '-'}
</div>
</div>
<div>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 500 }}>累计进度%</label>
<div style={{ padding: '8px 12px', background: '#f5f5f5', borderRadius: '4px' }}>
{project.cumulative_progress ? Number(project.cumulative_progress).toFixed(2) : '-'}
</div>
</div>
<div>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 500 }}>实际收款金额万元</label>
<div style={{ padding: '8px 12px', background: '#f5f5f5', borderRadius: '4px' }}>
{project.actual_receipt_amount ? Number(project.actual_receipt_amount).toLocaleString('zh-CN', { minimumFractionDigits: 2 }) : '-'}
</div>
</div>
<div>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 500 }}>回款完成率%</label>
<div style={{ padding: '8px 12px', background: '#f5f5f5', borderRadius: '4px' }}>
{project.receipt_completion_rate ? Number(project.receipt_completion_rate).toFixed(2) : '-'}
</div>
</div>
<div>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 500 }}>状态</label>
<div style={{ padding: '8px 12px', background: '#f5f5f5', borderRadius: '4px' }}>
{project.status || '-'}
</div>
</div>
</div>
{project.payment_method && (
<div style={{ marginTop: '24px' }}>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 500 }}>付款方式</label>
<div style={{ padding: '12px', background: '#f5f5f5', borderRadius: '4px', whiteSpace: 'pre-wrap' }}>
{project.payment_method}
</div>
</div>
)}
{project.problems && (
<div style={{ marginTop: '24px' }}>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 500, color: '#ff4d4f' }}>存在问题</label>
<div style={{ padding: '12px', background: '#fff2f0', borderRadius: '4px', border: '1px solid #ffccc7', whiteSpace: 'pre-wrap' }}>
{project.problems}
</div>
</div>
)}
{project.suggestions && (
<div style={{ marginTop: '24px' }}>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 500, color: '#1890ff' }}>建议</label>
<div style={{ padding: '12px', background: '#e6f7ff', borderRadius: '4px', border: '1px solid #91d5ff', whiteSpace: 'pre-wrap' }}>
{project.suggestions}
</div>
</div>
)}
{project.remarks && (
<div style={{ marginTop: '24px' }}>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 500 }}>备注</label>
<div style={{ padding: '12px', background: '#f5f5f5', borderRadius: '4px', whiteSpace: 'pre-wrap' }}>
{project.remarks}
</div>
</div>
)}
<div style={{ marginTop: '24px', padding: '12px', background: '#fafafa', borderRadius: '4px' }}>
<div style={{ fontSize: '12px', color: '#999' }}>
创建时间{project.created_at} | 创建人{project.created_by_name || '-'}
</div>
</div>
</div>
</div>
);
};
export default ProjectDetail;
+94 -6
View File
@@ -41,8 +41,9 @@ const ProjectList = ({ mode = 'list' }) => {
...filters,
...params,
});
setList(result.items || []);
setTotal(result.total || 0);
const data = result.data || result;
setList(data.items || []);
setTotal(data.total || 0);
} catch (error) {
console.error('Failed to fetch projects:', error);
alert('获取项目列表失败');
@@ -72,7 +73,8 @@ const ProjectList = ({ mode = 'list' }) => {
const handleEdit = async (id) => {
try {
const project = await projectAPI.getDetail(id);
const result = await projectAPI.getDetail(id);
const project = result.data || result;
setForm(project);
setEditingId(id);
setModalVisible(true);
@@ -143,6 +145,23 @@ const ProjectList = ({ mode = 'list' }) => {
});
};
const handleChange = (e) => {
const { name, value } = e.target;
setForm({ ...form, [name]: value });
};
const handleFilterChange = (e) => {
const { name, value } = e.target;
const newFilters = { ...filters };
if (value) {
newFilters[name] = value;
} else {
delete newFilters[name];
}
setFilters(newFilters);
fetchProjects(newFilters);
};
const canEdit = (createdBy) => {
return user.role === 'admin' || (user.role === 'market' && createdBy === user?.id);
};
@@ -157,10 +176,13 @@ const ProjectList = ({ mode = 'list' }) => {
{mode === 'create' ? '新建项目' : '项目列表'}
</h1>
<div style={{ marginBottom: '16px', display: 'flex', gap: '12px' }}>
<div style={{ marginBottom: '16px', display: 'flex', gap: '12px', flexWrap: 'wrap', alignItems: 'center' }}>
<input
type="text"
name="keyword"
placeholder="搜索项目名称、合同编号..."
value={filters.keyword || ''}
onChange={handleFilterChange}
style={{
padding: '8px 12px',
border: '1px solid #d9d9d9',
@@ -170,6 +192,9 @@ const ProjectList = ({ mode = 'list' }) => {
}}
/>
<select
name="engineering_type"
value={filters.engineering_type || ''}
onChange={handleFilterChange}
style={{
padding: '8px 12px',
border: '1px solid #d9d9d9',
@@ -184,6 +209,69 @@ const ProjectList = ({ mode = 'list' }) => {
<option value="营销">营销</option>
<option value="检修">检修</option>
</select>
<input
type="date"
name="signing_date_start"
value={filters.signing_date_start || ''}
onChange={handleFilterChange}
style={{
padding: '8px 12px',
border: '1px solid #d9d9d9',
borderRadius: '4px',
fontSize: '14px',
}}
placeholder="签订日期起"
/>
<input
type="date"
name="signing_date_end"
value={filters.signing_date_end || ''}
onChange={handleFilterChange}
style={{
padding: '8px 12px',
border: '1px solid #d9d9d9',
borderRadius: '4px',
fontSize: '14px',
}}
placeholder="签订日期止"
/>
<input
type="number"
name="contract_amount_min"
value={filters.contract_amount_min || ''}
onChange={handleFilterChange}
style={{
padding: '8px 12px',
border: '1px solid #d9d9d9',
borderRadius: '4px',
fontSize: '14px',
width: '120px',
}}
placeholder="最小金额"
/>
<input
type="number"
name="contract_amount_max"
value={filters.contract_amount_max || ''}
onChange={handleFilterChange}
style={{
padding: '8px 12px',
border: '1px solid #d9d9d9',
borderRadius: '4px',
fontSize: '14px',
width: '120px',
}}
placeholder="最大金额"
/>
<button
className="btn btn-default"
onClick={() => {
setFilters({});
fetchProjects({});
}}
>
重置筛选
</button>
{user?.role === 'admin' && (
<button
className="btn btn-primary"
@@ -247,8 +335,8 @@ const ProjectList = ({ mode = 'list' }) => {
<td style={{ padding: '12px 16px', fontSize: '14px', color: '#666' }}>
{project.engineering_type}
</td>
<td style={{ padding: '12px 16px', fontSize: '14px', color: '#666', textAlign: 'right' }}>
{project.contract_amount?.toLocaleString('zh-CN')}
<td style={{ padding: '12px 16px', fontSize: '14px', color: '#666', textAlign: 'right' }}>
{project.contract_amount ? Number(project.contract_amount).toLocaleString('zh-CN', { minimumFractionDigits: 2 }) : '-'}
</td>
<td style={{ padding: '12px 16px', fontSize: '14px', textAlign: 'center' }}>
<button
+22 -28
View File
@@ -21,14 +21,14 @@ const Statistics = () => {
setLoading(true);
const [statsResponse, groupResponse, timelineResponse] = await Promise.all([
projectAPI.getStatistics(filters),
projectAPI.getGroupStatistics(filters),
projectAPI.getTimelineStatistics(filters),
projectAPI.getStatistics(),
projectAPI.getGroupStatistics({ group_by: filters.group_by }),
projectAPI.getTimelineStatistics({ time_field: filters.time_field, group_by: filters.group_by_time }),
]);
setStatistics(statsResponse);
setGroupStatistics(groupResponse || []);
setTimelineStatistics(timelineResponse || []);
setStatistics(statsResponse.data || statsResponse);
setGroupStatistics(groupResponse.data || groupResponse || []);
setTimelineStatistics(timelineResponse.data || timelineResponse || []);
} catch (error) {
console.error('Failed to fetch statistics data:', error);
alert('获取统计数据失败');
@@ -155,36 +155,30 @@ const Statistics = () => {
</div>
</div>
<div className="stat-grid">
<div className="stat-grid">
<div className="stat-card">
<div className="stat-label">项目总数</div>
<div className="stat-value">{statistics?.total_count || 0}</div>
</div>
<div className="stat-card">
<div className="stat-label">总投资金额万元</div>
<div className="stat-label">平均进度%</div>
<div className="stat-value">
{(statistics?.total_investment || 0).toLocaleString('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}
{Number(statistics?.avg_cumulative_progress || 0).toFixed(1)}
</div>
</div>
<div className="stat-card">
<div className="stat-label">总合同金额万元</div>
<div className="stat-value">
{(statistics?.total_contract_amount || 0).toLocaleString('zh-CN', {
{Number(statistics?.total_contract_amount || 0).toLocaleString('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}
</div>
</div>
<div className="stat-card">
<div className="stat-label">总收款金额万元</div>
<div className="stat-label">回款完成率%</div>
<div className="stat-value">
{(statistics?.total_receipt_amount || 0).toLocaleString('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}
{Number(statistics?.avg_receipt_completion_rate || 0).toFixed(1)}
</div>
</div>
</div>
@@ -336,16 +330,16 @@ const Statistics = () => {
borderBottom: '1px solid #f0f0f0',
}}
>
<td
style={{
padding: '12px 16px',
fontSize: '14px',
color: '#666',
textAlign: 'right',
}}
>
{item.month}
</td>
<td
style={{
padding: '12px 16px',
fontSize: '14px',
color: '#666',
textAlign: 'left',
}}
>
{item.month || item.day || item.year}
</td>
<td
style={{
padding: '12px 16px',
+1 -1
View File
@@ -11,7 +11,7 @@ export const projectAPI = {
delete: (id) => api.delete(`/projects/${id}`),
getStatistics: (params) => api.get('/projects/statistics', { params }),
getStatistics: (params) => api.get('/projects/statistics/basic', { params }),
getGroupStatistics: (params) => api.get('/projects/statistics/group', { params }),
+1 -1
View File
@@ -48,7 +48,7 @@ body {
background: #ffffff;
border-right: 1px solid #e8e8e8;
position: fixed;
left: 64px;
left: 0;
top: 64px;
bottom: 0;
overflow-y: auto;