优化机器人任务界面,增加根据区域下发任务,增加规则配置界面

This commit is contained in:
mmc
2026-10-26 19:30:23 +08:00
parent fb363882ac
commit 08736d6d3f
12 changed files with 1821 additions and 544 deletions

View File

@@ -0,0 +1,200 @@
# 在 systemSetting/systemMaintenance 下新增「规则配置」Tab
## Context
当前「系统维护」页面已有「故障码管理」「告警设置」「工单设置」三个 Tab。用户希望在 `systemSetting/systemMaintenance` 页面下再新增一个「规则配置」Tab,用于管理场站的自动规则(autoRule)。该功能复用 `src/api/rule.tsx` 中的接口:
- `selectRuleBySiteId`:`GET /system/autoRule/selectBySiteId`
- `updateTriggerRule`:`POST /system/autoRule/save`(新增/编辑共用,按 `id` 区分)
- `deleteRule`:`POST /system/autoRule/delete`
后端实体结构(用户提供)包含 `ruleName`、`ruleType`、`deviceScope`、`conditions`(`TriggerConditions` JSON)、`actionConfig`(`ActionConfig` JSON)、`enabled`、`ForceExecute`。
## 目标
在 `SystemMaintenancePage` 增加「规则配置」Tab,点击后展示 `RuleConfigManagement` 组件:支持按场站查询规则列表、新增/编辑/查看/删除规则。
## 方案
### 1. 组件拆分
新增文件:`src/components/SystemSetting/RuleConfigManagement.tsx`
复用 `ErrorManagement.tsx` 的成熟模式:
- `Form` 搜索区(规则名称、规则类型、启用状态)
- `Table` 列表展示
- `Modal` + `Form` 新增/编辑/查看详情
### 2. 修改 SystemMaintain.tsx
在 `src/components/SystemSetting/SystemMaintain.tsx` 中:
- 在 `activeSubTab` 初始化 `useEffect` 的白名单里新增 `'rule'`。
- 在 `Tabs` 中新增一个 `TabPane`,key 为 `'rule'`,tab 文案使用 `t('systemSetting.ruleConfig')`。
- 条件渲染 `{activeSubTab === 'rule' && <RuleConfigManagement />}`。
- 引入新组件 `RuleConfigManagement`。
### 3. 接口调用
在 `RuleConfigManagement.tsx` 中引入:
```tsx
import {
updateTriggerRule,
selectRuleBySiteId,
deleteRule,
} from '@/api/rule';
```
- 列表:`selectRuleBySiteId({ siteId: stationId })`,兼容 `res.data` / `res.rows`。
- 保存:`updateTriggerRule(submitData)`,包含 `id`(编辑时)、`ruleName`、`ruleType`、`deviceScope`、`conditions`、`actionConfig`、`enabled`、`ForceExecute`、`orgId`、`siteId`。
- 删除:`deleteRule({ ids: [record.id] })`。
### 4. 前端枚举/选项
由于用户只给出 Java 实体,前端需要定义以下选项(放在组件内或 `src/lib/utils.ts`):
- `RuleTypeOptions`:
- `LOW_BATTERY_RETURN`:低电量返航
- 预留更多 key,方便后续扩展。
- `DeviceScopeOptions`:
- `ALL`:所有设备
- `GROUP`:设备组
- `SOME`:特定的设备
- `ActionTypeOptions`:
- `AUTO_TASK`:路径规划任务
- `SEND_COMMAND`:下发指令
- `ALARM`:告警
- `WORK_ORDER`:工单下发
- `TriggerKeyOptions`:
- `battery`:电量
- `temperature`:温度
- `offline_duration`:离线时长
- `voltage`:电压
- 比较运算符:直接复用现有 `CompareEnumOptions`(EQ/NEQ/GT/LT/GTE/LTE/BETWEEN/NOT_BETWEEN)。
> 考虑到项目现有 `CompareEnumOptions` 使用 `i18n.t('constants.xxx')` 作为 label,规则相关枚举 label 也统一使用 `t('systemSetting.xxx')`,并在 `zh/index.ts` 与 `en/index.ts` 的 `systemSetting` 命名空间下补充 key。
### 5. 表单设计
#### 列表搜索区
- 规则名称(Input)
- 规则类型(Select)
- 启用状态(Select:启用/禁用)
#### Modal 表单字段
- `ruleName`:Input,必填
- `ruleType`:Select(`RuleTypeOptions`),必填,默认 `LOW_BATTERY_RETURN`
- `deviceScope`:Select(`DeviceScopeOptions`),必填
- `conditions.key`:Select(`TriggerKeyOptions`)
- `conditions.operator`:Select(`CompareEnumOptions`)
- `conditions.value`:InputNumber / Input
- 若 operator 为区间(`BETWEEN` / `NOT_BETWEEN`),拆分为 `min` / `max`,保存时拼接为 `value`(如 `"20,80"`)。
- `actionConfig.actionType`:Select(`ActionTypeOptions`)
- `actionConfig.args`:动态表单(根据 `actionType` 显示不同字段)。
- 第一阶段先提供通用 `Map<String, Object>` 文本域或一组 key-value 输入。
- 若时间允许,可针对 `AUTO_TASK` 提供「选择路径」下拉。
- `enabled`:Switch(1/0 或 true/false,按后端实际类型转换)
- `ForceExecute`:Switch
### 6. 数据结构
提交时组装为:
```tsx
const submitData = {
id: modalType === 'edit' ? currentRecord.id : undefined,
ruleName: values.ruleName,
ruleType: values.ruleType,
deviceScope: values.deviceScope,
conditions: {
key: values.key,
operator: values.operator,
value: values.operator === 'BETWEEN' || values.operator === 'NOT_BETWEEN'
? `${values.min},${values.max}`
: values.value,
},
actionConfig: {
actionType: values.actionType,
args: values.args || {},
},
enabled: values.enabled ? 1 : 0, // 或 true/false,按后端 Boolean
ForceExecute: values.ForceExecute ? 1 : 0,
orgId: currentStation?.orgId || '',
siteId: stationId || '',
};
```
> 后端 `enabled` / `ForceExecute` 为 `Boolean`,若后端接受 JSON boolean 可直接传 `true/false`;若接受 0/1 则按实际接口调整。
### 7. i18n 补充
在 `src/locales/zh/index.ts` 与 `src/locales/en/index.ts` 的 `systemSetting` 命名空间下新增:
```ts
ruleConfig: '规则配置',
ruleName: '规则名称',
ruleType: '规则类型',
ruleTypeLowBatteryReturn: '低电量返航',
deviceScope: '适用范围',
deviceScopeAll: '所有设备',
deviceScopeGroup: '设备组',
deviceScopeSome: '特定设备',
triggerKey: '触发字段',
triggerKeyBattery: '电量',
triggerKeyTemperature: '温度',
triggerKeyOfflineDuration: '离线时长',
triggerKeyVoltage: '电压',
operator: '比较运算符',
triggerValue: '阈值',
actionType: '执行动作',
actionTypeAutoTask: '路径规划任务',
actionTypeSendCommand: '下发指令',
actionTypeAlarm: '告警',
actionTypeWorkOrder: '工单下发',
actionArgs: '动作参数',
enabled: '是否启用',
forceExecute: '强制执行',
pleaseInputRuleName: '请输入规则名称',
pleaseSelectRuleType: '请选择规则类型',
pleaseSelectDeviceScope: '请选择适用范围',
pleaseSelectTriggerKey: '请选择触发字段',
pleaseSelectOperator: '请选择比较运算符',
pleaseInputTriggerValue: '请输入阈值',
pleaseSelectActionType: '请选择执行动作',
ruleList: '规则列表',
addRule: '新增规则',
editRule: '编辑规则',
viewRule: '查看规则',
```
### 8. 表格列
| 列名 | 字段 | 说明 |
|---|---|---|
| 规则名称 | ruleName | - |
| 规则类型 | ruleType | 使用 `RuleTypeOptions` 渲染 label |
| 适用范围 | deviceScope | 使用 `DeviceScopeOptions` 渲染 |
| 触发条件 | conditions | 渲染为 `key operator value` 文本 |
| 执行动作 | actionConfig.actionType | 使用 `ActionTypeOptions` 渲染 |
| 启用 | enabled | Switch 或 Tag |
| 强制 | ForceExecute | Tag |
| 操作 | action | 查看/编辑/删除 |
## 关键文件
- `src/components/SystemSetting/SystemMaintain.tsx`:新增 Tab 入口
- `src/components/SystemSetting/RuleConfigManagement.tsx`:新增规则配置组件
- `src/api/rule.tsx`:复用已有接口
- `src/locales/zh/index.ts`:中文 i18n
- `src/locales/en/index.ts`:英文 i18n
## 验证
1. 启动项目,进入「系统设置」→「系统维护」。
2. 确认出现「规则配置」Tab。
3. 列表为空时显示空状态;若已有规则则正确展示。
4. 点击「新增」打开弹窗,填写后保存,调用 `/system/autoRule/save`。
5. 编辑已有规则,确认表单回填正确(区间值拆分为 min/max)。
6. 删除规则,确认调用 `/system/autoRule/delete`。
7. 切换站点后,列表按 `siteId` 重新加载。
8. 运行 `npx esbuild src/components/SystemSetting/RuleConfigManagement.tsx --outfile=_tmp_rule.js --log-level=warning` 校验语法。

