Преглед изворни кода

feat: 新增DTU配置管理功能

1. 新增后端DTU MQTT协议支持与配置API
2. 新增前端DTU配置页面与状态管理
3. 完善系统配置页签与接口服务
wenhongquan пре 13 часа
родитељ
комит
16195b3e88

+ 477 - 4
backend/app.py

@@ -30,7 +30,20 @@ from config import (
     SOCKETIO_NAMESPACE_STATUS,
     SOCKETIO_NAMESPACE_CONTROL,
     ERROR_CODES,
-    ERROR_MESSAGES
+    ERROR_MESSAGES,
+    DEFAULT_CUSTOMER_ID,
+    DEFAULT_DTU_ID,
+    DTU_HEARTBEAT_INTERVAL,
+    DEFAULT_MQTT_TOPIC_PREFIX,
+    DTU_REGISTRATION_TOPIC,
+    DTU_STATUS_TOPIC,
+    DTU_CONTROL_TOPIC,
+    DTU_RESPONSE_TOPIC,
+    DTU_EVENT_TOPIC,
+    DTU_ALARM_TOPIC,
+    DTU_BROADCAST_TOPIC,
+    MAX_PANELS,
+    MAX_PORTS_PER_PANEL
 )
 
 # 配置日志
@@ -78,6 +91,23 @@ forward_serial_to_mqtt = DEFAULT_FORWARD_SERIAL_TO_MQTT
 forward_mqtt_to_serial = DEFAULT_FORWARD_MQTT_TO_SERIAL
 mqtt_publish_topic = DEFAULT_MQTT_PUBLISH_TOPIC
 
+# DTU MQTT协议配置(运行时可修改)
+dtu_config = {
+    'topic_prefix': DEFAULT_MQTT_TOPIC_PREFIX,
+    'customer_id': DEFAULT_CUSTOMER_ID,
+    'dtu_id': DEFAULT_DTU_ID,
+    'firmware_version': 'v1.0.0',
+    'hardware_version': 'v1.0',
+    'heartbeat_interval': DTU_HEARTBEAT_INTERVAL,
+    'enabled': True  # 是否启用DTU MQTT协议
+}
+
+# 端口状态追踪(用于事件检测)
+port_state = {}  # {panel_id: {port_id: {'last_uid': str, 'expected_uid': str, 'alarm_count': int}}}
+
+# 面板配置(从地址配置模块加载)
+panel_config = {}  # {panel_id: {'address': int, 'position': int, 'panel_uid': str}}
+
 # 数据存储缓冲区
 serial_data_buffer = []
 mqtt_data_buffer = []
@@ -219,16 +249,339 @@ def mqtt_status_handler(status):
     try:
         global mqtt_status
         mqtt_status = status
-        
+
         # 通过WebSocket广播状态变化
         socketio.emit('mqtt_status', {
             'connected': status
         }, namespace=SOCKETIO_NAMESPACE_STATUS)
-        
+
         logger.info(f"MQTT状态更新: {'已连接' if status else '已断开'}")
+
+        # 如果MQTT连接成功且启用了DTU协议,发送注册消息和订阅控制主题
+        if status and dtu_config.get('enabled'):
+            socketio.sleep(1)  # 等待连接稳定
+
+            # 发送DTU注册消息
+            dtu_register()
+
+            # 订阅控制主题
+            control_topic = build_dtu_topic(dtu_config['customer_id'], 'dtu', dtu_config['dtu_id'], 'control')
+            mqtt_client.subscribe(control_topic)
+            logger.info(f"已订阅控制主题: {control_topic}")
     except Exception as e:
         logger.error(f"处理MQTT状态时出错: {str(e)}")
 
