chore: 统一代码风格并配置ESLint和Prettier

配置ESLint和Prettier规则
添加前端和后端的忽略文件
统一代码格式和缩进
修复代码风格问题
This commit is contained in:
zhang1106
2026-02-10 11:04:01 +08:00
parent f82d82e57e
commit afc372a432
61 changed files with 14976 additions and 6124 deletions
+438 -290
View File
@@ -1,6 +1,32 @@
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
import { Table, Button, Modal, Form, Input, Select, InputNumber, message, Card, Space, Popconfirm, Upload, Table as AntTable, Progress, Checkbox } from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, ExportOutlined, ImportOutlined, UploadOutlined, FileExcelOutlined, InboxOutlined } from '@ant-design/icons';
import {
Table,
Button,
Modal,
Form,
Input,
Select,
InputNumber,
message,
Card,
Space,
Popconfirm,
Upload,
Table as AntTable,
Progress,
Checkbox,
} from 'antd';
import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
SearchOutlined,
ExportOutlined,
ImportOutlined,
UploadOutlined,
FileExcelOutlined,
InboxOutlined,
} from '@ant-design/icons';
import axios from 'axios';
const { Option } = Select;
@@ -16,7 +42,7 @@ function ConsumableManagement() {
current: 1,
pageSize: 10,
total: 0,
showTotal: (total) => `${total} 条记录`
showTotal: total => `${total} 条记录`,
});
const [keyword, setKeyword] = useState('');
const [category, setCategory] = useState('all');
@@ -34,21 +60,24 @@ function ConsumableManagement() {
const [stockForm] = Form.useForm();
const [maxStockUnlimited, setMaxStockUnlimited] = useState(false);
const fetchConsumables = useCallback(async (page = 1, pageSize = 10) => {
try {
setLoading(true);
const response = await axios.get('/api/consumables', {
params: { page, pageSize, keyword, category, status }
});
setConsumables(response.data.consumables);
setPagination(prev => ({ ...prev, current: page, pageSize, total: response.data.total }));
} catch (error) {
message.error('获取耗材列表失败');
console.error('获取耗材列表失败:', error);
} finally {
setLoading(false);
}
}, [keyword, category, status]);
const fetchConsumables = useCallback(
async (page = 1, pageSize = 10) => {
try {
setLoading(true);
const response = await axios.get('/api/consumables', {
params: { page, pageSize, keyword, category, status },
});
setConsumables(response.data.consumables);
setPagination(prev => ({ ...prev, current: page, pageSize, total: response.data.total }));
} catch (error) {
message.error('获取耗材列表失败');
console.error('获取耗材列表失败:', error);
} finally {
setLoading(false);
}
},
[keyword, category, status]
);
const fetchCategories = useCallback(async () => {
try {
@@ -64,94 +93,134 @@ function ConsumableManagement() {
fetchCategories();
}, [fetchConsumables, fetchCategories]);
const showModal = useCallback((consumable = null) => {
setEditingConsumable(consumable);
if (consumable) {
const isUnlimited = consumable.maxStock === 0 || consumable.maxStock === null || consumable.maxStock === undefined;
setMaxStockUnlimited(isUnlimited);
form.setFieldsValue({
...consumable,
maxStock: isUnlimited ? undefined : consumable.maxStock
});
} else {
setMaxStockUnlimited(true);
form.resetFields();
form.setFieldsValue({
unit: '个',
currentStock: 0,
minStock: 0,
status: 'active',
unitPrice: 0
});
}
setModalVisible(true);
}, [form]);
const showModal = useCallback(
(consumable = null) => {
setEditingConsumable(consumable);
if (consumable) {
const isUnlimited =
consumable.maxStock === 0 ||
consumable.maxStock === null ||
consumable.maxStock === undefined;
setMaxStockUnlimited(isUnlimited);
form.setFieldsValue({
...consumable,
maxStock: isUnlimited ? undefined : consumable.maxStock,
});
} else {
setMaxStockUnlimited(true);
form.resetFields();
form.setFieldsValue({
unit: '个',
currentStock: 0,
minStock: 0,
status: 'active',
unitPrice: 0,
});
}
setModalVisible(true);
},
[form]
);
const handleCancel = useCallback(() => {
setModalVisible(false);
setEditingConsumable(null);
}, []);
const handleSubmit = useCallback(async (values) => {
try {
const submitData = {
...values,
maxStock: maxStockUnlimited ? 0 : values.maxStock,
unitPrice: values.unitPrice || 0
};
if (editingConsumable) {
await axios.put(`/api/consumables/${editingConsumable.consumableId}`, submitData);
message.success('耗材更新成功');
} else {
await axios.post('/api/consumables', {
...submitData,
consumableId: `CON${Date.now()}`
});
message.success('耗材创建成功');
const handleSubmit = useCallback(
async values => {
try {
const submitData = {
...values,
maxStock: maxStockUnlimited ? 0 : values.maxStock,
unitPrice: values.unitPrice || 0,
};
if (editingConsumable) {
await axios.put(`/api/consumables/${editingConsumable.consumableId}`, submitData);
message.success('耗材更新成功');
} else {
await axios.post('/api/consumables', {
...submitData,
consumableId: `CON${Date.now()}`,
});
message.success('耗材创建成功');
}
setModalVisible(false);
fetchConsumables();
setEditingConsumable(null);
} catch (error) {
message.error(editingConsumable ? '耗材更新失败' : '耗材创建失败');
console.error('提交失败:', error);
}
setModalVisible(false);
fetchConsumables();
setEditingConsumable(null);
} catch (error) {
message.error(editingConsumable ? '耗材更新失败' : '耗材创建失败');
console.error('提交失败:', error);
}
}, [editingConsumable, fetchConsumables, maxStockUnlimited]);
},
[editingConsumable, fetchConsumables, maxStockUnlimited]
);
const handleDelete = useCallback(async (consumableId) => {
try {
await axios.delete(`/api/consumables/${consumableId}`);
message.success('删除成功');
fetchConsumables();
} catch (error) {
message.error('删除失败');
console.error('删除失败:', error);
}
}, [fetchConsumables]);
const handleDelete = useCallback(
async consumableId => {
try {
await axios.delete(`/api/consumables/${consumableId}`);
message.success('删除成功');
fetchConsumables();
} catch (error) {
message.error('删除失败');
console.error('删除失败:', error);
}
},
[fetchConsumables]
);
const handleSearch = useCallback((value) => {
const handleSearch = useCallback(value => {
setKeyword(value);
}, []);
const exportToCSV = (data, filename) => {
const headers = ['耗材ID', '名称', '分类', '单位', '当前库存', '最小库存', '最大库存', '单价', '供应商', '存放位置', '状态'];
const keys = ['consumableId', 'name', 'category', 'unit', 'currentStock', 'minStock', 'maxStock', 'unitPrice', 'supplier', 'location', 'status'];
const headers = [
'耗材ID',
'名称',
'分类',
'单位',
'当前库存',
'最小库存',
'最大库存',
'单价',
'供应商',
'存放位置',
'状态',
];
const keys = [
'consumableId',
'name',
'category',
'unit',
'currentStock',
'minStock',
'maxStock',
'unitPrice',
'supplier',
'location',
'status',
];
const csvContent = [
headers.join(','),
...data.map(row => keys.map(key => {
let value = row[key];
if (key === 'unitPrice') value = `¥${parseFloat(value || 0).toFixed(2)}`;
if (key === 'status') value = value === 'active' ? '启用' : '停用';
if (value === null || value === undefined) value = '';
const str = String(value);
if (str.includes(',') || str.includes('"') || str.includes('\n')) {
return `"${str.replace(/"/g, '""')}"`;
}
return str;
}).join(','))
...data.map(row =>
keys
.map(key => {
let value = row[key];
if (key === 'unitPrice') value = `¥${parseFloat(value || 0).toFixed(2)}`;
if (key === 'status') value = value === 'active' ? '启用' : '停用';
if (value === null || value === undefined) value = '';
const str = String(value);
if (str.includes(',') || str.includes('"') || str.includes('\n')) {
return `"${str.replace(/"/g, '""')}"`;
}
return str;
})
.join(',')
),
].join('\n');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
@@ -166,7 +235,7 @@ function ConsumableManagement() {
const handleExport = async () => {
try {
const response = await axios.get('/api/consumables', {
params: { keyword, category, status, pageSize: 1000 }
params: { keyword, category, status, pageSize: 1000 },
});
const consumables = response.data.consumables;
exportToCSV(consumables, `consumables_${new Date().toISOString().split('T')[0]}.csv`);
@@ -177,21 +246,21 @@ function ConsumableManagement() {
}
};
const parseCSV = (text) => {
const parseCSV = text => {
const lines = text.trim().split('\n');
if (lines.length < 2) return [];
const headers = lines[0].split(',').map(h => h.trim().replace(/^"|"$/g, ''));
const data = [];
for (let i = 1; i < lines.length; i++) {
const line = lines[i].trim();
if (!line) continue;
let values = [];
let inQuotes = false;
let current = '';
for (let j = 0; j < line.length; j++) {
const char = line[j];
if (char === '"') {
@@ -204,22 +273,22 @@ function ConsumableManagement() {
}
}
values.push(current.trim().replace(/^"|"$/g, ''));
const row = {};
headers.forEach((header, idx) => {
row[header] = values[idx] || '';
});
data.push(row);
}
return data;
};
const handleFileChange = (info) => {
const handleFileChange = info => {
const file = info.fileList[info.fileList.length - 1];
if (file && file.originFileObj) {
const reader = new FileReader();
reader.onload = (e) => {
reader.onload = e => {
const text = e.target.result;
const parsedData = parseCSV(text);
setImportPreview(parsedData.slice(0, 10));
@@ -249,57 +318,57 @@ function ConsumableManagement() {
message.warning('请先选择文件');
return;
}
setImporting(true);
setImportProgress(0);
setImportPhase('正在读取文件...');
setImportResult(null);
try {
const reader = new FileReader();
reader.onload = async (e) => {
reader.onload = async e => {
const text = e.target.result;
setImportProgress(10);
setImportPhase('正在读取文件...');
setTimeout(() => {
setImportProgress(20);
setImportPhase('正在解析CSV数据...');
}, 100);
const items = parseCSV(text);
const totalItems = items.length;
setTimeout(() => {
setImportProgress(30);
setImportPhase(`共解析 ${totalItems} 条记录,准备提交...`);
}, 200);
setTimeout(() => {
setImportProgress(40);
setImportPhase('正在连接服务器...');
}, 300);
const response = await axios.post('/api/consumables/import', { items });
setTimeout(() => {
setImportProgress(60);
setImportPhase('正在处理服务器响应...');
}, 100);
setTimeout(() => {
setImportProgress(80);
setImportPhase('正在更新本地数据...');
}, 200);
const results = response.data.results;
setTimeout(() => {
setImportResult(results);
setImportProgress(100);
setImportPhase('导入完成');
setImporting(false);
if (results.failed > 0) {
message.warning(`导入完成,成功 ${results.success} 条,失败 ${results.failed}`);
} else {
@@ -317,7 +386,7 @@ function ConsumableManagement() {
imported: 0,
failed: 0,
errors: [{ row: '-', error: '文件读取失败,请检查文件是否损坏' }],
message: '文件读取失败'
message: '文件读取失败',
});
message.error('文件读取失败');
};
@@ -326,16 +395,16 @@ function ConsumableManagement() {
setImporting(false);
setImportProgress(0);
setImportPhase('导入失败');
let errorMessage = '导入失败,请检查网络连接或服务器状态';
let errorDetails = [];
if (error.response && error.response.data) {
const { data } = error.response;
if (data.errors && Array.isArray(data.errors) && data.errors.length > 0) {
errorDetails = data.errors.map((err, index) => ({
row: err.row || index + 1,
error: err.error || err.message || '数据格式错误'
error: err.error || err.message || '数据格式错误',
}));
errorMessage = `导入失败,共发现 ${errorDetails.length} 处数据错误`;
} else if (data.message) {
@@ -352,23 +421,24 @@ function ConsumableManagement() {
errorMessage = error.message;
}
}
setImportResult({
success: false,
total: 0,
imported: 0,
failed: 0,
errors: errorDetails,
message: errorMessage
message: errorMessage,
});
message.error(errorMessage);
console.error('导入耗材失败:', error);
}
};
const downloadTemplate = () => {
const template = '耗材ID,名称,分类,单位,当前库存,最小库存,最大库存,单价,供应商,存放位置,描述,状态\n,测试耗材,办公用品,个,100,10,500,5.00,XX公司,A柜-01层,测试数据,active';
const template =
'耗材ID,名称,分类,单位,当前库存,最小库存,最大库存,单价,供应商,存放位置,描述,状态\n,测试耗材,办公用品,个,100,10,500,5.00,XX公司,A柜-01层,测试数据,active';
const blob = new Blob([template], { type: 'text/csv;charset=utf-8;' });
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
@@ -380,163 +450,208 @@ function ConsumableManagement() {
window.URL.revokeObjectURL(url);
};
const showStockModal = useCallback((record, type) => {
setStockRecord(record);
setStockType(type);
stockForm.setFieldsValue({
consumableId: record.consumableId,
consumableName: record.name,
quantity: 1,
reason: '',
notes: ''
});
setStockModalVisible(true);
}, [stockForm]);
const showStockModal = useCallback(
(record, type) => {
setStockRecord(record);
setStockType(type);
stockForm.setFieldsValue({
consumableId: record.consumableId,
consumableName: record.name,
quantity: 1,
reason: '',
notes: '',
});
setStockModalVisible(true);
},
[stockForm]
);
const handleStockCancel = useCallback(() => {
setStockModalVisible(false);
setStockRecord(null);
}, []);
const handleStockSubmit = useCallback(async (values) => {
try {
const response = await axios.post('/api/consumables/quick-inout', {
consumableId: stockRecord.consumableId,
type: stockType,
quantity: values.quantity,
operator: values.operator || '系统管理员',
reason: values.reason,
notes: values.notes
});
message.success(`${stockType === 'in' ? '入库' : '出库'}操作成功`);
setStockModalVisible(false);
fetchConsumables();
} catch (error) {
message.error(error.response?.data?.error || `${stockType === 'in' ? '入库' : '出库'}操作失败`);
console.error('操作失败:', error);
}
}, [stockRecord, stockType, fetchConsumables]);
const columns = useMemo(() => [
{
title: '名称',
dataIndex: 'name',
key: 'name',
width: 150
},
{
title: '分类',
dataIndex: 'category',
key: 'category',
width: 120
},
{
title: '单位',
dataIndex: 'unit',
key: 'unit',
width: 80
},
{
title: '当前库存',
dataIndex: 'currentStock',
key: 'currentStock',
width: 100,
render: (value, record) => {
const isLow = value <= record.minStock;
return (
<span style={{ color: isLow ? '#ff4d4f' : '#52c41a', fontWeight: 'bold' }}>
{value}
</span>
const handleStockSubmit = useCallback(
async values => {
try {
const response = await axios.post('/api/consumables/quick-inout', {
consumableId: stockRecord.consumableId,
type: stockType,
quantity: values.quantity,
operator: values.operator || '系统管理员',
reason: values.reason,
notes: values.notes,
});
message.success(`${stockType === 'in' ? '入库' : '出库'}操作成功`);
setStockModalVisible(false);
fetchConsumables();
} catch (error) {
message.error(
error.response?.data?.error || `${stockType === 'in' ? '入库' : '出库'}操作失败`
);
console.error('操作失败:', error);
}
},
{
title: '最小库存',
dataIndex: 'minStock',
key: 'minStock',
width: 100
},
{
title: '最大库存',
dataIndex: 'maxStock',
key: 'maxStock',
width: 100,
render: (value) => value === 0 || value === null || value === undefined ? '无限制' : value
},
{
title: '单价(元)',
dataIndex: 'unitPrice',
key: 'unitPrice',
width: 100,
render: (value) => `¥${parseFloat(value || 0).toFixed(2)}`
},
{
title: '供应商',
dataIndex: 'supplier',
key: 'supplier',
width: 150,
render: (value) => value || '-'
},
{
title: '位置',
dataIndex: 'location',
key: 'location',
width: 120,
render: (value) => value || '-'
},
{
title: '描述',
dataIndex: 'description',
key: 'description',
width: 200,
render: (value) => value || '-',
ellipsis: true
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100,
render: (value) => (
<span style={{ color: value === 'active' ? '#52c41a' : '#ff4d4f' }}>
{value === 'active' ? '启用' : '停用'}
</span>
)
},
{
title: '操作',
key: 'action',
width: 200,
render: (_, record) => (
<Space>
<Button type="primary" icon={<EditOutlined />} size="small" onClick={() => showModal(record)}>编辑</Button>
<Button type="default" icon={<InboxOutlined />} size="small" style={{ background: '#f6ffed', borderColor: '#b7eb8f', color: '#52c41a' }} onClick={() => showStockModal(record, 'in')}>入库</Button>
<Button type="default" icon={<ExportOutlined />} size="small" style={{ background: '#fff2f0', borderColor: '#ffccc7', color: '#ff4d4f' }} onClick={() => showStockModal(record, 'out')}>出库</Button>
<Popconfirm title="确定删除?" onConfirm={() => handleDelete(record.consumableId)}>
<Button danger icon={<DeleteOutlined />} size="small">删除</Button>
</Popconfirm>
</Space>
)
}
], [showModal, showStockModal, handleDelete]);
[stockRecord, stockType, fetchConsumables]
);
const columns = useMemo(
() => [
{
title: '名称',
dataIndex: 'name',
key: 'name',
width: 150,
},
{
title: '分类',
dataIndex: 'category',
key: 'category',
width: 120,
},
{
title: '单位',
dataIndex: 'unit',
key: 'unit',
width: 80,
},
{
title: '当前库存',
dataIndex: 'currentStock',
key: 'currentStock',
width: 100,
render: (value, record) => {
const isLow = value <= record.minStock;
return (
<span style={{ color: isLow ? '#ff4d4f' : '#52c41a', fontWeight: 'bold' }}>
{value}
</span>
);
},
},
{
title: '最小库存',
dataIndex: 'minStock',
key: 'minStock',
width: 100,
},
{
title: '最大库存',
dataIndex: 'maxStock',
key: 'maxStock',
width: 100,
render: value => (value === 0 || value === null || value === undefined ? '无限制' : value),
},
{
title: '单价(元)',
dataIndex: 'unitPrice',
key: 'unitPrice',
width: 100,
render: value => `¥${parseFloat(value || 0).toFixed(2)}`,
},
{
title: '供应商',
dataIndex: 'supplier',
key: 'supplier',
width: 150,
render: value => value || '-',
},
{
title: '位置',
dataIndex: 'location',
key: 'location',
width: 120,
render: value => value || '-',
},
{
title: '描述',
dataIndex: 'description',
key: 'description',
width: 200,
render: value => value || '-',
ellipsis: true,
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100,
render: value => (
<span style={{ color: value === 'active' ? '#52c41a' : '#ff4d4f' }}>
{value === 'active' ? '启用' : '停用'}
</span>
),
},
{
title: '操作',
key: 'action',
width: 200,
render: (_, record) => (
<Space>
<Button
type="primary"
icon={<EditOutlined />}
size="small"
onClick={() => showModal(record)}
>
编辑
</Button>
<Button
type="default"
icon={<InboxOutlined />}
size="small"
style={{ background: '#f6ffed', borderColor: '#b7eb8f', color: '#52c41a' }}
onClick={() => showStockModal(record, 'in')}
>
入库
</Button>
<Button
type="default"
icon={<ExportOutlined />}
size="small"
style={{ background: '#fff2f0', borderColor: '#ffccc7', color: '#ff4d4f' }}
onClick={() => showStockModal(record, 'out')}
>
出库
</Button>
<Popconfirm title="确定删除?" onConfirm={() => handleDelete(record.consumableId)}>
<Button danger icon={<DeleteOutlined />} size="small">
删除
</Button>
</Popconfirm>
</Space>
),
},
],
[showModal, showStockModal, handleDelete]
);
const previewColumns = [
{ title: '名称', dataIndex: '名称', key: 'name', width: 120 },
{ title: '分类', dataIndex: '分类', key: 'category', width: 100 },
{ title: '单位', dataIndex: '单位', key: 'unit', width: 80 },
{ title: '当前库存', dataIndex: '当前库存', key: 'currentStock', width: 90 },
{ title: '单价', dataIndex: '单价', key: 'unitPrice', width: 80 }
{ title: '单价', dataIndex: '单价', key: 'unitPrice', width: 80 },
];
return (
<div>
<Card title="耗材管理" extra={
<Space>
<Button type="primary" icon={<PlusOutlined />} onClick={() => showModal()}>添加耗材</Button>
<Button icon={<ImportOutlined />} onClick={showImportModal}>导入</Button>
<Button icon={<ExportOutlined />} onClick={handleExport}>导出</Button>
</Space>
}>
<Card
title="耗材管理"
extra={
<Space>
<Button type="primary" icon={<PlusOutlined />} onClick={() => showModal()}>
添加耗材
</Button>
<Button icon={<ImportOutlined />} onClick={showImportModal}>
导入
</Button>
<Button icon={<ExportOutlined />} onClick={handleExport}>
导出
</Button>
</Space>
}
>
<Card size="small" style={{ marginBottom: 16 }}>
<Space>
<Input.Search
@@ -548,7 +663,9 @@ function ConsumableManagement() {
<Select value={category} onChange={setCategory} style={{ width: 150 }}>
<Option value="all">所有分类</Option>
{categories.map(cat => (
<Option key={cat.id} value={cat.name}>{cat.name}</Option>
<Option key={cat.id} value={cat.name}>
{cat.name}
</Option>
))}
</Select>
<Select value={status} onChange={setStatus} style={{ width: 120 }}>
@@ -565,7 +682,7 @@ function ConsumableManagement() {
rowKey="consumableId"
loading={loading}
pagination={pagination}
onChange={(pagination) => fetchConsumables(pagination.current, pagination.pageSize)}
onChange={pagination => fetchConsumables(pagination.current, pagination.pageSize)}
scroll={{ x: 1300 }}
/>
</Card>
@@ -581,31 +698,47 @@ function ConsumableManagement() {
<Form.Item name="name" label="名称" rules={[{ required: true, message: '请输入名称' }]}>
<Input placeholder="请输入耗材名称" />
</Form.Item>
<Form.Item name="category" label="分类" rules={[{ required: true, message: '请选择分类' }]}>
<Select
placeholder="请选择分类"
allowClear
>
<Form.Item
name="category"
label="分类"
rules={[{ required: true, message: '请选择分类' }]}
>
<Select placeholder="请选择分类" allowClear>
{categories.map(cat => (
<Option key={cat.id} value={cat.name}>{cat.name}</Option>
<Option key={cat.id} value={cat.name}>
{cat.name}
</Option>
))}
</Select>
</Form.Item>
<Form.Item name="unit" label="单位" rules={[{ required: true, message: '请输入单位' }]} initialValue="个">
<Form.Item
name="unit"
label="单位"
rules={[{ required: true, message: '请输入单位' }]}
initialValue="个"
>
<Input placeholder="如: 个、盒、卷、箱" />
</Form.Item>
<Space style={{ width: '100%' }}>
<Form.Item name="currentStock" label="当前库存" rules={[{ required: true, message: '请输入当前库存' }]}>
<Form.Item
name="currentStock"
label="当前库存"
rules={[{ required: true, message: '请输入当前库存' }]}
>
<InputNumber min={0} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="minStock" label="最小库存" rules={[{ required: true, message: '请输入最小库存' }]}>
<Form.Item
name="minStock"
label="最小库存"
rules={[{ required: true, message: '请输入最小库存' }]}
>
<InputNumber min={0} style={{ width: '100%' }} />
</Form.Item>
<Form.Item label="最大库存">
<Space direction="vertical" style={{ width: '100%' }}>
<Checkbox
<Checkbox
checked={maxStockUnlimited}
onChange={(e) => {
onChange={e => {
setMaxStockUnlimited(e.target.checked);
if (e.target.checked) {
form.setFieldsValue({ maxStock: undefined });
@@ -615,7 +748,11 @@ function ConsumableManagement() {
无限制
</Checkbox>
{!maxStockUnlimited && (
<Form.Item name="maxStock" noStyle rules={[{ required: true, message: '请输入最大库存' }]}>
<Form.Item
name="maxStock"
noStyle
rules={[{ required: true, message: '请输入最大库存' }]}
>
<InputNumber min={0} style={{ width: '100%' }} placeholder="请输入最大库存" />
</Form.Item>
)}
@@ -623,7 +760,13 @@ function ConsumableManagement() {
</Form.Item>
</Space>
<Form.Item name="unitPrice" label="单价(元)">
<InputNumber min={0} step={0.01} precision={2} style={{ width: '100%' }} placeholder="请输入单价" />
<InputNumber
min={0}
step={0.01}
precision={2}
style={{ width: '100%' }}
placeholder="请输入单价"
/>
</Form.Item>
<Form.Item name="supplier" label="供应商">
<Input placeholder="请输入供应商" />
@@ -642,7 +785,9 @@ function ConsumableManagement() {
</Form.Item>
<Form.Item>
<Space>
<Button type="primary" htmlType="submit">{editingConsumable ? '更新' : '创建'}</Button>
<Button type="primary" htmlType="submit">
{editingConsumable ? '更新' : '创建'}
</Button>
<Button onClick={handleCancel}>取消</Button>
</Space>
</Form.Item>
@@ -659,20 +804,17 @@ function ConsumableManagement() {
<Space direction="vertical" style={{ width: '100%' }} size="middle">
<Card size="small" style={{ background: '#f5f5f5' }}>
<Space>
<Button icon={<FileExcelOutlined />} onClick={downloadTemplate}>下载模板</Button>
<Button icon={<FileExcelOutlined />} onClick={downloadTemplate}>
下载模板
</Button>
<span style={{ color: '#888', fontSize: 12 }}>请下载模板后填写数据再导入</span>
</Space>
</Card>
<Upload
accept=".csv"
maxCount={1}
beforeUpload={() => false}
onChange={handleFileChange}
>
<Upload accept=".csv" maxCount={1} beforeUpload={() => false} onChange={handleFileChange}>
<Button icon={<UploadOutlined />}>选择CSV文件</Button>
</Upload>
{importPreview.length > 0 && (
<div>
<div style={{ marginBottom: 8, fontWeight: 'bold' }}>预览 (前10条):</div>
@@ -686,13 +828,13 @@ function ConsumableManagement() {
/>
</div>
)}
<div style={{ textAlign: 'right' }}>
<Space>
<Button onClick={handleImportCancel}>取消</Button>
<Button
type="primary"
onClick={handleImport}
<Button
type="primary"
onClick={handleImport}
loading={importing}
disabled={!importFile}
>
@@ -717,7 +859,11 @@ function ConsumableManagement() {
<Form.Item name="consumableName" label="耗材名称">
<Input disabled />
</Form.Item>
<Form.Item name="quantity" label="数量" rules={[{ required: true, message: '请输入数量' }]}>
<Form.Item
name="quantity"
label="数量"
rules={[{ required: true, message: '请输入数量' }]}
>
<InputNumber min={1} style={{ width: '100%' }} placeholder="请输入数量" />
</Form.Item>
<Form.Item name="operator" label="操作人">
@@ -731,7 +877,9 @@ function ConsumableManagement() {
</Form.Item>
<Form.Item>
<Space>
<Button type="primary" htmlType="submit">{stockType === 'in' ? '确认入库' : '确认出库'}</Button>
<Button type="primary" htmlType="submit">
{stockType === 'in' ? '确认入库' : '确认出库'}
</Button>
<Button onClick={handleStockCancel}>取消</Button>
</Space>
</Form.Item>