refactor: 统一代码风格并迁移至 ESLint 新配置
style(backend): 格式化模型文件代码 style(frontend): 调整组件代码格式 chore: 删除旧 ESLint 配置并添加新配置 refactor(backend): 重构模型定义语法 style: 统一箭头函数和对象属性简写
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -209,12 +209,21 @@ function CategoryManagement() {
|
||||
allowClear
|
||||
prefix={<SearchOutlined />}
|
||||
/>
|
||||
<Select value={status} onChange={setStatus} style={{ ...selectStyles.base, width: 120 }}>
|
||||
<Select
|
||||
value={status}
|
||||
onChange={setStatus}
|
||||
style={{ ...selectStyles.base, width: 120 }}
|
||||
>
|
||||
<Option value="all">所有状态</Option>
|
||||
<Option value="active">启用</Option>
|
||||
<Option value="inactive">停用</Option>
|
||||
</Select>
|
||||
<Button onClick={() => fetchCategories()} style={{ height: '40px', borderRadius: '10px' }}>刷新</Button>
|
||||
<Button
|
||||
onClick={() => fetchCategories()}
|
||||
style={{ height: '40px', borderRadius: '10px' }}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
@@ -249,12 +258,22 @@ function CategoryManagement() {
|
||||
<Input placeholder="请输入分类名称" style={inputStyles.form} />
|
||||
</Form.Item>
|
||||
<Form.Item name="description" label="描述">
|
||||
<Input.TextArea rows={3} placeholder={inputPlaceholders.description} maxLength={200} showCount style={textAreaStyles.base} />
|
||||
<Input.TextArea
|
||||
rows={3}
|
||||
placeholder={inputPlaceholders.description}
|
||||
maxLength={200}
|
||||
showCount
|
||||
style={textAreaStyles.base}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="sortOrder" label="排序">
|
||||
<InputNumber min={0} placeholder="数值越小越靠前" style={inputNumberStyles.base} />
|
||||
</Form.Item>
|
||||
<Form.Item name="status" label="状态" rules={[inputValidationRules.required('请选择状态')]}>
|
||||
<Form.Item
|
||||
name="status"
|
||||
label="状态"
|
||||
rules={[inputValidationRules.required('请选择状态')]}
|
||||
>
|
||||
<Select style={selectStyles.base}>
|
||||
<Option value="active">启用</Option>
|
||||
<Option value="inactive">停用</Option>
|
||||
@@ -262,10 +281,16 @@ function CategoryManagement() {
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Space>
|
||||
<Button type="primary" htmlType="submit" style={{ height: '40px', borderRadius: '10px' }}>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
style={{ height: '40px', borderRadius: '10px' }}
|
||||
>
|
||||
{editingCategory ? '更新' : '创建'}
|
||||
</Button>
|
||||
<Button onClick={handleCancel} style={{ height: '40px', borderRadius: '10px' }}>取消</Button>
|
||||
<Button onClick={handleCancel} style={{ height: '40px', borderRadius: '10px' }}>
|
||||
取消
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
@@ -84,7 +84,11 @@ function ConsumableLogs() {
|
||||
setLoading(true);
|
||||
const params = { page, pageSize };
|
||||
|
||||
if (currentFilters.operationType && currentFilters.operationType !== 'all' && currentFilters.operationType.length > 0) {
|
||||
if (
|
||||
currentFilters.operationType &&
|
||||
currentFilters.operationType !== 'all' &&
|
||||
currentFilters.operationType.length > 0
|
||||
) {
|
||||
params.operationType = currentFilters.operationType.join(',');
|
||||
}
|
||||
if (currentFilters.consumableId) {
|
||||
@@ -169,7 +173,9 @@ function ConsumableLogs() {
|
||||
<Space direction="vertical" size={0}>
|
||||
<span>{value}</span>
|
||||
{record.isConsumableDeleted && (
|
||||
<Tag color="red" size="small">已删除</Tag>
|
||||
<Tag color="red" size="small">
|
||||
已删除
|
||||
</Tag>
|
||||
)}
|
||||
</Space>
|
||||
</Tooltip>
|
||||
@@ -288,13 +294,15 @@ function ConsumableLogs() {
|
||||
width: 200,
|
||||
render: value => (
|
||||
<Tooltip title={value || '-'}>
|
||||
<span style={{
|
||||
display: 'inline-block',
|
||||
maxWidth: '180px',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
}}>
|
||||
<span
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
maxWidth: '180px',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{value || '-'}
|
||||
</span>
|
||||
</Tooltip>
|
||||
@@ -648,7 +656,11 @@ function ConsumableLogs() {
|
||||
导出 <DownOutlined />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
<Button icon={<UploadOutlined />} onClick={() => setImportModalVisible(true)} style={{ height: '40px', borderRadius: '10px' }}>
|
||||
<Button
|
||||
icon={<UploadOutlined />}
|
||||
onClick={() => setImportModalVisible(true)}
|
||||
style={{ height: '40px', borderRadius: '10px' }}
|
||||
>
|
||||
导入
|
||||
</Button>
|
||||
</Space>
|
||||
@@ -735,17 +747,29 @@ function ConsumableLogs() {
|
||||
>
|
||||
<Form form={form} layout="vertical" onFinish={handleEditSubmit}>
|
||||
<Form.Item label="操作原因" name="reason">
|
||||
<Input.TextArea rows={2} placeholder={inputPlaceholders.reason} style={textAreaStyles.base} />
|
||||
<Input.TextArea
|
||||
rows={2}
|
||||
placeholder={inputPlaceholders.reason}
|
||||
style={textAreaStyles.base}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="备注" name="notes">
|
||||
<Input.TextArea rows={3} placeholder={inputPlaceholders.notes} style={textAreaStyles.base} />
|
||||
<Input.TextArea
|
||||
rows={3}
|
||||
placeholder={inputPlaceholders.notes}
|
||||
style={textAreaStyles.base}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="修改原因"
|
||||
name="modificationReason"
|
||||
rules={[inputValidationRules.required('请输入修改原因')]}
|
||||
>
|
||||
<Input.TextArea rows={2} placeholder="请输入修改原因(必填)" style={textAreaStyles.base} />
|
||||
<Input.TextArea
|
||||
rows={2}
|
||||
placeholder="请输入修改原因(必填)"
|
||||
style={textAreaStyles.base}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="修改人" name="operator">
|
||||
<Input placeholder={inputPlaceholders.operator} style={inputStyles.form} />
|
||||
@@ -788,7 +812,11 @@ function ConsumableLogs() {
|
||||
<div style={{ fontSize: 12, color: '#666' }}>
|
||||
<p>
|
||||
<strong>耗材:</strong> {item.consumableName} ({item.consumableId})
|
||||
{item.isConsumableDeleted && <Tag color="red" style={{ marginLeft: 8 }}>已删除</Tag>}
|
||||
{item.isConsumableDeleted && (
|
||||
<Tag color="red" style={{ marginLeft: 8 }}>
|
||||
已删除
|
||||
</Tag>
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
<strong>操作人:</strong> {item.operator}
|
||||
@@ -806,8 +834,8 @@ function ConsumableLogs() {
|
||||
{item.consumableSnapshot && (
|
||||
<p>
|
||||
<strong>快照信息:</strong> 分类:{item.consumableSnapshot.category || '-'} |
|
||||
单位:{item.consumableSnapshot.unit || '-'} |
|
||||
单价:{item.consumableSnapshot.unitPrice || '-'}
|
||||
单位:{item.consumableSnapshot.unit || '-'} | 单价:
|
||||
{item.consumableSnapshot.unitPrice || '-'}
|
||||
</p>
|
||||
)}
|
||||
{item.modifiedBy && (
|
||||
@@ -854,12 +882,25 @@ function ConsumableLogs() {
|
||||
) : (
|
||||
<div>
|
||||
<Card size="small" title="基本信息" style={{ marginBottom: 16 }}>
|
||||
<p><strong>归档ID:</strong> <code>{currentArchive.archiveId}</code></p>
|
||||
<p><strong>耗材ID:</strong> <code>{currentArchive.consumableId}</code></p>
|
||||
<p><strong>耗材名称:</strong> {currentArchive.consumableName}</p>
|
||||
<p><strong>删除人:</strong> {currentArchive.deletedBy}</p>
|
||||
<p><strong>删除时间:</strong> {dayjs(currentArchive.deletedAt).format('YYYY-MM-DD HH:mm:ss')}</p>
|
||||
<p><strong>删除原因:</strong> {currentArchive.deleteReason || '-'}</p>
|
||||
<p>
|
||||
<strong>归档ID:</strong> <code>{currentArchive.archiveId}</code>
|
||||
</p>
|
||||
<p>
|
||||
<strong>耗材ID:</strong> <code>{currentArchive.consumableId}</code>
|
||||
</p>
|
||||
<p>
|
||||
<strong>耗材名称:</strong> {currentArchive.consumableName}
|
||||
</p>
|
||||
<p>
|
||||
<strong>删除人:</strong> {currentArchive.deletedBy}
|
||||
</p>
|
||||
<p>
|
||||
<strong>删除时间:</strong>{' '}
|
||||
{dayjs(currentArchive.deletedAt).format('YYYY-MM-DD HH:mm:ss')}
|
||||
</p>
|
||||
<p>
|
||||
<strong>删除原因:</strong> {currentArchive.deleteReason || '-'}
|
||||
</p>
|
||||
</Card>
|
||||
|
||||
<Card size="small" title="操作统计" style={{ marginBottom: 16 }}>
|
||||
@@ -875,20 +916,46 @@ function ConsumableLogs() {
|
||||
</Col>
|
||||
</Row>
|
||||
<Divider style={{ margin: '12px 0' }} />
|
||||
<p><strong>首次操作:</strong> {currentArchive.firstOperationAt ? dayjs(currentArchive.firstOperationAt).format('YYYY-MM-DD HH:mm:ss') : '-'}</p>
|
||||
<p><strong>最后操作:</strong> {currentArchive.lastOperationAt ? dayjs(currentArchive.lastOperationAt).format('YYYY-MM-DD HH:mm:ss') : '-'}</p>
|
||||
<p><strong>删除时库存:</strong> {currentArchive.finalStock}</p>
|
||||
<p>
|
||||
<strong>首次操作:</strong>{' '}
|
||||
{currentArchive.firstOperationAt
|
||||
? dayjs(currentArchive.firstOperationAt).format('YYYY-MM-DD HH:mm:ss')
|
||||
: '-'}
|
||||
</p>
|
||||
<p>
|
||||
<strong>最后操作:</strong>{' '}
|
||||
{currentArchive.lastOperationAt
|
||||
? dayjs(currentArchive.lastOperationAt).format('YYYY-MM-DD HH:mm:ss')
|
||||
: '-'}
|
||||
</p>
|
||||
<p>
|
||||
<strong>删除时库存:</strong> {currentArchive.finalStock}
|
||||
</p>
|
||||
</Card>
|
||||
|
||||
{currentArchive.consumableSnapshot && (
|
||||
<Card size="small" title="耗材快照">
|
||||
<p><strong>分类:</strong> {currentArchive.consumableSnapshot.category || '-'}</p>
|
||||
<p><strong>单位:</strong> {currentArchive.consumableSnapshot.unit || '-'}</p>
|
||||
<p><strong>单价:</strong> {currentArchive.consumableSnapshot.unitPrice || '-'}</p>
|
||||
<p><strong>供应商:</strong> {currentArchive.consumableSnapshot.supplier || '-'}</p>
|
||||
<p><strong>位置:</strong> {currentArchive.consumableSnapshot.location || '-'}</p>
|
||||
<p><strong>最小库存:</strong> {currentArchive.consumableSnapshot.minStock || '-'}</p>
|
||||
<p><strong>最大库存:</strong> {currentArchive.consumableSnapshot.maxStock || '-'}</p>
|
||||
<p>
|
||||
<strong>分类:</strong> {currentArchive.consumableSnapshot.category || '-'}
|
||||
</p>
|
||||
<p>
|
||||
<strong>单位:</strong> {currentArchive.consumableSnapshot.unit || '-'}
|
||||
</p>
|
||||
<p>
|
||||
<strong>单价:</strong> {currentArchive.consumableSnapshot.unitPrice || '-'}
|
||||
</p>
|
||||
<p>
|
||||
<strong>供应商:</strong> {currentArchive.consumableSnapshot.supplier || '-'}
|
||||
</p>
|
||||
<p>
|
||||
<strong>位置:</strong> {currentArchive.consumableSnapshot.location || '-'}
|
||||
</p>
|
||||
<p>
|
||||
<strong>最小库存:</strong> {currentArchive.consumableSnapshot.minStock || '-'}
|
||||
</p>
|
||||
<p>
|
||||
<strong>最大库存:</strong> {currentArchive.consumableSnapshot.maxStock || '-'}
|
||||
</p>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -49,10 +49,7 @@ import dayjs from 'dayjs';
|
||||
import api from '../api';
|
||||
import { consumableRecordAPI, consumableCategoryAPI, consumableAPI } from '../api/cache';
|
||||
import { message } from 'antd';
|
||||
import {
|
||||
selectStyles,
|
||||
datePickerStyles,
|
||||
} from '../styles/deviceManagementStyles';
|
||||
import { selectStyles, datePickerStyles } from '../styles/deviceManagementStyles';
|
||||
import { designTokens } from '../config/theme';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
@@ -145,14 +142,16 @@ const QuickFilterBtn = styled(Button)`
|
||||
padding: 0 16px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
border: 1px solid ${props => props.$active ? designTokens.colors.primary.main : designTokens.colors.border};
|
||||
background: ${props => props.$active ? designTokens.colors.primary.main : 'transparent'};
|
||||
color: ${props => props.$active ? 'white' : designTokens.colors.text.secondary};
|
||||
|
||||
border: 1px solid
|
||||
${props => (props.$active ? designTokens.colors.primary.main : designTokens.colors.border)};
|
||||
background: ${props => (props.$active ? designTokens.colors.primary.main : 'transparent')};
|
||||
color: ${props => (props.$active ? 'white' : designTokens.colors.text.secondary)};
|
||||
|
||||
&:hover {
|
||||
border-color: ${designTokens.colors.primary.main};
|
||||
color: ${props => props.$active ? 'white' : designTokens.colors.primary.main};
|
||||
background: ${props => props.$active ? designTokens.colors.primary.main : 'rgba(99, 102, 241, 0.05)'};
|
||||
color: ${props => (props.$active ? 'white' : designTokens.colors.primary.main)};
|
||||
background: ${props =>
|
||||
props.$active ? designTokens.colors.primary.main : 'rgba(99, 102, 241, 0.05)'};
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -304,17 +303,17 @@ const StatsCard = styled(motion.div)`
|
||||
font-weight: 600;
|
||||
padding: 4px 10px;
|
||||
border-radius: 20px;
|
||||
|
||||
|
||||
&.up {
|
||||
color: ${designTokens.colors.success.main};
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
}
|
||||
|
||||
|
||||
&.down {
|
||||
color: ${designTokens.colors.error.main};
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
|
||||
|
||||
&.neutral {
|
||||
color: ${designTokens.colors.text.secondary};
|
||||
background: rgba(107, 114, 128, 0.1);
|
||||
@@ -478,7 +477,7 @@ const ComparisonItem = styled.div`
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
|
||||
|
||||
strong {
|
||||
color: ${props => props.$color};
|
||||
font-weight: 600;
|
||||
@@ -519,7 +518,7 @@ const CategoryCard = styled(motion.div)`
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 24px ${props => props.$color}20;
|
||||
border-color: ${props => props.$color}40;
|
||||
|
||||
|
||||
&::before {
|
||||
opacity: 1;
|
||||
}
|
||||
@@ -567,7 +566,7 @@ const CategoryCard = styled(motion.div)`
|
||||
color: ${designTokens.colors.text.secondary};
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid ${designTokens.colors.border}40;
|
||||
|
||||
|
||||
strong {
|
||||
color: ${designTokens.colors.text.primary};
|
||||
font-weight: 600;
|
||||
@@ -731,7 +730,7 @@ const ConsumableStatistics = () => {
|
||||
const statsResponse = await consumableRecordAPI.statistics(params);
|
||||
console.log('[统计] 返回:', statsResponse);
|
||||
console.log('[统计] 最近记录:', statsResponse?.recentRecords);
|
||||
|
||||
|
||||
setStats({
|
||||
inCount: statsResponse?.inCount || 0,
|
||||
outCount: statsResponse?.outCount || 0,
|
||||
@@ -742,7 +741,7 @@ const ConsumableStatistics = () => {
|
||||
|
||||
const summaryResponse = await consumableAPI.getStatistics();
|
||||
console.log('[汇总] 返回:', summaryResponse);
|
||||
|
||||
|
||||
setSummary({
|
||||
total: summaryResponse?.total || 0,
|
||||
lowStock: summaryResponse?.lowStock || 0,
|
||||
@@ -808,7 +807,7 @@ const ConsumableStatistics = () => {
|
||||
};
|
||||
}, [realTimeRefresh]);
|
||||
|
||||
const handleQuickFilter = (key) => {
|
||||
const handleQuickFilter = key => {
|
||||
setQuickFilter(key);
|
||||
const filter = quickFilters.find(f => f.key === key);
|
||||
if (filter) {
|
||||
@@ -823,10 +822,7 @@ const ConsumableStatistics = () => {
|
||||
const handleRefresh = () => {
|
||||
setLoading(true);
|
||||
setIsAutoRefreshing(false);
|
||||
Promise.all([
|
||||
loadStatistics(false),
|
||||
loadLowStockItems(false)
|
||||
]).finally(() => {
|
||||
Promise.all([loadStatistics(false), loadLowStockItems(false)]).finally(() => {
|
||||
setLoading(false);
|
||||
if (!realTimeRefresh) {
|
||||
message.success('数据已手动刷新');
|
||||
@@ -838,22 +834,30 @@ const ConsumableStatistics = () => {
|
||||
message.info('导出功能开发中...');
|
||||
};
|
||||
|
||||
const getCategoryColor = (category) => {
|
||||
const getCategoryColor = category => {
|
||||
const predefinedColors = [
|
||||
'#6366f1', '#10b981', '#f59e0b', '#ec4899', '#8b5cf6',
|
||||
'#06b6d4', '#f97316', '#14b8a6', '#ef4444', '#3b82f6'
|
||||
'#6366f1',
|
||||
'#10b981',
|
||||
'#f59e0b',
|
||||
'#ec4899',
|
||||
'#8b5cf6',
|
||||
'#06b6d4',
|
||||
'#f97316',
|
||||
'#14b8a6',
|
||||
'#ef4444',
|
||||
'#3b82f6',
|
||||
];
|
||||
|
||||
|
||||
const colorMap = {
|
||||
'网络设备': '#6366f1',
|
||||
'线缆': '#10b981',
|
||||
'配件': '#f59e0b',
|
||||
'工具': '#ec4899',
|
||||
'其他': '#6b7280',
|
||||
网络设备: '#6366f1',
|
||||
线缆: '#10b981',
|
||||
配件: '#f59e0b',
|
||||
工具: '#ec4899',
|
||||
其他: '#6b7280',
|
||||
};
|
||||
|
||||
|
||||
if (colorMap[category]) return colorMap[category];
|
||||
|
||||
|
||||
let hash = 0;
|
||||
for (let i = 0; i < category.length; i++) {
|
||||
hash = category.charCodeAt(i) + ((hash << 5) - hash);
|
||||
@@ -880,23 +884,27 @@ const ConsumableStatistics = () => {
|
||||
<WarningOutlined />
|
||||
</Avatar>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{
|
||||
fontWeight: 600,
|
||||
color: designTokens.colors.text.primary,
|
||||
fontSize: '13px',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
color: designTokens.colors.text.primary,
|
||||
fontSize: '13px',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '11px',
|
||||
color: designTokens.colors.text.secondary,
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '11px',
|
||||
color: designTokens.colors.text.secondary,
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
>
|
||||
{record.specification || record.category || '-'}
|
||||
</div>
|
||||
</div>
|
||||
@@ -913,12 +921,16 @@ const ConsumableStatistics = () => {
|
||||
const min = record.minStock || 0;
|
||||
const isLow = current < min;
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'center', gap: 4 }}>
|
||||
<span style={{
|
||||
fontWeight: 700,
|
||||
fontSize: '14px',
|
||||
color: isLow ? designTokens.colors.error.main : designTokens.colors.text.primary,
|
||||
}}>
|
||||
<div
|
||||
style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'center', gap: 4 }}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontWeight: 700,
|
||||
fontSize: '14px',
|
||||
color: isLow ? designTokens.colors.error.main : designTokens.colors.text.primary,
|
||||
}}
|
||||
>
|
||||
{current}
|
||||
</span>
|
||||
<span style={{ color: designTokens.colors.text.secondary, fontSize: '11px' }}>/</span>
|
||||
@@ -942,13 +954,20 @@ const ConsumableStatistics = () => {
|
||||
const currentStock = record.currentStock || 0;
|
||||
|
||||
if (minStock <= 0) {
|
||||
return <Text type="secondary" style={{ fontSize: '11px' }}>未设置</Text>;
|
||||
return (
|
||||
<Text type="secondary" style={{ fontSize: '11px' }}>
|
||||
未设置
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
const rate = Math.min(100, Math.round((currentStock / minStock) * 100));
|
||||
const color = rate < 30 ? designTokens.colors.error.main :
|
||||
rate < 60 ? designTokens.colors.warning.main :
|
||||
designTokens.colors.success.main;
|
||||
const color =
|
||||
rate < 30
|
||||
? designTokens.colors.error.main
|
||||
: rate < 60
|
||||
? designTokens.colors.warning.main
|
||||
: designTokens.colors.success.main;
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8 }}>
|
||||
@@ -959,13 +978,15 @@ const ConsumableStatistics = () => {
|
||||
showInfo={false}
|
||||
style={{ width: 60 }}
|
||||
/>
|
||||
<span style={{
|
||||
fontWeight: 600,
|
||||
fontSize: '12px',
|
||||
color,
|
||||
minWidth: 32,
|
||||
textAlign: 'right',
|
||||
}}>
|
||||
<span
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: '12px',
|
||||
color,
|
||||
minWidth: 32,
|
||||
textAlign: 'right',
|
||||
}}
|
||||
>
|
||||
{rate}%
|
||||
</span>
|
||||
</div>
|
||||
@@ -980,7 +1001,7 @@ const ConsumableStatistics = () => {
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
width: 80,
|
||||
render: (type) => (
|
||||
render: type => (
|
||||
<Tag
|
||||
icon={type === 'in' ? <ArrowDownOutlined /> : <ArrowUpOutlined />}
|
||||
color={type === 'in' ? 'success' : 'processing'}
|
||||
@@ -1000,14 +1021,18 @@ const ConsumableStatistics = () => {
|
||||
<Avatar
|
||||
size={30}
|
||||
style={{
|
||||
background: record.category ? getCategoryColor(record.category) : designTokens.colors.info.main,
|
||||
background: record.category
|
||||
? getCategoryColor(record.category)
|
||||
: designTokens.colors.info.main,
|
||||
fontSize: '12px',
|
||||
}}
|
||||
>
|
||||
{record.category?.charAt(0) || '耗'}
|
||||
</Avatar>
|
||||
<div>
|
||||
<div style={{ fontWeight: 600, fontSize: '14px', color: designTokens.colors.text.primary }}>
|
||||
<div
|
||||
style={{ fontWeight: 600, fontSize: '14px', color: designTokens.colors.text.primary }}
|
||||
>
|
||||
{text || '-'}
|
||||
</div>
|
||||
{record.category && (
|
||||
@@ -1030,10 +1055,14 @@ const ConsumableStatistics = () => {
|
||||
strong
|
||||
style={{
|
||||
fontSize: '15px',
|
||||
color: record.type === 'in' ? designTokens.colors.success.main : designTokens.colors.error.main,
|
||||
color:
|
||||
record.type === 'in'
|
||||
? designTokens.colors.success.main
|
||||
: designTokens.colors.error.main,
|
||||
}}
|
||||
>
|
||||
{record.type === 'in' ? '+' : '-'}{quantity} {record.unit || '个'}
|
||||
{record.type === 'in' ? '+' : '-'}
|
||||
{quantity} {record.unit || '个'}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
@@ -1042,7 +1071,7 @@ const ConsumableStatistics = () => {
|
||||
dataIndex: 'operator',
|
||||
key: 'operator',
|
||||
width: 100,
|
||||
render: (operator) => (
|
||||
render: operator => (
|
||||
<Space size={6}>
|
||||
<Avatar size={24} style={{ background: designTokens.colors.info.main, fontSize: '12px' }}>
|
||||
{operator?.charAt(0) || '?'}
|
||||
@@ -1056,7 +1085,7 @@ const ConsumableStatistics = () => {
|
||||
dataIndex: 'createdAt',
|
||||
key: 'createdAt',
|
||||
width: 140,
|
||||
render: (date) => (
|
||||
render: date => (
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
{dayjs(date).format('MM-DD HH:mm')}
|
||||
</Text>
|
||||
@@ -1086,13 +1115,19 @@ const ConsumableStatistics = () => {
|
||||
</TitleSection>
|
||||
<Space>
|
||||
{lastUpdateTime && (
|
||||
<div style={{ fontSize: 12, color: designTokens.colors.text.secondary, display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
{isAutoRefreshing ? (
|
||||
<Spin size="small" />
|
||||
) : (
|
||||
<span style={{ fontSize: 10 }}>●</span>
|
||||
)}
|
||||
{isAutoRefreshing ? '刷新中...' : `更新于 ${dayjs(lastUpdateTime).format('HH:mm:ss')}`}
|
||||
<div
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: designTokens.colors.text.secondary,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
{isAutoRefreshing ? <Spin size="small" /> : <span style={{ fontSize: 10 }}>●</span>}
|
||||
{isAutoRefreshing
|
||||
? '刷新中...'
|
||||
: `更新于 ${dayjs(lastUpdateTime).format('HH:mm:ss')}`}
|
||||
</div>
|
||||
)}
|
||||
<Tooltip title={realTimeRefresh ? '已开启30秒自动刷新' : '已关闭自动刷新'}>
|
||||
@@ -1154,7 +1189,7 @@ const ConsumableStatistics = () => {
|
||||
<span className="filter-label">时间范围</span>
|
||||
<RangePicker
|
||||
value={dateRange}
|
||||
onChange={(dates) => {
|
||||
onChange={dates => {
|
||||
setDateRange(dates);
|
||||
setQuickFilter(null);
|
||||
}}
|
||||
@@ -1174,7 +1209,9 @@ const ConsumableStatistics = () => {
|
||||
>
|
||||
<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>
|
||||
</FilterItem>
|
||||
@@ -1227,7 +1264,9 @@ const ConsumableStatistics = () => {
|
||||
>
|
||||
<div className="card-content">
|
||||
<div className="card-header">
|
||||
<div className="card-icon"><DatabaseOutlined /></div>
|
||||
<div className="card-icon">
|
||||
<DatabaseOutlined />
|
||||
</div>
|
||||
<div className="card-trend neutral">
|
||||
<AppstoreOutlined /> 总览
|
||||
</div>
|
||||
@@ -1250,7 +1289,9 @@ const ConsumableStatistics = () => {
|
||||
>
|
||||
<div className="card-content">
|
||||
<div className="card-header">
|
||||
<div className="card-icon"><WarningOutlined /></div>
|
||||
<div className="card-icon">
|
||||
<WarningOutlined />
|
||||
</div>
|
||||
{summary?.lowStock > 0 && (
|
||||
<div className="card-trend down">
|
||||
<ExclamationCircleOutlined /> 需关注
|
||||
@@ -1277,7 +1318,9 @@ const ConsumableStatistics = () => {
|
||||
>
|
||||
<div className="card-content">
|
||||
<div className="card-header">
|
||||
<div className="card-icon"><ArrowDownOutlined /></div>
|
||||
<div className="card-icon">
|
||||
<ArrowDownOutlined />
|
||||
</div>
|
||||
<div className="card-trend up">
|
||||
<RiseOutlined /> 入库
|
||||
</div>
|
||||
@@ -1302,7 +1345,9 @@ const ConsumableStatistics = () => {
|
||||
>
|
||||
<div className="card-content">
|
||||
<div className="card-header">
|
||||
<div className="card-icon"><ArrowUpOutlined /></div>
|
||||
<div className="card-icon">
|
||||
<ArrowUpOutlined />
|
||||
</div>
|
||||
<div className="card-trend neutral">
|
||||
<FallOutlined /> 出库
|
||||
</div>
|
||||
@@ -1318,10 +1363,16 @@ const ConsumableStatistics = () => {
|
||||
</StatsGrid>
|
||||
|
||||
<BentoGrid variants={containerVariants} initial="hidden" animate="visible">
|
||||
<BentoCard variants={itemVariants} $col="span 6" $iconBg={designTokens.colors.info.gradient}>
|
||||
<BentoCard
|
||||
variants={itemVariants}
|
||||
$col="span 6"
|
||||
$iconBg={designTokens.colors.info.gradient}
|
||||
>
|
||||
<div className="card-header">
|
||||
<div className="header-left">
|
||||
<div className="header-icon"><ShoppingCartOutlined /></div>
|
||||
<div className="header-icon">
|
||||
<ShoppingCartOutlined />
|
||||
</div>
|
||||
<span className="header-title">出入库统计</span>
|
||||
</div>
|
||||
<div className="header-extra">
|
||||
@@ -1334,28 +1385,36 @@ const ConsumableStatistics = () => {
|
||||
$bg="rgba(16, 185, 129, 0.04)"
|
||||
$color={designTokens.colors.success.main}
|
||||
>
|
||||
<div className="item-icon"><ArrowDownOutlined /></div>
|
||||
<div className="item-icon">
|
||||
<ArrowDownOutlined />
|
||||
</div>
|
||||
<div className="item-value">{stats?.inCount || 0}</div>
|
||||
<div className="item-label">入库次数</div>
|
||||
<div className="item-sub">共 <strong>{stats?.inQuantity || 0}</strong> 件</div>
|
||||
<div className="item-sub">
|
||||
共 <strong>{stats?.inQuantity || 0}</strong> 件
|
||||
</div>
|
||||
</ComparisonItem>
|
||||
<ComparisonItem
|
||||
$bg="rgba(99, 102, 241, 0.04)"
|
||||
$color={designTokens.colors.primary.main}
|
||||
>
|
||||
<div className="item-icon"><ArrowUpOutlined /></div>
|
||||
<div className="item-icon">
|
||||
<ArrowUpOutlined />
|
||||
</div>
|
||||
<div className="item-value">{stats?.outCount || 0}</div>
|
||||
<div className="item-label">出库次数</div>
|
||||
<div className="item-sub">共 <strong>{stats?.outQuantity || 0}</strong> 件</div>
|
||||
<div className="item-sub">
|
||||
共 <strong>{stats?.outQuantity || 0}</strong> 件
|
||||
</div>
|
||||
</ComparisonItem>
|
||||
</InOutComparison>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
style={{
|
||||
marginTop: 20,
|
||||
padding: '18px 24px',
|
||||
style={{
|
||||
marginTop: 20,
|
||||
padding: '18px 24px',
|
||||
background: `linear-gradient(135deg, ${netQuantity >= 0 ? 'rgba(16, 185, 129, 0.08)' : 'rgba(239, 68, 68, 0.08)'} 0%, ${netQuantity >= 0 ? 'rgba(5, 150, 105, 0.04)' : 'rgba(185, 28, 28, 0.04)'} 100%)`,
|
||||
borderRadius: 14,
|
||||
display: 'flex',
|
||||
@@ -1368,14 +1427,22 @@ const ConsumableStatistics = () => {
|
||||
{netQuantity >= 0 ? (
|
||||
<>
|
||||
<RiseOutlined style={{ color: designTokens.colors.success.main, fontSize: 20 }} />
|
||||
<Text style={{ color: designTokens.colors.success.main, fontWeight: 600, fontSize: 15 }}>
|
||||
<Text
|
||||
style={{
|
||||
color: designTokens.colors.success.main,
|
||||
fontWeight: 600,
|
||||
fontSize: 15,
|
||||
}}
|
||||
>
|
||||
净入库 +{netQuantity} 件
|
||||
</Text>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FallOutlined style={{ color: designTokens.colors.error.main, fontSize: 20 }} />
|
||||
<Text style={{ color: designTokens.colors.error.main, fontWeight: 600, fontSize: 15 }}>
|
||||
<Text
|
||||
style={{ color: designTokens.colors.error.main, fontWeight: 600, fontSize: 15 }}
|
||||
>
|
||||
净出库 {Math.abs(netQuantity)} 件
|
||||
</Text>
|
||||
</>
|
||||
@@ -1384,10 +1451,16 @@ const ConsumableStatistics = () => {
|
||||
</div>
|
||||
</BentoCard>
|
||||
|
||||
<BentoCard variants={itemVariants} $col="span 6" $iconBg={designTokens.colors.secondary.gradient}>
|
||||
<BentoCard
|
||||
variants={itemVariants}
|
||||
$col="span 6"
|
||||
$iconBg={designTokens.colors.secondary.gradient}
|
||||
>
|
||||
<div className="card-header">
|
||||
<div className="header-left">
|
||||
<div className="header-icon"><PieChartOutlined /></div>
|
||||
<div className="header-icon">
|
||||
<PieChartOutlined />
|
||||
</div>
|
||||
<span className="header-title">类别分布</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1426,10 +1499,16 @@ const ConsumableStatistics = () => {
|
||||
</BentoGrid>
|
||||
|
||||
<BentoGrid variants={containerVariants} initial="hidden" animate="visible">
|
||||
<BentoCard variants={itemVariants} $col="span 6" $iconBg={designTokens.colors.warning.gradient}>
|
||||
<BentoCard
|
||||
variants={itemVariants}
|
||||
$col="span 6"
|
||||
$iconBg={designTokens.colors.warning.gradient}
|
||||
>
|
||||
<div className="card-header">
|
||||
<div className="header-left">
|
||||
<div className="header-icon"><ExclamationCircleOutlined /></div>
|
||||
<div className="header-icon">
|
||||
<ExclamationCircleOutlined />
|
||||
</div>
|
||||
<span className="header-title">库存预警</span>
|
||||
</div>
|
||||
{lowStockItems.length > 0 ? (
|
||||
@@ -1438,14 +1517,16 @@ const ConsumableStatistics = () => {
|
||||
style={{ backgroundColor: designTokens.colors.warning.main }}
|
||||
/>
|
||||
) : (
|
||||
<span style={{
|
||||
fontSize: 12,
|
||||
color: designTokens.colors.success.main,
|
||||
fontWeight: 500,
|
||||
padding: '4px 10px',
|
||||
background: 'rgba(16, 185, 129, 0.1)',
|
||||
borderRadius: 8
|
||||
}}>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: designTokens.colors.success.main,
|
||||
fontWeight: 500,
|
||||
padding: '4px 10px',
|
||||
background: 'rgba(16, 185, 129, 0.1)',
|
||||
borderRadius: 8,
|
||||
}}
|
||||
>
|
||||
全部充足
|
||||
</span>
|
||||
)}
|
||||
@@ -1461,15 +1542,18 @@ const ConsumableStatistics = () => {
|
||||
total: lowStockItems.length,
|
||||
showSizeChanger: false,
|
||||
showQuickJumper: false,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
onChange: (page) => setLowStockPagination(prev => ({ ...prev, current: page })),
|
||||
showTotal: total => `共 ${total} 条`,
|
||||
onChange: page => setLowStockPagination(prev => ({ ...prev, current: page })),
|
||||
}}
|
||||
size="small"
|
||||
scroll={{ x: 'max-content', y: 300 }}
|
||||
locale={{
|
||||
emptyText: (
|
||||
<EmptyState>
|
||||
<BoxPlotOutlined className="empty-icon" style={{ color: designTokens.colors.success.main }} />
|
||||
<BoxPlotOutlined
|
||||
className="empty-icon"
|
||||
style={{ color: designTokens.colors.success.main }}
|
||||
/>
|
||||
<div className="empty-text">库存充足</div>
|
||||
<div className="empty-subtext">所有耗材均在安全范围内</div>
|
||||
</EmptyState>
|
||||
@@ -1479,10 +1563,16 @@ const ConsumableStatistics = () => {
|
||||
</div>
|
||||
</BentoCard>
|
||||
|
||||
<BentoCard variants={itemVariants} $col="span 6" $iconBg={designTokens.colors.success.gradient}>
|
||||
<BentoCard
|
||||
variants={itemVariants}
|
||||
$col="span 6"
|
||||
$iconBg={designTokens.colors.success.gradient}
|
||||
>
|
||||
<div className="card-header">
|
||||
<div className="header-left">
|
||||
<div className="header-icon"><HistoryOutlined /></div>
|
||||
<div className="header-icon">
|
||||
<HistoryOutlined />
|
||||
</div>
|
||||
<span className="header-title">最近记录</span>
|
||||
</div>
|
||||
<div className="header-extra">最近10条</div>
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import { Card, Row, Col, Typography, message } from 'antd';
|
||||
import { DatabaseOutlined, CloudServerOutlined, WarningOutlined, HomeOutlined, TeamOutlined, BarChartOutlined, DashboardOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
DatabaseOutlined,
|
||||
CloudServerOutlined,
|
||||
WarningOutlined,
|
||||
HomeOutlined,
|
||||
TeamOutlined,
|
||||
BarChartOutlined,
|
||||
DashboardOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import api from '../api';
|
||||
import { designTokens } from '../config/theme';
|
||||
import { useFetch } from '../hooks/useSWR';
|
||||
@@ -185,22 +193,33 @@ function Dashboard() {
|
||||
let currentAngle = 0;
|
||||
|
||||
if (runningDeg > 0) {
|
||||
gradientParts.push(`${PIE_CHART_COLORS.success} ${currentAngle}deg ${currentAngle + runningDeg}deg`);
|
||||
gradientParts.push(
|
||||
`${PIE_CHART_COLORS.success} ${currentAngle}deg ${currentAngle + runningDeg}deg`
|
||||
);
|
||||
currentAngle += runningDeg;
|
||||
}
|
||||
if (maintenanceDeg > 0) {
|
||||
gradientParts.push(`${PIE_CHART_COLORS.warning} ${currentAngle}deg ${currentAngle + maintenanceDeg}deg`);
|
||||
gradientParts.push(
|
||||
`${PIE_CHART_COLORS.warning} ${currentAngle}deg ${currentAngle + maintenanceDeg}deg`
|
||||
);
|
||||
currentAngle += maintenanceDeg;
|
||||
}
|
||||
if (faultDeg > 0) {
|
||||
gradientParts.push(`${PIE_CHART_COLORS.error} ${currentAngle}deg ${currentAngle + faultDeg}deg`);
|
||||
gradientParts.push(
|
||||
`${PIE_CHART_COLORS.error} ${currentAngle}deg ${currentAngle + faultDeg}deg`
|
||||
);
|
||||
currentAngle += faultDeg;
|
||||
}
|
||||
if (offlineDeg > 0) {
|
||||
gradientParts.push(`${PIE_CHART_COLORS.primary} ${currentAngle}deg ${currentAngle + offlineDeg}deg`);
|
||||
gradientParts.push(
|
||||
`${PIE_CHART_COLORS.primary} ${currentAngle}deg ${currentAngle + offlineDeg}deg`
|
||||
);
|
||||
}
|
||||
|
||||
const conicGradient = gradientParts.length > 0 ? `conic-gradient(${gradientParts.join(', ')})` : 'conic-gradient(#e5e7eb 0deg 360deg)';
|
||||
const conicGradient =
|
||||
gradientParts.length > 0
|
||||
? `conic-gradient(${gradientParts.join(', ')})`
|
||||
: 'conic-gradient(#e5e7eb 0deg 360deg)';
|
||||
|
||||
return {
|
||||
width: '180px',
|
||||
@@ -215,11 +234,11 @@ function Dashboard() {
|
||||
}, [deviceStatusPercentages]);
|
||||
|
||||
const handleRefresh = useCallback(async () => {
|
||||
setAnimatedKey((prev) => prev + 1);
|
||||
setAnimatedKey(prev => prev + 1);
|
||||
await mutateStats();
|
||||
}, [mutateStats]);
|
||||
|
||||
const handleHover = useCallback((key) => {
|
||||
const handleHover = useCallback(key => {
|
||||
setHoveredCard(key);
|
||||
}, []);
|
||||
|
||||
@@ -325,17 +344,14 @@ function Dashboard() {
|
||||
[stats.deviceGrowth, stats.faultTrend, stats.userGrowth, stats.ticketTrend]
|
||||
);
|
||||
|
||||
const deviceTrendData = useMemo(
|
||||
() => {
|
||||
const rawData = statsData?.deviceTrendData || [];
|
||||
return rawData.map((item) => ({
|
||||
label: item.label,
|
||||
value: item.value,
|
||||
color: designTokens.colors.primary.main
|
||||
}));
|
||||
},
|
||||
[statsData?.deviceTrendData]
|
||||
);
|
||||
const deviceTrendData = useMemo(() => {
|
||||
const rawData = statsData?.deviceTrendData || [];
|
||||
return rawData.map(item => ({
|
||||
label: item.label,
|
||||
value: item.value,
|
||||
color: designTokens.colors.primary.main,
|
||||
}));
|
||||
}, [statsData?.deviceTrendData]);
|
||||
|
||||
const styles = `
|
||||
@keyframes fadeInDown {
|
||||
@@ -375,7 +391,7 @@ function Dashboard() {
|
||||
</div>
|
||||
|
||||
<Row gutter={[24, 24]} style={{ marginBottom: '32px' }}>
|
||||
{statCards.map((config) => (
|
||||
{statCards.map(config => (
|
||||
<StatCard
|
||||
key={config.statKey}
|
||||
config={config}
|
||||
@@ -544,7 +560,11 @@ function Dashboard() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<QuickStats onlineRate={stats.onlineRate} powerUsage={stats.powerUsage} totalMaxPower={stats.totalMaxPower} />
|
||||
<QuickStats
|
||||
onlineRate={stats.onlineRate}
|
||||
powerUsage={stats.powerUsage}
|
||||
totalMaxPower={stats.totalMaxPower}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} md={8}>
|
||||
|
||||
@@ -45,51 +45,51 @@ const OptionsEditor = ({ value = [], onChange }) => {
|
||||
};
|
||||
|
||||
const handleUpdate = (index, field, fieldValue) => {
|
||||
const newOptions = value.map((opt, i) =>
|
||||
i === index ? { ...opt, [field]: fieldValue } : opt
|
||||
);
|
||||
const newOptions = value.map((opt, i) => (i === index ? { ...opt, [field]: fieldValue } : opt));
|
||||
onChange(newOptions);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
border: '1px solid #e8e8e8',
|
||||
borderRadius: '12px',
|
||||
padding: '20px',
|
||||
background: 'linear-gradient(135deg, #fafbfc 0%, #f5f7fa 100%)',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.04)',
|
||||
}}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginBottom: '16px',
|
||||
gap: '8px',
|
||||
}}>
|
||||
<div style={{
|
||||
width: '4px',
|
||||
height: '16px',
|
||||
background: 'linear-gradient(180deg, #667eea 0%, #764ba2 100%)',
|
||||
borderRadius: '2px',
|
||||
}}/>
|
||||
<span style={{ color: '#333', fontSize: '14px', fontWeight: '600' }}>
|
||||
选项配置
|
||||
</span>
|
||||
<span style={{ color: '#999', fontSize: '12px' }}>
|
||||
(值用于提交,标签用于显示)
|
||||
</span>
|
||||
<div
|
||||
style={{
|
||||
border: '1px solid #e8e8e8',
|
||||
borderRadius: '12px',
|
||||
padding: '20px',
|
||||
background: 'linear-gradient(135deg, #fafbfc 0%, #f5f7fa 100%)',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.04)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginBottom: '16px',
|
||||
gap: '8px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: '4px',
|
||||
height: '16px',
|
||||
background: 'linear-gradient(180deg, #667eea 0%, #764ba2 100%)',
|
||||
borderRadius: '2px',
|
||||
}}
|
||||
/>
|
||||
<span style={{ color: '#333', fontSize: '14px', fontWeight: '600' }}>选项配置</span>
|
||||
<span style={{ color: '#999', fontSize: '12px' }}>(值用于提交,标签用于显示)</span>
|
||||
</div>
|
||||
|
||||
{value.length === 0 ? (
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
padding: '24px',
|
||||
background: '#fff',
|
||||
borderRadius: '8px',
|
||||
border: '1px dashed #d9d9d9',
|
||||
}}>
|
||||
<div style={{ color: '#bbb', fontSize: '14px', marginBottom: '12px' }}>
|
||||
暂无选项
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
padding: '24px',
|
||||
background: '#fff',
|
||||
borderRadius: '8px',
|
||||
border: '1px dashed #d9d9d9',
|
||||
}}
|
||||
>
|
||||
<div style={{ color: '#bbb', fontSize: '14px', marginBottom: '12px' }}>暂无选项</div>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusCircleOutlined />}
|
||||
@@ -105,15 +105,23 @@ const OptionsEditor = ({ value = [], onChange }) => {
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px', marginBottom: '16px' }}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
gap: '12px',
|
||||
padding: '0 4px',
|
||||
marginBottom: '4px',
|
||||
}}>
|
||||
<span style={{ width: '160px', color: '#666', fontSize: '12px', fontWeight: '500' }}>值(value)</span>
|
||||
<span style={{ width: '160px', color: '#666', fontSize: '12px', fontWeight: '500' }}>标签(label)</span>
|
||||
<div
|
||||
style={{ display: 'flex', flexDirection: 'column', gap: '10px', marginBottom: '16px' }}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: '12px',
|
||||
padding: '0 4px',
|
||||
marginBottom: '4px',
|
||||
}}
|
||||
>
|
||||
<span style={{ width: '160px', color: '#666', fontSize: '12px', fontWeight: '500' }}>
|
||||
值(value)
|
||||
</span>
|
||||
<span style={{ width: '160px', color: '#666', fontSize: '12px', fontWeight: '500' }}>
|
||||
标签(label)
|
||||
</span>
|
||||
</div>
|
||||
{value.map((opt, index) => (
|
||||
<div
|
||||
@@ -129,19 +137,21 @@ const OptionsEditor = ({ value = [], onChange }) => {
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
borderRadius: '50%',
|
||||
background: 'linear-gradient(135deg, #667eea20 0%, #764ba220 100%)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#667eea',
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
borderRadius: '50%',
|
||||
background: 'linear-gradient(135deg, #667eea20 0%, #764ba220 100%)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#667eea',
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{index + 1}
|
||||
</div>
|
||||
<Input
|
||||
|
||||
@@ -69,11 +69,7 @@ import {
|
||||
BatchStatusModal,
|
||||
} from '../components/device';
|
||||
import { useDebounce } from '../hooks/useDebounce';
|
||||
import {
|
||||
getDeviceTypeIcon,
|
||||
getStatusConfig,
|
||||
processDeviceData,
|
||||
} from '../utils/deviceUtils.jsx';
|
||||
import { getDeviceTypeIcon, getStatusConfig, processDeviceData } from '../utils/deviceUtils.jsx';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
@@ -165,7 +161,7 @@ function DeviceManagement() {
|
||||
} else {
|
||||
setAllDevices(processedDevices);
|
||||
}
|
||||
setPagination((prev) => ({ ...prev, current: page, pageSize, total }));
|
||||
setPagination(prev => ({ ...prev, current: page, pageSize, total }));
|
||||
hasMoreRef.current = page * pageSize < total;
|
||||
} catch (error) {
|
||||
message.error('获取设备列表失败');
|
||||
@@ -183,7 +179,7 @@ function DeviceManagement() {
|
||||
setLoadingFields(true);
|
||||
const response = await axios.get('/api/deviceFields');
|
||||
let fields = response.data.sort((a, b) => a.order - b.order);
|
||||
|
||||
|
||||
// 补充缺失的 options(状态和设备类型)
|
||||
fields = fields.map(field => {
|
||||
if (field.fieldName === 'type' && !field.options) {
|
||||
@@ -191,12 +187,14 @@ function DeviceManagement() {
|
||||
return { ...field, options: defaultTypeField?.options || [] };
|
||||
}
|
||||
if (field.fieldName === 'status' && !field.options) {
|
||||
const defaultStatusField = DEFAULT_DEVICE_FIELDS_LOCAL.find(f => f.fieldName === 'status');
|
||||
const defaultStatusField = DEFAULT_DEVICE_FIELDS_LOCAL.find(
|
||||
f => f.fieldName === 'status'
|
||||
);
|
||||
return { ...field, options: defaultStatusField?.options || [] };
|
||||
}
|
||||
return field;
|
||||
});
|
||||
|
||||
|
||||
setDeviceFields(fields);
|
||||
} catch (error) {
|
||||
message.error('获取字段配置失败');
|
||||
@@ -275,7 +273,13 @@ function DeviceManagement() {
|
||||
|
||||
const handleLoadMoreDevices = useCallback(() => {
|
||||
if (!hasMoreRef.current || isLoadingRef.current || deviceLoadingMore) return;
|
||||
if (debouncedKeyword || status !== 'all' || type !== 'all' || roomId !== 'all' || rackId !== 'all') {
|
||||
if (
|
||||
debouncedKeyword ||
|
||||
status !== 'all' ||
|
||||
type !== 'all' ||
|
||||
roomId !== 'all' ||
|
||||
rackId !== 'all'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
isLoadingRef.current = true;
|
||||
@@ -283,7 +287,17 @@ function DeviceManagement() {
|
||||
fetchDevices(nextPage, pagination.pageSize, true).then(() => {
|
||||
isLoadingRef.current = false;
|
||||
});
|
||||
}, [pagination.current, pagination.pageSize, debouncedKeyword, status, type, roomId, rackId, deviceLoadingMore, fetchDevices]);
|
||||
}, [
|
||||
pagination.current,
|
||||
pagination.pageSize,
|
||||
debouncedKeyword,
|
||||
status,
|
||||
type,
|
||||
roomId,
|
||||
rackId,
|
||||
deviceLoadingMore,
|
||||
fetchDevices,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loadMoreRef.current) return;
|
||||
@@ -291,7 +305,13 @@ function DeviceManagement() {
|
||||
const observer = new IntersectionObserver(
|
||||
entries => {
|
||||
if (entries[0].isIntersecting && hasMoreRef.current && !deviceLoadingMore) {
|
||||
if (!debouncedKeyword && status === 'all' && type === 'all' && roomId === 'all' && rackId === 'all') {
|
||||
if (
|
||||
!debouncedKeyword &&
|
||||
status === 'all' &&
|
||||
type === 'all' &&
|
||||
roomId === 'all' &&
|
||||
rackId === 'all'
|
||||
) {
|
||||
handleLoadMoreDevices();
|
||||
}
|
||||
}
|
||||
@@ -316,7 +336,7 @@ function DeviceManagement() {
|
||||
setEditingDevice(null);
|
||||
};
|
||||
|
||||
const handleSubmit = async (deviceData) => {
|
||||
const handleSubmit = async deviceData => {
|
||||
try {
|
||||
if (editingDevice) {
|
||||
await axios.put(`/api/devices/${editingDevice.deviceId}`, deviceData);
|
||||
@@ -336,7 +356,7 @@ function DeviceManagement() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = (values) => {
|
||||
const handleSearch = values => {
|
||||
setSearching(true);
|
||||
|
||||
setKeyword(values.keyword || '');
|
||||
@@ -345,7 +365,7 @@ function DeviceManagement() {
|
||||
setRoomId(values.roomId || 'all');
|
||||
setRackId(values.rackId || 'all');
|
||||
|
||||
setPagination((prev) => ({ ...prev, current: 1 }));
|
||||
setPagination(prev => ({ ...prev, current: 1 }));
|
||||
|
||||
setTimeout(() => setSearching(false), 300);
|
||||
};
|
||||
@@ -363,7 +383,7 @@ function DeviceManagement() {
|
||||
setTimeout(() => setSearching(false), 300);
|
||||
};
|
||||
|
||||
const handleTableChange = (newPagination) => {
|
||||
const handleTableChange = newPagination => {
|
||||
setPagination(newPagination);
|
||||
|
||||
const start = (newPagination.current - 1) * newPagination.pageSize;
|
||||
@@ -453,7 +473,7 @@ function DeviceManagement() {
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = async (deviceId) => {
|
||||
const handleDelete = async deviceId => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: '确定要删除这个设备吗?',
|
||||
@@ -473,18 +493,18 @@ function DeviceManagement() {
|
||||
});
|
||||
};
|
||||
|
||||
const handleShowDetail = (device) => {
|
||||
const handleShowDetail = device => {
|
||||
setSelectedDevice(device);
|
||||
setDetailModalVisible(true);
|
||||
};
|
||||
|
||||
const handleViewDeviceTickets = (device) => {
|
||||
const handleViewDeviceTickets = device => {
|
||||
navigate(
|
||||
`/tickets?deviceId=${device.deviceId}&deviceName=${encodeURIComponent(device.name)}&serialNumber=${encodeURIComponent(device.serialNumber || '')}&view=true`
|
||||
);
|
||||
};
|
||||
|
||||
const handleCreateTicketForDevice = (device) => {
|
||||
const handleCreateTicketForDevice = device => {
|
||||
navigate(
|
||||
`/tickets?deviceId=${device.deviceId}&deviceName=${encodeURIComponent(device.name)}&serialNumber=${encodeURIComponent(device.serialNumber || '')}&create=true`
|
||||
);
|
||||
@@ -498,7 +518,7 @@ function DeviceManagement() {
|
||||
setBatchStatusModalVisible(true);
|
||||
};
|
||||
|
||||
const handleBatchStatusChange = async (newStatus) => {
|
||||
const handleBatchStatusChange = async newStatus => {
|
||||
setBatchStatusLoading(true);
|
||||
try {
|
||||
const response = await axios.put('/api/devices/batch-status', {
|
||||
@@ -532,9 +552,9 @@ function DeviceManagement() {
|
||||
if (scope === 'selected') {
|
||||
deviceIds = selectedDevices;
|
||||
} else if (scope === 'currentPage') {
|
||||
deviceIds = allDevices.map((device) => device.deviceId);
|
||||
deviceIds = allDevices.map(device => device.deviceId);
|
||||
} else if (scope === 'all') {
|
||||
deviceIds = allDevices.map((device) => device.deviceId);
|
||||
deviceIds = allDevices.map(device => device.deviceId);
|
||||
}
|
||||
|
||||
if (deviceIds.length === 0) {
|
||||
@@ -543,7 +563,7 @@ function DeviceManagement() {
|
||||
}
|
||||
|
||||
const params = new URLSearchParams();
|
||||
deviceIds.forEach((id) => params.append('deviceIds', id));
|
||||
deviceIds.forEach(id => params.append('deviceIds', id));
|
||||
params.append('format', format);
|
||||
|
||||
const response = await axios.get(`/api/devices/enhanced-export?${params.toString()}`, {
|
||||
@@ -575,7 +595,7 @@ function DeviceManagement() {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
onUploadProgress: (progressEvent) => {
|
||||
onUploadProgress: progressEvent => {
|
||||
const progress = Math.round((progressEvent.loaded * 50) / progressEvent.total);
|
||||
callbacks.onProgress(Math.min(progress, 50), '正在上传文件...');
|
||||
},
|
||||
@@ -617,7 +637,7 @@ function DeviceManagement() {
|
||||
Array.isArray(data.errors) &&
|
||||
data.errors.length > 0
|
||||
) {
|
||||
errorDetails = data.errors.map((err) => ({
|
||||
errorDetails = data.errors.map(err => ({
|
||||
row: err.row || 0,
|
||||
error: err.error || err.message || '服务器内部错误',
|
||||
}));
|
||||
@@ -645,7 +665,7 @@ function DeviceManagement() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveFieldConfig = async (updatedFields) => {
|
||||
const handleSaveFieldConfig = async updatedFields => {
|
||||
try {
|
||||
const response = await axios.post('/api/deviceFields/config', updatedFields);
|
||||
setDeviceFields(response.data);
|
||||
@@ -657,26 +677,26 @@ function DeviceManagement() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleResetFieldConfig = (defaultFields) => {
|
||||
const handleResetFieldConfig = defaultFields => {
|
||||
setDeviceFields(defaultFields);
|
||||
};
|
||||
|
||||
const handleSelectionChange = (selectedRowKeys) => {
|
||||
const handleSelectionChange = selectedRowKeys => {
|
||||
setSelectedDevices(selectedRowKeys);
|
||||
setSelectAll(selectedRowKeys.length === allDevices.length && allDevices.length > 0);
|
||||
};
|
||||
|
||||
const handleHeaderCellResize = (key) => (column) => ({
|
||||
const handleHeaderCellResize = key => column => ({
|
||||
width: column.width,
|
||||
onResize: (width) => {
|
||||
setColumnWidths((prev) => ({ ...prev, [key]: width }));
|
||||
onResize: width => {
|
||||
setColumnWidths(prev => ({ ...prev, [key]: width }));
|
||||
},
|
||||
});
|
||||
|
||||
const columns = useMemo(() => {
|
||||
const generatedColumns = [];
|
||||
|
||||
deviceFields.forEach((field) => {
|
||||
deviceFields.forEach(field => {
|
||||
if (!field.visible) return;
|
||||
|
||||
if (field.fieldName === 'rackId') {
|
||||
@@ -701,7 +721,7 @@ function DeviceManagement() {
|
||||
key: field.fieldName,
|
||||
width: columnWidths[field.fieldName] || 100,
|
||||
onHeaderCell: handleHeaderCellResize(field.fieldName),
|
||||
render: (type) => (
|
||||
render: type => (
|
||||
<Space>
|
||||
{getDeviceTypeIcon(type)}
|
||||
<span>{TYPE_MAP[type]}</span>
|
||||
@@ -715,7 +735,7 @@ function DeviceManagement() {
|
||||
key: field.fieldName,
|
||||
width: columnWidths[field.fieldName] || 90,
|
||||
onHeaderCell: handleHeaderCellResize(field.fieldName),
|
||||
render: (status) => {
|
||||
render: status => {
|
||||
const config = STATUS_MAP[status] || { text: status, color: 'default' };
|
||||
const statusStyles = {
|
||||
running: { bg: '#f6ffed', border: '#52c41a', text: '#389e0d' },
|
||||
@@ -724,7 +744,11 @@ function DeviceManagement() {
|
||||
fault: { bg: '#fff2f0', border: '#ff4d4f', text: '#cf1322' },
|
||||
idle: { bg: '#E6FFFA', border: '#36cfc9', text: '#08979d' },
|
||||
};
|
||||
const style = statusStyles[status] || { bg: '#fafafa', border: '#d9d9d9', text: '#595959' };
|
||||
const style = statusStyles[status] || {
|
||||
bg: '#fafafa',
|
||||
border: '#d9d9d9',
|
||||
text: '#595959',
|
||||
};
|
||||
return (
|
||||
<Tag
|
||||
style={{
|
||||
@@ -752,7 +776,7 @@ function DeviceManagement() {
|
||||
key: field.fieldName,
|
||||
width: columnWidths[field.fieldName] || 120,
|
||||
onHeaderCell: handleHeaderCellResize(field.fieldName),
|
||||
render: (date) => {
|
||||
render: date => {
|
||||
if (!date) return '';
|
||||
|
||||
const dateObj = new Date(date);
|
||||
@@ -809,8 +833,8 @@ function DeviceManagement() {
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
onMouseEnter={(e) => (e.target.style.textDecoration = 'underline')}
|
||||
onMouseLeave={(e) => (e.target.style.textDecoration = 'none')}
|
||||
onMouseEnter={e => (e.target.style.textDecoration = 'underline')}
|
||||
onMouseLeave={e => (e.target.style.textDecoration = 'none')}
|
||||
>
|
||||
{value || '-'}
|
||||
</a>
|
||||
@@ -993,17 +1017,38 @@ function DeviceManagement() {
|
||||
style={{ width: '100%' }}
|
||||
className="filter-form"
|
||||
>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: designTokens.spacing.md, alignItems: 'center', width: '100%' }}>
|
||||
<div style={{
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: designTokens.spacing.md,
|
||||
alignItems: 'center',
|
||||
gap: designTokens.spacing.sm,
|
||||
padding: '0 12px',
|
||||
borderRight: `1px solid ${designTokens.colors.border.light}`,
|
||||
marginRight: 4,
|
||||
}}>
|
||||
<FilterOutlined style={{ color: designTokens.colors.primary.main, fontSize: '16px' }} />
|
||||
<span style={{ fontSize: '13px', fontWeight: 500, color: designTokens.colors.text.secondary, whiteSpace: 'nowrap' }}>筛选</span>
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: designTokens.spacing.sm,
|
||||
padding: '0 12px',
|
||||
borderRight: `1px solid ${designTokens.colors.border.light}`,
|
||||
marginRight: 4,
|
||||
}}
|
||||
>
|
||||
<FilterOutlined
|
||||
style={{ color: designTokens.colors.primary.main, fontSize: '16px' }}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
fontSize: '13px',
|
||||
fontWeight: 500,
|
||||
color: designTokens.colors.text.secondary,
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
筛选
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Form.Item name="keyword" style={{ margin: 0 }}>
|
||||
@@ -1017,7 +1062,7 @@ function DeviceManagement() {
|
||||
transition: `all ${designTokens.transitions.fast}`,
|
||||
}}
|
||||
value={keyword}
|
||||
onChange={(e) => setKeyword(e.target.value)}
|
||||
onChange={e => setKeyword(e.target.value)}
|
||||
allowClear
|
||||
/>
|
||||
</Form.Item>
|
||||
@@ -1064,10 +1109,14 @@ function DeviceManagement() {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
color: advancedSearchVisible ? designTokens.colors.primary.main : designTokens.colors.text.secondary,
|
||||
color: advancedSearchVisible
|
||||
? designTokens.colors.primary.main
|
||||
: designTokens.colors.text.secondary,
|
||||
fontWeight: 500,
|
||||
borderRadius: designTokens.borderRadius.medium,
|
||||
background: advancedSearchVisible ? `${designTokens.colors.primary.main}10` : 'transparent',
|
||||
background: advancedSearchVisible
|
||||
? `${designTokens.colors.primary.main}10`
|
||||
: 'transparent',
|
||||
transition: `all ${designTokens.transitions.fast}`,
|
||||
}}
|
||||
>
|
||||
@@ -1117,30 +1166,43 @@ function DeviceManagement() {
|
||||
</div>
|
||||
|
||||
{advancedSearchVisible && (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: designTokens.spacing.md,
|
||||
padding: `${designTokens.spacing.md}px 0`,
|
||||
borderTop: `1px dashed ${designTokens.colors.border.light}`,
|
||||
marginTop: designTokens.spacing.sm,
|
||||
}}>
|
||||
<div style={{
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: designTokens.spacing.sm,
|
||||
padding: '0 12px',
|
||||
borderRight: `1px solid ${designTokens.colors.border.light}`,
|
||||
marginRight: 4,
|
||||
}}>
|
||||
flexWrap: 'wrap',
|
||||
gap: designTokens.spacing.md,
|
||||
padding: `${designTokens.spacing.md}px 0`,
|
||||
borderTop: `1px dashed ${designTokens.colors.border.light}`,
|
||||
marginTop: designTokens.spacing.sm,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: designTokens.spacing.sm,
|
||||
padding: '0 12px',
|
||||
borderRight: `1px solid ${designTokens.colors.border.light}`,
|
||||
marginRight: 4,
|
||||
}}
|
||||
>
|
||||
<EnvironmentOutlined style={{ color: '#3b82f6', fontSize: '16px' }} />
|
||||
<span style={{ fontSize: '13px', fontWeight: 500, color: designTokens.colors.text.secondary, whiteSpace: 'nowrap' }}>位置筛选</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: '13px',
|
||||
fontWeight: 500,
|
||||
color: designTokens.colors.text.secondary,
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
位置筛选
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Form.Item name="roomId" style={{ margin: 0 }}>
|
||||
<Select
|
||||
value={roomId}
|
||||
onChange={(value) => {
|
||||
onChange={value => {
|
||||
setRoomId(value);
|
||||
setRackId('all');
|
||||
}}
|
||||
@@ -1149,7 +1211,7 @@ function DeviceManagement() {
|
||||
suffixIcon={<EnvironmentOutlined style={{ color: '#3b82f6' }} />}
|
||||
>
|
||||
<Option value="all">所有机房</Option>
|
||||
{rooms.map((room) => (
|
||||
{rooms.map(room => (
|
||||
<Option key={room.roomId} value={room.roomId}>
|
||||
{room.name}
|
||||
</Option>
|
||||
@@ -1169,8 +1231,8 @@ function DeviceManagement() {
|
||||
>
|
||||
<Option value="all">所有机柜</Option>
|
||||
{racks
|
||||
.filter((rack) => roomId === 'all' || rack.roomId === roomId)
|
||||
.map((rack) => (
|
||||
.filter(rack => roomId === 'all' || rack.roomId === roomId)
|
||||
.map(rack => (
|
||||
<Option key={rack.rackId} value={rack.rackId}>
|
||||
{rack.name}
|
||||
</Option>
|
||||
@@ -1217,7 +1279,7 @@ function DeviceManagement() {
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
pageSizeOptions: ['10', '20', '30', '50', '100'],
|
||||
showTotal: (total) => `共 ${total} 条记录`,
|
||||
showTotal: total => `共 ${total} 条记录`,
|
||||
style: { marginTop: '16px' },
|
||||
}}
|
||||
onChange={handleTableChange}
|
||||
@@ -1243,7 +1305,7 @@ function DeviceManagement() {
|
||||
key: 'all',
|
||||
text: '全选',
|
||||
onSelect: () => {
|
||||
const allIds = allDevices.map((device) => device.deviceId);
|
||||
const allIds = allDevices.map(device => device.deviceId);
|
||||
setSelectedDevices(allIds);
|
||||
setSelectAll(true);
|
||||
},
|
||||
@@ -1252,8 +1314,8 @@ function DeviceManagement() {
|
||||
key: 'invert',
|
||||
text: '反选',
|
||||
onSelect: () => {
|
||||
const visibleIds = allDevices.map((device) => device.deviceId);
|
||||
const newSelected = visibleIds.filter((id) => !selectedDevices.includes(id));
|
||||
const visibleIds = allDevices.map(device => device.deviceId);
|
||||
const newSelected = visibleIds.filter(id => !selectedDevices.includes(id));
|
||||
setSelectedDevices(newSelected);
|
||||
setSelectAll(newSelected.length === allDevices.length);
|
||||
},
|
||||
@@ -1268,11 +1330,11 @@ function DeviceManagement() {
|
||||
},
|
||||
],
|
||||
}}
|
||||
onRow={(record) => ({
|
||||
onRow={record => ({
|
||||
onClick: () =>
|
||||
handleSelectionChange(
|
||||
selectedDevices.includes(record.deviceId)
|
||||
? selectedDevices.filter((id) => id !== record.deviceId)
|
||||
? selectedDevices.filter(id => id !== record.deviceId)
|
||||
: [...selectedDevices, record.deviceId]
|
||||
),
|
||||
})}
|
||||
@@ -1283,11 +1345,16 @@ function DeviceManagement() {
|
||||
return index % 2 === 0 ? 'ant-table-row-even' : 'ant-table-row-odd';
|
||||
}}
|
||||
/>
|
||||
{hasMoreRef.current && !debouncedKeyword && status === 'all' && type === 'all' && roomId === 'all' && rackId === 'all' && (
|
||||
<div ref={loadMoreRef} style={{ textAlign: 'center', padding: '20px' }}>
|
||||
{deviceLoadingMore && <Spin tip="加载更多设备..." />}
|
||||
</div>
|
||||
)}
|
||||
{hasMoreRef.current &&
|
||||
!debouncedKeyword &&
|
||||
status === 'all' &&
|
||||
type === 'all' &&
|
||||
roomId === 'all' &&
|
||||
rackId === 'all' && (
|
||||
<div ref={loadMoreRef} style={{ textAlign: 'center', padding: '20px' }}>
|
||||
{deviceLoadingMore && <Spin tip="加载更多设备..." />}
|
||||
</div>
|
||||
)}
|
||||
{!hasMoreRef.current && allDevices.length > 0 && (
|
||||
<div style={{ textAlign: 'center', padding: '16px', color: '#999' }}>
|
||||
已加载全部 {pagination.total} 个设备
|
||||
|
||||
@@ -38,9 +38,7 @@ function ErrorBoundaryTest() {
|
||||
color: '#fff',
|
||||
}}
|
||||
>
|
||||
<h1 style={{ fontSize: '24px', marginBottom: '8px' }}>
|
||||
🧪 错误边界测试页面
|
||||
</h1>
|
||||
<h1 style={{ fontSize: '24px', marginBottom: '8px' }}>🧪 错误边界测试页面</h1>
|
||||
<p style={{ opacity: 0.9 }}>
|
||||
用于测试 React 错误边界(Error Boundary)功能,确保单个组件错误不会导致整个页面崩溃
|
||||
</p>
|
||||
@@ -62,11 +60,7 @@ function ErrorBoundaryTest() {
|
||||
|
||||
<Card title="测试 2:页面级别错误边界" size="small">
|
||||
<p>这个测试会触发页面级别的错误边界</p>
|
||||
<Button
|
||||
type="primary"
|
||||
danger
|
||||
onClick={() => setShowError(true)}
|
||||
>
|
||||
<Button type="primary" danger onClick={() => setShowError(true)}>
|
||||
触发页面错误
|
||||
</Button>
|
||||
{showError && <BrokenComponent />}
|
||||
@@ -95,9 +89,7 @@ function ErrorBoundaryTest() {
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<h3 style={{ color: '#ff4d4f', margin: '0 0 8px 0' }}>
|
||||
⚠️ 自定义错误提示
|
||||
</h3>
|
||||
<h3 style={{ color: '#ff4d4f', margin: '0 0 8px 0' }}>⚠️ 自定义错误提示</h3>
|
||||
<p style={{ margin: 0, color: '#666' }}>
|
||||
这是自定义的 fallback UI,当组件出错时会显示这个界面
|
||||
</p>
|
||||
@@ -131,16 +123,15 @@ function ErrorBoundaryTest() {
|
||||
<Card title="使用说明" size="small">
|
||||
<ul style={{ lineHeight: '2' }}>
|
||||
<li>
|
||||
<strong>错误边界(Error Boundary)</strong>:
|
||||
是 React 提供的错误处理机制,可以捕获子组件树中的 JavaScript 错误
|
||||
<strong>错误边界(Error Boundary)</strong>: 是 React
|
||||
提供的错误处理机制,可以捕获子组件树中的 JavaScript 错误
|
||||
</li>
|
||||
<li>
|
||||
<strong>作用</strong>:
|
||||
防止单个组件的错误导致整个应用崩溃,提供友好的错误提示界面
|
||||
<strong>作用</strong>: 防止单个组件的错误导致整个应用崩溃,提供友好的错误提示界面
|
||||
</li>
|
||||
<li>
|
||||
<strong>实现方式</strong>:
|
||||
使用 React.Component 的 componentDidCatch 和 getDerivedStateFromError 生命周期方法
|
||||
<strong>实现方式</strong>: 使用 React.Component 的 componentDidCatch 和
|
||||
getDerivedStateFromError 生命周期方法
|
||||
</li>
|
||||
<li>
|
||||
<strong>注意事项</strong>:
|
||||
|
||||
@@ -68,7 +68,7 @@ const IdleDeviceManagement = () => {
|
||||
};
|
||||
const response = await axios.get('/api/idle-devices', { params });
|
||||
setIdleDevices(response.data.idleDevices || []);
|
||||
setPagination((prev) => ({
|
||||
setPagination(prev => ({
|
||||
...prev,
|
||||
total: response.data.total || 0,
|
||||
}));
|
||||
@@ -113,7 +113,7 @@ const IdleDeviceManagement = () => {
|
||||
setIsModalVisible(true);
|
||||
};
|
||||
|
||||
const handleEdit = (record) => {
|
||||
const handleEdit = record => {
|
||||
setEditingDevice(record);
|
||||
let roomId = null;
|
||||
if (record.rackId && record.Rack) {
|
||||
@@ -136,7 +136,7 @@ const IdleDeviceManagement = () => {
|
||||
setIsModalVisible(true);
|
||||
};
|
||||
|
||||
const handleDelete = async (deviceId) => {
|
||||
const handleDelete = async deviceId => {
|
||||
try {
|
||||
await axios.delete(`/api/idle-devices/${deviceId}`);
|
||||
message.success('删除成功');
|
||||
@@ -146,7 +146,7 @@ const IdleDeviceManagement = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleShelve = (record) => {
|
||||
const handleShelve = record => {
|
||||
setShelvingDevice(record);
|
||||
setShelvePositionConflict(null);
|
||||
setShelveSelectedRackId(null);
|
||||
@@ -195,7 +195,7 @@ const IdleDeviceManagement = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleShelveRackChange = (value) => {
|
||||
const handleShelveRackChange = value => {
|
||||
setShelveSelectedRackId(value);
|
||||
const position = shelveForm.getFieldValue('position');
|
||||
const height = shelveForm.getFieldValue('height');
|
||||
@@ -206,7 +206,7 @@ const IdleDeviceManagement = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleShelvePositionChange = (e) => {
|
||||
const handleShelvePositionChange = e => {
|
||||
const value = e.target.value ? parseInt(e.target.value) : null;
|
||||
const height = shelveForm.getFieldValue('height');
|
||||
if (shelveSelectedRackId && value) {
|
||||
@@ -216,7 +216,7 @@ const IdleDeviceManagement = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleShelveHeightChange = (e) => {
|
||||
const handleShelveHeightChange = e => {
|
||||
const value = e.target.value ? parseInt(e.target.value) : null;
|
||||
const position = shelveForm.getFieldValue('position');
|
||||
if (shelveSelectedRackId && position) {
|
||||
@@ -279,7 +279,7 @@ const IdleDeviceManagement = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const getIdleDays = (idleDate) => {
|
||||
const getIdleDays = idleDate => {
|
||||
if (!idleDate) return 0;
|
||||
const diff = new Date() - new Date(idleDate);
|
||||
return Math.floor(diff / (1000 * 60 * 60 * 24));
|
||||
@@ -292,7 +292,10 @@ const IdleDeviceManagement = () => {
|
||||
width: 60,
|
||||
align: 'center',
|
||||
render: (_, __, index) => (
|
||||
<Badge count={index + 1 + (pagination.current - 1) * pagination.pageSize} style={{ backgroundColor: '#f59e0b' }} />
|
||||
<Badge
|
||||
count={index + 1 + (pagination.current - 1) * pagination.pageSize}
|
||||
style={{ backgroundColor: '#f59e0b' }}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -302,16 +305,36 @@ const IdleDeviceManagement = () => {
|
||||
render: (_, record) => (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<Avatar
|
||||
style={{ backgroundColor: record.type === 'server' ? '#3b82f6' : record.type === 'switch' ? '#8b5cf6' : '#64748b' }}
|
||||
style={{
|
||||
backgroundColor:
|
||||
record.type === 'server'
|
||||
? '#3b82f6'
|
||||
: record.type === 'switch'
|
||||
? '#8b5cf6'
|
||||
: '#64748b',
|
||||
}}
|
||||
icon={<InboxOutlined />}
|
||||
/>
|
||||
<div>
|
||||
<Text strong style={{ fontSize: '14px', display: 'block' }}>{record.name || '-'}</Text>
|
||||
<Text strong style={{ fontSize: '14px', display: 'block' }}>
|
||||
{record.name || '-'}
|
||||
</Text>
|
||||
<Space size={4}>
|
||||
<Tag color={record.type === 'server' ? 'blue' : record.type === 'switch' ? 'purple' : 'default'} style={{ marginRight: 0 }}>
|
||||
<Tag
|
||||
color={
|
||||
record.type === 'server'
|
||||
? 'blue'
|
||||
: record.type === 'switch'
|
||||
? 'purple'
|
||||
: 'default'
|
||||
}
|
||||
style={{ marginRight: 0 }}
|
||||
>
|
||||
{record.type === 'server' ? '服务器' : record.type === 'switch' ? '交换机' : '其他'}
|
||||
</Tag>
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>{record.model || '-'}</Text>
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
{record.model || '-'}
|
||||
</Text>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
@@ -322,8 +345,10 @@ const IdleDeviceManagement = () => {
|
||||
dataIndex: 'deviceId',
|
||||
key: 'deviceId',
|
||||
width: 100,
|
||||
render: (text) => (
|
||||
<Text code style={{ fontSize: '12px', padding: '2px 6px' }}>{text}</Text>
|
||||
render: text => (
|
||||
<Text code style={{ fontSize: '12px', padding: '2px 6px' }}>
|
||||
{text}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -340,7 +365,13 @@ const IdleDeviceManagement = () => {
|
||||
);
|
||||
}
|
||||
if (record.sourceType === 'rack' && record.Rack) {
|
||||
const location = [record.Rack.Room?.name, record.Rack.name, record.position ? `U${record.position}` : null].filter(Boolean).join(' / ');
|
||||
const location = [
|
||||
record.Rack.Room?.name,
|
||||
record.Rack.name,
|
||||
record.position ? `U${record.position}` : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' / ');
|
||||
return (
|
||||
<Space>
|
||||
<InboxOutlined style={{ color: '#3b82f6' }} />
|
||||
@@ -363,7 +394,9 @@ const IdleDeviceManagement = () => {
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Text style={{ color, fontWeight: 600, fontSize: '16px' }}>{days}</Text>
|
||||
<br />
|
||||
<Text type="secondary" style={{ fontSize: '11px' }}>天</Text>
|
||||
<Text type="secondary" style={{ fontSize: '11px' }}>
|
||||
天
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
@@ -374,9 +407,11 @@ const IdleDeviceManagement = () => {
|
||||
key: 'idleReason',
|
||||
width: 140,
|
||||
ellipsis: true,
|
||||
render: (text) => (
|
||||
render: text => (
|
||||
<Tooltip title={text || '-'}>
|
||||
<Text type="secondary" ellipsis>{text || '-'}</Text>
|
||||
<Text type="secondary" ellipsis>
|
||||
{text || '-'}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
@@ -386,7 +421,7 @@ const IdleDeviceManagement = () => {
|
||||
key: 'sourceType',
|
||||
width: 80,
|
||||
align: 'center',
|
||||
render: (type) => (
|
||||
render: type => (
|
||||
<Tag color={type === 'warehouse' ? 'green' : 'blue'}>
|
||||
{type === 'warehouse' ? '库房' : '机架'}
|
||||
</Tag>
|
||||
@@ -409,11 +444,27 @@ const IdleDeviceManagement = () => {
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="编辑">
|
||||
<Button type="text" icon={<EditOutlined />} onClick={() => handleEdit(record)} style={{ borderRadius: '6px', color: '#3b82f6' }} />
|
||||
<Button
|
||||
type="text"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => handleEdit(record)}
|
||||
style={{ borderRadius: '6px', color: '#3b82f6' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Popconfirm title="确认删除?" onConfirm={() => handleDelete(record.deviceId)} okText="确认" cancelText="取消" okButtonProps={{ danger: true }}>
|
||||
<Popconfirm
|
||||
title="确认删除?"
|
||||
onConfirm={() => handleDelete(record.deviceId)}
|
||||
okText="确认"
|
||||
cancelText="取消"
|
||||
okButtonProps={{ danger: true }}
|
||||
>
|
||||
<Tooltip title="删除">
|
||||
<Button type="text" danger icon={<DeleteOutlined />} style={{ borderRadius: '6px' }} />
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
style={{ borderRadius: '6px' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
@@ -452,7 +503,9 @@ const IdleDeviceManagement = () => {
|
||||
return (
|
||||
<div style={{ minHeight: '100vh', background: '#f8fafc', padding: '24px' }}>
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<Title level={4} style={{ marginBottom: '4px', color: '#1e293b' }}>空闲设备管理</Title>
|
||||
<Title level={4} style={{ marginBottom: '4px', color: '#1e293b' }}>
|
||||
空闲设备管理
|
||||
</Title>
|
||||
<Text type="secondary">管理已下线或空闲的设备,支持恢复领用到设备管理</Text>
|
||||
</div>
|
||||
|
||||
@@ -468,24 +521,38 @@ const IdleDeviceManagement = () => {
|
||||
}}
|
||||
bodyStyle={{ padding: '20px' }}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div
|
||||
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}
|
||||
>
|
||||
<div>
|
||||
<Text type="secondary" style={{ fontSize: '13px' }}>{stat.title}</Text>
|
||||
<div style={{ fontSize: '28px', fontWeight: 700, color: stat.color, lineHeight: 1.2, marginTop: '4px' }}>
|
||||
<Text type="secondary" style={{ fontSize: '13px' }}>
|
||||
{stat.title}
|
||||
</Text>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '28px',
|
||||
fontWeight: 700,
|
||||
color: stat.color,
|
||||
lineHeight: 1.2,
|
||||
marginTop: '4px',
|
||||
}}
|
||||
>
|
||||
{stat.value}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{
|
||||
width: '48px',
|
||||
height: '48px',
|
||||
borderRadius: '12px',
|
||||
background: 'rgba(255,255,255,0.7)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '20px',
|
||||
color: stat.color
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
width: '48px',
|
||||
height: '48px',
|
||||
borderRadius: '12px',
|
||||
background: 'rgba(255,255,255,0.7)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '20px',
|
||||
color: stat.color,
|
||||
}}
|
||||
>
|
||||
{stat.icon}
|
||||
</div>
|
||||
</div>
|
||||
@@ -502,12 +569,14 @@ const IdleDeviceManagement = () => {
|
||||
}}
|
||||
bodyStyle={{ padding: 0 }}
|
||||
>
|
||||
<div style={{
|
||||
padding: '20px 24px',
|
||||
borderBottom: '1px solid #f1f5f9',
|
||||
background: 'linear-gradient(135deg, #fefefe 0%, #f8fafc 100%)',
|
||||
borderRadius: '16px 16px 0 0',
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
padding: '20px 24px',
|
||||
borderBottom: '1px solid #f1f5f9',
|
||||
background: 'linear-gradient(135deg, #fefefe 0%, #f8fafc 100%)',
|
||||
borderRadius: '16px 16px 0 0',
|
||||
}}
|
||||
>
|
||||
<Row gutter={16} align="middle">
|
||||
<Col flex="auto">
|
||||
<Space size="middle" wrap>
|
||||
@@ -516,7 +585,7 @@ const IdleDeviceManagement = () => {
|
||||
prefix={<SearchOutlined style={{ color: '#94a3b8' }} />}
|
||||
style={{ borderRadius: '10px', width: '260px', height: '40px' }}
|
||||
value={searchKeyword}
|
||||
onChange={(e) => setSearchKeyword(e.target.value)}
|
||||
onChange={e => setSearchKeyword(e.target.value)}
|
||||
allowClear
|
||||
/>
|
||||
<Select
|
||||
@@ -528,7 +597,11 @@ const IdleDeviceManagement = () => {
|
||||
<Option value="rack">机架</Option>
|
||||
<Option value="warehouse">库房</Option>
|
||||
</Select>
|
||||
<Button icon={<ReloadOutlined />} onClick={fetchIdleDevices} style={{ height: 40, borderRadius: '10px' }}>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={fetchIdleDevices}
|
||||
style={{ height: 40, borderRadius: '10px' }}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
</Space>
|
||||
@@ -561,12 +634,14 @@ const IdleDeviceManagement = () => {
|
||||
loading={loading}
|
||||
pagination={false}
|
||||
scroll={{ x: 1100 }}
|
||||
rowClassName={(record, index) => index % 2 === 0 ? 'table-row-even' : 'table-row-odd'}
|
||||
rowClassName={(record, index) => (index % 2 === 0 ? 'table-row-even' : 'table-row-odd')}
|
||||
style={{ borderRadius: '0 0 16px 16px' }}
|
||||
/>
|
||||
|
||||
{pagination.total > 0 && (
|
||||
<div style={{ padding: '16px 24px', borderTop: '1px solid #f1f5f9', background: '#fafafa' }}>
|
||||
<div
|
||||
style={{ padding: '16px 24px', borderTop: '1px solid #f1f5f9', background: '#fafafa' }}
|
||||
>
|
||||
<Row justify="space-between" align="middle">
|
||||
<Col>
|
||||
<Text type="secondary">
|
||||
@@ -579,11 +654,11 @@ const IdleDeviceManagement = () => {
|
||||
pageSize={pagination.pageSize}
|
||||
total={pagination.total}
|
||||
onChange={(page, pageSize) =>
|
||||
setPagination((prev) => ({ ...prev, current: page, pageSize }))
|
||||
setPagination(prev => ({ ...prev, current: page, pageSize }))
|
||||
}
|
||||
showSizeChanger
|
||||
showQuickJumper
|
||||
showTotal={(total) => `共 ${total} 条`}
|
||||
showTotal={total => `共 ${total} 条`}
|
||||
size="small"
|
||||
/>
|
||||
</Col>
|
||||
@@ -595,12 +670,14 @@ const IdleDeviceManagement = () => {
|
||||
<Modal
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<div style={{
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
borderRadius: '50%',
|
||||
background: editingDevice ? '#3b82f6' : '#f59e0b'
|
||||
}} />
|
||||
<div
|
||||
style={{
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
borderRadius: '50%',
|
||||
background: editingDevice ? '#3b82f6' : '#f59e0b',
|
||||
}}
|
||||
/>
|
||||
{editingDevice ? '编辑空闲设备' : '添加空闲设备'}
|
||||
</div>
|
||||
}
|
||||
@@ -615,28 +692,38 @@ const IdleDeviceManagement = () => {
|
||||
style={{ top: 100 }}
|
||||
>
|
||||
<Form form={form} layout="vertical" size="middle">
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, #fefefe 0%, #f8fafc 100%)',
|
||||
borderRadius: '12px',
|
||||
padding: '20px',
|
||||
marginBottom: '16px',
|
||||
border: '1px solid #e2e8f0'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 600,
|
||||
color: '#334155',
|
||||
<div
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #fefefe 0%, #f8fafc 100%)',
|
||||
borderRadius: '12px',
|
||||
padding: '20px',
|
||||
marginBottom: '16px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px'
|
||||
}}>
|
||||
<div style={{ width: '4px', height: '16px', background: '#3b82f6', borderRadius: '2px' }} />
|
||||
border: '1px solid #e2e8f0',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 600,
|
||||
color: '#334155',
|
||||
marginBottom: '16px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ width: '4px', height: '16px', background: '#3b82f6', borderRadius: '2px' }}
|
||||
/>
|
||||
设备基本信息
|
||||
</div>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item name="name" label="设备名称" rules={[{ required: true, message: '请输入设备名称' }]}>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="设备名称"
|
||||
rules={[{ required: true, message: '请输入设备名称' }]}
|
||||
>
|
||||
<Input placeholder="请输入设备名称" style={{ borderRadius: '8px' }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
@@ -667,36 +754,51 @@ const IdleDeviceManagement = () => {
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item name="powerConsumption" label="功耗(W)">
|
||||
<Input type="number" placeholder="请输入功耗" min={0} style={{ borderRadius: '8px' }} />
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="请输入功耗"
|
||||
min={0}
|
||||
style={{ borderRadius: '8px' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
{editingDevice && (
|
||||
<Col span={12}>
|
||||
<Form.Item label="设备ID">
|
||||
<Input value={editingDevice.deviceId} disabled style={{ borderRadius: '8px' }} />
|
||||
<Input
|
||||
value={editingDevice.deviceId}
|
||||
disabled
|
||||
style={{ borderRadius: '8px' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, #fefefe 0%, #f8fafc 100%)',
|
||||
borderRadius: '12px',
|
||||
padding: '20px',
|
||||
marginBottom: '16px',
|
||||
border: '1px solid #e2e8f0'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 600,
|
||||
color: '#334155',
|
||||
<div
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #fefefe 0%, #f8fafc 100%)',
|
||||
borderRadius: '12px',
|
||||
padding: '20px',
|
||||
marginBottom: '16px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px'
|
||||
}}>
|
||||
<div style={{ width: '4px', height: '16px', background: '#22c55e', borderRadius: '2px' }} />
|
||||
border: '1px solid #e2e8f0',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 600,
|
||||
color: '#334155',
|
||||
marginBottom: '16px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ width: '4px', height: '16px', background: '#22c55e', borderRadius: '2px' }}
|
||||
/>
|
||||
位置信息
|
||||
</div>
|
||||
<Row gutter={16}>
|
||||
@@ -705,7 +807,7 @@ const IdleDeviceManagement = () => {
|
||||
<Select
|
||||
placeholder="请选择机房"
|
||||
allowClear
|
||||
onChange={(value) => {
|
||||
onChange={value => {
|
||||
setSelectedRoomId(value);
|
||||
form.setFieldsValue({ rackId: null, position: null });
|
||||
if (value) {
|
||||
@@ -714,7 +816,7 @@ const IdleDeviceManagement = () => {
|
||||
}}
|
||||
style={{ borderRadius: '8px' }}
|
||||
>
|
||||
{rooms.map((room) => (
|
||||
{rooms.map(room => (
|
||||
<Option key={room.roomId} value={room.roomId}>
|
||||
{room.name}
|
||||
</Option>
|
||||
@@ -725,14 +827,14 @@ const IdleDeviceManagement = () => {
|
||||
<Col span={8}>
|
||||
<Form.Item name="rackId" label="机柜">
|
||||
<Select
|
||||
placeholder={selectedRoomId ? "请选择机柜" : "请先选择机房"}
|
||||
placeholder={selectedRoomId ? '请选择机柜' : '请先选择机房'}
|
||||
allowClear
|
||||
disabled={!selectedRoomId}
|
||||
style={{ borderRadius: '8px' }}
|
||||
>
|
||||
{racks
|
||||
.filter((rack) => rack.roomId === selectedRoomId)
|
||||
.map((rack) => (
|
||||
.filter(rack => rack.roomId === selectedRoomId)
|
||||
.map(rack => (
|
||||
<Option key={rack.rackId} value={rack.rackId}>
|
||||
{rack.name}
|
||||
</Option>
|
||||
@@ -742,11 +844,19 @@ const IdleDeviceManagement = () => {
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item name="position" label="U位">
|
||||
<Input type="number" placeholder="请输入U位" min={1} disabled={!selectedRoomId} style={{ borderRadius: '8px' }} />
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="请输入U位"
|
||||
min={1}
|
||||
disabled={!selectedRoomId}
|
||||
style={{ borderRadius: '8px' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<div style={{ textAlign: 'center', color: '#94a3b8', fontSize: '12px', margin: '8px 0' }}>
|
||||
<div
|
||||
style={{ textAlign: 'center', color: '#94a3b8', fontSize: '12px', margin: '8px 0' }}
|
||||
>
|
||||
— 或 —
|
||||
</div>
|
||||
<Row gutter={16}>
|
||||
@@ -769,28 +879,37 @@ const IdleDeviceManagement = () => {
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, #fefefe 0%, #f8fafc 100%)',
|
||||
borderRadius: '12px',
|
||||
padding: '20px',
|
||||
border: '1px solid #e2e8f0'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 600,
|
||||
color: '#334155',
|
||||
marginBottom: '16px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px'
|
||||
}}>
|
||||
<div style={{ width: '4px', height: '16px', background: '#64748b', borderRadius: '2px' }} />
|
||||
<div
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #fefefe 0%, #f8fafc 100%)',
|
||||
borderRadius: '12px',
|
||||
padding: '20px',
|
||||
border: '1px solid #e2e8f0',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 600,
|
||||
color: '#334155',
|
||||
marginBottom: '16px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ width: '4px', height: '16px', background: '#64748b', borderRadius: '2px' }}
|
||||
/>
|
||||
附加信息
|
||||
</div>
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
<Form.Item name="idleReason" label="空闲原因">
|
||||
<Input placeholder="请输入空闲原因,如:设备下线、备件库存等" style={{ borderRadius: '8px' }} />
|
||||
<Input
|
||||
placeholder="请输入空闲原因,如:设备下线、备件库存等"
|
||||
style={{ borderRadius: '8px' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -808,7 +927,9 @@ const IdleDeviceManagement = () => {
|
||||
<Modal
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<div style={{ width: '8px', height: '8px', borderRadius: '50%', background: '#22c55e' }} />
|
||||
<div
|
||||
style={{ width: '8px', height: '8px', borderRadius: '50%', background: '#22c55e' }}
|
||||
/>
|
||||
设备上架
|
||||
</div>
|
||||
}
|
||||
@@ -822,33 +943,47 @@ const IdleDeviceManagement = () => {
|
||||
bodyStyle={{ padding: '24px' }}
|
||||
>
|
||||
<Form form={shelveForm} layout="vertical" size="middle">
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, #fefefe 0%, #f8fafc 100%)',
|
||||
borderRadius: '12px',
|
||||
padding: '20px',
|
||||
marginBottom: '16px',
|
||||
border: '1px solid #e2e8f0'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 600,
|
||||
color: '#334155',
|
||||
<div
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #fefefe 0%, #f8fafc 100%)',
|
||||
borderRadius: '12px',
|
||||
padding: '20px',
|
||||
marginBottom: '16px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px'
|
||||
}}>
|
||||
<div style={{ width: '4px', height: '16px', background: '#3b82f6', borderRadius: '2px' }} />
|
||||
border: '1px solid #e2e8f0',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 600,
|
||||
color: '#334155',
|
||||
marginBottom: '16px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ width: '4px', height: '16px', background: '#3b82f6', borderRadius: '2px' }}
|
||||
/>
|
||||
设备基本信息
|
||||
</div>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item name="name" label="设备名称" rules={[{ required: true, message: '请输入设备名称' }]}>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="设备名称"
|
||||
rules={[{ required: true, message: '请输入设备名称' }]}
|
||||
>
|
||||
<Input placeholder="请输入设备名称" style={{ borderRadius: '8px' }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="type" label="设备类型" rules={[{ required: true, message: '请选择设备类型' }]}>
|
||||
<Form.Item
|
||||
name="type"
|
||||
label="设备类型"
|
||||
rules={[{ required: true, message: '请选择设备类型' }]}
|
||||
>
|
||||
<Select placeholder="请选择设备类型" style={{ borderRadius: '8px' }}>
|
||||
<Option value="server">服务器</Option>
|
||||
<Option value="switch">交换机</Option>
|
||||
@@ -866,7 +1001,11 @@ const IdleDeviceManagement = () => {
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="serialNumber" label="序列号" rules={[{ required: true, message: '请输入序列号' }]}>
|
||||
<Form.Item
|
||||
name="serialNumber"
|
||||
label="序列号"
|
||||
rules={[{ required: true, message: '请输入序列号' }]}
|
||||
>
|
||||
<Input placeholder="请输入序列号" style={{ borderRadius: '8px' }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
@@ -874,34 +1013,50 @@ const IdleDeviceManagement = () => {
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item name="height" label="高度(U)">
|
||||
<Input type="number" placeholder="请输入高度" min={1} style={{ borderRadius: '8px' }} />
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="请输入高度"
|
||||
min={1}
|
||||
style={{ borderRadius: '8px' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="powerConsumption" label="功率(W)">
|
||||
<Input type="number" placeholder="请输入功率" min={0} style={{ borderRadius: '8px' }} />
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="请输入功率"
|
||||
min={0}
|
||||
style={{ borderRadius: '8px' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, #fefefe 0%, #f8fafc 100%)',
|
||||
borderRadius: '12px',
|
||||
padding: '20px',
|
||||
marginBottom: '16px',
|
||||
border: '1px solid #e2e8f0'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 600,
|
||||
color: '#334155',
|
||||
<div
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #fefefe 0%, #f8fafc 100%)',
|
||||
borderRadius: '12px',
|
||||
padding: '20px',
|
||||
marginBottom: '16px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px'
|
||||
}}>
|
||||
<div style={{ width: '4px', height: '16px', background: '#22c55e', borderRadius: '2px' }} />
|
||||
border: '1px solid #e2e8f0',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 600,
|
||||
color: '#334155',
|
||||
marginBottom: '16px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ width: '4px', height: '16px', background: '#22c55e', borderRadius: '2px' }}
|
||||
/>
|
||||
上架位置
|
||||
</div>
|
||||
<Row gutter={16}>
|
||||
@@ -909,13 +1064,13 @@ const IdleDeviceManagement = () => {
|
||||
<Form.Item name="roomId" label="机房">
|
||||
<Select
|
||||
placeholder="请选择机房"
|
||||
onChange={(value) => {
|
||||
onChange={value => {
|
||||
setSelectedShelveRoomId(value);
|
||||
shelveForm.setFieldsValue({ rackId: null });
|
||||
}}
|
||||
style={{ borderRadius: '8px' }}
|
||||
>
|
||||
{rooms.map((room) => (
|
||||
{rooms.map(room => (
|
||||
<Option key={room.roomId} value={room.roomId}>
|
||||
{room.name}
|
||||
</Option>
|
||||
@@ -926,14 +1081,14 @@ const IdleDeviceManagement = () => {
|
||||
<Col span={8}>
|
||||
<Form.Item name="rackId" label="机柜">
|
||||
<Select
|
||||
placeholder={selectedShelveRoomId ? "请选择机柜" : "请先选择机房"}
|
||||
placeholder={selectedShelveRoomId ? '请选择机柜' : '请先选择机房'}
|
||||
disabled={!selectedShelveRoomId}
|
||||
style={{ borderRadius: '8px' }}
|
||||
onChange={handleShelveRackChange}
|
||||
>
|
||||
{racks
|
||||
.filter((rack) => rack.roomId === selectedShelveRoomId)
|
||||
.map((rack) => (
|
||||
.filter(rack => rack.roomId === selectedShelveRoomId)
|
||||
.map(rack => (
|
||||
<Option key={rack.rackId} value={rack.rackId}>
|
||||
{rack.name}
|
||||
</Option>
|
||||
@@ -943,47 +1098,67 @@ const IdleDeviceManagement = () => {
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item name="position" label="U位">
|
||||
<Input type="number" placeholder="请输入U位" min={1} style={{ borderRadius: '8px' }} onChange={handleShelvePositionChange} />
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="请输入U位"
|
||||
min={1}
|
||||
style={{ borderRadius: '8px' }}
|
||||
onChange={handleShelvePositionChange}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
{shelvePositionConflict && (
|
||||
<div style={{ marginTop: '12px' }}>
|
||||
<div style={{
|
||||
background: '#fef2f2',
|
||||
border: '1px solid #fecaca',
|
||||
borderRadius: '8px',
|
||||
padding: '12px 16px',
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
gap: '10px'
|
||||
}}>
|
||||
<ExclamationCircleOutlined style={{ color: '#ef4444', fontSize: '18px', marginTop: '2px' }} />
|
||||
<div
|
||||
style={{
|
||||
background: '#fef2f2',
|
||||
border: '1px solid #fecaca',
|
||||
borderRadius: '8px',
|
||||
padding: '12px 16px',
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
gap: '10px',
|
||||
}}
|
||||
>
|
||||
<ExclamationCircleOutlined
|
||||
style={{ color: '#ef4444', fontSize: '18px', marginTop: '2px' }}
|
||||
/>
|
||||
<div>
|
||||
<div style={{ color: '#dc2626', fontWeight: 600, marginBottom: '4px' }}>U位冲突</div>
|
||||
<div style={{ color: '#991b1b', fontSize: '13px' }}>{shelvePositionConflict}</div>
|
||||
<div style={{ color: '#dc2626', fontWeight: 600, marginBottom: '4px' }}>
|
||||
U位冲突
|
||||
</div>
|
||||
<div style={{ color: '#991b1b', fontSize: '13px' }}>
|
||||
{shelvePositionConflict}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, #fefefe 0%, #f8fafc 100%)',
|
||||
borderRadius: '12px',
|
||||
padding: '20px',
|
||||
border: '1px solid #e2e8f0'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 600,
|
||||
color: '#334155',
|
||||
marginBottom: '16px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px'
|
||||
}}>
|
||||
<div style={{ width: '4px', height: '16px', background: '#64748b', borderRadius: '2px' }} />
|
||||
<div
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #fefefe 0%, #f8fafc 100%)',
|
||||
borderRadius: '12px',
|
||||
padding: '20px',
|
||||
border: '1px solid #e2e8f0',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 600,
|
||||
color: '#334155',
|
||||
marginBottom: '16px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ width: '4px', height: '16px', background: '#64748b', borderRadius: '2px' }}
|
||||
/>
|
||||
备注信息
|
||||
</div>
|
||||
<Row gutter={16}>
|
||||
@@ -1022,4 +1197,4 @@ const IdleDeviceManagement = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default IdleDeviceManagement;
|
||||
export default IdleDeviceManagement;
|
||||
|
||||
@@ -48,7 +48,7 @@ const api = axios.create({
|
||||
baseURL: '/api',
|
||||
});
|
||||
|
||||
api.interceptors.request.use((config) => {
|
||||
api.interceptors.request.use(config => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
@@ -92,7 +92,7 @@ const InventoryManagement = () => {
|
||||
};
|
||||
const res = await api.get('/inventory/plans', { params });
|
||||
setPlans(res.data.plans || []);
|
||||
setPagination((prev) => ({
|
||||
setPagination(prev => ({
|
||||
...prev,
|
||||
total: res.data.total || 0,
|
||||
}));
|
||||
@@ -119,7 +119,7 @@ const InventoryManagement = () => {
|
||||
const fetchRooms = async () => {
|
||||
try {
|
||||
const res = await api.get('/rooms', { params: { pageSize: 1000 } });
|
||||
setRooms(Array.isArray(res.data) ? res.data : (res.data.rooms || []));
|
||||
setRooms(Array.isArray(res.data) ? res.data : res.data.rooms || []);
|
||||
} catch (error) {
|
||||
console.error('获取机房失败', error);
|
||||
}
|
||||
@@ -128,7 +128,7 @@ const InventoryManagement = () => {
|
||||
const fetchRacks = async () => {
|
||||
try {
|
||||
const res = await api.get('/racks', { params: { pageSize: 1000 } });
|
||||
const allRacks = Array.isArray(res.data) ? res.data : (res.data.racks || []);
|
||||
const allRacks = Array.isArray(res.data) ? res.data : res.data.racks || [];
|
||||
setRacks(allRacks);
|
||||
setFilteredRacks(allRacks);
|
||||
} catch (error) {
|
||||
@@ -157,20 +157,21 @@ const InventoryManagement = () => {
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
const handleEdit = (record) => {
|
||||
const handleEdit = record => {
|
||||
setEditingPlan(record);
|
||||
const targetRooms = record.targetRooms || [];
|
||||
setSelectedRooms(targetRooms);
|
||||
|
||||
|
||||
if (targetRooms.length > 0) {
|
||||
const filtered = racks.filter(rack =>
|
||||
targetRooms.includes(rack.roomId) || (rack.Room && targetRooms.includes(rack.Room.roomId))
|
||||
const filtered = racks.filter(
|
||||
rack =>
|
||||
targetRooms.includes(rack.roomId) || (rack.Room && targetRooms.includes(rack.Room.roomId))
|
||||
);
|
||||
setFilteredRacks(filtered);
|
||||
} else {
|
||||
setFilteredRacks(racks);
|
||||
}
|
||||
|
||||
|
||||
form.setFieldsValue({
|
||||
name: record.name,
|
||||
type: record.type,
|
||||
@@ -182,7 +183,7 @@ const InventoryManagement = () => {
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
const handleDelete = async (planId) => {
|
||||
const handleDelete = async planId => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: '确定要删除这个盘点计划吗?此操作不可恢复!',
|
||||
@@ -202,7 +203,7 @@ const InventoryManagement = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async (values) => {
|
||||
const handleSubmit = async values => {
|
||||
try {
|
||||
const data = {
|
||||
...values,
|
||||
@@ -226,7 +227,7 @@ const InventoryManagement = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleStart = async (plan) => {
|
||||
const handleStart = async plan => {
|
||||
try {
|
||||
await api.post(`/inventory/plans/${plan.planId}/start`);
|
||||
message.success('盘点任务已启动');
|
||||
@@ -237,7 +238,7 @@ const InventoryManagement = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleComplete = async (plan) => {
|
||||
const handleComplete = async plan => {
|
||||
try {
|
||||
await api.post(`/inventory/plans/${plan.planId}/complete`);
|
||||
message.success('盘点已完成');
|
||||
@@ -248,23 +249,23 @@ const InventoryManagement = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewTasks = (plan) => {
|
||||
const handleViewTasks = plan => {
|
||||
navigate(`/inventory/execution?planId=${plan.planId}`);
|
||||
};
|
||||
|
||||
const handleRoomsChange = (roomIds) => {
|
||||
const handleRoomsChange = roomIds => {
|
||||
setSelectedRooms(roomIds || []);
|
||||
if (!roomIds || roomIds.length === 0) {
|
||||
setFilteredRacks(racks);
|
||||
} else {
|
||||
const filtered = racks.filter(rack =>
|
||||
roomIds.includes(rack.roomId) || (rack.Room && roomIds.includes(rack.Room.roomId))
|
||||
const filtered = racks.filter(
|
||||
rack => roomIds.includes(rack.roomId) || (rack.Room && roomIds.includes(rack.Room.roomId))
|
||||
);
|
||||
setFilteredRacks(filtered);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusTag = (status) => {
|
||||
const getStatusTag = status => {
|
||||
const statusMap = {
|
||||
draft: { color: 'default', text: '草稿', icon: <FileSearchOutlined /> },
|
||||
pending: { color: 'orange', text: '待执行', icon: <ClockCircleOutlined /> },
|
||||
@@ -274,17 +275,13 @@ const InventoryManagement = () => {
|
||||
};
|
||||
const config = statusMap[status] || statusMap.draft;
|
||||
return (
|
||||
<Tag
|
||||
color={config.color}
|
||||
icon={config.icon}
|
||||
style={{ borderRadius: 6, padding: '2px 8px' }}
|
||||
>
|
||||
<Tag color={config.color} icon={config.icon} style={{ borderRadius: 6, padding: '2px 8px' }}>
|
||||
{config.text}
|
||||
</Tag>
|
||||
);
|
||||
};
|
||||
|
||||
const getTypeTag = (type) => {
|
||||
const getTypeTag = type => {
|
||||
const typeMap = {
|
||||
full: { color: 'blue', text: '全面盘点' },
|
||||
partial: { color: 'cyan', text: '局部盘点' },
|
||||
@@ -294,7 +291,7 @@ const InventoryManagement = () => {
|
||||
return <Tag color={config.color}>{config.text}</Tag>;
|
||||
};
|
||||
|
||||
const getProgressPercent = (plan) => {
|
||||
const getProgressPercent = plan => {
|
||||
if (!plan.totalDevices || plan.totalDevices === 0) return 0;
|
||||
return Math.round((plan.checkedDevices / plan.totalDevices) * 100);
|
||||
};
|
||||
@@ -334,14 +331,14 @@ const InventoryManagement = () => {
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
width: 100,
|
||||
render: (type) => getTypeTag(type),
|
||||
render: type => getTypeTag(type),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 120,
|
||||
render: (status) => getStatusTag(status),
|
||||
render: status => getStatusTag(status),
|
||||
},
|
||||
{
|
||||
title: '盘点进度',
|
||||
@@ -357,8 +354,8 @@ const InventoryManagement = () => {
|
||||
{getProgressPercent(record)}%
|
||||
</span>
|
||||
</div>
|
||||
<Progress
|
||||
percent={getProgressPercent(record)}
|
||||
<Progress
|
||||
percent={getProgressPercent(record)}
|
||||
size="small"
|
||||
strokeColor={{
|
||||
'0%': '#108ee9',
|
||||
@@ -372,25 +369,26 @@ const InventoryManagement = () => {
|
||||
title: '异常设备',
|
||||
key: 'abnormal',
|
||||
width: 100,
|
||||
render: (_, record) => (
|
||||
record.abnormalDevices > 0 ?
|
||||
<Tag color="error">{record.abnormalDevices} 异常</Tag> :
|
||||
render: (_, record) =>
|
||||
record.abnormalDevices > 0 ? (
|
||||
<Tag color="error">{record.abnormalDevices} 异常</Tag>
|
||||
) : (
|
||||
<span style={{ color: '#8c8c8c' }}>-</span>
|
||||
),
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '创建人',
|
||||
dataIndex: ['Creator', 'realName'],
|
||||
key: 'creator',
|
||||
width: 100,
|
||||
render: (name) => name || '-',
|
||||
render: name => name || '-',
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createdAt',
|
||||
key: 'createdAt',
|
||||
width: 160,
|
||||
render: (date) => (date ? dayjs(date).format('YYYY-MM-DD HH:mm') : '-'),
|
||||
render: date => (date ? dayjs(date).format('YYYY-MM-DD HH:mm') : '-'),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
@@ -482,21 +480,29 @@ const InventoryManagement = () => {
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ padding: 24, background: designTokens.colors.background.secondary, minHeight: '100vh' }}>
|
||||
<div
|
||||
style={{
|
||||
padding: 24,
|
||||
background: designTokens.colors.background.secondary,
|
||||
minHeight: '100vh',
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<Row gutter={[16, 16]}>
|
||||
{statCards.map((stat, index) => (
|
||||
<Col xs={24} sm={8} key={index}>
|
||||
<Card
|
||||
bordered={false}
|
||||
style={{
|
||||
<Card
|
||||
bordered={false}
|
||||
style={{
|
||||
borderRadius: 16,
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.06)',
|
||||
background: stat.gradient,
|
||||
}}
|
||||
bodyStyle={{ padding: 20 }}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div
|
||||
style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}
|
||||
>
|
||||
<div>
|
||||
<div style={{ color: 'rgba(255,255,255,0.9)', fontSize: 14, marginBottom: 8 }}>
|
||||
{stat.title}
|
||||
@@ -505,17 +511,19 @@ const InventoryManagement = () => {
|
||||
{stat.value || 0}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: 12,
|
||||
background: 'rgba(255,255,255,0.2)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: 24,
|
||||
color: '#fff'
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: 12,
|
||||
background: 'rgba(255,255,255,0.2)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: 24,
|
||||
color: '#fff',
|
||||
}}
|
||||
>
|
||||
{stat.icon}
|
||||
</div>
|
||||
</div>
|
||||
@@ -525,25 +533,27 @@ const InventoryManagement = () => {
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
<Card
|
||||
bordered={false}
|
||||
style={{ borderRadius: 16, boxShadow: '0 2px 8px rgba(0,0,0,0.06)' }}
|
||||
>
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onChange={setActiveTab}
|
||||
style={{ marginBottom: 16 }}
|
||||
>
|
||||
<Card bordered={false} style={{ borderRadius: 16, boxShadow: '0 2px 8px rgba(0,0,0,0.06)' }}>
|
||||
<Tabs activeKey={activeTab} onChange={setActiveTab} style={{ marginBottom: 16 }}>
|
||||
<TabPane tab="盘点计划列表" key="list">
|
||||
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 12 }}>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: 16,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
gap: 12,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', gap: 12, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<Input
|
||||
placeholder="搜索计划名称"
|
||||
prefix={<SearchOutlined />}
|
||||
style={{ width: 240, borderRadius: 8 }}
|
||||
onChange={(e) => setSearchParamsObj({ keyword: e.target.value })}
|
||||
onChange={e => setSearchParamsObj({ keyword: e.target.value })}
|
||||
onPressEnter={() => {
|
||||
setPagination((prev) => ({ ...prev, current: 1 }));
|
||||
setPagination(prev => ({ ...prev, current: 1 }));
|
||||
fetchPlans();
|
||||
}}
|
||||
/>
|
||||
@@ -551,9 +561,9 @@ const InventoryManagement = () => {
|
||||
placeholder="选择状态"
|
||||
style={{ width: 140, borderRadius: 8 }}
|
||||
allowClear
|
||||
onChange={(value) => {
|
||||
setSearchParamsObj((prev) => ({ ...prev, status: value }));
|
||||
setPagination((prev) => ({ ...prev, current: 1 }));
|
||||
onChange={value => {
|
||||
setSearchParamsObj(prev => ({ ...prev, status: value }));
|
||||
setPagination(prev => ({ ...prev, current: 1 }));
|
||||
}}
|
||||
>
|
||||
<Select.Option value="draft">草稿</Select.Option>
|
||||
@@ -561,17 +571,21 @@ const InventoryManagement = () => {
|
||||
<Select.Option value="in_progress">进行中</Select.Option>
|
||||
<Select.Option value="completed">已完成</Select.Option>
|
||||
</Select>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={() => { setSearchParamsObj({}); fetchPlans(); fetchStats(); }}
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={() => {
|
||||
setSearchParamsObj({});
|
||||
fetchPlans();
|
||||
fetchStats();
|
||||
}}
|
||||
style={{ borderRadius: 8 }}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={handleAdd}
|
||||
style={{ borderRadius: 8, height: 40 }}
|
||||
>
|
||||
@@ -588,14 +602,16 @@ const InventoryManagement = () => {
|
||||
...pagination,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total) => `共 ${total} 条记录`,
|
||||
showTotal: total => `共 ${total} 条记录`,
|
||||
onChange: (page, pageSize) => {
|
||||
setPagination((prev) => ({ ...prev, current: page, pageSize }));
|
||||
setPagination(prev => ({ ...prev, current: page, pageSize }));
|
||||
},
|
||||
}}
|
||||
scroll={{ x: 1200 }}
|
||||
locale={{
|
||||
emptyText: <Empty description="暂无盘点计划" image={Empty.PRESENTED_IMAGE_SIMPLE} />,
|
||||
emptyText: (
|
||||
<Empty description="暂无盘点计划" image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</TabPane>
|
||||
@@ -615,17 +631,13 @@ const InventoryManagement = () => {
|
||||
footer={null}
|
||||
width={640}
|
||||
centered
|
||||
styles={{
|
||||
styles={{
|
||||
header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' },
|
||||
body: { padding: 24 },
|
||||
footer: { borderTop: '1px solid #f0f0f0', padding: '12px 24px' }
|
||||
footer: { borderTop: '1px solid #f0f0f0', padding: '12px 24px' },
|
||||
}}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleSubmit}
|
||||
>
|
||||
<Form form={form} layout="vertical" onFinish={handleSubmit}>
|
||||
<Row gutter={16}>
|
||||
<Col span={16}>
|
||||
<Form.Item
|
||||
@@ -652,35 +664,26 @@ const InventoryManagement = () => {
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item
|
||||
name="description"
|
||||
label="描述"
|
||||
>
|
||||
<Form.Item name="description" label="描述">
|
||||
<Input.TextArea rows={2} placeholder="请输入描述" style={{ borderRadius: 8 }} />
|
||||
</Form.Item>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="scheduledDate"
|
||||
label="计划日期"
|
||||
>
|
||||
<Form.Item name="scheduledDate" label="计划日期">
|
||||
<DatePicker style={{ width: '100%', borderRadius: 8 }} placeholder="选择计划日期" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="targetRooms"
|
||||
label="目标机房"
|
||||
>
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder="选择目标机房(不选则为全部)"
|
||||
<Form.Item name="targetRooms" label="目标机房">
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder="选择目标机房(不选则为全部)"
|
||||
allowClear
|
||||
onChange={handleRoomsChange}
|
||||
style={{ borderRadius: 8 }}
|
||||
>
|
||||
{rooms.map((room) => (
|
||||
{rooms.map(room => (
|
||||
<Select.Option key={room.roomId} value={room.roomId}>
|
||||
{room.name}
|
||||
</Select.Option>
|
||||
@@ -690,17 +693,14 @@ const InventoryManagement = () => {
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item
|
||||
name="targetRacks"
|
||||
label="目标机柜"
|
||||
>
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder="选择目标机柜(不选则为全部)"
|
||||
<Form.Item name="targetRacks" label="目标机柜">
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder="选择目标机柜(不选则为全部)"
|
||||
allowClear
|
||||
style={{ borderRadius: 8 }}
|
||||
>
|
||||
{filteredRacks.map((rack) => (
|
||||
{filteredRacks.map(rack => (
|
||||
<Select.Option key={rack.rackId} value={rack.rackId}>
|
||||
{rack.name} ({rack.Room?.name || ''})
|
||||
</Select.Option>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -374,9 +374,7 @@ const Login = () => {
|
||||
>
|
||||
{headerContent.title}
|
||||
</Title>
|
||||
<Text style={{ fontSize: '15px', color: '#64748b' }}>
|
||||
{headerContent.subtitle}
|
||||
</Text>
|
||||
<Text style={{ fontSize: '15px', color: '#64748b' }}>{headerContent.subtitle}</Text>
|
||||
</div>
|
||||
|
||||
{/* 首次使用提示 */}
|
||||
@@ -436,7 +434,11 @@ const Login = () => {
|
||||
rules={[{ required: true, message: '请输入真实姓名' }]}
|
||||
>
|
||||
<Input
|
||||
prefix={<SafetyCertificateOutlined style={{ color: '#94a3b8', fontSize: '18px' }} />}
|
||||
prefix={
|
||||
<SafetyCertificateOutlined
|
||||
style={{ color: '#94a3b8', fontSize: '18px' }}
|
||||
/>
|
||||
}
|
||||
placeholder="请输入真实姓名"
|
||||
style={{ borderRadius: '12px', height: '48px' }}
|
||||
/>
|
||||
|
||||
@@ -27,7 +27,11 @@ import {
|
||||
import api from '../api';
|
||||
import CloseButton from '../components/CloseButton';
|
||||
import dayjs from 'dayjs';
|
||||
import { selectStyles, filterInputStyles, inputPlaceholders } from '../styles/deviceManagementStyles';
|
||||
import {
|
||||
selectStyles,
|
||||
filterInputStyles,
|
||||
inputPlaceholders,
|
||||
} from '../styles/deviceManagementStyles';
|
||||
|
||||
const { RangePicker } = DatePicker;
|
||||
const { Option } = Select;
|
||||
@@ -98,7 +102,7 @@ function OperationLogs() {
|
||||
...prev,
|
||||
current: page,
|
||||
pageSize,
|
||||
total: response.data.total
|
||||
total: response.data.total,
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -135,7 +139,7 @@ function OperationLogs() {
|
||||
fetchLogs(1, pagination.pageSize, clearedFilters);
|
||||
};
|
||||
|
||||
const handleViewDetail = async (record) => {
|
||||
const handleViewDetail = async record => {
|
||||
setDetailLoading(true);
|
||||
setDetailVisible(true);
|
||||
try {
|
||||
@@ -151,7 +155,7 @@ function OperationLogs() {
|
||||
}
|
||||
};
|
||||
|
||||
const getModuleTag = (module) => {
|
||||
const getModuleTag = module => {
|
||||
const colors = {
|
||||
device: 'blue',
|
||||
user: 'green',
|
||||
@@ -175,7 +179,7 @@ function OperationLogs() {
|
||||
return <Tag color={colors[module] || 'default'}>{names[module] || module}</Tag>;
|
||||
};
|
||||
|
||||
const getOperationTag = (type) => {
|
||||
const getOperationTag = type => {
|
||||
const colors = {
|
||||
create: 'green',
|
||||
update: 'blue',
|
||||
@@ -203,10 +207,8 @@ function OperationLogs() {
|
||||
return <Tag color={colors[type] || 'default'}>{names[type] || type}</Tag>;
|
||||
};
|
||||
|
||||
const getResultTag = (result) => {
|
||||
return result === 'success'
|
||||
? <Tag color="success">成功</Tag>
|
||||
: <Tag color="error">失败</Tag>;
|
||||
const getResultTag = result => {
|
||||
return result === 'success' ? <Tag color="success">成功</Tag> : <Tag color="error">失败</Tag>;
|
||||
};
|
||||
|
||||
const columns = [
|
||||
@@ -272,18 +274,19 @@ function OperationLogs() {
|
||||
width: 80,
|
||||
fixed: 'right',
|
||||
render: (_, record) => (
|
||||
<Button
|
||||
type="link"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => handleViewDetail(record)}
|
||||
>
|
||||
<Button type="link" icon={<EyeOutlined />} onClick={() => handleViewDetail(record)}>
|
||||
详情
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const hasFilters = filters.module || filters.operationType || filters.keyword || filters.dateRange || filters.result;
|
||||
const hasFilters =
|
||||
filters.module ||
|
||||
filters.operationType ||
|
||||
filters.keyword ||
|
||||
filters.dateRange ||
|
||||
filters.result;
|
||||
|
||||
return (
|
||||
<div style={{ padding: '24px' }}>
|
||||
@@ -311,7 +314,9 @@ function OperationLogs() {
|
||||
onChange={value => handleFilterChange('module', value)}
|
||||
>
|
||||
{MODULE_OPTIONS.map(opt => (
|
||||
<Option key={opt.value} value={opt.value}>{opt.label}</Option>
|
||||
<Option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Col>
|
||||
@@ -324,7 +329,9 @@ function OperationLogs() {
|
||||
onChange={value => handleFilterChange('operationType', value)}
|
||||
>
|
||||
{OPERATION_TYPE_OPTIONS.map(opt => (
|
||||
<Option key={opt.value} value={opt.value}>{opt.label}</Option>
|
||||
<Option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Col>
|
||||
@@ -337,7 +344,9 @@ function OperationLogs() {
|
||||
onChange={value => handleFilterChange('result', value)}
|
||||
>
|
||||
{RESULT_OPTIONS.map(opt => (
|
||||
<Option key={opt.value} value={opt.value}>{opt.label}</Option>
|
||||
<Option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Col>
|
||||
@@ -368,10 +377,7 @@ function OperationLogs() {
|
||||
筛选
|
||||
</Button>
|
||||
{hasFilters && (
|
||||
<Button
|
||||
icon={<ClearOutlined />}
|
||||
onClick={handleClearFilters}
|
||||
>
|
||||
<Button icon={<ClearOutlined />} onClick={handleClearFilters}>
|
||||
清除
|
||||
</Button>
|
||||
)}
|
||||
@@ -391,7 +397,7 @@ function OperationLogs() {
|
||||
total: pagination.total,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
showTotal: total => `共 ${total} 条`,
|
||||
pageSizeOptions: ['10', '20', '50', '100'],
|
||||
}}
|
||||
onChange={handleTableChange}
|
||||
@@ -428,7 +434,9 @@ function OperationLogs() {
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="模块">{currentLog.module}</Descriptions.Item>
|
||||
<Descriptions.Item label="操作类型">{currentLog.operationType}</Descriptions.Item>
|
||||
<Descriptions.Item label="操作描述">{currentLog.operationDescription}</Descriptions.Item>
|
||||
<Descriptions.Item label="操作描述">
|
||||
{currentLog.operationDescription}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="目标ID">{currentLog.targetId || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="目标名称">{currentLog.targetName || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="操作人ID">{currentLog.operatorId}</Descriptions.Item>
|
||||
@@ -450,7 +458,15 @@ function OperationLogs() {
|
||||
{currentLog.beforeState && (
|
||||
<>
|
||||
<Text strong>变更前:</Text>
|
||||
<pre style={{ background: '#f5f5f5', padding: 12, borderRadius: 4, overflow: 'auto', maxHeight: 200 }}>
|
||||
<pre
|
||||
style={{
|
||||
background: '#f5f5f5',
|
||||
padding: 12,
|
||||
borderRadius: 4,
|
||||
overflow: 'auto',
|
||||
maxHeight: 200,
|
||||
}}
|
||||
>
|
||||
{JSON.stringify(currentLog.beforeState, null, 2)}
|
||||
</pre>
|
||||
</>
|
||||
@@ -458,7 +474,15 @@ function OperationLogs() {
|
||||
{currentLog.afterState && (
|
||||
<>
|
||||
<Text strong>变更后:</Text>
|
||||
<pre style={{ background: '#f0f0f0', padding: 12, borderRadius: 4, overflow: 'auto', maxHeight: 200 }}>
|
||||
<pre
|
||||
style={{
|
||||
background: '#f0f0f0',
|
||||
padding: 12,
|
||||
borderRadius: 4,
|
||||
overflow: 'auto',
|
||||
maxHeight: 200,
|
||||
}}
|
||||
>
|
||||
{JSON.stringify(currentLog.afterState, null, 2)}
|
||||
</pre>
|
||||
</>
|
||||
@@ -469,7 +493,15 @@ function OperationLogs() {
|
||||
{currentLog && currentLog.metadata && Object.keys(currentLog.metadata).length > 0 && (
|
||||
<>
|
||||
<h4 style={{ marginTop: 16 }}>扩展信息</h4>
|
||||
<pre style={{ background: '#f5f5f5', padding: 12, borderRadius: 4, overflow: 'auto', maxHeight: 200 }}>
|
||||
<pre
|
||||
style={{
|
||||
background: '#f5f5f5',
|
||||
padding: 12,
|
||||
borderRadius: 4,
|
||||
overflow: 'auto',
|
||||
maxHeight: 200,
|
||||
}}
|
||||
>
|
||||
{JSON.stringify(currentLog.metadata, null, 2)}
|
||||
</pre>
|
||||
</>
|
||||
|
||||
@@ -42,7 +42,7 @@ const api = axios.create({
|
||||
baseURL: '/api',
|
||||
});
|
||||
|
||||
api.interceptors.request.use((config) => {
|
||||
api.interceptors.request.use(config => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
@@ -64,13 +64,13 @@ const PendingDeviceManagement = () => {
|
||||
const [currentDevice, setCurrentDevice] = useState(null);
|
||||
const [form] = Form.useForm();
|
||||
const [selectedRoomId, setSelectedRoomId] = useState(null);
|
||||
|
||||
|
||||
const [pagination, setPagination] = useState({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
|
||||
const [filters, setFilters] = useState({
|
||||
status: null,
|
||||
planId: null,
|
||||
@@ -79,27 +79,133 @@ const PendingDeviceManagement = () => {
|
||||
});
|
||||
|
||||
const defaultDeviceFields = [
|
||||
{ fieldName: 'deviceName', displayName: '设备名称', fieldType: 'string', required: true, visible: true, order: 1 },
|
||||
{ fieldName: 'deviceType', displayName: '设备类型', fieldType: 'select', required: true, visible: true, order: 2, options: [
|
||||
{ value: 'server', label: '服务器' },
|
||||
{ value: 'switch', label: '交换机' },
|
||||
{ value: 'router', label: '路由器' },
|
||||
{ value: 'storage', label: '存储设备' },
|
||||
{ value: 'other', label: '其他' },
|
||||
]},
|
||||
{ fieldName: 'model', displayName: '型号', fieldType: 'string', required: false, visible: true, order: 3 },
|
||||
{ fieldName: 'serialNumber', displayName: '序列号', fieldType: 'string', required: true, visible: true, order: 4 },
|
||||
{ fieldName: 'roomId', displayName: '所属机房', fieldType: 'select', required: false, visible: true, order: 5 },
|
||||
{ fieldName: 'rackId', displayName: '所属机柜', fieldType: 'select', required: false, visible: true, order: 6 },
|
||||
{ fieldName: 'position', displayName: '位置(U)', fieldType: 'number', required: false, visible: true, order: 7 },
|
||||
{ fieldName: 'height', displayName: '高度(U)', fieldType: 'number', required: false, visible: true, order: 8 },
|
||||
{ fieldName: 'powerConsumption', displayName: '功率(W)', fieldType: 'number', required: false, visible: true, order: 9 },
|
||||
{ fieldName: 'purchaseDate', displayName: '购买日期', fieldType: 'date', required: false, visible: true, order: 10 },
|
||||
{ fieldName: 'warrantyExpiry', displayName: '保修到期', fieldType: 'date', required: false, visible: true, order: 11 },
|
||||
{ fieldName: 'ipAddress', displayName: 'IP地址', fieldType: 'string', required: false, visible: true, order: 12 },
|
||||
{ fieldName: 'brand', displayName: '品牌', fieldType: 'string', required: false, visible: true, order: 13 },
|
||||
{ fieldName: 'description', displayName: '描述', fieldType: 'textarea', required: false, visible: true, order: 14 },
|
||||
{ fieldName: 'remark', displayName: '备注', fieldType: 'textarea', required: false, visible: true, order: 15 },
|
||||
{
|
||||
fieldName: 'deviceName',
|
||||
displayName: '设备名称',
|
||||
fieldType: 'string',
|
||||
required: true,
|
||||
visible: true,
|
||||
order: 1,
|
||||
},
|
||||
{
|
||||
fieldName: 'deviceType',
|
||||
displayName: '设备类型',
|
||||
fieldType: 'select',
|
||||
required: true,
|
||||
visible: true,
|
||||
order: 2,
|
||||
options: [
|
||||
{ value: 'server', label: '服务器' },
|
||||
{ value: 'switch', label: '交换机' },
|
||||
{ value: 'router', label: '路由器' },
|
||||
{ value: 'storage', label: '存储设备' },
|
||||
{ value: 'other', label: '其他' },
|
||||
],
|
||||
},
|
||||
{
|
||||
fieldName: 'model',
|
||||
displayName: '型号',
|
||||
fieldType: 'string',
|
||||
required: false,
|
||||
visible: true,
|
||||
order: 3,
|
||||
},
|
||||
{
|
||||
fieldName: 'serialNumber',
|
||||
displayName: '序列号',
|
||||
fieldType: 'string',
|
||||
required: true,
|
||||
visible: true,
|
||||
order: 4,
|
||||
},
|
||||
{
|
||||
fieldName: 'roomId',
|
||||
displayName: '所属机房',
|
||||
fieldType: 'select',
|
||||
required: false,
|
||||
visible: true,
|
||||
order: 5,
|
||||
},
|
||||
{
|
||||
fieldName: 'rackId',
|
||||
displayName: '所属机柜',
|
||||
fieldType: 'select',
|
||||
required: false,
|
||||
visible: true,
|
||||
order: 6,
|
||||
},
|
||||
{
|
||||
fieldName: 'position',
|
||||
displayName: '位置(U)',
|
||||
fieldType: 'number',
|
||||
required: false,
|
||||
visible: true,
|
||||
order: 7,
|
||||
},
|
||||
{
|
||||
fieldName: 'height',
|
||||
displayName: '高度(U)',
|
||||
fieldType: 'number',
|
||||
required: false,
|
||||
visible: true,
|
||||
order: 8,
|
||||
},
|
||||
{
|
||||
fieldName: 'powerConsumption',
|
||||
displayName: '功率(W)',
|
||||
fieldType: 'number',
|
||||
required: false,
|
||||
visible: true,
|
||||
order: 9,
|
||||
},
|
||||
{
|
||||
fieldName: 'purchaseDate',
|
||||
displayName: '购买日期',
|
||||
fieldType: 'date',
|
||||
required: false,
|
||||
visible: true,
|
||||
order: 10,
|
||||
},
|
||||
{
|
||||
fieldName: 'warrantyExpiry',
|
||||
displayName: '保修到期',
|
||||
fieldType: 'date',
|
||||
required: false,
|
||||
visible: true,
|
||||
order: 11,
|
||||
},
|
||||
{
|
||||
fieldName: 'ipAddress',
|
||||
displayName: 'IP地址',
|
||||
fieldType: 'string',
|
||||
required: false,
|
||||
visible: true,
|
||||
order: 12,
|
||||
},
|
||||
{
|
||||
fieldName: 'brand',
|
||||
displayName: '品牌',
|
||||
fieldType: 'string',
|
||||
required: false,
|
||||
visible: true,
|
||||
order: 13,
|
||||
},
|
||||
{
|
||||
fieldName: 'description',
|
||||
displayName: '描述',
|
||||
fieldType: 'textarea',
|
||||
required: false,
|
||||
visible: true,
|
||||
order: 14,
|
||||
},
|
||||
{
|
||||
fieldName: 'remark',
|
||||
displayName: '备注',
|
||||
fieldType: 'textarea',
|
||||
required: false,
|
||||
visible: true,
|
||||
order: 15,
|
||||
},
|
||||
];
|
||||
|
||||
const fetchDeviceFields = async () => {
|
||||
@@ -185,11 +291,11 @@ const PendingDeviceManagement = () => {
|
||||
fetchDeviceFields();
|
||||
}, []);
|
||||
|
||||
const filteredRacks = selectedRoomId
|
||||
const filteredRacks = selectedRoomId
|
||||
? racks.filter(rack => rack.roomId === selectedRoomId)
|
||||
: racks;
|
||||
|
||||
const handleSync = async (pendingId) => {
|
||||
const handleSync = async pendingId => {
|
||||
try {
|
||||
const res = await api.post(`/inventory/pending-devices/${pendingId}/sync`);
|
||||
message.success(res.data.message);
|
||||
@@ -205,7 +311,7 @@ const PendingDeviceManagement = () => {
|
||||
message.warning('请先选择要同步的设备');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const res = await api.post('/inventory/pending-devices/batch-sync', {
|
||||
pendingIds: selectedRowKeys,
|
||||
@@ -219,27 +325,27 @@ const PendingDeviceManagement = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = (record) => {
|
||||
const handleEdit = record => {
|
||||
setCurrentDevice(record);
|
||||
setSelectedRoomId(record.roomId);
|
||||
|
||||
|
||||
// 字段名映射:数据库字段名 -> PendingDevice 模型字段名
|
||||
const fieldMapping = {
|
||||
'name': 'deviceName',
|
||||
'type': 'deviceType',
|
||||
'SN': 'serialNumber',
|
||||
name: 'deviceName',
|
||||
type: 'deviceType',
|
||||
SN: 'serialNumber',
|
||||
};
|
||||
|
||||
|
||||
// 确保使用 deviceFields 中定义的字段名来设置表单值
|
||||
const formValues = {
|
||||
serialNumber: record.serialNumber,
|
||||
roomId: record.roomId,
|
||||
rackId: record.rackId,
|
||||
};
|
||||
|
||||
|
||||
// 使用已加载的字段配置或默认配置
|
||||
const fields = deviceFields || defaultDeviceFields;
|
||||
|
||||
|
||||
// 遍历字段,将 record 中对应的值设置到表单
|
||||
fields.forEach(field => {
|
||||
const fieldName = field.fieldName;
|
||||
@@ -249,7 +355,7 @@ const PendingDeviceManagement = () => {
|
||||
formValues[fieldName] = record[recordFieldName];
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// 处理自定义字段
|
||||
if (record.customFields) {
|
||||
Object.entries(record.customFields).forEach(([key, value]) => {
|
||||
@@ -258,21 +364,21 @@ const PendingDeviceManagement = () => {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
form.setFieldsValue(formValues);
|
||||
setEditModalVisible(true);
|
||||
};
|
||||
|
||||
const handleEditSubmit = async (values) => {
|
||||
const handleEditSubmit = async values => {
|
||||
try {
|
||||
const { purchaseDate, warrantyExpiry, ...otherValues } = values;
|
||||
|
||||
|
||||
const payload = {
|
||||
...otherValues,
|
||||
purchaseDate: purchaseDate ? dayjs(purchaseDate).toISOString() : null,
|
||||
warrantyExpiry: warrantyExpiry ? dayjs(warrantyExpiry).toISOString() : null
|
||||
warrantyExpiry: warrantyExpiry ? dayjs(warrantyExpiry).toISOString() : null,
|
||||
};
|
||||
|
||||
|
||||
await api.put(`/inventory/pending-devices/${currentDevice.pendingId}`, payload);
|
||||
message.success('更新成功');
|
||||
setEditModalVisible(false);
|
||||
@@ -283,7 +389,7 @@ const PendingDeviceManagement = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (pendingId) => {
|
||||
const handleDelete = async pendingId => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: '确定要删除这个待同步设备吗?此操作不可恢复!',
|
||||
@@ -303,16 +409,20 @@ const PendingDeviceManagement = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const getStatusTag = (status) => {
|
||||
const getStatusTag = status => {
|
||||
const statusMap = {
|
||||
pending: { color: 'processing', text: '待同步', icon: <SyncOutlined spin /> },
|
||||
synced: { color: 'success', text: '已同步', icon: <CheckCircleOutlined /> },
|
||||
};
|
||||
const config = statusMap[status] || statusMap.pending;
|
||||
return <Tag color={config.color} icon={config.icon}>{config.text}</Tag>;
|
||||
return (
|
||||
<Tag color={config.color} icon={config.icon}>
|
||||
{config.text}
|
||||
</Tag>
|
||||
);
|
||||
};
|
||||
|
||||
const getTypeLabel = (type) => {
|
||||
const getTypeLabel = type => {
|
||||
const typeMap = {
|
||||
server: '服务器',
|
||||
switch: '交换机',
|
||||
@@ -323,7 +433,7 @@ const PendingDeviceManagement = () => {
|
||||
return typeMap[type] || type;
|
||||
};
|
||||
|
||||
const renderFormField = (field) => {
|
||||
const renderFormField = field => {
|
||||
const { fieldName, displayName, fieldType, required, options } = field;
|
||||
|
||||
if (fieldName === 'roomId') {
|
||||
@@ -334,12 +444,12 @@ const PendingDeviceManagement = () => {
|
||||
label={displayName}
|
||||
rules={required ? [{ required: true, message: `请选择${displayName}` }] : []}
|
||||
>
|
||||
<Select
|
||||
placeholder={`请选择${displayName}`}
|
||||
allowClear
|
||||
showSearch
|
||||
<Select
|
||||
placeholder={`请选择${displayName}`}
|
||||
allowClear
|
||||
showSearch
|
||||
optionFilterProp="children"
|
||||
onChange={(value) => {
|
||||
onChange={value => {
|
||||
setSelectedRoomId(value);
|
||||
form.setFieldsValue({ rackId: undefined });
|
||||
}}
|
||||
@@ -351,7 +461,9 @@ const PendingDeviceManagement = () => {
|
||||
</Select.Option>
|
||||
))
|
||||
) : (
|
||||
<Select.Option value="" disabled>暂无机房数据</Select.Option>
|
||||
<Select.Option value="" disabled>
|
||||
暂无机房数据
|
||||
</Select.Option>
|
||||
)}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
@@ -367,10 +479,10 @@ const PendingDeviceManagement = () => {
|
||||
label={displayName}
|
||||
rules={required ? [{ required: true, message: `请选择${displayName}` }] : []}
|
||||
>
|
||||
<Select
|
||||
placeholder={selectedRoomId ? `请选择${displayName}` : "请先选择机房"}
|
||||
allowClear
|
||||
showSearch
|
||||
<Select
|
||||
placeholder={selectedRoomId ? `请选择${displayName}` : '请先选择机房'}
|
||||
allowClear
|
||||
showSearch
|
||||
optionFilterProp="children"
|
||||
disabled={!selectedRoomId}
|
||||
>
|
||||
@@ -382,10 +494,14 @@ const PendingDeviceManagement = () => {
|
||||
</Select.Option>
|
||||
))
|
||||
) : (
|
||||
<Select.Option value="" disabled>该机房下无机柜</Select.Option>
|
||||
<Select.Option value="" disabled>
|
||||
该机房下无机柜
|
||||
</Select.Option>
|
||||
)
|
||||
) : (
|
||||
<Select.Option value="" disabled>请先选择机房</Select.Option>
|
||||
<Select.Option value="" disabled>
|
||||
请先选择机房
|
||||
</Select.Option>
|
||||
)}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
@@ -401,13 +517,16 @@ const PendingDeviceManagement = () => {
|
||||
rules={required ? [{ required: true, message: `请选择${displayName}` }] : []}
|
||||
>
|
||||
<Select placeholder={`请选择${displayName}`}>
|
||||
{(Array.isArray(options) ? options : [
|
||||
{ value: 'server', label: '服务器' },
|
||||
{ value: 'switch', label: '交换机' },
|
||||
{ value: 'router', label: '路由器' },
|
||||
{ value: 'storage', label: '存储设备' },
|
||||
{ value: 'other', label: '其他' },
|
||||
]).map(opt => (
|
||||
{(Array.isArray(options)
|
||||
? options
|
||||
: [
|
||||
{ value: 'server', label: '服务器' },
|
||||
{ value: 'switch', label: '交换机' },
|
||||
{ value: 'router', label: '路由器' },
|
||||
{ value: 'storage', label: '存储设备' },
|
||||
{ value: 'other', label: '其他' },
|
||||
]
|
||||
).map(opt => (
|
||||
<Select.Option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</Select.Option>
|
||||
@@ -490,11 +609,11 @@ const PendingDeviceManagement = () => {
|
||||
const renderFormFields = () => {
|
||||
// 如果字段配置未加载,使用默认配置
|
||||
const fields = deviceFields || defaultDeviceFields;
|
||||
|
||||
|
||||
// 排除机房和机柜字段(已在Modal中单独渲染)
|
||||
const textFields = []; // 描述、备注等全文本字段
|
||||
const otherFields = []; // 其他字段
|
||||
|
||||
|
||||
fields.forEach(field => {
|
||||
// 排除 roomId 和 rackId
|
||||
if (field.fieldName === 'roomId' || field.fieldName === 'rackId') {
|
||||
@@ -514,15 +633,11 @@ const PendingDeviceManagement = () => {
|
||||
{/* 其他字段两列布局 */}
|
||||
{otherFields.length > 0 && (
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
{otherFields.filter((_, i) => i % 2 === 0).map(renderFormField)}
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
{otherFields.filter((_, i) => i % 2 === 1).map(renderFormField)}
|
||||
</Col>
|
||||
<Col span={12}>{otherFields.filter((_, i) => i % 2 === 0).map(renderFormField)}</Col>
|
||||
<Col span={12}>{otherFields.filter((_, i) => i % 2 === 1).map(renderFormField)}</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
|
||||
{/* 文本字段全文本显示 */}
|
||||
{textFields.map(renderFormField)}
|
||||
</>
|
||||
@@ -548,7 +663,7 @@ const PendingDeviceManagement = () => {
|
||||
dataIndex: 'deviceType',
|
||||
key: 'deviceType',
|
||||
width: 100,
|
||||
render: (type) => getTypeLabel(type),
|
||||
render: type => getTypeLabel(type),
|
||||
},
|
||||
{
|
||||
title: '位置',
|
||||
@@ -556,8 +671,8 @@ const PendingDeviceManagement = () => {
|
||||
width: 180,
|
||||
render: (_, record) => (
|
||||
<span>
|
||||
{record.Room?.name || '-'}
|
||||
{record.Rack ? ` / ${record.Rack.name}` : ''}
|
||||
{record.Room?.name || '-'}
|
||||
{record.Rack ? ` / ${record.Rack.name}` : ''}
|
||||
{record.position ? ` / U${record.position}` : ''}
|
||||
</span>
|
||||
),
|
||||
@@ -567,35 +682,35 @@ const PendingDeviceManagement = () => {
|
||||
dataIndex: ['Plan', 'name'],
|
||||
key: 'planName',
|
||||
width: 150,
|
||||
render: (name) => name || '-',
|
||||
render: name => name || '-',
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
render: (status) => getStatusTag(status),
|
||||
render: status => getStatusTag(status),
|
||||
},
|
||||
{
|
||||
title: '创建人',
|
||||
dataIndex: ['Creator', 'realName'],
|
||||
key: 'creator',
|
||||
width: 100,
|
||||
render: (name) => name || '-',
|
||||
render: name => name || '-',
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createdAt',
|
||||
key: 'createdAt',
|
||||
width: 140,
|
||||
render: (date) => date ? dayjs(date).format('YYYY-MM-DD HH:mm') : '-',
|
||||
render: date => (date ? dayjs(date).format('YYYY-MM-DD HH:mm') : '-'),
|
||||
},
|
||||
{
|
||||
title: '同步时间',
|
||||
dataIndex: 'syncedAt',
|
||||
key: 'syncedAt',
|
||||
width: 140,
|
||||
render: (date) => date ? dayjs(date).format('YYYY-MM-DD HH:mm') : '-',
|
||||
render: date => (date ? dayjs(date).format('YYYY-MM-DD HH:mm') : '-'),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
@@ -641,11 +756,7 @@ const PendingDeviceManagement = () => {
|
||||
cancelText="取消"
|
||||
>
|
||||
<Tooltip title="删除">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<DeleteOutlined />}
|
||||
style={{ color: '#ff4d4f' }}
|
||||
/>
|
||||
<Button type="text" icon={<DeleteOutlined />} style={{ color: '#ff4d4f' }} />
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
@@ -654,15 +765,17 @@ const PendingDeviceManagement = () => {
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ padding: 24, background: designTokens.colors.background.secondary, minHeight: '100vh' }}>
|
||||
<div
|
||||
style={{
|
||||
padding: 24,
|
||||
background: designTokens.colors.background.secondary,
|
||||
minHeight: '100vh',
|
||||
}}
|
||||
>
|
||||
<Row gutter={16} style={{ marginBottom: 24 }}>
|
||||
<Col span={6}>
|
||||
<Card bordered={false} style={{ borderRadius: 12, height: '100%', minHeight: 100 }}>
|
||||
<Statistic
|
||||
title="暂存设备总数"
|
||||
value={stats.total}
|
||||
prefix={<InboxOutlined />}
|
||||
/>
|
||||
<Statistic title="暂存设备总数" value={stats.total} prefix={<InboxOutlined />} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
@@ -692,8 +805,8 @@ const PendingDeviceManagement = () => {
|
||||
value={stats.total > 0 ? Math.round((stats.synced / stats.total) * 100) : 0}
|
||||
suffix="%"
|
||||
/>
|
||||
<Progress
|
||||
percent={stats.total > 0 ? Math.round((stats.synced / stats.total) * 100) : 0}
|
||||
<Progress
|
||||
percent={stats.total > 0 ? Math.round((stats.synced / stats.total) * 100) : 0}
|
||||
showInfo={false}
|
||||
strokeColor="#52c41a"
|
||||
/>
|
||||
@@ -702,14 +815,23 @@ const PendingDeviceManagement = () => {
|
||||
</Row>
|
||||
|
||||
<Card bordered={false} style={{ borderRadius: 12 }}>
|
||||
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 12 }}>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: 16,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
gap: 12,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', gap: 12, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<Input
|
||||
placeholder="搜索序列号/设备名称"
|
||||
prefix={<SearchOutlined />}
|
||||
style={{ width: 200 }}
|
||||
value={filters.keyword}
|
||||
onChange={(e) => setFilters(prev => ({ ...prev, keyword: e.target.value }))}
|
||||
onChange={e => setFilters(prev => ({ ...prev, keyword: e.target.value }))}
|
||||
onPressEnter={() => setPagination(prev => ({ ...prev, current: 1 }))}
|
||||
/>
|
||||
<Select
|
||||
@@ -717,7 +839,7 @@ const PendingDeviceManagement = () => {
|
||||
style={{ width: 120 }}
|
||||
allowClear
|
||||
value={filters.status}
|
||||
onChange={(value) => setFilters(prev => ({ ...prev, status: value }))}
|
||||
onChange={value => setFilters(prev => ({ ...prev, status: value }))}
|
||||
>
|
||||
<Select.Option value="pending">待同步</Select.Option>
|
||||
<Select.Option value="synced">已同步</Select.Option>
|
||||
@@ -729,7 +851,7 @@ const PendingDeviceManagement = () => {
|
||||
showSearch
|
||||
optionFilterProp="children"
|
||||
value={filters.planId}
|
||||
onChange={(value) => setFilters(prev => ({ ...prev, planId: value }))}
|
||||
onChange={value => setFilters(prev => ({ ...prev, planId: value }))}
|
||||
>
|
||||
{plans.map(plan => (
|
||||
<Select.Option key={plan.planId} value={plan.planId}>
|
||||
@@ -762,7 +884,7 @@ const PendingDeviceManagement = () => {
|
||||
rowSelection={{
|
||||
selectedRowKeys,
|
||||
onChange: setSelectedRowKeys,
|
||||
getCheckboxProps: (record) => ({
|
||||
getCheckboxProps: record => ({
|
||||
disabled: record.status !== 'pending',
|
||||
}),
|
||||
}}
|
||||
@@ -774,7 +896,7 @@ const PendingDeviceManagement = () => {
|
||||
...pagination,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
showTotal: total => `共 ${total} 条`,
|
||||
onChange: (page, pageSize) => {
|
||||
setPagination(prev => ({ ...prev, current: page, pageSize }));
|
||||
},
|
||||
@@ -797,11 +919,7 @@ const PendingDeviceManagement = () => {
|
||||
footer={null}
|
||||
width={600}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleEditSubmit}
|
||||
>
|
||||
<Form form={form} layout="vertical" onFinish={handleEditSubmit}>
|
||||
<Descriptions bordered size="small" column={2} style={{ marginBottom: 16 }}>
|
||||
<Descriptions.Item label="序列号">
|
||||
<code>{currentDevice?.serialNumber}</code>
|
||||
@@ -814,16 +932,13 @@ const PendingDeviceManagement = () => {
|
||||
{/* 机房机柜选择 - 始终显示 */}
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="roomId"
|
||||
label="所属机房"
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择机房"
|
||||
allowClear
|
||||
showSearch
|
||||
<Form.Item name="roomId" label="所属机房">
|
||||
<Select
|
||||
placeholder="请选择机房"
|
||||
allowClear
|
||||
showSearch
|
||||
optionFilterProp="children"
|
||||
onChange={(value) => {
|
||||
onChange={value => {
|
||||
setSelectedRoomId(value);
|
||||
form.setFieldsValue({ rackId: undefined });
|
||||
}}
|
||||
@@ -835,20 +950,19 @@ const PendingDeviceManagement = () => {
|
||||
</Select.Option>
|
||||
))
|
||||
) : (
|
||||
<Select.Option value="" disabled>暂无机房数据</Select.Option>
|
||||
<Select.Option value="" disabled>
|
||||
暂无机房数据
|
||||
</Select.Option>
|
||||
)}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="rackId"
|
||||
label="所属机柜"
|
||||
>
|
||||
<Select
|
||||
placeholder={selectedRoomId ? "请选择机柜" : "请先选择机房"}
|
||||
allowClear
|
||||
showSearch
|
||||
<Form.Item name="rackId" label="所属机柜">
|
||||
<Select
|
||||
placeholder={selectedRoomId ? '请选择机柜' : '请先选择机房'}
|
||||
allowClear
|
||||
showSearch
|
||||
optionFilterProp="children"
|
||||
disabled={!selectedRoomId}
|
||||
>
|
||||
@@ -860,10 +974,14 @@ const PendingDeviceManagement = () => {
|
||||
</Select.Option>
|
||||
))
|
||||
) : (
|
||||
<Select.Option value="" disabled>该机房下无机柜</Select.Option>
|
||||
<Select.Option value="" disabled>
|
||||
该机房下无机柜
|
||||
</Select.Option>
|
||||
)
|
||||
) : (
|
||||
<Select.Option value="" disabled>请先选择机房</Select.Option>
|
||||
<Select.Option value="" disabled>
|
||||
请先选择机房
|
||||
</Select.Option>
|
||||
)}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
@@ -874,10 +992,14 @@ const PendingDeviceManagement = () => {
|
||||
|
||||
<Form.Item style={{ marginBottom: 0, textAlign: 'right', marginTop: 16 }}>
|
||||
<Space>
|
||||
<Button onClick={() => {
|
||||
setEditModalVisible(false);
|
||||
setSelectedRoomId(null);
|
||||
}}>取消</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setEditModalVisible(false);
|
||||
setSelectedRoomId(null);
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="primary" htmlType="submit">
|
||||
保存
|
||||
</Button>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -371,7 +371,7 @@ const Rack3DVisualization = () => {
|
||||
|
||||
// 计算已用U位(考虑设备高度)
|
||||
const usedU = devices.reduce((sum, device) => sum + (device.height || 1), 0);
|
||||
|
||||
|
||||
// 计算总功率
|
||||
const totalPower = devices.reduce((sum, device) => {
|
||||
const power = parseFloat(device.powerConsumption) || 0;
|
||||
@@ -380,7 +380,7 @@ const Rack3DVisualization = () => {
|
||||
|
||||
// 机柜总高度
|
||||
const totalHeight = selectedRack.height || 45;
|
||||
|
||||
|
||||
// 可用U位
|
||||
const availableU = totalHeight - usedU;
|
||||
|
||||
@@ -568,7 +568,8 @@ const Rack3DVisualization = () => {
|
||||
position: 'absolute',
|
||||
top: 88,
|
||||
right: 24,
|
||||
background: 'linear-gradient(135deg, rgba(15, 23, 42, 0.95) 0%, rgba(30, 41, 59, 0.9) 100%)',
|
||||
background:
|
||||
'linear-gradient(135deg, rgba(15, 23, 42, 0.95) 0%, rgba(30, 41, 59, 0.9) 100%)',
|
||||
backdropFilter: 'blur(16px)',
|
||||
padding: isRackInfoCollapsed ? '14px 18px' : '20px',
|
||||
borderRadius: '20px',
|
||||
@@ -615,10 +616,13 @@ const Rack3DVisualization = () => {
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: '50%',
|
||||
background: 'linear-gradient(180deg, rgba(255,255,255,0.15) 0%, transparent 100%)',
|
||||
background:
|
||||
'linear-gradient(180deg, rgba(255,255,255,0.15) 0%, transparent 100%)',
|
||||
}}
|
||||
/>
|
||||
<CloudServerOutlined style={{ color: 'white', fontSize: 22, position: 'relative', zIndex: 1 }} />
|
||||
<CloudServerOutlined
|
||||
style={{ color: 'white', fontSize: 22, position: 'relative', zIndex: 1 }}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
@@ -640,13 +644,15 @@ const Rack3DVisualization = () => {
|
||||
>
|
||||
{selectedRack.name}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '12px',
|
||||
color: 'rgba(148, 163, 184, 0.8)',
|
||||
marginTop: 4,
|
||||
fontFamily: 'monospace',
|
||||
letterSpacing: '0.5px',
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '12px',
|
||||
color: 'rgba(148, 163, 184, 0.8)',
|
||||
marginTop: 4,
|
||||
fontFamily: 'monospace',
|
||||
letterSpacing: '0.5px',
|
||||
}}
|
||||
>
|
||||
#{selectedRack.rackId}
|
||||
</div>
|
||||
</div>
|
||||
@@ -660,7 +666,7 @@ const Rack3DVisualization = () => {
|
||||
e.stopPropagation();
|
||||
setIsRackInfoCollapsed(!isRackInfoCollapsed);
|
||||
}}
|
||||
style={{
|
||||
style={{
|
||||
color: 'rgba(148, 163, 184, 0.7)',
|
||||
marginLeft: isRackInfoCollapsed ? 0 : 8,
|
||||
width: isRackInfoCollapsed ? 44 : 32,
|
||||
@@ -701,10 +707,24 @@ const Rack3DVisualization = () => {
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: '22px', fontWeight: 700, color: '#60a5fa', lineHeight: 1 }}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '22px',
|
||||
fontWeight: 700,
|
||||
color: '#60a5fa',
|
||||
lineHeight: 1,
|
||||
}}
|
||||
>
|
||||
{selectedRack.height}
|
||||
</div>
|
||||
<div style={{ fontSize: '10px', color: 'rgba(148, 163, 184, 0.7)', marginTop: 4, fontWeight: 500 }}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '10px',
|
||||
color: 'rgba(148, 163, 184, 0.7)',
|
||||
marginTop: 4,
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
总高度
|
||||
</div>
|
||||
</div>
|
||||
@@ -717,10 +737,24 @@ const Rack3DVisualization = () => {
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: '22px', fontWeight: 700, color: '#34d399', lineHeight: 1 }}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '22px',
|
||||
fontWeight: 700,
|
||||
color: '#34d399',
|
||||
lineHeight: 1,
|
||||
}}
|
||||
>
|
||||
{devices.length}
|
||||
</div>
|
||||
<div style={{ fontSize: '10px', color: 'rgba(148, 163, 184, 0.7)', marginTop: 4, fontWeight: 500 }}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '10px',
|
||||
color: 'rgba(148, 163, 184, 0.7)',
|
||||
marginTop: 4,
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
设备数
|
||||
</div>
|
||||
</div>
|
||||
@@ -733,10 +767,24 @@ const Rack3DVisualization = () => {
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: '22px', fontWeight: 700, color: '#fbbf24', lineHeight: 1 }}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '22px',
|
||||
fontWeight: 700,
|
||||
color: '#fbbf24',
|
||||
lineHeight: 1,
|
||||
}}
|
||||
>
|
||||
{Math.round((devices.length / selectedRack.height) * 100)}%
|
||||
</div>
|
||||
<div style={{ fontSize: '10px', color: 'rgba(148, 163, 184, 0.7)', marginTop: 4, fontWeight: 500 }}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '10px',
|
||||
color: 'rgba(148, 163, 184, 0.7)',
|
||||
marginTop: 4,
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
负载率
|
||||
</div>
|
||||
</div>
|
||||
@@ -752,49 +800,86 @@ const Rack3DVisualization = () => {
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: '11px', color: 'rgba(148, 163, 184, 0.6)', marginBottom: 12, fontWeight: 600, letterSpacing: '0.5px', textTransform: 'uppercase' }}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '11px',
|
||||
color: 'rgba(148, 163, 184, 0.6)',
|
||||
marginBottom: 12,
|
||||
fontWeight: 600,
|
||||
letterSpacing: '0.5px',
|
||||
textTransform: 'uppercase',
|
||||
}}
|
||||
>
|
||||
资源监控
|
||||
</div>
|
||||
{[
|
||||
{
|
||||
label: 'U位使用',
|
||||
value: `${rackStats.usedU}U / ${selectedRack.height}U`,
|
||||
status: rackStats.usagePercent > 80 ? 'warning' : 'normal',
|
||||
{
|
||||
label: 'U位使用',
|
||||
value: `${rackStats.usedU}U / ${selectedRack.height}U`,
|
||||
status: rackStats.usagePercent > 80 ? 'warning' : 'normal',
|
||||
icon: '📊',
|
||||
subValue: `剩余 ${rackStats.availableU}U`,
|
||||
},
|
||||
{
|
||||
label: '总功率',
|
||||
value: `${rackStats.totalPower.toFixed(1)}kW`,
|
||||
status: 'normal',
|
||||
{
|
||||
label: '总功率',
|
||||
value: `${rackStats.totalPower.toFixed(1)}kW`,
|
||||
status: 'normal',
|
||||
icon: '⚡',
|
||||
subValue: '当前负载',
|
||||
},
|
||||
{
|
||||
label: '设备数',
|
||||
value: `${devices.length} 台`,
|
||||
status: 'normal',
|
||||
{
|
||||
label: '设备数',
|
||||
value: `${devices.length} 台`,
|
||||
status: 'normal',
|
||||
icon: '🖥️',
|
||||
subValue: `负载率 ${rackStats.usagePercent}%`,
|
||||
},
|
||||
].map((item, i) => (
|
||||
<div key={i} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: i < 2 ? 12 : 0 }}>
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: i < 2 ? 12 : 0,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<span style={{ fontSize: 16 }}>{item.icon}</span>
|
||||
<div>
|
||||
<div style={{ fontSize: '13px', color: 'rgba(226, 232, 240, 0.9)', fontWeight: 500 }}>{item.label}</div>
|
||||
<div style={{ fontSize: '11px', color: 'rgba(148, 163, 184, 0.6)', marginTop: 2 }}>{item.subValue}</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '13px',
|
||||
color: 'rgba(226, 232, 240, 0.9)',
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '11px',
|
||||
color: 'rgba(148, 163, 184, 0.6)',
|
||||
marginTop: 2,
|
||||
}}
|
||||
>
|
||||
{item.subValue}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<span style={{ fontSize: '14px', fontWeight: 700, color: '#f8fafc' }}>{item.value}</span>
|
||||
<div style={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: '50%',
|
||||
background: item.status === 'normal' ? '#10b981' : '#f59e0b',
|
||||
boxShadow: `0 0 8px ${item.status === 'normal' ? 'rgba(16, 185, 129, 0.6)' : 'rgba(245, 158, 11, 0.6)'}`,
|
||||
}} />
|
||||
<span style={{ fontSize: '14px', fontWeight: 700, color: '#f8fafc' }}>
|
||||
{item.value}
|
||||
</span>
|
||||
<div
|
||||
style={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: '50%',
|
||||
background: item.status === 'normal' ? '#10b981' : '#f59e0b',
|
||||
boxShadow: `0 0 8px ${item.status === 'normal' ? 'rgba(16, 185, 129, 0.6)' : 'rgba(245, 158, 11, 0.6)'}`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -845,13 +930,15 @@ const Rack3DVisualization = () => {
|
||||
>
|
||||
<span style={{ fontSize: 14 }}>{item.icon}</span>
|
||||
<span style={{ flex: 1 }}>{item.text}</span>
|
||||
<div style={{
|
||||
width: 4,
|
||||
height: 4,
|
||||
borderRadius: '50%',
|
||||
background: item.color,
|
||||
boxShadow: `0 0 6px ${item.color}`,
|
||||
}} />
|
||||
<div
|
||||
style={{
|
||||
width: 4,
|
||||
height: 4,
|
||||
borderRadius: '50%',
|
||||
background: item.color,
|
||||
boxShadow: `0 0 6px ${item.color}`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -439,7 +439,9 @@ function RackManagement() {
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
try {
|
||||
const results = await Promise.allSettled(selectedRackIds.map(id => axios.delete(`/api/racks/${id}`)));
|
||||
const results = await Promise.allSettled(
|
||||
selectedRackIds.map(id => axios.delete(`/api/racks/${id}`))
|
||||
);
|
||||
const succeeded = results.filter(r => r.status === 'fulfilled').length;
|
||||
const failed = results.filter(r => r.status === 'rejected');
|
||||
if (succeeded > 0) {
|
||||
@@ -537,14 +539,14 @@ function RackManagement() {
|
||||
failedCount: 0,
|
||||
errors: [],
|
||||
createdRacks: resData.createdRacks || [],
|
||||
skippedRacks: resData.skippedRacks || []
|
||||
skippedRacks: resData.skippedRacks || [],
|
||||
};
|
||||
|
||||
if (resData.details && Array.isArray(resData.details)) {
|
||||
importResult.failedCount = resData.details.length;
|
||||
importResult.errors = resData.details.map(item => ({
|
||||
row: item.row,
|
||||
error: item.errors.join(';')
|
||||
error: item.errors.join(';'),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -571,8 +573,8 @@ function RackManagement() {
|
||||
failedCount: errorData.details.length,
|
||||
errors: errorData.details.map(item => ({
|
||||
row: item.row,
|
||||
error: item.errors.join(';')
|
||||
}))
|
||||
error: item.errors.join(';'),
|
||||
})),
|
||||
};
|
||||
setImportResult(importResult);
|
||||
setImportPhase('导入失败');
|
||||
@@ -654,7 +656,9 @@ function RackManagement() {
|
||||
</Text>
|
||||
);
|
||||
},
|
||||
sorter: (a, b) => (a.Devices?.reduce((sum, d) => sum + (d.height || 1), 0) || 0) - (b.Devices?.reduce((sum, d) => sum + (d.height || 1), 0) || 0),
|
||||
sorter: (a, b) =>
|
||||
(a.Devices?.reduce((sum, d) => sum + (d.height || 1), 0) || 0) -
|
||||
(b.Devices?.reduce((sum, d) => sum + (d.height || 1), 0) || 0),
|
||||
},
|
||||
{
|
||||
title: '功率使用',
|
||||
@@ -1437,14 +1441,16 @@ function RackManagement() {
|
||||
<p style={{ fontWeight: '600', marginBottom: '8px', color: '#52c41a' }}>
|
||||
✓ 本次新增机柜({importResult.createdRacks.length}):
|
||||
</p>
|
||||
<div style={{
|
||||
maxHeight: '150px',
|
||||
overflow: 'auto',
|
||||
background: '#f6ffed',
|
||||
border: '1px solid #b7eb8f',
|
||||
borderRadius: '8px',
|
||||
padding: '12px'
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
maxHeight: '150px',
|
||||
overflow: 'auto',
|
||||
background: '#f6ffed',
|
||||
border: '1px solid #b7eb8f',
|
||||
borderRadius: '8px',
|
||||
padding: '12px',
|
||||
}}
|
||||
>
|
||||
{importResult.createdRacks.map((rack, idx) => (
|
||||
<div key={idx} style={{ fontSize: '13px', marginBottom: '4px' }}>
|
||||
• {rack.rackId} - {rack.name}
|
||||
@@ -1459,14 +1465,16 @@ function RackManagement() {
|
||||
<p style={{ fontWeight: '600', marginBottom: '8px', color: '#faad14' }}>
|
||||
⚠ 已跳过机柜({importResult.skippedRacks.length}):
|
||||
</p>
|
||||
<div style={{
|
||||
maxHeight: '150px',
|
||||
overflow: 'auto',
|
||||
background: '#fffbe6',
|
||||
border: '1px solid #ffe58f',
|
||||
borderRadius: '8px',
|
||||
padding: '12px'
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
maxHeight: '150px',
|
||||
overflow: 'auto',
|
||||
background: '#fffbe6',
|
||||
border: '1px solid #ffe58f',
|
||||
borderRadius: '8px',
|
||||
padding: '12px',
|
||||
}}
|
||||
>
|
||||
{importResult.skippedRacks.map((rack, idx) => (
|
||||
<div key={idx} style={{ fontSize: '13px', marginBottom: '4px' }}>
|
||||
• {rack.rackId} - {rack.name}
|
||||
@@ -1478,7 +1486,9 @@ function RackManagement() {
|
||||
|
||||
{importResult.errors && importResult.errors.length > 0 && (
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<p style={{ fontWeight: '600', marginBottom: '8px', color: '#ff4d4f' }}>错误详情:</p>
|
||||
<p style={{ fontWeight: '600', marginBottom: '8px', color: '#ff4d4f' }}>
|
||||
错误详情:
|
||||
</p>
|
||||
{importResult.errors.slice(0, 5).map((err, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,13 @@ import {
|
||||
InputNumber,
|
||||
Switch,
|
||||
} from 'antd';
|
||||
import { PlusOutlined, EditOutlined, DeleteOutlined, PlusCircleOutlined, MinusCircleOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
PlusOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
PlusCircleOutlined,
|
||||
MinusCircleOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import axios from 'axios';
|
||||
import CloseButton from '../components/CloseButton';
|
||||
|
||||
@@ -37,44 +43,46 @@ const OptionsEditor = ({ value, onChange }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
border: '1px solid #e8e8e8',
|
||||
borderRadius: '12px',
|
||||
padding: '20px',
|
||||
background: 'linear-gradient(135deg, #fafbfc 0%, #f5f7fa 100%)',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.04)',
|
||||
}}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginBottom: '16px',
|
||||
gap: '8px',
|
||||
}}>
|
||||
<div style={{
|
||||
width: '4px',
|
||||
height: '16px',
|
||||
background: 'linear-gradient(180deg, #667eea 0%, #764ba2 100%)',
|
||||
borderRadius: '2px',
|
||||
}}/>
|
||||
<span style={{ color: '#333', fontSize: '14px', fontWeight: '600' }}>
|
||||
选项配置
|
||||
</span>
|
||||
<span style={{ color: '#999', fontSize: '12px' }}>
|
||||
(值用于提交,标签用于显示)
|
||||
</span>
|
||||
<div
|
||||
style={{
|
||||
border: '1px solid #e8e8e8',
|
||||
borderRadius: '12px',
|
||||
padding: '20px',
|
||||
background: 'linear-gradient(135deg, #fafbfc 0%, #f5f7fa 100%)',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.04)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginBottom: '16px',
|
||||
gap: '8px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: '4px',
|
||||
height: '16px',
|
||||
background: 'linear-gradient(180deg, #667eea 0%, #764ba2 100%)',
|
||||
borderRadius: '2px',
|
||||
}}
|
||||
/>
|
||||
<span style={{ color: '#333', fontSize: '14px', fontWeight: '600' }}>选项配置</span>
|
||||
<span style={{ color: '#999', fontSize: '12px' }}>(值用于提交,标签用于显示)</span>
|
||||
</div>
|
||||
|
||||
{options.length === 0 ? (
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
padding: '24px',
|
||||
background: '#fff',
|
||||
borderRadius: '8px',
|
||||
border: '1px dashed #d9d9d9',
|
||||
}}>
|
||||
<div style={{ color: '#bbb', fontSize: '14px', marginBottom: '12px' }}>
|
||||
暂无选项
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
padding: '24px',
|
||||
background: '#fff',
|
||||
borderRadius: '8px',
|
||||
border: '1px dashed #d9d9d9',
|
||||
}}
|
||||
>
|
||||
<div style={{ color: '#bbb', fontSize: '14px', marginBottom: '12px' }}>暂无选项</div>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusCircleOutlined />}
|
||||
@@ -90,15 +98,23 @@ const OptionsEditor = ({ value, onChange }) => {
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px', marginBottom: '16px' }}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
gap: '12px',
|
||||
padding: '0 4px',
|
||||
marginBottom: '4px',
|
||||
}}>
|
||||
<span style={{ width: '160px', color: '#666', fontSize: '12px', fontWeight: '500' }}>值(value)</span>
|
||||
<span style={{ width: '160px', color: '#666', fontSize: '12px', fontWeight: '500' }}>标签(label)</span>
|
||||
<div
|
||||
style={{ display: 'flex', flexDirection: 'column', gap: '10px', marginBottom: '16px' }}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: '12px',
|
||||
padding: '0 4px',
|
||||
marginBottom: '4px',
|
||||
}}
|
||||
>
|
||||
<span style={{ width: '160px', color: '#666', fontSize: '12px', fontWeight: '500' }}>
|
||||
值(value)
|
||||
</span>
|
||||
<span style={{ width: '160px', color: '#666', fontSize: '12px', fontWeight: '500' }}>
|
||||
标签(label)
|
||||
</span>
|
||||
</div>
|
||||
{value.map((opt, index) => (
|
||||
<div
|
||||
@@ -114,19 +130,21 @@ const OptionsEditor = ({ value, onChange }) => {
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
borderRadius: '50%',
|
||||
background: 'linear-gradient(135deg, #667eea20 0%, #764ba220 100%)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#667eea',
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
borderRadius: '50%',
|
||||
background: 'linear-gradient(135deg, #667eea20 0%, #764ba220 100%)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#667eea',
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{index + 1}
|
||||
</div>
|
||||
<Input
|
||||
@@ -432,11 +450,7 @@ function TicketFieldManagement() {
|
||||
<OptionsEditor />
|
||||
</Form.Item>
|
||||
) : (
|
||||
<Form.Item
|
||||
name="options"
|
||||
label="选项配置"
|
||||
tooltip="仅下拉选择类型需要配置选项"
|
||||
>
|
||||
<Form.Item name="options" label="选项配置" tooltip="仅下拉选择类型需要配置选项">
|
||||
<Input.TextArea
|
||||
rows={2}
|
||||
placeholder="仅下拉选择类型需要配置,此处不可编辑"
|
||||
|
||||
@@ -360,11 +360,13 @@ function TicketManagement() {
|
||||
const response = await axios.get('/api/ticket-fields');
|
||||
const dbFields = response.data.sort((a, b) => a.order - b.order);
|
||||
|
||||
const isValidOptions = (opts) => {
|
||||
const isValidOptions = opts => {
|
||||
if (!opts) return false;
|
||||
if (!Array.isArray(opts)) return false;
|
||||
if (opts.length === 0) return false;
|
||||
return opts.some(opt => opt && opt.value !== undefined && opt.value !== null && opt.value !== '');
|
||||
return opts.some(
|
||||
opt => opt && opt.value !== undefined && opt.value !== null && opt.value !== ''
|
||||
);
|
||||
};
|
||||
|
||||
const mergedFields = DEFAULT_TICKET_FIELDS.map(defaultField => {
|
||||
@@ -468,7 +470,11 @@ function TicketManagement() {
|
||||
break;
|
||||
case 'textarea':
|
||||
formItem = (
|
||||
<Input.TextArea rows={3} placeholder={placeholder || `请输入${displayName}`} showCount />
|
||||
<Input.TextArea
|
||||
rows={3}
|
||||
placeholder={placeholder || `请输入${displayName}`}
|
||||
showCount
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case 'boolean':
|
||||
@@ -1131,17 +1137,19 @@ function TicketManagement() {
|
||||
<Modal
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<div style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 8,
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#fff',
|
||||
fontSize: 16,
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 8,
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#fff',
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
<PlusOutlined />
|
||||
</div>
|
||||
<span style={{ fontWeight: 600, fontSize: 18 }}>
|
||||
@@ -1166,32 +1174,38 @@ function TicketManagement() {
|
||||
style={{ marginBottom: 16 }}
|
||||
>
|
||||
{!editingTicket && (
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, #f0f4ff 0%, #fafbff 100%)',
|
||||
border: '1px solid #e8eaff',
|
||||
borderRadius: 12,
|
||||
padding: '12px 16px',
|
||||
marginBottom: 20,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
}}>
|
||||
<div style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 8,
|
||||
background: '#667eea',
|
||||
<div
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #f0f4ff 0%, #fafbff 100%)',
|
||||
border: '1px solid #e8eaff',
|
||||
borderRadius: 12,
|
||||
padding: '12px 16px',
|
||||
marginBottom: 20,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#fff',
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
}}>
|
||||
gap: 12,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 8,
|
||||
background: '#667eea',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#fff',
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
TKT
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 12, color: '#666', marginBottom: 2 }}>工单编号(自动生成)</div>
|
||||
<div style={{ fontSize: 12, color: '#666', marginBottom: 2 }}>
|
||||
工单编号(自动生成)
|
||||
</div>
|
||||
<Form.Item name="ticketId" noStyle>
|
||||
<Input
|
||||
disabled
|
||||
@@ -1209,28 +1223,34 @@ function TicketManagement() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
gap: '0 24px',
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
gap: '0 24px',
|
||||
}}
|
||||
>
|
||||
<div style={{ gridColumn: '1 / -1', marginBottom: 8 }}>
|
||||
<div style={{
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
color: '#333',
|
||||
marginBottom: 12,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
}}>
|
||||
<span style={{
|
||||
width: 4,
|
||||
height: 16,
|
||||
background: 'linear-gradient(180deg, #667eea 0%, #764ba2 100%)',
|
||||
borderRadius: 2,
|
||||
display: 'inline-block',
|
||||
}} />
|
||||
<div
|
||||
style={{
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
color: '#333',
|
||||
marginBottom: 12,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
width: 4,
|
||||
height: 16,
|
||||
background: 'linear-gradient(180deg, #667eea 0%, #764ba2 100%)',
|
||||
borderRadius: 2,
|
||||
display: 'inline-block',
|
||||
}}
|
||||
/>
|
||||
设备信息
|
||||
</div>
|
||||
</div>
|
||||
@@ -1264,7 +1284,11 @@ function TicketManagement() {
|
||||
<div>
|
||||
<Form.Item
|
||||
name="serialNumber"
|
||||
label={<span style={{ fontWeight: 500 }}>设备序列号 <span style={{ color: '#ff4d4f' }}>*</span></span>}
|
||||
label={
|
||||
<span style={{ fontWeight: 500 }}>
|
||||
设备序列号 <span style={{ color: '#ff4d4f' }}>*</span>
|
||||
</span>
|
||||
}
|
||||
rules={[{ required: true, message: '请输入设备序列号' }]}
|
||||
>
|
||||
<Input placeholder="请输入设备序列号" size="large" />
|
||||
@@ -1283,7 +1307,11 @@ function TicketManagement() {
|
||||
<div style={{ gridColumn: '1 / -1' }}>
|
||||
<Form.Item
|
||||
name="deviceId"
|
||||
label={<span style={{ fontWeight: 500 }}>关联设备 <span style={{ color: '#ff4d4f' }}>*</span></span>}
|
||||
label={
|
||||
<span style={{ fontWeight: 500 }}>
|
||||
关联设备 <span style={{ color: '#ff4d4f' }}>*</span>
|
||||
</span>
|
||||
}
|
||||
rules={[{ required: true, message: '请选择关联设备' }]}
|
||||
>
|
||||
<Select
|
||||
@@ -1317,11 +1345,7 @@ function TicketManagement() {
|
||||
return renderDeviceSection();
|
||||
} else if (hasDeviceNameField) {
|
||||
if (!ticketFields.some(f => f.fieldName === 'deviceId')) {
|
||||
return (
|
||||
<div style={{ gridColumn: '1 / -1' }}>
|
||||
{renderDeviceSection()}
|
||||
</div>
|
||||
);
|
||||
return <div style={{ gridColumn: '1 / -1' }}>{renderDeviceSection()}</div>;
|
||||
}
|
||||
} else {
|
||||
return renderDeviceSection();
|
||||
@@ -1330,38 +1354,56 @@ function TicketManagement() {
|
||||
})()}
|
||||
|
||||
<div style={{ gridColumn: '1 / -1', marginBottom: 8, marginTop: 8 }}>
|
||||
<div style={{
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
color: '#333',
|
||||
marginBottom: 12,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
}}>
|
||||
<span style={{
|
||||
width: 4,
|
||||
height: 16,
|
||||
background: 'linear-gradient(180deg, #667eea 0%, #764ba2 100%)',
|
||||
borderRadius: 2,
|
||||
display: 'inline-block',
|
||||
}} />
|
||||
<div
|
||||
style={{
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
color: '#333',
|
||||
marginBottom: 12,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
width: 4,
|
||||
height: 16,
|
||||
background: 'linear-gradient(180deg, #667eea 0%, #764ba2 100%)',
|
||||
borderRadius: 2,
|
||||
display: 'inline-block',
|
||||
}}
|
||||
/>
|
||||
工单信息
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ticketFields.filter(f =>
|
||||
!['ticketId', 'deviceId', 'deviceName', 'serialNumber', 'expectedCompletionDate', 'resolution', 'notes', 'description'].includes(f.fieldName)
|
||||
).map(field => (
|
||||
<div key={field.fieldName}>
|
||||
{renderFormItem(field)}
|
||||
</div>
|
||||
))}
|
||||
{ticketFields
|
||||
.filter(
|
||||
f =>
|
||||
![
|
||||
'ticketId',
|
||||
'deviceId',
|
||||
'deviceName',
|
||||
'serialNumber',
|
||||
'expectedCompletionDate',
|
||||
'resolution',
|
||||
'notes',
|
||||
'description',
|
||||
].includes(f.fieldName)
|
||||
)
|
||||
.map(field => (
|
||||
<div key={field.fieldName}>{renderFormItem(field)}</div>
|
||||
))}
|
||||
|
||||
<div style={{ gridColumn: '1 / -1' }}>
|
||||
<Form.Item
|
||||
name="title"
|
||||
label={<span style={{ fontWeight: 500 }}>工单标题 <span style={{ color: '#ff4d4f' }}>*</span></span>}
|
||||
label={
|
||||
<span style={{ fontWeight: 500 }}>
|
||||
工单标题 <span style={{ color: '#ff4d4f' }}>*</span>
|
||||
</span>
|
||||
}
|
||||
rules={[{ required: true, message: '请输入工单标题' }]}
|
||||
>
|
||||
<Input placeholder="请输入工单标题" size="large" />
|
||||
@@ -1371,7 +1413,11 @@ function TicketManagement() {
|
||||
<div>
|
||||
<Form.Item
|
||||
name="faultCategory"
|
||||
label={<span style={{ fontWeight: 500 }}>故障分类 <span style={{ color: '#ff4d4f' }}>*</span></span>}
|
||||
label={
|
||||
<span style={{ fontWeight: 500 }}>
|
||||
故障分类 <span style={{ color: '#ff4d4f' }}>*</span>
|
||||
</span>
|
||||
}
|
||||
rules={[{ required: true, message: '请选择故障分类' }]}
|
||||
>
|
||||
<Select placeholder="请选择故障分类" size="large">
|
||||
@@ -1387,22 +1433,34 @@ function TicketManagement() {
|
||||
<div>
|
||||
<Form.Item
|
||||
name="priority"
|
||||
label={<span style={{ fontWeight: 500 }}>优先级 <span style={{ color: '#ff4d4f' }}>*</span></span>}
|
||||
label={
|
||||
<span style={{ fontWeight: 500 }}>
|
||||
优先级 <span style={{ color: '#ff4d4f' }}>*</span>
|
||||
</span>
|
||||
}
|
||||
rules={[{ required: true, message: '请选择优先级' }]}
|
||||
initialValue="medium"
|
||||
>
|
||||
<Select placeholder="请选择优先级" size="large">
|
||||
<Option value="low">
|
||||
<Tag color="green" style={{ margin: 0 }}>低</Tag>
|
||||
<Tag color="green" style={{ margin: 0 }}>
|
||||
低
|
||||
</Tag>
|
||||
</Option>
|
||||
<Option value="medium">
|
||||
<Tag color="orange" style={{ margin: 0 }}>中</Tag>
|
||||
<Tag color="orange" style={{ margin: 0 }}>
|
||||
中
|
||||
</Tag>
|
||||
</Option>
|
||||
<Option value="high">
|
||||
<Tag color="red" style={{ margin: 0 }}>高</Tag>
|
||||
<Tag color="red" style={{ margin: 0 }}>
|
||||
高
|
||||
</Tag>
|
||||
</Option>
|
||||
<Option value="urgent">
|
||||
<Tag color="magenta" style={{ margin: 0 }}>紧急</Tag>
|
||||
<Tag color="magenta" style={{ margin: 0 }}>
|
||||
紧急
|
||||
</Tag>
|
||||
</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
@@ -1426,7 +1484,11 @@ function TicketManagement() {
|
||||
<div style={{ gridColumn: '1 / -1' }}>
|
||||
<Form.Item
|
||||
name="description"
|
||||
label={<span style={{ fontWeight: 500 }}>故障描述 <span style={{ color: '#ff4d4f' }}>*</span></span>}
|
||||
label={
|
||||
<span style={{ fontWeight: 500 }}>
|
||||
故障描述 <span style={{ color: '#ff4d4f' }}>*</span>
|
||||
</span>
|
||||
}
|
||||
rules={[{ required: true, message: '请输入故障描述' }]}
|
||||
>
|
||||
<TextArea
|
||||
@@ -1439,10 +1501,7 @@ function TicketManagement() {
|
||||
</div>
|
||||
|
||||
<div style={{ gridColumn: '1 / -1', marginTop: 8 }}>
|
||||
<Form.Item
|
||||
name="notes"
|
||||
label={<span style={{ fontWeight: 500 }}>备注信息</span>}
|
||||
>
|
||||
<Form.Item name="notes" label={<span style={{ fontWeight: 500 }}>备注信息</span>}>
|
||||
<TextArea
|
||||
rows={2}
|
||||
placeholder="补充说明或其他相关信息(选填)"
|
||||
@@ -1453,14 +1512,16 @@ function TicketManagement() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
borderTop: '1px solid #f0f0f0',
|
||||
marginTop: 24,
|
||||
paddingTop: 20,
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
gap: 12,
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
borderTop: '1px solid #f0f0f0',
|
||||
marginTop: 24,
|
||||
paddingTop: 20,
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
gap: 12,
|
||||
}}
|
||||
>
|
||||
<Button onClick={handleCancel} size="large" style={{ minWidth: 100 }}>
|
||||
取消
|
||||
</Button>
|
||||
@@ -1706,15 +1767,19 @@ function TicketManagement() {
|
||||
<Row gutter={[24, 16]}>
|
||||
<Col span={12}>
|
||||
<div style={{ color: '#666', fontSize: 12, marginBottom: 4 }}>购买日期</div>
|
||||
<div>{selectedTicket.Device.purchaseDate
|
||||
? dayjs(selectedTicket.Device.purchaseDate).format('YYYY-MM-DD')
|
||||
: '-'}</div>
|
||||
<div>
|
||||
{selectedTicket.Device.purchaseDate
|
||||
? dayjs(selectedTicket.Device.purchaseDate).format('YYYY-MM-DD')
|
||||
: '-'}
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div style={{ color: '#666', fontSize: 12, marginBottom: 4 }}>保修到期</div>
|
||||
<div>{selectedTicket.Device.warrantyExpiry
|
||||
? dayjs(selectedTicket.Device.warrantyExpiry).format('YYYY-MM-DD')
|
||||
: '-'}</div>
|
||||
<div>
|
||||
{selectedTicket.Device.warrantyExpiry
|
||||
? dayjs(selectedTicket.Device.warrantyExpiry).format('YYYY-MM-DD')
|
||||
: '-'}
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
@@ -1722,28 +1787,35 @@ function TicketManagement() {
|
||||
{/* 描述信息 */}
|
||||
{selectedTicket.Device.description && (
|
||||
<Card title="描述" style={{ marginBottom: 16 }} size="small">
|
||||
<div style={{ whiteSpace: 'pre-wrap' }}>{selectedTicket.Device.description}</div>
|
||||
<div style={{ whiteSpace: 'pre-wrap' }}>
|
||||
{selectedTicket.Device.description}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 自定义字段卡片 */}
|
||||
{selectedTicket.Device.customFields && Object.keys(selectedTicket.Device.customFields).length > 0 && (
|
||||
<Card title="自定义字段" size="small">
|
||||
<Row gutter={[24, 16]}>
|
||||
{Object.entries(selectedTicket.Device.customFields).map(([key, value]) => {
|
||||
// 从 deviceFields 中查找对应的中文显示名称
|
||||
const fieldConfig = deviceFields.find(f => f.fieldName === key);
|
||||
const displayName = fieldConfig?.displayName || key;
|
||||
return (
|
||||
<Col span={8} key={key}>
|
||||
<div style={{ color: '#666', fontSize: 12, marginBottom: 4 }}>{displayName}</div>
|
||||
<div style={{ fontWeight: 500 }}>{String(value)}</div>
|
||||
</Col>
|
||||
);
|
||||
})}
|
||||
</Row>
|
||||
</Card>
|
||||
)}
|
||||
{selectedTicket.Device.customFields &&
|
||||
Object.keys(selectedTicket.Device.customFields).length > 0 && (
|
||||
<Card title="自定义字段" size="small">
|
||||
<Row gutter={[24, 16]}>
|
||||
{Object.entries(selectedTicket.Device.customFields).map(
|
||||
([key, value]) => {
|
||||
// 从 deviceFields 中查找对应的中文显示名称
|
||||
const fieldConfig = deviceFields.find(f => f.fieldName === key);
|
||||
const displayName = fieldConfig?.displayName || key;
|
||||
return (
|
||||
<Col span={8} key={key}>
|
||||
<div style={{ color: '#666', fontSize: 12, marginBottom: 4 }}>
|
||||
{displayName}
|
||||
</div>
|
||||
<div style={{ fontWeight: 500 }}>{String(value)}</div>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</Row>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</TabPane>
|
||||
)}
|
||||
@@ -1851,9 +1923,7 @@ function TicketManagement() {
|
||||
</div>
|
||||
<div style={{ marginBottom: '4px' }}>
|
||||
<span style={{ color: '#666' }}>操作人:</span>
|
||||
<span style={{ fontWeight: 500 }}>
|
||||
{record.operatorName || '-'}
|
||||
</span>
|
||||
<span style={{ fontWeight: 500 }}>{record.operatorName || '-'}</span>
|
||||
</div>
|
||||
{record.operationDescription && (
|
||||
<div
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
||||
import { Card, Row, Col, Statistic, Table, DatePicker, Select, Space, Tag, message, Button, Switch, Tooltip } from 'antd';
|
||||
import {
|
||||
Card,
|
||||
Row,
|
||||
Col,
|
||||
Statistic,
|
||||
Table,
|
||||
DatePicker,
|
||||
Select,
|
||||
Space,
|
||||
Tag,
|
||||
message,
|
||||
Button,
|
||||
Switch,
|
||||
Tooltip,
|
||||
} from 'antd';
|
||||
import {
|
||||
BarChartOutlined,
|
||||
PieChartOutlined,
|
||||
@@ -82,26 +96,29 @@ function TicketStatistics() {
|
||||
|
||||
const timerRef = useRef(null);
|
||||
|
||||
const fetchStatistics = useCallback(async (isManual = false) => {
|
||||
try {
|
||||
if (isManual) {
|
||||
setLoading(true);
|
||||
}
|
||||
const params = {
|
||||
startDate: dateRange[0].format('YYYY-MM-DD'),
|
||||
endDate: dateRange[1].format('YYYY-MM-DD'),
|
||||
};
|
||||
const fetchStatistics = useCallback(
|
||||
async (isManual = false) => {
|
||||
try {
|
||||
if (isManual) {
|
||||
setLoading(true);
|
||||
}
|
||||
const params = {
|
||||
startDate: dateRange[0].format('YYYY-MM-DD'),
|
||||
endDate: dateRange[1].format('YYYY-MM-DD'),
|
||||
};
|
||||
|
||||
const response = await axios.get('/api/tickets/stats', { params });
|
||||
setStatistics(response.data);
|
||||
setLastUpdateTime(new Date());
|
||||
} catch (error) {
|
||||
message.error('获取统计数据失败');
|
||||
console.error('获取统计数据失败:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [dateRange]);
|
||||
const response = await axios.get('/api/tickets/stats', { params });
|
||||
setStatistics(response.data);
|
||||
setLastUpdateTime(new Date());
|
||||
} catch (error) {
|
||||
message.error('获取统计数据失败');
|
||||
console.error('获取统计数据失败:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[dateRange]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetchStatistics();
|
||||
@@ -329,7 +346,13 @@ function TicketStatistics() {
|
||||
title="工单统计报表"
|
||||
extra={
|
||||
<Space>
|
||||
<Tooltip title={lastUpdateTime ? `最后更新: ${dayjs(lastUpdateTime).format('HH:mm:ss')}` : '尚未更新'}>
|
||||
<Tooltip
|
||||
title={
|
||||
lastUpdateTime
|
||||
? `最后更新: ${dayjs(lastUpdateTime).format('HH:mm:ss')}`
|
||||
: '尚未更新'
|
||||
}
|
||||
>
|
||||
<span style={{ fontSize: 12, color: '#888', marginRight: 8 }}>
|
||||
{lastUpdateTime && `更新于 ${dayjs(lastUpdateTime).format('HH:mm:ss')}`}
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user