+
+# ========== DTU MQTT协议处理函数 ==========
+
+def build_dtu_topic(*parts):
+    """构建DTU MQTT主题"""
+    prefix = dtu_config.get('topic_prefix', '线架系统')
+    return '/'.join([prefix] + list(parts))
+
+
+def dtu_register():
+    """发送DTU注册消息"""
+    if not mqtt_client.get_status():
+        logger.warning("MQTT未连接,无法发送注册消息")
+        return False
+
+    try:
+        # 加载面板配置
+        devices = address_config.get_stored_devices()
+        panels = []
+        panel_id = 1
+        for uid_hex, addr in devices.items():
+            panels.append({
+                'panel_id': f"PANEL_{dtu_config['dtu_id']}_{addr}",
+                'address': addr,
+                'position': panel_id
+            })
+            panel_id += 1
+
+        payload = {
+            'msg_id': f"reg_{int(time.time() * 1000)}",
+            'timestamp': int(time.time() * 1000),
+            'dtu_id': dtu_config['dtu_id'],
+            'type': 'REGISTER',
+            'payload': {
+                'firmware_version': dtu_config.get('firmware_version', 'v1.0.0'),
+                'hardware_version': dtu_config.get('hardware_version', 'v1.0'),
+                'panel_count': len(panels),
+                'network_type': 'ethernet',
+                'signal_strength': None,
+                'uptime': int(time.time() * 1000),  # 简化处理
+                'panels': panels
+            }
+        }
+
+        topic = build_dtu_topic(dtu_config['customer_id'], 'dtu', dtu_config['dtu_id'], 'register')
+        success, msg = mqtt_client.publish(topic, json.dumps(payload))
+
+        if success:
+            logger.info(f"DTU注册消息已发送: {topic}")
+        else:
+            logger.error(f"DTU注册消息发送失败: {msg}")
+
+        return success
+    except Exception as e:
+        logger.error(f"发送DTU注册消息失败: {str(e)}")
+        return False
+
+
+def dtu_publish_status(force=False):
+    """发送DTU状态/心跳消息"""
+    if not mqtt_client.get_status() or not dtu_config.get('enabled'):
+        return False
+
+    try:
+        # 统计面板在线状态
+        panel_online = 0
+        panel_offline = 0
+
+        # 检查串口状态
+        serial_st = serial_client.get_status()
+        rs485_status = 'normal' if (isinstance(serial_st, dict) and serial_st.get('connected', False)) else 'error'
+
+        payload = {
+            'msg_id': f"hbt_{int(time.time() * 1000)}",
+            'timestamp': int(time.time() * 1000),
+            'dtu_id': dtu_config['dtu_id'],
+            'type': 'STATUS',
+            'payload': {
+                'cpu_usage': None,  # 可后续添加
+                'memory_usage': None,
+                'temperature': None,
+                'network_status': 'online',
+                'panel_online': panel_online,
+                'panel_offline': panel_offline,
+                'screen_connected': False,
+                'mqtt_connected': mqtt_client.get_status(),
+                'rs485_status': rs485_status
+            }
+        }
+
+        topic = build_dtu_topic(dtu_config['customer_id'], 'dtu', dtu_config['dtu_id'], 'status')
+        success, _ = mqtt_client.publish(topic, json.dumps(payload))
+        return success
+    except Exception as e:
+        logger.error(f"发送DTU状态消息失败: {str(e)}")
+        return False
+
+
+def dtu_publish_event(panel_id, port_id, event_type, jumper_uid, previous_jumper_uid=None):
+    """发送端口事件消息"""
+    if not mqtt_client.get_status() or not dtu_config.get('enabled'):
+        return False
+
+    try:
+        payload = {
+            'msg_id': f"evt_{int(time.time() * 1000)}",
+            'timestamp': int(time.time() * 1000),
+            'dtu_id': dtu_config['dtu_id'],
+            'type': 'EVENT',
+            'payload': {
+                'panel_id': panel_id,
+                'port_id': port_id,
+                'event_type': event_type,
+                'event_id': f"evt_{uuid.uuid4().hex[:8]}",
+                'jumper_uid': jumper_uid,
+                'previous_jumper_uid': previous_jumper_uid
+            }
+        }
+
+        topic = build_dtu_topic(dtu_config['customer_id'], 'patchpanel', dtu_config['dtu_id'], panel_id, 'event')
+        success, _ = mqtt_client.publish(topic, json.dumps(payload))
+
+        if success:
+            logger.info(f"端口事件已发送: {panel_id}:{port_id} - {event_type}")
+
+        return success
+    except Exception as e:
+        logger.error(f"发送端口事件失败: {str(e)}")
+        return False
+
+
+def dtu_publish_alarm(panel_id, port_id, alarm_type, expected_jumper_uid, actual_jumper_uid):
+    """发送非法告警消息"""
+    if not mqtt_client.get_status() or not dtu_config.get('enabled'):
+        return False
+
+    try:
+        description = f"端口{port_id}期望跳线{expected_jumper_uid},实际{'未读到' if not actual_jumper_uid else actual_jumper_uid}"
+
+        payload = {
+            'msg_id': f"alm_{int(time.time() * 1000)}",
+            'timestamp': int(time.time() * 1000),
+            'dtu_id': dtu_config['dtu_id'],
+            'type': 'ALARM',
+            'payload': {
+                'panel_id': panel_id,
+                'port_id': port_id,
+                'alarm_type': alarm_type,
+                'severity': 'WARNING',
+                'expected_jumper_uid': expected_jumper_uid,
+                'actual_jumper_uid': actual_jumper_uid,
+                'description': description
+            }
+        }
+
+        topic = build_dtu_topic(dtu_config['customer_id'], 'patchpanel', dtu_config['dtu_id'], panel_id, 'alarm')
+        success, _ = mqtt_client.publish(topic, json.dumps(payload))
+
+        if success:
+            logger.warning(f"非法告警已发送: {panel_id}:{port_id} - {alarm_type}")
+
+        return success
+    except Exception as e:
+        logger.error(f"发送非法告警失败: {str(e)}")
+        return False
+
+
+def dtu_handle_control(topic, payload):
+    """处理下行控制指令"""
+    try:
+        if not dtu_config.get('enabled'):
+            return
+
+        command = payload.get('payload', {}).get('command')
+        target = payload.get('payload', {}).get('target')
+        params = payload.get('payload', {}).get('params', {})
+
+        logger.info(f"收到控制指令: command={command}, target={target}")
+
+        response_payload = {
+            'msg_id': payload.get('msg_id'),
+            'timestamp': int(time.time() * 1000),
+            'dtu_id': dtu_config['dtu_id'],
+            'type': 'RESPONSE',
+            'payload': {
+                'command': command,
+                'target': target,
+                'success': True,
+                'result': {}
+            }
+        }
+
+        # 处理各命令
+        if command == 'SET_PORT_LED':
+            # 设置端口LED
+            port_id = params.get('port_id')
+            led_mode = params.get('led_mode', 'OFF')
+
+            # LED模式映射
+            led_mode_map = {'OFF': 0, 'BLINK_RED': 1, 'BLINK_GREEN': 2, 'BLINK_BLUE': 3}
+            color = led_mode_map.get(led_mode, 0)
+
+            # 查找设备地址
+            device_address = 1  # 默认
+            for panel_id, cfg in panel_config.items():
+                if panel_id == target:
+                    device_address = cfg.get('address', 1)
+                    break
+
+            result = modbus_client.set_rgb_led(device_address, port_id, color)
+            response_payload['payload']['success'] = 'error' not in result
+            response_payload['payload']['result'] = result
+
+        elif command == 'QUERY_DTU_STATUS':
+            # 查询DTU状态
+            dtu_publish_status(force=True)
+
+        elif command == 'SYNC_PORT_MAPPING':
+            # 同步单端口期望映射
+            port_id = params.get('port_id')
+            jumper_uid = params.get('jumper_uid')
+
+            if target not in port_state:
+                port_state[target] = {}
+            if port_id not in port_state[target]:
+                port_state[target][port_id] = {'last_uid': None, 'expected_uid': None, 'alarm_count': 0}
+
+            port_state[target][port_id]['expected_uid'] = jumper_uid
+
+        elif command == 'SYNC_ALL_MAPPING':
+            # 批量同步期望映射
+            mappings = params.get('mappings', [])
+            for mapping in mappings:
+                panel_id = mapping.get('panel_id')
+                port_id = mapping.get('port_id')
+                jumper_uid = mapping.get('jumper_uid')
+
+                if panel_id not in port_state:
+                    port_state[panel_id] = {}
+                if port_id not in port_state[panel_id]:
+                    port_state[panel_id][port_id] = {'last_uid': None, 'expected_uid': None, 'alarm_count': 0}
+
+                port_state[panel_id][port_id]['expected_uid'] = jumper_uid
+
+        elif command == 'REBOOT':
+            # 重启DTU(模拟)
+            response_payload['payload']['result'] = {'message': 'Reboot command received'}
+
+        # 发送响应
+        response_topic = build_dtu_topic(dtu_config['customer_id'], 'dtu', dtu_config['dtu_id'], 'response')
+        mqtt_client.publish(response_topic, json.dumps(response_payload))
+
+    except Exception as e:
+        logger.error(f"处理控制指令失败: {str(e)}")
+
+
+# 启动DTU心跳定时器
+def start_dtu_heartbeat():
+    """启动DTU心跳定时任务"""
+    def heartbeat_task():
+        while True:
+            socketio.sleep(dtu_config.get('heartbeat_interval', DTU_HEARTBEAT_INTERVAL))
+            if mqtt_client.get_status() and dtu_config.get('enabled'):
+                dtu_publish_status()
+
+    socketio.start_background_task(target=heartbeat_task)
+
+
+# 修改mqtt_data_handler以处理控制指令
+def mqtt_data_handler_extended(data):
+    """处理MQTT接收的数据(扩展版,含DTU协议)"""
+    try:
+        topic = data.get('topic', '')
+        payload_str = data.get('payload', '')
+
+        # 尝试解析JSON
+        try:
+            payload = json.loads(payload_str) if isinstance(payload_str, str) else payload_str
+        except:
+            payload = payload_str
+
+        # 添加到缓冲区
+        timestamp = time.strftime('%Y-%m-%d %H:%M:%S')
+        mqtt_data_buffer.append({
+            'timestamp': timestamp,
+            'topic': topic,
+            'payload': payload_str
+        })
+        if len(mqtt_data_buffer) > MAX_BUFFER_SIZE:
+            mqtt_data_buffer.pop(0)
+
+        # 通过WebSocket广播
+        socketio.emit('mqtt_data', {
+            'timestamp': timestamp,
+            'topic': topic,
+            'payload': payload_str
+        }, namespace=SOCKETIO_NAMESPACE_DATA)
+
+        # 检查是否是控制指令主题
+        expected_control_topic = build_dtu_topic(dtu_config['customer_id'], 'dtu', dtu_config['dtu_id'], 'control')
+        if topic == expected_control_topic:
+            dtu_handle_control(topic, payload)
+
+        # 转发到串口(如果启用)
+        if forward_mqtt_to_serial and serial_client.get_status():
+            success, msg = serial_client.send_data(payload_str)
+            if not success:
+                logger.warning(f"MQTT数据转发到串口失败: {msg}")
+    except Exception as e:
+        logger.error(f"处理MQTT数据时出错: {str(e)}")
+
 # WebSocket事件处理
 @socketio.on('connect', namespace=SOCKETIO_NAMESPACE_DATA)
 def handle_data_connect():
