备份:大改造前的完整版本 - 修复合同细节/付款节点/文件上传/施工管理/项目保存等BUG

This commit is contained in:
root
2026-05-15 12:02:24 +08:00
parent 1cad1e438d
commit fad28741bd
6157 changed files with 5147 additions and 877912 deletions
+1 -1
View File
@@ -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
+57 -3
View File
@@ -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>
-438
View File
@@ -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>
722kV高压线路改造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;
+118 -78
View File
@@ -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>
+55 -3
View File
@@ -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>
+52 -4
View File
@@ -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: '老挝电力设备公司' },
-12
View File
@@ -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)
+47 -2
View File
@@ -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: '请输入姓名' }]}>
+84 -4
View File
@@ -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
+58 -20
View File
@@ -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>
+58 -20
View File
@@ -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>
-40
View File
@@ -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;
-31
View File
@@ -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;
-7
View File
@@ -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();
}, []);
+71 -11
View File
@@ -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', {
+110 -15
View File
@@ -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) {
+24 -22
View File
@@ -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('')}
+3 -5
View File
@@ -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>
-103
View File
@@ -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;