save code
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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={
|
||||
|
||||
@@ -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;
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user