@@ -490,7 +843,7 @@ def handle_clear_data_buffer(data):
 # 设置回调
 serial_client.set_data_callback(serial_data_handler)
 serial_client.set_status_callback(serial_status_handler)
-mqtt_client.set_data_callback(mqtt_data_handler)
+mqtt_client.set_data_callback(mqtt_data_handler_extended)
 mqtt_client.set_status_callback(mqtt_status_handler)
 
 # 设备配置文件路径
@@ -1404,6 +1757,126 @@ def set_all_leds():
 # 在 set_rgb_led 中更新 LED 状态追踪
 # 不再需要静态文件目录,前端由nginx提供服务
 
+
+# ========== DTU MQTT协议配置API ==========
+
+@app.route('/api/dtu/config', methods=['GET'])
+def get_dtu_config():
+    """获取DTU配置"""
+    return jsonify({
+        'success': True,
+        'data': dtu_config
+    })
+
+
+@app.route('/api/dtu/config', methods=['POST'])
+def update_dtu_config():
+    """更新DTU配置"""
+    try:
+        data = request.json
+        old_config = dtu_config.copy()
+
+        # 更新配置项
+        if 'topic_prefix' in data:
+            dtu_config['topic_prefix'] = data['topic_prefix']
+        if 'customer_id' in data:
+            dtu_config['customer_id'] = data['customer_id']
+        if 'dtu_id' in data:
+            dtu_config['dtu_id'] = data['dtu_id']
+        if 'firmware_version' in data:
+            dtu_config['firmware_version'] = data['firmware_version']
+        if 'hardware_version' in data:
+            dtu_config['hardware_version'] = data['hardware_version']
+        if 'heartbeat_interval' in data:
+            dtu_config['heartbeat_interval'] = data['heartbeat_interval']
+        if 'enabled' in data:
+            dtu_config['enabled'] = data['enabled']
+
+        # 如果MQTT已连接且主题配置发生变化,重新订阅
+        if mqtt_status and dtu_config.get('enabled'):
+            old_control_topic = build_dtu_topic(
+                old_config.get('customer_id', DEFAULT_CUSTOMER_ID),
+                'dtu',
+                old_config.get('dtu_id', DEFAULT_DTU_ID),
+                'control'
+            )
+            new_control_topic = build_dtu_topic(
+                dtu_config['customer_id'],
+                'dtu',
+                dtu_config['dtu_id'],
+                'control'
+            )
+
+            if old_control_topic != new_control_topic:
+                # 取消旧订阅,订阅新主题
+                mqtt_client.unsubscribe(old_control_topic)
+                mqtt_client.subscribe(new_control_topic)
+                logger.info(f"控制主题已更新: {old_control_topic} -> {new_control_topic}")
+
+                # 重新发送注册消息
+                dtu_register()
+
+        return jsonify({
+            'success': True,
+            'message': 'DTU配置已更新',
+            'data': dtu_config
+        })
+    except Exception as e:
+        logger.error(f"更新DTU配置失败: {str(e)}")
+        return jsonify({'success': False, 'message': str(e)}), 500
+
+
+@app.route('/api/dtu/register', methods=['POST'])
+def manual_dtu_register():
+    """手动触发DTU注册"""
+    try:
+        if not mqtt_status:
+            return jsonify({'success': False, 'message': 'MQTT未连接'}), 400
+
+        success = dtu_register()
+        if success:
+            return jsonify({'success': True, 'message': '注册消息已发送'})
+        else:
+            return jsonify({'success': False, 'message': '注册消息发送失败'}), 500
+    except Exception as e:
+        logger.error(f"手动触发DTU注册失败: {str(e)}")
+        return jsonify({'success': False, 'message': str(e)}), 500
+
+
+@app.route('/api/dtu/status', methods=['GET'])
+def get_dtu_status():
+    """获取DTU状态"""
+    try:
+        # 获取串口状态
+        serial_st = serial_client.get_status()
+
+        # 获取面板状态
+        devices = address_config.get_stored_devices()
+
+        status = {
+            'dtu_id': dtu_config.get('dtu_id'),
+            'mqtt_connected': mqtt_status,
+            'serial_connected': isinstance(serial_st, dict) and serial_st.get('connected', False),
+            'dtu_enabled': dtu_config.get('enabled', True),
+            'topic_prefix': dtu_config.get('topic_prefix'),
+            'customer_id': dtu_config.get('customer_id'),
+            'panel_count': len(devices),
+            'topics': {
+                'register': build_dtu_topic(dtu_config['customer_id'], 'dtu', dtu_config['dtu_id'], 'register'),
+                'status': build_dtu_topic(dtu_config['customer_id'], 'dtu', dtu_config['dtu_id'], 'status'),
+                'control': build_dtu_topic(dtu_config['customer_id'], 'dtu', dtu_config['dtu_id'], 'control'),
+                'response': build_dtu_topic(dtu_config['customer_id'], 'dtu', dtu_config['dtu_id'], 'response'),
+                'event': build_dtu_topic(dtu_config['customer_id'], 'dtu', dtu_config['dtu_id'], 'event'),
+                'alarm': build_dtu_topic(dtu_config['customer_id'], 'dtu', dtu_config['dtu_id'], 'alarm')
+            }
+        }
+
+        return jsonify({'success': True, 'data': status})
+    except Exception as e:
+        logger.error(f"获取DTU状态失败: {str(e)}")
+        return jsonify({'success': False, 'message': str(e)}), 500
+
+
 if __name__ == '__main__':
     try:
         # 启动前的初始化工作

