备份:大改造前的完整版本 - 修复合同细节/付款节点/文件上传/施工管理/项目保存等BUG
This commit is contained in:
@@ -7,7 +7,7 @@ import {
|
||||
ArrowLeftOutlined, HomeOutlined, UserOutlined, PhoneOutlined,
|
||||
DollarOutlined, FileTextOutlined
|
||||
} from '@ant-design/icons'
|
||||
import axios from 'axios'
|
||||
import apiClient from '../utils/request'
|
||||
import BusinessLedgerTab from '../components/BusinessLedgerTab'
|
||||
|
||||
const { Title, Text } = Typography
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Table, Button, Modal, Form, Input, message, Space, Tag, Card, Row, Col, Statistic, Image } from 'antd'
|
||||
import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, HomeOutlined, BankOutlined } from '@ant-design/icons'
|
||||
import type { ColumnsType } from 'antd/es/table'
|
||||
import FileUpload from '../components/FileUpload'
|
||||
import useFormDraft from '../hooks/useFormDraft'
|
||||
|
||||
interface Contact {
|
||||
name: string
|
||||
@@ -43,6 +44,16 @@ const CustomerPage: React.FC = () => {
|
||||
const [searchText, setSearchText] = useState('')
|
||||
const [form] = Form.useForm()
|
||||
|
||||
// 表单草稿保护
|
||||
const { save: saveDraft, restore: restoreDraft, clear: clearDraft, hasDraft } = useFormDraft({
|
||||
form,
|
||||
storageKey: 'customer_create',
|
||||
})
|
||||
|
||||
const handleFormChange = useCallback(() => {
|
||||
saveDraft()
|
||||
}, [saveDraft])
|
||||
|
||||
const fetchCustomers = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
@@ -160,6 +171,7 @@ const CustomerPage: React.FC = () => {
|
||||
const data = await response.json()
|
||||
if (data.success) {
|
||||
message.success(editingCustomer ? '更新成功' : '创建成功')
|
||||
clearDraft()
|
||||
setModalVisible(false)
|
||||
form.resetFields()
|
||||
setEditingCustomer(null)
|
||||
@@ -206,6 +218,28 @@ const CustomerPage: React.FC = () => {
|
||||
payment_infos: []
|
||||
})
|
||||
setModalVisible(true)
|
||||
// 检查是否有草稿,提示用户是否恢复
|
||||
setTimeout(() => {
|
||||
if (hasDraft()) {
|
||||
Modal.confirm({
|
||||
title: '发现未完成的草稿',
|
||||
content: '检测到上次未提交的客户信息,是否恢复?',
|
||||
okText: '恢复草稿',
|
||||
cancelText: '重新填写',
|
||||
onOk: () => {
|
||||
restoreDraft()
|
||||
},
|
||||
onCancel: () => {
|
||||
clearDraft()
|
||||
form.resetFields()
|
||||
form.setFieldsValue({
|
||||
contacts: [{ name: '', position: '', phone: '', is_primary: true }],
|
||||
payment_infos: []
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -230,11 +264,31 @@ const CustomerPage: React.FC = () => {
|
||||
<Modal
|
||||
title={editingCustomer ? '编辑客户' : '新增客户'}
|
||||
open={modalVisible}
|
||||
onCancel={() => { setModalVisible(false); form.resetFields(); setEditingCustomer(null) }}
|
||||
onCancel={() => {
|
||||
if (form.isFieldsTouched()) {
|
||||
Modal.confirm({
|
||||
title: '确认关闭',
|
||||
content: '表单数据尚未保存,关闭后可通过草稿恢复。确定关闭吗?',
|
||||
okText: '关闭',
|
||||
cancelText: '继续编辑',
|
||||
onOk: () => {
|
||||
saveDraft()
|
||||
setModalVisible(false)
|
||||
form.resetFields()
|
||||
setEditingCustomer(null)
|
||||
},
|
||||
})
|
||||
} else {
|
||||
setModalVisible(false)
|
||||
form.resetFields()
|
||||
setEditingCustomer(null)
|
||||
}
|
||||
}}
|
||||
onOk={() => form.submit()}
|
||||
width={800}
|
||||
maskClosable={false}
|
||||
>
|
||||
<Form form={form} layout="vertical" onFinish={handleSubmit}>
|
||||
<Form form={form} layout="vertical" onFinish={handleSubmit} onValuesChange={handleFormChange}>
|
||||
<Form.Item name="name" label="名称" rules={[{ required: true, message: '请输入名称' }]}>
|
||||
<Input placeholder="客户名称" />
|
||||
</Form.Item>
|
||||
|
||||
@@ -1,438 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Card, Typography, Button, Space, Tag, Table, List, Avatar,
|
||||
Row, Col, Divider, Tabs, Progress, Badge, Rate, Timeline,
|
||||
Statistic, Switch, Alert, Empty
|
||||
} from 'antd';
|
||||
import {
|
||||
UserOutlined, StarOutlined, LikeOutlined, MessageOutlined,
|
||||
EyeOutlined, HeartOutlined, ShoppingCartOutlined,
|
||||
CalendarOutlined, ClockCircleOutlined, CheckCircleOutlined
|
||||
} from '@ant-design/icons';
|
||||
|
||||
const { Title, Paragraph, Text } = Typography;
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
/**
|
||||
* 布局样式预览页面
|
||||
* 展示各种常见UI布局类型及其适用场景
|
||||
*/
|
||||
|
||||
const LayoutShowcase: React.FC = () => {
|
||||
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
|
||||
|
||||
// 模拟数据
|
||||
const listData = [
|
||||
{ id: 1, title: '项目A - 博纳斯线路改造', status: 'active', progress: 75, manager: '张三' },
|
||||
{ id: 2, title: '项目B - 变压器安装工程', status: 'pending', progress: 0, manager: '李四' },
|
||||
{ id: 3, title: '项目C - 电缆敷设施工', status: 'completed', progress: 100, manager: '王五' },
|
||||
];
|
||||
|
||||
const tableColumns = [
|
||||
{ title: '项目名称', dataIndex: 'title', key: 'title' },
|
||||
{ title: '负责人', dataIndex: 'manager', key: 'manager' },
|
||||
{ title: '进度', dataIndex: 'progress', key: 'progress', render: (v: number) => `${v}%` },
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
render: (v: string) => {
|
||||
const colors: Record<string, string> = { active: 'processing', pending: 'default', completed: 'success' };
|
||||
const texts: Record<string, string> = { active: '进行中', pending: '待开始', completed: '已完成' };
|
||||
return <Tag color={colors[v]}>{texts[v]}</Tag>;
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
// ============ 布局类型1: 卡片列表 ============
|
||||
const CardListDemo = () => (
|
||||
<div>
|
||||
<Alert
|
||||
message="卡片列表布局"
|
||||
description="适用于:项目列表、任务列表、产品展示。特点:信息层次清晰、视觉分隔明确、适合移动端"
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
|
||||
{listData.map(item => (
|
||||
<Card
|
||||
key={item.id}
|
||||
style={{ marginBottom: 16, borderRadius: 12 }}
|
||||
hoverable
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<Text strong style={{ fontSize: 16 }}>{item.title}</Text>
|
||||
<br />
|
||||
<Text type="secondary">负责人: {item.manager}</Text>
|
||||
</div>
|
||||
<Tag color={item.status === 'active' ? 'processing' : item.status === 'completed' ? 'success' : 'default'}>
|
||||
{item.status === 'active' ? '进行中' : item.status === 'completed' ? '已完成' : '待开始'}
|
||||
</Tag>
|
||||
</div>
|
||||
<Divider style={{ margin: '12px 0' }} />
|
||||
<Progress percent={item.progress} showInfo={false} />
|
||||
<div style={{ marginTop: 8, display: 'flex', gap: 8 }}>
|
||||
<Button size="small" type="primary">查看详情</Button>
|
||||
<Button size="small">编辑</Button>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
// ============ 布局类型2: 表格布局 ============
|
||||
const TableDemo = () => (
|
||||
<div>
|
||||
<Alert
|
||||
message="表格布局"
|
||||
description="适用于:数据管理、批量操作、对比分析。特点:信息密集、支持排序筛选、适合桌面端大量数据"
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
|
||||
<Table
|
||||
dataSource={listData}
|
||||
columns={tableColumns}
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
// ============ 布局类型3: 网格卡片 ============
|
||||
const GridCardDemo = () => (
|
||||
<div>
|
||||
<Alert
|
||||
message="网格卡片布局"
|
||||
description="适用于:仪表板、快捷入口、统计展示。特点:空间利用率高、视觉均衡、适合展示统计信息"
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col xs={24} sm={12} md={8} lg={6}>
|
||||
<Card hoverable style={{ borderRadius: 12, textAlign: 'center' }}>
|
||||
<Statistic title="进行中项目" value={12} suffix="个" />
|
||||
<Progress percent={60} showInfo={false} style={{ marginTop: 8 }} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={8} lg={6}>
|
||||
<Card hoverable style={{ borderRadius: 12, textAlign: 'center' }}>
|
||||
<Statistic title="待处理任务" value={5} suffix="项" valueStyle={{ color: '#cf1322' }} />
|
||||
<Progress percent={25} showInfo={false} strokeColor="#cf1322" style={{ marginTop: 8 }} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={8} lg={6}>
|
||||
<Card hoverable style={{ borderRadius: 12, textAlign: 'center' }}>
|
||||
<Statistic title="本月完成" value={28} suffix="个" valueStyle={{ color: '#3f8600' }} />
|
||||
<Progress percent={85} showInfo={false} strokeColor="#3f8600" style={{ marginTop: 8 }} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={8} lg={6}>
|
||||
<Card hoverable style={{ borderRadius: 12, textAlign: 'center' }}>
|
||||
<Statistic title="团队成员" value={8} suffix="人" />
|
||||
<Progress percent={100} showInfo={false} style={{ marginTop: 8 }} />
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
|
||||
// ============ 布局类型4: 时间线布局 ============
|
||||
const TimelineDemo = () => (
|
||||
<div>
|
||||
<Alert
|
||||
message="时间线布局"
|
||||
description="适用于:审批流程、施工进度、操作日志。特点:顺序清晰、时间节点明确、适合流程展示"
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
|
||||
<Timeline
|
||||
items={[
|
||||
{
|
||||
color: 'green',
|
||||
children: (
|
||||
<>
|
||||
<Text strong>项目启动</Text>
|
||||
<br />
|
||||
<Text type="secondary">2026-03-01 - 确定项目范围和团队</Text>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
color: 'blue',
|
||||
children: (
|
||||
<>
|
||||
<Text strong>施工准备</Text>
|
||||
<br />
|
||||
<Text type="secondary">2026-03-05 - 材料采购、人员调配</Text>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
color: 'blue',
|
||||
children: (
|
||||
<>
|
||||
<Text strong>施工进行中</Text>
|
||||
<br />
|
||||
<Text type="secondary">2026-03-10 - 开始现场施工</Text>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
color: 'gray',
|
||||
children: (
|
||||
<>
|
||||
<Text strong>竣工验收</Text>
|
||||
<br />
|
||||
<Text type="secondary">预计 2026-04-01</Text>
|
||||
</>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
// ============ 布局类型5: 瀑布流/Feed布局 ============
|
||||
const FeedDemo = () => (
|
||||
<div>
|
||||
<Alert
|
||||
message="Feed流布局"
|
||||
description="适用于:动态消息、施工日志、社交媒体风格。特点:沉浸式阅读、时间倒序、适合移动端滑动"
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
|
||||
<List
|
||||
itemLayout="vertical"
|
||||
dataSource={[
|
||||
{
|
||||
title: '今日施工进展',
|
||||
description: '完成了3号杆塔的基础浇筑工作,混凝土养护中。',
|
||||
author: '张三',
|
||||
date: '今天 14:30',
|
||||
avatar: '👨🔧',
|
||||
},
|
||||
{
|
||||
title: '材料到货通知',
|
||||
description: '电缆材料已到货,存放在仓库A区,请施工组负责人安排领取。',
|
||||
author: '李四',
|
||||
date: '今天 10:15',
|
||||
avatar: '📦',
|
||||
},
|
||||
{
|
||||
title: '安全检查完成',
|
||||
description: '本周安全检查已完成,未发现重大隐患。',
|
||||
author: '王五',
|
||||
date: '昨天 16:00',
|
||||
avatar: '✅',
|
||||
},
|
||||
]}
|
||||
renderItem={(item: any) => (
|
||||
<List.Item>
|
||||
<Card style={{ width: '100%', marginBottom: 12, borderRadius: 12 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 12 }}>
|
||||
<div style={{ fontSize: 32 }}>{item.avatar}</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Text strong>{item.author}</Text>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>{item.date}</Text>
|
||||
</div>
|
||||
<Text strong style={{ fontSize: 15, display: 'block', marginTop: 4 }}>{item.title}</Text>
|
||||
<Text type="secondary">{item.description}</Text>
|
||||
<div style={{ marginTop: 12, display: 'flex', gap: 16 }}>
|
||||
<Space>
|
||||
<LikeOutlined /> 赞
|
||||
</Space>
|
||||
<Space>
|
||||
<MessageOutlined /> 评论
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
// ============ 布局类型6: 详情页布局 ============
|
||||
const DetailDemo = () => (
|
||||
<div>
|
||||
<Alert
|
||||
message="详情页布局"
|
||||
description="适用于:项目详情、订单详情、用户档案。特点:信息分组明确、主次分明、适合深度阅读"
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
|
||||
<Card style={{ borderRadius: 12 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
||||
<Title level={4} style={{ margin: 0 }}>项目详情</Title>
|
||||
<Tag color="processing">进行中</Tag>
|
||||
</div>
|
||||
|
||||
<Row gutter={[24, 16]}>
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<Text type="secondary">项目名称</Text>
|
||||
<br />
|
||||
<Text strong>博纳斯线路改造工程</Text>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<Text type="secondary">客户</Text>
|
||||
<br />
|
||||
<Text strong>博纳斯稀土开采公司</Text>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<Text type="secondary">项目经理</Text>
|
||||
<br />
|
||||
<Text strong>罗仕林</Text>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<Text type="secondary">合同金额</Text>
|
||||
<br />
|
||||
<Text strong>¥1,250,000</Text>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<Text type="secondary">开工日期</Text>
|
||||
<br />
|
||||
<Text strong>2026-03-01</Text>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<Text type="secondary">预计完工</Text>
|
||||
<br />
|
||||
<Text strong>2026-05-30</Text>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Text type="secondary">项目描述</Text>
|
||||
<Paragraph>
|
||||
本项目包括7公里22kV高压线路改造,以及1250kVA变压器安装工程。
|
||||
施工地点位于老挝博纳斯矿区,需考虑当地气候条件。
|
||||
</Paragraph>
|
||||
|
||||
<Divider />
|
||||
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<Text type="secondary">施工进度</Text>
|
||||
</div>
|
||||
<Progress percent={65} status="active" />
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
|
||||
// ============ 布局对比总结 ============
|
||||
const ComparisonTable = () => (
|
||||
<Card title="布局类型对比" style={{ marginTop: 24, borderRadius: 12 }}>
|
||||
<Table
|
||||
dataSource={[
|
||||
{
|
||||
key: '1',
|
||||
layout: '卡片列表',
|
||||
bestFor: '项目/任务列表',
|
||||
mobile: '⭐⭐⭐⭐⭐',
|
||||
desktop: '⭐⭐⭐⭐',
|
||||
dataDensity: '中',
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
layout: '表格',
|
||||
bestFor: '数据管理/分析',
|
||||
mobile: '⭐⭐',
|
||||
desktop: '⭐⭐⭐⭐⭐',
|
||||
dataDensity: '高',
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
layout: '网格卡片',
|
||||
bestFor: '仪表板/统计',
|
||||
mobile: '⭐⭐⭐⭐',
|
||||
desktop: '⭐⭐⭐⭐⭐',
|
||||
dataDensity: '中',
|
||||
},
|
||||
{
|
||||
key: '4',
|
||||
layout: '时间线',
|
||||
bestFor: '流程/进度',
|
||||
mobile: '⭐⭐⭐⭐',
|
||||
desktop: '⭐⭐⭐',
|
||||
dataDensity: '低',
|
||||
},
|
||||
{
|
||||
key: '5',
|
||||
layout: 'Feed流',
|
||||
bestFor: '动态/日志',
|
||||
mobile: '⭐⭐⭐⭐⭐',
|
||||
desktop: '⭐⭐⭐',
|
||||
dataDensity: '低',
|
||||
},
|
||||
{
|
||||
key: '6',
|
||||
layout: '详情页',
|
||||
bestFor: '深度信息',
|
||||
mobile: '⭐⭐⭐',
|
||||
desktop: '⭐⭐⭐⭐⭐',
|
||||
dataDensity: '中',
|
||||
},
|
||||
]}
|
||||
columns={[
|
||||
{ title: '布局类型', dataIndex: 'layout', key: 'layout' },
|
||||
{ title: '适用场景', dataIndex: 'bestFor', key: 'bestFor' },
|
||||
{ title: '移动端', dataIndex: 'mobile', key: 'mobile' },
|
||||
{ title: '桌面端', dataIndex: 'desktop', key: 'desktop' },
|
||||
{ title: '数据密度', dataIndex: 'dataDensity', key: 'dataDensity' },
|
||||
]}
|
||||
pagination={false}
|
||||
size="small"
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ padding: isMobile ? 12 : 24, maxWidth: 1200, margin: '0 auto' }}>
|
||||
<Title level={2}>布局样式预览</Title>
|
||||
<Paragraph type="secondary">
|
||||
此页面展示各种常见UI布局类型,帮助开发者选择合适的布局方式
|
||||
</Paragraph>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Tabs defaultActiveKey="1" tabPosition="top">
|
||||
<TabPane tab="📋 卡片列表" key="1">
|
||||
<CardListDemo />
|
||||
</TabPane>
|
||||
<TabPane tab="📊 表格布局" key="2">
|
||||
<TableDemo />
|
||||
</TabPane>
|
||||
<TabPane tab="🔲 网格卡片" key="3">
|
||||
<GridCardDemo />
|
||||
</TabPane>
|
||||
<TabPane tab="⏱️ 时间线" key="4">
|
||||
<TimelineDemo />
|
||||
</TabPane>
|
||||
<TabPane tab="📝 Feed流" key="5">
|
||||
<FeedDemo />
|
||||
</TabPane>
|
||||
<TabPane tab="📄 详情页" key="6">
|
||||
<DetailDemo />
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
|
||||
<ComparisonTable />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LayoutShowcase;
|
||||
@@ -9,10 +9,10 @@
|
||||
* - 收款信息:支持多个银行账户,标记默认账户
|
||||
* - 业务台账:订单列表、运费总额、已付/未付金额
|
||||
*/
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import {
|
||||
Table, Button, Modal, Form, Input, Select, message, Space, Tag, Card,
|
||||
Row, Col, Popconfirm, Tabs, Descriptions, Upload, Image
|
||||
Row, Col, Popconfirm, Tabs, Descriptions, Upload, Image, Checkbox
|
||||
} from 'antd'
|
||||
import {
|
||||
PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined,
|
||||
@@ -21,15 +21,14 @@ import {
|
||||
import type { ColumnsType } from 'antd/es/table'
|
||||
import dayjs from 'dayjs'
|
||||
import BusinessLedgerTab from '../components/BusinessLedgerTab'
|
||||
import useFormDraft from '../hooks/useFormDraft'
|
||||
|
||||
interface LogisticsCompany {
|
||||
id: number
|
||||
code: string
|
||||
name: string
|
||||
address: string
|
||||
phone: string
|
||||
quotation_description: string
|
||||
status: string
|
||||
remark: string
|
||||
created_at: string
|
||||
contacts: Contact[]
|
||||
@@ -88,7 +87,6 @@ interface OrderRecord {
|
||||
const LogisticsCompaniesPage: React.FC = () => {
|
||||
const [companies, setCompanies] = useState<LogisticsCompany[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [selectedStatus, setSelectedStatus] = useState<string | null>(null)
|
||||
|
||||
const [modalVisible, setModalVisible] = useState(false)
|
||||
const [detailModalVisible, setDetailModalVisible] = useState(false)
|
||||
@@ -106,13 +104,20 @@ const LogisticsCompaniesPage: React.FC = () => {
|
||||
|
||||
const [form] = Form.useForm()
|
||||
|
||||
// 表单草稿保护
|
||||
const { save: saveDraft, restore: restoreDraft, clear: clearDraft, hasDraft } = useFormDraft({
|
||||
form,
|
||||
storageKey: 'logistics_company_create',
|
||||
})
|
||||
|
||||
const handleFormChange = useCallback(() => {
|
||||
saveDraft()
|
||||
}, [saveDraft])
|
||||
|
||||
const fetchCompanies = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const params = new URLSearchParams()
|
||||
if (selectedStatus) params.append('status', selectedStatus)
|
||||
|
||||
const response = await fetch(`/api/logistics-companies?${params}`)
|
||||
const response = await fetch('/api/logistics-companies')
|
||||
const data = await response.json()
|
||||
|
||||
if (data.success) {
|
||||
@@ -147,12 +152,30 @@ const LogisticsCompaniesPage: React.FC = () => {
|
||||
|
||||
useEffect(() => {
|
||||
fetchCompanies()
|
||||
}, [selectedStatus])
|
||||
}, [])
|
||||
|
||||
const handleCreate = () => {
|
||||
setEditingCompany(null)
|
||||
form.resetFields()
|
||||
setModalVisible(true)
|
||||
// 检查是否有草稿,提示用户是否恢复
|
||||
setTimeout(() => {
|
||||
if (hasDraft()) {
|
||||
Modal.confirm({
|
||||
title: '发现未完成的草稿',
|
||||
content: '检测到上次未提交的物流公司信息,是否恢复?',
|
||||
okText: '恢复草稿',
|
||||
cancelText: '重新填写',
|
||||
onOk: () => {
|
||||
restoreDraft()
|
||||
},
|
||||
onCancel: () => {
|
||||
clearDraft()
|
||||
form.resetFields()
|
||||
},
|
||||
})
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
|
||||
const handleEdit = (company: LogisticsCompany) => {
|
||||
@@ -185,15 +208,30 @@ const LogisticsCompaniesPage: React.FC = () => {
|
||||
: '/api/logistics-companies'
|
||||
const method = editingCompany ? 'PUT' : 'POST'
|
||||
|
||||
const { contacts, ...companyData } = values
|
||||
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(values)
|
||||
body: JSON.stringify(companyData)
|
||||
})
|
||||
const data = await response.json()
|
||||
|
||||
if (data.success) {
|
||||
if (!editingCompany && contacts && contacts.length > 0) {
|
||||
const companyId = data.data.id
|
||||
for (const contact of contacts) {
|
||||
if (contact.name) {
|
||||
await fetch(`/api/logistics-companies/${companyId}/contacts`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(contact)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
message.success(editingCompany ? '更新成功' : '创建成功')
|
||||
clearDraft()
|
||||
setModalVisible(false)
|
||||
fetchCompanies()
|
||||
} else {
|
||||
@@ -316,15 +354,6 @@ const LogisticsCompaniesPage: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusTag = (status: string) => {
|
||||
const statusMap: Record<string, { color: string; text: string }> = {
|
||||
active: { color: 'green', text: '合作中' },
|
||||
inactive: { color: 'default', text: '已停用' }
|
||||
}
|
||||
const info = statusMap[status] || { color: 'default', text: status }
|
||||
return <Tag color={info.color}>{info.text}</Tag>
|
||||
}
|
||||
|
||||
const getFreightStatusTag = (status: string) => {
|
||||
const statusMap: Record<string, { color: string; text: string }> = {
|
||||
pending: { color: 'default', text: '待付款' },
|
||||
@@ -359,14 +388,6 @@ const LogisticsCompaniesPage: React.FC = () => {
|
||||
ellipsis: true,
|
||||
render: (v: string) => v || '-'
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 80,
|
||||
align: 'center',
|
||||
render: getStatusTag
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'created_at',
|
||||
@@ -395,7 +416,7 @@ const LogisticsCompaniesPage: React.FC = () => {
|
||||
{ title: '姓名', dataIndex: 'name', key: 'name', width: 100 },
|
||||
{ title: '职位', dataIndex: 'position', key: 'position', width: 80 },
|
||||
{ title: '电话', dataIndex: 'phone', key: 'phone', width: 120 },
|
||||
{ title: '主联系人', dataIndex: 'is_primary', key: 'is_primary', width: 80, render: (v: number) => v ? <Tag color="blue">主联系人</Tag> : null },
|
||||
{ title: '主联系人', dataIndex: 'is_primary', key: 'is_primary', width: 80, render: (v: boolean | number) => v ? <Tag color="blue">主联系人</Tag> : null },
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
@@ -415,7 +436,7 @@ const LogisticsCompaniesPage: React.FC = () => {
|
||||
{ title: '收款户名', dataIndex: 'account_name', key: 'account_name', width: 120 },
|
||||
{ title: '银行账号', dataIndex: 'account_number', key: 'account_number', width: 150 },
|
||||
{ title: '开户银行', dataIndex: 'bank_name', key: 'bank_name', width: 120 },
|
||||
{ title: '默认', dataIndex: 'is_default', key: 'is_default', width: 60, render: (v: number) => v ? <Tag color="green">默认</Tag> : null },
|
||||
{ title: '默认', dataIndex: 'is_default', key: 'is_default', width: 60, render: (v: boolean | number) => v ? <Tag color="green">默认</Tag> : null },
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
@@ -450,14 +471,6 @@ const LogisticsCompaniesPage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<Card extra={<Button type="primary" icon={<PlusOutlined />} onClick={handleCreate}>新建物流公司</Button>}>
|
||||
<Row gutter={16} style={{ marginBottom: 16 }}>
|
||||
<Col span={6}>
|
||||
<Select placeholder="选择状态筛选" allowClear style={{ width: '100%' }} onChange={(v) => setSelectedStatus(v)}>
|
||||
<Select.Option value="active">合作中</Select.Option>
|
||||
<Select.Option value="inactive">已停用</Select.Option>
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Table columns={columns} dataSource={companies} rowKey="id" loading={loading} pagination={{ pageSize: 20 }} size="small" scroll={{ x: 1100 }} />
|
||||
</Card>
|
||||
|
||||
@@ -466,48 +479,77 @@ const LogisticsCompaniesPage: React.FC = () => {
|
||||
title={editingCompany ? '编辑物流公司' : '新建物流公司'}
|
||||
open={modalVisible}
|
||||
onOk={handleSave}
|
||||
onCancel={() => setModalVisible(false)}
|
||||
width={600}
|
||||
onCancel={() => {
|
||||
if (form.isFieldsTouched()) {
|
||||
Modal.confirm({
|
||||
title: '确认关闭',
|
||||
content: '表单数据尚未保存,关闭后可通过草稿恢复。确定关闭吗?',
|
||||
okText: '关闭',
|
||||
cancelText: '继续编辑',
|
||||
onOk: () => {
|
||||
saveDraft()
|
||||
setModalVisible(false)
|
||||
},
|
||||
})
|
||||
} else {
|
||||
setModalVisible(false)
|
||||
}
|
||||
}}
|
||||
width={700}
|
||||
maskClosable={false}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item name="name" label="公司名称" rules={[{ required: true }]}>
|
||||
<Input placeholder="请输入公司名称" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="code" label="公司编码">
|
||||
<Input placeholder="自动生成或手动输入" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item name="phone" label="联系电话">
|
||||
<Input placeholder="请输入联系电话" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form form={form} layout="vertical" onValuesChange={handleFormChange}>
|
||||
<Form.Item name="name" label="公司名称" rules={[{ required: true }]}>
|
||||
<Input placeholder="请输入公司名称" />
|
||||
</Form.Item>
|
||||
<Form.Item name="address" label="地址">
|
||||
<Input placeholder="请输入地址" />
|
||||
</Form.Item>
|
||||
<Form.Item name="quotation_description" label="报价描述">
|
||||
<Input.TextArea rows={3} placeholder="请输入报价描述(如:中国-老挝陆运报价、时效等)" />
|
||||
</Form.Item>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item name="status" label="状态">
|
||||
<Select placeholder="请选择状态">
|
||||
<Select.Option value="active">合作中</Select.Option>
|
||||
<Select.Option value="inactive">已停用</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item name="remark" label="备注">
|
||||
<Input.TextArea rows={2} placeholder="请输入备注" />
|
||||
</Form.Item>
|
||||
{!editingCompany && (
|
||||
<Form.List name="contacts">
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
<div style={{ marginBottom: 8, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<span style={{ fontWeight: 500 }}>联系人</span>
|
||||
<Button type="dashed" size="small" icon={<PlusOutlined />} onClick={() => add()}>添加联系人</Button>
|
||||
</div>
|
||||
{fields.map(({ key, name, ...restField }) => (
|
||||
<Row key={key} gutter={8} style={{ marginBottom: 8 }}>
|
||||
<Col span={6}>
|
||||
<Form.Item {...restField} name={[name, 'name']} rules={[{ required: true, message: '必填' }]}>
|
||||
<Input placeholder="姓名" size="small" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={5}>
|
||||
<Form.Item {...restField} name={[name, 'phone']}>
|
||||
<Input placeholder="电话" size="small" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={5}>
|
||||
<Form.Item {...restField} name={[name, 'position']}>
|
||||
<Input placeholder="职位" size="small" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={5}>
|
||||
<Form.Item {...restField} name={[name, 'is_primary']} valuePropName="checked">
|
||||
<Checkbox>主联系人</Checkbox>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={3}>
|
||||
<Button type="text" danger size="small" icon={<DeleteOutlined />} onClick={() => remove(name)} />
|
||||
</Col>
|
||||
</Row>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
)}
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
@@ -525,13 +567,11 @@ const LogisticsCompaniesPage: React.FC = () => {
|
||||
<Tabs.TabPane tab={<span><FileTextOutlined /> 基本信息</span>} key="basic">
|
||||
<Descriptions bordered column={2}>
|
||||
<Descriptions.Item label="公司名称">{currentCompany.name}</Descriptions.Item>
|
||||
<Descriptions.Item label="公司编码">{currentCompany.code}</Descriptions.Item>
|
||||
<Descriptions.Item label="联系电话">{currentCompany.phone || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="邮箱">{currentCompany.email || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="创建时间">{currentCompany.created_at}</Descriptions.Item>
|
||||
<Descriptions.Item label="地址" span={2}>{currentCompany.address || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="报价描述" span={2}>{currentCompany.quotation_description || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="状态">{getStatusTag(currentCompany.status)}</Descriptions.Item>
|
||||
<Descriptions.Item label="创建时间">{currentCompany.created_at}</Descriptions.Item>
|
||||
{currentCompany.remark && <Descriptions.Item label="备注" span={2}>{currentCompany.remark}</Descriptions.Item>}
|
||||
</Descriptions>
|
||||
</Tabs.TabPane>
|
||||
@@ -588,10 +628,10 @@ const LogisticsCompaniesPage: React.FC = () => {
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item name="is_primary" label="主联系人" valuePropName="checked">
|
||||
<Form.Item name="is_primary" label="主联系人">
|
||||
<Select placeholder="是否为主联系人">
|
||||
<Select.Option value={1}>是</Select.Option>
|
||||
<Select.Option value={0}>否</Select.Option>
|
||||
<Select.Option value={true}>是</Select.Option>
|
||||
<Select.Option value={false}>否</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
@@ -626,8 +666,8 @@ const LogisticsCompaniesPage: React.FC = () => {
|
||||
</Form.Item>
|
||||
<Form.Item name="is_default" label="默认账户">
|
||||
<Select placeholder="是否为默认账户">
|
||||
<Select.Option value={1}>是</Select.Option>
|
||||
<Select.Option value={0}>否</Select.Option>
|
||||
<Select.Option value={true}>是</Select.Option>
|
||||
<Select.Option value={false}>否</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Table, Button, Card, Space, Tag, Modal, Form, Input, InputNumber, Select, DatePicker, message, Descriptions, Image, Divider, Tabs } from 'antd';
|
||||
import { PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined, UndoOutlined } from '@ant-design/icons';
|
||||
import dayjs from 'dayjs';
|
||||
import { useAuthStore } from '../store/authStore';
|
||||
import FileUpload from '../components/FileUpload';
|
||||
import useFormDraft from '../hooks/useFormDraft';
|
||||
|
||||
const { Option } = Select;
|
||||
const { TextArea } = Input;
|
||||
@@ -69,6 +70,16 @@ const PaymentRequestsPage: React.FC = () => {
|
||||
const [selectedRecord, setSelectedRecord] = useState<any>(null);
|
||||
const [form] = Form.useForm();
|
||||
const [exchangeRates, setExchangeRates] = useState<Record<string, number>>({});
|
||||
|
||||
// 表单草稿保护
|
||||
const { save: saveDraft, restore: restoreDraft, clear: clearDraft, hasDraft } = useFormDraft({
|
||||
form,
|
||||
storageKey: 'payment_request_create',
|
||||
});
|
||||
|
||||
const handleFormChange = useCallback(() => {
|
||||
saveDraft();
|
||||
}, [saveDraft]);
|
||||
|
||||
// 数据列表
|
||||
const [subcontractors, setSubcontractors] = useState<PayeeEntity[]>([]);
|
||||
@@ -212,6 +223,31 @@ const PaymentRequestsPage: React.FC = () => {
|
||||
expense_type: 'company'
|
||||
});
|
||||
setModalVisible(true);
|
||||
setTimeout(() => {
|
||||
if (hasDraft()) {
|
||||
Modal.confirm({
|
||||
title: '发现未完成的草稿',
|
||||
content: '检测到上次未提交的付款申请,是否恢复?',
|
||||
okText: '恢复草稿',
|
||||
cancelText: '重新填写',
|
||||
onOk: () => {
|
||||
restoreDraft();
|
||||
},
|
||||
onCancel: () => {
|
||||
clearDraft();
|
||||
form.resetFields();
|
||||
form.setFieldsValue({
|
||||
application_date: dayjs(),
|
||||
currency: 'CNY',
|
||||
applicant: user?.name || user?.username || '当前用户',
|
||||
attachments: [],
|
||||
payee_type: 'other',
|
||||
expense_type: 'company'
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const handleEdit = (record: any) => {
|
||||
@@ -307,6 +343,7 @@ const PaymentRequestsPage: React.FC = () => {
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
message.success(editingId ? '更新成功' : '创建成功');
|
||||
clearDraft();
|
||||
setModalVisible(false);
|
||||
fetchRequests();
|
||||
} else {
|
||||
@@ -432,8 +469,23 @@ const PaymentRequestsPage: React.FC = () => {
|
||||
</Tabs>
|
||||
</Card>
|
||||
|
||||
<Modal title={editingId ? '编辑付款申请' : '新建付款申请'} open={modalVisible} onOk={handleSubmit} onCancel={() => setModalVisible(false)} width={900}>
|
||||
<Form form={form} layout="vertical">
|
||||
<Modal title={editingId ? '编辑付款申请' : '新建付款申请'} open={modalVisible} onOk={handleSubmit} onCancel={() => {
|
||||
if (form.isFieldsTouched()) {
|
||||
Modal.confirm({
|
||||
title: '确认关闭',
|
||||
content: '表单数据尚未保存,关闭后可通过草稿恢复。确定关闭吗?',
|
||||
okText: '关闭',
|
||||
cancelText: '继续编辑',
|
||||
onOk: () => {
|
||||
saveDraft();
|
||||
setModalVisible(false);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
setModalVisible(false);
|
||||
}
|
||||
}} maskClosable={false} width={900}>
|
||||
<Form form={form} layout="vertical" onValuesChange={handleFormChange}>
|
||||
<Form.Item name="applicant" label="申请人">
|
||||
<Input disabled style={{ color: 'rgba(0,0,0,0.85)', backgroundColor: '#f5f5f5' }} />
|
||||
</Form.Item>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { Card, Typography, Button, Table, Tag, Space, Modal, Form, Input, Select, DatePicker, InputNumber, message, Row, Col, Statistic } from 'antd';
|
||||
import { PlusOutlined, SearchOutlined, ShoppingOutlined } from '@ant-design/icons';
|
||||
import useFormDraft from '../hooks/useFormDraft';
|
||||
|
||||
const { Title, Paragraph } = Typography;
|
||||
const { RangePicker } = DatePicker;
|
||||
@@ -10,6 +11,16 @@ const ProcurementPage: React.FC = () => {
|
||||
const [modalVisible, setModalVisible] = React.useState(false);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
// 表单草稿保护
|
||||
const { save: saveDraft, restore: restoreDraft, clear: clearDraft, hasDraft } = useFormDraft({
|
||||
form,
|
||||
storageKey: 'procurement_create',
|
||||
})
|
||||
|
||||
const handleFormChange = useCallback(() => {
|
||||
saveDraft()
|
||||
}, [saveDraft])
|
||||
|
||||
const columns = [
|
||||
{ title: '采购单号', dataIndex: 'code', key: 'code', width: 140 },
|
||||
{ title: '采购日期', dataIndex: 'date', key: 'date', width: 120 },
|
||||
@@ -60,6 +71,7 @@ const ProcurementPage: React.FC = () => {
|
||||
|
||||
const handleSubmit = () => {
|
||||
message.success('采购申请已提交');
|
||||
clearDraft()
|
||||
setModalVisible(false);
|
||||
};
|
||||
|
||||
@@ -73,7 +85,27 @@ const ProcurementPage: React.FC = () => {
|
||||
<Space>
|
||||
<RangePicker placeholder={['开始日期', '结束日期']} />
|
||||
<Input.Search placeholder="搜索采购单号" style={{ width: 200 }} />
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => setModalVisible(true)}>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => {
|
||||
setModalVisible(true)
|
||||
// 检查是否有草稿,提示用户是否恢复
|
||||
setTimeout(() => {
|
||||
if (hasDraft()) {
|
||||
Modal.confirm({
|
||||
title: '发现未完成的草稿',
|
||||
content: '检测到上次未提交的采购信息,是否恢复?',
|
||||
okText: '恢复草稿',
|
||||
cancelText: '重新填写',
|
||||
onOk: () => {
|
||||
restoreDraft()
|
||||
},
|
||||
onCancel: () => {
|
||||
clearDraft()
|
||||
form.resetFields()
|
||||
},
|
||||
})
|
||||
}
|
||||
}, 0)
|
||||
}}>
|
||||
新建采购
|
||||
</Button>
|
||||
</Space>
|
||||
@@ -109,11 +141,27 @@ const ProcurementPage: React.FC = () => {
|
||||
<Modal
|
||||
title="新建采购申请"
|
||||
open={modalVisible}
|
||||
onCancel={() => setModalVisible(false)}
|
||||
onCancel={() => {
|
||||
if (form.isFieldsTouched()) {
|
||||
Modal.confirm({
|
||||
title: '确认关闭',
|
||||
content: '表单数据尚未保存,关闭后可通过草稿恢复。确定关闭吗?',
|
||||
okText: '关闭',
|
||||
cancelText: '继续编辑',
|
||||
onOk: () => {
|
||||
saveDraft()
|
||||
setModalVisible(false)
|
||||
},
|
||||
})
|
||||
} else {
|
||||
setModalVisible(false)
|
||||
}
|
||||
}}
|
||||
onOk={handleSubmit}
|
||||
width={600}
|
||||
maskClosable={false}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form form={form} layout="vertical" onValuesChange={handleFormChange}>
|
||||
<Form.Item label="供应商" name="supplier" rules={[{ required: true }]}>
|
||||
<Select placeholder="选择供应商" options={[
|
||||
{ value: 'supplier1', label: '老挝电力设备公司' },
|
||||
|
||||
@@ -116,7 +116,6 @@ const ProductPage: React.FC = () => {
|
||||
const fetchProducts = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
console.log('开始获取商品数据')
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
pageSize: pageSize.toString(),
|
||||
@@ -124,14 +123,10 @@ const ProductPage: React.FC = () => {
|
||||
...(searchText && { search: searchText })
|
||||
})
|
||||
|
||||
console.log('请求URL:', `/api/products?${params}`)
|
||||
const response = await fetch(`/api/products?${params}`)
|
||||
console.log('响应状态:', response.status)
|
||||
const data = await response.json()
|
||||
console.log('响应数据:', data)
|
||||
|
||||
if (data.success && data.data && Array.isArray(data.data)) {
|
||||
console.log('商品数据:', data.data.length, '条')
|
||||
setProducts(data.data)
|
||||
setTotal(data.total || data.data.length)
|
||||
} else {
|
||||
@@ -146,7 +141,6 @@ const ProductPage: React.FC = () => {
|
||||
setTotal(0)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
console.log('获取商品完成')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,21 +338,15 @@ const ProductPage: React.FC = () => {
|
||||
setImportProgress(0)
|
||||
|
||||
try {
|
||||
console.log('文件信息:', file)
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
console.log('表单数据:', formData)
|
||||
console.log('发送请求到:', '/api/products/batch-import')
|
||||
const response = await fetch('/api/products/batch-import', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
|
||||
console.log('响应状态:', response.status)
|
||||
console.log('响应状态文本:', response.statusText)
|
||||
const data = await response.json()
|
||||
console.log('响应数据:', data)
|
||||
|
||||
if (data.success) {
|
||||
message.success(data.message)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Card, Typography, Form, Input, Button, Avatar, Space, Upload, message, Row, Col, Modal } from 'antd';
|
||||
import { UserOutlined, LockOutlined, PhoneOutlined, MailOutlined, UploadOutlined } from '@ant-design/icons';
|
||||
import { useAuthStore } from '../store/authStore';
|
||||
import useFormDraft from '../hooks/useFormDraft';
|
||||
|
||||
const { Title, Paragraph } = Typography;
|
||||
|
||||
@@ -15,6 +16,50 @@ const ProfilePage: React.FC = () => {
|
||||
const [passportUrl, setPassportUrl] = useState<string | undefined>(user?.passport);
|
||||
const [driverLicenseUrl, setDriverLicenseUrl] = useState<string | undefined>(user?.driverLicense);
|
||||
|
||||
// 表单草稿保护
|
||||
const { save: saveDraft, restore: restoreDraft, clear: clearDraft, hasDraft } = useFormDraft({
|
||||
form,
|
||||
storageKey: 'profile_edit',
|
||||
onRestore: (data) => {
|
||||
if (data.avatarUrl !== undefined) setAvatarUrl(data.avatarUrl)
|
||||
if (data.passportUrl !== undefined) setPassportUrl(data.passportUrl)
|
||||
if (data.driverLicenseUrl !== undefined) setDriverLicenseUrl(data.driverLicenseUrl)
|
||||
},
|
||||
})
|
||||
|
||||
const handleFormChange = useCallback(() => {
|
||||
saveDraft({ avatarUrl, passportUrl, driverLicenseUrl })
|
||||
}, [saveDraft, avatarUrl, passportUrl, driverLicenseUrl])
|
||||
|
||||
// 页面级 beforeunload 保护
|
||||
useEffect(() => {
|
||||
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
|
||||
if (form.isFieldsTouched()) {
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
window.addEventListener('beforeunload', handleBeforeUnload)
|
||||
return () => window.removeEventListener('beforeunload', handleBeforeUnload)
|
||||
}, [form])
|
||||
|
||||
// 页面加载时检查草稿
|
||||
useEffect(() => {
|
||||
if (hasDraft()) {
|
||||
Modal.confirm({
|
||||
title: '发现未完成的草稿',
|
||||
content: '检测到上次未保存的个人信息修改,是否恢复?',
|
||||
okText: '恢复草稿',
|
||||
cancelText: '放弃草稿',
|
||||
onOk: () => {
|
||||
restoreDraft()
|
||||
},
|
||||
onCancel: () => {
|
||||
clearDraft()
|
||||
},
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
form.setFieldsValue({
|
||||
@@ -158,7 +203,7 @@ const ProfilePage: React.FC = () => {
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Form form={form} layout="vertical" onFinish={handleSubmit}>
|
||||
<Form form={form} layout="vertical" onFinish={handleSubmit} onValuesChange={handleFormChange}>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item label="姓名" name="name" rules={[{ required: true, message: '请输入姓名' }]}>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
* - 仅填写需求描述和预计金额
|
||||
* - 新增需求日期字段
|
||||
*/
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { useNavigate, useLocation } from 'react-router-dom'
|
||||
import {
|
||||
Table, Button, Modal, Form, Input, Select, message, Space, Tag, Card,
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
} from '@ant-design/icons'
|
||||
import type { ColumnsType } from 'antd/es/table'
|
||||
import dayjs from 'dayjs'
|
||||
import useFormDraft from '../hooks/useFormDraft'
|
||||
|
||||
interface PurchaseRequest {
|
||||
id: number
|
||||
@@ -71,6 +72,21 @@ const PurchaseRequestsPage: React.FC = () => {
|
||||
const [form] = Form.useForm()
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
|
||||
// 表单草稿保护
|
||||
const { save: saveDraft, restore: restoreDraft, clear: clearDraft, hasDraft } = useFormDraft({
|
||||
form,
|
||||
storageKey: 'purchase_request_create',
|
||||
onRestore: (data) => {
|
||||
if (data.attachments) setAttachments(data.attachments)
|
||||
if (data.purchaseType) setPurchaseType(data.purchaseType)
|
||||
if (data.currency) setCurrency(data.currency)
|
||||
},
|
||||
})
|
||||
|
||||
const handleFormChange = useCallback(() => {
|
||||
saveDraft({ attachments, purchaseType, currency })
|
||||
}, [saveDraft, attachments, purchaseType, currency])
|
||||
|
||||
const exchangeRates = {
|
||||
CNY: 1,
|
||||
@@ -187,6 +203,37 @@ const PurchaseRequestsPage: React.FC = () => {
|
||||
setAttachments([])
|
||||
setCurrentEditingStatus('')
|
||||
setModalVisible(true)
|
||||
// 检查是否有草稿,提示用户是否恢复
|
||||
setTimeout(() => {
|
||||
if (hasDraft()) {
|
||||
Modal.confirm({
|
||||
title: '发现未完成的草稿',
|
||||
content: '检测到上次未提交的采购申请,是否恢复?',
|
||||
okText: '恢复草稿',
|
||||
cancelText: '重新填写',
|
||||
onOk: () => {
|
||||
restoreDraft()
|
||||
},
|
||||
onCancel: () => {
|
||||
clearDraft()
|
||||
form.resetFields()
|
||||
form.setFieldsValue({
|
||||
purchase_type: 'inventory',
|
||||
request_date: dayjs(),
|
||||
expected_date: dayjs().add(7, 'day'),
|
||||
currency: 'CNY',
|
||||
expense_category: 'material',
|
||||
applicant: '系统管理员',
|
||||
total_amount: 0,
|
||||
attachments: []
|
||||
})
|
||||
setAttachments([])
|
||||
setPurchaseType('inventory')
|
||||
setCurrency('CNY')
|
||||
},
|
||||
})
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
|
||||
const handleEdit = async (record: PurchaseRequest) => {
|
||||
@@ -296,6 +343,7 @@ const PurchaseRequestsPage: React.FC = () => {
|
||||
|
||||
if (data.success) {
|
||||
message.success(editingRequest ? '保存成功' : '创建成功')
|
||||
clearDraft()
|
||||
if (!editingRequest) {
|
||||
setEditingRequest(data.data)
|
||||
}
|
||||
@@ -359,6 +407,7 @@ const PurchaseRequestsPage: React.FC = () => {
|
||||
|
||||
if (submitData.success) {
|
||||
message.success(editingRequest ? '提交成功' : '创建并提交成功')
|
||||
clearDraft()
|
||||
setModalVisible(false)
|
||||
setSelectedStatus(null)
|
||||
fetchPurchaseRequests()
|
||||
@@ -672,15 +721,46 @@ const PurchaseRequestsPage: React.FC = () => {
|
||||
<Modal
|
||||
title={editingRequest ? '编辑采购申请' : '新建采购申请'}
|
||||
open={modalVisible}
|
||||
onCancel={() => setModalVisible(false)}
|
||||
onCancel={() => {
|
||||
if (form.isFieldsTouched()) {
|
||||
Modal.confirm({
|
||||
title: '确认关闭',
|
||||
content: '表单数据尚未保存,关闭后可通过草稿恢复。确定关闭吗?',
|
||||
okText: '关闭',
|
||||
cancelText: '继续编辑',
|
||||
onOk: () => {
|
||||
saveDraft({ attachments, purchaseType, currency })
|
||||
setModalVisible(false)
|
||||
},
|
||||
})
|
||||
} else {
|
||||
setModalVisible(false)
|
||||
}
|
||||
}}
|
||||
footer={[
|
||||
<Button key="cancel" onClick={() => setModalVisible(false)}>取消</Button>,
|
||||
<Button key="cancel" onClick={() => {
|
||||
if (form.isFieldsTouched()) {
|
||||
Modal.confirm({
|
||||
title: '确认关闭',
|
||||
content: '表单数据尚未保存,关闭后可通过草稿恢复。确定关闭吗?',
|
||||
okText: '关闭',
|
||||
cancelText: '继续编辑',
|
||||
onOk: () => {
|
||||
saveDraft({ attachments, purchaseType, currency })
|
||||
setModalVisible(false)
|
||||
},
|
||||
})
|
||||
} else {
|
||||
setModalVisible(false)
|
||||
}
|
||||
}}>取消</Button>,
|
||||
<Button key="save" onClick={handleSave}>保存</Button>,
|
||||
<Button key="submit" type="primary" onClick={handleFormSubmit}>提交审批</Button>
|
||||
]}
|
||||
width={700}
|
||||
maskClosable={false}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form form={form} layout="vertical" onValuesChange={handleFormChange}>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Table, Button, Modal, Form, Input, Select, message, Space, Tag, Card, Row, Col, Statistic } from 'antd'
|
||||
import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, SolutionOutlined, BankOutlined } from '@ant-design/icons'
|
||||
import type { ColumnsType } from 'antd/es/table'
|
||||
import FileUpload from '../components/FileUpload'
|
||||
import useFormDraft from '../hooks/useFormDraft'
|
||||
|
||||
interface Contact {
|
||||
name: string
|
||||
@@ -45,6 +46,16 @@ const SubcontractorPage: React.FC = () => {
|
||||
const [searchText, setSearchText] = useState('')
|
||||
const [form] = Form.useForm()
|
||||
|
||||
// 表单草稿保护
|
||||
const { save: saveDraft, restore: restoreDraft, clear: clearDraft, hasDraft } = useFormDraft({
|
||||
form,
|
||||
storageKey: 'subcontractor_create',
|
||||
})
|
||||
|
||||
const handleFormChange = useCallback(() => {
|
||||
saveDraft()
|
||||
}, [saveDraft])
|
||||
|
||||
const fetchSubcontractors = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
@@ -84,23 +95,6 @@ const SubcontractorPage: React.FC = () => {
|
||||
)
|
||||
},
|
||||
{ title: '承包范围', dataIndex: 'scope', key: 'scope', width: 120 },
|
||||
{ title: '主联系人', key: 'primary_contact', width: 100, render: (_, record) => getPrimaryContact(record.contacts || []) },
|
||||
{
|
||||
title: '收款信息',
|
||||
key: 'payment_info',
|
||||
width: 200,
|
||||
render: (_, record) => {
|
||||
const primary = getPrimaryPaymentInfo(record.payment_infos || [])
|
||||
if (!primary) return <Tag>未设置</Tag>
|
||||
return (
|
||||
<div style={{ fontSize: 12 }}>
|
||||
<div><BankOutlined /> {primary.bank_name || '-'}</div>
|
||||
<div>户名: {primary.account_name || '-'}</div>
|
||||
<div>账号: {primary.bank_account ? primary.bank_account.slice(-4).padStart(primary.bank_account.length, '*') : '-'}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
{ title: '国家', dataIndex: 'country', key: 'country', width: 80, render: (c) => <Tag>{c || '-'}</Tag> },
|
||||
{ title: '合同金额', dataIndex: 'total_contract_amount', key: 'total_contract_amount', width: 100, render: (v) => `¥${(v || 0).toLocaleString()}` },
|
||||
{ title: '应付金额', dataIndex: 'total_payable', key: 'total_payable', width: 100, render: (v) => <span style={{ color: v > 0 ? '#ff4d4f' : '#52c41a' }}>¥{(v || 0).toLocaleString()}</span> },
|
||||
@@ -163,6 +157,7 @@ const SubcontractorPage: React.FC = () => {
|
||||
const data = await response.json()
|
||||
if (data.success) {
|
||||
message.success(editingSubcontractor ? '更新成功' : '创建成功')
|
||||
clearDraft()
|
||||
setModalVisible(false)
|
||||
form.resetFields()
|
||||
setEditingSubcontractor(null)
|
||||
@@ -212,6 +207,29 @@ const SubcontractorPage: React.FC = () => {
|
||||
payment_infos: []
|
||||
})
|
||||
setModalVisible(true)
|
||||
// 检查是否有草稿,提示用户是否恢复
|
||||
setTimeout(() => {
|
||||
if (hasDraft()) {
|
||||
Modal.confirm({
|
||||
title: '发现未完成的草稿',
|
||||
content: '检测到上次未提交的分包商信息,是否恢复?',
|
||||
okText: '恢复草稿',
|
||||
cancelText: '重新填写',
|
||||
onOk: () => {
|
||||
restoreDraft()
|
||||
},
|
||||
onCancel: () => {
|
||||
clearDraft()
|
||||
form.resetFields()
|
||||
form.setFieldsValue({
|
||||
country: 'Laos',
|
||||
contacts: [{ name: '', position: '', phone: '', is_primary: true }],
|
||||
payment_infos: []
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -236,11 +254,31 @@ const SubcontractorPage: React.FC = () => {
|
||||
<Modal
|
||||
title={editingSubcontractor ? '编辑分包商' : '新增分包商'}
|
||||
open={modalVisible}
|
||||
onCancel={() => { setModalVisible(false); form.resetFields(); setEditingSubcontractor(null) }}
|
||||
onCancel={() => {
|
||||
if (form.isFieldsTouched()) {
|
||||
Modal.confirm({
|
||||
title: '确认关闭',
|
||||
content: '表单数据尚未保存,关闭后可通过草稿恢复。确定关闭吗?',
|
||||
okText: '关闭',
|
||||
cancelText: '继续编辑',
|
||||
onOk: () => {
|
||||
saveDraft()
|
||||
setModalVisible(false)
|
||||
form.resetFields()
|
||||
setEditingSubcontractor(null)
|
||||
},
|
||||
})
|
||||
} else {
|
||||
setModalVisible(false)
|
||||
form.resetFields()
|
||||
setEditingSubcontractor(null)
|
||||
}
|
||||
}}
|
||||
onOk={() => form.submit()}
|
||||
width={800}
|
||||
maskClosable={false}
|
||||
>
|
||||
<Form form={form} layout="vertical" onFinish={handleSubmit}>
|
||||
<Form form={form} layout="vertical" onFinish={handleSubmit} onValuesChange={handleFormChange}>
|
||||
<Form.Item name="name" label="名称" rules={[{ required: true, message: '请输入名称' }]}>
|
||||
<Input placeholder="分包商名称" />
|
||||
</Form.Item>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { useNavigate, useLocation } from 'react-router-dom'
|
||||
import { Table, Button, Modal, Form, Input, Select, message, Space, Tag, Card, Row, Col, Statistic } from 'antd'
|
||||
import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, ShopOutlined, BankOutlined } from '@ant-design/icons'
|
||||
import type { ColumnsType } from 'antd/es/table'
|
||||
import FileUpload from '../components/FileUpload'
|
||||
import useFormDraft from '../hooks/useFormDraft'
|
||||
|
||||
interface Contact {
|
||||
name: string
|
||||
@@ -48,6 +49,16 @@ const SupplierPage: React.FC = () => {
|
||||
// 从 location state 中获取返回路径
|
||||
const returnTo = (location.state as { returnTo?: string })?.returnTo
|
||||
|
||||
// 表单草稿保护
|
||||
const { save: saveDraft, restore: restoreDraft, clear: clearDraft, hasDraft } = useFormDraft({
|
||||
form,
|
||||
storageKey: 'supplier_create',
|
||||
})
|
||||
|
||||
const handleFormChange = useCallback(() => {
|
||||
saveDraft()
|
||||
}, [saveDraft])
|
||||
|
||||
const fetchSuppliers = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
@@ -91,23 +102,6 @@ const SupplierPage: React.FC = () => {
|
||||
)
|
||||
},
|
||||
{ title: '供应类别', dataIndex: 'supply_category', key: 'supply_category', width: 120 },
|
||||
{ title: '主联系人', key: 'primary_contact', width: 100, render: (_, record) => getPrimaryContact(record.contacts || []) },
|
||||
{
|
||||
title: '收款信息',
|
||||
key: 'payment_info',
|
||||
width: 200,
|
||||
render: (_, record) => {
|
||||
const primary = getPrimaryPaymentInfo(record.payment_infos || [])
|
||||
if (!primary) return <Tag>未设置</Tag>
|
||||
return (
|
||||
<div style={{ fontSize: 12 }}>
|
||||
<div><BankOutlined /> {primary.bank_name || '-'}</div>
|
||||
<div>户名: {primary.account_name || '-'}</div>
|
||||
<div>账号: {primary.bank_account ? primary.bank_account.slice(-4).padStart(primary.bank_account.length, '*') : '-'}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
{ title: '国家', dataIndex: 'country', key: 'country', width: 80, render: (country) => <Tag>{country || '-'}</Tag> },
|
||||
{ title: '采购金额', dataIndex: 'total_purchase_amount', key: 'total_purchase_amount', width: 100, render: (amount) => `¥${(amount || 0).toLocaleString()}` },
|
||||
{ title: '应付金额', dataIndex: 'total_payable', key: 'total_payable', width: 100, render: (amount) => <span style={{ color: amount > 0 ? '#ff4d4f' : '#52c41a' }}>¥{(amount || 0).toLocaleString()}</span> },
|
||||
@@ -176,6 +170,7 @@ const SupplierPage: React.FC = () => {
|
||||
const data = await response.json()
|
||||
if (data.success) {
|
||||
message.success(editingSupplier ? '更新成功' : '创建成功')
|
||||
clearDraft()
|
||||
setModalVisible(false)
|
||||
form.resetFields()
|
||||
setEditingSupplier(null)
|
||||
@@ -234,6 +229,29 @@ const SupplierPage: React.FC = () => {
|
||||
payment_infos: []
|
||||
})
|
||||
setModalVisible(true)
|
||||
// 检查是否有草稿,提示用户是否恢复
|
||||
setTimeout(() => {
|
||||
if (hasDraft()) {
|
||||
Modal.confirm({
|
||||
title: '发现未完成的草稿',
|
||||
content: '检测到上次未提交的供应商信息,是否恢复?',
|
||||
okText: '恢复草稿',
|
||||
cancelText: '重新填写',
|
||||
onOk: () => {
|
||||
restoreDraft()
|
||||
},
|
||||
onCancel: () => {
|
||||
clearDraft()
|
||||
form.resetFields()
|
||||
form.setFieldsValue({
|
||||
country: 'Laos',
|
||||
contacts: [{ name: '', position: '', phone: '', is_primary: true }],
|
||||
payment_infos: []
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
|
||||
// 如果是从采购申请页面跳转过来的,自动打开新增供应商弹窗
|
||||
@@ -265,11 +283,31 @@ const SupplierPage: React.FC = () => {
|
||||
<Modal
|
||||
title={editingSupplier ? '编辑供应商' : '新增供应商'}
|
||||
open={modalVisible}
|
||||
onCancel={() => { setModalVisible(false); form.resetFields(); setEditingSupplier(null) }}
|
||||
onCancel={() => {
|
||||
if (form.isFieldsTouched()) {
|
||||
Modal.confirm({
|
||||
title: '确认关闭',
|
||||
content: '表单数据尚未保存,关闭后可通过草稿恢复。确定关闭吗?',
|
||||
okText: '关闭',
|
||||
cancelText: '继续编辑',
|
||||
onOk: () => {
|
||||
saveDraft()
|
||||
setModalVisible(false)
|
||||
form.resetFields()
|
||||
setEditingSupplier(null)
|
||||
},
|
||||
})
|
||||
} else {
|
||||
setModalVisible(false)
|
||||
form.resetFields()
|
||||
setEditingSupplier(null)
|
||||
}
|
||||
}}
|
||||
onOk={() => form.submit()}
|
||||
width={800}
|
||||
maskClosable={false}
|
||||
>
|
||||
<Form form={form} layout="vertical" onFinish={handleSubmit}>
|
||||
<Form form={form} layout="vertical" onFinish={handleSubmit} onValuesChange={handleFormChange}>
|
||||
<Form.Item name="name" label="名称" rules={[{ required: true, message: '请输入名称' }]}>
|
||||
<Input placeholder="供应商名称" />
|
||||
</Form.Item>
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
const TestAPI: React.FC = () => {
|
||||
const [users, setUsers] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchUsers = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/users');
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
setUsers(data.data || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取用户列表失败:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchUsers();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div style={{ padding: 24 }}>
|
||||
<h1>测试API页面</h1>
|
||||
{loading ? (
|
||||
<p>加载中...</p>
|
||||
) : (
|
||||
<div>
|
||||
<h2>用户列表</h2>
|
||||
<pre>{JSON.stringify(users, null, 2)}</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TestAPI;
|
||||
@@ -1,31 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
const TestPage2: React.FC = () => {
|
||||
console.log('TestPage2组件被渲染了');
|
||||
|
||||
useEffect(() => {
|
||||
console.log('TestPage2组件挂载了');
|
||||
// 测试API调用
|
||||
const testApi = async () => {
|
||||
try {
|
||||
console.log('开始测试API调用...');
|
||||
const response = await fetch('/api/users');
|
||||
console.log('响应状态:', response.status);
|
||||
const data = await response.json();
|
||||
console.log('API返回数据:', data);
|
||||
} catch (error) {
|
||||
console.error('API调用失败:', error);
|
||||
}
|
||||
};
|
||||
testApi();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div style={{ padding: 24 }}>
|
||||
<h1>测试页面</h1>
|
||||
<p>这是一个测试页面,用于检查console.log是否正常工作。</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TestPage2;
|
||||
@@ -2,24 +2,18 @@ import React, { useState, useEffect } from 'react';
|
||||
import apiClient from '../utils/request';
|
||||
|
||||
const UserManagement: React.FC = () => {
|
||||
console.log('UserManagement组件被渲染');
|
||||
const [users, setUsers] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// 从API获取用户数据
|
||||
const fetchUsers = async () => {
|
||||
console.log('开始获取用户列表...');
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
console.log('发起API请求...');
|
||||
const response = await apiClient.get('/api/users');
|
||||
console.log('响应状态:', response.status);
|
||||
console.log('API返回数据:', response.data);
|
||||
if (response.data.success) {
|
||||
setUsers(response.data.data || []);
|
||||
console.log('用户列表更新成功:', response.data.data || []);
|
||||
} else {
|
||||
throw new Error('API返回失败: ' + (response.data.message || '未知错误'));
|
||||
}
|
||||
@@ -32,7 +26,6 @@ const UserManagement: React.FC = () => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
console.log('组件挂载,开始获取用户列表...');
|
||||
fetchUsers();
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Table, Button, Card, Space, Tag, Modal, Form, Input, InputNumber, Select, DatePicker, message, Descriptions, Image, Divider, Tabs } from 'antd';
|
||||
import { PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined, UndoOutlined } from '@ant-design/icons';
|
||||
import dayjs from 'dayjs';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
import FileUpload from '../../components/FileUpload';
|
||||
import useFormDraft from '../../hooks/useFormDraft';
|
||||
|
||||
const { Option } = Select;
|
||||
const { TextArea } = Input;
|
||||
@@ -24,6 +25,16 @@ const AdvancesPage: React.FC = () => {
|
||||
const [currentEditingStatus, setCurrentEditingStatus] = useState<string>('');
|
||||
const [exchangeRates, setExchangeRates] = useState<Record<string, number>>({});
|
||||
|
||||
// 表单草稿保护
|
||||
const { save: saveDraft, restore: restoreDraft, clear: clearDraft, hasDraft } = useFormDraft({
|
||||
form,
|
||||
storageKey: 'advance_create',
|
||||
});
|
||||
|
||||
const handleFormChange = useCallback(() => {
|
||||
saveDraft();
|
||||
}, [saveDraft]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchAdvances();
|
||||
fetchProjects();
|
||||
@@ -81,6 +92,29 @@ const AdvancesPage: React.FC = () => {
|
||||
attachments: []
|
||||
});
|
||||
setModalVisible(true);
|
||||
setTimeout(() => {
|
||||
if (hasDraft()) {
|
||||
Modal.confirm({
|
||||
title: '发现未完成的草稿',
|
||||
content: '检测到上次未提交的预支申请,是否恢复?',
|
||||
okText: '恢复草稿',
|
||||
cancelText: '重新填写',
|
||||
onOk: () => {
|
||||
restoreDraft();
|
||||
},
|
||||
onCancel: () => {
|
||||
clearDraft();
|
||||
form.resetFields();
|
||||
form.setFieldsValue({
|
||||
advance_date: dayjs(),
|
||||
currency: 'CNY',
|
||||
applicant: user?.name || user?.username || '当前用户',
|
||||
attachments: []
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const handleEdit = (record: any) => {
|
||||
@@ -162,8 +196,6 @@ const AdvancesPage: React.FC = () => {
|
||||
const values = await form.validateFields();
|
||||
// 保存时使用编辑时的状态
|
||||
const saveStatus = currentEditingStatus || 'pending_edit';
|
||||
console.log('保存操作 - 状态:', saveStatus);
|
||||
console.log('currentEditingStatus:', currentEditingStatus);
|
||||
const data = {
|
||||
...values,
|
||||
advance_date: values.advance_date?.format('YYYY-MM-DD'),
|
||||
@@ -171,7 +203,6 @@ const AdvancesPage: React.FC = () => {
|
||||
applicant: user?.name || user?.username,
|
||||
status: saveStatus
|
||||
};
|
||||
console.log('保存操作 - 提交的数据:', data);
|
||||
const url = editingId ? '/api/advances/' + editingId : '/api/advances';
|
||||
const method = editingId ? 'PUT' : 'POST';
|
||||
const res = await fetch(url, {
|
||||
@@ -180,9 +211,9 @@ const AdvancesPage: React.FC = () => {
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
const result = await res.json();
|
||||
console.log('保存操作 - 响应:', result);
|
||||
if (result.success) {
|
||||
message.success(editingId ? '保存成功' : '创建成功');
|
||||
clearDraft();
|
||||
setModalVisible(false);
|
||||
fetchAdvances();
|
||||
} else {
|
||||
@@ -200,7 +231,6 @@ const AdvancesPage: React.FC = () => {
|
||||
const values = await form.validateFields();
|
||||
// 提交时使用pending状态
|
||||
const saveStatus = 'pending';
|
||||
console.log('提交操作 - 状态:', saveStatus);
|
||||
const data = {
|
||||
...values,
|
||||
advance_date: values.advance_date?.format('YYYY-MM-DD'),
|
||||
@@ -208,7 +238,6 @@ const AdvancesPage: React.FC = () => {
|
||||
applicant: user?.name || user?.username,
|
||||
status: saveStatus
|
||||
};
|
||||
console.log('提交操作 - 提交的数据:', data);
|
||||
const url = editingId ? '/api/advances/' + editingId : '/api/advances';
|
||||
const method = editingId ? 'PUT' : 'POST';
|
||||
const res = await fetch(url, {
|
||||
@@ -217,9 +246,9 @@ const AdvancesPage: React.FC = () => {
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
const result = await res.json();
|
||||
console.log('提交操作 - 响应:', result);
|
||||
if (result.success) {
|
||||
message.success(editingId ? '提交成功' : '创建成功');
|
||||
clearDraft();
|
||||
setModalVisible(false);
|
||||
fetchAdvances();
|
||||
} else {
|
||||
@@ -334,15 +363,46 @@ const AdvancesPage: React.FC = () => {
|
||||
<Modal
|
||||
title={editingId ? '编辑预支' : '新建预支'}
|
||||
open={modalVisible}
|
||||
onCancel={() => setModalVisible(false)}
|
||||
onCancel={() => {
|
||||
if (form.isFieldsTouched()) {
|
||||
Modal.confirm({
|
||||
title: '确认关闭',
|
||||
content: '表单数据尚未保存,关闭后可通过草稿恢复。确定关闭吗?',
|
||||
okText: '关闭',
|
||||
cancelText: '继续编辑',
|
||||
onOk: () => {
|
||||
saveDraft();
|
||||
setModalVisible(false);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
setModalVisible(false);
|
||||
}
|
||||
}}
|
||||
footer={[
|
||||
<Button key="cancel" onClick={() => setModalVisible(false)}>取消</Button>,
|
||||
<Button key="cancel" onClick={() => {
|
||||
if (form.isFieldsTouched()) {
|
||||
Modal.confirm({
|
||||
title: '确认关闭',
|
||||
content: '表单数据尚未保存,关闭后可通过草稿恢复。确定关闭吗?',
|
||||
okText: '关闭',
|
||||
cancelText: '继续编辑',
|
||||
onOk: () => {
|
||||
saveDraft();
|
||||
setModalVisible(false);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
setModalVisible(false);
|
||||
}
|
||||
}}>取消</Button>,
|
||||
<Button key="save" onClick={handleSave}>保存</Button>,
|
||||
<Button key="submit" type="primary" onClick={handleSubmitAndSubmit}>提交</Button>
|
||||
]}
|
||||
maskClosable={false}
|
||||
width={700}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form form={form} layout="vertical" onValuesChange={handleFormChange}>
|
||||
<Form.Item name="applicant" label="申请人">
|
||||
<Input disabled style={{ color: 'rgba(0,0,0,0.85)', backgroundColor: '#f5f5f5' }} />
|
||||
</Form.Item>
|
||||
|
||||
@@ -59,7 +59,6 @@ const ApprovalManagement: React.FC = () => {
|
||||
const fetchApprovalHistory = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
console.log('开始获取审批历史记录');
|
||||
// 获取所有类型的申请记录
|
||||
const types = ['advances', 'reimbursements', 'payment-requests', 'verifications', 'purchase-requests'];
|
||||
const historyData = [];
|
||||
@@ -150,7 +149,6 @@ const ApprovalManagement: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
console.log('审批历史记录:', historyData);
|
||||
setApprovalHistory(historyData);
|
||||
} catch (error) {
|
||||
console.error('获取审批历史记录失败:', error);
|
||||
@@ -179,45 +177,32 @@ const ApprovalManagement: React.FC = () => {
|
||||
const fetchPendingData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
console.log('开始获取待审批数据');
|
||||
// 获取预支申请
|
||||
const advancesRes = await fetch('/api/advances');
|
||||
console.log('Advances response status:', advancesRes.status);
|
||||
const advancesData = await advancesRes.json();
|
||||
console.log('Advances data:', advancesData);
|
||||
|
||||
// 获取报销申请
|
||||
const reimbursementsRes = await fetch('/api/reimbursements');
|
||||
console.log('Reimbursements response status:', reimbursementsRes.status);
|
||||
const reimbursementsData = await reimbursementsRes.json();
|
||||
console.log('Reimbursements data:', reimbursementsData);
|
||||
|
||||
// 获取付款申请
|
||||
const paymentsRes = await fetch('/api/payment-requests');
|
||||
console.log('Payments response status:', paymentsRes.status);
|
||||
const paymentsData = await paymentsRes.json();
|
||||
console.log('Payments data:', paymentsData);
|
||||
|
||||
// 获取核销申请
|
||||
const verificationsRes = await fetch('/api/verifications');
|
||||
console.log('Verifications response status:', verificationsRes.status);
|
||||
const verificationsData = await verificationsRes.json();
|
||||
console.log('Verifications data:', verificationsData);
|
||||
|
||||
// 获取采购申请
|
||||
const purchaseRes = await fetch('/api/purchase-requests');
|
||||
console.log('Purchase requests response status:', purchaseRes.status);
|
||||
const purchaseData = await purchaseRes.json();
|
||||
console.log('Purchase requests data:', purchaseData);
|
||||
|
||||
// 合并数据
|
||||
const allPendingData = [];
|
||||
|
||||
// 添加预支申请
|
||||
if (advancesData.success && advancesData.data) {
|
||||
console.log('Advances data length:', advancesData.data.length);
|
||||
advancesData.data.forEach((item: any) => {
|
||||
console.log('Advance item:', item);
|
||||
if (item.status === 'pending') {
|
||||
allPendingData.push({
|
||||
key: `adv-${item.id}`,
|
||||
@@ -238,9 +223,7 @@ const ApprovalManagement: React.FC = () => {
|
||||
|
||||
// 添加报销申请
|
||||
if (reimbursementsData.success && reimbursementsData.data) {
|
||||
console.log('Reimbursements data length:', reimbursementsData.data.length);
|
||||
reimbursementsData.data.forEach((item: any) => {
|
||||
console.log('Reimbursement item:', item);
|
||||
if (item.status === 'pending') {
|
||||
allPendingData.push({
|
||||
key: `reimb-${item.id}`,
|
||||
@@ -261,9 +244,7 @@ const ApprovalManagement: React.FC = () => {
|
||||
|
||||
// 添加付款申请
|
||||
if (paymentsData.success && paymentsData.data) {
|
||||
console.log('Payments data length:', paymentsData.data.length);
|
||||
paymentsData.data.forEach((item: any) => {
|
||||
console.log('Payment item:', item);
|
||||
if (item.status === 'pending') {
|
||||
allPendingData.push({
|
||||
key: `pay-${item.id}`,
|
||||
@@ -284,9 +265,7 @@ const ApprovalManagement: React.FC = () => {
|
||||
|
||||
// 添加核销申请
|
||||
if (verificationsData.success && verificationsData.data) {
|
||||
console.log('Verifications data length:', verificationsData.data.length);
|
||||
verificationsData.data.forEach((item: any) => {
|
||||
console.log('Verification item:', item);
|
||||
if (item.status === 'pending') {
|
||||
allPendingData.push({
|
||||
key: `ver-${item.id}`,
|
||||
@@ -307,9 +286,7 @@ const ApprovalManagement: React.FC = () => {
|
||||
|
||||
// 添加采购申请
|
||||
if (purchaseData.success && purchaseData.data) {
|
||||
console.log('Purchase requests data length:', purchaseData.data.length);
|
||||
purchaseData.data.forEach((item: any) => {
|
||||
console.log('Purchase request item:', item);
|
||||
if (item.status === 'pending') {
|
||||
allPendingData.push({
|
||||
key: `pur-${item.id}`,
|
||||
@@ -328,7 +305,6 @@ const ApprovalManagement: React.FC = () => {
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Final pending data:', allPendingData);
|
||||
setPendingData(allPendingData);
|
||||
} catch (error) {
|
||||
console.error('获取待审批数据失败:', error);
|
||||
|
||||
@@ -296,8 +296,6 @@ const ExecutionManagement: React.FC = () => {
|
||||
.filter(url => url); // 过滤掉空值
|
||||
}
|
||||
|
||||
console.log('上传的凭证文件:', voucherFiles);
|
||||
console.log('凭证文件URL列表:', voucherFileUrls);
|
||||
|
||||
// 调用执行API
|
||||
const executeResponse = await fetch('/api/executions', {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, Typography, Button, Form, Input, Select, DatePicker, InputNumber, Radio, Space, message, Divider, Row, Col } from 'antd';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Card, Typography, Button, Form, Input, Select, DatePicker, InputNumber, Radio, Space, message, Divider, Row, Col, Modal } from 'antd';
|
||||
import { SaveOutlined, ArrowLeftOutlined } from '@ant-design/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
import apiClient from '../../utils/request';
|
||||
import dayjs from 'dayjs';
|
||||
import FileUpload from '../../components/FileUpload';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
import useFormDraft from '../../hooks/useFormDraft';
|
||||
|
||||
const { Title, Paragraph } = Typography;
|
||||
const { Option } = Select;
|
||||
@@ -31,9 +32,71 @@ const BudgetProjectCreate: React.FC = () => {
|
||||
const [attachments, setAttachments] = useState<string[]>([]);
|
||||
const [surveyPhotos, setSurveyPhotos] = useState<string[]>([]);
|
||||
const navigate = useNavigate();
|
||||
|
||||
|
||||
const { user: currentUser } = useAuthStore();
|
||||
const isAdmin = currentUser?.role === 'admin';
|
||||
|
||||
// 表单草稿保护
|
||||
const { save: saveDraft, restore: restoreDraft, clear: clearDraft, hasDraft } = useFormDraft({
|
||||
form,
|
||||
storageKey: 'budget_project_create',
|
||||
onRestore: (data) => {
|
||||
if (data.attachments) setAttachments(data.attachments);
|
||||
if (data.surveyPhotos) setSurveyPhotos(data.surveyPhotos);
|
||||
},
|
||||
});
|
||||
|
||||
// 保存草稿(包含外部状态)
|
||||
const handleFormChange = useCallback(() => {
|
||||
saveDraft({ attachments, surveyPhotos });
|
||||
}, [saveDraft, attachments, surveyPhotos]);
|
||||
|
||||
// 浏览器刷新/关闭拦截
|
||||
useEffect(() => {
|
||||
const handler = (e: BeforeUnloadEvent) => {
|
||||
if (form.isFieldsTouched()) {
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
window.addEventListener('beforeunload', handler);
|
||||
return () => window.removeEventListener('beforeunload', handler);
|
||||
}, [form]);
|
||||
|
||||
// 移动端:页面切到后台时保存草稿
|
||||
useEffect(() => {
|
||||
const handler = () => {
|
||||
if (document.visibilityState === 'hidden' && form.isFieldsTouched()) {
|
||||
saveDraft({ attachments, surveyPhotos });
|
||||
}
|
||||
};
|
||||
document.addEventListener('visibilitychange', handler);
|
||||
const pageHideHandler = () => {
|
||||
if (form.isFieldsTouched()) saveDraft({ attachments, surveyPhotos });
|
||||
};
|
||||
window.addEventListener('pagehide', pageHideHandler);
|
||||
return () => {
|
||||
document.removeEventListener('visibilitychange', handler);
|
||||
window.removeEventListener('pagehide', pageHideHandler);
|
||||
};
|
||||
}, [form, saveDraft, attachments, surveyPhotos]);
|
||||
|
||||
// 页面加载时检查草稿
|
||||
useEffect(() => {
|
||||
if (hasDraft()) {
|
||||
Modal.confirm({
|
||||
title: '发现未完成的草稿',
|
||||
content: '检测到上次未提交的商谈项目,是否恢复?',
|
||||
okText: '恢复草稿',
|
||||
cancelText: '重新填写',
|
||||
onOk: () => {
|
||||
restoreDraft();
|
||||
},
|
||||
onCancel: () => {
|
||||
clearDraft();
|
||||
},
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 检查权限,如果不是管理员,重定向到列表页面
|
||||
useEffect(() => {
|
||||
@@ -62,7 +125,7 @@ const BudgetProjectCreate: React.FC = () => {
|
||||
|
||||
const fetchCustomers = async () => {
|
||||
try {
|
||||
const res = await axios.get('/api/customers');
|
||||
const res = await apiClient.get('/customers');
|
||||
if (res.data.success) setCustomers(res.data.data);
|
||||
} catch (error) {
|
||||
console.error('获取客户列表失败:', error);
|
||||
@@ -71,7 +134,7 @@ const BudgetProjectCreate: React.FC = () => {
|
||||
|
||||
const fetchUsers = async () => {
|
||||
try {
|
||||
const res = await axios.get('/api/users');
|
||||
const res = await apiClient.get('/users');
|
||||
if (res.data.success) setUsers(res.data.data);
|
||||
} catch (error) {
|
||||
console.error('获取用户列表失败:', error);
|
||||
@@ -91,13 +154,14 @@ const BudgetProjectCreate: React.FC = () => {
|
||||
status: 'negotiating',
|
||||
};
|
||||
|
||||
const res = await axios.post('/api/budget-projects', projectData, {
|
||||
const res = await apiClient.post('/budget-projects', projectData, {
|
||||
headers: {
|
||||
'x-user-role': 'admin' // 创建预算项目需要管理员权限
|
||||
'x-user-role': 'admin'
|
||||
}
|
||||
});
|
||||
if (res.data.success) {
|
||||
message.success('创建成功');
|
||||
clearDraft();
|
||||
navigate('/budget-projects');
|
||||
}
|
||||
} catch (error: any) {
|
||||
@@ -117,7 +181,22 @@ const BudgetProjectCreate: React.FC = () => {
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 16, marginBottom: 8 }}>
|
||||
<Button
|
||||
icon={<ArrowLeftOutlined />}
|
||||
onClick={() => navigate('/budget-projects')}
|
||||
onClick={() => {
|
||||
if (form.isFieldsTouched()) {
|
||||
Modal.confirm({
|
||||
title: '确认离开',
|
||||
content: '表单数据尚未保存,离开后可通过草稿恢复。确定离开吗?',
|
||||
okText: '离开',
|
||||
cancelText: '继续编辑',
|
||||
onOk: () => {
|
||||
saveDraft({ attachments, surveyPhotos });
|
||||
navigate('/budget-projects');
|
||||
},
|
||||
});
|
||||
} else {
|
||||
navigate('/budget-projects');
|
||||
}
|
||||
}}
|
||||
>
|
||||
返回
|
||||
</Button>
|
||||
@@ -127,10 +206,11 @@ const BudgetProjectCreate: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<Form
|
||||
form={form}
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
onValuesChange={handleFormChange}
|
||||
initialValues={{
|
||||
intermediary_fee_type: 'fixed',
|
||||
survey_date: dayjs(), // 勘察日期默认为当天
|
||||
attachments: [],
|
||||
@@ -256,7 +336,7 @@ const BudgetProjectCreate: React.FC = () => {
|
||||
<Form.Item label="附件上传">
|
||||
<FileUpload
|
||||
value={attachments}
|
||||
onChange={setAttachments}
|
||||
onChange={(urls) => { setAttachments(urls); saveDraft({ attachments: urls, surveyPhotos }); }}
|
||||
accept=".pdf,.doc,.docx,.jpg,.jpeg,.png,.xlsx,.xls"
|
||||
/>
|
||||
</Form.Item>
|
||||
@@ -265,7 +345,7 @@ const BudgetProjectCreate: React.FC = () => {
|
||||
<Form.Item label="勘察照片">
|
||||
<FileUpload
|
||||
value={surveyPhotos}
|
||||
onChange={setSurveyPhotos}
|
||||
onChange={(urls) => { setSurveyPhotos(urls); saveDraft({ attachments, surveyPhotos: urls }); }}
|
||||
accept="image/*"
|
||||
/>
|
||||
</Form.Item>
|
||||
@@ -275,7 +355,22 @@ const BudgetProjectCreate: React.FC = () => {
|
||||
{/* 提交按钮 */}
|
||||
<div style={{ marginTop: 24, textAlign: 'right' }}>
|
||||
<Space>
|
||||
<Button onClick={() => navigate('/budget-projects')}>取消</Button>
|
||||
<Button onClick={() => {
|
||||
if (form.isFieldsTouched()) {
|
||||
Modal.confirm({
|
||||
title: '确认离开',
|
||||
content: '表单数据尚未保存,离开后可通过草稿恢复。确定离开吗?',
|
||||
okText: '离开',
|
||||
cancelText: '继续编辑',
|
||||
onOk: () => {
|
||||
saveDraft({ attachments, surveyPhotos });
|
||||
navigate('/budget-projects');
|
||||
},
|
||||
});
|
||||
} else {
|
||||
navigate('/budget-projects');
|
||||
}
|
||||
}}>取消</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SaveOutlined />}
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
|
||||
import { Card, Typography, Button, Space, Tag, message, Empty, Divider, Row, Col, List, Descriptions, Avatar, Badge, Modal, Input } from 'antd';
|
||||
import { ArrowLeftOutlined, EyeOutlined, FileAddOutlined, FileOutlined, CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
import apiClient from '../../utils/request';
|
||||
import dayjs from 'dayjs';
|
||||
import QuotationCreateModal from './QuotationCreateModal';
|
||||
import ContractCreateModal from './ContractCreateModal';
|
||||
@@ -78,7 +78,7 @@ const BudgetProjectDetail: React.FC = () => {
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await axios.get(`/api/budget-projects/${id}`);
|
||||
const res = await apiClient.get(`/budget-projects/${id}`);
|
||||
if (res.data.success) {
|
||||
const projectData = res.data.data;
|
||||
// 后端已经解析了数据,直接使用
|
||||
@@ -136,7 +136,7 @@ const BudgetProjectDetail: React.FC = () => {
|
||||
if (!project) return;
|
||||
|
||||
try {
|
||||
const res = await axios.put(`/api/budget-projects/${project.id}/unsigned`, {}, {
|
||||
const res = await apiClient.put(`/budget-projects/${project.id}/unsigned`, {}, {
|
||||
headers: {
|
||||
'x-user-role': currentUser?.role || 'employee'
|
||||
}
|
||||
@@ -167,7 +167,7 @@ const BudgetProjectDetail: React.FC = () => {
|
||||
|
||||
setDeleteLoading(true);
|
||||
try {
|
||||
const res = await axios.delete(`/api/budget-projects/${project.id}/quotations/${quotationDeleteId}`, {
|
||||
const res = await apiClient.delete(`/budget-projects/${project.id}/quotations/${quotationDeleteId}`, {
|
||||
headers: {
|
||||
'x-user-role': currentUser?.role || 'employee'
|
||||
}
|
||||
@@ -218,7 +218,7 @@ const BudgetProjectDetail: React.FC = () => {
|
||||
|
||||
setDeleteLoading(true);
|
||||
try {
|
||||
const res = await axios.delete(`/api/budget-projects/${project.id}`, {
|
||||
const res = await apiClient.delete(`/budget-projects/${project.id}`, {
|
||||
headers: {
|
||||
'x-user-role': currentUser?.role || 'employee'
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
|
||||
import { Card, Typography, Button, Space, Tag, message, Empty, Radio, Modal, Input } from 'antd';
|
||||
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
import apiClient from '../../utils/request';
|
||||
import dayjs from 'dayjs';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
|
||||
@@ -79,7 +79,7 @@ const BudgetProjectList: React.FC = () => {
|
||||
const fetchProjects = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await axios.get('/api/budget-projects');
|
||||
const res = await apiClient.get('/budget-projects');
|
||||
if (res.data.success) {
|
||||
// 后端已经解析了数据,直接使用
|
||||
const projectsWithParsedData = res.data.data.map((project: any) => {
|
||||
@@ -148,7 +148,7 @@ const BudgetProjectList: React.FC = () => {
|
||||
|
||||
setDeleteLoading(true);
|
||||
try {
|
||||
const res = await axios.delete(`/api/budget-projects/${deleteProjectId}`, {
|
||||
const res = await apiClient.delete(`/budget-projects/${deleteProjectId}`, {
|
||||
headers: {
|
||||
'x-user-role': currentUser?.role || 'employee'
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Modal, Form, Input, DatePicker, InputNumber, Select, Space, message } from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
import axios from 'axios';
|
||||
import apiClient from '../../utils/request';
|
||||
import useFormDraft from '../../hooks/useFormDraft';
|
||||
|
||||
interface ContractCreateModalProps {
|
||||
visible: boolean;
|
||||
@@ -20,6 +21,16 @@ const ContractCreateModal: React.FC<ContractCreateModalProps> = ({
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [contractAmount, setContractAmount] = useState(0);
|
||||
|
||||
// 表单草稿保护
|
||||
const { save: saveDraft, restore: restoreDraft, clear: clearDraft, hasDraft } = useFormDraft({
|
||||
form,
|
||||
storageKey: 'contract_create',
|
||||
})
|
||||
|
||||
const handleFormChange = useCallback(() => {
|
||||
saveDraft()
|
||||
}, [saveDraft])
|
||||
|
||||
// 生成默认的合同编号(包含时间戳确保唯一性)
|
||||
const today = dayjs();
|
||||
@@ -66,19 +77,18 @@ const ContractCreateModal: React.FC<ContractCreateModalProps> = ({
|
||||
unit_price_items: []
|
||||
};
|
||||
|
||||
console.log('提交的合同信息:', submitData);
|
||||
|
||||
try {
|
||||
const res = await axios.put(`/api/budget-projects/${projectId}/sign`, submitData, {
|
||||
const res = await apiClient.put(`/budget-projects/${projectId}/sign`, submitData, {
|
||||
headers: {
|
||||
'x-user-role': 'admin' // 签约操作需要管理员权限
|
||||
}
|
||||
});
|
||||
|
||||
console.log('API响应:', res);
|
||||
|
||||
if (res.data.success) {
|
||||
message.success('签约成功,项目已自动创建');
|
||||
clearDraft()
|
||||
onSuccess();
|
||||
onCancel();
|
||||
} else {
|
||||
@@ -97,15 +107,32 @@ const ContractCreateModal: React.FC<ContractCreateModalProps> = ({
|
||||
title="快速签约"
|
||||
open={visible}
|
||||
onOk={() => form.submit()}
|
||||
onCancel={onCancel}
|
||||
onCancel={() => {
|
||||
if (form.isFieldsTouched()) {
|
||||
Modal.confirm({
|
||||
title: '确认关闭',
|
||||
content: '表单数据尚未保存,关闭后可通过草稿恢复。确定关闭吗?',
|
||||
okText: '关闭',
|
||||
cancelText: '继续编辑',
|
||||
onOk: () => {
|
||||
saveDraft()
|
||||
onCancel()
|
||||
},
|
||||
})
|
||||
} else {
|
||||
onCancel()
|
||||
}
|
||||
}}
|
||||
width={600}
|
||||
okText="确认签约"
|
||||
cancelText="取消"
|
||||
maskClosable={false}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleSubmit}
|
||||
onValuesChange={handleFormChange}
|
||||
>
|
||||
{/* 基本信息 */}
|
||||
<Form.Item
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Modal, Form, Input, DatePicker, InputNumber, Select, Upload, Button, message } from 'antd';
|
||||
import { UploadOutlined, DeleteOutlined, FileOutlined } from '@ant-design/icons';
|
||||
import dayjs from 'dayjs';
|
||||
import axios from 'axios';
|
||||
import apiClient from '../../utils/request';
|
||||
import useFormDraft from '../../hooks/useFormDraft';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
@@ -47,6 +48,19 @@ const QuotationCreateModal: React.FC<QuotationCreateModalProps> = ({
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [uploadedFile, setUploadedFile] = useState<{ url: string; name: string } | null>(null);
|
||||
|
||||
// 表单草稿保护
|
||||
const { save: saveDraft, restore: restoreDraft, clear: clearDraft, hasDraft } = useFormDraft({
|
||||
form,
|
||||
storageKey: 'quotation_create',
|
||||
onRestore: (data) => {
|
||||
if (data.uploadedFile) setUploadedFile(data.uploadedFile)
|
||||
},
|
||||
})
|
||||
|
||||
const handleFormChange = useCallback(() => {
|
||||
saveDraft({ uploadedFile })
|
||||
}, [saveDraft, uploadedFile])
|
||||
|
||||
// 计算下一个版本号
|
||||
const nextVersion = project?.quotations && Array.isArray(project.quotations) && project.quotations.length > 0
|
||||
? Math.max(...project.quotations.map(q => q.version || 0)) + 1
|
||||
@@ -110,13 +124,14 @@ const QuotationCreateModal: React.FC<QuotationCreateModalProps> = ({
|
||||
version: nextVersion,
|
||||
};
|
||||
|
||||
const res = await axios.post(`/api/budget-projects/${project.id}/quotations`, quotationData, {
|
||||
const res = await apiClient.post(`/budget-projects/${project.id}/quotations`, quotationData, {
|
||||
headers: {
|
||||
'x-user-role': 'admin' // 创建报价版本需要管理员权限
|
||||
}
|
||||
});
|
||||
if (res.data.success) {
|
||||
message.success('新增报价版本成功');
|
||||
clearDraft()
|
||||
onSuccess();
|
||||
}
|
||||
} catch (error: any) {
|
||||
@@ -152,13 +167,29 @@ const QuotationCreateModal: React.FC<QuotationCreateModalProps> = ({
|
||||
title="新增报价版本"
|
||||
open={visible}
|
||||
onOk={handleSubmit}
|
||||
onCancel={onCancel}
|
||||
onCancel={() => {
|
||||
if (form.isFieldsTouched()) {
|
||||
Modal.confirm({
|
||||
title: '确认关闭',
|
||||
content: '表单数据尚未保存,关闭后可通过草稿恢复。确定关闭吗?',
|
||||
okText: '关闭',
|
||||
cancelText: '继续编辑',
|
||||
onOk: () => {
|
||||
saveDraft({ uploadedFile })
|
||||
onCancel()
|
||||
},
|
||||
})
|
||||
} else {
|
||||
onCancel()
|
||||
}
|
||||
}}
|
||||
width={600}
|
||||
confirmLoading={loading}
|
||||
okText="保存"
|
||||
cancelText="取消"
|
||||
maskClosable={false}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form form={form} layout="vertical" onValuesChange={handleFormChange}>
|
||||
{/* 项目信息展示 */}
|
||||
<div style={{
|
||||
padding: 16,
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
|
||||
import { Card, Typography, Button, Space, Tag, Progress, Empty, Spin, message, Row, Col, Divider } from 'antd';
|
||||
import { FileTextOutlined, CameraOutlined, ScheduleOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
import apiClient from '../../utils/request';
|
||||
import dayjs from 'dayjs';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
|
||||
@@ -67,7 +67,7 @@ const ConstructionList: React.FC = () => {
|
||||
const fetchProjects = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await axios.get('/api/construction/my-projects');
|
||||
const res = await apiClient.get('/construction/my-projects');
|
||||
if (res.data.success) {
|
||||
setProjects(res.data.data);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
PlusOutlined, ArrowLeftOutlined, DeleteOutlined,
|
||||
CameraOutlined, CalendarOutlined, CloudOutlined
|
||||
} from '@ant-design/icons';
|
||||
import axios from 'axios';
|
||||
import apiClient from '../../utils/request';
|
||||
import dayjs from 'dayjs';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
|
||||
@@ -75,7 +75,7 @@ const ConstructionLog: React.FC = () => {
|
||||
const fetchLogs = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await axios.get(`/api/projects/${projectId}/construction-logs`);
|
||||
const res = await apiClient.get(`/projects/${projectId}/construction-logs`);
|
||||
if (res.data.success) {
|
||||
setLogs(res.data.data);
|
||||
}
|
||||
@@ -89,7 +89,7 @@ const ConstructionLog: React.FC = () => {
|
||||
|
||||
const fetchProjectInfo = async () => {
|
||||
try {
|
||||
const res = await axios.get(`/api/projects/${projectId}`);
|
||||
const res = await apiClient.get(`/projects/${projectId}`);
|
||||
if (res.data.success) {
|
||||
setProjectInfo(res.data.data);
|
||||
}
|
||||
@@ -103,7 +103,7 @@ const ConstructionLog: React.FC = () => {
|
||||
const values = await form.validateFields();
|
||||
setSubmitting(true);
|
||||
|
||||
const res = await axios.post(`/api/projects/${projectId}/construction-logs`, {
|
||||
const res = await apiClient.post(`/projects/${projectId}/construction-logs`, {
|
||||
log_date: values.log_date.format('YYYY-MM-DD'),
|
||||
weather: values.weather,
|
||||
work_content: values.work_content,
|
||||
@@ -126,7 +126,7 @@ const ConstructionLog: React.FC = () => {
|
||||
|
||||
const handleDeleteLog = async (logId: number) => {
|
||||
try {
|
||||
const res = await axios.delete(`/api/construction-logs/${logId}`);
|
||||
const res = await apiClient.delete(`/construction-logs/${logId}`);
|
||||
if (res.data.success) {
|
||||
message.success('日志删除成功');
|
||||
fetchLogs();
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
ArrowLeftOutlined, CheckCircleOutlined, ClockCircleOutlined,
|
||||
SyncOutlined, CloseCircleOutlined
|
||||
} from '@ant-design/icons';
|
||||
import axios from 'axios';
|
||||
import apiClient from '../../utils/request';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const { Title, Paragraph, Text } = Typography;
|
||||
@@ -80,7 +80,7 @@ const ConstructionMilestones: React.FC = () => {
|
||||
const fetchMilestones = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await axios.get(`/api/construction/projects/${projectId}/milestones`);
|
||||
const res = await apiClient.get(`/construction/projects/${projectId}/milestones`);
|
||||
if (res.data.success) {
|
||||
setMilestones(res.data.data);
|
||||
}
|
||||
@@ -93,7 +93,7 @@ const ConstructionMilestones: React.FC = () => {
|
||||
|
||||
const fetchProjectInfo = async () => {
|
||||
try {
|
||||
const res = await axios.get(`/api/projects/${projectId}`);
|
||||
const res = await apiClient.get(`/projects/${projectId}`);
|
||||
if (res.data.success) {
|
||||
setProjectInfo(res.data.data);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
FileTextOutlined,
|
||||
TeamOutlined
|
||||
} from '@ant-design/icons';
|
||||
import axios from 'axios';
|
||||
import apiClient from '../../utils/request';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
@@ -35,9 +35,7 @@ const DashboardPage: React.FC = () => {
|
||||
// 从 API 获取真实项目数据
|
||||
const fetchProjects = async () => {
|
||||
try {
|
||||
console.log('开始获取项目数据...');
|
||||
const response = await axios.get('/api/projects');
|
||||
console.log('获取项目数据成功:', response.data);
|
||||
const response = await apiClient.get('/projects');
|
||||
|
||||
if (response.data.success) {
|
||||
const projects = response.data.data.map((project: any) => ({
|
||||
@@ -49,7 +47,6 @@ const DashboardPage: React.FC = () => {
|
||||
budget: parseFloat(project.budget || 0),
|
||||
spent: parseFloat(project.spent || 0)
|
||||
}));
|
||||
console.log('转换后的项目数据:', projects);
|
||||
setProjectData(projects);
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
DollarOutlined,
|
||||
SafetyOutlined
|
||||
} from '@ant-design/icons'
|
||||
import axios from 'axios'
|
||||
import apiClient from '../../utils/request'
|
||||
|
||||
const { TabPane } = Tabs
|
||||
|
||||
@@ -106,8 +106,8 @@ const ProjectDetail: React.FC = () => {
|
||||
const fetchUsers = async () => {
|
||||
setUsersLoading(true)
|
||||
try {
|
||||
const response = await fetch('/api/users')
|
||||
const data = await response.json()
|
||||
const response = await apiClient.get('/users')
|
||||
const data = response.data
|
||||
if (data.success) {
|
||||
setUsers(data.data)
|
||||
}
|
||||
@@ -209,7 +209,7 @@ const ProjectDetail: React.FC = () => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
const response = await axios.post('/api/upload/single', formData, {
|
||||
const response = await apiClient.post('/upload/single', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
@@ -286,7 +286,7 @@ const ProjectDetail: React.FC = () => {
|
||||
contractAmount = totalAmount;
|
||||
}
|
||||
|
||||
const response = await axios.post(`/api/projects/${id}/subcontracts`, {
|
||||
const response = await apiClient.post(`/projects/${id}/subcontracts`, {
|
||||
subcontractor_id: values.subcontractor_id,
|
||||
subcontractor_name: values.subcontractor_name,
|
||||
contract_amount: contractAmount,
|
||||
@@ -322,7 +322,7 @@ const ProjectDetail: React.FC = () => {
|
||||
|
||||
const fetchProject = async () => {
|
||||
try {
|
||||
const response = await axios.get(`/api/projects/${id}`)
|
||||
const response = await apiClient.get(`/projects/${id}`)
|
||||
if (response.data.success) {
|
||||
setProject(response.data.data)
|
||||
}
|
||||
@@ -336,43 +336,43 @@ const ProjectDetail: React.FC = () => {
|
||||
const fetchProjectData = async () => {
|
||||
try {
|
||||
// 获取合同信息
|
||||
const contractsResponse = await axios.get(`/api/projects/${id}/contracts`)
|
||||
const contractsResponse = await apiClient.get(`/projects/${id}/contracts`)
|
||||
if (contractsResponse.data.success) {
|
||||
setContracts(contractsResponse.data.data)
|
||||
}
|
||||
|
||||
// 获取分包信息
|
||||
const subcontractsResponse = await axios.get(`/api/projects/${id}/subcontracts`)
|
||||
const subcontractsResponse = await apiClient.get(`/projects/${id}/subcontracts`)
|
||||
if (subcontractsResponse.data.success) {
|
||||
setSubcontracts(subcontractsResponse.data.data)
|
||||
}
|
||||
|
||||
// 获取材料信息
|
||||
const materialsResponse = await axios.get(`/api/projects/${id}/materials`)
|
||||
const materialsResponse = await apiClient.get(`/projects/${id}/materials`)
|
||||
if (materialsResponse.data.success) {
|
||||
setMaterials(materialsResponse.data.data)
|
||||
}
|
||||
|
||||
// 获取施工节点
|
||||
const milestonesResponse = await axios.get(`/api/projects/${id}/milestones`)
|
||||
const milestonesResponse = await apiClient.get(`/projects/${id}/milestones`)
|
||||
if (milestonesResponse.data.success) {
|
||||
setMilestones(milestonesResponse.data.data)
|
||||
}
|
||||
|
||||
// 获取财务信息
|
||||
const financesResponse = await axios.get(`/api/projects/${id}/finances`)
|
||||
const financesResponse = await apiClient.get(`/projects/${id}/finances`)
|
||||
if (financesResponse.data.success) {
|
||||
setFinances(financesResponse.data.data)
|
||||
}
|
||||
|
||||
// 获取质保金信息
|
||||
const warrantyDepositsResponse = await axios.get(`/api/projects/${id}/warranty-deposits`)
|
||||
const warrantyDepositsResponse = await apiClient.get(`/projects/${id}/warranty-deposits`)
|
||||
if (warrantyDepositsResponse.data.success) {
|
||||
setWarrantyDeposits(warrantyDepositsResponse.data.data)
|
||||
}
|
||||
|
||||
// 获取施工日志
|
||||
const constructionLogsResponse = await axios.get(`/api/projects/${id}/construction-logs`)
|
||||
const constructionLogsResponse = await apiClient.get(`/projects/${id}/construction-logs`)
|
||||
if (constructionLogsResponse.data.success) {
|
||||
setConstructionLogs(constructionLogsResponse.data.data)
|
||||
}
|
||||
@@ -392,7 +392,7 @@ const ProjectDetail: React.FC = () => {
|
||||
// 获取分包商列表
|
||||
const fetchSubcontractors = async () => {
|
||||
try {
|
||||
const response = await axios.get('/api/subcontractors')
|
||||
const response = await apiClient.get('/subcontractors')
|
||||
if (response.data.success) {
|
||||
setSubcontractors(response.data.data)
|
||||
}
|
||||
@@ -464,6 +464,10 @@ const ProjectDetail: React.FC = () => {
|
||||
newNodes[index].percentage = value
|
||||
newNodes[index].amount = Math.round((contractTotal * value) / 100)
|
||||
setPaymentNodes(newNodes)
|
||||
const total = newNodes.reduce((sum, n) => sum + (n.percentage || 0), 0)
|
||||
if (total > 100) {
|
||||
message.warning(`付款比例合计为 ${total}%,已超过 100%,请调整`)
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
@@ -1046,10 +1050,9 @@ const ProjectDetail: React.FC = () => {
|
||||
description: values.description
|
||||
};
|
||||
|
||||
console.log('保存基本信息数据:', saveData);
|
||||
|
||||
// 发送保存请求
|
||||
const response = await axios.put(`/api/projects/${id}`, saveData);
|
||||
const response = await apiClient.put(`/projects/${id}`, saveData);
|
||||
if (response.data.success) {
|
||||
message.success('基本信息保存成功');
|
||||
// 重新获取项目信息
|
||||
@@ -1475,10 +1478,9 @@ const ProjectDetail: React.FC = () => {
|
||||
contract_file: contractFile
|
||||
};
|
||||
|
||||
console.log('保存数据:', saveData);
|
||||
|
||||
// 发送保存请求
|
||||
const response = await axios.put(`/api/projects/${id}/contract`, saveData);
|
||||
const response = await apiClient.put(`/projects/${id}/contract`, saveData);
|
||||
if (response.data.success) {
|
||||
message.success('合同细节保存成功');
|
||||
// 重新获取项目信息
|
||||
@@ -1630,7 +1632,7 @@ const ProjectDetail: React.FC = () => {
|
||||
/>
|
||||
)
|
||||
},
|
||||
{ title: '操作', key: 'action', render: () => <Button danger>删除</Button> }
|
||||
{ title: '操作', key: 'action', render: (_, record, index) => <Button danger onClick={() => removeUnitPriceItem(index)}>删除</Button> }
|
||||
]}
|
||||
pagination={false}
|
||||
locale={{ emptyText: '暂无项目单项' }}
|
||||
@@ -1709,12 +1711,12 @@ const ProjectDetail: React.FC = () => {
|
||||
key: 'status',
|
||||
render: () => <Tag color="default">未到达付款节点</Tag>
|
||||
},
|
||||
{ title: '操作', key: 'action', render: () => <Button danger>删除</Button> }
|
||||
{ title: '操作', key: 'action', render: (_, record, index) => <Button danger onClick={() => { const newNodes = [...paymentNodes]; newNodes.splice(index, 1); setPaymentNodes(newNodes); }}>删除</Button> }
|
||||
]}
|
||||
pagination={false}
|
||||
locale={{ emptyText: '暂无付款节点' }}
|
||||
/>
|
||||
<Button type="dashed" style={{ marginTop: 16 }}>添加付款节点</Button>
|
||||
<Button type="dashed" style={{ marginTop: 16 }} onClick={() => { setPaymentNodes([...paymentNodes, { key: String(Date.now()), name: '', condition: '', percentage: 0, amount: 0, status: 'pending' }]); }}>添加付款节点</Button>
|
||||
</Form.Item>
|
||||
|
||||
{/* 合同附件 */}
|
||||
@@ -1722,7 +1724,7 @@ const ProjectDetail: React.FC = () => {
|
||||
<Upload
|
||||
name="file"
|
||||
customRequest={handleFileUpload}
|
||||
listType="file"
|
||||
listType="text"
|
||||
maxCount={1}
|
||||
fileList={contractFile ? [{ uid: '1', name: contractFile.split('/').pop() || '', status: 'done', url: contractFile }] : []}
|
||||
onRemove={() => setContractFile('')}
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Card, Typography, Button, Space, Table, Tag, message, Spin, Modal, Input } from 'antd';
|
||||
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
import axios from 'axios';
|
||||
import apiClient from '../../utils/request';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
|
||||
const { Title, Paragraph } = Typography;
|
||||
@@ -52,9 +52,7 @@ const ProjectsPage: React.FC = () => {
|
||||
|
||||
const fetchProjects = async () => {
|
||||
try {
|
||||
console.log('开始获取项目列表...');
|
||||
const response = await axios.get('/api/projects');
|
||||
console.log('API响应:', response.data);
|
||||
const response = await apiClient.get('/projects');
|
||||
if (response.data.success) {
|
||||
setProjects(response.data.data.map((p: Project) => ({
|
||||
...p,
|
||||
@@ -93,7 +91,7 @@ const ProjectsPage: React.FC = () => {
|
||||
|
||||
setDeleteLoading(true);
|
||||
try {
|
||||
const response = await axios.delete(`/api/projects/${deleteProjectId}`, {
|
||||
const response = await apiClient.delete(`/projects/${deleteProjectId}`, {
|
||||
headers: {
|
||||
'x-user-role': 'admin'
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Table, Button, Card, Space, Tag, Modal, Form, Input, InputNumber, Select, DatePicker, message, Descriptions, Image, Divider, Tabs } from 'antd';
|
||||
import { PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined, UndoOutlined, PlusCircleOutlined, MinusCircleOutlined } from '@ant-design/icons';
|
||||
import dayjs from 'dayjs';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
import FileUpload from '../../components/FileUpload';
|
||||
import useFormDraft from '../../hooks/useFormDraft';
|
||||
|
||||
const { Option } = Select;
|
||||
const { TextArea } = Input;
|
||||
@@ -18,8 +19,8 @@ interface DetailItem {
|
||||
|
||||
const ReimbursementsPage: React.FC = () => {
|
||||
const { user } = useAuthStore();
|
||||
const [reimbursements, setReimbursements] = useState<any[]>([]);
|
||||
const [completedReimbursements, setCompletedReimbursements] = useState<any[]>([]);
|
||||
const [reimbursements, setReimbursements] = useState<any[]>([]);
|
||||
const [completedReimbursements, setCompletedReimbursements] = useState<any[]>([]);
|
||||
const [activeTab, setActiveTab] = useState('active');
|
||||
const [projects, setProjects] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -33,6 +34,20 @@ const ReimbursementsPage: React.FC = () => {
|
||||
const [detailItems, setDetailItems] = useState<DetailItem[]>([]);
|
||||
const [exchangeRates, setExchangeRates] = useState<Record<string, number>>({});
|
||||
|
||||
// 表单草稿保护
|
||||
const { save: saveDraft, restore: restoreDraft, clear: clearDraft, hasDraft } = useFormDraft({
|
||||
form,
|
||||
storageKey: 'reimbursement_create',
|
||||
onRestore: (data) => {
|
||||
if (data.detailItems) setDetailItems(data.detailItems);
|
||||
},
|
||||
});
|
||||
|
||||
// 保存草稿(包含 detailItems 外部状态)
|
||||
const handleFormChange = useCallback(() => {
|
||||
saveDraft({ detailItems });
|
||||
}, [saveDraft, detailItems]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchReimbursements();
|
||||
fetchProjects();
|
||||
@@ -99,6 +114,32 @@ const ReimbursementsPage: React.FC = () => {
|
||||
attachments: []
|
||||
});
|
||||
setModalVisible(true);
|
||||
// 检查是否有草稿,提示用户是否恢复(在下一帧执行,确保 form 状态已更新)
|
||||
setTimeout(() => {
|
||||
if (hasDraft()) {
|
||||
Modal.confirm({
|
||||
title: '发现未完成的草稿',
|
||||
content: '检测到上次未提交的报销申请,是否恢复?',
|
||||
okText: '恢复草稿',
|
||||
cancelText: '重新填写',
|
||||
onOk: () => {
|
||||
restoreDraft();
|
||||
},
|
||||
onCancel: () => {
|
||||
clearDraft();
|
||||
form.resetFields();
|
||||
form.setFieldsValue({
|
||||
reimbursement_date: dayjs(),
|
||||
currency: 'CNY',
|
||||
expense_type: 'company',
|
||||
applicant: user?.name || user?.username || '当前用户',
|
||||
attachments: []
|
||||
});
|
||||
setDetailItems([]);
|
||||
},
|
||||
});
|
||||
}
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const handleEdit = (record: any) => {
|
||||
@@ -189,6 +230,7 @@ const ReimbursementsPage: React.FC = () => {
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
message.success(editingId ? '保存成功' : '创建成功');
|
||||
clearDraft();
|
||||
setModalVisible(false);
|
||||
fetchReimbursements();
|
||||
} else {
|
||||
@@ -223,6 +265,7 @@ const ReimbursementsPage: React.FC = () => {
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
message.success(editingId ? '提交成功' : '创建成功');
|
||||
clearDraft();
|
||||
setModalVisible(false);
|
||||
fetchReimbursements();
|
||||
} else {
|
||||
@@ -238,17 +281,22 @@ const ReimbursementsPage: React.FC = () => {
|
||||
};
|
||||
|
||||
const addDetailItem = () => {
|
||||
setDetailItems([...detailItems, { description: '', amount: 0, category: '', attachments: [] }]);
|
||||
const newItems = [...detailItems, { description: '', amount: 0, category: '', attachments: [] }];
|
||||
setDetailItems(newItems);
|
||||
saveDraft({ detailItems: newItems });
|
||||
};
|
||||
|
||||
const updateDetailItem = (index: number, field: keyof DetailItem, value: any) => {
|
||||
const newItems = [...detailItems];
|
||||
newItems[index] = { ...newItems[index], [field]: value };
|
||||
setDetailItems(newItems);
|
||||
saveDraft({ detailItems: newItems });
|
||||
};
|
||||
|
||||
const removeDetailItem = (index: number) => {
|
||||
setDetailItems(detailItems.filter((_, i) => i !== index));
|
||||
const newItems = detailItems.filter((_, i) => i !== index);
|
||||
setDetailItems(newItems);
|
||||
saveDraft({ detailItems: newItems });
|
||||
};
|
||||
|
||||
const getStatusTag = (status: string) => {
|
||||
@@ -325,18 +373,49 @@ const ReimbursementsPage: React.FC = () => {
|
||||
</Tabs>
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
title={editingId ? '编辑报销' : '新建报销'}
|
||||
open={modalVisible}
|
||||
onCancel={() => setModalVisible(false)}
|
||||
footer={[
|
||||
<Button key="cancel" onClick={() => setModalVisible(false)}>取消</Button>,
|
||||
<Button key="save" onClick={handleSave}>保存</Button>,
|
||||
<Button key="submit" type="primary" onClick={handleSubmitAndSubmit}>提交</Button>
|
||||
]}
|
||||
width={900}
|
||||
<Modal
|
||||
title={editingId ? '编辑报销' : '新建报销'}
|
||||
open={modalVisible}
|
||||
onCancel={() => {
|
||||
if (form.isFieldsTouched()) {
|
||||
Modal.confirm({
|
||||
title: '确认关闭',
|
||||
content: '表单数据尚未保存,关闭后可通过草稿恢复。确定关闭吗?',
|
||||
okText: '关闭',
|
||||
cancelText: '继续编辑',
|
||||
onOk: () => {
|
||||
saveDraft({ detailItems });
|
||||
setModalVisible(false);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
setModalVisible(false);
|
||||
}
|
||||
}}
|
||||
maskClosable={false}
|
||||
footer={[
|
||||
<Button key="cancel" onClick={() => {
|
||||
if (form.isFieldsTouched()) {
|
||||
Modal.confirm({
|
||||
title: '确认关闭',
|
||||
content: '表单数据尚未保存,关闭后可通过草稿恢复。确定关闭吗?',
|
||||
okText: '关闭',
|
||||
cancelText: '继续编辑',
|
||||
onOk: () => {
|
||||
saveDraft({ detailItems });
|
||||
setModalVisible(false);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
setModalVisible(false);
|
||||
}
|
||||
}}>取消</Button>,
|
||||
<Button key="save" onClick={handleSave}>保存</Button>,
|
||||
<Button key="submit" type="primary" onClick={handleSubmitAndSubmit}>提交</Button>
|
||||
]}
|
||||
width={900}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form form={form} layout="vertical" onValuesChange={handleFormChange}>
|
||||
<Form.Item name="applicant" label="申请人">
|
||||
<Input disabled style={{ color: 'rgba(0,0,0,0.85)', backgroundColor: '#f5f5f5' }} />
|
||||
</Form.Item>
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, Button, Table, message } from 'antd';
|
||||
|
||||
const TestPage: React.FC = () => {
|
||||
const [advances, setAdvances] = useState<any[]>([]);
|
||||
const [reimbursements, setReimbursements] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const fetchAdvances = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch('/api/advances');
|
||||
console.log('Advances response:', res);
|
||||
const data = await res.json();
|
||||
console.log('Advances data:', data);
|
||||
if (data.success) {
|
||||
setAdvances(data.data);
|
||||
message.success(`获取到 ${data.data.length} 条预支申请`);
|
||||
} else {
|
||||
message.error('获取预支申请失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching advances:', error);
|
||||
message.error('获取预支申请失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchReimbursements = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch('/api/reimbursements');
|
||||
console.log('Reimbursements response:', res);
|
||||
const data = await res.json();
|
||||
console.log('Reimbursements data:', data);
|
||||
if (data.success) {
|
||||
setReimbursements(data.data);
|
||||
message.success(`获取到 ${data.data.length} 条报销申请`);
|
||||
} else {
|
||||
message.error('获取报销申请失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching reimbursements:', error);
|
||||
message.error('获取报销申请失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchAdvances();
|
||||
fetchReimbursements();
|
||||
}, []);
|
||||
|
||||
const advanceColumns = [
|
||||
{ title: 'ID', dataIndex: 'id', key: 'id' },
|
||||
{ title: '编号', dataIndex: 'advance_code', key: 'advance_code' },
|
||||
{ title: '申请人', dataIndex: 'applicant', key: 'applicant' },
|
||||
{ title: '金额', dataIndex: 'amount', key: 'amount' },
|
||||
{ title: '币种', dataIndex: 'currency', key: 'currency' },
|
||||
{ title: '日期', dataIndex: 'advance_date', key: 'advance_date' },
|
||||
{ title: '事由', dataIndex: 'reason', key: 'reason' },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status' },
|
||||
];
|
||||
|
||||
const reimbursementColumns = [
|
||||
{ title: 'ID', dataIndex: 'id', key: 'id' },
|
||||
{ title: '编号', dataIndex: 'reimbursement_code', key: 'reimbursement_code' },
|
||||
{ title: '申请人', dataIndex: 'applicant', key: 'applicant' },
|
||||
{ title: '金额', dataIndex: 'amount', key: 'amount' },
|
||||
{ title: '币种', dataIndex: 'currency', key: 'currency' },
|
||||
{ title: '日期', dataIndex: 'reimbursement_date', key: 'reimbursement_date' },
|
||||
{ title: '事由', dataIndex: 'reason', key: 'reason' },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status' },
|
||||
{ title: '支出类型', dataIndex: 'expense_type', key: 'expense_type' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ padding: 24 }}>
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<h2>测试API数据</h2>
|
||||
<p>此页面用于测试API是否正常返回数据</p>
|
||||
</div>
|
||||
|
||||
<Card title="预支申请" style={{ marginBottom: 24 }}>
|
||||
<Button type="primary" onClick={fetchAdvances} loading={loading} style={{ marginBottom: 16 }}>
|
||||
刷新预支申请
|
||||
</Button>
|
||||
<Table dataSource={advances} columns={advanceColumns} rowKey="id" />
|
||||
</Card>
|
||||
|
||||
<Card title="报销申请">
|
||||
<Button type="primary" onClick={fetchReimbursements} loading={loading} style={{ marginBottom: 16 }}>
|
||||
刷新报销申请
|
||||
</Button>
|
||||
<Table dataSource={reimbursements} columns={reimbursementColumns} rowKey="id" />
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TestPage;
|
||||
Reference in New Issue
Block a user