Ver Fonte

跟新展厅控制

liuq há 3 meses atrás
pai
commit
92c110a4f7

+ 153 - 0
src/display/gui_display.py

@@ -18,6 +18,7 @@ from PyQt5.QtWidgets import QApplication, QVBoxLayout, QWidget
 
 from src.display.base_display import BaseDisplay
 from src.display.gui_display_model import GuiDisplayModel
+from src.utils.exhibition_control import ExhibitionControl
 from src.utils.resource_finder import find_assets_dir
 from src.views.components.numeric_keypad import NumericKeypad
 
@@ -50,6 +51,9 @@ class GuiDisplay(BaseDisplay, QObject, metaclass=CombinedMeta):
         # 数据模型
         self.display_model = GuiDisplayModel()
 
+        # 展厅控制器
+        self.exhibition_control = ExhibitionControl()
+
         # 表情管理
         self._emotion_cache = {}
         self._last_emotion_name = None
@@ -416,6 +420,16 @@ class GuiDisplay(BaseDisplay, QObject, metaclass=CombinedMeta):
             "modeButtonClicked": self._on_mode_button_click,
             "sendButtonClicked": self._on_send_button_click,
             "settingsButtonClicked": self._on_settings_button_click,
+            "exhibitionButtonClicked": self._on_exhibition_button_click,
+            "setExhibitionVolume": self._on_set_exhibition_volume,
+            "toggleExhibitionLight": self._on_toggle_exhibition_light,
+            "controlAllTvs": self._on_control_all_tvs,
+            "setWelcomeMode": self._on_set_welcome_mode,
+            "setDoorMode": self._on_set_door_mode,
+            "setCeilingLightLevel": self._on_set_ceiling_light_level,
+            "restartAllDevices": self._on_restart_all_devices,
+            "videoButtonClicked": self._on_video_button_click,
+            "playVideo": self._on_play_video,
         }
 
         # 标题栏控制信号映射
@@ -512,6 +526,145 @@ class GuiDisplay(BaseDisplay, QObject, metaclass=CombinedMeta):
         except Exception as e:
             self.logger.error(f"打开设置窗口失败: {e}", exc_info=True)
 