+ 23 - 1
backend/config.py

@@ -94,4 +94,26 @@ ERROR_MESSAGES = {
     ERROR_CODES['CONFIG_ERROR']: '配置错误',
     ERROR_CODES['NETWORK_ERROR']: '网络错误',
     ERROR_CODES['UNKNOWN_ERROR']: '未知错误'
-}
+}
+
+# DTU配置
+DEFAULT_CUSTOMER_ID = os.getenv('CUSTOMER_ID', 'default_customer')
+DEFAULT_DTU_ID = os.getenv('DTU_ID', 'dtu_001')
+DTU_HEARTBEAT_INTERVAL = 30  # DTU状态上报间隔(秒)
+
+# MQTT主题前缀(可配置 - 通过web界面设置)
+DEFAULT_MQTT_TOPIC_PREFIX = os.getenv('MQTT_TOPIC_PREFIX', '线架系统')
+
+# DTU MQTT主题模板(运行时根据topic_prefix动态生成)
+# 格式: {topic_prefix}/{customer_id}/dtu/{dtu_id}/{topic}
+DTU_REGISTRATION_TOPIC = 'register'
+DTU_STATUS_TOPIC = 'status'
+DTU_CONTROL_TOPIC = 'control'
+DTU_RESPONSE_TOPIC = 'response'
+DTU_EVENT_TOPIC = 'event'
+DTU_ALARM_TOPIC = 'alarm'
+DTU_BROADCAST_TOPIC = 'broadcast'
+
+# 面板配置
+MAX_PANELS = 8  # 最大面板数
+MAX_PORTS_PER_PANEL = 24  # 每个面板最大端口数

