save code

This commit is contained in:
xsl
2026-01-26 08:04:53 +08:00
parent 0895398138
commit e4b59c5ee4
1238 changed files with 95107 additions and 1068 deletions
+127
View File
@@ -0,0 +1,127 @@
import React, { useEffect, useState } from 'react';
import { authAPI } from '../../services/auth';
import { projectAPI } from '../../services/project';
const Dashboard = () => {
const [statistics, setStatistics] = useState(null);
const [recentProjects, setRecentProjects] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchData();
}, []);
const fetchData = async () => {
try {
setLoading(true);
const [statsResponse, projectsResponse] = await Promise.all([
authAPI.getStatistics(),
projectAPI.getList({ page: 1, page_size: 5 }),
]);
setStatistics(statsResponse);
setRecentProjects(projectsResponse.items || []);
} catch (error) {
console.error('Failed to fetch dashboard data:', error);
} finally {
setLoading(false);
}
};
if (loading) {
return <div>加载中...</div>;
}
return (
<div>
<h1 style={{ fontSize: '24px', marginBottom: '24px' }}>
仪表盘
</h1>
<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-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}
</div>
</div>
<div className="stat-card">
<div className="stat-label">总合同金额万元</div>
<div className="stat-value">
{(statistics?.total_contract_amount || 0).toLocaleString('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}
</div>
</div>
</div>
<div className="card">
<div className="card-header">
最近项目
</div>
{recentProjects.length === 0 ? (
<div style={{ textAlign: 'center', padding: '40px', color: '#999' }}>
暂无项目
</div>
) : (
<div>
{recentProjects.map((project) => (
<div
key={project.id}
style={{
padding: '16px 0',
borderBottom: '1px solid #f0f0f0',
cursor: 'pointer',
transition: 'all 0.3s',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#fafafa';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'transparent';
}}
onClick={() => window.location.href = `/projects/${project.id}`}
>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
marginBottom: '8px',
}}
>
<div style={{ fontSize: '14px', fontWeight: 500 }}>
{project.name}
</div>
<div
style={{ fontSize: '12px', color: '#999' }}
>
{project.created_at?.split('T')[0] || ''}
</div>
</div>
<div style={{ fontSize: '14px', color: '#666' }}>
合同金额{project.contract_amount?.toLocaleString('zh-CN')} 万元
</div>
</div>
))}
</div>
)}
</div>
</div>
);
};
export default Dashboard;
+176
View File
@@ -0,0 +1,176 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../../contexts/AuthContext';
const Login = () => {
const navigate = useNavigate();
const { login, loading } = useAuth();
const [form, setForm] = useState({
username: '',
password: '',
});
const [error, setError] = useState('');
const handleChange = (e) => {
setForm({ ...form, [e.target.name]: e.target.value });
if (error) setError('');
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!form.username) {
setError('请输入用户名');
return;
}
if (form.username.length < 3) {
setError('用户名至少3个字符');
return;
}
if (!form.password) {
setError('请输入密码');
return;
}
if (form.password.length < 6) {
setError('密码至少6个字符');
return;
}
try {
await login(form.username, form.password);
navigate('/dashboard');
} catch (err) {
setError(err.message || '登录失败,请重试');
}
};
return (
<div
style={{
minHeight: '100vh',
background: 'linear-gradient(135deg, #001529 0%, #1890ff 100%)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
padding: '20px',
}}
>
<div
style={{
background: '#ffffff',
padding: '40px',
borderRadius: '8px',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
width: '400px',
}}
>
<h1
style={{
textAlign: 'center',
color: '#1890ff',
fontSize: '24px',
fontWeight: 600,
marginBottom: '32px',
marginTop: 0,
}}
>
海洋项目管理系统
</h1>
<form onSubmit={handleSubmit}>
<div style={{ marginBottom: '20px' }}>
<label
style={{
display: 'block',
marginBottom: '8px',
color: '#333',
fontSize: '14px',
fontWeight: 500,
}}
>
用户名
</label>
<input
type="text"
name="username"
value={form.username}
onChange={handleChange}
placeholder="请输入用户名"
style={{
width: '100%',
height: '40px',
padding: '8px 12px',
border: '1px solid #d9d9d9',
borderRadius: '4px',
fontSize: '14px',
boxSizing: 'border-box',
}}
/>
</div>
<div style={{ marginBottom: '20px' }}>
<label
style={{
display: 'block',
marginBottom: '8px',
color: '#333',
fontSize: '14px',
fontWeight: 500',
}}
>
密码
</label>
<input
type="password"
name="password"
value={form.password}
onChange={handleChange}
placeholder="请输入密码"
style={{
width: '100%',
height: '40px',
padding: '8px 12px',
border: '1px solid #d9d9d9',
borderRadius: '4px',
fontSize: '14px',
boxSizing: 'border-box',
}}
/>
</div>
{error && (
<div
style={{
color: '#ff4d4f',
fontSize: '14px',
marginBottom: '20px',
}}
>
{error}
</div>
)}
<button
type="submit"
disabled={loading}
style={{
width: '100%',
height: '40px',
background: '#1890ff',
color: '#ffffff',
border: 'none',
borderRadius: '4px',
fontSize: '14px',
fontWeight: 500',
cursor: loading ? 'not-allowed' : 'pointer',
transition: 'all 0.3s',
}}
>
{loading ? '登录中...' : '登录'}
</button>
</form>
</div>
</div>
);
};
export default Login;
+611
View File
@@ -0,0 +1,611 @@
import React, { useState, useEffect } from 'react';
import { useAuth } from '../../contexts/AuthContext';
import { projectAPI } from '../../services/project';
const ProjectList = ({ mode = 'list' }) => {
const { user } = useAuth();
const navigate = useNavigate();
const [list, setList] = useState([]);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(false);
const [pagination, setPagination] = useState({
current: 1,
pageSize: 10,
});
const [filters, setFilters] = useState({});
const [modalVisible, setModalVisible] = useState(false);
const [editingId, setEditingId] = useState(null);
const [form, setForm] = useState({
project_no: '',
name: '',
engineering_type: '',
contract_amount: '',
signing_date: '',
start_date: '',
planned_end_date: '',
});
useEffect(() => {
if (mode === 'list') {
fetchProjects();
}
}, [mode]);
const fetchProjects = async (params = {}) => {
try {
setLoading(true);
const result = await projectAPI.getList({
page: pagination.current,
page_size: pagination.pageSize,
...filters,
...params,
});
setList(result.items || []);
setTotal(result.total || 0);
} catch (error) {
console.error('Failed to fetch projects:', error);
alert('获取项目列表失败');
} finally {
setLoading(false);
}
};
const handlePageChange = (page) => {
setPagination({ ...pagination, current: page });
fetchProjects({ page });
};
const handleCreate = () => {
setEditingId(null);
setForm({
project_no: '',
name: '',
engineering_type: '',
contract_amount: '',
signing_date: '',
start_date: '',
planned_end_date: '',
});
setModalVisible(true);
};
const handleEdit = async (id) => {
try {
const project = await projectAPI.getDetail(id);
setForm(project);
setEditingId(id);
setModalVisible(true);
} catch (error) {
console.error('Failed to fetch project detail:', error);
alert('获取项目详情失败');
}
};
const handleDelete = async (id) => {
if (!window.confirm('确定要删除该项目吗?')) {
return;
}
try {
await projectAPI.delete(id);
alert('项目删除成功');
fetchProjects();
} catch (error) {
console.error('Failed to delete project:', error);
alert('删除项目失败');
}
};
const handleView = (id) => {
navigate(`/projects/${id}`);
};
const handleSubmit = async (e) => {
e.preventDefault();
const requiredFields = ['project_no', 'name', 'engineering_type', 'contract_amount'];
const missingFields = requiredFields.filter(field => !form[field]);
if (missingFields.length > 0) {
alert(`请填写必填字段: ${missingFields.join(', ')}`);
return;
}
try {
if (editingId) {
await projectAPI.update(editingId, form);
alert('项目更新成功');
} else {
await projectAPI.create(form);
alert('项目创建成功');
}
setModalVisible(false);
fetchProjects();
} catch (error) {
console.error('Failed to save project:', error);
alert('保存项目失败');
}
};
const handleCancel = () => {
setModalVisible(false);
setEditingId(null);
setForm({
project_no: '',
name: '',
engineering_type: '',
contract_amount: '',
signing_date: '',
start_date: '',
planned_end_date: '',
});
};
const canEdit = (createdBy) => {
return user.role === 'admin' || (user.role === 'market' && createdBy === user?.id);
};
const canDelete = (createdBy) => {
return user.role === 'admin' || (user.role === 'market' && createdBy === user?.id);
};
return (
<div>
<h1 style={{ fontSize: '24px', marginBottom: '24px' }}>
{mode === 'create' ? '新建项目' : '项目列表'}
</h1>
<div style={{ marginBottom: '16px', display: 'flex', gap: '12px' }}>
<input
type="text"
placeholder="搜索项目名称、合同编号..."
style={{
padding: '8px 12px',
border: '1px solid #d9d9d9',
borderRadius: '4px',
fontSize: '14px',
width: '300px',
}}
/>
<select
style={{
padding: '8px 12px',
border: '1px solid #d9d9d9',
borderRadius: '4px',
fontSize: '14px',
}}
>
<option value="">工程类别</option>
<option value="基建">基建</option>
<option value="业扩">业扩</option>
<option value="客户">客户</option>
<option value="营销">营销</option>
<option value="检修">检修</option>
</select>
{user?.role === 'admin' && (
<button
className="btn btn-primary"
onClick={handleCreate}
>
新建项目
</button>
)}
</div>
{loading ? (
<div style={{ textAlign: 'center', padding: '40px' }}>加载中...</div>
) : list.length === 0 ? (
<div style={{ textAlign: 'center', padding: '40px', color: '#999' }}>
暂无项目
</div>
) : (
<>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ background: '#fafafa' }}>
<th style={{ padding: '12px 16px', textAlign: 'left', fontSize: '14px', fontWeight: 600, borderBottom: '1px solid #e8e8e8' }}>
合同编号
</th>
<th style={{ padding: '12px 16px', textAlign: 'left', fontSize: '14px', fontWeight: 600', borderBottom: '1px solid #e8e8e8' }}>
项目名称
</th>
<th style={{ padding: '12px 16px', textAlign: 'left', fontSize: '14px', fontWeight: 600', borderBottom: '1px solid #e8e8e8' }}>
工程类别
</th>
<th style={{ padding: '12px 16px', textAlign: 'right', fontSize: '14px', fontWeight: 600', borderBottom: '1px solid #e8e8e8' }}>
合同金额(万元)
</th>
<th style={{ padding: '12px 16px', textAlign: 'center', fontSize: '14px', fontWeight: 600', borderBottom: '1px solid #e8e8e8' }}>
操作
</th>
</tr>
</thead>
<tbody>
{list.map((project) => (
<tr
key={project.id}
style={{
borderBottom: '1px solid #f0f0f0',
cursor: 'pointer',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#fafafa';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'transparent';
}}
onClick={() => handleView(project.id)}
>
<td style={{ padding: '12px 16px', fontSize: '14px', color: '#666' }}>
{project.project_no}
</td>
<td style={{ padding: '12px 16px', fontSize: '14px', color: '#333' }}>
{project.name}
</td>
<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>
<td style={{ padding: '12px 16px', fontSize: '14px', textAlign: 'center' }}>
<button
className="btn btn-link"
onClick={(e) => {
e.stopPropagation();
handleView(project.id);
}}
>
查看
</button>
{canEdit(project.created_by) && (
<button
className="btn btn-link"
onClick={(e) => {
e.stopPropagation();
handleEdit(project.id);
}}
>
编辑
</button>
)}
{canDelete(project.created_by) && (
<button
className="btn btn-danger"
onClick={(e) => {
e.stopPropagation();
handleDelete(project.id);
}}
>
删除
</button>
)}
</td>
</tr>
))}
</tbody>
</table>
<div
style={{
marginTop: '24px',
display: 'flex',
justifyContent: 'center',
gap: '12px',
}}
>
{total > pagination.pageSize && (
<>
<button
className="btn btn-default"
disabled={pagination.current === 1}
onClick={() => handlePageChange(pagination.current - 1)}
>
上一页
</button>
<span>
{pagination.current} / {Math.ceil(total / pagination.pageSize)}
</span>
<button
className="btn btn-default"
disabled={pagination.current >= Math.ceil(total / pagination.pageSize)}
onClick={() => handlePageChange(pagination.current + 1)}
>
下一页
</button>
</>
)}
</div>
</>
)}
{modalVisible && (
<div
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'rgba(0, 0, 0, 0.45)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
zIndex: 1000,
}}
onClick={handleCancel}
>
<div
style={{
background: '#ffffff',
width: '800px',
maxWidth: '90vw',
maxHeight: '90vh',
borderRadius: '8px',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
}}
onClick={(e) => e.stopPropagation()}
>
<div
style={{
padding: '16px 24px',
borderBottom: '1px solid #e8e8e8',
fontSize: '16px',
fontWeight: 600',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
{editingId ? '编辑项目' : '新建项目'}
<button
style={{
background: 'transparent',
border: 'none',
fontSize: '20px',
cursor: 'pointer',
color: '#999',
}}
onClick={handleCancel}
>
×
</button>
</div>
<form onSubmit={handleSubmit}>
<div style={{ padding: '24px' }}>
<div style={{ marginBottom: '20px' }}>
<label
style={{
display: 'block',
marginBottom: '8px',
color: '#333',
fontSize: '14px',
fontWeight: '500',
}}
>
合同编号 *
</label>
<input
type="text"
name="project_no"
value={form.project_no}
onChange={handleChange}
required
style={{
width: '100%',
height: '32px',
padding: '4px 12px',
border: '1px solid #d9d9d9',
borderRadius: '4px',
fontSize: '14px',
}}
/>
</div>
<div style={{ marginBottom: '20px' }}>
<label
style={{
display: 'block',
marginBottom: '8px',
color: '#333',
fontSize: '14px',
fontWeight: '500',
}}
>
项目名称 *
</label>
<input
type="text"
name="name"
value={form.name}
onChange={handleChange}
required
style={{
width: '100%',
height: '32px',
padding: '4px 12px',
border: '1px solid #d9d9d9',
borderRadius: '4px',
fontSize: '14px',
}}
/>
</div>
<div style={{ marginBottom: '20px' }}>
<label
style={{
display: 'block',
marginBottom: '8px',
color: '#333',
fontSize: '14px',
fontWeight: '500',
}}
>
工程类别 *
</label>
<select
name="engineering_type"
value={form.engineering_type}
onChange={handleChange}
required
style={{
width: '100%',
height: '32px',
padding: '4px 12px',
border: '1px solid #d9d9d9',
borderRadius: '4px',
fontSize: '14px',
}}
>
<option value="">请选择</option>
<option value="基建">基建</option>
<option value="业扩">业扩</option>
<option value="客户">客户</option>
<option value="营销">营销</option>
<option value="检修">检修</option>
</select>
</div>
<div style={{ marginBottom: '20px' }}>
<label
style={{
display: 'block',
marginBottom: '8px',
color: '#333',
fontSize: '14px',
fontWeight: '500',
}}
>
合同金额(万元) *
</label>
<input
type="number"
name="contract_amount"
value={form.contract_amount}
onChange={handleChange}
required
min="0"
step="0.01"
style={{
width: '100%',
height: '32px',
padding: '4px 12px',
border: '1px solid #d9d9d9',
borderRadius: '4px',
fontSize: '14px',
}}
/>
</div>
<div style={{ marginBottom: '20px' }}>
<label
style={{
display: 'block',
marginBottom: '8px',
color: '#333',
fontSize: '14px',
fontWeight: '500',
}}
>
签订日期 *
</label>
<input
type="date"
name="signing_date"
value={form.signing_date}
onChange={handleChange}
required
style={{
width: '100%',
height: '32px',
padding: '4px 12px',
border: '1px solid #d9d9d9',
borderRadius: '4px',
fontSize: '14px',
}}
/>
</div>
<div style={{ marginBottom: '20px' }}>
<label
style={{
display: 'block',
marginBottom: '8px',
color: '#333',
fontSize: '14px',
fontWeight: '500',
}}
>
开工日期
</label>
<input
type="date"
name="start_date"
value={form.start_date}
onChange={handleChange}
style={{
width: '100%',
height: '32px',
padding: '4px 12px',
border: '1px solid #d9d9d9d9',
borderRadius: '4px',
fontSize: '14px',
}}
/>
</div>
<div style={{ marginBottom: '20px' }}>
<label
style={{
display: 'block',
marginBottom: '8px',
color: '#333',
fontSize: '14px',
fontWeight: '500',
}}
>
计划竣工日期
</label>
<input
type="date"
name="planned_end_date"
value={form.planned_end_date}
onChange={handleChange}
style={{
width: '100%',
height: '32px',
padding: '4px 12px',
border: '1px solid #d9d9d9d9',
borderRadius: '4px',
fontSize: '14px',
}}
/>
</div>
<div
style={{
marginTop: '24px',
padding: '16px 24px',
borderTop: '1px solid #e8e8e8',
display: 'flex',
justifyContent: 'flex',
gap: '12px',
}}
>
<button
className="btn btn-default"
onClick={handleCancel}
>
取消
</button>
<button
type="submit"
className="btn btn-primary"
onClick={() => {}}
>
保存
</button>
</div>
</div>
</form>
</div>
</div>
)}
</div>
);
};
export default ProjectList;
@@ -0,0 +1,387 @@
import React, { useState, useEffect } from 'react';
import { authAPI } from '../../services/auth';
import { projectAPI } from '../../services/project';
const Statistics = () => {
const [statistics, setStatistics] = useState(null);
const [groupStatistics, setGroupStatistics] = useState([]);
const [timelineStatistics, setTimelineStatistics] = useState([]);
const [loading, setLoading] = useState(true);
const [filters, setFilters] = useState({
group_by: 'engineering_type',
time_field: 'signing_date',
group_by_time: 'month',
});
useEffect(() => {
fetchData();
}, []);
const fetchData = async () => {
try {
setLoading(true);
const [statsResponse, groupResponse, timelineResponse] = await Promise.all([
projectAPI.getStatistics(filters),
projectAPI.getGroupStatistics(filters),
projectAPI.getTimelineStatistics(filters),
]);
setStatistics(statsResponse);
setGroupStatistics(groupResponse || []);
setTimelineStatistics(timelineResponse || []);
} catch (error) {
console.error('Failed to fetch statistics data:', error);
alert('获取统计数据失败');
} finally {
setLoading(false);
}
};
const handleFilterChange = (field, value) => {
setFilters({ ...filters, [field]: value });
fetchData();
};
if (loading) {
return <div>加载中...</div>;
}
return (
<div>
<h1 style={{ fontSize: '24px', marginBottom: '24px' }}>
项目统计
</h1>
<div
style={{
marginBottom: '24px',
padding: '20px',
background: '#ffffff',
borderRadius: '8px',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)',
}}
>
<h3
style={{
fontSize: '18px',
marginBottom: '16px',
color: '#333',
fontWeight: 600,
}}
>
筛选条件
</h3>
<div
style={{
display: 'flex',
gap: '16px',
flexWrap: 'wrap',
}}
>
<div>
<label
style={{
marginRight: '8px',
fontSize: '14px',
color: '#333',
}}
>
分组方式
</label>
<select
value={filters.group_by}
onChange={(e) => handleFilterChange('group_by', e.target.value)}
style={{
padding: '8px 12px',
border: '1px solid #d9d9d9',
borderRadius: '4px',
fontSize: '14px',
}}
>
<option value="engineering_type">按工程类别</option>
<option value="project_department">按项目部</option>
</select>
</div>
<div>
<label
style={{
marginRight: '8px',
fontSize: '14px',
color: '#333',
}}
>
时间字段
</label>
<select
value={filters.time_field}
onChange={(e) => handleFilterChange('time_field', e.target.value)}
style={{
padding: '8px 12px',
border: '1px solid #d9d9d9',
borderRadius: '4px',
fontSize: '14px',
}}
>
<option value="signing_date">签订日期</option>
<option value="start_date">开工日期</option>
<option value="planned_end_date">计划竣工日期</option>
</select>
</div>
<div>
<label
style={{
marginRight: '8px',
fontSize: '14px',
color: '#333',
}}
>
时间粒度
</label>
<select
value={filters.group_by_time}
onChange={(e) => handleFilterChange('group_by_time', e.target.value)}
style={{
padding: '8px 12px',
border: '1px solid #d9d9d9d9',
borderRadius: '4px',
fontSize: '14px',
}}
>
<option value="day">按天</option>
<option value="month">按月</option>
<option value="year">按年</option>
</select>
</div>
</div>
</div>
<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-value">
{(statistics?.total_investment || 0).toLocaleString('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}
</div>
</div>
<div className="stat-card">
<div className="stat-label">总合同金额万元</div>
<div className="stat-value">
{(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-value">
{(statistics?.total_receipt_amount || 0).toLocaleString('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}
</div>
</div>
</div>
<div className="card">
<div className="card-header">
分组统计
</div>
{groupStatistics.length > 0 ? (
<table
style={{ width: '100%', borderCollapse: 'collapse' }}
>
<thead>
<tr style={{ background: '#fafafa' }}>
<th
style={{
padding: '12px 16px',
textAlign: 'left',
fontSize: '14px',
fontWeight: 600,
borderBottom: '1px solid #e8e8e8',
}}
>
{filters.group_by === 'engineering_type' ? '工程类别' : '项目部'}
</th>
<th
style={{
padding: '12px 16px',
textAlign: 'right',
fontSize: '14px',
fontWeight: 600',
borderBottom: '1px solid #e8e8e8',
}}
>
项目数
</th>
<th
style={{
padding: '12px 16px',
textAlign: 'right',
fontSize: '14px',
fontWeight: 600',
borderBottom: '1px solid #e8e8e8',
}}
>
合同金额万元
</th>
</tr>
</thead>
<tbody>
{groupStatistics.map((item, index) => (
<tr
key={`${filters.group_by}-${index}`}
style={{
borderBottom: '1px solid #f0f0f0',
}}
>
<td
style={{ padding: '12px 16px', fontSize: '14px', color: '#666' }}
>
{item[filters.group_by]}
</td>
<td
style={{
padding: '12px 16px',
fontSize: '14px,
color: '#666',
textAlign: 'right',
}}
>
{item.count?.toLocaleString('zh-CN')}
</td>
<td
style={{
padding: '12px 16px',
fontSize: '14px',
color: '#666',
textAlign: 'right',
}}
>
{item.total_contract_amount?.toLocaleString('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}
</td>
</tr>
))}
</tbody>
</table>
) : (
<div
style={{ textAlign: 'center', padding: '40px', color: '#999' }}
>
暂无统计数据
</div>
)}
</div>
<div className="card">
<div className="card-header">
时间维度统计
</div>
{timelineStatistics.length > 0 ? (
<table
style={{ width: '100%', borderCollapse: 'collapse' }}
>
<thead>
<tr style={{ background: '#fafafa' }}>
<th
style={{
padding: '12px 16px',
textAlign: 'left',
fontSize: '14px',
fontWeight: 600',
borderBottom: '1px solid #e8e8e8',
}}
>
时间
</th>
<th
style={{
padding: '12px 16px',
textAlign: 'right',
fontSize: '14px',
fontWeight: 600',
borderBottom: '1px solid #e8e8e8',
}}
>
项目数
</th>
<th
style={{
padding: '12px 16px',
textAlign: 'right',
fontSize: '14px',
fontWeight: 600',
borderBottom: '1px solid #e8e8e8',
}}
>
合同金额万元
</th>
</tr>
</thead>
<tbody>
{timelineStatistics.map((item, index) => ({
key={`${filters.time_field}-${index}`}
style={{
borderBottom: '1px solid #f0f0f0',
}}
>
<td
style={{
padding: '12px 16px',
fontSize: '14px',
color: '#666',
}}
>
{item.month}
</td>
<td
style={{
padding: '12px 16px',
fontSize: '14px',
color: '#666',
textAlign: 'right',
}}
>
{item.count?.toLocaleString('zh-CN')}
</td>
<td
style={{
padding: '12px 16px',
fontSize: '14px',
color: '#666',
textAlign: 'right',
}}
>
{item.total_contract_amount?.toLocaleString('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}
</td>
</tr>
))}
</tbody>
</table>
) : (
<div
style={{ textAlign: 'center', padding: '40px', color: '#999' }}
>
暂无时间统计数据
</div>
)}
</div>
</div>
);
};
export default Statistics;
+580
View File
@@ -0,0 +1,580 @@
import React, { useState, useEffect } from 'react';
import { useAuth } from '../../contexts/AuthContext';
import { userAPI } from '../../services/user';
const UserList = () => {
const { user } = useAuth();
const [list, setList] = useState([]);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(false);
const [pagination, setPagination] = useState({
current: 1,
pageSize: 10,
});
const [modalVisible, setModalVisible] = useState(false);
const [editingId, setEditingId] = useState(null);
const [form, setForm] = useState({
username: '',
password: '',
real_name: '',
department: '',
role: 'other',
email: '',
phone: '',
});
useEffect(() => {
fetchUsers();
}, []);
const fetchUsers = async (params = {}) => {
try {
setLoading(true);
const result = await userAPI.getList({
page: pagination.current,
page_size: pagination.pageSize,
...params,
});
setList(result.items || []);
setTotal(result.total || 0);
} catch (error) {
console.error('Failed to fetch users:', error);
alert('获取用户列表失败');
} finally {
setLoading(false);
}
};
const handlePageChange = (page) => {
setPagination({ ...pagination, current: page });
fetchUsers({ page });
};
const handleCreate = () => {
setEditingId(null);
setForm({
username: '',
password: '',
real_name: '',
department: '',
role: 'other',
email: '',
phone: '',
});
setModalVisible(true);
};
const handleEdit = async (id) => {
try {
const user = await userAPI.getDetail(id);
setForm({ ...user, password: '' });
setEditingId(id);
setModalVisible(true);
} catch (error) {
console.error('Failed to fetch user detail:', error);
alert('获取用户详情失败');
}
};
const handleDelete = async (id) => {
if (!window.confirm('确定要删除该用户吗?')) {
return;
}
try {
await userAPI.delete(id);
alert('用户删除成功');
fetchUsers();
} catch (error) {
console.error('Failed to delete user:', error);
alert('删除用户失败');
}
};
const handleResetPassword = async (id) => {
const newPassword = window.prompt('请输入新密码:');
if (!newPassword) return;
if (newPassword.length < 6) {
alert('密码至少6个字符');
return;
}
try {
await userAPI.resetPassword(id, newPassword);
alert('密码重置成功');
} catch (error) {
console.error('Failed to reset password:', error);
alert('密码重置失败');
}
};
const handleSubmit = async (e) => {
e.preventDefault();
const requiredFields = ['username', 'password', 'real_name', 'department', 'role'];
const missingFields = requiredFields.filter(field => !form[field]);
if (missingFields.length > 0) {
alert(`请填写必填字段: ${missingFields.join(', ')}`);
return;
}
if (form.username.length < 3) {
alert('用户名至少3个字符');
return;
}
if (form.password.length < 6) {
alert('密码至少6个字符');
return;
}
try {
if (editingId) {
await userAPI.update(editingId, form);
alert('用户更新成功');
} else {
await userAPI.create(form);
alert('用户创建成功');
}
setModalVisible(false);
fetchUsers();
} catch (error) {
console.error('Failed to save user:', error);
alert('保存用户失败');
}
};
const handleCancel = () => {
setModalVisible(false);
setEditingId(null);
setForm({
username: '',
password: '',
real_name: '',
department: '',
role: 'other',
email: '',
phone: '',
});
};
return (
<div>
<h1 style={{ fontSize: '24px', marginBottom: '24px' }}>
用户管理
</h1>
<div style={{ marginBottom: '16px' }}>
<input
type="text"
placeholder="搜索用户名、真实姓名、邮箱..."
style={{
padding: '8px 12px',
border: '1px solid #d9d9d9d9',
borderRadius: '4px',
fontSize: '14px',
width: '300px',
}}
/>
<button
className="btn btn-primary"
onClick={handleCreate}
>
新建用户
</button>
</div>
{loading ? (
<div style={{ textAlign: 'center', padding: '40px' }}>加载中...</div>
) : list.length === 0 ? (
<div style={{ textAlign: 'center', padding: '40px', color: '#999' }}>
暂无用户
</div>
) : (
<>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ background: '#fafafa' }}>
<th style={{ padding: '12px 16px', textAlign: 'left', fontSize: '14px', fontWeight: 600, borderBottom: '1px solid #e8e8e8' }}>
用户名
</th>
<th style={{ padding: '12px 16px', textAlign: 'left', fontSize: '14px', fontWeight: 600', borderBottom: '1px solid #e8e8e8' }}>
真实姓名
</th>
<th style={{ padding: '12px 16px', textAlign: 'left', fontSize: '14px', fontWeight: 600', borderBottom: '1px solid #e8e8e8' }}>
部门
</th>
<th style={{ padding: '12px 16px', textAlign: 'left', fontSize: '14px', fontWeight: 600', borderBottom: '1px solid #e8e8e8' }}>
角色
</th>
<th style={{ padding: '12px 16px', textAlign: 'center', fontSize: '14px', fontWeight: 600', borderBottom: '1px solid #e8e8e8' }}>
操作
</th>
</tr>
</thead>
<tbody>
{list.map((user) => (
<tr
key={user.id}
style={{
borderBottom: '1px solid #f0f0f0',
}}
>
<td style={{ padding: '12px 16px', fontSize: '14px', color: '#666' }}>
{user.username}
</td>
<td style={{ padding: '12px 16px', fontSize: '14px', color: '#333' }}>
{user.real_name}
</td>
<td style={{ padding: '12px 16px', fontSize: '14px', color: '#666' }}>
{user.department}
</td>
<td style={{ padding: '12px 16px', fontSize: '14px', color: '#666' }}>
{user.role === 'admin' ? '管理员' : user.role === 'market' ? '市场部' : '其他'}
</td>
<td style={{ padding: '12px 16px', fontSize: '14px', textAlign: 'center' }}>
<button className="btn btn-link" onClick={() => handleEdit(user.id)}>
编辑
</button>
<button className="btn btn-danger" onClick={() => handleDelete(user.id)}>
删除
</button>
<button className="btn btn-link" onClick={() => handleResetPassword(user.id)}>
重置密码
</button>
</td>
</tr>
))}
</tbody>
</table>
<div
style={{
marginTop: '24px',
display: 'flex',
justifyContent: 'center',
gap: '12px',
}}
>
{total > pagination.pageSize && (
<>
<button
className="btn btn-default"
disabled={pagination.current === 1}
onClick={() => handlePageChange(pagination.current - 1)}
>
上一页
</button>
<span>
{pagination.current} / {Math.ceil(total / pagination.pageSize)}
</span>
<button
className="btn btn-default"
disabled={pagination.current >= Math.ceil(total / pagination.pageSize)}
onClick={() => handlePageChange(pagination.current + 1)}
>
下一页
</button>
</>
)}
</div>
</>
)}
{modalVisible && (
<div
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'rgba(0, 0, 0, 0.45)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
zIndex: 1000,
}}
onClick={handleCancel}
>
<div
style={{
background: '#ffffff',
width: '800px',
maxWidth: '90vw',
maxHeight: '90vh',
borderRadius: '8px',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
}}
onClick={(e) => e.stopPropagation()}
>
<div
style={{
padding: '16px 24px',
borderBottom: '1px solid #e8e8e8',
fontSize: '16px',
fontWeight: 600',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
{editingId ? '编辑用户' : '新建用户'}
<button
style={{
background: 'transparent',
border: 'none',
fontSize: '20px',
cursor: 'pointer',
color: '#999',
}}
onClick={handleCancel}
>
×
</button>
</div>
<form onSubmit={handleSubmit}>
<div style={{ padding: '24px' }}>
<div style={{ marginBottom: '20px' }}>
<label
style={{
display: 'block',
marginBottom: '8px',
color: '#333',
fontSize: '14px',
fontWeight: '500',
}}
>
用户名 *
</label>
<input
type="text"
name="username"
value={form.username}
onChange={handleChange}
required
disabled={!!editingId}
style={{
width: '100%',
height: '32px',
padding: '4px 12px',
border: '1px solid #d9d9d9',
borderRadius: '4px',
fontSize: '14px',
}}
/>
</div>
<div style={{ marginBottom: '20px' }}>
<label
style={{
display: 'block',
marginBottom: '8px',
color: '#333',
fontSize: '14px',
fontWeight: '500',
}}
>
密码 *
</label>
<input
type="password"
name="password"
value={form.password}
onChange={handleChange}
required={!editingId}
placeholder={editingId ? '留空不修改' : '请输入密码'}
style={{
width: '100%',
height: '32px',
padding: '4px 12px',
border: '1px solid #d9d9d9d9',
borderRadius: '4px',
fontSize: '14px',
}}
/>
</div>
<div style={{ marginBottom: '20px' }}>
<label
style={{
display: 'block',
marginBottom: '8px',
color: '#333',
fontSize: '14px',
fontWeight: '500',
}}
>
真实姓名 *
</label>
<input
type="text"
name="real_name"
value={form.real_name}
onChange={handleChange}
required
style={{
width: '100%',
height: '32px',
padding: '4px 12px',
border: '1px solid #d9d9d9',
borderRadius: '4px',
fontSize: '14px',
}}
/>
</div>
<div style={{ marginBottom: '20px' }}>
<label
style={{
display: 'block',
marginBottom: '8px',
color: '#333',
fontSize: '14px',
fontWeight: '500',
}}
>
部门 *
</label>
<select
name="department"
value={form.department}
onChange={handleChange}
required
style={{
width: '100%',
height: '32px',
padding: '4px 12px',
border: '1px solid #d9d9d9',
borderRadius: '4px',
fontSize: '14px',
}}
>
<option value="">请选择部门</option>
<option value="管理部">管理部</option>
<option value="市场部">市场部</option>
<option value="技术部">技术部</option>
<option value="财务部">财务部</option>
<option value="项目部">项目部</option>
</select>
</div>
<div style={{ marginBottom: '20px' }}>
<label
style={{
display: 'block',
marginBottom: '8px',
color: '#333',
fontSize: '14px',
fontWeight: '500',
}}
>
角色 *
</label>
<select
name="role"
value={form.role}
onChange={handleChange}
required
style={{
width: '100%',
height: '32px',
padding: '4px 12px',
border: '1px solid #d9d9d9',
borderRadius: '4px',
fontSize: '14px',
}}
>
<option value="">请选择角色</option>
<option value="admin">管理员</option>
<option value="market">市场部</option>
<option value="other">其他</option>
</select>
</div>
<div style={{ marginBottom: '20px' }}>
<label
style={{
display: 'block',
marginBottom: '8px',
color: '#333',
fontSize: '14px',
fontWeight: '500',
}}
>
邮箱
</label>
<input
type="email"
name="email"
value={form.email}
onChange={handleChange}
placeholder="example@example.com"
style={{
width: '100%',
height: '32px',
padding: '4px 12px',
border: '1px solid #d9d9d9',
borderRadius: '4px',
fontSize: '14px',
}}
/>
</div>
<div style={{ marginBottom: '20px' }}>
<label
style={{
display: 'block',
marginBottom: '8px',
color: '#333',
fontSize: '14px',
fontWeight: '500',
}}
>
电话
</label>
<input
type="tel"
name="phone"
value={form.phone}
onChange={handleChange}
placeholder="请输入手机号"
style={{
width: '100%',
height: '32px',
padding: '4px 12px',
border: '1px solid #d9d9d9',
borderRadius: '4px',
fontSize: '14px',
}}
/>
</div>
<div
style={{
marginTop: '24px',
padding: '16px 24px',
borderTop: '1px solid #e8e8e8',
display: 'flex',
justifyContent: 'flex',
gap: '12px',
}}
>
<button
className="btn btn-default"
onClick={handleCancel}
>
取消
</button>
<button
type="submit"
className="btn btn-primary"
>
保存
</button>
</div>
</div>
</form>
</div>
</div>
)}
</div>
);
};
export default UserList;