feat(主题): 实现动态主题颜色配置功能

This commit is contained in:
zhang1106
2026-03-06 15:41:07 +08:00
parent 885ac47fd2
commit ad7c45d453
5 changed files with 695 additions and 13 deletions
+94
View File
@@ -520,4 +520,98 @@ router.get('/stats/dashboard', async (req, res) => {
}
});
router.post('/quick-add-device', async (req, res) => {
try {
const { taskId, planId, serialNumber, deviceName, deviceType, rackId, position, remark } = req.body;
if (!taskId || !planId || !serialNumber) {
return res.status(400).json({ error: '缺少必要参数:taskId, planId, serialNumber' });
}
const task = await InventoryTask.findByPk(taskId);
if (!task) {
return res.status(404).json({ error: '盘点任务不存在' });
}
const plan = await InventoryPlan.findByPk(planId);
if (!plan) {
return res.status(404).json({ error: '盘点计划不存在' });
}
const existingDevice = await Device.findOne({ where: { serialNumber } });
if (existingDevice) {
return res.status(400).json({ error: '该序列号的设备已存在', deviceId: existingDevice.deviceId });
}
let deviceId;
const devices = await Device.findAll({
where: { deviceId: { [Op.like]: 'DEV%' } }
});
let maxNumber = 0;
devices.forEach(device => {
const match = device.deviceId.match(/^DEV(\d+)$/);
if (match) {
const num = parseInt(match[1], 10);
if (num > maxNumber) maxNumber = num;
}
});
deviceId = `DEV${String(maxNumber + 1).padStart(3, '0')}`;
const newDevice = await Device.create({
deviceId,
name: deviceName || `新设备-${serialNumber.slice(-6)}`,
type: deviceType || 'other',
serialNumber,
rackId: rackId || null,
position: position || null,
status: 'running'
});
const record = await InventoryRecord.create({
recordId: generateRecordId(),
taskId,
planId,
deviceId: newDevice.deviceId,
deviceName: newDevice.name,
deviceType: newDevice.type,
serialNumber: newDevice.serialNumber,
rackId: newDevice.rackId,
position: newDevice.position,
status: 'normal',
abnormalType: 'extra_device',
checkedBy: req.user?.userId,
checkedAt: new Date(),
remark: remark || '盘点时新增设备'
});
const taskRecords = await InventoryRecord.findAll({ where: { taskId: task.taskId } });
const taskStats = {
totalDevices: taskRecords.length,
checkedDevices: taskRecords.filter(r => r.status !== 'pending').length,
normalDevices: taskRecords.filter(r => r.status === 'normal').length,
abnormalDevices: taskRecords.filter(r => r.status === 'abnormal').length
};
await task.update(taskStats);
const planRecords = await InventoryRecord.findAll({ where: { planId: plan.planId } });
const planStats = {
totalDevices: planRecords.length,
checkedDevices: planRecords.filter(r => r.status !== 'pending').length,
normalDevices: planRecords.filter(r => r.status === 'normal').length,
abnormalDevices: planRecords.filter(r => r.status === 'abnormal').length,
missedDevices: planRecords.filter(r => r.status === 'pending').length
};
await plan.update(planStats);
res.status(201).json({
message: '设备添加成功',
device: newDevice,
record
});
} catch (error) {
console.error('快速添加设备错误:', error);
res.status(500).json({ error: error.message });
}
});
module.exports = router;
+23 -7
View File
@@ -3,9 +3,21 @@ import axios from 'axios';
const ConfigContext = createContext();
const applyThemeColors = (primaryColor, secondaryColor) => {
const root = document.documentElement;
if (primaryColor) {
root.style.setProperty('--primary-color', primaryColor);
root.style.setProperty('--primary-light', `${primaryColor}20`);
root.style.setProperty('--primary-gradient', `linear-gradient(135deg, ${primaryColor} 0%, ${secondaryColor || '#764ba2'} 100%)`);
}
if (secondaryColor) {
root.style.setProperty('--secondary-color', secondaryColor);
root.style.setProperty('--secondary-light', `${secondaryColor}20`);
}
};
export const ConfigProvider = ({ children }) => {
const [config, setConfig] = useState({
// 默认配置
site_name: '机柜管理系统',
primary_color: '#667eea',
secondary_color: '#764ba2',
@@ -21,14 +33,12 @@ export const ConfigProvider = ({ children }) => {
});
const [loading, setLoading] = useState(true);
// 加载系统配置
const loadConfig = async () => {
try {
const response = await axios.get('/api/system-settings');
const settings = response.data;
const configValues = {};
// 将配置转换为扁平结构
Object.entries(settings).forEach(([key, value]) => {
configValues[key] = value.value;
});
@@ -37,6 +47,10 @@ export const ConfigProvider = ({ children }) => {
...prev,
...configValues,
}));
if (configValues.primary_color || configValues.secondary_color) {
applyThemeColors(configValues.primary_color, configValues.secondary_color);
}
} catch (error) {
console.error('加载系统配置失败:', error);
} finally {
@@ -44,12 +58,16 @@ export const ConfigProvider = ({ children }) => {
}
};
// 初始化加载配置
useEffect(() => {
loadConfig();
}, []);
// 更新配置
useEffect(() => {
if (config.primary_color || config.secondary_color) {
applyThemeColors(config.primary_color, config.secondary_color);
}
}, [config.primary_color, config.secondary_color]);
const updateConfig = newConfig => {
setConfig(prev => ({
...prev,
@@ -57,7 +75,6 @@ export const ConfigProvider = ({ children }) => {
}));
};
// 重新加载配置
const reloadConfig = async () => {
await loadConfig();
};
@@ -69,7 +86,6 @@ export const ConfigProvider = ({ children }) => {
);
};
// 自定义钩子,方便组件使用配置
export const useConfig = () => {
const context = useContext(ConfigContext);
if (!context) {
+12 -4
View File
@@ -1,3 +1,11 @@
:root {
--primary-color: #667eea;
--primary-light: rgba(102, 126, 234, 0.125);
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--secondary-color: #764ba2;
--secondary-light: rgba(118, 75, 162, 0.125);
}
* {
margin: 0;
padding: 0;
@@ -349,19 +357,19 @@ body {
}
.ant-btn-primary {
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%) !important;
background: var(--primary-gradient) !important;
border: none !important;
box-shadow: 0 2px 6px rgba(24, 144, 255, 0.35) !important;
}
.ant-btn-primary:hover {
background: linear-gradient(135deg, #40a9ff 0%, #1890ff 100%) !important;
background: var(--primary-gradient) !important;
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.45) !important;
transform: translateY(-2px) !important;
}
.ant-btn-primary:active {
background: linear-gradient(135deg, #096dd9 0%, #0050b3 100%) !important;
background: var(--primary-gradient) !important;
box-shadow: 0 2px 6px rgba(24, 144, 255, 0.35) !important;
transform: translateY(0) !important;
}
@@ -871,7 +879,7 @@ body {
}
.welcome-banner {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: var(--primary-gradient);
border-radius: 12px;
padding: 24px;
color: #fff;
@@ -30,6 +30,7 @@ import {
ScanOutlined,
CheckOutlined,
CloseOutlined,
PlusOutlined,
} from '@ant-design/icons';
import axios from 'axios';
import dayjs from 'dayjs';
@@ -67,6 +68,12 @@ const InventoryTaskExecution = () => {
const [scanResult, setScanResult] = useState(null);
const [scanModalVisible, setScanModalVisible] = useState(false);
const [recordPagination, setRecordPagination] = useState({ current: 1, pageSize: 20, total: 0 });
const [quickAddModalVisible, setQuickAddModalVisible] = useState(false);
const [quickAddForm] = Form.useForm();
const [quickAddLoading, setQuickAddLoading] = useState(false);
const [rooms, setRooms] = useState([]);
const [racks, setRacks] = useState([]);
const [selectedRoomId, setSelectedRoomId] = useState(null);
const planId = searchParams.get('planId');
@@ -245,6 +252,84 @@ const InventoryTaskExecution = () => {
fetchPlan();
}, [fetchPlan]);
const fetchRooms = async () => {
try {
const res = await api.get('/rooms');
setRooms(res.data.rooms || res.data || []);
} catch (error) {
console.error('获取机房列表失败', error);
}
};
const fetchRacks = async () => {
try {
const res = await api.get('/racks');
setRacks(res.data.racks || res.data || []);
} catch (error) {
console.error('获取机柜列表失败', error);
}
};
useEffect(() => {
fetchRooms();
fetchRacks();
}, []);
const filteredRacks = selectedRoomId
? racks.filter(rack => rack.roomId === selectedRoomId)
: racks;
const handleQuickAddDevice = async (values) => {
if (!currentTask || !plan) {
message.error('请先选择盘点任务');
return;
}
setQuickAddLoading(true);
try {
const res = await api.post('/inventory/quick-add-device', {
taskId: currentTask.taskId,
planId: plan.planId,
serialNumber: scanResult?.sn || values.serialNumber,
deviceName: values.deviceName,
deviceType: values.deviceType,
rackId: values.rackId,
position: values.position,
remark: values.remark
});
message.success('设备添加成功!');
setQuickAddModalVisible(false);
quickAddForm.resetFields();
setScanResult({
success: true,
message: `新设备 "${res.data.device.name}" 已添加并完成盘点!`,
record: res.data.record,
sn: res.data.device.serialNumber
});
if (currentTask) {
fetchTaskRecords(currentTask.taskId);
}
fetchPlan();
} catch (error) {
message.error(error.response?.data?.error || '添加设备失败');
} finally {
setQuickAddLoading(false);
}
};
const openQuickAddModal = () => {
setSelectedRoomId(null);
quickAddForm.setFieldsValue({
serialNumber: scanResult?.sn || '',
deviceName: '',
deviceType: 'other',
roomId: undefined,
rackId: undefined,
position: undefined,
remark: undefined,
});
setQuickAddModalVisible(true);
};
const handleCheck = (record) => {
setCurrentRecord(record);
form.setFieldsValue({
@@ -912,6 +997,18 @@ const InventoryTaskExecution = () => {
</Row>
</div>
)}
{!scanResult.success && scanResult.sn && (
<div style={{ textAlign: 'center', marginTop: 16 }}>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={openQuickAddModal}
style={{ borderRadius: 8 }}
>
快速添加此设备
</Button>
</div>
)}
</div>
)}
@@ -1037,6 +1134,130 @@ const InventoryTaskExecution = () => {
</Form>
)}
</Modal>
<Modal
title={
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<PlusOutlined style={{ fontSize: 18, color: '#1890ff' }} />
<span>快速添加设备</span>
</div>
}
open={quickAddModalVisible}
onCancel={() => {
setQuickAddModalVisible(false);
setSelectedRoomId(null);
}}
footer={null}
width={500}
centered
>
<Form
form={quickAddForm}
layout="vertical"
onFinish={handleQuickAddDevice}
>
<Form.Item
name="serialNumber"
label="序列号"
rules={[{ required: true, message: '请输入序列号' }]}
>
<Input placeholder="请输入设备序列号" disabled />
</Form.Item>
<Form.Item
name="deviceName"
label="设备名称"
>
<Input placeholder="请输入设备名称(可选)" />
</Form.Item>
<Form.Item
name="deviceType"
label="设备类型"
initialValue="other"
>
<Select placeholder="请选择设备类型">
<Select.Option value="server">服务器</Select.Option>
<Select.Option value="switch">交换机</Select.Option>
<Select.Option value="router">路由器</Select.Option>
<Select.Option value="storage">存储设备</Select.Option>
<Select.Option value="other">其他</Select.Option>
</Select>
</Form.Item>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="roomId"
label="所属机房"
>
<Select
placeholder="请选择机房"
allowClear
showSearch
optionFilterProp="children"
onChange={(value) => {
setSelectedRoomId(value);
quickAddForm.setFieldsValue({ rackId: undefined });
}}
>
{rooms.map(room => (
<Select.Option key={room.roomId} value={room.roomId}>
{room.name}
</Select.Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="rackId"
label="所属机柜"
>
<Select
placeholder={selectedRoomId ? "请选择机柜" : "请先选择机房"}
allowClear
showSearch
optionFilterProp="children"
disabled={!selectedRoomId}
>
{filteredRacks.map(rack => (
<Select.Option key={rack.rackId} value={rack.rackId}>
{rack.name}
</Select.Option>
))}
</Select>
</Form.Item>
</Col>
</Row>
<Form.Item
name="position"
label="U位"
>
<Input type="number" placeholder="请输入U位(可选)" min={1} />
</Form.Item>
<Form.Item
name="remark"
label="备注"
>
<Input.TextArea rows={2} placeholder="请输入备注(可选)" />
</Form.Item>
<Form.Item style={{ marginBottom: 0, textAlign: 'right' }}>
<Space>
<Button onClick={() => {
setQuickAddModalVisible(false);
setSelectedRoomId(null);
}}>取消</Button>
<Button type="primary" htmlType="submit" loading={quickAddLoading}>
添加设备
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
</div>
);
};
+345 -2
View File
@@ -19,6 +19,7 @@ import {
Badge,
Tooltip,
Avatar,
ColorPicker,
} from 'antd';
import {
SettingOutlined,
@@ -668,18 +669,360 @@ const SystemSettings = () => {
);
};
const presetColors = [
'#667eea',
'#764ba2',
'#f093fb',
'#4facfe',
'#43e97b',
'#fa709a',
'#fee140',
'#00b4db',
'#0083b0',
'#fcb045',
'#1890ff',
'#52c41a',
'#eb2f96',
'#722ed1',
'#13c2c2',
'#fa8c16',
];
const handleColorChange = (color, key) => {
const hexColor = typeof color === 'string' ? color : color.toHexString();
form.setFieldValue(key, hexColor);
const root = document.documentElement;
if (key === 'primary_color') {
root.style.setProperty('--primary-color', hexColor);
root.style.setProperty('--primary-light', `${hexColor}20`);
} else if (key === 'secondary_color') {
root.style.setProperty('--secondary-color', hexColor);
root.style.setProperty('--secondary-light', `${hexColor}20`);
}
};
const renderColorFormItem = (key, data) => {
const currentValue = form.getFieldValue(key) || settings[key]?.value || '#667eea';
return (
<Form.Item
key={key}
label={
<Space>
<span style={{ fontSize: 14, color: '#262626', fontWeight: 500 }}>
{data.description || key}
</span>
<div
style={{
width: 20,
height: 20,
borderRadius: 4,
backgroundColor: currentValue,
border: '1px solid #d9d9d9',
boxShadow: '0 1px 2px rgba(0,0,0,0.1)'
}}
/>
</Space>
}
name={key}
style={{ marginBottom: 0 }}
>
<Input type="hidden" />
</Form.Item>
);
};
const renderAppearanceSettings = () => {
const primaryColor = form.getFieldValue('primary_color') || settings.primary_color?.value || '#667eea';
const secondaryColor = form.getFieldValue('secondary_color') || settings.secondary_color?.value || '#764ba2';
return (
<Form form={form} layout="vertical" onFinish={handleSaveSettings}>
<div style={{ paddingBottom: 80 }}>
<Alert
message="主题颜色设置"
description="修改主题颜色后需要刷新页面才能生效。建议选择对比度适中的颜色组合。"
description="选择颜色后可实时预览效果,保存设置后永久生效。建议选择对比度适中的颜色组合。"
type="info"
showIcon
style={{ marginBottom: 24, borderRadius: 12 }}
/>
{settingGroups.appearance.map(group => renderSettingGroup(group))}
<Card
style={{
marginBottom: 24,
borderRadius: 12,
border: '1px solid #e8e8e8',
boxShadow: '0 2px 8px rgba(0,0,0,0.04)',
}}
bodyStyle={{ padding: '24px' }}
>
<div style={{ textAlign: 'center', marginBottom: 24 }}>
<div style={{
width: 48,
height: 48,
borderRadius: '50%',
background: `linear-gradient(135deg, ${primaryColor} 0%, ${secondaryColor} 100%)`,
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
marginBottom: 12,
}}>
<BgColorsOutlined style={{ color: '#fff', fontSize: 24 }} />
</div>
<div style={{ fontSize: 18, fontWeight: 600, color: '#262626', marginBottom: 6 }}>
主题颜色
</div>
<div style={{ fontSize: 14, color: '#666' }}>
自定义系统主题配色方案
</div>
</div>
<Divider style={{ margin: '0 0 24px 0' }} />
<Row gutter={[32, 24]}>
<Col xs={24} md={12}>
<div style={{ marginBottom: 8 }}>
<Text strong style={{ fontSize: 14, color: '#262626' }}>主色调</Text>
<Text type="secondary" style={{ fontSize: 12, marginLeft: 8 }}>
用于按钮链接等主要交互元素
</Text>
</div>
<Space direction="vertical" style={{ width: '100%' }}>
<ColorPicker
value={primaryColor}
onChange={(color) => handleColorChange(color, 'primary_color')}
format="hex"
showText
presets={[
{
label: '推荐配色',
colors: presetColors,
},
]}
style={{ width: '100%' }}
/>
<Input
value={primaryColor}
onChange={(e) => handleColorChange(e.target.value, 'primary_color')}
placeholder="#667eea"
style={{ borderRadius: 8 }}
prefix={<BgColorsOutlined style={{ color: '#bfbfbf' }} />}
/>
</Space>
</Col>
<Col xs={24} md={12}>
<div style={{ marginBottom: 8 }}>
<Text strong style={{ fontSize: 14, color: '#262626' }}>次色调</Text>
<Text type="secondary" style={{ fontSize: 12, marginLeft: 8 }}>
用于渐变悬停效果等辅助元素
</Text>
</div>
<Space direction="vertical" style={{ width: '100%' }}>
<ColorPicker
value={secondaryColor}
onChange={(color) => handleColorChange(color, 'secondary_color')}
format="hex"
showText
presets={[
{
label: '推荐配色',
colors: presetColors,
},
]}
style={{ width: '100%' }}
/>
<Input
value={secondaryColor}
onChange={(e) => handleColorChange(e.target.value, 'secondary_color')}
placeholder="#764ba2"
style={{ borderRadius: 8 }}
prefix={<BgColorsOutlined style={{ color: '#bfbfbf' }} />}
/>
</Space>
</Col>
</Row>
<Divider style={{ margin: '24px 0' }} />
<div style={{
padding: 16,
background: '#fafafa',
borderRadius: 8,
border: '1px solid #f0f0f0'
}}>
<Text strong style={{ fontSize: 13, color: '#595959', marginBottom: 12, display: 'block' }}>
实时预览
</Text>
<Space size="middle" wrap>
<Button
type="primary"
style={{
background: `linear-gradient(135deg, ${primaryColor} 0%, ${secondaryColor} 100%)`,
border: 'none'
}}
>
主要按钮
</Button>
<Button
style={{
borderColor: primaryColor,
color: primaryColor
}}
>
次要按钮
</Button>
<Tag
color={primaryColor}
style={{ borderRadius: 4 }}
>
标签样式
</Tag>
<div
style={{
width: 60,
height: 24,
borderRadius: 4,
background: `linear-gradient(135deg, ${primaryColor} 0%, ${secondaryColor} 100%)`,
}}
/>
</Space>
</div>
</Card>
<Card
style={{
marginBottom: 24,
borderRadius: 12,
border: '1px solid #e8e8e8',
boxShadow: '0 2px 8px rgba(0,0,0,0.04)',
}}
bodyStyle={{ padding: '24px' }}
>
<div style={{ textAlign: 'center', marginBottom: 24 }}>
<div style={{
width: 48,
height: 48,
borderRadius: '50%',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
marginBottom: 12,
}}>
<DesktopOutlined style={{ color: '#fff', fontSize: 24 }} />
</div>
<div style={{ fontSize: 18, fontWeight: 600, color: '#262626', marginBottom: 6 }}>
界面布局
</div>
<div style={{ fontSize: 14, color: '#666' }}>
调整界面显示密度和布局方式
</div>
</div>
<Divider style={{ margin: '0 0 24px 0' }} />
<Row gutter={[32, 16]}>
<Col xs={24} md={8}>
<Form.Item
label={<span style={{ fontSize: 14, color: '#262626', fontWeight: 500 }}>紧凑模式</span>}
name="compact_mode"
valuePropName="checked"
style={{ marginBottom: 0 }}
>
<Tooltip title="开启后界面元素间距缩小,显示更多内容">
<Switch
checkedChildren="开启"
unCheckedChildren="关闭"
/>
</Tooltip>
</Form.Item>
</Col>
<Col xs={24} md={8}>
<Form.Item
label={<span style={{ fontSize: 14, color: '#262626', fontWeight: 500 }}>侧边栏折叠</span>}
name="sidebar_collapsed"
valuePropName="checked"
style={{ marginBottom: 0 }}
>
<Tooltip title="开启后侧边栏默认折叠为图标模式">
<Switch
checkedChildren="开启"
unCheckedChildren="关闭"
/>
</Tooltip>
</Form.Item>
</Col>
<Col xs={24} md={8}>
<Form.Item
label={<span style={{ fontSize: 14, color: '#262626', fontWeight: 500 }}>表格行高度</span>}
name="table_row_height"
style={{ marginBottom: 0 }}
>
<Select
placeholder="请选择"
style={{ width: '100%', height: 40 }}
dropdownStyle={{ borderRadius: 8 }}
>
{getSelectOptions('table_row_height').map(opt => (
<Option key={opt.value} value={opt.value}>{opt.label}</Option>
))}
</Select>
</Form.Item>
</Col>
</Row>
</Card>
<Card
style={{
marginBottom: 24,
borderRadius: 12,
border: '1px solid #e8e8e8',
boxShadow: '0 2px 8px rgba(0,0,0,0.04)',
}}
bodyStyle={{ padding: '24px' }}
>
<div style={{ textAlign: 'center', marginBottom: 24 }}>
<div style={{
width: 48,
height: 48,
borderRadius: '50%',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
marginBottom: 12,
}}>
<ThunderboltOutlined style={{ color: '#fff', fontSize: 24 }} />
</div>
<div style={{ fontSize: 18, fontWeight: 600, color: '#262626', marginBottom: 6 }}>
动画效果
</div>
<div style={{ fontSize: 14, color: '#666' }}>
控制界面动画和过渡效果
</div>
</div>
<Divider style={{ margin: '0 0 24px 0' }} />
<Row gutter={[32, 16]}>
<Col xs={24} md={12}>
<Form.Item
label={<span style={{ fontSize: 14, color: '#262626', fontWeight: 500 }}>启用动画</span>}
name="animation_enabled"
valuePropName="checked"
style={{ marginBottom: 0 }}
>
<Tooltip title="关闭后可提升低配设备性能">
<Switch
checkedChildren="开启"
unCheckedChildren="关闭"
/>
</Tooltip>
</Form.Item>
</Col>
</Row>
</Card>
</div>
{renderFixedFooter()}
</Form>