+    def _on_exhibition_button_click(self):
+        """
+        展厅控制按钮点击.
+        """
+        # 异步获取状态,避免阻塞 UI
+        async def fetch_status():
+            try:
+                # 运行在线程池中以免阻塞 asyncio 循环 (ExhibitionControl uses synchronous requests)
+                loop = asyncio.get_running_loop()
+                volume = await loop.run_in_executor(None, self.exhibition_control.get_volume)
+                self.display_model.exhibitionVolume = volume
+            except Exception as e:
+                self.logger.error(f"获取展厅状态失败: {e}")
+
+        asyncio.create_task(fetch_status())
+
+    def _on_set_exhibition_volume(self, volume: int):
+        """
+        设置展厅音量.
+        """
+        # 乐观更新 UI
+        self.display_model.exhibitionVolume = volume
+        
+        async def set_volume():
+            try:
+                loop = asyncio.get_running_loop()
+                await loop.run_in_executor(None, lambda: self.exhibition_control.set_volume(volume))
+            except Exception as e:
+                self.logger.error(f"设置音量失败: {e}")
+
+        asyncio.create_task(set_volume())
+
+    def _on_toggle_exhibition_light(self, device_key: str, on: bool):
+        """
+        控制展厅设备.
+        """
+        action = "turn_on" if on else "turn_off"
+        async def toggle_device():
+            try:
+                loop = asyncio.get_running_loop()
+                await loop.run_in_executor(None, lambda: self.exhibition_control.control_device(device_key, action))
+            except Exception as e:
+                self.logger.error(f"控制设备失败: {e}")
+
+        asyncio.create_task(toggle_device())
+
+    def _on_control_all_tvs(self, action: str):
+        """
+        控制所有电视.
+        """
+        async def control_tvs():
+            try:
+                loop = asyncio.get_running_loop()
+                await loop.run_in_executor(None, lambda: self.exhibition_control.control_all_tvs(action))
+            except Exception as e:
+                self.logger.error(f"控制电视失败: {e}")
+
+        asyncio.create_task(control_tvs())
+
+    def _on_set_welcome_mode(self, on: bool):
+        """
+        设置迎宾模式.
+        """
+        async def set_mode():
+            try:
+                loop = asyncio.get_running_loop()
+                await loop.run_in_executor(None, lambda: self.exhibition_control.control_welcome_mode(on))
+            except Exception as e:
+                self.logger.error(f"设置迎宾模式失败: {e}")
+
+        asyncio.create_task(set_mode())
+
+    def _on_set_door_mode(self, mode: int):
+        """
+        设置大门模式.
+        """
+        async def set_mode():
+            try:
+                loop = asyncio.get_running_loop()
+                await loop.run_in_executor(None, lambda: self.exhibition_control.set_door_mode(mode))
+            except Exception as e:
+                self.logger.error(f"设置大门模式失败: {e}")
+
+        asyncio.create_task(set_mode())
+
+    def _on_set_ceiling_light_level(self, level: int):
+        """
+        设置展厅顶灯等级.
+        """
+        async def set_level():
+            try:
+                loop = asyncio.get_running_loop()
+                await loop.run_in_executor(None, lambda: self.exhibition_control.set_ceiling_lights_level(level))
+            except Exception as e:
+                self.logger.error(f"设置顶灯等级失败: {e}")
+
+        asyncio.create_task(set_level())
+
+    def _on_restart_all_devices(self):
+        """
+        重启所有设备.
+        """
+        async def restart():
+            try:
+                loop = asyncio.get_running_loop()
+                await loop.run_in_executor(None, self.exhibition_control.restart_all_devices)
+            except Exception as e:
+                self.logger.error(f"重启设备失败: {e}")
+
+        asyncio.create_task(restart())
+
+    def _on_video_button_click(self):
+        """
+        视频播放按钮点击.
+        """
+        async def fetch_videos():
+            try:
+                loop = asyncio.get_running_loop()
+                videos = await loop.run_in_executor(None, self.exhibition_control.get_videos)
+                # 确保在主线程更新 UI
+                self.display_model.videoList = videos
+            except Exception as e:
+                self.logger.error(f"获取视频列表失败: {e}")
+
+        asyncio.create_task(fetch_videos())
+
+    def _on_play_video(self, video_id: int):
+        """
+        播放视频.
+        """
+        async def play():
+            try:
+                loop = asyncio.get_running_loop()
+                await loop.run_in_executor(None, lambda: self.exhibition_control.play_video(video_id))
+            except Exception as e:
+                self.logger.error(f"播放视频失败: {e}")
+
+        asyncio.create_task(play())
+
     def _dispatch_callback(self, callback_name: str, *args):
         """
         通用回调调度器.

+ 827 - 84
src/display/gui_display.qml

@@ -15,6 +15,17 @@ Rectangle {
     signal modeButtonClicked()
     signal sendButtonClicked(string text)
     signal settingsButtonClicked()
+    signal exhibitionButtonClicked()
+    signal setExhibitionVolume(int volume)
+    signal toggleExhibitionLight(string deviceKey, bool on)
+    signal controlAllTvs(string action)
+    signal setWelcomeMode(bool on)
+    signal setDoorMode(int mode)
+    signal setCeilingLightLevel(int level)
+    signal restartAllDevices()
+    signal videoButtonClicked()
+    signal playVideo(int videoId)
+
     // 标题栏相关信号
     signal titleMinimize()
     signal titleClose()
@@ -30,8 +41,8 @@ Rectangle {
         parent: root
         x: (root.width - width) / 2
         y: (root.height - height) / 2
-        modal: true
-        focus: true
+        modal: false  // 改为非模态,允许同时输入
+        focus: false  // 不自动获取焦点,防止打断输入
         closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
 
         background: Rectangle {
@@ -112,6 +123,649 @@ Rectangle {
         }
     }
 
+    // 重启确认弹窗
+    Popup {
+        id: restartConfirmPopup
+        width: 400
+        height: 240
+        parent: root
+        x: (root.width - width) / 2
+        y: (root.height - height) / 2
+        modal: true
+        focus: true
+        closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
+
+        background: Rectangle {
+            color: "white"
+            radius: 12
+            border.color: "#ffccc7"
+            border.width: 1
+            layer.enabled: true
+            layer.effect: DropShadow {
+                transparentBorder: true
+                horizontalOffset: 0
+                verticalOffset: 4
+                radius: 16
+                samples: 25
+                color: "#40000000"
+            }
+        }
+
+        ColumnLayout {
+            anchors.fill: parent
+            anchors.margins: 24
+            spacing: 20
+
+            Text {
+                text: "⚠️ 警告"
+                font.family: "PingFang SC, Microsoft YaHei UI"
+                font.weight: Font.Bold
+                font.pixelSize: 22
+                color: "#cf1322"
+                Layout.alignment: Qt.AlignHCenter
+            }
+
+            Text {
+                text: "检测到设备可能出现异常,建议重启展厅所有设备进行恢复,是否现在执行重启?\n(全程约需 1 分钟,请耐心等待)"
+                font.family: "PingFang SC, Microsoft YaHei UI"
+                font.pixelSize: 15
+                color: "#333"
+                wrapMode: Text.WordWrap
+                Layout.fillWidth: true
+                horizontalAlignment: Text.AlignHCenter
+            }
+
+            RowLayout {
+                Layout.alignment: Qt.AlignHCenter
+                spacing: 20
+
+                Button {
+                    text: "取消"
+                    Layout.preferredWidth: 100
+                    Layout.preferredHeight: 40
+                    background: Rectangle { color: parent.pressed ? "#e5e6eb" : "#f2f3f5"; radius: 6 }
+                    contentItem: Text { text: parent.text; font.family: "PingFang SC"; font.pixelSize: 14; color: "#4e5969"; horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter }
+                    onClicked: restartConfirmPopup.close()
+                }
+
+                Button {
+                    text: "确认重启"
+                    Layout.preferredWidth: 100
+                    Layout.preferredHeight: 40
+                    background: Rectangle { color: parent.pressed ? "#ffccc7" : "#cf1322"; radius: 6 }
+                    contentItem: Text { text: parent.text; font.family: "PingFang SC"; font.weight: Font.Bold; font.pixelSize: 14; color: "white"; horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter }
+                    onClicked: {
+                        root.restartAllDevices()
+                        restartConfirmPopup.close()
+                    }
+                }
+            }
+        }
+    }
+
+    // 视频列表弹窗
+    Popup {
+        id: videoPopup
+        width: root.width * 0.9
+        height: root.height * 0.8  // 高度也适应主界面
+        parent: root
+        x: (root.width - width) / 2
+        y: (root.height - height) / 2
+        modal: true
+        focus: true
+        closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
+
+        onOpened: root.videoButtonClicked()
+
+        background: Rectangle {
+            color: "white"
+            radius: 16
+            border.color: "#e5e6eb"
+            border.width: 1
+            layer.enabled: true
+            layer.effect: DropShadow {
+                transparentBorder: true
+                horizontalOffset: 0
+                verticalOffset: 4
+                radius: 20
+                samples: 25
+                color: "#40000000"
+            }
+        }
+
+        ColumnLayout {
+            anchors.fill: parent
+            anchors.margins: 24
+            spacing: 20
+
+            Text {
+                text: "视频播放列表"
+                font.family: "PingFang SC, Microsoft YaHei UI"
+                font.weight: Font.Bold
+                font.pixelSize: 28
+                color: "#faad14"
+                Layout.alignment: Qt.AlignHCenter
+            }
+
+            // 动态计算卡片尺寸(单行铺满,强制不滑动)
+            property int totalItems: displayModel && displayModel.videoList && displayModel.videoList.length > 0 ? displayModel.videoList.length : 1
+            property real cardSpacing: 16
+            property real availableWidth: videoPopup.contentItem.width - 48 // 减去弹窗左右 margin
+            
+            // 宽度:平分剩余空间
+            property real cardWidth: (availableWidth - (totalItems - 1) * cardSpacing) / totalItems
+            
+            // 高度:设置为竖向长方形(类似扑克牌),或者根据宽度自适应,但受限于弹窗高度
+            // 预留顶部标题和底部关闭按钮的高度 (约 120px)
+            property real maxCardHeight: videoPopup.contentItem.height - 120
+            property real cardHeight: Math.min(cardWidth * 1.5, maxCardHeight) 
+
+            ListView {
+                id: videoListView
+                Layout.fillWidth: true
+                Layout.fillHeight: true
+                orientation: ListView.Horizontal
+                spacing: parent.cardSpacing
+                clip: true
+                interactive: false // 禁止滑动
+                
+                model: displayModel ? displayModel.videoList : []
+                
+                // 垂直居中
+                Layout.alignment: Qt.AlignVCenter
+
+                delegate: Rectangle {
+                    width: videoListView.parent.cardWidth
+                    height: videoListView.parent.cardHeight
+                    // 垂直居中于 ListView
+                    y: (videoListView.height - height) / 2
+                    
+                    radius: 12
+                    color: {
+                        var colors = ["#fff1b8", "#e6f7ff", "#d9f7be", "#efdbff", "#ffccc7", "#fff0f6"];
+                        return colors[index % colors.length];
+                    }
+                    border.width: 1
+                    border.color: "#f0f0f0"
+
+                    // 视频标题
+                    Text {
+                        anchors.centerIn: parent
+                        width: parent.width - 16
+                        height: parent.height - 16
+                        text: modelData.name || "未知视频"
+                        font.family: "PingFang SC, Microsoft YaHei UI"
+                        font.weight: Font.Bold
+                        // 字体大小自适应:根据卡片尺寸动态计算,确保尽量大
+                        font.pixelSize: Math.max(14, Math.min(parent.width, parent.height / 2) * 0.25)
+                        color: "#1d2129"
+                        wrapMode: Text.Wrap
+                        horizontalAlignment: Text.AlignHCenter
+                        verticalAlignment: Text.AlignVCenter
+                        maximumLineCount: 5
+                        elide: Text.ElideRight
+                    }
+
+                    MouseArea {
+                        anchors.fill: parent
+                        hoverEnabled: true
+                        cursorShape: Qt.PointingHandCursor
+                        onClicked: {
+                            root.playVideo(modelData.id)
+                            videoPopup.close()
+                        }
+                    }
+                    
+                    // 悬停动画
+                    scale: MouseArea.containsMouse ? 1.05 : 1.0
+                    Behavior on scale { NumberAnimation { duration: 150 } }
+                }
+            }
+            
+            
+            Button {
+                text: "关闭"
+                Layout.alignment: Qt.AlignHCenter
+                Layout.preferredWidth: 120
+                Layout.preferredHeight: 48
+                background: Rectangle { color: parent.pressed ? "#e5e6eb" : "#f2f3f5"; radius: 8 }
+                contentItem: Text { text: parent.text; font.pixelSize: 16; color: "#1d2129"; horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter }
+                onClicked: videoPopup.close()
+            }
+        }
+    }
+
+    // 展厅控制弹窗
+    Popup {
+        id: exhibitionControlPopup
+        width: root.width * 0.8
+        height: root.height * 0.8
+        parent: root
+        x: (root.width - width) / 2
+        y: (root.height - height) / 2
+        modal: true
+        focus: true
+        closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
+
+        onOpened: root.exhibitionButtonClicked()
+
+        background: Rectangle {
+            color: "white"
+            radius: 16
+            border.color: "#e5e6eb"
+            border.width: 1
+            layer.enabled: true
+            layer.effect: DropShadow {
+                transparentBorder: true
+                horizontalOffset: 0
+                verticalOffset: 4
+                radius: 20
+                samples: 25
+                color: "#40000000"
+            }
+        }
+
+        ColumnLayout {
+            anchors.fill: parent
+            anchors.margins: 24
+            spacing: 20
+
+            Text {
+                text: "展厅控制中心"
+                font.family: "PingFang SC, Microsoft YaHei UI"
+                font.weight: Font.Bold
+                font.pixelSize: 24
+                color: "#1d2129"
+                Layout.alignment: Qt.AlignHCenter
+            }
+            
+            // 分割线
+            Rectangle { Layout.fillWidth: true; height: 1; color: "#f2f3f5" }
+
+            // 第一栏:全局音量
+            RowLayout {
+                Layout.fillWidth: true
+                spacing: 20
+                
+                Text {
+                    text: "全局音量"
+                    font.family: "PingFang SC, Microsoft YaHei UI"
+                    font.weight: Font.Bold
+                    font.pixelSize: 20
+                    color: "#333"
+                    Layout.preferredWidth: 80
+                }
+
+                Slider {
+                    id: volumeSlider
+                    Layout.fillWidth: true
+                    from: 0
+                    to: 100
+                    stepSize: 1
+                    value: displayModel ? displayModel.exhibitionVolume : 50
+                    onMoved: root.setExhibitionVolume(value)
+                    
+                    background: Rectangle {
+                        x: volumeSlider.leftPadding
+                        y: volumeSlider.topPadding + volumeSlider.availableHeight / 2 - height / 2
+                        implicitWidth: 200
+                        implicitHeight: 12
+                        width: volumeSlider.availableWidth
+                        height: implicitHeight
+                        radius: 6
+                        color: "#e5e6eb"
+
+                        Rectangle {
+                            width: volumeSlider.visualPosition * parent.width
+                            height: parent.height
+                            color: "#165dff"
+                            radius: 6
+                        }
+                    }
+                    handle: Rectangle {
+                        x: volumeSlider.leftPadding + volumeSlider.visualPosition * (volumeSlider.availableWidth - width)
+                        y: volumeSlider.topPadding + volumeSlider.availableHeight / 2 - height / 2
+                        implicitWidth: 32
+                        implicitHeight: 32
+                        radius: 16
+                        color: volumeSlider.pressed ? "#f0f0f0" : "#ffffff"
+                        border.color: "#165dff"
+                        border.width: 2
+                        layer.enabled: true
+                        layer.effect: DropShadow {
+                            transparentBorder: true
+                            horizontalOffset: 0
+                            verticalOffset: 2
+                            radius: 4
+                            color: "#20000000"
+                        }
+                    }
+                }
+
+                Text {
+                    text: volumeSlider.value.toFixed(0) + "%"
+                    font.family: "PingFang SC, Microsoft YaHei UI"
+                    font.weight: Font.Bold
+                    font.pixelSize: 20
+                    color: "#165dff"
+                    Layout.preferredWidth: 50
+                    horizontalAlignment: Text.AlignRight
+                }
+            }
+
+            // 第二栏:电视控制
+            RowLayout {
+                Layout.fillWidth: true
+                spacing: 20
+                
+                Text {
+                    text: "电视控制"
+                    font.family: "PingFang SC, Microsoft YaHei UI"
+                    font.weight: Font.Bold
+                    font.pixelSize: 20
+                    color: "#333"
+                    Layout.preferredWidth: 80
+                }
+
+                Flow {
+                    Layout.fillWidth: true
+                    spacing: 15
+                    
+                    Button {
+                        text: "开所有电视"
+                        font.family: "PingFang SC, Microsoft YaHei UI"
+                        font.pixelSize: 14
+                        font.weight: Font.Bold
+                        padding: 12
+                        background: Rectangle { color: parent.pressed ? "#b7eb8f" : "#f6ffed"; radius: 8; border.color: "#b7eb8f" }
+                        contentItem: Text { text: parent.text; font: parent.font; color: "#389e0d"; horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter }
+                        onClicked: root.controlAllTvs("turn_on_all")
+                    }
+                    Button {
+                        text: "关所有电视"
+                        font.family: "PingFang SC, Microsoft YaHei UI"
+                        font.pixelSize: 14
+                        font.weight: Font.Bold
+                        padding: 12
+                        background: Rectangle { color: parent.pressed ? "#ffccc7" : "#fff1f0"; radius: 8; border.color: "#ffccc7" }
+                        contentItem: Text { text: parent.text; font: parent.font; color: "#cf1322"; horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter }
+                        onClicked: root.controlAllTvs("turn_off_all")
+                    }
+                }
+            }
+
+            // 第三栏:迎宾模式
+            RowLayout {
+                Layout.fillWidth: true
+                spacing: 20
+                
+                Text {
+                    text: "迎宾模式"
+                    font.family: "PingFang SC, Microsoft YaHei UI"
+                    font.weight: Font.Bold
+                    font.pixelSize: 20
+                    color: "#333"
+                    Layout.preferredWidth: 80
+                }
+
+                Flow {
+                    Layout.fillWidth: true
+                    spacing: 15
+                    
+                    Button {
+                        text: "启动迎宾"
+                        font.family: "PingFang SC, Microsoft YaHei UI"
+                        font.pixelSize: 14
+                        font.weight: Font.Bold
+                        padding: 12
+                        background: Rectangle { color: parent.pressed ? "#d3adf7" : "#f9f0ff"; radius: 8; border.color: "#d3adf7" }
+                        contentItem: Text { text: parent.text; font: parent.font; color: "#722ed1"; horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter }
+                        onClicked: root.setWelcomeMode(true)
+                    }
+                    Button {
+                        text: "关闭迎宾"
+                        font.family: "PingFang SC, Microsoft YaHei UI"
+                        font.pixelSize: 14
+                        font.weight: Font.Bold
+                        padding: 12
+                        background: Rectangle { color: parent.pressed ? "#ffccc7" : "#fff1f0"; radius: 8; border.color: "#ffccc7" }
+                        contentItem: Text { text: parent.text; font: parent.font; color: "#cf1322"; horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter }
+                        onClicked: root.setWelcomeMode(false)
+                    }
+                }
+            }
+
+            // 第四栏:大门控制
+            RowLayout {
+                Layout.fillWidth: true
+                spacing: 20
+                
+                Text {
+                    text: "大门控制"
+                    font.family: "PingFang SC, Microsoft YaHei UI"
+                    font.weight: Font.Bold
+                    font.pixelSize: 20
+                    color: "#333"
+                    Layout.preferredWidth: 80
+                }
+
+                Flow {
+                    Layout.fillWidth: true
+                    spacing: 15
+                    
+                    Button {
+                        text: "正常模式"
+                        font.family: "PingFang SC, Microsoft YaHei UI"
+                        font.pixelSize: 14
+                        font.weight: Font.Bold
+                        padding: 12
+                        background: Rectangle { color: parent.pressed ? "#bae7ff" : "#e6f7ff"; radius: 8; border.color: "#bae7ff" }
+                        contentItem: Text { text: parent.text; font: parent.font; color: "#096dd9"; horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter }
+                        onClicked: root.setDoorMode(0)
+                    }
+                    Button {
+                        text: "常开模式"
+                        font.family: "PingFang SC, Microsoft YaHei UI"
+                        font.pixelSize: 14
+                        font.weight: Font.Bold
+                        padding: 12
+                        background: Rectangle { color: parent.pressed ? "#b7eb8f" : "#f6ffed"; radius: 8; border.color: "#b7eb8f" }
+                        contentItem: Text { text: parent.text; font: parent.font; color: "#389e0d"; horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter }
+                        onClicked: root.setDoorMode(1)
+                    }
+                    Button {
+                        text: "常闭模式"
+                        font.family: "PingFang SC, Microsoft YaHei UI"
+                        font.pixelSize: 14
+                        font.weight: Font.Bold
+                        padding: 12
+                        background: Rectangle { color: parent.pressed ? "#ffccc7" : "#fff1f0"; radius: 8; border.color: "#ffccc7" }
+                        contentItem: Text { text: parent.text; font: parent.font; color: "#cf1322"; horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter }
+                        onClicked: root.setDoorMode(2)
+                    }
+                }
+            }
+
+            // 第五栏:展厅顶灯等级控制
+            RowLayout {
+                Layout.fillWidth: true
+                spacing: 20
+                
+                Text {
+                    text: "顶灯等级"
+                    font.family: "PingFang SC, Microsoft YaHei UI"
+                    font.weight: Font.Bold
+                    font.pixelSize: 20
+                    color: "#333"
+                    Layout.preferredWidth: 80
+                }
+
+                Flow {
+                    Layout.fillWidth: true
+                    spacing: 15
+                    
+                    Button {
+                        text: "全关"
+                        font.family: "PingFang SC, Microsoft YaHei UI"
+                        font.pixelSize: 14
+                        font.weight: Font.Bold
+                        padding: 12
+                        background: Rectangle { color: parent.pressed ? "#ffccc7" : "#fff1f0"; radius: 8; border.color: "#ffccc7" }
+                        contentItem: Text { text: parent.text; font: parent.font; color: "#cf1322"; horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter }
+                        onClicked: root.setCeilingLightLevel(0)
+                    }
+                    Button {
+                        text: "单灯"
+                        font.family: "PingFang SC, Microsoft YaHei UI"
+                        font.pixelSize: 14
+                        font.weight: Font.Bold
+                        padding: 12
+                        background: Rectangle { color: parent.pressed ? "#bae7ff" : "#e6f7ff"; radius: 8; border.color: "#bae7ff" }
+                        contentItem: Text { text: parent.text; font: parent.font; color: "#096dd9"; horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter }
+                        onClicked: root.setCeilingLightLevel(1)
+                    }
+                    Button {
+                        text: "双灯"
+                        font.family: "PingFang SC, Microsoft YaHei UI"
+                        font.pixelSize: 14
+                        font.weight: Font.Bold
+                        padding: 12
+                        background: Rectangle { color: parent.pressed ? "#b7eb8f" : "#f6ffed"; radius: 8; border.color: "#b7eb8f" }
+                        contentItem: Text { text: parent.text; font: parent.font; color: "#389e0d"; horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter }
+                        onClicked: root.setCeilingLightLevel(2)
+                    }
+                    Button {
+                        text: "全开"
+                        font.family: "PingFang SC, Microsoft YaHei UI"
+                        font.pixelSize: 14
+                        font.weight: Font.Bold
+                        padding: 12
+                        background: Rectangle { color: parent.pressed ? "#d3adf7" : "#f9f0ff"; radius: 8; border.color: "#d3adf7" }
+                        contentItem: Text { text: parent.text; font: parent.font; color: "#722ed1"; horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter }
+                        onClicked: root.setCeilingLightLevel(3)
+                    }
+                }
+            }
+
+            // 第六栏:展厅灯光与设备
+            ColumnLayout {
+                Layout.fillWidth: true
+                spacing: 15
+                
+                Text {
+                    text: "展厅控制"
+                    font.family: "PingFang SC, Microsoft YaHei UI"
+                    font.weight: Font.Bold
+                    font.pixelSize: 20
+                    color: "#333"
+                }
+
+                Flow {
+                    Layout.fillWidth: true
+                    spacing: 12
+                    
+                    Repeater {
+                        model: [
+                            { name: "玄关顶灯", key: "entrance_lights" },
+                            { name: "玄关射灯", key: "exhibition_spotlight" },
+                            { name: "桌面总开关", key: "exhibition_desktop_switch" },
+                            { name: "3D风扇", key: "exhibition_3d_fan" },
+                            { name: "展台灯带", key: "exhibition_stand_light_strip" }
+                        ]
+                        
+                        delegate: Button {
+                            text: modelData.name
+                            font.family: "PingFang SC, Microsoft YaHei UI"
+                            font.weight: Font.Bold
+                            font.pixelSize: 14
+                            padding: 12
+                            
+                            property bool isOn: false
+                            
+                            background: Rectangle {
+                                color: parent.isOn ? "#ffb400" : "#f2f3f5"
+                                radius: 8
+                                border.color: parent.isOn ? "#ffb400" : "#e5e6eb"
+                            }
+                            contentItem: Text {
+                                text: parent.text
+                                font: parent.font
+                                color: parent.isOn ? "white" : "#4e5969"
+                                horizontalAlignment: Text.AlignHCenter
+                                verticalAlignment: Text.AlignVCenter
+                            }
+                            
+                            onClicked: {
+                                isOn = !isOn
+                                root.toggleExhibitionLight(modelData.key, isOn)
+                            }
+                        }
+                    }
+                }
+            }
+            
+            // 第五栏:重启与维护
+            RowLayout {
+                Layout.fillWidth: true
+                spacing: 20
+                
+                Text {
+                    text: "系统维护"
+                    font.family: "PingFang SC, Microsoft YaHei UI"
+                    font.weight: Font.Bold
+                    font.pixelSize: 20
+                    color: "#333"
+                    Layout.preferredWidth: 80
+                }
+
+                Button {
+                    text: "重启展厅所有设备"
+                    Layout.fillWidth: true
+                    Layout.preferredHeight: 44
+                    background: Rectangle { 
+                        color: parent.pressed ? "#ffccc7" : "#fff1f0"
+                        radius: 8
+                        border.color: "#ffccc7" 
+                    }
+                    contentItem: Text { 
+                        text: parent.text
+                        font.family: "PingFang SC, Microsoft YaHei UI"
+                        font.weight: Font.Bold
+                        font.pixelSize: 16
+                        color: "#cf1322"
+                        horizontalAlignment: Text.AlignHCenter
+                        verticalAlignment: Text.AlignVCenter 
+                    }
+                    onClicked: restartConfirmPopup.open()
+                }
+            }
+            
+            Item { Layout.fillHeight: true } // Spacer
+            
+            Button {
+                text: "关闭"
+                Layout.alignment: Qt.AlignHCenter
+                Layout.preferredWidth: 120
+                Layout.preferredHeight: 48
+                
+                background: Rectangle {
+                    color: parent.pressed ? "#e5e6eb" : "#f2f3f5"
+                    radius: 8
+                }
+                contentItem: Text {
+                    text: parent.text
+                    font.family: "PingFang SC, Microsoft YaHei UI"
+                    font.weight: Font.Bold
+                    font.pixelSize: 16
+                    color: "#1d2129"
+                    horizontalAlignment: Text.AlignHCenter
+                    verticalAlignment: Text.AlignVCenter
+                }
+                onClicked: exhibitionControlPopup.close()
+            }
+        }
+    }
+
     // 主布局
     ColumnLayout {
         anchors.fill: parent
@@ -222,71 +876,167 @@ Rectangle {
                     Layout.fillHeight: true
                     Layout.minimumHeight: 80
 
-                    // 动态加载表情:AnimatedImage 用于 GIF,Image 用于静态图,Text 用于 emoji
-                    Loader {
-                        id: emotionLoader
-                        anchors.centerIn: parent
-                        // 保持正方形,取宽高中较小值的 70%,最小60px
-                        property real maxSize: Math.max(Math.min(parent.width, parent.height) * 0.7, 60)
-                        width: maxSize
-                        height: maxSize
-
-                        sourceComponent: {
-                            var path = displayModel ? displayModel.emotionPath : ""
-                            if (!path || path.length === 0) {
-                                return emojiComponent
-                            }
-                            if (path.indexOf(".gif") !== -1) {
-                                return gifComponent
-                            }
-                            if (path.indexOf(".") !== -1) {
-                                return imageComponent
+                    // 使用 RowLayout 来放置 左侧按钮 - 表情 - 右侧按钮
+                    RowLayout {
+                        anchors.fill: parent
+                        spacing: 20
+
+                        // 左侧:视频播放悬浮球
+                        Item {
+                            Layout.fillWidth: true
+                            Layout.fillHeight: true
+                            
+                            // 视频播放悬浮球
+                            Rectangle {
+                                anchors.centerIn: parent
+                                width: 240
+                                height: 240
+                                radius: 120
+                                color: videoMouse.pressed ? "#fff7e6" : (videoMouse.containsMouse ? "#fff1b8" : "#ffffff")
+                                border.color: "#faad14"
+                                border.width: 4
+                                visible: true 
+                                
+                                layer.enabled: true
+                                layer.effect: DropShadow {
+                                    transparentBorder: true
+                                    horizontalOffset: 0
+                                    verticalOffset: 4
+                                    radius: 16
+                                    color: "#20faad14"
+                                }
+
+                                Text {
+                                    anchors.centerIn: parent
+                                    text: "视频播放"
+                                    font.family: "PingFang SC, Microsoft YaHei UI"
+                                    font.weight: Font.Bold
+                                    font.pixelSize: 36
+                                    color: "#faad14"
+                                }
+
+                                MouseArea {
+                                    id: videoMouse
+                                    anchors.fill: parent
+                                    hoverEnabled: true
+                                    cursorShape: Qt.PointingHandCursor
+                                    onClicked: videoPopup.open()
+                                }
+                                
+                                // 鼠标悬停动画
+                                scale: videoMouse.containsMouse ? 1.05 : 1.0
+                                Behavior on scale { NumberAnimation { duration: 150; easing.type: Easing.OutQuad } }
                             }
-                            return emojiComponent
                         }
 
-                        // GIF 动图组件
-                        Component {
-                            id: gifComponent
-                            AnimatedImage {
-                                fillMode: Image.PreserveAspectCrop
-                                source: displayModel ? displayModel.emotionPath : ""
-                                playing: true
-                                speed: 1.05
-                                cache: true
-                                clip: true
-                                onStatusChanged: {
-                                    if (status === Image.Error) {
-                                        console.error("AnimatedImage error:", errorString, "src=", source)
+                        // 中间:动态加载表情
+                        Item {
+                            Layout.preferredWidth: Math.min(parent.width * 0.3, parent.height)
+                            Layout.fillHeight: true
+                            
+                            Loader {
+                                id: emotionLoader
+                                anchors.centerIn: parent
+                                // 保持正方形
+                                property real maxSize: Math.min(parent.width, parent.height)
+                                width: maxSize
+                                height: maxSize
+
+                                sourceComponent: {
+                                    var path = displayModel ? displayModel.emotionPath : ""
+                                    if (!path || path.length === 0) {
+                                        return emojiComponent
+                                    }
+                                    if (path.indexOf(".gif") !== -1) {
+                                        return gifComponent
+                                    }
+                                    if (path.indexOf(".") !== -1) {
+                                        return imageComponent
+                                    }
+                                    return emojiComponent
+                                }
+
+                                // GIF 动图组件
+                                Component {
+                                    id: gifComponent
+                                    AnimatedImage {
+                                        fillMode: Image.PreserveAspectCrop
+                                        source: displayModel ? displayModel.emotionPath : ""
+                                        playing: true
+                                        speed: 1.05
+                                        cache: true
+                                        clip: true
+                                    }
+                                }
+
+                                // 静态图片组件
+                                Component {
+                                    id: imageComponent
+                                    Image {
+                                        fillMode: Image.PreserveAspectCrop
+                                        source: displayModel ? displayModel.emotionPath : ""
+                                        cache: true
+                                        clip: true
                                     }
                                 }
-                            }
-                        }
 
-                        // 静态图片组件
-                        Component {
-                            id: imageComponent
-                            Image {
-                                fillMode: Image.PreserveAspectCrop
-                                source: displayModel ? displayModel.emotionPath : ""
-                                cache: true
-                                clip: true
-                                onStatusChanged: {
-                                    if (status === Image.Error) {
-                                        console.error("Image error:", errorString, "src=", source)
+                                // Emoji 文本组件
+                                Component {
+                                    id: emojiComponent
+                                    Text {
+                                        text: displayModel ? displayModel.emotionPath : "😊"
+                                        font.pixelSize: 80
+                                        horizontalAlignment: Text.AlignHCenter
+                                        verticalAlignment: Text.AlignVCenter
                                     }
                                 }
                             }
                         }
 
-                        // Emoji 文本组件
-                        Component {
-                            id: emojiComponent
-                            Text {
-                                text: displayModel ? displayModel.emotionPath : "😊"
-                                font.pixelSize: 80
-                                horizontalAlignment: Text.AlignHCenter
-                                verticalAlignment: Text.AlignVCenter
+                        // 右侧:展厅控制悬浮球
+                        Item {
+                            Layout.fillWidth: true
+                            Layout.fillHeight: true
+
+                            Rectangle {
+                                anchors.centerIn: parent
+                                width: 240
+                                height: 240
+                                radius: 120
+                                color: exhibitionMouse.pressed ? "#cce4ff" : (exhibitionMouse.containsMouse ? "#e6f4ff" : "#ffffff")
+                                border.color: "#165dff"
+                                border.width: 4
+                                
+                                layer.enabled: true
+                                layer.effect: DropShadow {
+                                    transparentBorder: true
+                                    horizontalOffset: 0
+                                    verticalOffset: 4
+                                    radius: 16
+                                    color: "#20165dff"
+                                }
+
+                                Text {
+                                    anchors.centerIn: parent
+                                    text: "展厅控制"
+                                    font.family: "PingFang SC, Microsoft YaHei UI"
+                                    font.weight: Font.Bold
+                                    font.pixelSize: 36
+                                    color: "#165dff"
+                                    Layout.alignment: Qt.AlignHCenter
+                                }
+
+                                MouseArea {
+                                    id: exhibitionMouse
+                                    anchors.fill: parent
+                                    hoverEnabled: true
+                                    cursorShape: Qt.PointingHandCursor
+                                    onClicked: exhibitionControlPopup.open()
+                                }
+
+                                // 鼠标悬停动画
+                                scale: exhibitionMouse.containsMouse ? 1.05 : 1.0
+                                Behavior on scale { NumberAnimation { duration: 150; easing.type: Easing.OutQuad } }
                             }
                         }
                     }
@@ -410,7 +1160,7 @@ Rectangle {
                 // 输入 + 发送
                 RowLayout {
                     Layout.fillWidth: false
-                    Layout.preferredWidth: 160
+                    Layout.preferredWidth: 480
                     Layout.preferredHeight: 38
                     spacing: 6
 
@@ -438,6 +1188,26 @@ Rectangle {
                             Text { anchors.fill: parent; text: "输入文字..."; font: textInput.font; color: "#c9cdd4"; verticalAlignment: Text.AlignVCenter; visible: !textInput.text && !textInput.activeFocus }
 
                             Keys.onReturnPressed: { if (textInput.text.trim().length > 0) { root.sendButtonClicked(textInput.text); textInput.text = "" } }
+
+                            // 获得焦点时触发快捷指令弹窗
+                            onActiveFocusChanged: {
+                                if (activeFocus && !quickCmdPopup.visible) {
+                                    quickCmdPopup.open()
+                                }
+                            }
+                            
+                            // 点击时也能触发(如果已经有焦点但弹窗关了的情况)
+                            MouseArea {
+                                anchors.fill: parent
+                                propagateComposedEvents: true
+                                cursorShape: Qt.IBeamCursor
+                                onClicked: {
+                                    if (!quickCmdPopup.visible) {
+                                        quickCmdPopup.open()
+                                    }
+                                    mouse.accepted = false
+                                }
+                            }
                         }
                     }
 
@@ -481,33 +1251,6 @@ Rectangle {
                     onClicked: root.modeButtonClicked()
                 }
 
-                // 快捷指令(次要)
-                Button {
-                    id: quickCmdBtn
-                    Layout.preferredWidth: 200
-                    Layout.fillWidth: true
-                    Layout.maximumWidth: 300
-                    Layout.preferredHeight: 38
-                    text: "快捷指令"
-                    background: Rectangle { 
-                        color: quickCmdBtn.pressed ? "#b7eb8f" : (quickCmdBtn.hovered ? "#d9f7be" : "#f6ffed")
-                        radius: 8 
-                        border.color: "#b7eb8f"
-                        border.width: 1
-                    }
-                    contentItem: Text {
-                        text: quickCmdBtn.text
-                        font.family: "PingFang SC, Microsoft YaHei UI"
-                        font.pixelSize: 16
-                        font.weight: Font.Bold
-                        color: "#389e0d"
-                        horizontalAlignment: Text.AlignHCenter
-                        verticalAlignment: Text.AlignVCenter
-                        elide: Text.ElideRight
-                    }
-                    onClicked: quickCmdPopup.open()
-                }
-
                 // 设置(次要)
                 Button {
                     id: settingsBtn

+ 29 - 0
src/display/gui_display_model.py

@@ -19,6 +19,8 @@ class GuiDisplayModel(QObject):
     modeTextChanged = pyqtSignal()
     autoModeChanged = pyqtSignal()
     quickCommandsChanged = pyqtSignal()
+    exhibitionVolumeChanged = pyqtSignal()
+    videoListChanged = pyqtSignal()
 
     # 用户操作信号
     manualButtonPressed = pyqtSignal()
@@ -26,6 +28,9 @@ class GuiDisplayModel(QObject):
     autoButtonClicked = pyqtSignal()
     abortButtonClicked = pyqtSignal()
     modeButtonClicked = pyqtSignal()
+    exhibitionButtonClicked = pyqtSignal()
+    videoButtonClicked = pyqtSignal()
+    playVideo = pyqtSignal(int)
     sendButtonClicked = pyqtSignal(str)  # 携带输入的文本
     settingsButtonClicked = pyqtSignal()
 
@@ -41,6 +46,8 @@ class GuiDisplayModel(QObject):
         self._auto_mode = False  # 是否自动模式
         self._is_connected = False
         self._quick_commands = []  # 快捷指令列表
+        self._exhibition_volume = 50  # 展厅音量
+        self._video_list = []  # 视频列表
 
     # 状态文本属性
     @pyqtProperty(str, notify=statusTextChanged)
@@ -119,6 +126,28 @@ class GuiDisplayModel(QObject):
             self._quick_commands = value
             self.quickCommandsChanged.emit()
 
+    # 展厅音量属性
+    @pyqtProperty(int, notify=exhibitionVolumeChanged)
+    def exhibitionVolume(self):
+        return self._exhibition_volume
+
+    @exhibitionVolume.setter
+    def exhibitionVolume(self, value):
+        if self._exhibition_volume != value:
+            self._exhibition_volume = value
+            self.exhibitionVolumeChanged.emit()
+
+    # 视频列表属性
+    @pyqtProperty(list, notify=videoListChanged)
+    def videoList(self):
+        return self._video_list
+
+    @videoList.setter
+    def videoList(self, value):
+        if self._video_list != value:
+            self._video_list = value
+            self.videoListChanged.emit()
+
     # 便捷方法
     def update_status(self, status: str, connected: bool):
         """

