feat(设备管理): 重构设备导出功能,支持导出所有字段
refactor(字段管理): 优化字段选项编辑界面,使用可视化编辑器 refactor(工单字段管理): 重构选项编辑组件,提升用户体验 chore: 移除不再使用的设备字段选项迁移脚本
This commit is contained in:
+175
-130
@@ -1581,6 +1581,181 @@ router.put('/batch-move', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 增强导出设备数据(支持所有字段和自定义字段)
|
||||||
|
router.get('/enhanced-export', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { deviceIds, format = 'csv' } = req.query;
|
||||||
|
|
||||||
|
// 从数据库读取所有字段配置(不过滤 visible,以导出所有信息)
|
||||||
|
const allFields = await DeviceField.findAll({
|
||||||
|
order: [['order', 'ASC']]
|
||||||
|
});
|
||||||
|
|
||||||
|
// 构建字段映射表
|
||||||
|
const fieldMap = {};
|
||||||
|
const fieldLabels = {};
|
||||||
|
allFields.forEach(field => {
|
||||||
|
fieldMap[field.fieldName] = field;
|
||||||
|
fieldLabels[field.fieldName] = field.displayName;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 构建查询条件
|
||||||
|
const where = {};
|
||||||
|
if (deviceIds) {
|
||||||
|
const ids = Array.isArray(deviceIds) ? deviceIds : [deviceIds];
|
||||||
|
where.deviceId = { [Op.in]: ids };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询设备数据
|
||||||
|
const devices = await Device.findAll({
|
||||||
|
where,
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: Rack,
|
||||||
|
include: [{ model: Room }]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (devices.length === 0) {
|
||||||
|
return res.status(404).json({ error: '未找到指定的设备' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 状态和类型映射
|
||||||
|
const statusMap = {
|
||||||
|
running: '运行中',
|
||||||
|
maintenance: '维护中',
|
||||||
|
offline: '离线',
|
||||||
|
fault: '故障'
|
||||||
|
};
|
||||||
|
const typeMap = {
|
||||||
|
server: '服务器',
|
||||||
|
switch: '交换机',
|
||||||
|
router: '路由器',
|
||||||
|
storage: '存储设备',
|
||||||
|
other: '其他设备'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 准备导出数据 - 遍历所有设备
|
||||||
|
const exportData = devices.map(device => {
|
||||||
|
const data = {};
|
||||||
|
|
||||||
|
// 首先处理关联字段(机房)- 如果 DeviceField 中没有配置 roomName,也导出机房信息
|
||||||
|
const hasRoomField = allFields.some(f => f.fieldName === 'roomName');
|
||||||
|
if (!hasRoomField) {
|
||||||
|
data['所在机房'] = device.Rack?.Room?.name || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 遍历所有字段配置动态获取值
|
||||||
|
allFields.forEach(field => {
|
||||||
|
const fieldName = field.fieldName;
|
||||||
|
const label = field.displayName;
|
||||||
|
|
||||||
|
// 首先检查 device 表的直字段
|
||||||
|
if (device[fieldName] !== undefined && device[fieldName] !== null) {
|
||||||
|
if (fieldName === 'rackId') {
|
||||||
|
data[label] = device.Rack?.name || '';
|
||||||
|
} else if (fieldName === 'roomName') {
|
||||||
|
data[label] = device.Rack?.Room?.name || '';
|
||||||
|
} else if (fieldName === 'status') {
|
||||||
|
data[label] = statusMap[device.status] || device.status || '';
|
||||||
|
} else if (fieldName === 'type') {
|
||||||
|
data[label] = typeMap[device.type] || device.type || '';
|
||||||
|
} else if (fieldName === 'purchaseDate' || fieldName === 'warrantyExpiry') {
|
||||||
|
data[label] = device[fieldName] ? new Date(device[fieldName]).toLocaleDateString('zh-CN') : '';
|
||||||
|
} else {
|
||||||
|
data[label] = device[fieldName];
|
||||||
|
}
|
||||||
|
} else if (device.customFields && typeof device.customFields === 'object' && device.customFields[fieldName] !== undefined) {
|
||||||
|
data[label] = device.customFields[fieldName];
|
||||||
|
} else {
|
||||||
|
// 设备表中没有该字段且 customFields 中也没有,设为空字符串
|
||||||
|
data[label] = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 展开 customFields 中额外的自定义字段(不在 DeviceField 配置中的)
|
||||||
|
if (device.customFields && typeof device.customFields === 'object') {
|
||||||
|
Object.entries(device.customFields).forEach(([key, value]) => {
|
||||||
|
if (!fieldMap[key]) {
|
||||||
|
data[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
|
||||||
|
// CSV 导出
|
||||||
|
if (format === 'csv') {
|
||||||
|
// 构建完整的 header 列表(基于所有字段配置 + customFields 中的额外字段)
|
||||||
|
const headerSet = new Set();
|
||||||
|
|
||||||
|
// 添加机房字段(如果存在)
|
||||||
|
const hasRoomField = allFields.some(f => f.fieldName === 'roomName');
|
||||||
|
if (!hasRoomField) {
|
||||||
|
headerSet.add('所在机房');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加所有 DeviceField 配置的字段
|
||||||
|
allFields.forEach(field => {
|
||||||
|
headerSet.add(field.displayName);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 收集所有设备 customFields 中的额外字段键
|
||||||
|
devices.forEach(device => {
|
||||||
|
if (device.customFields && typeof device.customFields === 'object') {
|
||||||
|
Object.keys(device.customFields).forEach(key => {
|
||||||
|
if (!fieldMap[key]) {
|
||||||
|
headerSet.add(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const headers = Array.from(headerSet).map(key => ({ id: key, title: key }));
|
||||||
|
|
||||||
|
if (headers.length === 0) {
|
||||||
|
return res.status(400).json({ error: '没有可导出的字段' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const csvWriter = createObjectCsvWriter({
|
||||||
|
path: path.join(__dirname, '../temp/enhanced_export.csv'),
|
||||||
|
header: headers,
|
||||||
|
encoding: 'utf8'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!fs.existsSync(path.join(__dirname, '../temp'))) {
|
||||||
|
fs.mkdirSync(path.join(__dirname, '../temp'));
|
||||||
|
}
|
||||||
|
|
||||||
|
await csvWriter.writeRecords(exportData);
|
||||||
|
|
||||||
|
const csvContent = fs.readFileSync(path.join(__dirname, '../temp/enhanced_export.csv'), 'utf8');
|
||||||
|
const gbkContent = iconv.encode(csvContent, 'gbk');
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', 'text/csv');
|
||||||
|
res.setHeader('Content-Disposition', 'attachment; filename=devices.csv');
|
||||||
|
res.send(gbkContent);
|
||||||
|
|
||||||
|
fs.unlinkSync(path.join(__dirname, '../temp/enhanced_export.csv'));
|
||||||
|
} else {
|
||||||
|
// JSON 导出
|
||||||
|
res.setHeader('Content-Type', 'application/json');
|
||||||
|
res.setHeader('Content-Disposition', 'attachment; filename=devices.json');
|
||||||
|
res.json({
|
||||||
|
exportTime: new Date().toISOString(),
|
||||||
|
totalCount: devices.length,
|
||||||
|
fields: Object.values(fieldLabels),
|
||||||
|
devices: exportData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('增强导出失败:', error);
|
||||||
|
res.status(500).json({ error: '增强导出失败' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 获取单个设备
|
// 获取单个设备
|
||||||
router.get('/:deviceId', async (req, res) => {
|
router.get('/:deviceId', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@@ -2044,134 +2219,4 @@ router.delete('/:deviceId', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 增强导出设备数据(支持自定义字段)
|
|
||||||
router.get('/enhanced-export', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { deviceIds, format = 'csv', fields, fieldLabels } = req.query;
|
|
||||||
|
|
||||||
// 解析字段列表
|
|
||||||
let selectedFields = [];
|
|
||||||
try {
|
|
||||||
selectedFields = fields ? JSON.parse(fields) : [];
|
|
||||||
} catch (e) {
|
|
||||||
selectedFields = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析字段标签
|
|
||||||
let fieldLabelMap = {};
|
|
||||||
try {
|
|
||||||
fieldLabelMap = fieldLabels ? JSON.parse(fieldLabels) : {};
|
|
||||||
} catch (e) {
|
|
||||||
fieldLabelMap = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构建查询条件
|
|
||||||
const where = {};
|
|
||||||
if (deviceIds) {
|
|
||||||
const ids = Array.isArray(deviceIds) ? deviceIds : [deviceIds];
|
|
||||||
where.deviceId = { [Op.in]: ids };
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查询设备数据
|
|
||||||
const devices = await Device.findAll({
|
|
||||||
where,
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: Rack,
|
|
||||||
include: [
|
|
||||||
{ model: Room }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
if (devices.length === 0) {
|
|
||||||
return res.status(404).json({ error: '未找到指定的设备' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 准备导出数据
|
|
||||||
const exportData = devices.map(device => {
|
|
||||||
const data = {};
|
|
||||||
|
|
||||||
selectedFields.forEach(fieldName => {
|
|
||||||
// 映射字段名到中文标签
|
|
||||||
const label = fieldLabelMap[fieldName] || fieldName;
|
|
||||||
|
|
||||||
// 根据字段名获取值
|
|
||||||
if (fieldName === 'rackName') {
|
|
||||||
data[label] = device.Rack?.name || '';
|
|
||||||
} else if (fieldName === 'roomName') {
|
|
||||||
data[label] = device.Rack?.Room?.name || '';
|
|
||||||
} else if (fieldName === 'status') {
|
|
||||||
const statusMap = {
|
|
||||||
running: '运行中',
|
|
||||||
maintenance: '维护中',
|
|
||||||
offline: '离线',
|
|
||||||
fault: '故障'
|
|
||||||
};
|
|
||||||
data[label] = statusMap[device.status] || device.status;
|
|
||||||
} else if (fieldName === 'type') {
|
|
||||||
const typeMap = {
|
|
||||||
server: '服务器',
|
|
||||||
switch: '交换机',
|
|
||||||
router: '路由器',
|
|
||||||
storage: '存储设备',
|
|
||||||
other: '其他设备'
|
|
||||||
};
|
|
||||||
data[label] = typeMap[device.type] || device.type;
|
|
||||||
} else if (fieldName === 'purchaseDate' || fieldName === 'warrantyExpiry') {
|
|
||||||
data[label] = device[fieldName] ? new Date(device[fieldName]).toLocaleDateString('zh-CN') : '';
|
|
||||||
} else if (fieldName === 'customFields' && device.customFields) {
|
|
||||||
// 如果选择导出自定义字段,展开为单独的列
|
|
||||||
Object.entries(device.customFields).forEach(([key, value]) => {
|
|
||||||
data[key] = value;
|
|
||||||
});
|
|
||||||
} else if (device[fieldName] !== undefined) {
|
|
||||||
data[label] = device[fieldName];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return data;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (format === 'json') {
|
|
||||||
// JSON格式导出
|
|
||||||
res.setHeader('Content-Type', 'application/json');
|
|
||||||
res.setHeader('Content-Disposition', 'attachment; filename=devices.json');
|
|
||||||
res.json({
|
|
||||||
exportTime: new Date().toISOString(),
|
|
||||||
totalCount: devices.length,
|
|
||||||
devices: exportData
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// CSV格式导出
|
|
||||||
const csvWriter = createObjectCsvWriter({
|
|
||||||
path: path.join(__dirname, '../temp/enhanced_export.csv'),
|
|
||||||
header: Object.keys(exportData[0] || {}).map(key => ({ id: key, title: key })),
|
|
||||||
encoding: 'utf8'
|
|
||||||
});
|
|
||||||
|
|
||||||
// 确保temp目录存在
|
|
||||||
if (!fs.existsSync(path.join(__dirname, '../temp'))) {
|
|
||||||
fs.mkdirSync(path.join(__dirname, '../temp'));
|
|
||||||
}
|
|
||||||
|
|
||||||
await csvWriter.writeRecords(exportData);
|
|
||||||
|
|
||||||
const csvContent = fs.readFileSync(path.join(__dirname, '../temp/enhanced_export.csv'), 'utf8');
|
|
||||||
const gbkContent = iconv.encode(csvContent, 'gbk');
|
|
||||||
|
|
||||||
res.setHeader('Content-Type', 'text/csv');
|
|
||||||
res.setHeader('Content-Disposition', 'attachment; filename=devices.csv');
|
|
||||||
|
|
||||||
res.send(gbkContent);
|
|
||||||
|
|
||||||
fs.unlinkSync(path.join(__dirname, '../temp/enhanced_export.csv'));
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('增强导出失败:', error);
|
|
||||||
res.status(500).json({ error: '增强导出失败' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -94,11 +94,6 @@ const migrations = [
|
|||||||
name: '设备字段系统标记',
|
name: '设备字段系统标记',
|
||||||
description: '为 deviceFields 表添加 isSystem 字段,标记系统字段不可删除',
|
description: '为 deviceFields 表添加 isSystem 字段,标记系统字段不可删除',
|
||||||
migrate: migrateDeviceFieldsIsSystem
|
migrate: migrateDeviceFieldsIsSystem
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '设备字段Options配置',
|
|
||||||
description: '确保 deviceFields 表的 type 和 status 字段有正确的 options 配置',
|
|
||||||
migrate: migrateDeviceFieldsOptions
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -637,56 +632,6 @@ async function migrateDeviceFieldsIsSystem() {
|
|||||||
console.log(' 设备字段系统标记迁移完成');
|
console.log(' 设备字段系统标记迁移完成');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function migrateDeviceFieldsOptions() {
|
|
||||||
const DeviceField = require('../models/DeviceField');
|
|
||||||
|
|
||||||
if (!(await tableExists('deviceFields'))) {
|
|
||||||
console.log(' deviceFields 表不存在,跳过');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const deviceTypeOptions = [
|
|
||||||
{ value: 'server', label: '服务器' },
|
|
||||||
{ value: 'switch', label: '交换机' },
|
|
||||||
{ value: 'router', label: '路由器' },
|
|
||||||
{ value: 'storage', label: '存储设备' },
|
|
||||||
{ value: 'other', label: '其他设备' }
|
|
||||||
];
|
|
||||||
|
|
||||||
const statusOptions = [
|
|
||||||
{ value: 'running', label: '运行中' },
|
|
||||||
{ value: 'maintenance', label: '维护中' },
|
|
||||||
{ value: 'offline', label: '离线' },
|
|
||||||
{ value: 'fault', label: '故障' }
|
|
||||||
];
|
|
||||||
|
|
||||||
const typeField = await DeviceField.findOne({ where: { fieldName: 'type' } });
|
|
||||||
if (typeField) {
|
|
||||||
if (!typeField.options || typeField.options.length === 0) {
|
|
||||||
await typeField.update({ options: deviceTypeOptions });
|
|
||||||
console.log(' 已更新 type 字段的 options');
|
|
||||||
} else {
|
|
||||||
console.log(' type 字段 options 已存在,跳过');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log(' type 字段不存在,跳过');
|
|
||||||
}
|
|
||||||
|
|
||||||
const statusField = await DeviceField.findOne({ where: { fieldName: 'status' } });
|
|
||||||
if (statusField) {
|
|
||||||
if (!statusField.options || statusField.options.length === 0) {
|
|
||||||
await statusField.update({ options: statusOptions });
|
|
||||||
console.log(' 已更新 status 字段的 options');
|
|
||||||
} else {
|
|
||||||
console.log(' status 字段 options 已存在,跳过');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log(' status 字段不存在,跳过');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(' 设备字段 Options 配置迁移完成');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function migrateIdleDeviceAndBusiness() {
|
async function migrateIdleDeviceAndBusiness() {
|
||||||
const queryInterface = sequelize.getQueryInterface();
|
const queryInterface = sequelize.getQueryInterface();
|
||||||
const dialect = sequelize.getDialect();
|
const dialect = sequelize.getDialect();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useMemo } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Modal, Form, Select, Checkbox, Button, message } from 'antd';
|
import { Modal, Form, Select, Button } from 'antd';
|
||||||
import { ExportOutlined } from '@ant-design/icons';
|
import { ExportOutlined } from '@ant-design/icons';
|
||||||
import { designTokens } from '../../config/theme';
|
import { designTokens } from '../../config/theme';
|
||||||
|
|
||||||
@@ -15,7 +15,6 @@ const modalHeaderStyle = {
|
|||||||
|
|
||||||
const ExportModal = ({
|
const ExportModal = ({
|
||||||
visible,
|
visible,
|
||||||
deviceFields,
|
|
||||||
selectedDevices,
|
selectedDevices,
|
||||||
currentPageDevices,
|
currentPageDevices,
|
||||||
allDevices,
|
allDevices,
|
||||||
@@ -24,31 +23,14 @@ const ExportModal = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [exportFormat, setExportFormat] = useState('csv');
|
const [exportFormat, setExportFormat] = useState('csv');
|
||||||
const [exportScope, setExportScope] = useState('selected');
|
const [exportScope, setExportScope] = useState('selected');
|
||||||
const [exportFields, setExportFields] = useState([]);
|
|
||||||
const [exportLoading, setExportLoading] = useState(false);
|
const [exportLoading, setExportLoading] = useState(false);
|
||||||
|
|
||||||
const visibleFields = useMemo(() => {
|
|
||||||
return deviceFields.filter((f) => f.visible && f.fieldName !== 'rackId');
|
|
||||||
}, [deviceFields]);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (visible) {
|
|
||||||
setExportFields(visibleFields.map((f) => f.fieldName));
|
|
||||||
}
|
|
||||||
}, [visible, visibleFields]);
|
|
||||||
|
|
||||||
const handleExport = async () => {
|
const handleExport = async () => {
|
||||||
if (exportFields.length === 0) {
|
|
||||||
message.warning('请至少选择一个导出字段');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setExportLoading(true);
|
setExportLoading(true);
|
||||||
try {
|
try {
|
||||||
await onExport({
|
await onExport({
|
||||||
format: exportFormat,
|
format: exportFormat,
|
||||||
scope: exportScope,
|
scope: exportScope,
|
||||||
fields: exportFields,
|
|
||||||
});
|
});
|
||||||
onCancel();
|
onCancel();
|
||||||
} finally {
|
} finally {
|
||||||
@@ -121,7 +103,7 @@ const ExportModal = ({
|
|||||||
},
|
},
|
||||||
body: { padding: '24px' },
|
body: { padding: '24px' },
|
||||||
}}
|
}}
|
||||||
width={600}
|
width={500}
|
||||||
>
|
>
|
||||||
<Form layout="vertical">
|
<Form layout="vertical">
|
||||||
<Form.Item label="导出格式">
|
<Form.Item label="导出格式">
|
||||||
@@ -137,39 +119,8 @@ const ExportModal = ({
|
|||||||
<Option value="all">全部设备 ({allDevices.length} 个)</Option>
|
<Option value="all">全部设备 ({allDevices.length} 个)</Option>
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label="选择导出字段">
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
maxHeight: '300px',
|
|
||||||
overflow: 'auto',
|
|
||||||
border: '1px solid #f0f0f0',
|
|
||||||
borderRadius: '8px',
|
|
||||||
padding: '12px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{visibleFields.map((field) => (
|
|
||||||
<div key={field.fieldName} style={{ marginBottom: '8px' }}>
|
|
||||||
<Checkbox
|
|
||||||
checked={exportFields.includes(field.fieldName)}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (e.target.checked) {
|
|
||||||
setExportFields([...exportFields, field.fieldName]);
|
|
||||||
} else {
|
|
||||||
setExportFields(exportFields.filter((f) => f !== field.fieldName));
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{field.displayName}
|
|
||||||
</Checkbox>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Form.Item>
|
|
||||||
<div style={{ color: '#666', fontSize: '13px' }}>
|
<div style={{ color: '#666', fontSize: '13px' }}>
|
||||||
已选择{' '}
|
将导出设备的所有字段(包括自定义字段)
|
||||||
<span style={{ color: '#1890ff', fontWeight: 600 }}>{selectedDevices.length}</span> 个设备,
|
|
||||||
将导出{' '}
|
|
||||||
<span style={{ color: '#52c41a', fontWeight: 600 }}>{exportFields.length}</span> 个字段
|
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ import {
|
|||||||
CalendarOutlined,
|
CalendarOutlined,
|
||||||
FileTextOutlined,
|
FileTextOutlined,
|
||||||
LockOutlined,
|
LockOutlined,
|
||||||
|
PlusCircleOutlined,
|
||||||
|
MinusCircleOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { designTokens } from '../config/theme';
|
import { designTokens } from '../config/theme';
|
||||||
@@ -33,6 +35,161 @@ import CloseButton from '../components/CloseButton';
|
|||||||
|
|
||||||
const { Option = Select.Option } = Select;
|
const { Option = Select.Option } = Select;
|
||||||
|
|
||||||
|
const OptionsEditor = ({ value = [], onChange }) => {
|
||||||
|
const handleAdd = () => {
|
||||||
|
onChange([...value, { value: '', label: '' }]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemove = index => {
|
||||||
|
onChange(value.filter((_, i) => i !== index));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdate = (index, field, fieldValue) => {
|
||||||
|
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>
|
||||||
|
|
||||||
|
{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>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<PlusCircleOutlined />}
|
||||||
|
onClick={handleAdd}
|
||||||
|
style={{
|
||||||
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '6px',
|
||||||
|
height: '36px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
添加第一个选项
|
||||||
|
</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>
|
||||||
|
{value.map((opt, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '12px',
|
||||||
|
padding: '12px',
|
||||||
|
background: '#fff',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: '1px solid #e8e8e8',
|
||||||
|
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,
|
||||||
|
}}>
|
||||||
|
{index + 1}
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
placeholder="值"
|
||||||
|
value={opt.value}
|
||||||
|
onChange={e => handleUpdate(index, 'value', e.target.value)}
|
||||||
|
style={{ width: '160px', borderRadius: '6px' }}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="标签"
|
||||||
|
value={opt.label}
|
||||||
|
onChange={e => handleUpdate(index, 'label', e.target.value)}
|
||||||
|
style={{ width: '160px', borderRadius: '6px' }}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
danger
|
||||||
|
icon={<MinusCircleOutlined />}
|
||||||
|
onClick={() => handleRemove(index)}
|
||||||
|
style={{ flexShrink: 0 }}
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{value.length > 0 && (
|
||||||
|
<Button
|
||||||
|
type="dashed"
|
||||||
|
icon={<PlusCircleOutlined />}
|
||||||
|
onClick={handleAdd}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '40px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
borderColor: '#d9d9d9',
|
||||||
|
color: '#666',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
添加选项
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const pageContainerStyle = {
|
const pageContainerStyle = {
|
||||||
minHeight: '100vh',
|
minHeight: '100vh',
|
||||||
background: designTokens.colors.background.secondary,
|
background: designTokens.colors.background.secondary,
|
||||||
@@ -176,6 +333,7 @@ function DeviceFieldManagement() {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [modalVisible, setModalVisible] = useState(false);
|
const [modalVisible, setModalVisible] = useState(false);
|
||||||
const [editingField, setEditingField] = useState(null);
|
const [editingField, setEditingField] = useState(null);
|
||||||
|
const [selectedFieldType, setSelectedFieldType] = useState('string');
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [pagination, setPagination] = useState({
|
const [pagination, setPagination] = useState({
|
||||||
current: 1,
|
current: 1,
|
||||||
@@ -208,11 +366,13 @@ function DeviceFieldManagement() {
|
|||||||
if (field) {
|
if (field) {
|
||||||
const fieldData = {
|
const fieldData = {
|
||||||
...field,
|
...field,
|
||||||
options: field.options ? JSON.stringify(field.options, null, 2) : '',
|
options: field.options || [],
|
||||||
};
|
};
|
||||||
|
setSelectedFieldType(field.fieldType || 'string');
|
||||||
form.setFieldsValue(fieldData);
|
form.setFieldsValue(fieldData);
|
||||||
} else {
|
} else {
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
|
setSelectedFieldType('string');
|
||||||
}
|
}
|
||||||
setModalVisible(true);
|
setModalVisible(true);
|
||||||
};
|
};
|
||||||
@@ -220,13 +380,21 @@ function DeviceFieldManagement() {
|
|||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
setModalVisible(false);
|
setModalVisible(false);
|
||||||
setEditingField(null);
|
setEditingField(null);
|
||||||
|
setSelectedFieldType('string');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFieldTypeChange = value => {
|
||||||
|
setSelectedFieldType(value);
|
||||||
|
if (value !== 'select') {
|
||||||
|
form.setFieldsValue({ options: [] });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async values => {
|
const handleSubmit = async values => {
|
||||||
try {
|
try {
|
||||||
const fieldData = {
|
const fieldData = {
|
||||||
...values,
|
...values,
|
||||||
options: values.options ? JSON.parse(values.options) : null,
|
options: values.options && values.options.length > 0 ? values.options : null,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (editingField) {
|
if (editingField) {
|
||||||
@@ -240,6 +408,7 @@ function DeviceFieldManagement() {
|
|||||||
setModalVisible(false);
|
setModalVisible(false);
|
||||||
fetchFields();
|
fetchFields();
|
||||||
setEditingField(null);
|
setEditingField(null);
|
||||||
|
setSelectedFieldType('string');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error(editingField ? '字段更新失败' : '字段创建失败');
|
message.error(editingField ? '字段更新失败' : '字段创建失败');
|
||||||
console.error(editingField ? '字段更新失败:' : '字段创建失败:', error);
|
console.error(editingField ? '字段更新失败:' : '字段创建失败:', error);
|
||||||
@@ -474,7 +643,7 @@ function DeviceFieldManagement() {
|
|||||||
label={<span style={formLabelStyle}>字段类型</span>}
|
label={<span style={formLabelStyle}>字段类型</span>}
|
||||||
rules={[{ required: true, message: '请选择字段类型' }]}
|
rules={[{ required: true, message: '请选择字段类型' }]}
|
||||||
>
|
>
|
||||||
<Select placeholder="请选择字段类型">
|
<Select placeholder="请选择字段类型" onChange={handleFieldTypeChange}>
|
||||||
{FIELD_TYPE_OPTIONS.map(opt => (
|
{FIELD_TYPE_OPTIONS.map(opt => (
|
||||||
<Option key={opt.value} value={opt.value}>
|
<Option key={opt.value} value={opt.value}>
|
||||||
{opt.label}
|
{opt.label}
|
||||||
@@ -511,17 +680,28 @@ function DeviceFieldManagement() {
|
|||||||
<InputNumber placeholder="请输入显示顺序" min={0} style={{ width: '100%' }} />
|
<InputNumber placeholder="请输入显示顺序" min={0} style={{ width: '100%' }} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
{selectedFieldType === 'select' ? (
|
||||||
name="options"
|
<Form.Item
|
||||||
label={<span style={formLabelStyle}>选项配置(JSON格式)</span>}
|
name="options"
|
||||||
tooltip="格式示例:[{value: 'option1', label: '选项1'}],仅下拉选择类型需要配置"
|
label={<span style={formLabelStyle}>选项配置</span>}
|
||||||
>
|
tooltip="为下拉选择类型添加选项,值(value)用于提交数据,标签(label)用于显示"
|
||||||
<Input.TextArea
|
>
|
||||||
rows={3}
|
<OptionsEditor />
|
||||||
placeholder="请输入JSON格式的选项配置,使用单引号"
|
</Form.Item>
|
||||||
style={textAreaStyle}
|
) : (
|
||||||
/>
|
<Form.Item
|
||||||
</Form.Item>
|
name="options"
|
||||||
|
label={<span style={formLabelStyle}>选项配置</span>}
|
||||||
|
tooltip="仅下拉选择类型需要配置选项"
|
||||||
|
>
|
||||||
|
<Input.TextArea
|
||||||
|
rows={2}
|
||||||
|
placeholder="仅下拉选择类型需要配置,此处不可编辑"
|
||||||
|
disabled
|
||||||
|
style={{ background: '#f5f5f5' }}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
|
||||||
<Form.Item style={formActionsStyle}>
|
<Form.Item style={formActionsStyle}>
|
||||||
<Space>
|
<Space>
|
||||||
|
|||||||
@@ -478,12 +478,7 @@ function DeviceManagement() {
|
|||||||
setExportModalVisible(true);
|
setExportModalVisible(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEnhancedExport = async ({ format, scope, fields }) => {
|
const handleEnhancedExport = async ({ format, scope }) => {
|
||||||
const fieldLabels = {};
|
|
||||||
deviceFields.forEach((field) => {
|
|
||||||
fieldLabels[field.fieldName] = field.displayName;
|
|
||||||
});
|
|
||||||
|
|
||||||
let deviceIds = [];
|
let deviceIds = [];
|
||||||
if (scope === 'selected') {
|
if (scope === 'selected') {
|
||||||
deviceIds = selectedDevices;
|
deviceIds = selectedDevices;
|
||||||
@@ -501,8 +496,6 @@ function DeviceManagement() {
|
|||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
deviceIds.forEach((id) => params.append('deviceIds', id));
|
deviceIds.forEach((id) => params.append('deviceIds', id));
|
||||||
params.append('format', format);
|
params.append('format', format);
|
||||||
params.append('fields', JSON.stringify(fields));
|
|
||||||
params.append('fieldLabels', JSON.stringify(fieldLabels));
|
|
||||||
|
|
||||||
const response = await axios.get(`/api/devices/enhanced-export?${params.toString()}`, {
|
const response = await axios.get(`/api/devices/enhanced-export?${params.toString()}`, {
|
||||||
responseType: 'blob',
|
responseType: 'blob',
|
||||||
@@ -1221,7 +1214,6 @@ function DeviceManagement() {
|
|||||||
|
|
||||||
<ExportModal
|
<ExportModal
|
||||||
visible={exportModalVisible}
|
visible={exportModalVisible}
|
||||||
deviceFields={deviceFields}
|
|
||||||
selectedDevices={selectedDevices}
|
selectedDevices={selectedDevices}
|
||||||
currentPageDevices={currentPageDevices}
|
currentPageDevices={currentPageDevices}
|
||||||
allDevices={allDevices}
|
allDevices={allDevices}
|
||||||
|
|||||||
@@ -12,17 +12,173 @@ import {
|
|||||||
InputNumber,
|
InputNumber,
|
||||||
Switch,
|
Switch,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
|
import { PlusOutlined, EditOutlined, DeleteOutlined, PlusCircleOutlined, MinusCircleOutlined } from '@ant-design/icons';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import CloseButton from '../components/CloseButton';
|
import CloseButton from '../components/CloseButton';
|
||||||
|
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
|
|
||||||
|
const OptionsEditor = ({ value = [], onChange }) => {
|
||||||
|
const handleAdd = () => {
|
||||||
|
onChange([...value, { value: '', label: '' }]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemove = index => {
|
||||||
|
onChange(value.filter((_, i) => i !== index));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdate = (index, field, fieldValue) => {
|
||||||
|
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>
|
||||||
|
|
||||||
|
{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>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<PlusCircleOutlined />}
|
||||||
|
onClick={handleAdd}
|
||||||
|
style={{
|
||||||
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '6px',
|
||||||
|
height: '36px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
添加第一个选项
|
||||||
|
</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>
|
||||||
|
{value.map((opt, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '12px',
|
||||||
|
padding: '12px',
|
||||||
|
background: '#fff',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: '1px solid #e8e8e8',
|
||||||
|
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,
|
||||||
|
}}>
|
||||||
|
{index + 1}
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
placeholder="值"
|
||||||
|
value={opt.value}
|
||||||
|
onChange={e => handleUpdate(index, 'value', e.target.value)}
|
||||||
|
style={{ width: '160px', borderRadius: '6px' }}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="标签"
|
||||||
|
value={opt.label}
|
||||||
|
onChange={e => handleUpdate(index, 'label', e.target.value)}
|
||||||
|
style={{ width: '160px', borderRadius: '6px' }}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
danger
|
||||||
|
icon={<MinusCircleOutlined />}
|
||||||
|
onClick={() => handleRemove(index)}
|
||||||
|
style={{ flexShrink: 0 }}
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{value.length > 0 && (
|
||||||
|
<Button
|
||||||
|
type="dashed"
|
||||||
|
icon={<PlusCircleOutlined />}
|
||||||
|
onClick={handleAdd}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '40px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
borderColor: '#d9d9d9',
|
||||||
|
color: '#666',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
添加选项
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
function TicketFieldManagement() {
|
function TicketFieldManagement() {
|
||||||
const [fields, setFields] = useState([]);
|
const [fields, setFields] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [modalVisible, setModalVisible] = useState(false);
|
const [modalVisible, setModalVisible] = useState(false);
|
||||||
const [editingField, setEditingField] = useState(null);
|
const [editingField, setEditingField] = useState(null);
|
||||||
|
const [selectedFieldType, setSelectedFieldType] = useState('string');
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
const fetchFields = async () => {
|
const fetchFields = async () => {
|
||||||
@@ -47,11 +203,13 @@ function TicketFieldManagement() {
|
|||||||
if (field) {
|
if (field) {
|
||||||
const fieldData = {
|
const fieldData = {
|
||||||
...field,
|
...field,
|
||||||
options: field.options ? JSON.stringify(field.options, null, 2) : '',
|
options: field.options || [],
|
||||||
};
|
};
|
||||||
|
setSelectedFieldType(field.fieldType || 'string');
|
||||||
form.setFieldsValue(fieldData);
|
form.setFieldsValue(fieldData);
|
||||||
} else {
|
} else {
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
|
setSelectedFieldType('string');
|
||||||
}
|
}
|
||||||
setModalVisible(true);
|
setModalVisible(true);
|
||||||
};
|
};
|
||||||
@@ -59,13 +217,21 @@ function TicketFieldManagement() {
|
|||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
setModalVisible(false);
|
setModalVisible(false);
|
||||||
setEditingField(null);
|
setEditingField(null);
|
||||||
|
setSelectedFieldType('string');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFieldTypeChange = value => {
|
||||||
|
setSelectedFieldType(value);
|
||||||
|
if (value !== 'select') {
|
||||||
|
form.setFieldsValue({ options: [] });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async values => {
|
const handleSubmit = async values => {
|
||||||
try {
|
try {
|
||||||
const fieldData = {
|
const fieldData = {
|
||||||
...values,
|
...values,
|
||||||
options: values.options ? JSON.parse(values.options || '[]') : null,
|
options: values.options && values.options.length > 0 ? values.options : null,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (editingField) {
|
if (editingField) {
|
||||||
@@ -79,6 +245,7 @@ function TicketFieldManagement() {
|
|||||||
setModalVisible(false);
|
setModalVisible(false);
|
||||||
fetchFields();
|
fetchFields();
|
||||||
setEditingField(null);
|
setEditingField(null);
|
||||||
|
setSelectedFieldType('string');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error(editingField ? '字段更新失败' : '字段创建失败');
|
message.error(editingField ? '字段更新失败' : '字段创建失败');
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@@ -226,7 +393,7 @@ function TicketFieldManagement() {
|
|||||||
label="字段类型"
|
label="字段类型"
|
||||||
rules={[{ required: true, message: '请选择字段类型' }]}
|
rules={[{ required: true, message: '请选择字段类型' }]}
|
||||||
>
|
>
|
||||||
<Select placeholder="请选择字段类型">
|
<Select placeholder="请选择字段类型" onChange={handleFieldTypeChange}>
|
||||||
<Option value="string">文本</Option>
|
<Option value="string">文本</Option>
|
||||||
<Option value="number">数字</Option>
|
<Option value="number">数字</Option>
|
||||||
<Option value="boolean">布尔值</Option>
|
<Option value="boolean">布尔值</Option>
|
||||||
@@ -254,13 +421,28 @@ function TicketFieldManagement() {
|
|||||||
<InputNumber placeholder="请输入显示顺序" min={0} style={{ width: '100%' }} />
|
<InputNumber placeholder="请输入显示顺序" min={0} style={{ width: '100%' }} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
{selectedFieldType === 'select' ? (
|
||||||
name="options"
|
<Form.Item
|
||||||
label="选项配置(仅下拉选择类型,JSON格式)"
|
name="options"
|
||||||
tooltip="格式示例:[{value: 'option1', label: '选项1'}]"
|
label="选项配置"
|
||||||
>
|
tooltip="为下拉选择类型添加选项,值(value)用于提交数据,标签(label)用于显示"
|
||||||
<Input.TextArea rows={3} placeholder='[{"value": "option1", "label": "选项1"}]' />
|
>
|
||||||
</Form.Item>
|
<OptionsEditor />
|
||||||
|
</Form.Item>
|
||||||
|
) : (
|
||||||
|
<Form.Item
|
||||||
|
name="options"
|
||||||
|
label="选项配置"
|
||||||
|
tooltip="仅下拉选择类型需要配置选项"
|
||||||
|
>
|
||||||
|
<Input.TextArea
|
||||||
|
rows={2}
|
||||||
|
placeholder="仅下拉选择类型需要配置,此处不可编辑"
|
||||||
|
disabled
|
||||||
|
style={{ background: '#f5f5f5' }}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
|
||||||
<Form.Item style={{ textAlign: 'right' }}>
|
<Form.Item style={{ textAlign: 'right' }}>
|
||||||
<Space>
|
<Space>
|
||||||
|
|||||||
Reference in New Issue
Block a user