+ 19 - 1
frontend/src/api/apiService.js

@@ -76,9 +76,27 @@ const apiService = {
   forward: {
     // 设置转发配置
     setConfig: (config) => apiClient.post('/forward/config', config),
-    
+
     // 获取转发状态
     getStatus: () => apiClient.get('/forward/status')
+  },
+
+  // DTU配置API
+  dtu: {
+    // 获取DTU配置
+    getConfig: () => apiClient.get('/dtu/config'),
+
+    // 更新DTU配置
+    updateConfig: (config) => apiClient.post('/dtu/config', config),
+
+    // 手动触发DTU注册
+    register: () => apiClient.post('/dtu/register'),
+
+    // 获取DTU状态
+    getStatus: () => apiClient.get('/dtu/status'),
+
+    // 发送控制命令
+    sendControl: (command) => apiClient.post('/dtu/control', command)
   }
 }
 

+ 304 - 0
frontend/src/components/DTUConfig.vue

@@ -0,0 +1,304 @@
+<template>
+  <div class="dtu-config">
+    <a-space direction="vertical" style="width: 100%">
+      <div style="display: flex; align-items: center; justify-content: flex-end; margin-bottom: 16px">
+        <StatusIndicator :connected="dataStore.dtuStatus.online" />
+        <span style="margin-left: 8px; font-size: 12px; color: #8c8c8c">
+          {{ dataStore.dtuStatus.online ? '在线' : '离线' }}
+        </span>
+      </div>
+
+      <a-alert
+        v-if="topicPreview"
+        type="info"
+        show-icon
+        style="margin-bottom: 16px"
+      >
+        <template #message>
+          <div>主题预览: <code>{{ topicPreview }}</code></div>
+        </template>
+      </a-alert>
+
+      <a-form layout="vertical">
+        <a-divider orientation="left">MQTT主题配置</a-divider>
+
+        <a-form-item
+          label="根主题前缀"
+          :validate-status="!formData.topic_prefix && showErrors ? 'error' : undefined"
+          help="用于在同一Broker上与其他业务系统做命名空间隔离"
+        >
+          <a-input
+            v-model:value="formData.topic_prefix"
+            placeholder="例如: 线架系统"
+            :status="!formData.topic_prefix && showErrors ? 'error' : undefined"
+          >
+            <template #prefix>
+              <span style="color: #8c8c8c">/</span>
+            </template>
+          </a-input>
+        </a-form-item>
+
+        <a-row :gutter="16">
+          <a-col :span="12">
+            <a-form-item
+              label="客户ID"
+              :validate-status="!formData.customer_id && showErrors ? 'error' : undefined"
+              help="用于主题隔离,不同客户使用不同ID"
+            >
+              <a-input
+                v-model:value="formData.customer_id"
+                placeholder="例如: customer001"
+                :status="!formData.customer_id && showErrors ? 'error' : undefined"
+              >
+                <template #prefix>
+                  <span style="color: #8c8c8c">/</span>
+                </template>
+              </a-input>
+            </a-form-item>
+          </a-col>
+          <a-col :span="12">
+            <a-form-item
+              label="DTU ID"
+              :validate-status="!formData.dtu_id && showErrors ? 'error' : undefined"
+              help="DTU设备的唯一标识"
+            >
+              <a-input
+                v-model:value="formData.dtu_id"
+                placeholder="例如: dtu001"
+                :status="!formData.dtu_id && showErrors ? 'error' : undefined"
+              >
+                <template #prefix>
+                  <span style="color: #8c8c8c">/</span>
+                </template>
+              </a-input>
+            </a-form-item>
+          </a-col>
+        </a-row>
+
+        <a-divider orientation="left">设备信息</a-divider>
+
+        <a-row :gutter="16">
+          <a-col :span="12">
+            <a-form-item label="固件版本">
+              <a-input
+                v-model:value="formData.firmware_version"
+                placeholder="例如: 1.0.0"
+              />
+            </a-form-item>
+          </a-col>
+          <a-col :span="12">
+            <a-form-item label="心跳间隔(秒)">
+              <a-input-number
+                v-model:value="formData.heartbeat_interval"
+                :min="10"
+                :max="3600"
+                style="width: 100%"
+              />
+            </a-form-item>
+          </a-col>
+        </a-row>
+
+        <a-form-item label="DTU功能">
+          <a-switch
+            v-model:checked="formData.enabled"
+            checked-children="启用"
+            un-checked-children="禁用"
+          />
+          <span style="margin-left: 12px; color: #8c8c8c; font-size: 12px">
+            {{ formData.enabled ? 'DTU功能已启用' : 'DTU功能已禁用' }}
+          </span>
+        </a-form-item>
+
+        <a-divider orientation="left">状态信息</a-divider>
+
+        <a-descriptions :column="2" size="small" bordered>
+          <a-descriptions-item label="注册状态">
+            <a-tag :color="dataStore.dtuStatus.registered ? 'green' : 'default'">
+              {{ dataStore.dtuStatus.registered ? '已注册' : '未注册' }}
+            </a-tag>
+          </a-descriptions-item>
+          <a-descriptions-item label="在线状态">
+            <a-tag :color="dataStore.dtuStatus.online ? 'green' : 'red'">
+              {{ dataStore.dtuStatus.online ? '在线' : '离线' }}
+            </a-tag>
+          </a-descriptions-item>
+          <a-descriptions-item label="最后心跳">
+            {{ dataStore.dtuStatus.last_heartbeat || '无' }}
+          </a-descriptions-item>
+          <a-descriptions-item label="主题前缀">
+            {{ dataStore.dtuConfig.topic_prefix || '未配置' }}
+          </a-descriptions-item>
+        </a-descriptions>
+
+        <div style="display: flex; justify-content: flex-end; margin-top: 16px">
+          <a-space wrap>
+            <a-button
+              type="primary"
+              @click="saveConfig"
+              :loading="isSaving"
+            >
+              保存配置
+            </a-button>
+            <a-button
+              @click="registerDTU"
+              :loading="isRegistering"
+              :disabled="!canRegister"
+            >
+              手动注册
+            </a-button>
+            <a-button
+              @click="refreshStatus"
+              :loading="isRefreshing"
+            >
+              刷新状态
+            </a-button>
+          </a-space>
+        </div>
+      </a-form>
+    </a-space>
+  </div>
+</template>
+
+<script setup>
+import { onMounted, ref, computed, watch } from 'vue'
+import { useDataStore } from '../stores/dataStore'
+import apiService from '../api/apiService'
+import StatusIndicator from './StatusIndicator.vue'
+import { message } from 'ant-design-vue'
+
+const dataStore = useDataStore()
+
+const isSaving = ref(false)
+const isRegistering = ref(false)
+const isRefreshing = ref(false)
+const showErrors = ref(false)
+
+const formData = ref({
+  topic_prefix: '线架系统',
+  customer_id: '',
+  dtu_id: '',
+  firmware_version: '1.0.0',
+  heartbeat_interval: 30,
+  enabled: true
+})
+
+const topicPreview = computed(() => {
+  if (formData.value.topic_prefix && formData.value.customer_id && formData.value.dtu_id) {
+    return `${formData.value.topic_prefix}/${formData.value.customer_id}/dtu/${formData.value.dtu_id}/register`
+  }
+  return ''
+})
+
+const canRegister = computed(() => {
+  return formData.value.topic_prefix && formData.value.customer_id && formData.value.dtu_id
+})
+
+watch(() => formData.value.topic_prefix, () => {
+  if (formData.value.topic_prefix) {
+    showErrors.value = false
+  }
+})
+
+const saveConfig = async () => {
+  if (!formData.value.topic_prefix) {
+    showErrors.value = true
+    message.error('请输入根主题前缀')
+    return
+  }
+
+  try {
+    isSaving.value = true
+    const response = await apiService.dtu.updateConfig(formData.value)
+
+    if (response.data.success) {
+      dataStore.setDTUConfig(formData.value)
+      message.success('配置保存成功')
+    } else {
+      message.error(response.data.message || '保存失败')
+    }
+  } catch (error) {
+    console.error('保存DTU配置失败:', error)
+    message.error('保存配置失败: ' + (error.response?.data?.message || error.message))
+  } finally {
+    isSaving.value = false
+  }
+}
+
+const registerDTU = async () => {
+  if (!canRegister.value) {
+    showErrors.value = true
+    message.error('请完整填写主题配置')
+    return
+  }
+
+  try {
+    isRegistering.value = true
+    const response = await apiService.dtu.register()
+
+    if (response.data.success) {
+      message.success('注册请求已发送')
+      await refreshStatus()
+    } else {
+      message.error(response.data.message || '注册失败')
+    }
+  } catch (error) {
+    console.error('DTU注册失败:', error)
+    message.error('注册失败: ' + (error.response?.data?.message || error.message))
+  } finally {
+    isRegistering.value = false
+  }
+}
+
+const refreshStatus = async () => {
+  try {
+    isRefreshing.value = true
+
+    const [configResponse, statusResponse] = await Promise.all([
+      apiService.dtu.getConfig(),
+      apiService.dtu.getStatus()
+    ])
+
+    if (configResponse.data.success) {
+      dataStore.setDTUConfig(configResponse.data.data)
+      formData.value = { ...configResponse.data.data }
+    }
+
+    if (statusResponse.data.success) {
+      dataStore.setDTUStatus(statusResponse.data.data)
+    }
+  } catch (error) {
+    console.error('获取DTU状态失败:', error)
+  } finally {
+    isRefreshing.value = false
+  }
+}
+
+onMounted(() => {
+  refreshStatus()
+})
+</script>
+
+<style scoped>
+.dtu-config {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+}
+
+code {
+  background-color: #f5f5f5;
+  padding: 2px 6px;
+  border-radius: 4px;
+  font-family: monospace;
+  font-size: 12px;
+}
+
+:deep(.ant-descriptions-bordered .ant-descriptions-item-label) {
+  background-color: #fafafa;
+}
+
+:deep(.ant-divider-inner-text) {
+  font-weight: 500;
+  color: #1890ff;
+}
+</style>