View File

@@ -489,6 +489,18 @@ export function downloadDeviceTemplate(params) {
}
///iot/deviceTask/createRegionDefaultTask 下发区域默认任务
export function createRegionDefaultTask(params) {
return request({
url: '/iot/deviceTask/createRegionDefaultTask',
method: 'get',
params,
})
}

34
src/api/rule.tsx Normal file
View File

@@ -0,0 +1,34 @@
import request from './request';
// ==================== 机器人类型 ====================
// 修改触发规则
export function updateTriggerRule(data) {
return request({
url: '/system/autoRule/save',
method: 'post',
data
});
}
//获取场站规则/system/autoRule/selectBySiteId
export function selectRuleBySiteId(data) {
return request({
url: '/system/autoRule/selectBySiteId',
method: 'get',
params: data
});
}
//删除规则/system/autoRule/delete
export function deleteRule(data) {
return request({
url: '/system/autoRule/delete',
method: 'post',
data
});
}

View File

@@ -1280,6 +1280,9 @@ const CesiumMap = React.forwardRef(({ ifClear, ifClearPlanLine, ifClearNavLine,
if (viewerRef.current) return;
const viewer = new window.Cesium.Viewer(containerRef.current, {
useBrowserRecommendedResolution: false,
animation: false,
timeline: false,
baseLayerPicker: true,
@@ -1294,6 +1297,8 @@ const CesiumMap = React.forwardRef(({ ifClear, ifClearPlanLine, ifClearNavLine,
fileExtension: "jpg"
})
});
viewer.resolutionScale = 2.0;
viewerRef.current = viewer;
viewer.scene.globe.baseColor = window.Cesium?.Color.BLACK;
viewer.scene.globe.depthTestAgainstTerrain = false;
@@ -1485,6 +1490,20 @@ const CesiumMap = React.forwardRef(({ ifClear, ifClearPlanLine, ifClearNavLine,
),
duration: 0.8,
});
} else if (focusOverlay.type === 'route') {
// 飞到路线的边界框
const pts = Array.isArray(focusOverlay.points) ? focusOverlay.points : [];
const lons = pts.map(p => Number(p?.lng ?? p?.lon)).filter(n => isFinite(n) && n !== 0);
const lats = pts.map(p => Number(p?.lat)).filter(n => isFinite(n) && n !== 0);
if (!lons.length || !lats.length) return;
setLegendHighlight('');
setHighlightReturnPointId('');
viewer.camera.flyTo({
destination: Cesium.Rectangle.fromDegrees(
Math.min(...lons), Math.min(...lats), Math.max(...lons), Math.max(...lats)
),
duration: 0.8,
});
}
}, [focusOverlay, stationOverlay]);

View File

@@ -0,0 +1,507 @@
import React, { useState, useEffect, useMemo } from 'react';
import {
Table,
Input,
Button,
Form,
Select,
Space,
Tag,
Modal,
Card,
Typography,
Switch,
InputNumber,
Row,
Col,
App,
} from 'antd';
import {
SearchOutlined,
ReloadOutlined,
PlusOutlined,
EditOutlined,
DeleteOutlined,
EyeOutlined,
ExclamationCircleOutlined,
} from '@ant-design/icons';
import {
updateTriggerRule,
selectRuleBySiteId,
deleteRule,
} from '@/api/rule';
import utils, { CompareEnumOptions } from '@/lib/utils';
import { useSelector } from 'react-redux';
import { RootState } from '@/src/store';
import { useTranslation } from 'react-i18next';
const { TextArea } = Input;
const { Text } = Typography;
const { Option } = Select;
const AntdCard = Card as any;
// 规则类型
const RuleTypeOptions = [
{ value: 'LOW_BATTERY_RETURN', labelKey: 'ruleTypeLowBatteryReturn' },
];
// 适用范围
const DeviceScopeOptions = [
{ value: 'ALL', labelKey: 'deviceScopeAll' },
{ value: 'GROUP', labelKey: 'deviceScopeGroup' },
{ value: 'SOME', labelKey: 'deviceScopeSome' },
];
// 触发字段
const TriggerKeyOptions = [
{ value: 'battery', labelKey: 'triggerKeyBattery' },
{ value: 'temperature', labelKey: 'triggerKeyTemperature' },
{ value: 'offline_duration', labelKey: 'triggerKeyOfflineDuration' },
{ value: 'voltage', labelKey: 'triggerKeyVoltage' },
];
// 执行动作
const ActionTypeOptions = [
{ value: 'AUTO_TASK', labelKey: 'actionTypeAutoTask' },
{ value: 'SEND_COMMAND', labelKey: 'actionTypeSendCommand' },
{ value: 'ALARM', labelKey: 'actionTypeAlarm' },
{ value: 'WORK_ORDER', labelKey: 'actionTypeWorkOrder' },
];
const rangeOperators = ['BETWEEN', 'NOT_BETWEEN'];
export default function RuleConfigManagement() {
const { t } = useTranslation();
const { stationId, currentStation } = useSelector((state: RootState) => state.station);
const { modal } = App.useApp();
const [form] = Form.useForm();
const [searchForm] = Form.useForm();
const [loading, setLoading] = useState(false);
const [ruleList, setRuleList] = useState<any[]>([]);
const [filteredList, setFilteredList] = useState<any[]>([]);
const [modalVisible, setModalVisible] = useState(false);
const [modalType, setModalType] = useState<'add' | 'edit' | 'view'>('add');
const [currentRecord, setCurrentRecord] = useState<any>(null);
const [submitting, setSubmitting] = useState(false);
// 获取规则列表
const fetchRuleList = async () => {
if (!stationId) return;
setLoading(true);
try {
const res = await selectRuleBySiteId({ siteId: stationId });
const list = res?.data || res?.rows || [];
setRuleList(list);
setFilteredList(list);
} catch (err) {
utils.message.error(t('systemSetting.operationFailed'));
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchRuleList();
}, [stationId]);
// 搜索过滤
const handleSearch = () => {
searchForm.validateFields().then(values => {
const next = ruleList.filter(item => {
const matchName = !values.ruleName || (item.ruleName || '').toLowerCase().includes(values.ruleName.toLowerCase());
const matchType = !values.ruleType || item.ruleType === values.ruleType;
const matchEnabled = values.enabled === undefined || values.enabled === '' || Number(item.enabled) === Number(values.enabled);
return matchName && matchType && matchEnabled;
});
setFilteredList(next);
});
};
const handleReset = () => {
searchForm.resetFields();
setFilteredList(ruleList);
};
// 打开弹窗
const openModal = (type: 'add' | 'edit' | 'view', record?: any) => {
setModalType(type);
setCurrentRecord(record || null);
setModalVisible(true);
form.resetFields();
if ((type === 'edit' || type === 'view') && record) {
const conditions = record.conditions || {};
const actionConfig = record.actionConfig || {};
const isRange = rangeOperators.includes(conditions.operator);
let min = '';
let max = '';
let value = conditions.value;
if (isRange && typeof conditions.value === 'string' && conditions.value.includes(',')) {
const [a, b] = conditions.value.split(',');
min = a;
max = b;
value = '';
}
let argsJson = '';
try {
argsJson = actionConfig.args ? JSON.stringify(actionConfig.args, null, 2) : '';
} catch (e) {
argsJson = '';
}
form.setFieldsValue({
ruleName: record.ruleName,
ruleType: record.ruleType,
deviceScope: record.deviceScope,
key: conditions.key,
operator: conditions.operator,
value,
min,
max,
actionType: actionConfig.actionType,
argsJson,
enabled: record.enabled === true || record.enabled === 1,
ForceExecute: record.ForceExecute === true || record.ForceExecute === 1,
});
} else {
form.setFieldsValue({
ruleType: 'LOW_BATTERY_RETURN',
deviceScope: 'ALL',
key: 'battery',
operator: 'LT',
actionType: 'AUTO_TASK',
enabled: true,
ForceExecute: false,
});
}
};
// 保存规则
const handleSave = async (values: any) => {
let argsObj: any = {};
if (values.argsJson) {
try {
argsObj = JSON.parse(values.argsJson);
} catch (e) {
utils.message.error(t('systemSetting.invalidJson'));
return;
}
}
const isRange = rangeOperators.includes(values.operator);
const value = isRange ? `${values.min},${values.max}` : values.value;
const submitData: any = {
id: modalType === 'edit' ? currentRecord?.id : undefined,
ruleName: values.ruleName,
ruleType: values.ruleType,
deviceScope: values.deviceScope,
conditions: {
key: values.key,
operator: values.operator,
value,
},
actionConfig: {
actionType: values.actionType,
args: argsObj,
},
enabled: values.enabled ? 1 : 0,
ForceExecute: values.ForceExecute ? 1 : 0,
orgId: currentStation?.orgId || '',
siteId: stationId || '',
};
setSubmitting(true);
try {
const res = await updateTriggerRule(submitData);
if (res?.code === 200) {
utils.message.success(modalType === 'edit' ? t('common.editSuccess') : t('common.addSuccess'));
setModalVisible(false);
fetchRuleList();
} else {
utils.message.error(res?.msg || t('systemSetting.operationFailed'));
}
} catch (err) {
utils.message.error(t('systemSetting.operationFailed'));
} finally {
setSubmitting(false);
}
};
// 删除规则
const handleDelete = (record: any) => {
modal.confirm({
title: t('common.confirmDelete'),
content: t('systemSetting.confirmDeleteRule', { name: record.ruleName }),
icon: <ExclamationCircleOutlined />,
okText: t('common.ok'),
cancelText: t('common.cancel'),
onOk: async () => {
try {
const res = await deleteRule({ ids: [record.id] });
if (res?.code === 200) {
utils.message.success(t('common.deleteSuccess'));
fetchRuleList();
} else {
utils.message.error(res?.msg || t('common.deleteFail'));
}
} catch (err) {
utils.message.error(t('common.deleteFail'));
}
},
});
};
const operatorWatch = Form.useWatch('operator', form);
const isRangeOperator = useMemo(() => rangeOperators.includes(operatorWatch), [operatorWatch]);
const getLabel = (options: any[], value: string, ns: string = 'systemSetting') => {
const item = options.find(o => o.value === value);
return item ? t(`${ns}.${item.labelKey}`) : value;
};
const renderConditions = (record: any) => {
const c = record.conditions || {};
const opItem = CompareEnumOptions.find(o => o.value === c.operator);
const opLabel = opItem ? t(`constants.${opItem.label}`) : c.operator;
return <Text type="secondary">{c.key} {opLabel} {c.value}</Text>;
};
const columns = [
{
title: t('systemSetting.ruleName'),
dataIndex: 'ruleName',
key: 'ruleName',
width: 180,
fixed: 'left' as const,
},
{
title: t('systemSetting.ruleType'),
dataIndex: 'ruleType',
key: 'ruleType',
width: 140,
render: (text: string) => getLabel(RuleTypeOptions, text),
},
{
title: t('systemSetting.deviceScope'),
dataIndex: 'deviceScope',
key: 'deviceScope',
width: 120,
render: (text: string) => getLabel(DeviceScopeOptions, text),
},
{
title: t('systemSetting.triggerKey'),
key: 'conditions',
width: 220,
render: (_: any, record: any) => renderConditions(record),
},
{
title: t('systemSetting.actionType'),
dataIndex: ['actionConfig', 'actionType'],
key: 'actionType',
width: 140,
render: (text: string) => getLabel(ActionTypeOptions, text),
},
{
title: t('systemSetting.enabledLabel'),
dataIndex: 'enabled',
key: 'enabled',
width: 100,
render: (val: any) => <Tag color={val === 1 || val === true ? 'green' : 'default'}>{val === 1 || val === true ? t('common.enable') : t('common.disable')}</Tag>,
},
{
title: t('systemSetting.forceExecute'),
dataIndex: 'ForceExecute',
key: 'ForceExecute',
width: 100,
render: (val: any) => <Tag color={val === 1 || val === true ? 'orange' : 'default'}>{val === 1 || val === true ? t('common.yes') : t('common.no')}</Tag>,
},
{
title: t('common.actions'),
key: 'action',
width: 220,
fixed: 'right' as const,
render: (_: any, record: any) => (
<Space>
<Button type="link" size="small" icon={<EyeOutlined />} onClick={() => openModal('view', record)}>{t('common.view')}</Button>
<Button type="link" size="small" icon={<EditOutlined />} onClick={() => openModal('edit', record)}>{t('common.edit')}</Button>
<Button type="link" size="small" danger icon={<DeleteOutlined />} onClick={() => handleDelete(record)}>{t('common.delete')}</Button>
</Space>
),
},
];
const disabledForm = modalType === 'view';
return (
<div style={{ padding: '0px 0px', background: '#f5f7fa', minHeight: 'calc(100vh - 120px)' }}>
<AntdCard style={{ borderRadius: 8 }}>
<Form form={searchForm} layout="inline" style={{ marginBottom: 16 }}>
<Form.Item name="ruleName" label={t('systemSetting.ruleName')}>
<Input placeholder={t('systemSetting.pleaseInputRuleName')} style={{ width: 160 }} allowClear />
</Form.Item>
<Form.Item name="ruleType" label={t('systemSetting.ruleType')}>
<Select placeholder={t('systemSetting.pleaseSelectRuleType')} style={{ width: 150 }} allowClear>
{RuleTypeOptions.map(option => (
<Option key={option.value} value={option.value}>{t(`systemSetting.${option.labelKey}`)}</Option>
))}
</Select>
</Form.Item>
<Form.Item name="enabled" label={t('systemSetting.enabledLabel')}>
<Select placeholder={t('common.pleaseSelect')} style={{ width: 120 }} allowClear>
<Option value={1}>{t('common.enable')}</Option>
<Option value={0}>{t('common.disable')}</Option>
</Select>
</Form.Item>
<Form.Item>
<Space>
<Button type="primary" icon={<SearchOutlined />} onClick={handleSearch}>{t('common.search2')}</Button>
<Button icon={<ReloadOutlined />} onClick={handleReset}>{t('common.reset')}</Button>
<Button type="primary" icon={<PlusOutlined />} onClick={() => openModal('add')}>{t('systemSetting.addRule')}</Button>
</Space>
</Form.Item>
</Form>
<Table
rowKey="id"
columns={columns}
dataSource={filteredList}
loading={loading}
scroll={{ x: 1100 }}
pagination={{ pageSize: 10, showSizeChanger: true, showTotal: total => t('common.total', { total }) }}
/>
</AntdCard>
<Modal
title={modalType === 'add' ? t('systemSetting.addRule') : modalType === 'edit' ? t('systemSetting.editRule') : t('systemSetting.viewRule')}
open={modalVisible}
width={720}
maskClosable={false}
destroyOnClose
onCancel={() => setModalVisible(false)}
footer={
disabledForm ? (
<Button onClick={() => setModalVisible(false)}>{t('common.close')}</Button>
) : (
<Space>
<Button onClick={() => setModalVisible(false)}>{t('common.cancel')}</Button>
<Button type="primary" loading={submitting} onClick={() => form.submit()}>{t('common.save')}</Button>
</Space>
)
}
>
<Form
form={form}
layout="vertical"
disabled={disabledForm}
onFinish={handleSave}
>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="ruleName" label={t('systemSetting.ruleName')} rules={[{ required: true, message: t('systemSetting.pleaseInputRuleName') }]}>
<Input placeholder={t('systemSetting.pleaseInputRuleName')} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="ruleType" label={t('systemSetting.ruleType')} rules={[{ required: true, message: t('systemSetting.pleaseSelectRuleType') }]}>
<Select placeholder={t('systemSetting.pleaseSelectRuleType')}>
{RuleTypeOptions.map(option => (
<Option key={option.value} value={option.value}>{t(`systemSetting.${option.labelKey}`)}</Option>
))}
</Select>
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="deviceScope" label={t('systemSetting.deviceScope')} rules={[{ required: true, message: t('systemSetting.pleaseSelectDeviceScope') }]}>
<Select placeholder={t('systemSetting.pleaseSelectDeviceScope')}>
{DeviceScopeOptions.map(option => (
<Option key={option.value} value={option.value}>{t(`systemSetting.${option.labelKey}`)}</Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="key" label={t('systemSetting.triggerKey')} rules={[{ required: true, message: t('systemSetting.pleaseSelectTriggerKey') }]}>
<Select placeholder={t('systemSetting.pleaseSelectTriggerKey')}>
{TriggerKeyOptions.map(option => (
<Option key={option.value} value={option.value}>{t(`systemSetting.${option.labelKey}`)}</Option>
))}
</Select>
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="operator" label={t('systemSetting.operator')} rules={[{ required: true, message: t('systemSetting.pleaseSelectOperator') }]}>
<Select placeholder={t('systemSetting.pleaseSelectOperator')}>
{CompareEnumOptions.map(option => (
<Option key={option.value} value={option.value}>{t(`constants.${option.label}`)}</Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={12}>
{isRangeOperator ? (
<Row gutter={8}>
<Col span={12}>
<Form.Item name="min" label={t('systemSetting.triggerValue')} rules={[{ required: true, message: t('systemSetting.pleaseInputTriggerValue') }]}>
<InputNumber style={{ width: '100%' }} placeholder={t('systemSetting.pleaseInputTriggerValue')} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="max" label=" " rules={[{ required: true, message: t('systemSetting.pleaseInputTriggerValue') }]}>
<InputNumber style={{ width: '100%' }} placeholder={t('systemSetting.pleaseInputTriggerValue')} />
</Form.Item>
</Col>
</Row>
) : (
<Form.Item name="value" label={t('systemSetting.triggerValue')} rules={[{ required: true, message: t('systemSetting.pleaseInputTriggerValue') }]}>
<InputNumber style={{ width: '100%' }} placeholder={t('systemSetting.pleaseInputTriggerValue')} />
</Form.Item>
)}
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="actionType" label={t('systemSetting.actionType')} rules={[{ required: true, message: t('systemSetting.pleaseSelectActionType') }]}>
<Select placeholder={t('systemSetting.pleaseSelectActionType')}>
{ActionTypeOptions.map(option => (
<Option key={option.value} value={option.value}>{t(`systemSetting.${option.labelKey}`)}</Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="enabled" label={t('systemSetting.enabledLabel')} valuePropName="checked">
<Switch checkedChildren={t('common.enable')} unCheckedChildren={t('common.disable')} />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={24}>
<Form.Item name="argsJson" label={t('systemSetting.actionArgs')}>
<TextArea rows={4} placeholder={`{\n "routeId": 123\n}`} />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="ForceExecute" label={t('systemSetting.forceExecute')} valuePropName="checked">
<Switch checkedChildren={t('common.yes')} unCheckedChildren={t('common.no')} />
</Form.Item>
</Col>
</Row>
</Form>
</Modal>
</div>
);
}

View File

@@ -1,10 +1,11 @@
import React, { useState, useEffect } from 'react';
import { useSearchParams } from 'react-router-dom';
import { Card, Tabs, Typography } from 'antd';
import { ExclamationCircleOutlined, AlertOutlined, FormOutlined } from '@ant-design/icons';
import { ExclamationCircleOutlined, AlertOutlined, FormOutlined, SafetyCertificateOutlined } from '@ant-design/icons';
import ErrorManagement from './ErrorManagement';
import AlarmSettingsManagement from './AlarmSettingsManagement';
import WorkOrderSettingsManagement from './WorkOrderSettingsManagement';
import RuleConfigManagement from './RuleConfigManagement';
import { useTranslation } from 'react-i18next';
const { TabPane } = Tabs;
@@ -17,7 +18,7 @@ export const SystemMaintenancePage = () => {
// 初始化:从url读取tab
useEffect(() => {
const tab = searchParams.get('subTab');
if (tab && ['error', 'alarm', 'workOrder'].includes(tab)) {
if (tab && ['error', 'alarm', 'workOrder', 'rule'].includes(tab)) {
setActiveSubTab(tab);
}
}, [searchParams]);
@@ -38,12 +39,14 @@ export const SystemMaintenancePage = () => {
<TabPane tab={<span><ExclamationCircleOutlined />{t('systemSetting.fixed2Error')}</span>} key="error" />
<TabPane tab={<span><AlertOutlined />{t('systemSetting.fixed2Alert')}</span>} key="alarm" />
<TabPane tab={<span><FormOutlined />{t('systemSetting.s2')}</span>} key="workOrder" />
<TabPane tab={<span><SafetyCertificateOutlined />{t('systemSetting.ruleConfig')}</span>} key="rule" />
</Tabs>
</Card>
{activeSubTab === 'error' && <ErrorManagement />}
{activeSubTab === 'alarm' && <AlarmSettingsManagement />}
{activeSubTab === 'workOrder' && <WorkOrderSettingsManagement />}
{activeSubTab === 'rule' && <RuleConfigManagement />}
</div>
);
};

View File

@@ -853,7 +853,7 @@ export default function MapEditorPage() {
>
<MapPinned size={18} color="#13C2C2" style={{ alignSelf: 'center' }} />
<div style={{ fontSize: 13, marginTop: 4 }}>分区</div>
<div style={{ fontSize: 11, color: '#86909C' }}>机器人任务中的休息位置</div>
<div style={{ fontSize: 11, color: '#86909C' }}>场站中发分区,绑定规划路线</div>
</div>
<div
onClick={() => setActiveType('routeGen')}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,161 @@
.task-type-radio {
width: 100%;
box-sizing: border-box;
}
.task-type-radio__title {
margin-bottom: 12px;
color: #000;
font-size: 14px;
font-weight: 600;
line-height: 22px;
}
.task-type-radio__group {
display: flex;
width: 100%;
gap: 12px;
}
.task-type-radio__option.ant-radio-wrapper {
display: flex;
flex: 1;
min-width: 0;
margin-inline-end: 0;
align-items: stretch;
color: inherit;
white-space: normal;
}
.task-type-radio__option > .ant-radio {
display: none;
}
.task-type-radio__option > span:last-child {
display: block;
flex: 1;
min-width: 0;
}
.task-type-radio__card {
position: relative;
display: flex;
width: 100%;
min-height: 62px;
padding: 10px;
box-sizing: border-box;
align-items: center;
gap: 10px;
background: #fff;
border: 1px solid #dce5f2;
border-radius: 6px;
cursor: pointer;
transition:
border-color 0.2s ease,
background-color 0.2s ease,
box-shadow 0.2s ease;
}
.task-type-radio__card:hover {
border-color: #91caff;
}
.task-type-radio__card--checked {
border: 1px solid #4096ff;
background: #f0f7ff;
box-shadow: 0 0 0 1px rgb(22 119 255 / 5%);
}
.task-type-radio__icon {
display: flex;
flex-shrink: 0;
width: 32px;
height: 32px;
align-items: center;
justify-content: center;
color: #1677ff;
background: #e6f4ff;
border-radius: 5px;
font-size: 20px;
}
.task-type-radio__content {
flex: 1;
min-width: 0;
overflow: hidden;
}
.task-type-radio__name {
margin-bottom: 4px;
overflow: hidden;
color: #262626;
font-size: 13px;
font-weight: 600;
line-height: 18px;
text-overflow: ellipsis;
white-space: nowrap;
}
.task-type-radio__description {
overflow: hidden;
color: #8c9bb0;
font-size: 11px;
font-weight: 400;
line-height: 15px;
text-overflow: ellipsis;
white-space: nowrap;
}
.task-type-radio__status {
display: flex;
flex-shrink: 0;
width: 14px;
height: 14px;
align-items: center;
justify-content: center;
color: #1677ff;
font-size: 12px;
}
.task-type-radio__empty {
display: block;
width: 12px;
height: 12px;
box-sizing: border-box;
border: 1px solid #cbd5e1;
border-radius: 50%;
background: #fff;
}
.task-type-radio__option:focus-visible
.task-type-radio__card {
outline: 2px solid #91caff;
outline-offset: 2px;
}
@media (max-width: 480px) {
.task-type-radio__group {
gap: 8px;
}
.task-type-radio__card {
padding: 8px;
gap: 7px;
}
.task-type-radio__icon {
width: 28px;
height: 28px;
font-size: 17px;
}
.task-type-radio__description {
white-space: normal;
}
}

View File

@@ -0,0 +1,100 @@
import React from 'react';
import { Radio } from 'antd';
import {
LoginOutlined,
ClockCircleOutlined,
CheckCircleFilled,
} from '@ant-design/icons';
import './TaskTypeRadio.css'
type DispatchMode = 'immediate' | 'scheduled';
interface TaskTypeRadioProps {
value: DispatchMode;
onChange: (value: DispatchMode) => void;
}
const TaskTypeRadio: React.FC<TaskTypeRadioProps> = ({
value,
onChange,
}) => {
const options: {
value: DispatchMode;
title: string;
description: string;
icon: React.ReactNode;
}[] = [
{
value: 'immediate',
title: '立即执行任务',
description: '下发后机器人立即开始执行',
icon: <LoginOutlined />,
},
{
value: 'scheduled',
title: '定时任务',
description: '设置开始时间和重复周期',
icon: <ClockCircleOutlined />,
},
];
return (
<div className="task-type-radio">
<div className="task-type-radio__title">
选择任务类型
</div>
<Radio.Group
value={value}
onChange={(e) => onChange(e.target.value)}
className="task-type-radio__group"
>
{options.map((option) => {
const checked = value === option.value;
return (
<Radio
key={option.value}
value={option.value}
className="task-type-radio__option"
>
<div
className={[
'task-type-radio__card',
checked
? 'task-type-radio__card--checked'
: '',
]
.filter(Boolean)
.join(' ')}
>
<div className="task-type-radio__icon">
{option.icon}
</div>
<div className="task-type-radio__content">
<div className="task-type-radio__name">
{option.title}
</div>
<div className="task-type-radio__description">
{option.description}
</div>
</div>
<div className="task-type-radio__status">
{checked ? (
<CheckCircleFilled />
) : (
<span className="task-type-radio__empty" />
)}
</div>
</div>
</Radio>
);
})}
</Radio.Group>
</div>
);
};
export default TaskTypeRadio;

View File

@@ -960,6 +960,28 @@
repeatNone: 'Repeat None',
repeatTypeLabel: 'Repeat Type Label',
repeatWeekly: 'Repeat Weekly',
taskDispatch: 'Task Dispatch',
dispatchSource: 'Dispatch Source',
dispatchByRoute: 'By Route',
dispatchByRegion: 'By Region',
noRouteInRegion: 'No routes bound to this region',
taskSelectRegion: 'Select Task Region',
taskDispatchMode: 'Dispatch Mode',
taskModeImmediate: 'Immediate',
taskModeScheduled: 'Scheduled',
taskModeImmediateTip: 'Execute immediately after dispatch',
taskModeScheduledTip: 'Execute automatically by the configured schedule',
taskConfig: 'Task Configuration',
taskSelectRoute: 'Select Routes',
taskSelectRegionTip: 'Please select task regions first — bound routes will be linked automatically',
taskSelectRobot: 'Execute Robot',
taskScheduleSetting: 'Scheduled Task Settings',
taskExecuteNow: 'Dispatch',
taskSaveSchedule: 'Save Schedule',
taskSelectRegionFirst: 'Please select task region first',
taskSelectRouteFirst: 'The selected region has no bound routes',
taskSelectRobotFirst: 'Please select an execute robot',
taskDispatchResult: 'Dispatched. Success {success}, Failed {fail}',
requestControlPermission: 'Request Control Permission',
controlRequesting: 'Requesting...',
controlGranted: 'Control Granted',
@@ -2582,6 +2604,42 @@
workOrderCompletionRate: 'Work Order Completion Rate',
},
systemSetting: {
ruleConfig: 'Rule Config',
ruleName: 'Rule Name',
ruleType: 'Rule Type',
ruleTypeLowBatteryReturn: 'Low Battery Return',
deviceScope: 'Device Scope',
deviceScopeAll: 'All Devices',
deviceScopeGroup: 'Device Group',
deviceScopeSome: 'Specific Devices',
triggerKey: 'Trigger Field',
triggerKeyBattery: 'Battery',
triggerKeyTemperature: 'Temperature',
triggerKeyOfflineDuration: 'Offline Duration',
triggerKeyVoltage: 'Voltage',
triggerValue: 'Threshold',
actionType: 'Action Type',
actionTypeAutoTask: 'Auto Task',
actionTypeSendCommand: 'Send Command',
actionTypeAlarm: 'Alarm',
actionTypeWorkOrder: 'Work Order',
actionArgs: 'Action Args',
enabledLabel: 'Enabled',
forceExecute: 'Force Execute',
pleaseInputRuleName: 'Please input rule name',
pleaseSelectRuleType: 'Please select rule type',
pleaseSelectDeviceScope: 'Please select device scope',
pleaseSelectTriggerKey: 'Please select trigger field',
pleaseSelectOperator: 'Please select operator',
pleaseInputTriggerValue: 'Please input threshold',
pleaseSelectActionType: 'Please select action type',
ruleList: 'Rule List',
addRule: 'Add Rule',
editRule: 'Edit Rule',
viewRule: 'View Rule',
operator: 'Operator',
invalidJson: 'Invalid action args JSON',
confirmDeleteRule: 'Are you sure to delete rule "{{name}}"?',
flvPlaybackError: 'FLV playback error',
flvNetworkError: 'Network error while loading the stream',
mediaDecodeError: 'Media decode error',

View File

@@ -902,6 +902,28 @@ const zh = {
repeatNone: '重复无',
repeatTypeLabel: '重复类型标签',
repeatWeekly: '重复每周',
taskDispatch: '任务下发',
dispatchSource: '下发方式',
dispatchByRoute: '按路线下发',
dispatchByRegion: '按区域下发',
noRouteInRegion: '该区域暂无绑定路径',
taskSelectRegion: '选择任务区域',
taskDispatchMode: '选择任务方式',
taskModeImmediate: '立即执行',
taskModeScheduled: '定时任务',
taskModeImmediateTip: '下发后立即开始执行',
taskModeScheduledTip: '按照设定的时间周期自动执行',
taskConfig: '任务配置',
taskSelectRoute: '选择路径',
taskSelectRegionTip: '请先选择任务区域,系统会自动关联已绑定的路径',
taskSelectRobot: '执行机器人',
taskScheduleSetting: '定时任务设置',
taskExecuteNow: '下发任务',
taskSaveSchedule: '保存定时任务',
taskSelectRegionFirst: '请先选择任务区域',
taskSelectRouteFirst: '所选区域未关联任何路径',
taskSelectRobotFirst: '请先选择执行机器人',
taskDispatchResult: '下发完成。成功 {success} 条,失败 {fail} 条',
requestControlPermission: '请求控制权限',
controlRequesting: '申请中...',
controlGranted: '已获得控制',
@@ -2644,6 +2666,42 @@ const zh = {
workOrderCompletionRate: '工作工单完成率',
},
systemSetting: {
ruleConfig: '规则配置',
ruleName: '规则名称',
ruleType: '规则类型',
ruleTypeLowBatteryReturn: '低电量返航',
deviceScope: '适用范围',
deviceScopeAll: '所有设备',
deviceScopeGroup: '设备组',
deviceScopeSome: '特定设备',
triggerKey: '触发字段',
triggerKeyBattery: '电量',
triggerKeyTemperature: '温度',
triggerKeyOfflineDuration: '离线时长',
triggerKeyVoltage: '电压',
triggerValue: '阈值',
actionType: '执行动作',
actionTypeAutoTask: '路径规划任务',
actionTypeSendCommand: '下发指令',
actionTypeAlarm: '告警',
actionTypeWorkOrder: '工单下发',
actionArgs: '动作参数',
enabledLabel: '是否启用',
forceExecute: '强制执行',
pleaseInputRuleName: '请输入规则名称',
pleaseSelectRuleType: '请选择规则类型',
pleaseSelectDeviceScope: '请选择适用范围',
pleaseSelectTriggerKey: '请选择触发字段',
pleaseSelectOperator: '请选择比较运算符',
pleaseInputTriggerValue: '请输入阈值',
pleaseSelectActionType: '请选择执行动作',
ruleList: '规则列表',
addRule: '新增规则',
editRule: '编辑规则',
viewRule: '查看规则',
operator: '比较运算符',
invalidJson: '动作参数 JSON 格式不正确',
confirmDeleteRule: '确定删除规则「{{name}}」吗?',
flvPlaybackError: 'FLV 播放出错',
flvNetworkError: '网络错误,加载失败',
mediaDecodeError: '媒体解码失败',