feat(耗材管理): 增强扫码入库功能并优化UI样式

添加入库原因、操作人和备注字段到扫码入库功能
重构入库/出库弹窗的UI布局和交互设计
优化扫码弹窗的视觉体验和信息展示
This commit is contained in:
zhang1106
2026-04-16 16:54:10 +08:00
parent 01a05719dc
commit 7df6c73d7b
+403 -211
View File
@@ -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>
{/* 扫码快速出库到设备弹窗 */} {/* 扫码快速出库到设备弹窗 */}