Initial commit: ERP system with advance verification fixes

This commit is contained in:
System Administrator
2026-03-25 23:55:36 +07:00
commit 563ca12d76
5920 changed files with 828689 additions and 0 deletions
@@ -0,0 +1,441 @@
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import {
Card, Typography, Button, Space, Modal, Form, Input, DatePicker, Select,
Upload, message, Spin, Empty, Image, Tag, Divider, Popconfirm, Row, Col
} from 'antd';
import {
PlusOutlined, ArrowLeftOutlined, DeleteOutlined,
CameraOutlined, CalendarOutlined, CloudOutlined
} from '@ant-design/icons';
import axios from 'axios';
import dayjs from 'dayjs';
import { useAuthStore } from '../../store/authStore';
const { Title, Paragraph, Text } = Typography;
const { TextArea } = Input;
const { Option } = Select;
// 天气选项
const WEATHER_OPTIONS = [
{ value: 'sunny', label: '☀️ 晴', icon: '☀️' },
{ value: 'cloudy', label: '⛅ 多云', icon: '⛅' },
{ value: 'rainy', label: '🌧️ 雨', icon: '🌧️' },
{ value: 'stormy', label: '⛈️ 雷暴', icon: '⛈️' },
{ value: 'windy', label: '💨 大风', icon: '💨' },
];
interface Log {
id: number;
log_date: string;
weather: string;
work_content: string;
next_plan: string;
issues: string;
recorder_name: string;
photos: Photo[];
created_at: string;
}
interface Photo {
id: number;
photo_url: string;
photo_name: string;
photo_type: string;
file_size: number;
created_at: string;
}
const ConstructionLog: React.FC = () => {
const { id: projectId } = useParams<{ id: string }>();
const navigate = useNavigate();
const [isMobile, setIsMobile] = useState(false);
const [logs, setLogs] = useState<Log[]>([]);
const [loading, setLoading] = useState(true);
const [modalVisible, setModalVisible] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [projectInfo, setProjectInfo] = useState<any>(null);
const [form] = Form.useForm();
const { user } = useAuthStore();
useEffect(() => {
const checkMobile = () => setIsMobile(window.innerWidth <= 768);
checkMobile();
window.addEventListener('resize', checkMobile);
return () => window.removeEventListener('resize', checkMobile);
}, []);
useEffect(() => {
if (projectId) {
fetchLogs();
fetchProjectInfo();
}
}, [projectId]);
const fetchLogs = async () => {
setLoading(true);
try {
const res = await axios.get(`/api/projects/${projectId}/construction-logs`);
if (res.data.success) {
setLogs(res.data.data);
}
} catch (error) {
console.error('获取日志列表失败:', error);
message.error('获取日志列表失败');
} finally {
setLoading(false);
}
};
const fetchProjectInfo = async () => {
try {
const res = await axios.get(`/api/projects/${projectId}`);
if (res.data.success) {
setProjectInfo(res.data.data);
}
} catch (error) {
console.error('获取项目信息失败:', error);
}
};
const handleSubmit = async () => {
try {
const values = await form.validateFields();
setSubmitting(true);
const res = await axios.post(`/api/projects/${projectId}/construction-logs`, {
log_date: values.log_date.format('YYYY-MM-DD'),
weather: values.weather,
work_content: values.work_content,
photos: '', // 暂时为空,后续添加照片上传功能
});
if (res.data.success) {
message.success('日志添加成功');
setModalVisible(false);
form.resetFields();
fetchLogs();
}
} catch (error) {
console.error('添加日志失败:', error);
message.error('添加日志失败');
} finally {
setSubmitting(false);
}
};
const handleDeleteLog = async (logId: number) => {
try {
const res = await axios.delete(`/api/construction-logs/${logId}`);
if (res.data.success) {
message.success('日志删除成功');
fetchLogs();
}
} catch (error) {
console.error('删除日志失败:', error);
message.error('删除日志失败');
}
};
// 按日期分组
const groupedLogs = logs.reduce((acc, log) => {
const month = dayjs(log.log_date).format('YYYY年MM月');
if (!acc[month]) {
acc[month] = [];
}
acc[month].push(log);
return acc;
}, {} as Record<string, Log[]>);
const getWeatherLabel = (value: string) => {
const option = WEATHER_OPTIONS.find(o => o.value === value);
return option ? option.label : value;
};
const formatFileSize = (bytes: number) => {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
};
const renderLogCard = (log: Log) => (
<Card
key={log.id}
style={{
marginBottom: 16,
borderRadius: 12,
boxShadow: '0 2px 8px rgba(0,0,0,0.06)',
}}
styles={{ body: { padding: isMobile ? 16 : 20 } }}
>
{/* 日志头部 */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
<Space>
<CalendarOutlined style={{ color: '#1890ff' }} />
<Text strong style={{ fontSize: 15 }}>{dayjs(log.log_date).format('MM月DD日')}</Text>
<Tag color="blue">{getWeatherLabel(log.weather)}</Tag>
</Space>
<Space>
<Text type="secondary" style={{ fontSize: 12 }}>: {log.recorder_name || '未知'}</Text>
<Popconfirm
title="确定删除此日志?"
description="删除后无法恢复"
onConfirm={() => handleDeleteLog(log.id)}
okText="确定"
cancelText="取消"
>
<Button type="text" danger size="small" icon={<DeleteOutlined />} />
</Popconfirm>
</Space>
</div>
{/* 工作内容 */}
{log.work_content && (
<div style={{ marginBottom: 12 }}>
<Text type="secondary" style={{ fontSize: 12 }}>:</Text>
<Paragraph style={{ margin: '4px 0 0', whiteSpace: 'pre-wrap' }}>
{log.work_content}
</Paragraph>
</div>
)}
{/* 明日计划 */}
{log.next_plan && (
<div style={{ marginBottom: 12 }}>
<Text type="secondary" style={{ fontSize: 12 }}>:</Text>
<Paragraph style={{ margin: '4px 0 0', whiteSpace: 'pre-wrap' }}>
{log.next_plan}
</Paragraph>
</div>
)}
{/* 问题记录 */}
{log.issues && (
<div style={{ marginBottom: 12 }}>
<Text type="secondary" style={{ fontSize: 12 }}>:</Text>
<Paragraph style={{ margin: '4px 0 0', color: '#fa8c16', whiteSpace: 'pre-wrap' }}>
{log.issues}
</Paragraph>
</div>
)}
{/* 照片展示 */}
{log.photos && log.photos.length > 0 && (
<div style={{ marginTop: 12 }}>
<Text type="secondary" style={{ fontSize: 12, marginBottom: 8, display: 'block' }}>
({log.photos.length}):
</Text>
<Image.PreviewGroup>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
{log.photos.map(photo => (
<Image
key={photo.id}
src={photo.photo_url}
width={isMobile ? 80 : 100}
height={isMobile ? 80 : 100}
style={{
borderRadius: 8,
objectFit: 'cover',
cursor: 'pointer'
}}
placeholder={
<div style={{
width: isMobile ? 80 : 100,
height: isMobile ? 80 : 100,
background: '#f0f0f0',
borderRadius: 8,
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
<CameraOutlined style={{ fontSize: 24, color: '#bfbfbf' }} />
</div>
}
/>
))}
</div>
</Image.PreviewGroup>
</div>
)}
</Card>
);
return (
<div style={{
padding: isMobile ? 12 : 24,
maxWidth: 800,
margin: '0 auto'
}}>
{/* 页面头部 */}
<div style={{ marginBottom: isMobile ? 16 : 24 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 8 }}>
<Button
icon={<ArrowLeftOutlined />}
onClick={() => navigate('/construction')}
/>
<div>
<Title level={isMobile ? 4 : 3} style={{ margin: 0 }}>
</Title>
{projectInfo && (
<Text type="secondary" style={{ fontSize: 13 }}>
{projectInfo.name}
</Text>
)}
</div>
</div>
</div>
{/* 日志列表 */}
{loading ? (
<div style={{ textAlign: 'center', padding: 60 }}>
<Spin size="large" />
</div>
) : logs.length === 0 ? (
<Card style={{ borderRadius: 12 }}>
<Empty description="暂无施工日志">
<Button type="primary" icon={<PlusOutlined />} onClick={() => setModalVisible(true)}>
</Button>
</Empty>
</Card>
) : (
<div>
{Object.entries(groupedLogs).map(([month, monthLogs]) => (
<div key={month}>
<Divider orientation="left" style={{ margin: '16px 0' }}>
<Text strong style={{ fontSize: 14 }}>{month}</Text>
</Divider>
{monthLogs.map(log => renderLogCard(log))}
</div>
))}
</div>
)}
{/* 底部添加按钮 */}
<div style={{
position: 'fixed',
bottom: 24,
right: 24,
zIndex: 100
}}>
<Button
type="primary"
icon={<PlusOutlined />}
size="large"
onClick={() => setModalVisible(true)}
style={{
borderRadius: 24,
height: 48,
paddingLeft: 24,
paddingRight: 24,
boxShadow: '0 4px 12px rgba(24, 144, 255, 0.4)'
}}
>
</Button>
</div>
{/* 新增日志弹窗 */}
<Modal
title="新增施工日志"
open={modalVisible}
onOk={handleSubmit}
onCancel={() => setModalVisible(false)}
confirmLoading={submitting}
okText="提交"
cancelText="取消"
width={isMobile ? '95%' : 500}
style={{ top: 20 }}
>
<Form
form={form}
layout="vertical"
initialValues={{
log_date: dayjs(),
weather: 'sunny'
}}
>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="log_date"
label="日期"
rules={[{ required: true, message: '请选择日期' }]}
>
<DatePicker
style={{ width: '100%' }}
size="large"
disabledDate={(current) => current && current > dayjs().endOf('day')}
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="weather"
label="天气"
rules={[{ required: true, message: '请选择天气' }]}
>
<Select size="large">
{WEATHER_OPTIONS.map(opt => (
<Option key={opt.value} value={opt.value}>
{opt.label}
</Option>
))}
</Select>
</Form.Item>
</Col>
</Row>
<Form.Item
name="work_content"
label="今日工作"
rules={[{ required: true, message: '请填写今日工作内容' }]}
>
<TextArea
rows={3}
placeholder="描述今日完成的施工工作..."
size="large"
/>
</Form.Item>
<Form.Item name="next_plan" label="明日计划">
<TextArea
rows={2}
placeholder="明日工作计划..."
size="large"
/>
</Form.Item>
<Form.Item name="issues" label="问题记录">
<TextArea
rows={2}
placeholder="遇到的问题或需要协调的事项..."
size="large"
/>
</Form.Item>
<Form.Item label="上传照片">
<Upload
listType="picture-card"
multiple
maxCount={9}
accept="image/*"
beforeUpload={() => false}
>
<div>
<CameraOutlined style={{ fontSize: 20 }} />
<div style={{ marginTop: 4, fontSize: 12 }}></div>
</div>
</Upload>
<Text type="secondary" style={{ fontSize: 12 }}>
9
</Text>
</Form.Item>
</Form>
</Modal>
</div>
);
};
export default ConstructionLog;