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