feat(耗材管理): 增强扫码入库功能并优化UI样式
添加入库原因、操作人和备注字段到扫码入库功能 重构入库/出库弹窗的UI布局和交互设计 优化扫码弹窗的视觉体验和信息展示
This commit is contained in:
@@ -65,6 +65,8 @@ import {
|
|||||||
DownOutlined,
|
DownOutlined,
|
||||||
CheckSquareOutlined,
|
CheckSquareOutlined,
|
||||||
SettingOutlined,
|
SettingOutlined,
|
||||||
|
InboxOutlined,
|
||||||
|
UserOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import * as XLSX from 'xlsx';
|
import * as XLSX from 'xlsx';
|
||||||
@@ -163,6 +165,9 @@ function ConsumableManagement() {
|
|||||||
const [scanChecking, setScanChecking] = useState(false);
|
const [scanChecking, setScanChecking] = useState(false);
|
||||||
const [scannedSnList, setScannedSnList] = useState([]);
|
const [scannedSnList, setScannedSnList] = useState([]);
|
||||||
const [selectedScanInConsumable, setSelectedScanInConsumable] = useState(null);
|
const [selectedScanInConsumable, setSelectedScanInConsumable] = useState(null);
|
||||||
|
const [scanInOperator, setScanInOperator] = useState('');
|
||||||
|
const [scanInReason, setScanInReason] = useState('');
|
||||||
|
const [scanInNotes, setScanInNotes] = useState('');
|
||||||
const scanInputRef = React.useRef(null);
|
const scanInputRef = React.useRef(null);
|
||||||
const [quickOutModalVisible, setQuickOutModalVisible] = useState(false);
|
const [quickOutModalVisible, setQuickOutModalVisible] = useState(false);
|
||||||
const [quickOutConsumable, setQuickOutConsumable] = useState(null);
|
const [quickOutConsumable, setQuickOutConsumable] = useState(null);
|
||||||
@@ -1066,6 +1071,9 @@ function ConsumableManagement() {
|
|||||||
setScannedSnList([]);
|
setScannedSnList([]);
|
||||||
setSelectedScanInConsumable(null);
|
setSelectedScanInConsumable(null);
|
||||||
setScanChecking(false);
|
setScanChecking(false);
|
||||||
|
setScanInOperator('');
|
||||||
|
setScanInReason('');
|
||||||
|
setScanInNotes('');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const addToPendingOut = (consumable, snList = []) => {
|
const addToPendingOut = (consumable, snList = []) => {
|
||||||
@@ -1253,8 +1261,9 @@ function ConsumableManagement() {
|
|||||||
consumableId,
|
consumableId,
|
||||||
type: 'in',
|
type: 'in',
|
||||||
quantity: scannedSnList.length,
|
quantity: scannedSnList.length,
|
||||||
operator: '系统管理员',
|
operator: scanInOperator || '系统管理员',
|
||||||
reason: '扫码入库',
|
reason: scanInReason || '扫码入库',
|
||||||
|
notes: scanInNotes || '',
|
||||||
snList: scannedSnList,
|
snList: scannedSnList,
|
||||||
});
|
});
|
||||||
message.success({
|
message.success({
|
||||||
@@ -1268,7 +1277,7 @@ function ConsumableManagement() {
|
|||||||
console.error('入库失败:', error);
|
console.error('入库失败:', error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[scannedSnList, handleScanCancel, fetchConsumables]
|
[scannedSnList, scanInOperator, scanInReason, scanInNotes, handleScanCancel, fetchConsumables]
|
||||||
);
|
);
|
||||||
|
|
||||||
const searchDevices = useCallback(async keyword => {
|
const searchDevices = useCallback(async keyword => {
|
||||||
@@ -3920,68 +3929,135 @@ function ConsumableManagement() {
|
|||||||
|
|
||||||
{/* 入库/出库弹窗 */}
|
{/* 入库/出库弹窗 */}
|
||||||
<Modal
|
<Modal
|
||||||
title={
|
title={null}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: '36px',
|
|
||||||
height: '36px',
|
|
||||||
borderRadius: designTokens.borderRadius.md,
|
|
||||||
background:
|
|
||||||
stockType === 'in'
|
|
||||||
? designTokens.colors.success.gradient
|
|
||||||
: designTokens.colors.error.gradient,
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
color: '#fff',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{stockType === 'in' ? <ArrowDownOutlined /> : <ArrowUpOutlined />}
|
|
||||||
</div>
|
|
||||||
<span style={{ fontSize: '18px', fontWeight: 600 }}>
|
|
||||||
{stockType === 'in' ? '耗材入库' : '耗材出库'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
open={stockModalVisible}
|
open={stockModalVisible}
|
||||||
closeIcon={<CloseButton />}
|
closeIcon={<CloseButton />}
|
||||||
onCancel={handleStockCancel}
|
onCancel={handleStockCancel}
|
||||||
footer={null}
|
footer={null}
|
||||||
width={500}
|
width={560}
|
||||||
|
style={{ top: 80 }}
|
||||||
|
bodyStyle={{ padding: 0 }}
|
||||||
>
|
>
|
||||||
<Form
|
{/* 标题栏 */}
|
||||||
form={stockForm}
|
<div
|
||||||
layout="vertical"
|
style={{
|
||||||
onFinish={handleStockSubmit}
|
padding: '20px 24px 16px',
|
||||||
style={{ marginTop: '16px' }}
|
background: stockType === 'in'
|
||||||
|
? 'linear-gradient(135deg, #52c41a15 0%, #52c41a08 100%)'
|
||||||
|
: 'linear-gradient(135deg, #ff4d4f15 0%, #ff4d4f08 100%)',
|
||||||
|
borderBottom: `1px solid ${designTokens.colors.neutral[200]}`,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Form.Item name="consumableId" label="耗材ID">
|
<div style={{ display: 'flex', alignItems: 'center', gap: '14px' }}>
|
||||||
<Input disabled style={{ ...inputStyles.form, ...inputStyles.disabled }} />
|
<div
|
||||||
</Form.Item>
|
style={{
|
||||||
<Form.Item name="consumableName" label="耗材名称">
|
width: '48px',
|
||||||
<Input disabled style={{ ...inputStyles.form, ...inputStyles.disabled }} />
|
height: '48px',
|
||||||
</Form.Item>
|
borderRadius: designTokens.borderRadius.lg,
|
||||||
|
background: stockType === 'in'
|
||||||
{stockType === 'out' && stockRecord?.snList && stockRecord.snList.length > 0 && (
|
? designTokens.colors.success.gradient
|
||||||
<Form.Item
|
: designTokens.colors.error.gradient,
|
||||||
label={
|
display: 'flex',
|
||||||
<span>
|
alignItems: 'center',
|
||||||
选择SN序列号
|
justifyContent: 'center',
|
||||||
<Text type="secondary" style={{ fontSize: '12px', marginLeft: '8px' }}>
|
color: '#fff',
|
||||||
(已选 {selectedSnList.length} 个)
|
boxShadow: stockType === 'in'
|
||||||
</Text>
|
? `0 4px 16px ${designTokens.colors.success.main}50`
|
||||||
</span>
|
: `0 4px 16px ${designTokens.colors.error.main}50`,
|
||||||
}
|
}}
|
||||||
>
|
>
|
||||||
<Space direction="vertical" style={{ width: '100%' }} size="small">
|
{stockType === 'in' ? <ArrowDownOutlined style={{ fontSize: '22px' }} /> : <ArrowUpOutlined style={{ fontSize: '22px' }} />}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: '20px', fontWeight: 600, color: designTokens.colors.text.primary }}>
|
||||||
|
{stockType === 'in' ? '耗材入库' : '耗材出库'}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '13px', color: designTokens.colors.neutral[500], marginTop: '2px' }}>
|
||||||
|
{stockType === 'in' ? '填写入库信息,增加库存数量' : '填写出库信息,减少库存数量'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ padding: '20px 24px 24px' }}>
|
||||||
|
<Form
|
||||||
|
form={stockForm}
|
||||||
|
layout="vertical"
|
||||||
|
onFinish={handleStockSubmit}
|
||||||
|
>
|
||||||
|
{/* 耗材信息卡片 */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(135deg, ${designTokens.colors.primary.main}08 0%, ${designTokens.colors.primary.main}03 100%)`,
|
||||||
|
border: `1px solid ${designTokens.colors.primary.main}20`,
|
||||||
|
borderRadius: designTokens.borderRadius.md,
|
||||||
|
padding: '16px',
|
||||||
|
marginBottom: '20px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '12px' }}>
|
||||||
|
<InboxOutlined style={{ color: designTokens.colors.primary.main, fontSize: '16px' }} />
|
||||||
|
<span style={{ fontSize: '13px', fontWeight: 600, color: designTokens.colors.text.secondary }}>
|
||||||
|
耗材信息
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={12}>
|
||||||
|
<div style={{ marginBottom: '8px' }}>
|
||||||
|
<div style={{ fontSize: '12px', color: designTokens.colors.neutral[500], marginBottom: '2px' }}>耗材ID</div>
|
||||||
|
<div style={{ fontSize: '14px', fontWeight: 500, color: designTokens.colors.text.primary, fontFamily: 'monospace' }}>
|
||||||
|
<Form.Item name="consumableId" style={{ margin: 0 }}>
|
||||||
|
<Input disabled style={{ ...inputStyles.form, ...inputStyles.disabled, background: 'transparent', border: 'none', padding: 0 }} />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<div style={{ marginBottom: '8px' }}>
|
||||||
|
<div style={{ fontSize: '12px', color: designTokens.colors.neutral[500], marginBottom: '2px' }}>耗材名称</div>
|
||||||
|
<div style={{ fontSize: '14px', fontWeight: 500, color: designTokens.colors.text.primary }}>
|
||||||
|
<Form.Item name="consumableName" style={{ margin: 0 }}>
|
||||||
|
<Input disabled style={{ ...inputStyles.form, ...inputStyles.disabled, background: 'transparent', border: 'none', padding: 0 }} />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* SN序列号选择区(出库) */}
|
||||||
|
{stockType === 'out' && stockRecord?.snList && stockRecord.snList.length > 0 && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: designTokens.colors.neutral[50],
|
||||||
|
border: `1px solid ${designTokens.colors.neutral[200]}`,
|
||||||
|
borderRadius: designTokens.borderRadius.md,
|
||||||
|
padding: '16px',
|
||||||
|
marginBottom: '20px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '12px' }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||||
|
<BarcodeOutlined style={{ color: designTokens.colors.neutral[600], fontSize: '14px' }} />
|
||||||
|
<span style={{ fontSize: '13px', fontWeight: 600, color: designTokens.colors.text.secondary }}>
|
||||||
|
SN序列号选择
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Badge
|
||||||
|
count={`已选 ${selectedSnList.length}`}
|
||||||
|
style={{
|
||||||
|
backgroundColor: selectedSnList.length > 0 ? designTokens.colors.success.main : designTokens.colors.neutral[400],
|
||||||
|
fontSize: '11px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<Input
|
<Input
|
||||||
placeholder="搜索SN序列号..."
|
placeholder="搜索SN序列号..."
|
||||||
prefix={<SearchOutlined />}
|
prefix={<SearchOutlined />}
|
||||||
allowClear
|
allowClear
|
||||||
value={snSearchKeyword}
|
value={snSearchKeyword}
|
||||||
onChange={e => setSnSearchKeyword(e.target.value)}
|
onChange={e => setSnSearchKeyword(e.target.value)}
|
||||||
style={{ ...inputStyles.search, marginBottom: '8px' }}
|
style={{ ...inputStyles.search, marginBottom: '12px' }}
|
||||||
/>
|
/>
|
||||||
<Space size="small" style={{ marginBottom: '8px' }}>
|
<Space size="small" style={{ marginBottom: '8px' }}>
|
||||||
<Button
|
<Button
|
||||||
@@ -4012,8 +4088,9 @@ function ConsumableManagement() {
|
|||||||
</Space>
|
</Space>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
maxHeight: '150px',
|
maxHeight: '140px',
|
||||||
overflowY: 'auto',
|
overflowY: 'auto',
|
||||||
|
background: '#fff',
|
||||||
border: `1px solid ${designTokens.colors.neutral[200]}`,
|
border: `1px solid ${designTokens.colors.neutral[200]}`,
|
||||||
borderRadius: designTokens.borderRadius.sm,
|
borderRadius: designTokens.borderRadius.sm,
|
||||||
padding: '8px',
|
padding: '8px',
|
||||||
@@ -4026,10 +4103,7 @@ function ConsumableManagement() {
|
|||||||
);
|
);
|
||||||
if (filteredSnList.length === 0) {
|
if (filteredSnList.length === 0) {
|
||||||
return (
|
return (
|
||||||
<Text
|
<Text type="secondary" style={{ display: 'block', textAlign: 'center', padding: '16px 0' }}>
|
||||||
type="secondary"
|
|
||||||
style={{ display: 'block', textAlign: 'center', padding: '16px 0' }}
|
|
||||||
>
|
|
||||||
无匹配的 SN
|
无匹配的 SN
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
@@ -4055,86 +4129,165 @@ function ConsumableManagement() {
|
|||||||
));
|
));
|
||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
<Text type="secondary" style={{ fontSize: '11px', marginTop: '8px', display: 'block' }}>
|
||||||
{snSearchKeyword
|
{snSearchKeyword
|
||||||
? `过滤结果: ${stockRecord.snList.filter(sn => sn.toLowerCase().includes(snSearchKeyword.toLowerCase())).length} 个SN`
|
? `过滤结果: ${stockRecord.snList.filter(sn => sn.toLowerCase().includes(snSearchKeyword.toLowerCase())).length} 个SN`
|
||||||
: `共 ${stockRecord.snList.length} 个SN`}
|
: `共 ${stockRecord.snList.length} 个SN`}
|
||||||
,点击SN进行选择
|
|
||||||
</Text>
|
</Text>
|
||||||
</Space>
|
</div>
|
||||||
</Form.Item>
|
)}
|
||||||
)}
|
|
||||||
|
|
||||||
{stockType === 'in' && (
|
{/* 入库SN输入区 */}
|
||||||
<Form.Item name="snList" label="入库SN序列号(可选)">
|
{stockType === 'in' && (
|
||||||
<Select
|
<div style={{ marginBottom: '20px' }}>
|
||||||
mode="tags"
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}>
|
||||||
style={selectStyles.base}
|
<BarcodeOutlined style={{ color: designTokens.colors.neutral[600], fontSize: '14px' }} />
|
||||||
placeholder={inputPlaceholders.snList}
|
<span style={{ fontSize: '13px', fontWeight: 500, color: designTokens.colors.text.secondary }}>
|
||||||
tokenSeparators={[',', '\n']}
|
入库SN序列号
|
||||||
/>
|
</span>
|
||||||
</Form.Item>
|
<Text type="secondary" style={{ fontSize: '12px' }}>(可选)</Text>
|
||||||
)}
|
</div>
|
||||||
|
<Form.Item name="snList" style={{ margin: 0 }}>
|
||||||
|
<Select
|
||||||
|
mode="tags"
|
||||||
|
style={{ ...selectStyles.base, maxHeight: '120px', overflowY: 'auto' }}
|
||||||
|
placeholder="输入SN后按回车添加,支持批量粘贴"
|
||||||
|
tokenSeparators={[',', '\n']}
|
||||||
|
maxTagCount="responsive"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<Form.Item
|
{/* 操作信息区 */}
|
||||||
name="quantity"
|
<div
|
||||||
label="数量"
|
style={{
|
||||||
rules={[inputValidationRules.required('请输入数量')]}
|
background: designTokens.colors.neutral[50],
|
||||||
>
|
borderRadius: designTokens.borderRadius.md,
|
||||||
<InputNumber
|
padding: '16px',
|
||||||
min={1}
|
marginBottom: '20px',
|
||||||
max={
|
}}
|
||||||
stockType === 'out' && stockRecord?.snList?.length > 0
|
>
|
||||||
? stockRecord.snList.length
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '16px' }}>
|
||||||
: undefined
|
<InfoCircleOutlined style={{ color: designTokens.colors.neutral[600], fontSize: '14px' }} />
|
||||||
}
|
<span style={{ fontSize: '13px', fontWeight: 600, color: designTokens.colors.text.secondary }}>
|
||||||
style={inputNumberStyles.base}
|
操作信息
|
||||||
placeholder="请输入数量"
|
</span>
|
||||||
/>
|
</div>
|
||||||
</Form.Item>
|
|
||||||
<Form.Item name="operator" label="操作人">
|
<Row gutter={12}>
|
||||||
<Input placeholder={inputPlaceholders.operator} style={inputStyles.form} />
|
<Col span={12}>
|
||||||
</Form.Item>
|
<Form.Item
|
||||||
<Form.Item name="reason" label={stockType === 'in' ? '入库原因' : '出库原因'}>
|
name="quantity"
|
||||||
<Input
|
label="数量"
|
||||||
placeholder={stockType === 'in' ? '如: 采购入库' : '如: 部门领用、报损出库'}
|
rules={[inputValidationRules.required('请输入数量')]}
|
||||||
style={inputStyles.form}
|
style={{ marginBottom: '16px' }}
|
||||||
/>
|
>
|
||||||
</Form.Item>
|
<InputNumber
|
||||||
<Form.Item name="notes" label="备注">
|
min={1}
|
||||||
<TextArea rows={2} placeholder={inputPlaceholders.notes} style={textAreaStyles.base} />
|
max={
|
||||||
</Form.Item>
|
stockType === 'out' && stockRecord?.snList?.length > 0
|
||||||
<Form.Item>
|
? stockRecord.snList.length
|
||||||
<Space>
|
: undefined
|
||||||
<Button
|
}
|
||||||
type="primary"
|
style={{ ...inputNumberStyles.base, width: '100%' }}
|
||||||
htmlType="submit"
|
placeholder="请输入数量"
|
||||||
style={{
|
/>
|
||||||
background:
|
</Form.Item>
|
||||||
stockType === 'in'
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item name="operator" label="操作人" style={{ marginBottom: '16px' }}>
|
||||||
|
<Input prefix={<UserOutlined style={{ color: designTokens.colors.neutral[400] }} />} placeholder={inputPlaceholders.operator} style={inputStyles.form} />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Form.Item name="reason" label={stockType === 'in' ? '入库原因' : '出库原因'} style={{ marginBottom: '16px' }}>
|
||||||
|
<Input
|
||||||
|
prefix={<FileTextOutlined style={{ color: designTokens.colors.neutral[400] }} />}
|
||||||
|
placeholder={stockType === 'in' ? '如: 采购入库、退还入库' : '如: 部门领用、报损出库'}
|
||||||
|
style={inputStyles.form}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item name="notes" label="备注">
|
||||||
|
<TextArea rows={2} placeholder="请输入备注信息(可选)" style={textAreaStyles.base} />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 操作按钮 */}
|
||||||
|
<Form.Item style={{ marginBottom: 0 }}>
|
||||||
|
<div style={{ display: 'flex', gap: '12px', justifyContent: 'flex-end' }}>
|
||||||
|
<Button
|
||||||
|
onClick={handleStockCancel}
|
||||||
|
style={{
|
||||||
|
height: '44px',
|
||||||
|
borderRadius: designTokens.borderRadius.md,
|
||||||
|
paddingLeft: '24px',
|
||||||
|
paddingRight: '24px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
htmlType="submit"
|
||||||
|
style={{
|
||||||
|
background: stockType === 'in'
|
||||||
? designTokens.colors.success.gradient
|
? designTokens.colors.success.gradient
|
||||||
: designTokens.colors.error.gradient,
|
: designTokens.colors.error.gradient,
|
||||||
border: 'none',
|
border: 'none',
|
||||||
borderRadius: designTokens.borderRadius.sm,
|
borderRadius: designTokens.borderRadius.md,
|
||||||
}}
|
height: '44px',
|
||||||
>
|
paddingLeft: '32px',
|
||||||
{stockType === 'in' ? '确认入库' : '确认出库'}
|
paddingRight: '32px',
|
||||||
</Button>
|
fontSize: '15px',
|
||||||
<Button onClick={handleStockCancel}>取消</Button>
|
fontWeight: 500,
|
||||||
</Space>
|
boxShadow: stockType === 'in'
|
||||||
</Form.Item>
|
? `0 4px 12px ${designTokens.colors.success.main}40`
|
||||||
</Form>
|
: `0 4px 12px ${designTokens.colors.error.main}40`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Space>
|
||||||
|
{stockType === 'in' ? <ArrowDownOutlined /> : <ArrowUpOutlined />}
|
||||||
|
{stockType === 'in' ? '确认入库' : '确认出库'}
|
||||||
|
</Space>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
{/* 扫码弹窗 - 支持入库/出库/添加SN三种模式 */}
|
{/* 扫码弹窗 - 支持入库/出库/添加SN三种模式 */}
|
||||||
<Modal
|
<Modal
|
||||||
title={
|
title={null}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
open={scanModalVisible}
|
||||||
|
closeIcon={<CloseButton />}
|
||||||
|
onCancel={handleScanCancel}
|
||||||
|
footer={null}
|
||||||
|
width={560}
|
||||||
|
style={{ top: 80 }}
|
||||||
|
bodyStyle={{ padding: 0 }}
|
||||||
|
>
|
||||||
|
{/* 标题栏 */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: '20px 24px 16px',
|
||||||
|
background: scanMode === 'in'
|
||||||
|
? 'linear-gradient(135deg, #52c41a15 0%, #52c41a08 100%)'
|
||||||
|
: scanMode === 'out'
|
||||||
|
? 'linear-gradient(135deg, #ff4d4f15 0%, #ff4d4f08 100%)'
|
||||||
|
: 'linear-gradient(135deg, #1890ff15 0%, #1890ff08 100%)',
|
||||||
|
borderBottom: `1px solid ${designTokens.colors.neutral[200]}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '14px' }}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: '40px',
|
width: '48px',
|
||||||
height: '40px',
|
height: '48px',
|
||||||
borderRadius: designTokens.borderRadius.md,
|
borderRadius: designTokens.borderRadius.lg,
|
||||||
background: scanMode === 'in'
|
background: scanMode === 'in'
|
||||||
? designTokens.colors.success.gradient
|
? designTokens.colors.success.gradient
|
||||||
: scanMode === 'out'
|
: scanMode === 'out'
|
||||||
@@ -4145,108 +4298,100 @@ function ConsumableManagement() {
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
boxShadow: scanMode === 'in'
|
boxShadow: scanMode === 'in'
|
||||||
? `0 4px 12px ${designTokens.colors.success.main}40`
|
? `0 4px 16px ${designTokens.colors.success.main}50`
|
||||||
: scanMode === 'out'
|
: scanMode === 'out'
|
||||||
? `0 4px 12px ${designTokens.colors.error.main}40`
|
? `0 4px 16px ${designTokens.colors.error.main}50`
|
||||||
: `0 4px 12px ${designTokens.colors.primary.main}40`,
|
: `0 4px 16px ${designTokens.colors.primary.main}50`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{scanMode === 'in' ? <ArrowDownOutlined style={{ fontSize: '20px' }} /> :
|
{scanMode === 'in' ? <ArrowDownOutlined style={{ fontSize: '22px' }} /> :
|
||||||
scanMode === 'out' ? <ArrowUpOutlined style={{ fontSize: '20px' }} /> :
|
scanMode === 'out' ? <ArrowUpOutlined style={{ fontSize: '22px' }} /> :
|
||||||
<BarcodeOutlined style={{ fontSize: '20px' }} />}
|
<BarcodeOutlined style={{ fontSize: '22px' }} />}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span style={{ fontSize: '18px', fontWeight: 600, color: designTokens.colors.text.primary }}>
|
<div style={{ fontSize: '20px', fontWeight: 600, color: designTokens.colors.text.primary }}>
|
||||||
{scanMode === 'in' ? '扫码入库' : scanMode === 'out' ? '扫码出库' : '扫码添加SN'}
|
{scanMode === 'in' ? '扫码入库' : scanMode === 'out' ? '扫码出库' : '扫码添加SN'}
|
||||||
</span>
|
</div>
|
||||||
<div style={{ fontSize: '12px', color: designTokens.colors.neutral[500], marginTop: '2px' }}>
|
<div style={{ fontSize: '13px', color: designTokens.colors.neutral[500], marginTop: '2px' }}>
|
||||||
{scanMode === 'in' ? '扫描 SN 序列号快速入库' :
|
{scanMode === 'in' ? '扫描 SN 序列号快速入库' :
|
||||||
scanMode === 'out' ? '扫描 SN 序列号快速出库' :
|
scanMode === 'out' ? '扫描 SN 序列号快速出库' :
|
||||||
'扫描 SN 序列号添加到表单'}
|
'扫描 SN 序列号添加到表单'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
open={scanModalVisible}
|
|
||||||
closeIcon={<CloseButton />}
|
|
||||||
onCancel={handleScanCancel}
|
|
||||||
footer={null}
|
|
||||||
width={560}
|
|
||||||
bodyStyle={{ padding: '0 24px 24px' }}
|
|
||||||
style={{ top: 60 }}
|
|
||||||
>
|
|
||||||
{/* 统计卡片区 - 根据模式显示不同内容 */}
|
|
||||||
<div style={{ marginBottom: '24px', marginTop: '-8px' }}>
|
|
||||||
{scanMode === 'out' ? (
|
|
||||||
/* 出库模式:显示待出库列表 */
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
background: `linear-gradient(135deg, ${designTokens.colors.error.main}10 0%, ${designTokens.colors.error.main}05 100%)`,
|
|
||||||
border: `1px solid ${designTokens.colors.error.main}30`,
|
|
||||||
borderRadius: designTokens.borderRadius.md,
|
|
||||||
padding: '20px',
|
|
||||||
textAlign: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ fontSize: '32px', fontWeight: 700, color: designTokens.colors.error.main, lineHeight: 1 }}>
|
|
||||||
{pendingOutItems.length}
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: '13px', color: designTokens.colors.neutral[600], marginTop: '6px' }}>
|
|
||||||
待出库耗材项
|
|
||||||
</div>
|
|
||||||
{pendingOutItems.length > 0 && (
|
|
||||||
<div style={{ marginTop: '12px', paddingTop: '12px', borderTop: `1px dashed ${designTokens.colors.error.main}30` }}>
|
|
||||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
|
||||||
共 {pendingOutItems.reduce((sum, item) => sum + item.quantity, 0)} 件耗材
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
/* 入库/添加SN模式:显示已扫描数量 */
|
|
||||||
<Row gutter={12}>
|
|
||||||
<Col span={12}>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
background: `linear-gradient(135deg, ${designTokens.colors.success.main}15 0%, ${designTokens.colors.success.main}05 100%)`,
|
|
||||||
border: `1px solid ${designTokens.colors.success.main}30`,
|
|
||||||
borderRadius: designTokens.borderRadius.md,
|
|
||||||
padding: '16px',
|
|
||||||
textAlign: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ fontSize: '28px', fontWeight: 700, color: designTokens.colors.success.main, lineHeight: 1 }}>
|
|
||||||
{(scanMode === 'in' ? scannedSnList : snList).length}
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: '12px', color: designTokens.colors.neutral[500], marginTop: '4px' }}>
|
|
||||||
已扫描SN
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
<Col span={12}>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
background: `linear-gradient(135deg, ${designTokens.colors.primary.main}15 0%, ${designTokens.colors.primary.main}05 100%)`,
|
|
||||||
border: `1px solid ${designTokens.colors.primary.main}30`,
|
|
||||||
borderRadius: designTokens.borderRadius.md,
|
|
||||||
padding: '16px',
|
|
||||||
textAlign: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ fontSize: '28px', fontWeight: 700, color: designTokens.colors.primary.main, lineHeight: 1 }}>
|
|
||||||
{allConsumablesForScan.length}
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: '12px', color: designTokens.colors.neutral[500], marginTop: '4px' }}>
|
|
||||||
{scanMode === 'in' ? '可入库耗材' : '可选耗材'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 扫码动画输入区 */}
|
<div style={{ padding: '20px 24px 24px' }}>
|
||||||
<div style={{ marginBottom: '20px' }}>
|
{/* 统计卡片区 - 根据模式显示不同内容 */}
|
||||||
|
<div style={{ marginBottom: '20px' }}>
|
||||||
|
{scanMode === 'out' ? (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(135deg, ${designTokens.colors.error.main}10 0%, ${designTokens.colors.error.main}05 100%)`,
|
||||||
|
border: `1px solid ${designTokens.colors.error.main}30`,
|
||||||
|
borderRadius: designTokens.borderRadius.md,
|
||||||
|
padding: '16px',
|
||||||
|
textAlign: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ fontSize: '28px', fontWeight: 700, color: designTokens.colors.error.main, lineHeight: 1 }}>
|
||||||
|
{pendingOutItems.length}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '12px', color: designTokens.colors.neutral[600], marginTop: '4px' }}>
|
||||||
|
待出库耗材项
|
||||||
|
</div>
|
||||||
|
{pendingOutItems.length > 0 && (
|
||||||
|
<div style={{ marginTop: '10px', paddingTop: '10px', borderTop: `1px dashed ${designTokens.colors.error.main}30` }}>
|
||||||
|
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||||
|
共 {pendingOutItems.reduce((sum, item) => sum + item.quantity, 0)} 件耗材
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Row gutter={12}>
|
||||||
|
<Col span={12}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(135deg, ${designTokens.colors.success.main}15 0%, ${designTokens.colors.success.main}05 100%)`,
|
||||||
|
border: `1px solid ${designTokens.colors.success.main}30`,
|
||||||
|
borderRadius: designTokens.borderRadius.md,
|
||||||
|
padding: '14px',
|
||||||
|
textAlign: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ fontSize: '26px', fontWeight: 700, color: designTokens.colors.success.main, lineHeight: 1 }}>
|
||||||
|
{(scanMode === 'in' ? scannedSnList : snList).length}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '12px', color: designTokens.colors.neutral[500], marginTop: '4px' }}>
|
||||||
|
已扫描SN
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(135deg, ${designTokens.colors.primary.main}15 0%, ${designTokens.colors.primary.main}05 100%)`,
|
||||||
|
border: `1px solid ${designTokens.colors.primary.main}30`,
|
||||||
|
borderRadius: designTokens.borderRadius.md,
|
||||||
|
padding: '14px',
|
||||||
|
textAlign: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ fontSize: '26px', fontWeight: 700, color: designTokens.colors.primary.main, lineHeight: 1 }}>
|
||||||
|
{allConsumablesForScan.length}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '12px', color: designTokens.colors.neutral[500], marginTop: '4px' }}>
|
||||||
|
{scanMode === 'in' ? '可入库耗材' : '可选耗材'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 扫码动画输入区 */}
|
||||||
|
<div style={{ marginBottom: '20px' }}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
@@ -4434,6 +4579,52 @@ function ConsumableManagement() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 入库模式:入库原因、操作人、备注 */}
|
||||||
|
{scanMode === 'in' && (
|
||||||
|
<div style={{ marginBottom: '16px' }}>
|
||||||
|
<Row gutter={12}>
|
||||||
|
<Col span={12}>
|
||||||
|
<div style={{ marginBottom: '12px' }}>
|
||||||
|
<div style={{ fontSize: '13px', fontWeight: 500, color: designTokens.colors.neutral[600], marginBottom: '4px' }}>
|
||||||
|
入库原因
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
placeholder="如: 采购入库、退还入库"
|
||||||
|
value={scanInReason}
|
||||||
|
onChange={e => setScanInReason(e.target.value)}
|
||||||
|
style={{ borderRadius: designTokens.borderRadius.sm }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<div style={{ marginBottom: '12px' }}>
|
||||||
|
<div style={{ fontSize: '13px', fontWeight: 500, color: designTokens.colors.neutral[600], marginBottom: '4px' }}>
|
||||||
|
操作人
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
placeholder="请输入操作人"
|
||||||
|
value={scanInOperator}
|
||||||
|
onChange={e => setScanInOperator(e.target.value)}
|
||||||
|
style={{ borderRadius: designTokens.borderRadius.sm }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: '13px', fontWeight: 500, color: designTokens.colors.neutral[600], marginBottom: '4px' }}>
|
||||||
|
备注
|
||||||
|
</div>
|
||||||
|
<Input.TextArea
|
||||||
|
placeholder="请输入备注信息(可选)"
|
||||||
|
value={scanInNotes}
|
||||||
|
onChange={e => setScanInNotes(e.target.value)}
|
||||||
|
rows={2}
|
||||||
|
style={{ borderRadius: designTokens.borderRadius.sm }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 入库/添加SN模式:SN列表网格 */}
|
{/* 入库/添加SN模式:SN列表网格 */}
|
||||||
{scanMode !== 'out' && (scanMode === 'in' ? scannedSnList : snList).length > 0 && (
|
{scanMode !== 'out' && (scanMode === 'in' ? scannedSnList : snList).length > 0 && (
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -4683,6 +4874,7 @@ function ConsumableManagement() {
|
|||||||
'扫描 SN 序列号自动添加到表单中'}
|
'扫描 SN 序列号自动添加到表单中'}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
{/* 扫码快速出库到设备弹窗 */}
|
{/* 扫码快速出库到设备弹窗 */}
|
||||||
|
|||||||
Reference in New Issue
Block a user