feat(主题): 实现动态主题颜色配置功能
This commit is contained in:
@@ -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;
|
module.exports = router;
|
||||||
|
|||||||
@@ -3,9 +3,21 @@ import axios from 'axios';
|
|||||||
|
|
||||||
const ConfigContext = createContext();
|
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 }) => {
|
export const ConfigProvider = ({ children }) => {
|
||||||
const [config, setConfig] = useState({
|
const [config, setConfig] = useState({
|
||||||
// 默认配置
|
|
||||||
site_name: '机柜管理系统',
|
site_name: '机柜管理系统',
|
||||||
primary_color: '#667eea',
|
primary_color: '#667eea',
|
||||||
secondary_color: '#764ba2',
|
secondary_color: '#764ba2',
|
||||||
@@ -21,14 +33,12 @@ export const ConfigProvider = ({ children }) => {
|
|||||||
});
|
});
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
// 加载系统配置
|
|
||||||
const loadConfig = async () => {
|
const loadConfig = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get('/api/system-settings');
|
const response = await axios.get('/api/system-settings');
|
||||||
const settings = response.data;
|
const settings = response.data;
|
||||||
const configValues = {};
|
const configValues = {};
|
||||||
|
|
||||||
// 将配置转换为扁平结构
|
|
||||||
Object.entries(settings).forEach(([key, value]) => {
|
Object.entries(settings).forEach(([key, value]) => {
|
||||||
configValues[key] = value.value;
|
configValues[key] = value.value;
|
||||||
});
|
});
|
||||||
@@ -37,6 +47,10 @@ export const ConfigProvider = ({ children }) => {
|
|||||||
...prev,
|
...prev,
|
||||||
...configValues,
|
...configValues,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
if (configValues.primary_color || configValues.secondary_color) {
|
||||||
|
applyThemeColors(configValues.primary_color, configValues.secondary_color);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载系统配置失败:', error);
|
console.error('加载系统配置失败:', error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -44,12 +58,16 @@ export const ConfigProvider = ({ children }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 初始化加载配置
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadConfig();
|
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 => {
|
const updateConfig = newConfig => {
|
||||||
setConfig(prev => ({
|
setConfig(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
@@ -57,7 +75,6 @@ export const ConfigProvider = ({ children }) => {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
// 重新加载配置
|
|
||||||
const reloadConfig = async () => {
|
const reloadConfig = async () => {
|
||||||
await loadConfig();
|
await loadConfig();
|
||||||
};
|
};
|
||||||
@@ -69,7 +86,6 @@ export const ConfigProvider = ({ children }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 自定义钩子,方便组件使用配置
|
|
||||||
export const useConfig = () => {
|
export const useConfig = () => {
|
||||||
const context = useContext(ConfigContext);
|
const context = useContext(ConfigContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
|
|||||||
+12
-4
@@ -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;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -349,19 +357,19 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ant-btn-primary {
|
.ant-btn-primary {
|
||||||
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%) !important;
|
background: var(--primary-gradient) !important;
|
||||||
border: none !important;
|
border: none !important;
|
||||||
box-shadow: 0 2px 6px rgba(24, 144, 255, 0.35) !important;
|
box-shadow: 0 2px 6px rgba(24, 144, 255, 0.35) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-btn-primary:hover {
|
.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;
|
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.45) !important;
|
||||||
transform: translateY(-2px) !important;
|
transform: translateY(-2px) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-btn-primary:active {
|
.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;
|
box-shadow: 0 2px 6px rgba(24, 144, 255, 0.35) !important;
|
||||||
transform: translateY(0) !important;
|
transform: translateY(0) !important;
|
||||||
}
|
}
|
||||||
@@ -871,7 +879,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.welcome-banner {
|
.welcome-banner {
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: var(--primary-gradient);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
ScanOutlined,
|
ScanOutlined,
|
||||||
CheckOutlined,
|
CheckOutlined,
|
||||||
CloseOutlined,
|
CloseOutlined,
|
||||||
|
PlusOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
@@ -67,6 +68,12 @@ const InventoryTaskExecution = () => {
|
|||||||
const [scanResult, setScanResult] = useState(null);
|
const [scanResult, setScanResult] = useState(null);
|
||||||
const [scanModalVisible, setScanModalVisible] = useState(false);
|
const [scanModalVisible, setScanModalVisible] = useState(false);
|
||||||
const [recordPagination, setRecordPagination] = useState({ current: 1, pageSize: 20, total: 0 });
|
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');
|
const planId = searchParams.get('planId');
|
||||||
|
|
||||||
@@ -245,6 +252,84 @@ const InventoryTaskExecution = () => {
|
|||||||
fetchPlan();
|
fetchPlan();
|
||||||
}, [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) => {
|
const handleCheck = (record) => {
|
||||||
setCurrentRecord(record);
|
setCurrentRecord(record);
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
@@ -912,6 +997,18 @@ const InventoryTaskExecution = () => {
|
|||||||
</Row>
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{!scanResult.success && scanResult.sn && (
|
||||||
|
<div style={{ textAlign: 'center', marginTop: 16 }}>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
onClick={openQuickAddModal}
|
||||||
|
style={{ borderRadius: 8 }}
|
||||||
|
>
|
||||||
|
快速添加此设备
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -1037,6 +1134,130 @@ const InventoryTaskExecution = () => {
|
|||||||
</Form>
|
</Form>
|
||||||
)}
|
)}
|
||||||
</Modal>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
Badge,
|
Badge,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Avatar,
|
Avatar,
|
||||||
|
ColorPicker,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
SettingOutlined,
|
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 renderAppearanceSettings = () => {
|
||||||
|
const primaryColor = form.getFieldValue('primary_color') || settings.primary_color?.value || '#667eea';
|
||||||
|
const secondaryColor = form.getFieldValue('secondary_color') || settings.secondary_color?.value || '#764ba2';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form form={form} layout="vertical" onFinish={handleSaveSettings}>
|
<Form form={form} layout="vertical" onFinish={handleSaveSettings}>
|
||||||
<div style={{ paddingBottom: 80 }}>
|
<div style={{ paddingBottom: 80 }}>
|
||||||
<Alert
|
<Alert
|
||||||
message="主题颜色设置"
|
message="主题颜色设置"
|
||||||
description="修改主题颜色后需要刷新页面才能生效。建议选择对比度适中的颜色组合。"
|
description="选择颜色后可实时预览效果,保存设置后永久生效。建议选择对比度适中的颜色组合。"
|
||||||
type="info"
|
type="info"
|
||||||
showIcon
|
showIcon
|
||||||
style={{ marginBottom: 24, borderRadius: 12 }}
|
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>
|
</div>
|
||||||
{renderFixedFooter()}
|
{renderFixedFooter()}
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
Reference in New Issue
Block a user