+ 31 - 0
frontend/src/stores/dataStore.js

@@ -30,6 +30,22 @@ export const useDataStore = defineStore('data', {
     forwardConfig: {
       serialToMQTT: false,
       mqttToSerial: false
+    },
+
+    // DTU相关状态
+    dtuConnected: false,
+    dtuConfig: {
+      topic_prefix: '线架系统',
+      customer_id: '',
+      dtu_id: '',
+      firmware_version: '1.0.0',
+      heartbeat_interval: 30,
+      enabled: true
+    },
+    dtuStatus: {
+      registered: false,
+      last_heartbeat: null,
+      online: false
     }
   }),
   
@@ -71,6 +87,21 @@ export const useDataStore = defineStore('data', {
     clearData() {
       this.serialData = []
       this.mqttData = []
+    },
+
+    // 设置DTU连接状态
+    setDTUConnected(status) {
+      this.dtuConnected = status
+    },
+
+    // 设置DTU配置
+    setDTUConfig(config) {
+      this.dtuConfig = { ...this.dtuConfig, ...config }
+    },
+
+    // 设置DTU状态
+    setDTUStatus(status) {
+      this.dtuStatus = { ...this.dtuStatus, ...status }
     }
   }
 })

+ 4 - 0
frontend/src/views/SystemConfig.vue

@@ -12,6 +12,9 @@
             <a-tab-pane tab="MQTT配置" key="2">
               <MQTTConfig />
             </a-tab-pane>
+            <a-tab-pane tab="DTU配置" key="3">
+              <DTUConfig />
+            </a-tab-pane>
           </a-tabs>
         </a-card>
         
@@ -37,6 +40,7 @@
 <script setup>
 import SerialConfig from '../components/SerialConfig.vue'
 import MQTTConfig from '../components/MQTTConfig.vue'
+import DTUConfig from '../components/DTUConfig.vue'
 import ForwardConfig from '../components/ForwardConfig.vue'
 import SerialDataDisplay from '../components/SerialDataDisplay.vue'
 import MQTTDataDisplay from '../components/MQTTDataDisplay.vue'