+ 5 - 0
src/utils/config_manager.py

@@ -75,6 +75,11 @@ class ConfigManager:
             "input_channels": None,
             "output_channels": None,
         },
+        "EXHIBITION_CONTROL": {
+            "flask_api_base": "http://192.168.254.242:5050",
+            "username": "admin",
+            "password": "HNYZ0821",
+        },
     }
 
     def __new__(cls):

+ 333 - 0
src/utils/exhibition_control.py

@@ -0,0 +1,333 @@
+import os
+import time
+import threading
+import requests
+from src.utils.config_manager import ConfigManager
+from src.utils.logging_config import get_logger
+
+logger = get_logger(__name__)
+
+class ExhibitionControl:
+    _instance = None
+    
+    def __new__(cls):
+        if cls._instance is None:
+            cls._instance = super().__new__(cls)
+            cls._instance._initialized = False
+        return cls._instance
+
+    def __init__(self):
+        if self._initialized:
+            return
+        self._initialized = True
+        
+        self.config_manager = ConfigManager.get_instance()
+        self._session = requests.Session()
+        self._has_logged_in = False
+        
+        # Load config
+        self._load_config()
+
+    def _load_config(self):
+        config = self.config_manager.get_config("EXHIBITION_CONTROL", {})
+        # 优先使用配置文件,其次环境变量,最后默认值
+        default_base = os.environ.get('FLASK_API_BASE', 'http://192.168.254.242:5050')
+        self.FLASK_BASE = config.get('flask_api_base', default_base)
+        self.ADMIN_USERNAME = config.get('username', 'admin')
+        self.ADMIN_PASSWORD = config.get('password', 'HNYZ0821')
+
+    def _ensure_login(self):
+        if self._has_logged_in:
+            return
+            
+        # 尝试常用登录路径 (先尝试 /auth/login, 再尝试 /login)
+        for login_path in ['/auth/login', '/login']:
+            url = f"{self.FLASK_BASE}{login_path}"
+            try:
+                # 只有当响应不是 404 时才认为是有效的登录端点
+                resp = self._session.post(url, data={
+                    'username': self.ADMIN_USERNAME,
+                    'password': self.ADMIN_PASSWORD
+                }, timeout=5)
+                
+                if resp.status_code != 404:
+                    resp.raise_for_status()
+                    logger.info(f"登录成功: {url}")
+                    self._has_logged_in = True
+                    return
+            except Exception as e:
+                logger.warning(f"尝试登录 {url} 失败: {e}")
+                
+        logger.error("无法登录 Flask API,后续请求可能会失败")
+
+    def set_volume(self, volume: int) -> bool:
+        """设置全局音量 (0-100)"""
+        try:
+            self._ensure_login()
+            url = f"{self.FLASK_BASE}/api/kodi/set_volume"
+            resp = self._session.post(url, json={"volume": volume}, timeout=5)
+            resp.raise_for_status()
+            logger.info(f"设置音量成功: {volume}")
+            return True
+        except Exception as e:
+            logger.error(f"设置音量失败: {e}")
+            return False
+
+    def get_volume(self) -> int:
+        """获取全局音量"""
+        try:
+            self._ensure_login()
+            url = f"{self.FLASK_BASE}/api/kodi/get_volume"
+            resp = self._session.get(url, timeout=5)
+            resp.raise_for_status()
+            data = resp.json()
+            if data.get("success"):
+                return data.get("data", {}).get("volume", 50)
+            return 50
+        except Exception as e:
+            logger.error(f"获取音量失败: {e}")
+            return 50
+
+    def set_ceiling_lights_level(self, level: int) -> bool:
+        """
+        设置展厅顶灯等级 (一楼)
+        level: 0-3 (0:全关, 1:单灯, 2:双灯, 3:全开)
+        """
+        if level not in [0, 1, 2, 3]:
+            logger.error(f"无效的灯光等级: {level}")
+            return False
+
+        try:
+            self._ensure_login()
+            url = f"{self.FLASK_BASE}/api/ha/exhibition_ceiling_lights/set_level"
+            resp = self._session.post(url, json={"level": level}, timeout=5)
+            resp.raise_for_status()
+            logger.info(f"设置顶灯等级成功: {level}")
+            return True
+        except Exception as e:
+            logger.error(f"设置顶灯等级失败: {e}")
+            return False
+
+    def control_device(self, device_key: str, action: str) -> bool:
+        """
+        通用设备控制
+        device_key: 设备标识符 (如 'entrance_lights')
+        action: 'turn_on' 或 'turn_off'
+        """
+        if action not in ["turn_on", "turn_off"]:
+            logger.error(f"无效的动作: {action}")
+            return False
+
+        try:
+            self._ensure_login()
+            url = f"{self.FLASK_BASE}/api/ha/{device_key}/{action}"
+            resp = self._session.post(url, timeout=5)
+            resp.raise_for_status()
+            logger.info(f"设备控制成功: {device_key} -> {action}")
+            return True
+        except Exception as e:
+            logger.error(f"设备控制失败 {device_key}: {e}")
+            return False
+
+    def control_all_tvs(self, action: str) -> bool:
+        """
+        控制所有电视
+        action: 'turn_on_all' 或 'turn_off_all'
+        """
+        if action not in ["turn_on_all", "turn_off_all"]:
+            logger.error(f"无效的动作: {action}")
+            return False
+
+        try:
+            self._ensure_login()
+            url = f"{self.FLASK_BASE}/api/mitv/{action}"
+            resp = self._session.post(url, timeout=5)
+            resp.raise_for_status()
+            logger.info(f"所有电视控制成功: {action}")
+            return True
+        except Exception as e:
+            logger.error(f"所有电视控制失败: {e}")
+            return False
+
+    def control_welcome_mode(self, on: bool) -> bool:
+        """
+        控制迎宾模式
+        on: True (启动) / False (关闭)
+        """
+        action_desc = "启动" if on else "关闭"
+        light_action = "turn_on" if on else "turn_off"
+        door_mode = 1 if on else 0  # 1:常开, 0:正常
+
+        try:
+            self._ensure_login()
+            logger.info(f"【迎宾模式】正在{action_desc}...")
+
+            # 批量控制灯光设备
+            lights = [
+                "entrance_lights",
+                "exhibition_spotlight",
+                "exhibition_stand_light_strip"
+            ]
+            
+            for light in lights:
+                try:
+                    self._session.post(
+                        f"{self.FLASK_BASE}/api/ha/{light}/{light_action}",
+                        timeout=2
+                    )
+                except Exception as e:
+                    logger.error(f"控制 {light} 失败: {e}")
+
+            # 控制顶灯等级 (启动: 3级, 关闭: 0级)
+            ceiling_level = 3 if on else 0
+            self.set_ceiling_lights_level(ceiling_level)
+
+            # 控制大门模式
+            try:
+                self._session.post(
+                    f"{self.FLASK_BASE}/api/door/control",
+                    json={"control_way": door_mode},
+                    timeout=2
+                )
+            except Exception as e:
+                logger.error(f"控制大门模式失败: {e}")
+
+            logger.info(f"【迎宾模式】{action_desc}完成")
+            return True
+
+        except Exception as e:
+            logger.error(f"【迎宾模式】{action_desc}失败: {e}")
+            return False
+
+    def set_door_mode(self, control_way: int) -> bool:
+        """
+        设置办公楼大门模式
+        control_way: 0:正常模式, 1:常开模式, 2:常闭模式
+        """
+        if control_way not in [0, 1, 2]:
+            logger.error(f"无效的大门模式: {control_way}")
+            return False
+
+        try:
+            self._ensure_login()
+            url = f"{self.FLASK_BASE}/api/door/control"
+            resp = self._session.post(url, json={"control_way": control_way}, timeout=5)
+            resp.raise_for_status()
+            logger.info(f"大门模式设置成功: {control_way}")
+            return True
+        except Exception as e:
+            logger.error(f"设置大门模式失败: {e}")
+            return False
+
+    def restart_all_devices(self) -> bool:
+        """重启展厅所有设备"""
+        def _restart_process():
+            try:
+                self._ensure_login()
+                logger.info("【重启任务】开始执行...")
+
+                # 0. 撤销独立状态
+                logger.info("【重启任务】正在撤销独立状态...")
+                try:
+                    self._session.post(f"{self.FLASK_BASE}/api/kodi/revoke_individual_state", timeout=5)
+                    logger.info("【重启任务】撤销独立状态指令已发送,等待 3 秒...")
+                    time.sleep(3)
+                except Exception as e:
+                    logger.error(f"撤销独立状态失败: {e}")
+                
+                # 1. 关闭设备
+                logger.info("【重启任务】正在关闭设备...")
+                # 关闭所有电视
+                try:
+                    self._session.post(f"{self.FLASK_BASE}/api/mitv/turn_off_all", timeout=5)
+                except Exception as e:
+                    logger.error(f"关闭电视失败: {e}")
+
+                # 关闭桌面3D风扇投影
+                try:
+                    self._session.post(f"{self.FLASK_BASE}/api/ha/exhibition_3d_fan/turn_off", timeout=5)
+                except Exception as e:
+                    logger.error(f"关闭3D风扇失败: {e}")
+
+                # 关闭展品灯座
+                try:
+                    self._session.post(f"{self.FLASK_BASE}/api/ha/exhibition_desktop_switch/turn_off", timeout=5)
+                except Exception as e:
+                    logger.error(f"关闭展品灯座失败: {e}")
+                
+                # 2. 等待
+                logger.info("【重启任务】设备已关闭,等待 60 秒...")
+                time.sleep(60)
+                
+                # 3. 启动设备
+                logger.info("【重启任务】正在启动设备...")
+                self._ensure_login() # Ensure login again just in case
+                
+                # 启动所有电视
+                try:
+                    self._session.post(f"{self.FLASK_BASE}/api/mitv/turn_on_all", timeout=5)
+                except Exception as e:
+                    logger.error(f"启动电视失败: {e}")
+
+                # 启动桌面3D风扇投影
+                try:
+                    self._session.post(f"{self.FLASK_BASE}/api/ha/exhibition_3d_fan/turn_on", timeout=5)
+                except Exception as e:
+                    logger.error(f"启动3D风扇失败: {e}")
+
+                # 启动展品灯座
+                try:
+                    self._session.post(f"{self.FLASK_BASE}/api/ha/exhibition_desktop_switch/turn_on", timeout=5)
+                except Exception as e:
+                    logger.error(f"启动展品灯座失败: {e}")
+
+                # 启动视频播放
+                try:
+                    self._session.post(f"{self.FLASK_BASE}/api/kodi/free_time/control", json={"action": "start"}, timeout=5)
+                except Exception as e:
+                    logger.error(f"启动视频播放失败: {e}")
+                
+                logger.info("【重启任务】执行完成")
+            except Exception as e:
+                logger.error(f"【重启任务】发生异常: {e}")
+
+        t = threading.Thread(target=_restart_process)
+        t.daemon = True
+        t.start()
+        return True
+
+    def get_videos(self) -> list:
+        """获取视频列表"""
+        try:
+            self._ensure_login()
+            url = f"{self.FLASK_BASE}/api/kodi/videos"
+            resp = self._session.get(url, timeout=5)
+            resp.raise_for_status()
+            data = resp.json()
+            # 假设返回的数据结构是 {"success": true, "data": [...]} 或直接是列表
+            if isinstance(data, list):
+                return data
+            elif isinstance(data, dict) and "data" in data:
+                return data["data"]
+            return []
+        except Exception as e:
+            logger.error(f"获取视频列表失败: {e}")
+            return []
+
+    def play_video(self, video_id: int, volume: int = -1) -> bool:
+        """播放视频"""
+        try:
+            self._ensure_login()
+            url = f"{self.FLASK_BASE}/api/kodi/start"
+            payload = {"video_id": video_id}
+            if volume >= 0:
+                payload["volume"] = volume
+            
+            resp = self._session.post(url, json=payload, timeout=5)
+            resp.raise_for_status()
+            logger.info(f"播放视频成功: {video_id}")
+            return True
+        except Exception as e:
+            logger.error(f"播放视频失败: {e}")
+            return False
+