refactor(devices): 实现设备字段动态校验与配置同步

1. 新增动态设备校验Schema生成器,支持从数据库读取字段配置生成校验规则
2. 重构设备增改接口,使用动态校验替代硬编码Schema
3. 调整前端设备表单,适配字段配置并锁定核心系统字段
4. 修复前后端默认字段配置不一致问题
5. 新增字段管理页面防护,锁定核心字段的必填/可见配置
This commit is contained in:
zhang1106
2026-06-12 16:01:41 +08:00
parent 85d6b2c633
commit 1b9a5571ae
8 changed files with 621 additions and 22 deletions
+7 -1
View File
@@ -1,10 +1,16 @@
const logger = require('../utils/logger').module('ValidationMiddleware');
const validate = (schema, source = 'body') => {
const validate = (schemaOrFn, source = 'body') => {
return async (req, res, next) => {
const data = source === 'query' ? req.query : req.body;
try {
// 支持函数形式的schema(动态schema:异步函数或同步函数返回Joi schema)
let schema = schemaOrFn;
if (typeof schemaOrFn === 'function') {
schema = await schemaOrFn();
}
let value;
if (schema.validate && typeof schema.validate === 'function') {
+3 -4
View File
@@ -16,13 +16,12 @@ const {
} = require('../utils/operationLogger');
const { validateBody, validateQuery } = require('../middleware/validation');
const {
createDeviceSchema,
updateDeviceSchema,
batchDeviceIdsSchema,
batchStatusSchema,
batchMoveSchema,
queryDeviceSchema,
} = require('../validation/deviceSchema');
const { createDeviceSchema } = require('../validation/dynamicDeviceSchema');
const PREVIEW_COUNT = 20;
@@ -736,7 +735,7 @@ async function generateDeviceId() {
}
// 创建设备
router.post('/', validateBody(createDeviceSchema), async (req, res) => {
router.post('/', validateBody(() => createDeviceSchema(false)), async (req, res) => {
try {
const deviceData = { ...req.body };
@@ -2285,7 +2284,7 @@ router.get('/:deviceId/tickets', async (req, res) => {
});
// 更新设备
router.put('/:deviceId', validateBody(updateDeviceSchema), async (req, res) => {
router.put('/:deviceId', validateBody(() => createDeviceSchema(true)), async (req, res) => {
try {
const oldDevice = await Device.findByPk(req.params.deviceId);
if (!oldDevice) {
-2
View File
@@ -114,8 +114,6 @@ const queryDeviceSchema = Joi.object({
});
module.exports = {
createDeviceSchema,
updateDeviceSchema,
batchDeviceIdsSchema,
batchStatusSchema,
batchMoveSchema,
+146
View File
@@ -0,0 +1,146 @@
/**
* 动态设备字段验证Schema生成器
* 根据数据库DeviceField表的配置动态生成Joi验证Schema
* 系统核心字段(name/serialNumber/position/height)强制锁定必填
*/
const Joi = require('joi');
const DeviceField = require('../models/DeviceField');
const DEVICE_TYPES = ['server', 'switch', 'router', 'storage', 'other'];
const DEVICE_STATUS = ['running', 'maintenance', 'offline', 'fault', 'idle'];
// 强制锁定必填的字段(不受字段管理配置影响)
const FORCE_REQUIRED_FIELDS = ['name', 'serialNumber', 'position', 'height'];
/**
* 根据字段配置生成单个字段的Joi校验器
* @param {Object} field - DeviceField数据库记录
* @param {boolean} isUpdate - 是否为更新模式
* @returns {Joi.AnySchema} 该字段对应的Joi校验器
*/
function buildFieldValidator(field, isUpdate) {
const { fieldName, required: fieldRequired } = field;
// 系统核心字段强制必填
const isRequired = FORCE_REQUIRED_FIELDS.includes(fieldName) || fieldRequired;
let validator;
switch (fieldName) {
case 'name':
validator = Joi.string().max(100).messages({
'string.empty': '设备名称不能为空',
'string.max': '设备名称不能超过100个字符',
'any.required': '设备名称是必填字段',
});
if (isRequired && !isUpdate) validator = validator.required();
else validator = validator.allow('', null);
break;
case 'type':
validator = Joi.string().valid(...DEVICE_TYPES).messages({
'any.only': `设备类型必须是以下之一: ${DEVICE_TYPES.join(', ')}`,
'any.required': '设备类型是必填字段',
});
if (isRequired && !isUpdate) validator = validator.required();
break;
case 'serialNumber':
validator = Joi.string().max(100).messages({
'string.empty': '序列号不能为空',
'string.max': '序列号不能超过100个字符',
'any.required': '序列号是必填字段',
});
if (isRequired && !isUpdate) validator = validator.required();
else validator = validator.allow('', null);
break;
case 'rackId':
validator = Joi.string().allow('', null).max(50);
break;
case 'position':
validator = Joi.number().integer().min(1).max(100).allow(null);
break;
case 'height':
validator = Joi.number().integer().min(1).max(50).allow(null);
break;
case 'powerConsumption':
validator = Joi.number().min(0).max(100000).allow(null);
break;
case 'status':
validator = Joi.string().valid(...DEVICE_STATUS).default('offline');
break;
case 'model':
validator = Joi.string().allow('', null).max(100);
break;
case 'ipAddress':
validator = Joi.string().allow('', null).max(50);
break;
case 'description':
validator = Joi.string().allow('', null).max(500);
break;
case 'purchaseDate':
case 'warrantyExpiry':
validator = Joi.date().allow(null);
break;
case 'brand':
validator = Joi.string().allow('', null).max(100);
break;
default:
// 自定义字段走宽松校验
validator = Joi.any().allow(null);
}
return validator;
}
/**
* 动态生成设备创建/更新的Joi Schema
* 每次请求时从DeviceField表读取最新配置
* @param {boolean} isUpdate - 是否为更新模式
* @returns {Promise<Joi.ObjectSchema>} 动态生成的Joi Schema
*/
async function createDeviceSchema(isUpdate = false) {
// 查询所有字段配置
const fieldConfigs = await DeviceField.findAll({
order: [['order', 'ASC']],
});
const schemaMap = {};
// 根据字段配置动态生成每个字段的校验器
fieldConfigs.forEach(field => {
schemaMap[field.fieldName] = buildFieldValidator(field, isUpdate);
});
// 补充customFields(不在DeviceField表中)
schemaMap.customFields = Joi.object().allow(null);
let schema = Joi.object(schemaMap);
// 更新模式要求至少传一个字段
if (isUpdate) {
schema = schema.min(1).messages({
'object.min': '至少需要提供一个字段进行更新',
});
}
return schema;
}
module.exports = {
createDeviceSchema,
DEVICE_TYPES,
DEVICE_STATUS,
FORCE_REQUIRED_FIELDS,
};
@@ -0,0 +1,418 @@
# 设备字段必填规则与可见性同步设计文档
> 版本:1.0.0 | 更新日期:2026-06-12
---
## 1. 问题概述
### 1.1 现状
设备字段管理页面(`/api/deviceFields`)允许用户配置每个字段的:
- **是否必填**`required`
- **是否可见**`visible`
但在设备管理页面添加/编辑设备时,这些配置**完全不生效**。具体表现为:
| 层面 | 当前行为 | 预期行为 |
|------|---------|---------|
| **后端 Joi Schema** | 硬编码写死,不查询数据库 | 从 `DeviceField` 表动态读取 `required` 配置 |
| **前端表单渲染** | `rackId/position/height` 跳过字段配置,硬编码必填 | 跟随字段配置,但对强依赖字段做防护 |
| **前端默认常量** | `powerConsumption` 与后端不一致 | 与后端 `initDeviceFields.js` 保持同步 |
### 1.2 影响范围
- 用户在字段管理页面修改配置后感到困惑(修改不生效)
- 无法灵活控制不同场景下的业务校验规则
- `powerConsumption` 等字段的前后端默认值不一致
---
## 2. 字段强依赖分析
### 2.1 系统字段依赖矩阵
| 字段名 | 字段标识 | 系统字段 | 下游依赖场景 | 是否可改必填 | 是否可改可见 |
|--------|---------|---------|-------------|------------|------------|
| 设备名称 | `name` | 是 | 操作日志、工单关联、拓扑图标签、导出文件名 | **否**(强制必填) | **否**(强制可见) |
| 设备类型 | `type` | 是 | 搜索筛选、统计分类、3D可视化类型渲染 | 是 | 是(但筛选功能受限) |
| 序列号 | `serialNumber` | 是 | **设备唯一标识**:端口关联(按SN)、盘点扫码匹配、空闲设备恢复、工单关联、导入去重、搜索定位 | **否**(强制必填) | **否**(强制可见) |
| 所在机柜 | `rackId` | 是 | 3D可视化定位、机柜容量统计、U位冲突检测、机柜功率计算 | 是 | 是 |
| 位置(U) | `position` | 是 | 3D可视化定位、U位冲突检测、机柜容量计算 | **否**(强制必填) | 是 |
| 高度(U) | `height` | 是 | 3D可视化渲染、U位占用计算 | **否**(强制必填) | 是 |
| 状态 | `status` | 是 | 搜索筛选、统计卡片、3D可视化颜色 | 是 | 是(但筛选功能受限) |
| 设备型号 | `model` | 是 | 搜索筛选 | 是 | 是 |
| 功率(W) | `powerConsumption` | 否 | 机柜功率统计、机房功率监控 | 是 | 是 |
| IP地址 | `ipAddress` | 否 | 搜索筛选、端口关联 | 是 | 是 |
| 购买日期 | `purchaseDate` | 否 | 无强依赖 | 是 | 是 |
| 保修到期 | `warrantyExpiry` | 否 | 无强依赖 | 是 | 是 |
| 描述 | `description` | 否 | 无强依赖 | 是 | 是 |
| 品牌 | `brand` | 否 | 无强依赖 | 是 | 是 |
### 2.2 强依赖字段判定标准
满足以下任一条件的字段,标记为**强制锁定字段**:
1. **唯一标识性**:作为设备在系统中的唯一标识,被其他模块硬引用(如 `serialNumber`
2. **显示必要性**:无此字段设备无法在其他模块中被识别(如 `name`
3. **物理定位性**:无此字段设备无法在3D/平面图中定位(如 `position``height`
---
## 3. 修复方案
### 3.1 总体架构
```
+------------------+ +------------------+ +------------------+
| 字段管理页面 | ----> | DeviceField表 | <---- | 启动初始化 |
| (FieldConfig) | PUT | (数据库) | | (initDeviceFields)|
+------------------+ +------------------+ +------------------+
|
+--------------+--------------+
| | |
v v v
+-----------+ +-----------+ +-----------+
| 设备添加 | | 设备导入 | | 设备编辑 |
| 前端表单 | | 后端导入 | | 后端接口 |
| (Device | | 验证 | | (Joi |
| FormModal)| | (已正常) | | 动态Schema)|
+-----------+ +-----------+ +-----------+
| |
| 读取field.required | 动态查询DeviceField
v v
表单Item rules Joi Schema校验
(强制锁定字段额外防护) (强制锁定字段额外防护)
```
### 3.2 方案一:后端 Joi Schema 动态化(核心)
#### 3.2.1 新增文件
**`backend/validation/dynamicDeviceSchema.js`**
```javascript
const Joi = require('joi');
const DeviceField = require('../models/DeviceField');
const DEVICE_TYPES = ['server', 'switch', 'router', 'storage', 'other'];
const DEVICE_STATUS = ['running', 'maintenance', 'offline', 'fault', 'idle'];
// 强制锁定字段列表(不受字段管理配置影响)
const FORCE_REQUIRED_FIELDS = ['name', 'serialNumber', 'position', 'height'];
/**
* 动态生建设备创建Joi Schema
* 从DeviceField表读取字段配置,结合强制锁定字段规则
* @param {boolean} isUpdate - 是否为更新模式
* @returns {Promise<Joi.ObjectSchema>}
*/
async function createDeviceSchema(isUpdate = false) {
const fieldConfigs = await DeviceField.findAll({
order: [['order', 'ASC']],
});
const schemaMap = {};
fieldConfigs.forEach(field => {
let validator;
// 判断是否强制必填
const isRequired = FORCE_REQUIRED_FIELDS.includes(field.fieldName) || field.required;
switch (field.fieldName) {
case 'name':
validator = Joi.string().max(100);
if (isRequired) validator = validator.required();
else validator = validator.allow('', null);
break;
case 'type':
validator = Joi.string().valid(...DEVICE_TYPES);
if (isRequired) validator = validator.required();
break;
case 'serialNumber':
validator = Joi.string().max(100);
if (isRequired) validator = validator.required();
else validator = validator.allow('', null);
break;
case 'rackId':
validator = Joi.string().allow('', null).max(50);
break;
case 'position':
validator = Joi.number().integer().min(1).max(100).allow(null);
break;
case 'height':
validator = Joi.number().integer().min(1).max(50).allow(null);
break;
case 'powerConsumption':
validator = Joi.number().min(0).max(100000).allow(null);
break;
case 'status':
validator = Joi.string().valid(...DEVICE_STATUS).default('offline');
break;
case 'model':
case 'ipAddress':
case 'description':
validator = Joi.string().allow('', null).max(100);
break;
case 'purchaseDate':
case 'warrantyExpiry':
validator = Joi.date().allow(null);
break;
default:
// 自定义字段走宽松校验
validator = Joi.any().allow(null);
}
// 仅在创建模式下(非更新模式)且字段非强制锁定、且数据库配置为required时使用required()
// 更新模式下避免object.min(1)策略与动态required冲突
schemaMap[field.fieldName] = validator;
});
// 补充未在DeviceField表中的字段
schemaMap.customFields = Joi.object().allow(null);
let schema = Joi.object(schemaMap);
if (isUpdate) {
schema = schema.min(1).messages({
'object.min': '至少需要提供一个字段进行更新',
});
}
return schema;
}
module.exports = {
createDeviceSchema,
DEVICE_TYPES,
DEVICE_STATUS,
FORCE_REQUIRED_FIELDS,
};
```
#### 3.2.2 修改文件
**`backend/routes/devices.js`**
- 移除对静态 `createDeviceSchema``updateDeviceSchema` 的引用
- `router.post('/')` 中请求到来时调用 `createDeviceSchema(false)` 生成动态 schema 进行验证
- `router.put('/:deviceId')` 中调用 `createDeviceSchema(true)` 生成动态 schema
```javascript
// 替换:
// const { createDeviceSchema, updateDeviceSchema } = require('../validation/deviceSchema');
// 为:
const { createDeviceSchema } = require('../validation/dynamicDeviceSchema');
```
**`backend/validation/deviceSchema.js`**
- 移除 `createDeviceSchema``updateDeviceSchema` 导出
- 保留 `batchDeviceIdsSchema``batchStatusSchema``batchMoveSchema``queryDeviceSchema`(这些与字段配置无关)
- `DEVICE_TYPES``DEVICE_STATUS` 枚举可保留供其他模块引用
### 3.3 方案二:前端表单完整适配字段配置
#### 3.3.1 修改文件
**`frontend/src/components/device/DeviceFormModal.jsx`**
**修改点 A** — 取消 `rackId/position/height` 的排除(第272~278行):
```javascript
// 修改前:排除rackId/position/height
const filteredFields = deviceFields.filter(
field =>
field.fieldName !== 'deviceId' &&
field.fieldName !== 'rackId' && // 移除
field.fieldName !== 'position' && // 移除
field.fieldName !== 'height' // 移除
);
// 修改后:只排除deviceId
const filteredFields = deviceFields.filter(
field => field.fieldName !== 'deviceId'
);
```
**修改点 B** — "设备位置选择"区块(第326~429行)的 rules 改为动态:
```javascript
// 修改前:硬编码required: true
<Form.Item
name="rackId"
rules={[{ required: true, message: '请选择机柜' }]}
>...
// 修改后:读取字段配置
const rackField = deviceFields.find(f => f.fieldName === 'rackId');
// ... 使用 rackField?.required 决定是否需要 required 校验
// 强制锁定字段:position/height 即使字段配置为非必填,仍强制必填
const posField = deviceFields.find(f => f.fieldName === 'position');
const isPositionRequired = true; // 强制锁定
<Form.Item
name="position"
rules={isPositionRequired ? [{ required: true, message: '请输入U位' }] : []}
>...
```
**修改点 C** — 位置信息如果设为非必填,提交时给警告:
```javascript
const handleSubmit = values => {
if (!values.position || !values.height) {
Modal.warning({
title: '位置信息不完整',
content: '位置(U位)或高度信息为空,设备将无法在3D视图中准确定位,建议填写完整。',
});
}
// ... 继续提交
};
```
### 3.4 方案三:字段管理页面防护
#### 3.4.1 修改文件
**`frontend/src/pages/FieldConfig.jsx`**(或对应字段管理页面组件)
对强制锁定字段的"必填"和"可见"开关做禁用处理:
| 字段 | 必填开关 | 可见开关 |
|------|---------|---------|
| `name` | 禁用,显示"系统核心字段" | 禁用,显示"系统核心字段" |
| `serialNumber` | 禁用,显示"系统核心字段" | 禁用,显示"系统核心字段" |
| `position` | 禁用,显示"3D定位依赖" | 启用 |
| `height` | 禁用,显示"3D定位依赖" | 启用 |
```jsx
// 伪代码逻辑
const isLockedRequired = ['name', 'serialNumber', 'position', 'height'].includes(field.fieldName);
const isLockedVisible = ['name', 'serialNumber'].includes(field.fieldName);
<Form.Item label="必填">
<Switch
checked={field.required}
disabled={isLockedRequired}
title={isLockedRequired ? '系统核心字段,不可关闭必填' : ''}
/>
</Form.Item>
```
### 3.5 方案四:同步默认值
#### 3.5.1 修改文件
**`frontend/src/constants/deviceManagementConstants.js`**
| 字段 | 修改前 | 修改后 |
|------|-------|-------|
| `deviceId``required` | `true` | `false` |
| `powerConsumption``required` | `false` | `true` |
#### 3.5.2 修改文件
**`backend/validation/deviceSchema.js`**
- 移除 `createDeviceSchema``updateDeviceSchema``module.exports`
- 保留 `batchDeviceIdsSchema``batchStatusSchema``batchMoveSchema``queryDeviceSchema`
---
## 4. 数据流对比
### 4.1 修复前数据流
```
字段管理页面修改 required=true → DeviceField表 更新
|
┌─────────────────────┘
▼ (无读取)
后端 Joi Schema (硬编码) ← 忽略数据库配置,直接拦截请求
前端 DeviceFormModal ← 只读部分字段配置,position等硬编码
```
### 4.2 修复后数据流
```
字段管理页面修改 required=true/false → DeviceField表 更新
|
┌─────────────────────────┴─────────────┐
▼ ▼
设备添加接口 (POST /api/devices) 设备添加弹窗 (DeviceFormModal)
│ │
▼ ▼
createDeviceSchema(false) GET /api/deviceFields
│ │
▼ ▼
动态查询DeviceField表 读取 field.required
强制锁定字段 = required() 强制锁定字段覆盖为必填
其他字段跟随数据库配置 其他字段跟随配置
│ │
▼ ▼
Joi校验 → 通过 → Device.create() Form.Item rules 动态生成
```
---
## 5. 风险与注意事项
### 5.1 兼容性
- 本方案为**新功能**(让字段管理配置真正生效),不涉及向后兼容性破坏
- 已有数据库中的 `DeviceField` 配置保持不变
- 已有 `Device` 表中的数据不受影响
### 5.2 边界情况
| 场景 | 处理策略 |
|------|---------|
| DeviceField 表为空(首次部署) | 动态 schema 降级为最小验证集 |
| 用户在字段管理修改后立即添加设备 | 实时查询 DeviceField 表,无需缓存 |
| 高并发场景 | 每次创建设备都查表,建议后续可加内存缓存+过期机制 |
| 自定义字段(非预定义字段) | 走 `Joi.any().allow(null)` 不限制 |
### 5.3 性能考量
每次创建/更新设备时多一次 `DeviceField.findAll()` 查询。由于:
- `DeviceField` 表数据量很小(<50 条)
- 设备创建/更新频率远低于查询频率
- 无复杂关联查询
该额外查询对性能影响可以忽略不计。
---
## 6. 实施计划
| 步骤 | 文件 | 工作量估算 | 说明 |
|------|------|-----------|------|
| 1 | 新建 `dynamicDeviceSchema.js` | ~60 行 | 核心动态 schema 生成逻辑 |
| 2 | 修改 `devices.js` | ~10 行 | 替换静态 schema 引用 |
| 3 | 修改 `deviceSchema.js` | ~5 行 | 移除已迁移的导出 |
| 4 | 修改 `DeviceFormModal.jsx` | ~30 行 | 适配字段配置 |
| 5 | 修改 `FieldConfig.jsx` | ~20 行 | 锁定字段防护 |
| 6 | 修改 `deviceManagementConstants.js` | ~2 行 | 同步默认值 |
---
## 7. 附录
### 7.1 相关文件清单
| 文件路径 | 操作类型 |
|----------|---------|
| `backend/validation/dynamicDeviceSchema.js` | **新增** |
| `backend/routes/devices.js` | 修改 |
| `backend/validation/deviceSchema.js` | 修改 |
| `frontend/src/components/device/DeviceFormModal.jsx` | 修改 |
| `frontend/src/pages/FieldConfig.jsx` | 修改 |
| `frontend/src/constants/deviceManagementConstants.js` | 修改 |
### 7.2 参考
- 后端导入功能的必填验证逻辑(`backend/routes/devices.js` 第1191~1215行)已正确查询 `DeviceField` 表,本设计参考其实现模式
- 设备模型定义:`backend/models/Device.js`
- 设备字段模型定义:`backend/models/DeviceField.js`
- 默认字段初始化:`backend/initDeviceFields.js`
@@ -269,14 +269,22 @@ const DeviceFormModal = ({
}
};
// 强制锁定必填的字段(不受字段管理配置影响)
const FORCE_REQUIRED_FIELDS = ['name', 'serialNumber', 'position', 'height'];
const filteredFields = deviceFields.filter(
field =>
field.fieldName !== 'deviceId' &&
field.fieldName !== 'rackId' &&
field.fieldName !== 'position' &&
field.fieldName !== 'height'
field => field.fieldName !== 'deviceId'
);
// 获取关键字段的配置(用于设备位置区块)
const rackFieldConfig = deviceFields.find(f => f.fieldName === 'rackId');
const positionFieldConfig = deviceFields.find(f => f.fieldName === 'position');
const heightFieldConfig = deviceFields.find(f => f.fieldName === 'height');
// 动态判断是否必填(强制锁定字段 > 字段配置)
const isPositionRequired = FORCE_REQUIRED_FIELDS.includes('position') || positionFieldConfig?.required;
const isHeightRequired = FORCE_REQUIRED_FIELDS.includes('height') || heightFieldConfig?.required;
const formItems = [];
filteredFields.forEach(field => {
if (field.fieldName === 'serialNumber') {
@@ -356,10 +364,10 @@ const DeviceFormModal = ({
label={
<span>
机柜
<span style={{ color: '#ff4d4f', marginLeft: '4px' }}>*</span>
{rackFieldConfig?.required && <span style={{ color: '#ff4d4f', marginLeft: '4px' }}>*</span>}
</span>
}
rules={[{ required: true, message: '请选择机柜' }]}
rules={rackFieldConfig?.required ? [{ required: true, message: '请选择机柜' }] : []}
style={{ marginBottom: '0' }}
>
<Select
@@ -389,10 +397,10 @@ const DeviceFormModal = ({
label={
<span>
安装位置 (U位)
<span style={{ color: '#ff4d4f', marginLeft: '4px' }}>*</span>
{isPositionRequired && <span style={{ color: '#ff4d4f', marginLeft: '4px' }}>*</span>}
</span>
}
rules={[{ required: true, message: '请输入U位' }]}
rules={isPositionRequired ? [{ required: true, message: '请输入U位' }] : []}
style={{ marginBottom: '0' }}
>
<InputNumber
@@ -410,10 +418,10 @@ const DeviceFormModal = ({
label={
<span>
设备高度 (U)
<span style={{ color: '#ff4d4f', marginLeft: '4px' }}>*</span>
{isHeightRequired && <span style={{ color: '#ff4d4f', marginLeft: '4px' }}>*</span>}
</span>
}
rules={[{ required: true, message: '请输入设备高度' }]}
rules={isHeightRequired ? [{ required: true, message: '请输入设备高度' }] : []}
initialValue={1}
style={{ marginBottom: '0' }}
>
@@ -30,7 +30,7 @@ export const DEFAULT_DEVICE_FIELDS = [
fieldName: 'deviceId',
displayName: '设备ID',
fieldType: 'text',
required: true,
required: false,
visible: true,
editable: false,
},
@@ -101,7 +101,7 @@ export const DEFAULT_DEVICE_FIELDS = [
fieldName: 'powerConsumption',
displayName: '功率(W)',
fieldType: 'number',
required: false,
required: true,
visible: true,
editable: true,
},
+26 -2
View File
@@ -338,6 +338,11 @@ const FIELD_TYPE_OPTIONS = [
{ value: 'textarea', label: '多行文本' },
];
// 强制锁定必填的系统核心字段(不受字段管理配置影响)
const FORCE_LOCKED_REQUIRED_FIELDS = ['name', 'serialNumber', 'position', 'height'];
// 强制锁定可见的系统核心字段(在其他模块强引用,不可关闭可见)
const FORCE_LOCKED_VISIBLE_FIELDS = ['name', 'serialNumber'];
function DeviceFieldManagement() {
const [fields, setFields] = useState([]);
const [loading, setLoading] = useState(true);
@@ -668,8 +673,17 @@ function DeviceFieldManagement() {
label={<span style={formLabelStyle}>必填</span>}
valuePropName="checked"
style={formItemFlexStyle}
tooltip={
editingField && FORCE_LOCKED_REQUIRED_FIELDS.includes(editingField.fieldName)
? '系统核心字段,不可关闭必填'
: undefined
}
>
<Switch />
<Switch
disabled={
editingField && FORCE_LOCKED_REQUIRED_FIELDS.includes(editingField.fieldName)
}
/>
</Form.Item>
<Form.Item
@@ -677,8 +691,18 @@ function DeviceFieldManagement() {
label={<span style={formLabelStyle}>可见</span>}
valuePropName="checked"
style={formItemFlexStyle}
tooltip={
editingField && FORCE_LOCKED_VISIBLE_FIELDS.includes(editingField.fieldName)
? '系统核心字段,在其他模块中被引用,不可关闭可见'
: undefined
}
>
<Switch defaultChecked />
<Switch
defaultChecked
disabled={
editingField && FORCE_LOCKED_VISIBLE_FIELDS.includes(editingField.fieldName)
}
/>
</Form.Item>
</div>