From bf64e8016283b18c04d5392dd9c002b921021af2 Mon Sep 17 00:00:00 2001
From: Junjie <fallin.jie@qq.com>
Date: 星期六, 07 三月 2026 09:47:04 +0800
Subject: [PATCH] #

---
 src/main/webapp/components/WatchRgvCard.js                            |  386 ++--
 src/main/java/com/zy/asrs/controller/WatchStationColorController.java |  152 +
 src/main/webapp/components/MapCanvas.js                               |  562 ++++++-
 src/main/webapp/views/watch/console.html                              |  794 ++++++++-
 src/main/webapp/views/config/config.html                              |    2 
 src/main/java/com/zy/core/utils/WmsOperateUtils.java                  |    2 
 src/main/webapp/components/WatchDualCrnCard.js                        |  576 +++----
 src/main/webapp/components/MonitorWorkbench.js                        |  661 ++++++++
 src/main/java/com/zy/core/plugin/NormalProcess.java                   |    2 
 src/main/java/com/zy/core/plugin/XiaosongProcess.java                 |    2 
 src/main/webapp/components/MonitorCardKit.js                          |  147 +
 src/main/webapp/views/watch/stationColorConfig.html                   |  290 +++
 src/main/webapp/components/DevpCard.js                                |  645 ++++----
 src/main/webapp/components/WatchCrnCard.js                            |  419 ++--
 src/main/webapp/static/js/watch/stationColorConfig.js                 |  127 +
 src/main/java/com/zy/core/enums/RedisKeyType.java                     |    1 
 src/main/webapp/views/deviceLogs/deviceLogs.html                      |    3 
 src/main/resources/application.yml                                    |    2 
 src/main/java/com/zy/core/plugin/FakeProcess.java                     |    2 
 19 files changed, 3,566 insertions(+), 1,209 deletions(-)

diff --git a/src/main/java/com/zy/asrs/controller/WatchStationColorController.java b/src/main/java/com/zy/asrs/controller/WatchStationColorController.java
new file mode 100644
index 0000000..e8bd67b
--- /dev/null
+++ b/src/main/java/com/zy/asrs/controller/WatchStationColorController.java
@@ -0,0 +1,152 @@
+package com.zy.asrs.controller;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.core.annotations.ManagerAuth;
+import com.core.common.R;
+import com.zy.common.utils.RedisUtil;
+import com.zy.core.enums.RedisKeyType;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.*;
+
+@RestController
+@RequestMapping("/watch/stationColor")
+public class WatchStationColorController {
+
+    @Autowired
+    private RedisUtil redisUtil;
+
+    @GetMapping("/config/auth")
+    @ManagerAuth
+    public R getConfig() {
+        return R.ok(buildResponseData(loadStoredColorMap()));
+    }
+
+    @PostMapping("/config/save/auth")
+    @ManagerAuth
+    public R saveConfig(@RequestBody Map<String, Object> payload) {
+        Map<String, String> storedMap = loadStoredColorMap();
+        Map<String, String> mergedMap = new LinkedHashMap<>(storedMap);
+
+        Object itemsObj = payload == null ? null : payload.get("items");
+        if (!(itemsObj instanceof List)) {
+            return R.error("璇蜂紶鍏ラ鑹查厤缃垪琛�");
+        }
+
+        List<?> items = (List<?>) itemsObj;
+        Set<String> allowedStatuses = getDefaultColorMap().keySet();
+        for (Object obj : items) {
+            if (!(obj instanceof Map)) {
+                continue;
+            }
+            Map<?, ?> item = (Map<?, ?>) obj;
+            String status = item.get("status") == null ? null : String.valueOf(item.get("status")).trim();
+            String color = item.get("color") == null ? null : String.valueOf(item.get("color")).trim();
+            if (status == null || status.isEmpty() || !allowedStatuses.contains(status)) {
+                continue;
+            }
+            mergedMap.put(status, normalizeColor(color, getDefaultColorMap().get(status)));
+        }
+
+        redisUtil.set(RedisKeyType.WATCH_STATION_COLOR_CONFIG.key, JSON.toJSONString(mergedMap));
+        return R.ok(buildResponseData(mergedMap));
+    }
+
+    private Map<String, Object> buildResponseData(Map<String, String> storedMap) {
+        Map<String, String> defaults = getDefaultColorMap();
+        List<Map<String, String>> items = new ArrayList<>();
+        for (Map<String, String> meta : getColorMetaList()) {
+            String status = meta.get("status");
+            Map<String, String> item = new LinkedHashMap<>(meta);
+            item.put("defaultColor", defaults.get(status));
+            item.put("color", normalizeColor(storedMap.get(status), defaults.get(status)));
+            items.add(item);
+        }
+        Map<String, Object> data = new LinkedHashMap<>();
+        data.put("items", items);
+        data.put("defaults", defaults);
+        return data;
+    }
+
+    private Map<String, String> loadStoredColorMap() {
+        Map<String, String> result = new LinkedHashMap<>(getDefaultColorMap());
+        Object object = redisUtil.get(RedisKeyType.WATCH_STATION_COLOR_CONFIG.key);
+        if (object == null) {
+            return result;
+        }
+        try {
+            JSONObject jsonObject;
+            if (object instanceof JSONObject) {
+                jsonObject = (JSONObject) object;
+            } else {
+                jsonObject = JSON.parseObject(String.valueOf(object));
+            }
+            if (jsonObject == null) {
+                return result;
+            }
+            for (String status : getDefaultColorMap().keySet()) {
+                String color = jsonObject.getString(status);
+                if (color != null) {
+                    result.put(status, normalizeColor(color, getDefaultColorMap().get(status)));
+                }
+            }
+        } catch (Exception ignore) {
+        }
+        return result;
+    }
+
+    private Map<String, String> getDefaultColorMap() {
+        Map<String, String> defaults = new LinkedHashMap<>();
+        defaults.put("site-auto", "#78FF81");
+        defaults.put("site-auto-run", "#FA51F6");
+        defaults.put("site-auto-id", "#C4C400");
+        defaults.put("site-auto-run-id", "#30BFFC");
+        defaults.put("site-enable-in", "#18C7B8");
+        defaults.put("site-unauto", "#B8B8B8");
+        defaults.put("machine-pakin", "#30BFFC");
+        defaults.put("machine-pakout", "#97B400");
+        defaults.put("site-run-block", "#E69138");
+        return defaults;
+    }
+
+    private List<Map<String, String>> getColorMetaList() {
+        List<Map<String, String>> list = new ArrayList<>();
+        list.add(buildMeta("site-auto", "鑷姩", "绔欑偣鑷姩寰呭懡鏃剁殑棰滆壊銆�"));
+        list.add(buildMeta("site-auto-run", "鑷姩 + 鏈夌墿", "绔欑偣鑷姩杩愯涓旀湁鐗╋紝浣嗚繕娌℃湁宸ヤ綔鍙锋椂鐨勯鑹层��"));
+        list.add(buildMeta("site-auto-id", "鑷姩 + 宸ヤ綔鍙�", "绔欑偣鑷姩涓斿瓨鍦ㄥ伐浣滃彿锛屼絾褰撳墠鏃犵墿鏃剁殑棰滆壊銆�"));
+        list.add(buildMeta("site-auto-run-id", "鑷姩 + 鏈夌墿 + 宸ヤ綔鍙�", "鏅�氳繍琛屼腑鐨勭珯鐐归鑹诧紝鏈懡涓叆搴�/鍑哄簱鑼冨洿鏃朵娇鐢ㄣ��"));
+        list.add(buildMeta("site-enable-in", "鍚姩鍏ュ簱", "宸ヤ綔鍙蜂负 9998 鎴栫珯鐐瑰甫鍚姩鍏ュ簱鏍囪鏃剁殑棰滆壊銆�"));
+        list.add(buildMeta("machine-pakin", "鍏ュ簱浠诲姟", "宸ヤ綔鍙峰懡涓叆搴撹寖鍥存椂鐨勯鑹层��"));
+        list.add(buildMeta("machine-pakout", "鍑哄簱浠诲姟", "宸ヤ綔鍙峰懡涓嚭搴撹寖鍥存椂鐨勯鑹层��"));
+        list.add(buildMeta("site-run-block", "杩愯鍫靛", "绔欑偣澶勪簬杩愯鍫靛鏃剁殑棰滆壊銆�"));
+        list.add(buildMeta("site-unauto", "闈炶嚜鍔�", "绔欑偣闈炶嚜鍔ㄦ椂鐨勯鑹层��"));
+        return list;
+    }
+
+    private Map<String, String> buildMeta(String status, String name, String desc) {
+        Map<String, String> map = new LinkedHashMap<>();
+        map.put("status", status);
+        map.put("name", name);
+        map.put("desc", desc);
+        return map;
+    }
+
+    private String normalizeColor(String color, String fallback) {
+        if (color == null) {
+            return fallback;
+        }
+        String value = color.trim();
+        if (value.matches("^#[0-9a-fA-F]{6}$")) {
+            return value.toUpperCase();
+        }
+        if (value.matches("^#[0-9a-fA-F]{3}$")) {
+            return ("#" + value.charAt(1) + value.charAt(1) + value.charAt(2) + value.charAt(2) + value.charAt(3) + value.charAt(3)).toUpperCase();
+        }
+        if (value.matches("^0x[0-9a-fA-F]{6}$")) {
+            return ("#" + value.substring(2)).toUpperCase();
+        }
+        return fallback;
+    }
+}
diff --git a/src/main/java/com/zy/core/enums/RedisKeyType.java b/src/main/java/com/zy/core/enums/RedisKeyType.java
index 916a7bb..caf05c3 100644
--- a/src/main/java/com/zy/core/enums/RedisKeyType.java
+++ b/src/main/java/com/zy/core/enums/RedisKeyType.java
@@ -55,6 +55,7 @@
     CRN_OUT_TASK_COMPLETE_STATION_INFO("crn_out_task_complete_station_info_"),
 
     WATCH_CIRCLE_STATION_("watch_circle_station_"),
+    WATCH_STATION_COLOR_CONFIG("watch_station_color_config"),
     STATION_CYCLE_LOAD_RESERVE("station_cycle_load_reserve"),
 
     CURRENT_CIRCLE_TASK_CRN_NO("current_circle_task_crn_no_"),
diff --git a/src/main/java/com/zy/core/plugin/FakeProcess.java b/src/main/java/com/zy/core/plugin/FakeProcess.java
index 6ef204c..d03023b 100644
--- a/src/main/java/com/zy/core/plugin/FakeProcess.java
+++ b/src/main/java/com/zy/core/plugin/FakeProcess.java
@@ -496,7 +496,7 @@
                         // 1. 棣栧厛鏌ヨ鏄惁鏈夊凡瀹屾垚鐨勫紓姝ュ搷搴�
                         String response = wmsOperateUtils.queryAsyncInTaskResponse(barcode, stationIdVal);
 
-                        if (response != null) {
+                        if (!Cools.isEmpty(response)) {
                             // 2. 鏈夊搷搴旂粨鏋滐紝澶勭悊鍝嶅簲
                             if (response.equals("FAILED") || response.startsWith("ERROR:")) {
                                 // 璇锋眰澶辫触锛岄噸鏂板彂璧峰紓姝ヨ姹�
diff --git a/src/main/java/com/zy/core/plugin/NormalProcess.java b/src/main/java/com/zy/core/plugin/NormalProcess.java
index 047b07e..92f770b 100644
--- a/src/main/java/com/zy/core/plugin/NormalProcess.java
+++ b/src/main/java/com/zy/core/plugin/NormalProcess.java
@@ -149,7 +149,7 @@
                         // 1. 棣栧厛鏌ヨ鏄惁鏈夊凡瀹屾垚鐨勫紓姝ュ搷搴�
                         String response = wmsOperateUtils.queryAsyncInTaskResponse(barcode, stationIdVal);
 
-                        if (response != null) {
+                        if (!Cools.isEmpty(response)) {
                             // 2. 鏈夊搷搴旂粨鏋滐紝澶勭悊鍝嶅簲
                             if (response.equals("FAILED") || response.startsWith("ERROR:")) {
                                 // 璇锋眰澶辫触锛岄噸鏂板彂璧峰紓姝ヨ姹�
diff --git a/src/main/java/com/zy/core/plugin/XiaosongProcess.java b/src/main/java/com/zy/core/plugin/XiaosongProcess.java
index 6750c53..2fdf863 100644
--- a/src/main/java/com/zy/core/plugin/XiaosongProcess.java
+++ b/src/main/java/com/zy/core/plugin/XiaosongProcess.java
@@ -176,7 +176,7 @@
                         // 1. 棣栧厛鏌ヨ鏄惁鏈夊凡瀹屾垚鐨勫紓姝ュ搷搴�
                         String response = wmsOperateUtils.queryAsyncInTaskResponse(barcode, stationIdVal);
 
-                        if (response != null) {
+                        if (!Cools.isEmpty(response)) {
                             // 2. 鏈夊搷搴旂粨鏋滐紝澶勭悊鍝嶅簲
                             if (response.equals("FAILED") || response.startsWith("ERROR:")) {
                                 // 璇锋眰澶辫触锛岄噸鏂板彂璧峰紓姝ヨ姹�
diff --git a/src/main/java/com/zy/core/utils/WmsOperateUtils.java b/src/main/java/com/zy/core/utils/WmsOperateUtils.java
index 4ef1979..27b19bd 100644
--- a/src/main/java/com/zy/core/utils/WmsOperateUtils.java
+++ b/src/main/java/com/zy/core/utils/WmsOperateUtils.java
@@ -103,7 +103,7 @@
                     .setTimeout(30, TimeUnit.SECONDS)
                     .build()
                     .doPost();
-            if (response != null) {
+            if (!Cools.isEmpty(response)) {
                 JSONObject jsonObject = JSON.parseObject(response);
                 if (jsonObject.getInteger("code") == 200) {
                     result = 1;
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 6e76f8b..6d9b66c 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -1,6 +1,6 @@
 # 绯荤粺鐗堟湰淇℃伅
 app:
-  version: 1.0.4.7
+  version: 1.0.4.8
   version-type: dev  # prd 鎴� dev
 
 server:
diff --git a/src/main/webapp/components/DevpCard.js b/src/main/webapp/components/DevpCard.js
index 39e69eb..d699cfc 100644
--- a/src/main/webapp/components/DevpCard.js
+++ b/src/main/webapp/components/DevpCard.js
@@ -1,49 +1,63 @@
 Vue.component("devp-card", {
   template: `
-    <div>
-        <div style="display: flex;margin-bottom: 10px;">
-            <div style="width: 100%;">杈撻�佺洃鎺�</div>
-            <div style="width: 100%;text-align: right;display: flex;"><el-input size="mini" v-model="searchStationId" placeholder="璇疯緭鍏ョ珯鍙�"></el-input><el-button @click="getDevpStateInfo" size="mini">鏌ヨ</el-button></div>
+    <div class="mc-root">
+      <div class="mc-toolbar">
+        <div class="mc-title">杈撻�佺洃鎺�</div>
+        <div class="mc-search">
+          <input class="mc-input" v-model="searchStationId" placeholder="璇疯緭鍏ョ珯鍙�" />
+          <button type="button" class="mc-btn mc-btn-ghost" @click="getDevpStateInfo">鏌ヨ</button>
         </div>
-        <div style="margin-bottom: 10px;" v-if="!readOnly">
-            <div style="margin-bottom: 5px;">
-               <el-button v-if="showControl" @click="openControl" size="mini">鍏抽棴鎺у埗涓績</el-button>
-               <el-button v-else @click="openControl" size="mini">鎵撳紑鎺у埗涓績</el-button>
-            </div>
-            <div v-if="showControl" style="display: flex;justify-content: space-between;flex-wrap: wrap;">
-                <div style="margin-bottom: 10px;width: 33%;"><el-input size="mini" v-model="controlParam.stationId" placeholder="绔欏彿"></el-input></div>
-                <div style="margin-bottom: 10px;width: 33%;"><el-input size="mini" v-model="controlParam.taskNo" placeholder="宸ヤ綔鍙�"></el-input></div>
-                <div style="margin-bottom: 10px;width: 33%;"><el-input size="mini" v-model="controlParam.targetStationId" placeholder="鐩爣绔�"></el-input></div>
-                <div style="margin-bottom: 10px;"><el-button @click="controlCommand()" size="mini">涓嬪彂</el-button></div>
-                <div style="margin-bottom: 10px;"><el-button @click="resetCommand()" size="mini">澶嶄綅</el-button></div>
-            </div>
+      </div>
+
+      <div v-if="!readOnly" class="mc-control-toggle">
+        <button type="button" class="mc-btn mc-btn-ghost" @click="openControl">
+          {{ showControl ? '鏀惰捣鎺у埗涓績' : '鎵撳紑鎺у埗涓績' }}
+        </button>
+      </div>
+
+      <div v-if="showControl" class="mc-control">
+        <div class="mc-control-grid">
+          <label class="mc-field">
+            <span class="mc-field-label">绔欏彿</span>
+            <input class="mc-input" v-model="controlParam.stationId" placeholder="渚嬪 101" />
+          </label>
+          <label class="mc-field">
+            <span class="mc-field-label">宸ヤ綔鍙�</span>
+            <input class="mc-input" v-model="controlParam.taskNo" placeholder="杈撳叆宸ヤ綔鍙�" />
+          </label>
+          <label class="mc-field mc-span-2">
+            <span class="mc-field-label">鐩爣绔�</span>
+            <input class="mc-input" v-model="controlParam.targetStationId" placeholder="杈撳叆鐩爣绔欏彿" />
+          </label>
+          <div class="mc-action-row">
+            <button type="button" class="mc-btn" @click="controlCommand">涓嬪彂</button>
+            <button type="button" class="mc-btn mc-btn-soft" @click="resetCommand">澶嶄綅</button>
+          </div>
         </div>
-        <div style="max-height: 55vh; overflow:auto;">
-          <el-collapse v-model="activeNames" accordion>
-            <el-collapse-item v-for="(item) in displayStationList" :name="item.stationId">
-            <template slot="title">
-                <div style="width: 100%;display: flex;">
-                   <div style="width: 50%;">{{ item.stationId }}绔�</div>
-                   <div style="width: 50%;text-align: right;">
-                      <el-tag v-if="item.autoing" type="success" size="small">鑷姩</el-tag>
-                      <el-tag v-else type="warning" size="small">鎵嬪姩</el-tag>
-                   </div>
-                </div>
-            </template>
-            <el-descriptions border direction="vertical">
-                <el-descriptions-item label="缂栧彿">{{ item.stationId }}</el-descriptions-item>
-                <el-descriptions-item label="宸ヤ綔鍙�">{{ item.taskNo }}</el-descriptions-item>
-                <el-descriptions-item label="鐩爣绔�">{{ item.targetStaNo }}</el-descriptions-item>
-                <el-descriptions-item label="妯″紡">{{ item.autoing ? '鑷姩' : '鎵嬪姩' }}</el-descriptions-item>
-                <el-descriptions-item label="鏈夌墿">{{ item.loading ? '鏈�' : '鏃�' }}</el-descriptions-item>
-                <el-descriptions-item label="鍙叆">{{ item.inEnable ? 'Y' : 'N' }}</el-descriptions-item>
-                <el-descriptions-item label="鍙嚭">{{ item.outEnable ? 'Y' : 'N' }}</el-descriptions-item>
-                <el-descriptions-item label="绌烘澘淇″彿">{{ item.emptyMk ? 'Y' : 'N' }}</el-descriptions-item>
-                <el-descriptions-item label="婊℃澘淇″彿">{{ item.fullPlt ? 'Y' : 'N' }}</el-descriptions-item>
-                <el-descriptions-item label="杩愯闃诲">{{ item.runBlock ? 'Y' : 'N' }}</el-descriptions-item>
-                <el-descriptions-item label="鍚姩鍏ュ簱">{{ item.enableIn ? 'Y' : 'N' }}</el-descriptions-item>
-                <el-descriptions-item label="鎵樼洏楂樺害">{{ item.palletHeight }}</el-descriptions-item>
-                <el-descriptions-item label="鏉$爜">
+      </div>
+
+      <div class="mc-collapse">
+        <div
+          v-for="item in displayStationList"
+          :key="item.stationId"
+          :class="['mc-item', { 'is-open': isActive(item.stationId) }]"
+        >
+          <button type="button" class="mc-head" @click="toggleItem(item)">
+            <div class="mc-head-main">
+              <div class="mc-head-title">{{ item.stationId }}绔�</div>
+              <div class="mc-head-subtitle">浠诲姟 {{ orDash(item.taskNo) }} | 鐩爣绔� {{ orDash(item.targetStaNo) }}</div>
+            </div>
+            <div class="mc-head-right">
+              <span :class="['mc-badge', 'is-' + getStatusTone(item)]">{{ getStatusLabel(item) }}</span>
+              <span class="mc-chevron">{{ isActive(item.stationId) ? '鈻�' : '鈻�' }}</span>
+            </div>
+          </button>
+
+          <div v-if="isActive(item.stationId)" class="mc-body">
+            <div class="mc-detail-grid">
+              <div v-for="entry in buildDetailEntries(item)" :key="entry.label" class="mc-detail-cell">
+                <div class="mc-detail-label">{{ entry.label }}</div>
+                <div v-if="entry.type === 'barcode'" class="mc-detail-value mc-code">
                   <el-popover v-if="item.barcode" placement="top" width="460" trigger="hover">
                     <div style="text-align: center;">
                       <img
@@ -53,135 +67,287 @@
                       />
                       <div style="margin-top: 4px; font-size: 12px; word-break: break-all;">{{ item.barcode }}</div>
                     </div>
-                    <span slot="reference" @click.stop="handleBarcodeClick(item)" style="cursor: pointer; color: #409EFF;">{{ item.barcode }}</span>
+                    <span
+                      slot="reference"
+                      @click.stop="handleBarcodeClick(item)"
+                      style="cursor: pointer; color: #4677a4; font-weight: 600;"
+                    >{{ entry.value }}</span>
                   </el-popover>
-                  <span v-else @click.stop="handleBarcodeClick(item)" style="cursor: pointer; color: #409EFF;">-</span>
-                </el-descriptions-item>
-                <el-descriptions-item label="閲嶉噺">{{ item.weight }}</el-descriptions-item>
-                <el-descriptions-item label="浠诲姟鍙啓鍖�">{{ item.taskWriteIdx }}</el-descriptions-item>
-                <el-descriptions-item label="鏁呴殰浠g爜">{{ item.error }}</el-descriptions-item>
-                <el-descriptions-item label="鏁呴殰淇℃伅">{{ item.errorMsg }}</el-descriptions-item>
-                <el-descriptions-item label="鎵╁睍鏁版嵁">{{ item.extend }}</el-descriptions-item>
-            </el-descriptions>
-            </el-collapse-item>                                                   
-          </el-collapse>
+                  <span
+                    v-else
+                    @click.stop="handleBarcodeClick(item)"
+                    style="cursor: pointer; color: #4677a4; font-weight: 600;"
+                  >{{ entry.value }}</span>
+                </div>
+                <div v-else class="mc-detail-value" :class="{ 'mc-code': entry.code }">{{ entry.value }}</div>
+              </div>
+            </div>
+          </div>
         </div>
-        <div style="display:flex; justify-content:flex-end; margin-top:8px;">
-          <el-pagination
-            small
-            @current-change="handlePageChange"
-            @size-change="handleSizeChange"
-            :current-page="currentPage"
-            :page-size="pageSize"
-            :page-sizes="[10,20,50,100]"
-            layout="total, prev, pager, next"
-            :total="stationList.length">
-          </el-pagination>
-        </div>
+
+        <div v-if="displayStationList.length === 0" class="mc-empty">褰撳墠娌℃湁鍙睍绀虹殑绔欑偣鏁版嵁</div>
+      </div>
+
+      <div class="mc-footer">
+        <button type="button" class="mc-page-btn" :disabled="currentPage <= 1" @click="handlePageChange(currentPage - 1)">涓婁竴椤�</button>
+        <span>{{ currentPage }} / {{ totalPages }}</span>
+        <button type="button" class="mc-page-btn" :disabled="currentPage >= totalPages" @click="handlePageChange(currentPage + 1)">涓嬩竴椤�</button>
+      </div>
     </div>
-    `,
+  `,
   props: {
-    param: {
-      type: Object,
-      default: () => ({})
-    },
-    autoRefresh: {
-      type: Boolean,
-      default: true
-    },
-    readOnly: {
-      type: Boolean,
-      default: false
-    }
+    param: { type: Object, default: function () { return {}; } },
+    items: { type: Array, default: null },
+    autoRefresh: { type: Boolean, default: true },
+    readOnly: { type: Boolean, default: false }
   },
-  data() {
+  data: function () {
     return {
       stationList: [],
-      fullStationList: [],
       activeNames: "",
       searchStationId: "",
       showControl: false,
       controlParam: {
         stationId: "",
         taskNo: "",
-        targetStationId: "",
+        targetStationId: ""
       },
       barcodePreviewCache: {},
-      pageSize: 25,
+      pageSize: 12,
       currentPage: 1,
       timer: null
     };
   },
-  created() {
-    if (this.autoRefresh) {
-      this.timer = setInterval(() => {
-        this.getDevpStateInfo();
-      }, 1000);
-    }
-  },
-  beforeDestroy() {
-    if (this.timer) {
-      clearInterval(this.timer);
-    }
-  },
   computed: {
-    displayStationList() {
-      const start = (this.currentPage - 1) * this.pageSize;
-      const end = start + this.pageSize;
-      return this.stationList.slice(start, end);
+    sourceList: function () {
+      return Array.isArray(this.items) ? this.items : this.stationList;
+    },
+    filteredStationList: function () {
+      var keyword = String(this.searchStationId || "").trim();
+      if (!keyword) {
+        return this.sourceList;
+      }
+      return this.sourceList.filter(function (item) {
+        return String(item.stationId) === keyword;
+      });
+    },
+    displayStationList: function () {
+      var start = (this.currentPage - 1) * this.pageSize;
+      return this.filteredStationList.slice(start, start + this.pageSize);
+    },
+    totalPages: function () {
+      return Math.max(1, Math.ceil(this.filteredStationList.length / this.pageSize) || 1);
     }
   },
   watch: {
-    param: {
-      handler(newVal, oldVal) {
-        if (newVal && newVal.stationId && newVal.stationId != 0) {
-          this.activeNames = newVal.stationId;
-          this.searchStationId = newVal.stationId;
-        }
-      },
-      deep: true, // 娣卞害鐩戝惉宓屽灞炴��
-      immediate: true, // 绔嬪嵆瑙﹀彂涓�娆★紙鍙�夛級
+    items: function () {
+      this.afterDataRefresh();
     },
+    param: {
+      deep: true,
+      immediate: true,
+      handler: function (newVal) {
+        if (newVal && newVal.stationId && newVal.stationId !== 0) {
+          this.focusStation(newVal.stationId);
+        }
+      }
+    }
+  },
+  created: function () {
+    MonitorCardKit.ensureStyles();
+    if (this.autoRefresh) {
+      this.timer = setInterval(this.getDevpStateInfo, 1000);
+    }
+  },
+  beforeDestroy: function () {
+    if (this.timer) {
+      clearInterval(this.timer);
+      this.timer = null;
+    }
   },
   methods: {
-    handlePageChange(page) {
+    orDash: function (value) {
+      return MonitorCardKit.orDash(value);
+    },
+    getStatusLabel: function (item) {
+      return item && item.autoing ? "鑷姩" : "鎵嬪姩";
+    },
+    getStatusTone: function (item) {
+      return MonitorCardKit.statusTone(this.getStatusLabel(item));
+    },
+    isActive: function (stationId) {
+      return String(this.activeNames) === String(stationId);
+    },
+    toggleItem: function (item) {
+      var next = String(item.stationId);
+      this.activeNames = this.activeNames === next ? "" : next;
+    },
+    focusStation: function (stationId) {
+      this.searchStationId = String(stationId);
+      var index = this.filteredStationList.findIndex(function (item) {
+        return String(item.stationId) === String(stationId);
+      });
+      this.currentPage = index >= 0 ? Math.floor(index / this.pageSize) + 1 : 1;
+      this.activeNames = String(stationId);
+    },
+    afterDataRefresh: function () {
+      if (this.currentPage > this.totalPages) {
+        this.currentPage = this.totalPages;
+      }
+      if (this.activeNames) {
+        var exists = this.filteredStationList.some(function (item) {
+          return String(item.stationId) === String(this.activeNames);
+        }, this);
+        if (!exists) {
+          this.activeNames = "";
+        }
+      }
+    },
+    handlePageChange: function (page) {
+      if (page < 1 || page > this.totalPages) {
+        return;
+      }
       this.currentPage = page;
     },
-    handleSizeChange(size) {
-      this.pageSize = size;
-      this.currentPage = 1;
+    getDevpStateInfo: function () {
+      if (this.$root && this.$root.sendWs) {
+        this.$root.sendWs(JSON.stringify({
+          url: "/console/latest/data/station",
+          data: {}
+        }));
+      }
     },
-    getBarcodePreview(barcode) {
-      const value = String(barcode || "").trim();
+    setStationList: function (res) {
+      if (res && res.code === 200) {
+        this.stationList = res.data || [];
+        this.afterDataRefresh();
+      }
+    },
+    openControl: function () {
+      this.showControl = !this.showControl;
+    },
+    buildDetailEntries: function (item) {
+      return [
+        { label: "缂栧彿", value: this.orDash(item.stationId) },
+        { label: "宸ヤ綔鍙�", value: this.orDash(item.taskNo) },
+        { label: "鐩爣绔�", value: this.orDash(item.targetStaNo) },
+        { label: "妯″紡", value: item.autoing ? "鑷姩" : "鎵嬪姩" },
+        { label: "鏈夌墿", value: MonitorCardKit.yesNo(item.loading) },
+        { label: "鍙叆", value: MonitorCardKit.yesNo(item.inEnable) },
+        { label: "鍙嚭", value: MonitorCardKit.yesNo(item.outEnable) },
+        { label: "绌烘澘淇″彿", value: MonitorCardKit.yesNo(item.emptyMk) },
+        { label: "婊℃澘淇″彿", value: MonitorCardKit.yesNo(item.fullPlt) },
+        { label: "杩愯闃诲", value: MonitorCardKit.yesNo(item.runBlock) },
+        { label: "鍚姩鍏ュ簱", value: MonitorCardKit.yesNo(item.enableIn) },
+        { label: "鎵樼洏楂樺害", value: this.orDash(item.palletHeight) },
+        { label: "鏉$爜", value: this.orDash(item.barcode), code: true, type: "barcode" },
+        { label: "閲嶉噺", value: this.orDash(item.weight) },
+        { label: "浠诲姟鍙啓鍖�", value: this.orDash(item.taskWriteIdx) },
+        { label: "鏁呴殰浠g爜", value: this.orDash(item.error) },
+        { label: "鏁呴殰淇℃伅", value: this.orDash(item.errorMsg) },
+        { label: "鎵╁睍鏁版嵁", value: this.orDash(item.extend) }
+      ];
+    },
+    postControl: function (url, payload) {
+      $.ajax({
+        url: baseUrl + url,
+        headers: {
+          token: localStorage.getItem("token")
+        },
+        contentType: "application/json",
+        method: "post",
+        data: JSON.stringify(payload),
+        success: function (res) {
+          if (res && res.code === 200) {
+            MonitorCardKit.showMessage(this, res.msg || "鎿嶄綔鎴愬姛", "success");
+          } else {
+            MonitorCardKit.showMessage(this, (res && res.msg) || "鎿嶄綔澶辫触", "warning");
+          }
+        }.bind(this)
+      });
+    },
+    handleBarcodeClick: function (item) {
+      if (this.readOnly || !item || item.stationId == null) {
+        return;
+      }
+      this.$prompt("璇疯緭鍏ユ柊鐨勬潯鐮佸�硷紙鍙暀绌烘竻绌猴級", "淇敼鏉$爜", {
+        confirmButtonText: "纭畾",
+        cancelButtonText: "鍙栨秷",
+        inputValue: item.barcode || "",
+        inputPlaceholder: "璇疯緭鍏ユ潯鐮�"
+      }).then(function (result) {
+        this.updateStationBarcode(item.stationId, result && result.value == null ? "" : String(result.value).trim());
+      }.bind(this)).catch(function () {});
+    },
+    updateStationBarcode: function (stationId, barcode) {
+      $.ajax({
+        url: baseUrl + "/station/command/barcode",
+        headers: {
+          token: localStorage.getItem("token")
+        },
+        contentType: "application/json",
+        method: "post",
+        data: JSON.stringify({
+          stationId: stationId,
+          barcode: barcode
+        }),
+        success: function (res) {
+          if (res && res.code === 200) {
+            this.syncLocalBarcode(stationId, barcode);
+            MonitorCardKit.showMessage(this, "鏉$爜淇敼鎴愬姛", "success");
+          } else {
+            MonitorCardKit.showMessage(this, (res && res.msg) || "鏉$爜淇敼澶辫触", "warning");
+          }
+        }.bind(this)
+      });
+    },
+    syncLocalBarcode: function (stationId, barcode) {
+      var update = function (list) {
+        if (!list || !list.length) {
+          return;
+        }
+        list.forEach(function (item) {
+          if (item.stationId == stationId) {
+            item.barcode = barcode;
+          }
+        });
+      };
+      update(this.stationList);
+      if (Array.isArray(this.items)) {
+        update(this.items);
+      }
+    },
+    getBarcodePreview: function (barcode) {
+      var value = String(barcode || "").trim();
       if (!value) {
         return "";
       }
       if (this.barcodePreviewCache[value]) {
         return this.barcodePreviewCache[value];
       }
-      const encodeResult = this.encodeCode128(value);
+      var encodeResult = this.encodeCode128(value);
       if (!encodeResult) {
         return "";
       }
-      const svg = this.buildCode128Svg(encodeResult, value);
-      const dataUrl = "data:image/svg+xml;charset=UTF-8," + encodeURIComponent(svg);
+      var svg = this.buildCode128Svg(encodeResult, value);
+      var dataUrl = "data:image/svg+xml;charset=UTF-8," + encodeURIComponent(svg);
       this.$set(this.barcodePreviewCache, value, dataUrl);
       return dataUrl;
     },
-    encodeCode128(value) {
+    encodeCode128: function (value) {
       if (!value) {
         return null;
       }
-      const isNumeric = /^\d+$/.test(value);
+      var isNumeric = /^\d+$/.test(value);
       if (isNumeric && value.length % 2 === 0) {
         return this.encodeCode128C(value);
       }
       return this.encodeCode128B(value);
     },
-    encodeCode128B(value) {
-      const codes = [104];
-      for (let i = 0; i < value.length; i++) {
-        const code = value.charCodeAt(i) - 32;
+    encodeCode128B: function (value) {
+      var codes = [104];
+      for (var i = 0; i < value.length; i++) {
+        var code = value.charCodeAt(i) - 32;
         if (code < 0 || code > 94) {
           return null;
         }
@@ -189,64 +355,62 @@
       }
       return this.buildCode128Pattern(codes);
     },
-    encodeCode128C(value) {
+    encodeCode128C: function (value) {
       if (value.length % 2 !== 0) {
         return null;
       }
-      const codes = [105];
-      for (let i = 0; i < value.length; i += 2) {
+      var codes = [105];
+      for (var i = 0; i < value.length; i += 2) {
         codes.push(parseInt(value.substring(i, i + 2), 10));
       }
       return this.buildCode128Pattern(codes);
     },
-    buildCode128Pattern(codes) {
-      const patterns = this.getCode128Patterns();
-      let checksum = codes[0];
-      for (let i = 1; i < codes.length; i++) {
+    buildCode128Pattern: function (codes) {
+      var patterns = this.getCode128Patterns();
+      var checksum = codes[0];
+      for (var i = 1; i < codes.length; i++) {
         checksum += codes[i] * i;
       }
-      const checkCode = checksum % 103;
-      const fullCodes = codes.concat([checkCode, 106]);
-      let bars = "";
-      for (let i = 0; i < fullCodes.length; i++) {
-        const code = fullCodes[i];
-        if (patterns[code] == null) {
+      var checkCode = checksum % 103;
+      var fullCodes = codes.concat([checkCode, 106]);
+      var bars = "";
+      for (var j = 0; j < fullCodes.length; j++) {
+        if (patterns[fullCodes[j]] == null) {
           return null;
         }
-        bars += patterns[code];
+        bars += patterns[fullCodes[j]];
       }
       bars += "11";
       return bars;
     },
-    buildCode128Svg(bars, text) {
-      const quietModules = 20;
-      const modules = quietModules * 2 + bars.split("").reduce((sum, n) => sum + parseInt(n, 10), 0);
-      const moduleWidth = modules > 300 ? 1 : 2;
-      const width = modules * moduleWidth;
-      const barTop = 10;
-      const barHeight = 110;
-      let x = quietModules * moduleWidth;
-      let black = true;
-      let rects = "";
-      for (let i = 0; i < bars.length; i++) {
-        const w = parseInt(bars[i], 10) * moduleWidth;
+    buildCode128Svg: function (bars, text) {
+      var quietModules = 20;
+      var modules = quietModules * 2 + bars.split("").reduce(function (sum, n) {
+        return sum + parseInt(n, 10);
+      }, 0);
+      var moduleWidth = modules > 300 ? 1 : 2;
+      var width = modules * moduleWidth;
+      var barTop = 10;
+      var barHeight = 110;
+      var x = quietModules * moduleWidth;
+      var black = true;
+      var rects = "";
+      for (var i = 0; i < bars.length; i++) {
+        var w = parseInt(bars[i], 10) * moduleWidth;
         if (black) {
           rects += '<rect x="' + x + '" y="' + barTop + '" width="' + w + '" height="' + barHeight + '" fill="#000" shape-rendering="crispEdges" />';
         }
         x += w;
         black = !black;
       }
-      return (
-        '<svg xmlns="http://www.w3.org/2000/svg" width="' + width + '" height="145" viewBox="0 0 ' + width + ' 145">' +
+      return '<svg xmlns="http://www.w3.org/2000/svg" width="' + width + '" height="145" viewBox="0 0 ' + width + ' 145">' +
         '<rect width="100%" height="100%" fill="#fff" />' +
         rects +
         '<text x="' + (width / 2) + '" y="136" text-anchor="middle" font-family="monospace" font-size="14" fill="#111">' +
         this.escapeXml(text) +
-        "</text>" +
-        "</svg>"
-      );
+        "</text></svg>";
     },
-    getCode128Patterns() {
+    getCode128Patterns: function () {
       return [
         "212222", "222122", "222221", "121223", "121322", "131222", "122213", "122312", "132212", "221213",
         "221312", "231212", "112232", "122132", "122231", "113222", "123122", "123221", "223211", "221132",
@@ -261,7 +425,7 @@
         "114131", "311141", "411131", "211412", "211214", "211232", "2331112"
       ];
     },
-    escapeXml(text) {
+    escapeXml: function (text) {
       return String(text)
         .replace(/&/g, "&amp;")
         .replace(/</g, "&lt;")
@@ -269,170 +433,11 @@
         .replace(/"/g, "&quot;")
         .replace(/'/g, "&apos;");
     },
-    getDevpStateInfo() {
-      if (this.readOnly) {
-          // Frontend filtering for readOnly mode
-          if (this.searchStationId == "") {
-              this.stationList = this.fullStationList;
-          } else {
-              this.stationList = this.fullStationList.filter(item => item.stationId == this.searchStationId);
-              this.currentPage = 1;
-          }
-      } else if (this.$root.sendWs) {
-        this.$root.sendWs(JSON.stringify({
-          "url": "/console/latest/data/station",
-          "data": {}
-        }));
-      }
+    controlCommand: function () {
+      this.postControl("/station/command/move", this.controlParam);
     },
-    setStationList(res) {
-      let that = this;
-      if (res.code == 200) {
-        let list = res.data;
-        that.fullStationList = list;
-        if (that.searchStationId == "") {
-          that.stationList = list;
-        } else {
-          let tmp = [];
-          list.forEach((item) => {
-            if (item.stationId == that.searchStationId) {
-              tmp.push(item);
-            }
-          });
-          that.stationList = tmp;
-          that.currentPage = 1;
-        }
-      }
-    },
-    handleBarcodeClick(item) {
-      if (this.readOnly || !item || item.stationId == null) {
-        return;
-      }
-
-      let that = this;
-      $.ajax({
-        url: baseUrl + "/openapi/getFakeSystemRunStatus",
-        headers: {
-          token: localStorage.getItem("token"),
-        },
-        method: "get",
-        success: (res) => {
-          if (res.code !== 200 || !res.data || !res.data.isFake || !res.data.running) {
-            that.$message({
-              message: "浠呬豢鐪熸ā寮忚繍琛屼腑鍙慨鏀规潯鐮�",
-              type: "warning",
-            });
-            return;
-          }
-
-          that.$prompt("璇疯緭鍏ユ柊鐨勬潯鐮佸�硷紙鍙暀绌烘竻绌猴級", "淇敼鏉$爜", {
-            confirmButtonText: "纭畾",
-            cancelButtonText: "鍙栨秷",
-            inputValue: item.barcode || "",
-            inputPlaceholder: "璇疯緭鍏ユ潯鐮�",
-          }).then(({ value }) => {
-            that.updateStationBarcode(item.stationId, value == null ? "" : String(value).trim());
-          }).catch(() => {});
-        },
-      });
-    },
-    updateStationBarcode(stationId, barcode) {
-      let that = this;
-      $.ajax({
-        url: baseUrl + "/station/command/barcode",
-        headers: {
-          token: localStorage.getItem("token"),
-        },
-        contentType: "application/json",
-        method: "post",
-        data: JSON.stringify({
-          stationId: stationId,
-          barcode: barcode,
-        }),
-        success: (res) => {
-          if (res.code == 200) {
-            that.syncLocalBarcode(stationId, barcode);
-            that.$message({
-              message: "鏉$爜淇敼鎴愬姛",
-              type: "success",
-            });
-          } else {
-            that.$message({
-              message: res.msg || "鏉$爜淇敼澶辫触",
-              type: "warning",
-            });
-          }
-        },
-      });
-    },
-    syncLocalBarcode(stationId, barcode) {
-      let updateFn = (list) => {
-        if (!list || list.length === 0) {
-          return;
-        }
-        list.forEach((row) => {
-          if (row.stationId == stationId) {
-            row.barcode = barcode;
-          }
-        });
-      };
-      updateFn(this.stationList);
-      updateFn(this.fullStationList);
-    },
-    openControl() {
-      this.showControl = !this.showControl;
-    },
-    controlCommand() {
-      let that = this;
-      //涓嬪彂鍛戒护
-      $.ajax({
-        url: baseUrl + "/station/command/move",
-        headers: {
-          token: localStorage.getItem("token"),
-        },
-        contentType: "application/json",
-        method: "post",
-        data: JSON.stringify(that.controlParam),
-        success: (res) => {
-          if (res.code == 200) {
-            that.$message({
-              message: res.msg,
-              type: "success",
-            });
-          } else {
-            that.$message({
-              message: res.msg,
-              type: "warning",
-            });
-          }
-        },
-      });
-    },
-    resetCommand() {
-      let that = this;
-      //涓嬪彂鍛戒护
-      $.ajax({
-        url: baseUrl + "/station/command/reset",
-        headers: {
-          token: localStorage.getItem("token"),
-        },
-        contentType: "application/json",
-        method: "post",
-        data: JSON.stringify(that.controlParam),
-        success: (res) => {
-          if (res.code == 200) {
-            that.$message({
-              message: res.msg,
-              type: "success",
-            });
-          } else {
-            that.$message({
-              message: res.msg,
-              type: "warning",
-            });
-          }
-        },
-      });
-    },
-  },
+    resetCommand: function () {
+      this.postControl("/station/command/reset", this.controlParam);
+    }
+  }
 });
diff --git a/src/main/webapp/components/MapCanvas.js b/src/main/webapp/components/MapCanvas.js
index 276827b..05d703b 100644
--- a/src/main/webapp/components/MapCanvas.js
+++ b/src/main/webapp/components/MapCanvas.js
@@ -2,7 +2,7 @@
   template: `
     <div style="width: 100%; height: 100%; position: relative;">
       <div ref="pixiView" style="position: absolute; inset: 0;"></div>
-      <div style="position: absolute; top: 12px; left: 14px; z-index: 30; pointer-events: none; max-width: 52%;">
+      <div :style="cycleCapacityPanelStyle()">
         <div style="display: flex; flex-direction: column; gap: 6px; align-items: flex-start;">
           <div v-for="item in cycleCapacity.loopList"
                :key="'loop-' + item.loopNo"
@@ -24,14 +24,31 @@
         <div :style="mapToolFpsStyle()">FPS {{ mapFps }}</div>
         <button type="button" @click="toggleMapToolPanel" :style="mapToolToggleStyle(showMapToolPanel)">{{ showMapToolPanel ? '鏀惰捣鎿嶄綔' : '鍦板浘鎿嶄綔' }}</button>
         <div v-show="showMapToolPanel" :style="mapToolBarStyle()">
-          <button type="button" @click="toggleStationDirection" :style="mapToolButtonStyle(showStationDirection)">{{ showStationDirection ? '闅愯棌绔欑偣鏂瑰悜' : '鏄剧ず绔欑偣鏂瑰悜' }}</button>
-          <button type="button" @click="rotateMap" :style="mapToolButtonStyle(false)">鏃嬭浆</button>
-          <button type="button" @click="toggleMirror" :style="mapToolButtonStyle(mapMirrorX)">{{ mapMirrorX ? '鍙栨秷闀滃儚' : '闀滃儚' }}</button>
+          <div :style="mapToolRowStyle()">
+            <button type="button" @click="toggleStationDirection" :style="mapToolButtonStyle(showStationDirection)">{{ showStationDirection ? '闅愯棌绔欑偣鏂瑰悜' : '鏄剧ず绔欑偣鏂瑰悜' }}</button>
+            <button type="button" @click="rotateMap" :style="mapToolButtonStyle(false)">鏃嬭浆</button>
+            <button type="button" @click="toggleMirror" :style="mapToolButtonStyle(mapMirrorX)">{{ mapMirrorX ? '鍙栨秷闀滃儚' : '闀滃儚' }}</button>
+          </div>
+          <div :style="mapToolRowStyle()">
+            <button type="button" @click="openStationColorConfigPage" :style="mapToolButtonStyle(false)">绔欑偣棰滆壊</button>
+          </div>
+          <div v-if="levList && levList.length > 1" :style="mapToolFloorSectionStyle()">
+            <div :style="mapToolSectionLabelStyle()">妤煎眰</div>
+            <div :style="mapToolFloorListStyle()">
+              <button
+                v-for="floor in levList"
+                :key="'tool-floor-' + floor"
+                type="button"
+                @click="selectFloorFromTool(floor)"
+                :style="mapToolFloorButtonStyle(currentLev == floor)"
+              >{{ floor }}F</button>
+            </div>
+          </div>
         </div>
       </div>
     </div>
   `,
-  props: ['lev', 'crnParam', 'rgvParam', 'devpParam', 'highlightOnParamChange'],
+  props: ['lev', 'levList', 'crnParam', 'rgvParam', 'devpParam', 'stationTaskRange', 'highlightOnParamChange', 'viewportPadding', 'hudPadding'],
   data() {
     return {
       map: [],
@@ -63,6 +80,10 @@
       pixiDevpTextureMap: new Map(),
       pixiCrnColorTextureMap: new Map(),
       pixiRgvColorTextureMap: new Map(),
+      shelfChunkList: [],
+      shelfChunkSize: 2048,
+      shelfCullPadding: 160,
+      shelfCullRaf: null,
       crnList: [],
       dualCrnList: [],
       rgvList: [],
@@ -111,7 +132,18 @@
       hoverLoopNo: null,
       hoverLoopStationIdSet: new Set(),
       loopHighlightColor: 0xfff34d,
-      stationDirectionColor: 0xff5a36
+      stationDirectionColor: 0xff5a36,
+      stationStatusColors: {
+        'site-auto': 0x78ff81,
+        'site-auto-run': 0xfa51f6,
+        'site-auto-id': 0xc4c400,
+        'site-auto-run-id': 0x30bffc,
+        'site-enable-in': 0x18c7b8,
+        'site-unauto': 0xb8b8b8,
+        'machine-pakin': 0x30bffc,
+        'machine-pakout': 0x97b400,
+        'site-run-block': 0xe69138
+      }
     }
   },
     mounted() {
@@ -119,6 +151,7 @@
     this.createMap();
     this.startContainerResizeObserve();
     this.loadMapTransformConfig();
+    this.loadStationColorConfig();
     this.loadLocList();
     this.connectWs();
     
@@ -138,6 +171,8 @@
     if (this.timer) { clearInterval(this.timer); }
 
     if (this.hoverRaf) { cancelAnimationFrame(this.hoverRaf); this.hoverRaf = null; }
+    if (this.shelfCullRaf) { cancelAnimationFrame(this.shelfCullRaf); this.shelfCullRaf = null; }
+    if (window.gsap && this.pixiApp && this.pixiApp.stage) { window.gsap.killTweensOf(this.pixiApp.stage.position); }
     if (this.pixiApp) { this.pixiApp.destroy(true, { children: true }); }
     if (this.containerResizeObserver) { this.containerResizeObserver.disconnect(); this.containerResizeObserver = null; }
     window.removeEventListener('resize', this.resizeToContainer);
@@ -147,6 +182,14 @@
   watch: {
     lev(newLev) {
       if (newLev != null) { this.changeFloor(newLev); }
+    },
+    viewportPadding: {
+      deep: true,
+      handler(newVal, oldVal) {
+        if (this.mapContentSize && this.mapContentSize.width > 0 && this.mapContentSize.height > 0) {
+          this.adjustStageForViewportPadding(oldVal, newVal);
+        }
+      }
     },
     crnParam: {
       deep: true,
@@ -192,17 +235,65 @@
     }
   },
   methods: {
+    cycleCapacityPanelStyle() {
+      const hud = this.hudPadding || {};
+      const left = Math.max(14, Number(hud.left) || 0);
+      const rightReserve = 220;
+      return {
+        position: 'absolute',
+        top: '12px',
+        left: left + 'px',
+        zIndex: 30,
+        pointerEvents: 'none',
+        maxWidth: 'calc(100% - ' + (left + rightReserve) + 'px)'
+      };
+    },
     mapToolBarStyle() {
       return {
         display: 'flex',
+        flexDirection: 'column',
         gap: '8px',
-        alignItems: 'center',
+        alignItems: 'stretch',
         padding: '7px',
         borderRadius: '14px',
         background: 'rgba(255, 255, 255, 0.72)',
         border: '1px solid rgba(160, 180, 205, 0.3)',
         boxShadow: '0 8px 20px rgba(37, 64, 97, 0.08)',
         backdropFilter: 'blur(4px)'
+      };
+    },
+    mapToolRowStyle() {
+      return {
+        display: 'flex',
+        gap: '8px',
+        alignItems: 'center',
+        justifyContent: 'flex-end',
+        flexWrap: 'wrap'
+      };
+    },
+    mapToolFloorSectionStyle() {
+      return {
+        display: 'flex',
+        flexDirection: 'column',
+        gap: '4px',
+        paddingTop: '6px',
+        borderTop: '1px solid rgba(160, 180, 205, 0.22)'
+      };
+    },
+    mapToolSectionLabelStyle() {
+      return {
+        color: '#6a7f95',
+        fontSize: '10px',
+        lineHeight: '14px',
+        textAlign: 'right'
+      };
+    },
+    mapToolFloorListStyle() {
+      return {
+        display: 'flex',
+        flexDirection: 'column',
+        gap: '4px',
+        alignItems: 'stretch'
       };
     },
     mapToolFpsStyle() {
@@ -252,8 +343,30 @@
         whiteSpace: 'nowrap'
       };
     },
+    mapToolFloorButtonStyle(active) {
+      return {
+        appearance: 'none',
+        border: '1px solid ' + (active ? 'rgba(96, 132, 170, 0.36)' : 'rgba(160, 180, 205, 0.3)'),
+        background: active ? 'rgba(235, 243, 251, 0.96)' : 'rgba(255, 255, 255, 0.88)',
+        color: active ? '#27425c' : '#4d647d',
+        minWidth: '44px',
+        height: '26px',
+        padding: '0 10px',
+        borderRadius: '8px',
+        fontSize: '11px',
+        lineHeight: '26px',
+        cursor: 'pointer',
+        fontWeight: '700',
+        boxShadow: active ? '0 4px 12px rgba(37, 64, 97, 0.08)' : 'none',
+        whiteSpace: 'nowrap'
+      };
+    },
     toggleMapToolPanel() {
       this.showMapToolPanel = !this.showMapToolPanel;
+    },
+    selectFloorFromTool(lev) {
+      if (lev == null || lev === this.currentLev) { return; }
+      this.$emit('switch-lev', lev);
     },
     createMap() {
       this.pixiApp = new PIXI.Application({ backgroundColor: 0xF5F7F9, antialias: false, powerPreference: 'high-performance', autoDensity: true, resolution: Math.min(window.devicePixelRatio || 1, 2) });
@@ -270,9 +383,8 @@
       this.objectsContainer2 = new PIXI.Container();
       this.tracksContainer = new PIXI.ParticleContainer(10000, { scale: true, position: true, rotation: false, uvs: false, alpha: false });
       this.tracksGraphics = new PIXI.Graphics();
-      this.shelvesContainer = new PIXI.ParticleContainer(10000, { scale: true, position: true, rotation: false, uvs: false, alpha: false });
+      this.shelvesContainer = new PIXI.Container();
       this.tracksContainer.autoResize = true;
-      this.shelvesContainer.autoResize = true;
       this.mapRoot = new PIXI.Container();
       this.pixiApp.stage.addChild(this.mapRoot);
       this.mapRoot.addChild(this.tracksGraphics);
@@ -320,6 +432,7 @@
           const dx = globalPos.x - mouseDownPoint[0];
           const dy = globalPos.y - mouseDownPoint[1];
           this.pixiApp.stage.position.set(stageOriginalPos[0] + dx, stageOriginalPos[1] + dy);
+          this.scheduleShelfChunkCulling();
         }
       });
       this.pixiApp.renderer.plugins.interaction.on('pointerup', () => { touchBlank = false; });
@@ -345,7 +458,8 @@
         const newPosX = sx - worldX * newZoomX;
         const newPosY = sy - worldY * newZoomY;
         this.pixiApp.stage.setTransform(newPosX, newPosY, newZoomX, newZoomY, 0, 0, 0, 0, 0);
-          this.scheduleAdjustLabels();
+        this.scheduleAdjustLabels();
+        this.scheduleShelfChunkCulling();
       });
       //*******************缂╂斁鐢诲竷*******************
 
@@ -388,6 +502,67 @@
       const rect = this.pixiApp.view ? this.pixiApp.view.getBoundingClientRect() : null;
       return { width: rect ? rect.width : 0, height: rect ? rect.height : 0 };
     },
+    getViewportPadding() {
+      return this.normalizeViewportPadding(this.viewportPadding);
+    },
+    normalizeViewportPadding(padding) {
+      const source = padding || {};
+      const normalize = (value) => {
+        const num = Number(value);
+        return isFinite(num) && num > 0 ? num : 0;
+      };
+      return {
+        top: normalize(source.top),
+        right: normalize(source.right),
+        bottom: normalize(source.bottom),
+        left: normalize(source.left)
+      };
+    },
+    getViewportCenter(viewport, padding) {
+      const normalized = this.normalizeViewportPadding(padding);
+      const availableW = Math.max(1, viewport.width - normalized.left - normalized.right);
+      const availableH = Math.max(1, viewport.height - normalized.top - normalized.bottom);
+      return {
+        x: normalized.left + availableW / 2,
+        y: normalized.top + availableH / 2
+      };
+    },
+    adjustStageForViewportPadding(oldPadding, newPadding) {
+      if (!this.pixiApp || !this.pixiApp.stage) { return; }
+      const viewport = this.getViewportSize();
+      if (viewport.width <= 0 || viewport.height <= 0) { return; }
+      const prevCenter = this.getViewportCenter(viewport, oldPadding);
+      const nextCenter = this.getViewportCenter(viewport, newPadding);
+      const deltaX = nextCenter.x - prevCenter.x;
+      const deltaY = nextCenter.y - prevCenter.y;
+      if (Math.abs(deltaX) < 0.5 && Math.abs(deltaY) < 0.5) {
+        return;
+      }
+      const targetX = this.pixiApp.stage.position.x + deltaX;
+      const targetY = this.pixiApp.stage.position.y + deltaY;
+      if (window.gsap) {
+        window.gsap.killTweensOf(this.pixiApp.stage.position);
+        window.gsap.to(this.pixiApp.stage.position, {
+          x: targetX,
+          y: targetY,
+          duration: 0.18,
+          ease: 'power1.out',
+          onUpdate: () => {
+            this.scheduleAdjustLabels();
+            this.scheduleShelfChunkCulling();
+          },
+          onComplete: () => {
+            this.scheduleAdjustLabels();
+            this.scheduleShelfChunkCulling();
+          }
+        });
+        return;
+      }
+      this.pixiApp.stage.position.x = targetX;
+      this.pixiApp.stage.position.y = targetY;
+      this.scheduleAdjustLabels();
+      this.scheduleShelfChunkCulling();
+    },
     resizeToContainer() {
       const w = this.$el.clientWidth || 0;
       const h = this.$el.clientHeight || 0;
@@ -419,7 +594,7 @@
       this.objectsContainer2.removeChildren();
       if (this.tracksContainer) { this.tracksContainer.removeChildren(); }
       if (this.tracksGraphics) { this.tracksGraphics.clear(); }
-      if (this.shelvesContainer) { this.shelvesContainer.removeChildren(); }
+      this.clearShelfChunks();
       this.crnList = [];
       this.dualCrnList = [];
       this.rgvList = [];
@@ -448,7 +623,7 @@
       this.objectsContainer2.removeChildren();
       if (this.tracksContainer) { this.tracksContainer.removeChildren(); }
       if (this.tracksGraphics) { this.tracksGraphics.clear(); }
-      if (this.shelvesContainer) { this.shelvesContainer.removeChildren(); }
+      this.clearShelfChunks();
       this.crnList = [];
       this.dualCrnList = [];
       this.rgvList = [];
@@ -573,15 +748,12 @@
             this.collectTrackItem(val);
             continue;
           }
+          if (val.type === 'shelf') { continue; }
           let sprite = this.getSprite(val, (e) => {
             //鍥炶皟
           });
           if (sprite == null) { continue; }
-          if (sprite._kind === 'shelf') {
-            this.shelvesContainer.addChild(sprite);
-          } else {
-            this.objectsContainer.addChild(sprite);
-          }
+          this.objectsContainer.addChild(sprite);
           this.pixiStageList[index][idx] = sprite;
         }
       });
@@ -698,6 +870,7 @@
         }
       }
       this.mapContentSize = { width: contentW, height: contentH };
+      this.buildShelfChunks(map, contentW, contentH);
       this.applyMapTransform(true);
       this.map = map;
       this.isSwitchingFloor = false;
@@ -743,7 +916,6 @@
       if (!sites) { return; }
       sites.forEach((item) => {
         let id = item.siteId != null ? item.siteId : item.stationId;
-        let status = item.siteStatus != null ? item.siteStatus : item.stationStatus;
         let workNo = item.workNo != null ? item.workNo : item.taskNo;
         if (id == null) { return; }
         let sta = this.pixiStaMap.get(parseInt(id));
@@ -754,7 +926,7 @@
           sta.statusObj = null;
           if (sta.textObj.parent !== sta) { sta.addChild(sta.textObj); sta.textObj.position.set(sta.width / 2, sta.height / 2); }
         }
-        this.setStationBaseColor(sta, this.getStationStatusColor(status));
+        this.setStationBaseColor(sta, this.getStationStatusColor(this.resolveStationStatus(item)));
       });
     },
     getCrnInfo() {
@@ -1285,15 +1457,47 @@
       return brightness > 150 ? '#000000' : '#ffffff';
     },
     getStationStatusColor(status) {
-      if (status === "site-auto") { return 0x78ff81; }
-      if (status === "site-auto-run") { return 0xfa51f6; }
-      if (status === "site-auto-id") { return 0xc4c400; }
-      if (status === "site-auto-run-id") { return 0x30bffc; }
-      if (status === "site-unauto") { return 0xb8b8b8; }
-      if (status === "machine-pakin") { return 0x30bffc; }
-      if (status === "machine-pakout") { return 0x97b400; }
-      if (status === "site-run-block") { return 0xe69138; }
-      return 0xb8b8b8;
+      const colorMap = this.stationStatusColors || this.getDefaultStationStatusColors();
+      if (status && colorMap[status] != null) { return colorMap[status]; }
+      return colorMap['site-unauto'] != null ? colorMap['site-unauto'] : 0xb8b8b8;
+    },
+    resolveStationStatus(item) {
+      const status = item && (item.siteStatus != null ? item.siteStatus : item.stationStatus);
+      const taskNo = this.parseStationTaskNo(item && (item.workNo != null ? item.workNo : item.taskNo));
+      const autoing = !!(item && item.autoing);
+      const loading = !!(item && item.loading);
+      const runBlock = !!(item && item.runBlock);
+      const enableIn = !!(item && item.enableIn);
+      if (taskNo === 9998 || enableIn) { return 'site-enable-in'; }
+      if (autoing && loading && taskNo > 0 && !runBlock) {
+        const taskClass = this.getStationTaskClass(taskNo);
+        if (taskClass) { return taskClass; }
+      }
+      if (status) { return status; }
+      if (autoing && loading && taskNo > 0 && runBlock) { return 'site-run-block'; }
+      if (autoing && loading && taskNo > 0) { return 'site-auto-run-id'; }
+      if (autoing && loading) { return 'site-auto-run'; }
+      if (autoing && taskNo > 0) { return 'site-auto-id'; }
+      if (autoing) { return 'site-auto'; }
+      return 'site-unauto';
+    },
+    parseStationTaskNo(value) {
+      const taskNo = parseInt(value, 10);
+      return isNaN(taskNo) ? 0 : taskNo;
+    },
+    getStationTaskClass(taskNo) {
+      if (!(taskNo > 0)) { return null; }
+      const range = this.stationTaskRange || {};
+      if (this.isTaskNoInRange(taskNo, range.inbound)) { return 'machine-pakin'; }
+      if (this.isTaskNoInRange(taskNo, range.outbound)) { return 'machine-pakout'; }
+      return null;
+    },
+    isTaskNoInRange(taskNo, range) {
+      if (!range) { return false; }
+      const start = parseInt(range.start, 10);
+      const end = parseInt(range.end, 10);
+      if (isNaN(start) || isNaN(end)) { return false; }
+      return taskNo >= start && taskNo <= end;
     },
     getCrnStatusColor(status) {
       if (status === "machine-auto") { return 0x21BA45; }
@@ -1837,6 +2041,135 @@
         }
       }
     },
+    clearShelfChunks() {
+      if (this.shelfCullRaf) {
+        cancelAnimationFrame(this.shelfCullRaf);
+        this.shelfCullRaf = null;
+      }
+      this.shelfChunkList = [];
+      if (!this.shelvesContainer) { return; }
+      const children = this.shelvesContainer.removeChildren();
+      children.forEach((child) => {
+        if (child && typeof child.destroy === 'function') {
+          child.destroy({ children: true, texture: true, baseTexture: true });
+        }
+      });
+    },
+    buildShelfChunks(map, contentW, contentH) {
+      this.clearShelfChunks();
+      if (!this.pixiApp || !this.pixiApp.renderer || !this.shelvesContainer || !Array.isArray(map)) { return; }
+      const chunkSize = Math.max(256, parseInt(this.shelfChunkSize, 10) || 2048);
+      const chunkMap = new Map();
+      for (let r = 0; r < map.length; r++) {
+        const row = map[r];
+        if (!row) { continue; }
+        for (let c = 0; c < row.length; c++) {
+          const cell = row[c];
+          if (!cell || cell.type !== 'shelf' || cell.type === 'merge') { continue; }
+          const startChunkX = Math.floor(cell.posX / chunkSize);
+          const endChunkX = Math.floor((cell.posX + Math.max(1, cell.width) - 0.01) / chunkSize);
+          const startChunkY = Math.floor(cell.posY / chunkSize);
+          const endChunkY = Math.floor((cell.posY + Math.max(1, cell.height) - 0.01) / chunkSize);
+          for (let chunkY = startChunkY; chunkY <= endChunkY; chunkY++) {
+            for (let chunkX = startChunkX; chunkX <= endChunkX; chunkX++) {
+              const key = chunkX + ',' + chunkY;
+              let list = chunkMap.get(key);
+              if (!list) {
+                list = [];
+                chunkMap.set(key, list);
+              }
+              list.push(cell);
+            }
+          }
+        }
+      }
+
+      const chunkList = [];
+      chunkMap.forEach((cells, key) => {
+        const keyParts = key.split(',');
+        const chunkX = parseInt(keyParts[0], 10) || 0;
+        const chunkY = parseInt(keyParts[1], 10) || 0;
+        const chunkLeft = chunkX * chunkSize;
+        const chunkTop = chunkY * chunkSize;
+        const chunkWidth = Math.max(1, Math.min(chunkSize, contentW - chunkLeft));
+        const chunkHeight = Math.max(1, Math.min(chunkSize, contentH - chunkTop));
+        const graphics = new PIXI.Graphics();
+        graphics.beginFill(0xb6e2e2);
+        graphics.lineStyle(1, 0xffffff, 1);
+        for (let i = 0; i < cells.length; i++) {
+          const cell = cells[i];
+          graphics.drawRect(cell.posX - chunkLeft, cell.posY - chunkTop, cell.width, cell.height);
+        }
+        graphics.endFill();
+        const texture = this.pixiApp.renderer.generateTexture(
+          graphics,
+          PIXI.SCALE_MODES.LINEAR,
+          1,
+          new PIXI.Rectangle(0, 0, chunkWidth, chunkHeight)
+        );
+        graphics.destroy(true);
+        const sprite = new PIXI.Sprite(texture);
+        sprite.position.set(chunkLeft, chunkTop);
+        sprite._chunkBounds = {
+          x: chunkLeft,
+          y: chunkTop,
+          width: chunkWidth,
+          height: chunkHeight
+        };
+        this.shelvesContainer.addChild(sprite);
+        chunkList.push(sprite);
+      });
+      this.shelfChunkList = chunkList;
+      this.updateVisibleShelfChunks();
+    },
+    getViewportLocalBounds(padding) {
+      if (!this.mapRoot || !this.pixiApp) { return null; }
+      const viewport = this.getViewportSize();
+      const pad = Math.max(0, Number(padding) || 0);
+      const points = [
+        new PIXI.Point(-pad, -pad),
+        new PIXI.Point(viewport.width + pad, -pad),
+        new PIXI.Point(-pad, viewport.height + pad),
+        new PIXI.Point(viewport.width + pad, viewport.height + pad)
+      ];
+      let minX = Infinity;
+      let minY = Infinity;
+      let maxX = -Infinity;
+      let maxY = -Infinity;
+      points.forEach((point) => {
+        const local = this.mapRoot.toLocal(point);
+        if (local.x < minX) { minX = local.x; }
+        if (local.y < minY) { minY = local.y; }
+        if (local.x > maxX) { maxX = local.x; }
+        if (local.y > maxY) { maxY = local.y; }
+      });
+      if (!isFinite(minX) || !isFinite(minY) || !isFinite(maxX) || !isFinite(maxY)) { return null; }
+      return { minX: minX, minY: minY, maxX: maxX, maxY: maxY };
+    },
+    updateVisibleShelfChunks() {
+      if (!this.shelfChunkList || this.shelfChunkList.length === 0) { return; }
+      const localBounds = this.getViewportLocalBounds(this.shelfCullPadding);
+      if (!localBounds) { return; }
+      for (let i = 0; i < this.shelfChunkList.length; i++) {
+        const sprite = this.shelfChunkList[i];
+        const bounds = sprite && sprite._chunkBounds;
+        if (!bounds) { continue; }
+        const visible = bounds.x < localBounds.maxX &&
+          bounds.x + bounds.width > localBounds.minX &&
+          bounds.y < localBounds.maxY &&
+          bounds.y + bounds.height > localBounds.minY;
+        if (sprite.visible !== visible) {
+          sprite.visible = visible;
+        }
+      }
+    },
+    scheduleShelfChunkCulling() {
+      if (this.shelfCullRaf) { return; }
+      this.shelfCullRaf = requestAnimationFrame(() => {
+        this.shelfCullRaf = null;
+        this.updateVisibleShelfChunks();
+      });
+    },
     findIndexByOffsets(offsets, sizes, value) {
       if (!offsets || !sizes || offsets.length === 0) { return -1; }
       for (let i = 0; i < offsets.length; i++) {
@@ -2084,6 +2417,23 @@
       this.applyMapTransform(true);
       this.saveMapTransformConfig();
     },
+    openStationColorConfigPage() {
+      if (typeof window === 'undefined') { return; }
+      const url = (typeof baseUrl !== 'undefined' ? baseUrl : '') + '/views/watch/stationColorConfig.html';
+      const layerInstance = (window.top && window.top.layer) || window.layer;
+      if (layerInstance && typeof layerInstance.open === 'function') {
+        layerInstance.open({
+          type: 2,
+          title: '绔欑偣棰滆壊閰嶇疆',
+          maxmin: true,
+          area: ['980px', '760px'],
+          shadeClose: false,
+          content: url
+        });
+        return;
+      }
+      window.open(url, '_blank');
+    },
     parseRotation(value) {
       const num = parseInt(value, 10);
       if (!isFinite(num)) { return 0; }
@@ -2095,6 +2445,98 @@
       if (value == null) { return false; }
       const str = String(value).toLowerCase();
       return str === '1' || str === 'true' || str === 'y';
+    },
+    getDefaultStationStatusColors() {
+      return {
+        'site-auto': 0x78ff81,
+        'site-auto-run': 0xfa51f6,
+        'site-auto-id': 0xc4c400,
+        'site-auto-run-id': 0x30bffc,
+        'site-enable-in': 0x18c7b8,
+        'site-unauto': 0xb8b8b8,
+        'machine-pakin': 0x30bffc,
+        'machine-pakout': 0x97b400,
+        'site-run-block': 0xe69138
+      };
+    },
+    parseColorConfigValue(value, fallback) {
+      if (typeof value === 'number' && isFinite(value)) {
+        return value;
+      }
+      const str = String(value == null ? '' : value).trim();
+      if (!str) { return fallback; }
+      if (/^#[0-9a-fA-F]{6}$/.test(str)) { return parseInt(str.slice(1), 16); }
+      if (/^#[0-9a-fA-F]{3}$/.test(str)) {
+        const expanded = str.charAt(1) + str.charAt(1) + str.charAt(2) + str.charAt(2) + str.charAt(3) + str.charAt(3);
+        return parseInt(expanded, 16);
+      }
+      if (/^0x[0-9a-fA-F]{6}$/i.test(str)) { return parseInt(str.slice(2), 16); }
+      if (/^[0-9]+$/.test(str)) {
+        const num = parseInt(str, 10);
+        return isNaN(num) ? fallback : num;
+      }
+      return fallback;
+    },
+    loadStationColorConfig() {
+      if (!window.$ || typeof baseUrl === 'undefined') { return; }
+      $.ajax({
+        url: baseUrl + "/watch/stationColor/config/auth",
+        headers: { 'token': localStorage.getItem('token') },
+        dataType: 'json',
+        method: 'GET',
+        success: (res) => {
+          if (!res || res.code !== 200 || !res.data) {
+            if (res && res.code === 403) { parent.location.href = baseUrl + "/login"; }
+            return;
+          }
+          this.applyStationColorConfigPayload(res.data);
+        }
+      });
+    },
+    applyStationColorConfigPayload(data) {
+      const defaults = this.getDefaultStationStatusColors();
+      const nextColors = Object.assign({}, defaults);
+      const items = Array.isArray(data.items) ? data.items : [];
+      items.forEach((item) => {
+        if (!item || !item.status || defaults[item.status] == null) { return; }
+        nextColors[item.status] = this.parseColorConfigValue(item.color, defaults[item.status]);
+      });
+      this.stationStatusColors = nextColors;
+    },
+    buildMissingMapConfigList(byCode) {
+      const createList = [];
+      if (!byCode[this.mapConfigCodes.rotate]) {
+        createList.push({
+          name: '鍦板浘鏃嬭浆',
+          code: this.mapConfigCodes.rotate,
+          value: String(this.mapRotation || 0),
+          type: 1,
+          status: 1,
+          selectType: 'map'
+        });
+      }
+      if (!byCode[this.mapConfigCodes.mirror]) {
+        createList.push({
+          name: '鍦板浘闀滃儚',
+          code: this.mapConfigCodes.mirror,
+          value: this.mapMirrorX ? '1' : '0',
+          type: 1,
+          status: 1,
+          selectType: 'map'
+        });
+      }
+      return createList;
+    },
+    createMapConfigs(createList) {
+      if (!window.$ || typeof baseUrl === 'undefined' || !Array.isArray(createList) || createList.length === 0) { return; }
+      createList.forEach((cfg) => {
+        $.ajax({
+          url: baseUrl + "/config/add/auth",
+          headers: { 'token': localStorage.getItem('token') },
+          method: 'POST',
+          data: cfg
+        });
+      });
     },
     loadMapTransformConfig() {
       if (!window.$ || typeof baseUrl === 'undefined') { return; }
@@ -2120,45 +2562,11 @@
           if (mirrorCfg && mirrorCfg.value != null) {
             this.mapMirrorX = this.parseMirror(mirrorCfg.value);
           }
-          if (rotateCfg == null || mirrorCfg == null) {
-            this.createMapTransformConfigIfMissing(rotateCfg, mirrorCfg);
-          }
+          this.createMapConfigs(this.buildMissingMapConfigList(byCode));
           if (this.mapContentSize && this.mapContentSize.width > 0) {
             this.applyMapTransform(true);
           }
         }
-      });
-    },
-    createMapTransformConfigIfMissing(rotateCfg, mirrorCfg) {
-      if (!window.$ || typeof baseUrl === 'undefined') { return; }
-      const createList = [];
-      if (!rotateCfg) {
-        createList.push({
-          name: '鍦板浘鏃嬭浆',
-          code: this.mapConfigCodes.rotate,
-          value: String(this.mapRotation || 0),
-          type: 1,
-          status: 1,
-          selectType: 'map'
-        });
-      }
-      if (!mirrorCfg) {
-        createList.push({
-          name: '鍦板浘闀滃儚',
-          code: this.mapConfigCodes.mirror,
-          value: this.mapMirrorX ? '1' : '0',
-          type: 1,
-          status: 1,
-          selectType: 'map'
-        });
-      }
-      createList.forEach((cfg) => {
-        $.ajax({
-          url: baseUrl + "/config/add/auth",
-          headers: { 'token': localStorage.getItem('token') },
-          method: 'POST',
-          data: cfg
-        });
       });
     },
     saveMapTransformConfig() {
@@ -2193,15 +2601,20 @@
       const viewport = this.getViewportSize();
       const vw = viewport.width;
       const vh = viewport.height;
-      let scale = Math.min(vw / contentW, vh / contentH) * 0.95;
+      const padding = this.getViewportPadding();
+      const availableW = Math.max(1, vw - padding.left - padding.right);
+      const availableH = Math.max(1, vh - padding.top - padding.bottom);
+      let scale = Math.min(availableW / contentW, availableH / contentH) * 0.95;
       if (!isFinite(scale) || scale <= 0) { scale = 1; }
       const baseW = this.mapContentSize.width || contentW;
       const baseH = this.mapContentSize.height || contentH;
       const mirrorX = this.mapMirrorX ? -1 : 1;
       const scaleX = scale * mirrorX;
       const scaleY = scale;
-      const posX = (vw / 2) - (baseW / 2) * scaleX;
-      const posY = (vh / 2) - (baseH / 2) * scaleY;
+      const centerX = padding.left + availableW / 2;
+      const centerY = padding.top + availableH / 2;
+      const posX = centerX - (baseW / 2) * scaleX;
+      const posY = centerY - (baseH / 2) * scaleY;
       this.pixiApp.stage.setTransform(posX, posY, scaleX, scaleY, 0, 0, 0, 0, 0);
     },
     applyMapTransform(fitToView) {
@@ -2215,6 +2628,7 @@
       this.mapRoot.scale.set(1, 1);
       if (fitToView) { this.fitStageToContent(); }
       this.scheduleAdjustLabels();
+      this.scheduleShelfChunkCulling();
     },
     scheduleAdjustLabels() {
       if (this.adjustLabelTimer) { clearTimeout(this.adjustLabelTimer); }
@@ -2226,22 +2640,6 @@
     }
   }
 });
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
 
 
 
diff --git a/src/main/webapp/components/MonitorCardKit.js b/src/main/webapp/components/MonitorCardKit.js
new file mode 100644
index 0000000..6d893ad
--- /dev/null
+++ b/src/main/webapp/components/MonitorCardKit.js
@@ -0,0 +1,147 @@
+(function (global) {
+  if (global.MonitorCardKit) {
+    return;
+  }
+
+  function ensureStyles() {
+    if (document.getElementById("monitor-card-kit-style")) {
+      return;
+    }
+    var style = document.createElement("style");
+    style.id = "monitor-card-kit-style";
+    style.textContent = [
+      "watch-crn-card,watch-dual-crn-card,devp-card,watch-rgv-card{display:block;width:100%;min-width:0;min-height:0;flex:1 1 auto;}",
+      ".mc-root{display:flex;flex-direction:column;width:100%;height:100%;min-width:0;min-height:0;color:#375067;font-size:12px;}",
+      ".mc-toolbar{display:flex;align-items:center;gap:10px;margin-bottom:10px;}",
+      ".mc-title{flex:1;font-size:14px;font-weight:700;color:#22384f;}",
+      ".mc-search{display:flex;gap:8px;flex:1;}",
+      ".mc-input,.mc-select{width:100%;height:34px;padding:0 11px;border-radius:10px;border:1px solid rgba(219,228,236,.96);background:rgba(255,255,255,.84);box-sizing:border-box;color:#334155;outline:none;}",
+      ".mc-input:focus,.mc-select:focus{border-color:rgba(112,148,190,.62);box-shadow:0 0 0 3px rgba(112,148,190,.1);}",
+      ".mc-btn{height:34px;padding:0 12px;border-radius:10px;border:none;background:#6f95bd;color:#fff;font-size:12px;font-weight:600;cursor:pointer;white-space:nowrap;}",
+      ".mc-btn.mc-btn-ghost{background:rgba(255,255,255,.88);color:#46607a;border:1px solid rgba(219,228,236,.96);}",
+      ".mc-btn.mc-btn-soft{background:rgba(233,239,245,.92);color:#46607a;border:1px solid rgba(210,220,231,.98);}",
+      ".mc-control-toggle{margin-bottom:8px;}",
+      ".mc-control{margin-bottom:10px;padding:12px;border:1px solid rgba(223,232,240,.94);border-radius:14px;background:rgba(248,251,253,.88);}",
+      ".mc-control-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;}",
+      ".mc-field{display:flex;flex-direction:column;gap:5px;}",
+      ".mc-field.mc-span-2{grid-column:1 / -1;}",
+      ".mc-field-label{font-size:11px;font-weight:700;color:#72859a;}",
+      ".mc-action-row{display:flex;flex-wrap:wrap;gap:8px;grid-column:1 / -1;padding-top:8px;margin-top:2px;border-top:1px dashed rgba(216,226,235,.92);}",
+      ".mc-collapse{flex:1;min-height:0;overflow:auto;padding-right:2px;}",
+      ".mc-item{border:1px solid rgba(223,232,240,.92);border-radius:14px;background:rgba(255,255,255,.72);margin-bottom:10px;overflow:hidden;}",
+      ".mc-item.is-open{border-color:rgba(132,166,201,.48);box-shadow:0 10px 18px rgba(148,163,184,.08);}",
+      ".mc-head{width:100%;display:flex;align-items:center;justify-content:space-between;gap:10px;padding:12px 14px;border:none;background:transparent;cursor:pointer;text-align:left;color:inherit;}",
+      ".mc-head-main{min-width:0;flex:1;}",
+      ".mc-head-title{font-size:13px;font-weight:700;color:#27425c;line-height:1.35;}",
+      ".mc-head-subtitle{margin-top:3px;font-size:11px;color:#7c8d9f;line-height:1.35;}",
+      ".mc-head-right{display:flex;align-items:center;gap:8px;flex-shrink:0;}",
+      ".mc-badge{padding:4px 8px;border-radius:999px;font-size:10px;font-weight:700;}",
+      ".mc-badge.is-success{background:rgba(82,177,126,.12);color:#2d7650;}",
+      ".mc-badge.is-working{background:rgba(111,149,189,.12);color:#3f6286;}",
+      ".mc-badge.is-warning{background:rgba(214,162,94,.14);color:#9b6a24;}",
+      ".mc-badge.is-danger{background:rgba(207,126,120,.14);color:#a14e4a;}",
+      ".mc-badge.is-muted{background:rgba(148,163,184,.14);color:#748397;}",
+      ".mc-chevron{font-size:14px;color:#6f8194;line-height:1;}",
+      ".mc-body{padding:0 14px 14px;border-top:1px solid rgba(232,238,244,.92);}",
+      ".mc-detail-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin-top:12px;}",
+      ".mc-detail-cell{padding:10px 12px;border-radius:12px;background:rgba(247,250,252,.9);border:1px solid rgba(232,238,244,.96);}",
+      ".mc-detail-label{font-size:11px;color:#7c8d9f;}",
+      ".mc-detail-value{margin-top:4px;font-size:12px;color:#31485f;line-height:1.45;word-break:break-all;}",
+      ".mc-inline-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px;}",
+      ".mc-link{padding:0;border:none;background:transparent;color:#4677a4;font-size:12px;font-weight:600;cursor:pointer;}",
+      ".mc-empty{padding:24px 10px;text-align:center;color:#8b9aad;}",
+      ".mc-footer{display:flex;align-items:center;justify-content:flex-end;gap:10px;margin-top:10px;color:#708396;font-size:11px;}",
+      ".mc-page-btn{height:30px;padding:0 10px;border-radius:8px;border:1px solid rgba(219,228,236,.96);background:rgba(255,255,255,.86);color:#46607a;cursor:pointer;}",
+      ".mc-page-btn[disabled]{opacity:.45;cursor:not-allowed;}",
+      ".mc-code{font-family:monospace;}",
+      "@media (max-width: 1100px){.mc-control-grid{grid-template-columns:1fr;}.mc-field.mc-span-2{grid-column:auto;}.mc-toolbar{flex-direction:column;align-items:stretch;}.mc-search{width:100%;}.mc-detail-grid{grid-template-columns:1fr;}}"
+    ].join("");
+    document.head.appendChild(style);
+  }
+
+  function showMessage(vm, message, type) {
+    if (!message) {
+      return;
+    }
+    if (vm && typeof vm.$message === "function") {
+      vm.$message({
+        message: message,
+        type: type === "danger" ? "error" : (type || "info")
+      });
+      return;
+    }
+    if (vm && vm.$root && typeof vm.$root.showPageMessage === "function") {
+      vm.$root.showPageMessage(message, type === "danger" ? "error" : type);
+      return;
+    }
+    if (global.ELEMENT && typeof global.ELEMENT.Message === "function") {
+      global.ELEMENT.Message({
+        message: message,
+        type: type === "danger" ? "error" : (type || "info")
+      });
+      return;
+    }
+    if (global.layer && typeof global.layer.msg === "function") {
+      var iconMap = { success: 1, danger: 2, warning: 0 };
+      global.layer.msg(message, {
+        icon: iconMap[type] != null ? iconMap[type] : 0,
+        time: 1800
+      });
+      return;
+    }
+    console[type === "danger" ? "error" : "log"](message);
+  }
+
+  function orDash(value) {
+    return value == null || value === "" ? "-" : String(value);
+  }
+
+  function yesNo(value) {
+    if (value === true || value === "Y" || value === "y" || value === 1 || value === "1") {
+      return "Y";
+    }
+    if (value === false || value === "N" || value === "n" || value === 0 || value === "0") {
+      return "N";
+    }
+    return value ? "Y" : "N";
+  }
+
+  function deviceStatusLabel(status, fallbackManual) {
+    var normalized = String(status || "").toUpperCase();
+    if (normalized === "AUTO") {
+      return "鑷姩";
+    }
+    if (normalized === "WORKING") {
+      return "浣滀笟涓�";
+    }
+    if (normalized === "ERROR") {
+      return "鏁呴殰";
+    }
+    return fallbackManual || "绂荤嚎";
+  }
+
+  function statusTone(label) {
+    if (label === "鑷姩") {
+      return "success";
+    }
+    if (label === "浣滀笟涓�") {
+      return "working";
+    }
+    if (label === "鎵嬪姩") {
+      return "warning";
+    }
+    if (label === "鏁呴殰" || label === "鎶ヨ") {
+      return "danger";
+    }
+    return "muted";
+  }
+
+  global.MonitorCardKit = {
+    ensureStyles: ensureStyles,
+    showMessage: showMessage,
+    orDash: orDash,
+    yesNo: yesNo,
+    deviceStatusLabel: deviceStatusLabel,
+    statusTone: statusTone
+  };
+})(window);
diff --git a/src/main/webapp/components/MonitorWorkbench.js b/src/main/webapp/components/MonitorWorkbench.js
new file mode 100644
index 0000000..a532d1f
--- /dev/null
+++ b/src/main/webapp/components/MonitorWorkbench.js
@@ -0,0 +1,661 @@
+Vue.component("monitor-workbench", {
+  template: `
+    <div class="wb-root">
+      <div class="wb-tabs" role="tablist">
+        <button
+          v-for="tab in tabs"
+          :key="tab.key"
+          type="button"
+          :class="['wb-tab', { 'is-active': activeCard === tab.key }]"
+          @click="changeTab(tab.key)"
+        >{{ tab.label }}</button>
+      </div>
+
+      <div class="wb-toolbar">
+        <input
+          class="wb-input"
+          :value="currentSearch"
+          :placeholder="currentSearchPlaceholder()"
+          @input="updateSearch($event.target.value)"
+        />
+        <div class="wb-toolbar-actions">
+          <button type="button" class="wb-btn wb-btn-ghost" @click="refreshCurrent">鍒锋柊</button>
+          <button type="button" class="wb-btn" @click="toggleControl">
+            {{ currentShowControl ? '鏀惰捣鎿嶄綔' : '灞曞紑鎿嶄綔' }}
+          </button>
+        </div>
+      </div>
+
+      <div class="wb-main">
+        <div class="wb-side">
+          <div class="wb-list-card">
+            <div class="wb-side-title">璁惧閫夋嫨</div>
+            <div class="wb-list">
+              <button
+                v-for="item in filteredList"
+                :key="activeCard + '-' + getItemId(activeCard, item)"
+                type="button"
+                :class="['wb-list-item', { 'is-active': isSelected(activeCard, item) }]"
+                @click="selectItem(activeCard, item)"
+              >
+                <span :class="['wb-badge', 'is-' + getStatusTone(activeCard, item)]">{{ getStatusLabel(activeCard, item) }}</span>
+                <div class="wb-list-main">
+                  <div class="wb-list-title">{{ getItemTitle(activeCard, item) }}</div>
+                  <div class="wb-list-meta">{{ getItemMeta(activeCard, item) }}</div>
+                </div>
+              </button>
+              <div v-if="filteredList.length === 0" class="wb-empty">褰撳墠娌℃湁鍙睍绀虹殑鏁版嵁</div>
+            </div>
+          </div>
+
+          <div v-if="currentShowControl" class="wb-control-card">
+            <div class="wb-side-title">蹇嵎鎿嶄綔</div>
+            <div class="wb-control-target">{{ controlTargetText }}</div>
+            <div class="wb-control-subtitle">{{ controlPanelHint }}</div>
+
+            <div v-if="activeCard === 'crn'" class="wb-form-grid">
+              <label class="wb-field">
+                <span class="wb-field-label">鍫嗗灈鏈哄彿</span>
+                <input class="wb-input" v-model="controlForms.crn.crnNo" placeholder="1" />
+              </label>
+              <label class="wb-field">
+                <span class="wb-field-label">婧愬簱浣�</span>
+                <input class="wb-input" v-model="controlForms.crn.sourceLocNo" placeholder="婧愮偣" />
+              </label>
+              <label class="wb-field wb-field-span-2">
+                <span class="wb-field-label">鐩爣搴撲綅</span>
+                <input class="wb-input" v-model="controlForms.crn.targetLocNo" placeholder="鐩爣鐐�" />
+              </label>
+              <div class="wb-action-row">
+                <button type="button" class="wb-btn wb-btn-primary" @click="submitControl('transport')">鍙栨斁璐�</button>
+                <button type="button" class="wb-btn wb-btn-ghost" @click="submitControl('move')">绉诲姩</button>
+                <button type="button" class="wb-btn wb-btn-soft" @click="submitControl('taskComplete')">瀹屾垚</button>
+              </div>
+            </div>
+
+            <div v-else-if="activeCard === 'dualCrn'" class="wb-form-grid">
+              <label class="wb-field">
+                <span class="wb-field-label">鍫嗗灈鏈哄彿</span>
+                <input class="wb-input" v-model="controlForms.dualCrn.crnNo" placeholder="2" />
+              </label>
+              <label class="wb-field">
+                <span class="wb-field-label">宸ヤ綅</span>
+                <select class="wb-select" v-model="controlForms.dualCrn.station">
+                  <option :value="1">宸ヤ綅1</option>
+                  <option :value="2">宸ヤ綅2</option>
+                </select>
+              </label>
+              <label class="wb-field">
+                <span class="wb-field-label">婧愬簱浣�</span>
+                <input class="wb-input" v-model="controlForms.dualCrn.sourceLocNo" placeholder="婧愮偣" />
+              </label>
+              <label class="wb-field">
+                <span class="wb-field-label">鐩爣搴撲綅</span>
+                <input class="wb-input" v-model="controlForms.dualCrn.targetLocNo" placeholder="鐩爣鐐�" />
+              </label>
+              <div class="wb-action-row">
+                <button type="button" class="wb-btn wb-btn-primary" @click="submitControl('transport')">鍙栨斁璐�</button>
+                <button type="button" class="wb-btn wb-btn-ghost" @click="submitControl('pickup')">鍙栬揣</button>
+                <button type="button" class="wb-btn wb-btn-ghost" @click="submitControl('putdown')">鏀捐揣</button>
+                <button type="button" class="wb-btn wb-btn-ghost" @click="submitControl('move')">绉诲姩</button>
+                <button type="button" class="wb-btn wb-btn-soft" @click="submitControl('taskComplete')">瀹屾垚</button>
+              </div>
+            </div>
+
+            <div v-else-if="activeCard === 'devp'" class="wb-form-grid">
+              <label class="wb-field">
+                <span class="wb-field-label">绔欏彿</span>
+                <input class="wb-input" v-model="controlForms.devp.stationId" placeholder="101" />
+              </label>
+              <label class="wb-field">
+                <span class="wb-field-label">宸ヤ綔鍙�</span>
+                <input class="wb-input" v-model="controlForms.devp.taskNo" placeholder="宸ヤ綔鍙�" />
+              </label>
+              <label class="wb-field wb-field-span-2">
+                <span class="wb-field-label">鐩爣绔�</span>
+                <input class="wb-input" v-model="controlForms.devp.targetStationId" placeholder="鐩爣绔欏彿" />
+              </label>
+              <div class="wb-action-row">
+                <button type="button" class="wb-btn wb-btn-primary" @click="submitControl('move')">涓嬪彂</button>
+                <button type="button" class="wb-btn wb-btn-soft" @click="submitControl('reset')">澶嶄綅</button>
+                <button
+                  v-if="selectedItem"
+                  type="button"
+                  class="wb-btn wb-btn-ghost"
+                  @click="editBarcode"
+                >鏉$爜</button>
+              </div>
+            </div>
+
+            <div v-else-if="activeCard === 'rgv'" class="wb-form-grid">
+              <label class="wb-field">
+                <span class="wb-field-label">RGV鍙�</span>
+                <input class="wb-input" v-model="controlForms.rgv.rgvNo" placeholder="1" />
+              </label>
+              <label class="wb-field">
+                <span class="wb-field-label">婧愮偣</span>
+                <input class="wb-input" v-model="controlForms.rgv.sourcePos" placeholder="婧愮偣" />
+              </label>
+              <label class="wb-field wb-field-span-2">
+                <span class="wb-field-label">鐩爣鐐�</span>
+                <input class="wb-input" v-model="controlForms.rgv.targetPos" placeholder="鐩爣鐐�" />
+              </label>
+              <div class="wb-action-row">
+                <button type="button" class="wb-btn wb-btn-primary" @click="submitControl('transport')">鍙栨斁璐�</button>
+                <button type="button" class="wb-btn wb-btn-ghost" @click="submitControl('move')">绉诲姩</button>
+                <button type="button" class="wb-btn wb-btn-soft" @click="submitControl('taskComplete')">瀹屾垚</button>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <div class="wb-detail-panel">
+          <div class="wb-detail" v-if="selectedItem">
+            <div class="wb-detail-header">
+              <div>
+                <div class="wb-section-title">{{ getItemTitle(activeCard, selectedItem) }}</div>
+                <div class="wb-detail-subtitle">{{ getItemMeta(activeCard, selectedItem) }}</div>
+              </div>
+              <div class="wb-detail-actions">
+                <button
+                  v-if="activeCard === 'dualCrn'"
+                  type="button"
+                  class="wb-link"
+                  @click="editDualTask(1)"
+                >宸ヤ綅1浠诲姟鍙�</button>
+                <button
+                  v-if="activeCard === 'dualCrn'"
+                  type="button"
+                  class="wb-link"
+                  @click="editDualTask(2)"
+                >宸ヤ綅2浠诲姟鍙�</button>
+              </div>
+            </div>
+
+            <div class="wb-detail-grid">
+              <div v-for="entry in detailEntries" :key="entry.label" class="wb-detail-cell">
+                <div class="wb-detail-label">{{ entry.label }}</div>
+                <div class="wb-detail-value">{{ entry.value }}</div>
+              </div>
+            </div>
+          </div>
+          <div class="wb-detail wb-detail-empty" v-else>
+            <div class="wb-empty">璇峰厛浠庡乏渚ч�夋嫨涓�涓澶�</div>
+          </div>
+        </div>
+      </div>
+
+      <div v-if="noticeMessage" :class="['wb-notice', 'is-' + noticeType]">{{ noticeMessage }}</div>
+    </div>
+  `,
+  props: {
+    activeCard: { type: String, default: "crn" },
+    crnParam: { type: Object, default: function () { return {}; } },
+    dualCrnParam: { type: Object, default: function () { return {}; } },
+    devpParam: { type: Object, default: function () { return {}; } },
+    rgvParam: { type: Object, default: function () { return {}; } },
+    crnList: { type: Array, default: function () { return []; } },
+    dualCrnList: { type: Array, default: function () { return []; } },
+    stationList: { type: Array, default: function () { return []; } },
+    rgvList: { type: Array, default: function () { return []; } }
+  },
+  data() {
+    return {
+      tabs: [
+        { key: "crn", label: "鍫嗗灈鏈�" },
+        { key: "dualCrn", label: "鍙屽伐浣�" },
+        { key: "devp", label: "杈撻�佺珯" },
+        { key: "rgv", label: "RGV" }
+      ],
+      searchMap: {
+        crn: "",
+        dualCrn: "",
+        devp: "",
+        rgv: ""
+      },
+      selectedIdMap: {
+        crn: null,
+        dualCrn: null,
+        devp: null,
+        rgv: null
+      },
+      showControlMap: {
+        crn: false,
+        dualCrn: false,
+        devp: false,
+        rgv: false
+      },
+      controlForms: {
+        crn: { crnNo: "", sourceLocNo: "", targetLocNo: "" },
+        dualCrn: { crnNo: "", sourceLocNo: "", targetLocNo: "", station: 1 },
+        devp: { stationId: "", taskNo: "", targetStationId: "" },
+        rgv: { rgvNo: "", sourcePos: "", targetPos: "" }
+      },
+      noticeMessage: "",
+      noticeType: "info",
+      noticeTimer: null
+    };
+  },
+  computed: {
+    currentSearch() {
+      return this.searchMap[this.activeCard] || "";
+    },
+    currentShowControl() {
+      return !!this.showControlMap[this.activeCard];
+    },
+    currentList() {
+      return this.getListByType(this.activeCard);
+    },
+    filteredList() {
+      const keyword = String(this.currentSearch || "").trim().toLowerCase();
+      if (!keyword) { return this.currentList; }
+      return this.currentList.filter((item) => this.matchesKeyword(this.activeCard, item, keyword));
+    },
+    selectedItem() {
+      return this.getSelectedItem(this.activeCard);
+    },
+    detailEntries() {
+      return this.buildDetailEntries(this.activeCard, this.selectedItem);
+    },
+    controlPanelTitle() {
+      if (this.activeCard === "crn") { return "鍫嗗灈鏈烘帶鍒�"; }
+      if (this.activeCard === "dualCrn") { return "鍙屽伐浣嶆帶鍒�"; }
+      if (this.activeCard === "devp") { return "杈撻�佺珯鎺у埗"; }
+      if (this.activeCard === "rgv") { return "RGV鎺у埗"; }
+      return "鎺у埗鎿嶄綔";
+    },
+    controlPanelHint() {
+      if (this.activeCard === "crn") { return "鍏堢‘璁よ澶囧彿锛屽啀濉啓婧愬簱浣嶅拰鐩爣搴撲綅銆�"; }
+      if (this.activeCard === "dualCrn") { return "鍏堥�夋嫨宸ヤ綅锛屽啀涓嬪彂鍙栬揣銆佹斁璐ф垨绉诲姩鎸囦护銆�"; }
+      if (this.activeCard === "devp") { return "鐢ㄤ簬绔欑偣涓嬪彂銆佸浣嶅拰鏉$爜缁存姢銆�"; }
+      if (this.activeCard === "rgv") { return "鐢ㄤ簬杞ㄩ亾杞﹀彇鏀捐揣銆佺Щ鍔ㄥ拰浠诲姟瀹屾垚銆�"; }
+      return "";
+    },
+    controlTargetText() {
+      if (!this.selectedItem) { return "鏈�変腑璁惧"; }
+      return "褰撳墠鐩爣: " + this.getItemTitle(this.activeCard, this.selectedItem);
+    }
+  },
+  watch: {
+    activeCard: {
+      immediate: true,
+      handler(type) {
+        this.ensureSelection(type);
+      }
+    },
+    crnList() { this.ensureSelection("crn"); },
+    dualCrnList() { this.ensureSelection("dualCrn"); },
+    stationList() { this.ensureSelection("devp"); },
+    rgvList() { this.ensureSelection("rgv"); },
+    crnParam: {
+      deep: true,
+      handler(v) { this.applyExternalFocus("crn", v && v.crnNo); }
+    },
+    dualCrnParam: {
+      deep: true,
+      handler(v) { this.applyExternalFocus("dualCrn", v && v.crnNo); }
+    },
+    devpParam: {
+      deep: true,
+      handler(v) { this.applyExternalFocus("devp", v && v.stationId); }
+    },
+    rgvParam: {
+      deep: true,
+      handler(v) { this.applyExternalFocus("rgv", v && v.rgvNo); }
+    }
+  },
+  beforeDestroy() {
+    if (this.noticeTimer) {
+      clearTimeout(this.noticeTimer);
+      this.noticeTimer = null;
+    }
+  },
+  methods: {
+    changeTab(type) {
+      if (type === this.activeCard) { return; }
+      this.$emit("change-tab", type);
+    },
+    updateSearch(value) {
+      this.$set(this.searchMap, this.activeCard, value);
+      this.ensureSelection(this.activeCard);
+    },
+    refreshCurrent() {
+      this.$emit("refresh-request", this.activeCard);
+    },
+    toggleControl() {
+      this.$set(this.showControlMap, this.activeCard, !this.currentShowControl);
+      if (this.currentShowControl && this.selectedItem) {
+        this.hydrateControlForm(this.activeCard, this.selectedItem);
+      }
+    },
+    getListByType(type) {
+      if (type === "crn") { return this.crnList || []; }
+      if (type === "dualCrn") { return this.dualCrnList || []; }
+      if (type === "devp") { return this.stationList || []; }
+      if (type === "rgv") { return this.rgvList || []; }
+      return [];
+    },
+    getItemId(type, item) {
+      if (!item) { return null; }
+      if (type === "crn" || type === "dualCrn") { return item.crnNo; }
+      if (type === "devp") { return item.stationId; }
+      if (type === "rgv") { return item.rgvNo; }
+      return null;
+    },
+    getSelectedItem(type) {
+      const list = this.filteredListForType(type);
+      if (!list.length) { return null; }
+      const selectedId = this.selectedIdMap[type];
+      for (let i = 0; i < list.length; i++) {
+        if (String(this.getItemId(type, list[i])) === String(selectedId)) {
+          return list[i];
+        }
+      }
+      return list[0];
+    },
+    filteredListForType(type) {
+      const keyword = String(this.searchMap[type] || "").trim().toLowerCase();
+      const list = this.getListByType(type);
+      if (!keyword) { return list; }
+      return list.filter((item) => this.matchesKeyword(type, item, keyword));
+    },
+    ensureSelection(type) {
+      const list = this.filteredListForType(type);
+      if (!list.length) {
+        this.$set(this.selectedIdMap, type, null);
+        return;
+      }
+      const currentId = this.selectedIdMap[type];
+      const exists = list.some((item) => String(this.getItemId(type, item)) === String(currentId));
+      if (!exists) {
+        this.$set(this.selectedIdMap, type, this.getItemId(type, list[0]));
+      }
+    },
+    applyExternalFocus(type, rawId) {
+      if (rawId == null || rawId === "" || rawId === 0) { return; }
+      this.$set(this.selectedIdMap, type, rawId);
+      if (this.activeCard === type) {
+        const item = this.getSelectedItem(type);
+        if (item) { this.hydrateControlForm(type, item); }
+      }
+    },
+    selectItem(type, item) {
+      this.$set(this.selectedIdMap, type, this.getItemId(type, item));
+      this.hydrateControlForm(type, item);
+    },
+    hydrateControlForm(type, item) {
+      if (!item) { return; }
+      if (type === "crn") {
+        this.controlForms.crn.crnNo = this.orEmpty(item.crnNo);
+      } else if (type === "dualCrn") {
+        this.controlForms.dualCrn.crnNo = this.orEmpty(item.crnNo);
+      } else if (type === "devp") {
+        this.controlForms.devp.stationId = this.orEmpty(item.stationId);
+        this.controlForms.devp.taskNo = this.orEmpty(item.taskNo);
+        this.controlForms.devp.targetStationId = this.orEmpty(item.targetStaNo);
+      } else if (type === "rgv") {
+        this.controlForms.rgv.rgvNo = this.orEmpty(item.rgvNo);
+      }
+    },
+    matchesKeyword(type, item, keyword) {
+      const fields = [];
+      if (type === "crn" || type === "dualCrn") {
+        fields.push(item.crnNo, item.taskNo, item.taskNoTwo, item.locNo, item.sourceLocNo, item.status, item.mode);
+      } else if (type === "devp") {
+        fields.push(item.stationId, item.taskNo, item.targetStaNo, item.barcode, item.errorMsg, item.extend);
+      } else if (type === "rgv") {
+        fields.push(item.rgvNo, item.taskNo, item.trackSiteNo, item.status, item.mode, item.alarm);
+      }
+      return fields.some((field) => String(field == null ? "" : field).toLowerCase().indexOf(keyword) !== -1);
+    },
+    currentSearchPlaceholder() {
+      if (this.activeCard === "crn") { return "鎼滅储鍫嗗灈鏈哄彿"; }
+      if (this.activeCard === "dualCrn") { return "鎼滅储鍙屽伐浣嶅爢鍨涙満鍙�"; }
+      if (this.activeCard === "devp") { return "鎼滅储绔欏彿 / 鏉$爜"; }
+      if (this.activeCard === "rgv") { return "鎼滅储RGV鍙�"; }
+      return "鎼滅储";
+    },
+    getItemTitle(type, item) {
+      if (!item) { return "-"; }
+      if (type === "crn") { return item.crnNo + "鍙峰爢鍨涙満"; }
+      if (type === "dualCrn") { return item.crnNo + "鍙峰弻宸ヤ綅鍫嗗灈鏈�"; }
+      if (type === "devp") { return item.stationId + "鍙风珯鐐�"; }
+      if (type === "rgv") { return item.rgvNo + "鍙稲GV"; }
+      return "-";
+    },
+    getItemMeta(type, item) {
+      if (!item) { return "-"; }
+      if (type === "crn") { return "浠诲姟 " + this.orDash(item.workNo) + " | 鐩爣 " + this.orDash(item.locNo); }
+      if (type === "dualCrn") { return "宸ヤ綅1 " + this.orDash(item.taskNo) + " | 宸ヤ綅2 " + this.orDash(item.taskNoTwo); }
+      if (type === "devp") { return "浠诲姟 " + this.orDash(item.taskNo) + " | 鐩爣绔� " + this.orDash(item.targetStaNo); }
+      if (type === "rgv") { return "杞ㄩ亾浣� " + this.orDash(item.trackSiteNo) + " | 浠诲姟 " + this.orDash(item.taskNo); }
+      return "-";
+    },
+    getStatusLabel(type, item) {
+      if (!item) { return "鏈煡"; }
+      if (type === "devp") { return item.autoing ? "鑷姩" : "鎵嬪姩"; }
+      const status = String(item.deviceStatus || "").toUpperCase();
+      if (status === "AUTO") { return "鑷姩"; }
+      if (status === "WORKING") { return "浣滀笟涓�"; }
+      if (status === "ERROR") { return "鏁呴殰"; }
+      return "绂荤嚎";
+    },
+    getStatusTone(type, item) {
+      const label = this.getStatusLabel(type, item);
+      if (label === "鑷姩") { return "success"; }
+      if (label === "浣滀笟涓�") { return "working"; }
+      if (label === "鎵嬪姩") { return "warning"; }
+      if (label === "鏁呴殰") { return "danger"; }
+      return "muted";
+    },
+    isSelected(type, item) {
+      return String(this.getItemId(type, item)) === String(this.selectedIdMap[type]);
+    },
+    buildDetailEntries(type, item) {
+      if (!item) { return []; }
+      if (type === "crn") {
+        return [
+          { label: "缂栧彿", value: this.orDash(item.crnNo) },
+          { label: "宸ヤ綔鍙�", value: this.orDash(item.workNo) },
+          { label: "妯″紡", value: this.orDash(item.mode) },
+          { label: "鐘舵��", value: this.orDash(item.status) },
+          { label: "婧愬簱浣�", value: this.orDash(item.sourceLocNo) },
+          { label: "鐩爣搴撲綅", value: this.orDash(item.locNo) },
+          { label: "鏄惁鏈夌墿", value: this.yesNo(item.loading) },
+          { label: "浠诲姟鎺ユ敹", value: this.orDash(item.taskReceive) },
+          { label: "鍒�", value: this.orDash(item.bay) },
+          { label: "灞�", value: this.orDash(item.lev) },
+          { label: "璐у弶瀹氫綅", value: this.orDash(item.forkOffset) },
+          { label: "杞借揣鍙板畾浣�", value: this.orDash(item.liftPos) },
+          { label: "璧拌瀹氫綅", value: this.orDash(item.walkPos) },
+          { label: "璧拌閫熷害", value: this.orDash(item.xspeed) },
+          { label: "鍗囬檷閫熷害", value: this.orDash(item.yspeed) },
+          { label: "鍙夌墮閫熷害", value: this.orDash(item.zspeed) },
+          { label: "绉伴噸鏁版嵁", value: this.orDash(item.weight) },
+          { label: "鏉$爜鏁版嵁", value: this.orDash(item.barcode) },
+          { label: "鏁呴殰浠g爜", value: this.orDash(item.warnCode) },
+          { label: "鏁呴殰鎻忚堪", value: this.orDash(item.alarm) }
+        ];
+      }
+      if (type === "dualCrn") {
+        return [
+          { label: "妯″紡", value: this.orDash(item.mode) },
+          { label: "寮傚父鐮�", value: this.orDash(item.warnCode) },
+          { label: "宸ヤ綅1浠诲姟鍙�", value: this.orDash(item.taskNo) },
+          { label: "宸ヤ綅2浠诲姟鍙�", value: this.orDash(item.taskNoTwo) },
+          { label: "宸ヤ綅1鐘舵��", value: this.orDash(item.status) },
+          { label: "宸ヤ綅2鐘舵��", value: this.orDash(item.statusTwo) },
+          { label: "宸ヤ綅1鏈夌墿", value: this.yesNo(item.loading) },
+          { label: "宸ヤ綅2鏈夌墿", value: this.yesNo(item.loadingTwo) },
+          { label: "鍒�", value: this.orDash(item.bay) },
+          { label: "灞�", value: this.orDash(item.lev) },
+          { label: "杞借揣鍙板畾浣�", value: this.orDash(item.liftPos) },
+          { label: "璧拌瀹氫綅", value: this.orDash(item.walkPos) },
+          { label: "璧拌閫熷害", value: this.orDash(item.xspeed) },
+          { label: "鍗囬檷閫熷害", value: this.orDash(item.yspeed) },
+          { label: "鍙夌墮閫熷害", value: this.orDash(item.zspeed) },
+          { label: "鎵╁睍鏁版嵁", value: this.orDash(item.extend) }
+        ];
+      }
+      if (type === "devp") {
+        return [
+          { label: "缂栧彿", value: this.orDash(item.stationId) },
+          { label: "宸ヤ綔鍙�", value: this.orDash(item.taskNo) },
+          { label: "鐩爣绔�", value: this.orDash(item.targetStaNo) },
+          { label: "妯″紡", value: item.autoing ? "鑷姩" : "鎵嬪姩" },
+          { label: "鏈夌墿", value: this.yesNo(item.loading) },
+          { label: "鍙叆", value: this.yesNo(item.inEnable) },
+          { label: "鍙嚭", value: this.yesNo(item.outEnable) },
+          { label: "绌烘澘淇″彿", value: this.yesNo(item.emptyMk) },
+          { label: "婊℃澘淇″彿", value: this.yesNo(item.fullPlt) },
+          { label: "杩愯闃诲", value: this.yesNo(item.runBlock) },
+          { label: "鍚姩鍏ュ簱", value: this.yesNo(item.enableIn) },
+          { label: "鎵樼洏楂樺害", value: this.orDash(item.palletHeight) },
+          { label: "鏉$爜", value: this.orDash(item.barcode) },
+          { label: "閲嶉噺", value: this.orDash(item.weight) },
+          { label: "浠诲姟鍙啓鍖�", value: this.orDash(item.taskWriteIdx) },
+          { label: "鏁呴殰浠g爜", value: this.orDash(item.error) },
+          { label: "鏁呴殰淇℃伅", value: this.orDash(item.errorMsg) }
+        ];
+      }
+      if (type === "rgv") {
+        return [
+          { label: "缂栧彿", value: this.orDash(item.rgvNo) },
+          { label: "宸ヤ綔鍙�", value: this.orDash(item.taskNo) },
+          { label: "妯″紡", value: this.orDash(item.mode) },
+          { label: "鐘舵��", value: this.orDash(item.status) },
+          { label: "杞ㄩ亾浣�", value: this.orDash(item.trackSiteNo) },
+          { label: "鏄惁鏈夌墿", value: this.yesNo(item.loading) },
+          { label: "鏁呴殰浠g爜", value: this.orDash(item.warnCode) },
+          { label: "鏁呴殰鎻忚堪", value: this.orDash(item.alarm) },
+          { label: "鎵╁睍鏁版嵁", value: this.orDash(item.extend) }
+        ];
+      }
+      return [];
+    },
+    submitControl(action) {
+      const config = this.getControlConfig(this.activeCard, action);
+      if (!config) { return; }
+      $.ajax({
+        url: baseUrl + config.url,
+        headers: { token: localStorage.getItem("token") },
+        contentType: "application/json",
+        method: "post",
+        data: JSON.stringify(config.payload),
+        success: (res) => {
+          if (res && res.code === 200) {
+            this.showNotice(res.msg || "鎿嶄綔鎴愬姛", "success");
+            this.$emit("refresh-request", this.activeCard);
+          } else {
+            this.showNotice((res && res.msg) || "鎿嶄綔澶辫触", "warning");
+          }
+        },
+        error: () => {
+          this.showNotice("璇锋眰澶辫触", "danger");
+        }
+      });
+    },
+    getControlConfig(type, action) {
+      if (type === "crn") {
+        return {
+          url: { transport: "/crn/command/take", move: "/crn/command/move", taskComplete: "/crn/command/taskComplete" }[action],
+          payload: this.controlForms.crn
+        };
+      }
+      if (type === "dualCrn") {
+        return {
+          url: {
+            transport: "/dualcrn/command/take",
+            pickup: "/dualcrn/command/pick",
+            putdown: "/dualcrn/command/put",
+            move: "/dualcrn/command/move",
+            taskComplete: "/dualcrn/command/taskComplete"
+          }[action],
+          payload: this.controlForms.dualCrn
+        };
+      }
+      if (type === "devp") {
+        return {
+          url: { move: "/station/command/move", reset: "/station/command/reset" }[action],
+          payload: this.controlForms.devp
+        };
+      }
+      if (type === "rgv") {
+        return {
+          url: { transport: "/rgv/command/transport", move: "/rgv/command/move", taskComplete: "/rgv/command/taskComplete" }[action],
+          payload: this.controlForms.rgv
+        };
+      }
+      return null;
+    },
+    editBarcode() {
+      const item = this.selectedItem;
+      if (!item || item.stationId == null) { return; }
+      const barcode = window.prompt("璇疯緭鍏ユ柊鐨勬潯鐮佸�硷紙鍙暀绌烘竻绌猴級", item.barcode || "");
+      if (barcode === null) { return; }
+      $.ajax({
+        url: baseUrl + "/station/command/barcode",
+        headers: { token: localStorage.getItem("token") },
+        contentType: "application/json",
+        method: "post",
+        data: JSON.stringify({ stationId: item.stationId, barcode: String(barcode).trim() }),
+        success: (res) => {
+          if (res && res.code === 200) {
+            this.showNotice("鏉$爜淇敼鎴愬姛", "success");
+            this.$emit("refresh-request", "devp");
+          } else {
+            this.showNotice((res && res.msg) || "鏉$爜淇敼澶辫触", "warning");
+          }
+        },
+        error: () => { this.showNotice("鏉$爜淇敼澶辫触", "danger"); }
+      });
+    },
+    editDualTask(station) {
+      const item = this.selectedItem;
+      if (!item || item.crnNo == null) { return; }
+      const currentValue = station === 1 ? item.taskNo : item.taskNoTwo;
+      const value = window.prompt("璇疯緭鍏ュ伐浣�" + station + "浠诲姟鍙�", currentValue == null ? "" : String(currentValue));
+      if (value === null) { return; }
+      if (!/^\d+$/.test(String(value).trim())) {
+        this.showNotice("浠诲姟鍙峰繀椤绘槸闈炶礋鏁存暟", "warning");
+        return;
+      }
+      $.ajax({
+        url: baseUrl + "/dualcrn/command/updateTaskNo",
+        headers: { token: localStorage.getItem("token") },
+        contentType: "application/json",
+        method: "post",
+        data: JSON.stringify({ crnNo: item.crnNo, station: station, taskNo: Number(value) }),
+        success: (res) => {
+          if (res && res.code === 200) {
+            this.showNotice("浠诲姟鍙锋洿鏂版垚鍔�", "success");
+            this.$emit("refresh-request", "dualCrn");
+          } else {
+            this.showNotice((res && res.msg) || "浠诲姟鍙锋洿鏂板け璐�", "warning");
+          }
+        },
+        error: () => { this.showNotice("浠诲姟鍙锋洿鏂板け璐�", "danger"); }
+      });
+    },
+    showNotice(message, type) {
+      this.noticeMessage = message;
+      this.noticeType = type || "info";
+      if (this.noticeTimer) { clearTimeout(this.noticeTimer); }
+      this.noticeTimer = setTimeout(() => {
+        this.noticeMessage = "";
+        this.noticeTimer = null;
+      }, 2200);
+    },
+    yesNo(value) {
+      if (value === true || value === "Y" || value === "y" || value === 1 || value === "1") { return "Y"; }
+      if (value === false || value === 0 || value === "0") { return "N"; }
+      return value ? "Y" : "N";
+    },
+    orDash(value) {
+      return value == null || value === "" ? "-" : String(value);
+    },
+    orEmpty(value) {
+      return value == null ? "" : String(value);
+    }
+  }
+});
diff --git a/src/main/webapp/components/WatchCrnCard.js b/src/main/webapp/components/WatchCrnCard.js
index 53c90a2..0d7e292 100644
--- a/src/main/webapp/components/WatchCrnCard.js
+++ b/src/main/webapp/components/WatchCrnCard.js
@@ -1,96 +1,86 @@
 Vue.component("watch-crn-card", {
   template: `
-    <div>
-        <div style="display: flex;margin-bottom: 10px;">
-            <div style="width: 100%;">鍫嗗灈鏈虹洃鎺�</div>
-            <div style="width: 100%;text-align: right;display: flex;"><el-input size="mini" v-model="searchCrnNo" placeholder="璇疯緭鍏ュ爢鍨涙満鍙�"></el-input><el-button @click="getCrnStateInfo" size="mini">鏌ヨ</el-button></div>
+    <div class="mc-root">
+      <div class="mc-toolbar">
+        <div class="mc-title">鍫嗗灈鏈虹洃鎺�</div>
+        <div class="mc-search">
+          <input class="mc-input" v-model="searchCrnNo" placeholder="璇疯緭鍏ュ爢鍨涙満鍙�" />
+          <button type="button" class="mc-btn mc-btn-ghost" @click="getCrnStateInfo">鏌ヨ</button>
         </div>
-        <div style="margin-bottom: 10px;" v-if="!readOnly">
-            <div style="margin-bottom: 5px;">
-               <el-button v-if="showControl" @click="openControl" size="mini">鍏抽棴鎺у埗涓績</el-button>
-               <el-button v-else @click="openControl" size="mini">鎵撳紑鎺у埗涓績</el-button>
+      </div>
+
+      <div v-if="!readOnly" class="mc-control-toggle">
+        <button type="button" class="mc-btn mc-btn-ghost" @click="openControl">
+          {{ showControl ? '鏀惰捣鎺у埗涓績' : '鎵撳紑鎺у埗涓績' }}
+        </button>
+      </div>
+
+      <div v-if="showControl" class="mc-control">
+        <div class="mc-control-grid">
+          <label class="mc-field">
+            <span class="mc-field-label">鍫嗗灈鏈哄彿</span>
+            <input class="mc-input" v-model="controlParam.crnNo" placeholder="渚嬪 1" />
+          </label>
+          <label class="mc-field">
+            <span class="mc-field-label">婧愬簱浣�</span>
+            <input class="mc-input" v-model="controlParam.sourceLocNo" placeholder="杈撳叆婧愮偣" />
+          </label>
+          <label class="mc-field mc-span-2">
+            <span class="mc-field-label">鐩爣搴撲綅</span>
+            <input class="mc-input" v-model="controlParam.targetLocNo" placeholder="杈撳叆鐩爣鐐�" />
+          </label>
+          <div class="mc-action-row">
+            <button type="button" class="mc-btn" @click="controlCommandTransport">鍙栨斁璐�</button>
+            <button type="button" class="mc-btn mc-btn-ghost" @click="controlCommandMove">绉诲姩</button>
+            <button type="button" class="mc-btn mc-btn-soft" @click="controlCommandTaskComplete">浠诲姟瀹屾垚</button>
+          </div>
+        </div>
+      </div>
+
+      <div class="mc-collapse">
+        <div
+          v-for="item in displayCrnList"
+          :key="item.crnNo"
+          :class="['mc-item', { 'is-open': isActive(item.crnNo) }]"
+        >
+          <button type="button" class="mc-head" @click="toggleItem(item)">
+            <div class="mc-head-main">
+              <div class="mc-head-title">{{ item.crnNo }}鍙峰爢鍨涙満</div>
+              <div class="mc-head-subtitle">宸ヤ綔鍙� {{ orDash(item.workNo) }} | 鐩爣 {{ orDash(item.locNo) }}</div>
             </div>
-            <div v-if="showControl" style="display: flex;justify-content: space-between;flex-wrap: wrap;">
-                <div style="margin-bottom: 10px;width: 33%;"><el-input size="mini" v-model="controlParam.crnNo" placeholder="鍫嗗灈鏈哄彿"></el-input></div>
-                <div style="margin-bottom: 10px;width: 33%;"><el-input size="mini" v-model="controlParam.sourceLocNo" placeholder="婧愮偣"></el-input></div>
-                <div style="margin-bottom: 10px;width: 33%;"><el-input size="mini" v-model="controlParam.targetLocNo" placeholder="鐩爣鐐�"></el-input></div>
-                <div style="margin-bottom: 10px;"><el-button @click="controlCommandTransport()" size="mini">鍙栨斁璐�</el-button></div>
-                <div style="margin-bottom: 10px;"><el-button @click="controlCommandMove()" size="mini">绉诲姩</el-button></div>
-                <div style="margin-bottom: 10px;"><el-button @click="controlCommandTaskComplete()" size="mini">浠诲姟瀹屾垚</el-button></div>
+            <div class="mc-head-right">
+              <span :class="['mc-badge', 'is-' + getStatusTone(item)]">{{ getStatusLabel(item) }}</span>
+              <span class="mc-chevron">{{ isActive(item.crnNo) ? '鈻�' : '鈻�' }}</span>
             </div>
+          </button>
+
+          <div v-if="isActive(item.crnNo)" class="mc-body">
+            <div class="mc-detail-grid">
+              <div v-for="entry in buildDetailEntries(item)" :key="entry.label" class="mc-detail-cell">
+                <div class="mc-detail-label">{{ entry.label }}</div>
+                <div class="mc-detail-value">{{ entry.value }}</div>
+              </div>
+            </div>
+          </div>
         </div>
-        <div style="max-height: 55vh; overflow:auto;">
-          <el-collapse v-model="activeNames" accordion>
-            <el-collapse-item v-for="(item) in displayCrnList" :name="item.crnNo">
-            <template slot="title">
-                <div style="width: 100%;display: flex;">
-                   <div style="width: 50%;">{{ item.crnNo }}鍙峰爢鍨涙満</div>
-                   <div style="width: 50%;text-align: right;">
-                      <el-tag v-if="item.deviceStatus == 'AUTO'" type="success" size="small">鑷姩</el-tag>
-                      <el-tag v-else-if="item.deviceStatus == 'WORKING'" size="small">浣滀笟涓�</el-tag>
-                      <el-tag v-else-if="item.deviceStatus == 'ERROR'" type="danger" size="small">鏁呴殰</el-tag>
-                      <el-tag v-else type="warning" size="small">绂荤嚎</el-tag>
-                   </div>
-                </div>
-            </template>
-            <el-descriptions border direction="vertical">
-                <el-descriptions-item label="缂栧彿">{{ item.crnNo }}</el-descriptions-item>
-                <el-descriptions-item label="宸ヤ綔鍙�">{{ item.workNo }}</el-descriptions-item>
-                <el-descriptions-item label="妯″紡">{{ item.mode }}</el-descriptions-item>
-                <el-descriptions-item label="鐘舵��">{{ item.status }}</el-descriptions-item>
-                <el-descriptions-item label="婧愬簱浣�">{{ item.sourceLocNo }}</el-descriptions-item>
-                <el-descriptions-item label="鐩爣搴撲綅">{{ item.locNo }}</el-descriptions-item>
-                <el-descriptions-item label="鏄惁鏈夌墿">{{ item.loading }}</el-descriptions-item>
-                <el-descriptions-item label="浠诲姟鎺ユ敹">{{ item.taskReceive }}</el-descriptions-item>
-                <el-descriptions-item label="鍒�">{{ item.bay }}</el-descriptions-item>
-                <el-descriptions-item label="灞�">{{ item.lev }}</el-descriptions-item>
-                <el-descriptions-item label="璐у弶瀹氫綅">{{ item.forkOffset }}</el-descriptions-item>
-                <el-descriptions-item label="杞借揣鍙板畾浣�">{{ item.liftPos }}</el-descriptions-item>
-                <el-descriptions-item label="璧拌鍦ㄥ畾浣�">{{ item.walkPos }}</el-descriptions-item>
-                <el-descriptions-item label="璧拌閫熷害锛坢/min)">{{ item.xspeed }}</el-descriptions-item>
-                <el-descriptions-item label="鍗囬檷閫熷害锛坢/min)">{{ item.yspeed }}</el-descriptions-item>
-                <el-descriptions-item label="鍙夌墮閫熷害锛坢/min)">{{ item.zspeed }}</el-descriptions-item>
-                <el-descriptions-item label="璧拌璺濈(Km)">{{ item.xdistance }}</el-descriptions-item>
-                <el-descriptions-item label="鍗囬檷璺濈(Km)">{{ item.ydistance }}</el-descriptions-item>
-                <el-descriptions-item label="璧拌鏃堕暱(H)">{{ item.xduration }}</el-descriptions-item>
-                <el-descriptions-item label="鍗囬檷鏃堕暱(H)">{{ item.yduration }}</el-descriptions-item>
-                <el-descriptions-item label="绉伴噸鏁版嵁">{{ item.weight }}</el-descriptions-item>
-                <el-descriptions-item label="鏉$爜鏁版嵁">{{ item.barcode }}</el-descriptions-item>
-                <el-descriptions-item label="鏁呴殰浠g爜">{{ item.warnCode }}</el-descriptions-item>
-                <el-descriptions-item label="鏁呴殰鎻忚堪">{{ item.alarm }}</el-descriptions-item>
-                <el-descriptions-item label="鎵╁睍鏁版嵁">{{ item.extend }}</el-descriptions-item>
-            </el-descriptions>
-            </el-collapse-item>
-          </el-collapse>
-        </div>
-        <div style="display:flex; justify-content:flex-end; margin-top:8px;">
-          <el-pagination
-            @current-change="handlePageChange"
-            @size-change="handleSizeChange"
-            :current-page="currentPage"
-            :page-size="pageSize"
-            :page-sizes="[10,20,50,100]"
-            layout="total, prev, pager, next"
-            :total="crnList.length">
-          </el-pagination>
-        </div>
+
+        <div v-if="displayCrnList.length === 0" class="mc-empty">褰撳墠娌℃湁鍙睍绀虹殑鍫嗗灈鏈烘暟鎹�</div>
+      </div>
+
+      <div class="mc-footer">
+        <button type="button" class="mc-page-btn" :disabled="currentPage <= 1" @click="handlePageChange(currentPage - 1)">涓婁竴椤�</button>
+        <span>{{ currentPage }} / {{ totalPages }}</span>
+        <button type="button" class="mc-page-btn" :disabled="currentPage >= totalPages" @click="handlePageChange(currentPage + 1)">涓嬩竴椤�</button>
+      </div>
     </div>
-    `,
+  `,
   props: {
-    param: {
-      type: Object,
-      default: () => ({})
-    },
-    autoRefresh: {
-      type: Boolean,
-      default: true
-    },
-    readOnly: {
-      type: Boolean,
-      default: false
-    }
+    param: { type: Object, default: function () { return {}; } },
+    items: { type: Array, default: null },
+    autoRefresh: { type: Boolean, default: true },
+    readOnly: { type: Boolean, default: false }
   },
-  data() {
+  data: function () {
     return {
       crnList: [],
       activeNames: "",
@@ -99,159 +89,176 @@
       controlParam: {
         crnNo: "",
         sourceLocNo: "",
-        targetLocNo: "",
+        targetLocNo: ""
       },
-      pageSize: 25,
+      pageSize: 12,
       currentPage: 1,
       timer: null
     };
   },
-  created() {
-    if (this.autoRefresh) {
-      this.timer = setInterval(() => {
-        this.getCrnStateInfo();
-      }, 1000);
-    }
-  },
-  beforeDestroy() {
-    if (this.timer) {
-      clearInterval(this.timer);
-    }
-  },
   computed: {
-    displayCrnList() {
-      const start = (this.currentPage - 1) * this.pageSize;
-      const end = start + this.pageSize;
-      return this.crnList.slice(start, end);
+    sourceList: function () {
+      return Array.isArray(this.items) ? this.items : this.crnList;
+    },
+    filteredCrnList: function () {
+      var keyword = String(this.searchCrnNo || "").trim();
+      if (!keyword) {
+        return this.sourceList;
+      }
+      return this.sourceList.filter(function (item) {
+        return String(item.crnNo) === keyword;
+      });
+    },
+    displayCrnList: function () {
+      var start = (this.currentPage - 1) * this.pageSize;
+      return this.filteredCrnList.slice(start, start + this.pageSize);
+    },
+    totalPages: function () {
+      return Math.max(1, Math.ceil(this.filteredCrnList.length / this.pageSize) || 1);
     }
   },
   watch: {
-    param: {
-      handler(newVal, oldVal) {
-        if (newVal && newVal.crnNo && newVal.crnNo != 0) {
-          this.activeNames = newVal.crnNo;
-          this.searchCrnNo = newVal.crnNo;
-          const idx = this.crnList.findIndex(i => i.crnNo == newVal.crnNo);
-          if (idx >= 0) { this.currentPage = Math.floor(idx / this.pageSize) + 1; }
-        }
-      },
-      deep: true, // 娣卞害鐩戝惉宓屽灞炴��
-      immediate: true, // 绔嬪嵆瑙﹀彂涓�娆★紙鍙�夛級
+    items: function () {
+      this.afterDataRefresh();
     },
+    param: {
+      deep: true,
+      immediate: true,
+      handler: function (newVal) {
+        if (newVal && newVal.crnNo && newVal.crnNo !== 0) {
+          this.focusCrn(newVal.crnNo);
+        }
+      }
+    }
+  },
+  created: function () {
+    MonitorCardKit.ensureStyles();
+    if (this.autoRefresh) {
+      this.timer = setInterval(this.getCrnStateInfo, 1000);
+    }
+  },
+  beforeDestroy: function () {
+    if (this.timer) {
+      clearInterval(this.timer);
+      this.timer = null;
+    }
   },
   methods: {
-    handlePageChange(page) {
+    orDash: function (value) {
+      return MonitorCardKit.orDash(value);
+    },
+    getStatusLabel: function (item) {
+      return MonitorCardKit.deviceStatusLabel(item && item.deviceStatus);
+    },
+    getStatusTone: function (item) {
+      return MonitorCardKit.statusTone(this.getStatusLabel(item));
+    },
+    isActive: function (crnNo) {
+      return String(this.activeNames) === String(crnNo);
+    },
+    toggleItem: function (item) {
+      var next = String(item.crnNo);
+      this.activeNames = this.activeNames === next ? "" : next;
+    },
+    focusCrn: function (crnNo) {
+      this.searchCrnNo = String(crnNo);
+      var index = this.filteredCrnList.findIndex(function (item) {
+        return String(item.crnNo) === String(crnNo);
+      });
+      if (index >= 0) {
+        this.currentPage = Math.floor(index / this.pageSize) + 1;
+      } else {
+        this.currentPage = 1;
+      }
+      this.activeNames = String(crnNo);
+    },
+    afterDataRefresh: function () {
+      if (this.currentPage > this.totalPages) {
+        this.currentPage = this.totalPages;
+      }
+      if (this.activeNames) {
+        var exists = this.filteredCrnList.some(function (item) {
+          return String(item.crnNo) === String(this.activeNames);
+        }, this);
+        if (!exists) {
+          this.activeNames = "";
+        }
+      }
+    },
+    handlePageChange: function (page) {
+      if (page < 1 || page > this.totalPages) {
+        return;
+      }
       this.currentPage = page;
     },
-    handleSizeChange(size) {
-      this.pageSize = size;
-      this.currentPage = 1;
-    },
-    getCrnStateInfo() {
-      if (this.$root.sendWs) {
+    getCrnStateInfo: function () {
+      if (this.$root && this.$root.sendWs) {
         this.$root.sendWs(JSON.stringify({
-          "url": "/crn/table/crn/state",
-          "data": {}
+          url: "/crn/table/crn/state",
+          data: {}
         }));
       }
     },
-    setCrnList(res) {
-      let that = this;
-      if (res.code == 200) {
-        let list = res.data;
-
-        if (that.searchCrnNo == "") {
-          that.crnList = list;
-        } else {
-          let tmp = [];
-          list.forEach((item) => {
-            if (item.crnNo == that.searchCrnNo) {
-              tmp.push(item);
-            }
-          });
-          that.crnList = tmp;
-          that.currentPage = 1;
-        }
+    setCrnList: function (res) {
+      if (res && res.code === 200) {
+        this.crnList = res.data || [];
+        this.afterDataRefresh();
       }
     },
-    openControl() {
+    openControl: function () {
       this.showControl = !this.showControl;
     },
-    controlCommandTransport() {
-      let that = this;
-      //鍙栨斁璐�
+    buildDetailEntries: function (item) {
+      return [
+        { label: "缂栧彿", value: this.orDash(item.crnNo) },
+        { label: "宸ヤ綔鍙�", value: this.orDash(item.workNo) },
+        { label: "妯″紡", value: this.orDash(item.mode) },
+        { label: "鐘舵��", value: this.orDash(item.status) },
+        { label: "婧愬簱浣�", value: this.orDash(item.sourceLocNo) },
+        { label: "鐩爣搴撲綅", value: this.orDash(item.locNo) },
+        { label: "鏄惁鏈夌墿", value: MonitorCardKit.yesNo(item.loading) },
+        { label: "浠诲姟鎺ユ敹", value: this.orDash(item.taskReceive) },
+        { label: "鍒�", value: this.orDash(item.bay) },
+        { label: "灞�", value: this.orDash(item.lev) },
+        { label: "璐у弶瀹氫綅", value: this.orDash(item.forkOffset) },
+        { label: "杞借揣鍙板畾浣�", value: this.orDash(item.liftPos) },
+        { label: "璧拌瀹氫綅", value: this.orDash(item.walkPos) },
+        { label: "璧拌閫熷害", value: this.orDash(item.xspeed) },
+        { label: "鍗囬檷閫熷害", value: this.orDash(item.yspeed) },
+        { label: "鍙夌墮閫熷害", value: this.orDash(item.zspeed) },
+        { label: "绉伴噸鏁版嵁", value: this.orDash(item.weight) },
+        { label: "鏉$爜鏁版嵁", value: this.orDash(item.barcode) },
+        { label: "鏁呴殰浠g爜", value: this.orDash(item.warnCode) },
+        { label: "鏁呴殰鎻忚堪", value: this.orDash(item.alarm) },
+        { label: "鎵╁睍鏁版嵁", value: this.orDash(item.extend) }
+      ];
+    },
+    postControl: function (url, payload) {
       $.ajax({
-        url: baseUrl + "/crn/command/take",
+        url: baseUrl + url,
         headers: {
-          token: localStorage.getItem("token"),
+          token: localStorage.getItem("token")
         },
         contentType: "application/json",
         method: "post",
-        data: JSON.stringify(that.controlParam),
-        success: (res) => {
-          if (res.code == 200) {
-            that.$message({
-              message: res.msg,
-              type: "success",
-            });
+        data: JSON.stringify(payload),
+        success: function (res) {
+          if (res && res.code === 200) {
+            MonitorCardKit.showMessage(this, res.msg || "鎿嶄綔鎴愬姛", "success");
           } else {
-            that.$message({
-              message: res.msg,
-              type: "warning",
-            });
+            MonitorCardKit.showMessage(this, (res && res.msg) || "鎿嶄綔澶辫触", "warning");
           }
-        },
+        }.bind(this)
       });
     },
-    controlCommandMove() {
-      let that = this;
-      $.ajax({
-        url: baseUrl + "/crn/command/move",
-        headers: {
-          token: localStorage.getItem("token"),
-        },
-        contentType: "application/json",
-        method: "post",
-        data: JSON.stringify(that.controlParam),
-        success: (res) => {
-          if (res.code == 200) {
-            that.$message({
-              message: res.msg,
-              type: "success",
-            });
-          } else {
-            that.$message({
-              message: res.msg,
-              type: "warning",
-            });
-          }
-        },
-      });
+    controlCommandTransport: function () {
+      this.postControl("/crn/command/take", this.controlParam);
     },
-    controlCommandTaskComplete() {
-      let that = this;
-      $.ajax({
-        url: baseUrl + "/crn/command/taskComplete",
-        headers: {
-          token: localStorage.getItem("token"),
-        },
-        contentType: "application/json",
-        method: "post",
-        data: JSON.stringify(that.controlParam),
-        success: (res) => {
-          if (res.code == 200) {
-            that.$message({
-              message: res.msg,
-              type: "success",
-            });
-          } else {
-            that.$message({
-              message: res.msg,
-              type: "warning",
-            });
-          }
-        },
-      });
+    controlCommandMove: function () {
+      this.postControl("/crn/command/move", this.controlParam);
     },
-  },
+    controlCommandTaskComplete: function () {
+      this.postControl("/crn/command/taskComplete", this.controlParam);
+    }
+  }
 });
diff --git a/src/main/webapp/components/WatchDualCrnCard.js b/src/main/webapp/components/WatchDualCrnCard.js
index 6845338..886db1e 100644
--- a/src/main/webapp/components/WatchDualCrnCard.js
+++ b/src/main/webapp/components/WatchDualCrnCard.js
@@ -1,128 +1,99 @@
 Vue.component("watch-dual-crn-card", {
   template: `
-    <div>
-        <div style="display: flex;margin-bottom: 10px;">
-            <div style="width: 100%;">鍙屽伐浣嶅爢鍨涙満鐩戞帶</div>
-            <div style="width: 100%;text-align: right;display: flex;">
-              <el-input size="mini" v-model="searchCrnNo" placeholder="璇疯緭鍏ュ爢鍨涙満鍙�"></el-input>
-              <el-button @click="getDualCrnStateInfo" size="mini">鏌ヨ</el-button>
-            </div>
+    <div class="mc-root">
+      <div class="mc-toolbar">
+        <div class="mc-title">鍙屽伐浣嶅爢鍨涙満鐩戞帶</div>
+        <div class="mc-search">
+          <input class="mc-input" v-model="searchCrnNo" placeholder="璇疯緭鍏ュ爢鍨涙満鍙�" />
+          <button type="button" class="mc-btn mc-btn-ghost" @click="getDualCrnStateInfo">鏌ヨ</button>
         </div>
-        <div style="margin-bottom: 10px;" v-if="!readOnly">
-          <div style="margin-bottom: 5px;">
-            <el-button v-if="showControl" @click="openControl" size="mini">鍏抽棴鎺у埗涓績</el-button>
-            <el-button v-else @click="openControl" size="mini">鎵撳紑鎺у埗涓績</el-button>
-          </div>
-          <div v-if="showControl" style="display: flex;justify-content: space-between;flex-wrap: wrap;">
-            <div style="margin-bottom: 10px;width: 33%;"><el-input size="mini" v-model="controlParam.crnNo" placeholder="鍫嗗灈鏈哄彿"></el-input></div>
-            <div style="margin-bottom: 10px;width: 33%;"><el-input size="mini" v-model="controlParam.sourceLocNo" placeholder="婧愮偣"></el-input></div>
-            <div style="margin-bottom: 10px;width: 33%;"><el-input size="mini" v-model="controlParam.targetLocNo" placeholder="鐩爣鐐�"></el-input></div>
-            <div style="margin-bottom: 10px;width: 33%;">
-              <el-select size="mini" v-model="controlParam.station" placeholder="宸ヤ綅">
-                <el-option :label="'宸ヤ綅1'" :value="1"></el-option>
-                <el-option :label="'宸ヤ綅2'" :value="2"></el-option>
-              </el-select>
-            </div>
-            <div style="margin-bottom: 10px;"><el-button @click="controlCommandTransport()" size="mini">鍙栨斁璐�</el-button></div>
-            <div style="margin-bottom: 10px;"><el-button @click="controlCommandPickup()" size="mini">鍙栬揣</el-button></div>
-            <div style="margin-bottom: 10px;"><el-button @click="controlCommandPutdown()" size="mini">鏀捐揣</el-button></div>
-            <div style="margin-bottom: 10px;"><el-button @click="controlCommandMove()" size="mini">绉诲姩</el-button></div>
-            <div style="margin-bottom: 10px;"><el-button @click="controlCommandTaskComplete()" size="mini">浠诲姟瀹屾垚</el-button></div>
+      </div>
+
+      <div v-if="!readOnly" class="mc-control-toggle">
+        <button type="button" class="mc-btn mc-btn-ghost" @click="openControl">
+          {{ showControl ? '鏀惰捣鎺у埗涓績' : '鎵撳紑鎺у埗涓績' }}
+        </button>
+      </div>
+
+      <div v-if="showControl" class="mc-control">
+        <div class="mc-control-grid">
+          <label class="mc-field">
+            <span class="mc-field-label">鍫嗗灈鏈哄彿</span>
+            <input class="mc-input" v-model="controlParam.crnNo" placeholder="渚嬪 2" />
+          </label>
+          <label class="mc-field">
+            <span class="mc-field-label">宸ヤ綅</span>
+            <select class="mc-select" v-model="controlParam.station">
+              <option :value="1">宸ヤ綅1</option>
+              <option :value="2">宸ヤ綅2</option>
+            </select>
+          </label>
+          <label class="mc-field">
+            <span class="mc-field-label">婧愬簱浣�</span>
+            <input class="mc-input" v-model="controlParam.sourceLocNo" placeholder="杈撳叆婧愮偣" />
+          </label>
+          <label class="mc-field">
+            <span class="mc-field-label">鐩爣搴撲綅</span>
+            <input class="mc-input" v-model="controlParam.targetLocNo" placeholder="杈撳叆鐩爣鐐�" />
+          </label>
+          <div class="mc-action-row">
+            <button type="button" class="mc-btn" @click="controlCommandTransport">鍙栨斁璐�</button>
+            <button type="button" class="mc-btn mc-btn-ghost" @click="controlCommandPickup">鍙栬揣</button>
+            <button type="button" class="mc-btn mc-btn-ghost" @click="controlCommandPutdown">鏀捐揣</button>
+            <button type="button" class="mc-btn mc-btn-ghost" @click="controlCommandMove">绉诲姩</button>
+            <button type="button" class="mc-btn mc-btn-soft" @click="controlCommandTaskComplete">浠诲姟瀹屾垚</button>
           </div>
         </div>
-        <div style="max-height: 55vh; overflow:auto;">
-          <el-collapse v-model="activeNames" accordion>
-            <el-collapse-item v-for="(item) in displayCrnList" :name="item.crnNo">
-              <template slot="title">
-                <div style="width: 100%;display: flex;">
-                  <div style="width: 50%;">{{ item.crnNo }}鍙峰弻宸ヤ綅鍫嗗灈鏈�</div>
-                  <div style="width: 50%;text-align: right;">
-                    <el-tag v-if="item.deviceStatus == 'AUTO'" type="success" size="small">鑷姩</el-tag>
-                    <el-tag v-else-if="item.deviceStatus == 'WORKING'" size="small">浣滀笟涓�</el-tag>
-                    <el-tag v-else-if="item.deviceStatus == 'ERROR'" type="danger" size="small">鏁呴殰</el-tag>
-                    <el-tag v-else type="warning" size="small">绂荤嚎</el-tag>
-                  </div>
-                </div>
-              </template>
-              <el-descriptions border direction="vertical">
-                <el-descriptions-item label="妯″紡">{{ item.mode }}</el-descriptions-item>
-                <el-descriptions-item label="寮傚父鐮�">{{ item.warnCode }}</el-descriptions-item>
-                <el-descriptions-item label="宸ヤ綅1浠诲姟鍙�">
-                  <span v-if="readOnly">{{ item.taskNo }}</span>
-                  <el-button
-                    v-else
-                    type="text"
-                    size="mini"
-                    style="padding:0;"
-                    @click.stop="editTaskNo(item, 1)"
-                  >{{ item.taskNo }}</el-button>
-                </el-descriptions-item>
-                <el-descriptions-item label="宸ヤ綅2浠诲姟鍙�">
-                  <span v-if="readOnly">{{ item.taskNoTwo }}</span>
-                  <el-button
-                    v-else
-                    type="text"
-                    size="mini"
-                    style="padding:0;"
-                    @click.stop="editTaskNo(item, 2)"
-                  >{{ item.taskNoTwo }}</el-button>
-                </el-descriptions-item>
-                <el-descriptions-item label="璁惧宸ヤ綅1浠诲姟鍙�">{{ item.deviceTaskNo }}</el-descriptions-item>
-                <el-descriptions-item label="璁惧宸ヤ綅2浠诲姟鍙�">{{ item.deviceTaskNoTwo }}</el-descriptions-item>
-                <el-descriptions-item label="宸ヤ綅1鐘舵��">{{ item.status }}</el-descriptions-item>
-                <el-descriptions-item label="宸ヤ綅2鐘舵��">{{ item.statusTwo }}</el-descriptions-item>
-                <el-descriptions-item label="宸ヤ綅1鏄惁鏈夌墿">{{ item.loading }}</el-descriptions-item>
-                <el-descriptions-item label="宸ヤ綅2鏄惁鏈夌墿">{{ item.loadingTwo }}</el-descriptions-item>
-                <el-descriptions-item label="宸ヤ綅1璐у弶瀹氫綅">{{ item.forkOffset }}</el-descriptions-item>
-                <el-descriptions-item label="宸ヤ綅2璐у弶瀹氫綅">{{ item.forkOffsetTwo }}</el-descriptions-item>
-                <el-descriptions-item label="宸ヤ綅1浠诲姟鎺ユ敹">{{ item.taskReceive }}</el-descriptions-item>
-                <el-descriptions-item label="宸ヤ綅2浠诲姟鎺ユ敹">{{ item.taskReceiveTwo }}</el-descriptions-item>
-                <el-descriptions-item label="宸ヤ綅1涓嬪彂鏁版嵁">{{ item.taskSend }}</el-descriptions-item>
-                <el-descriptions-item label="宸ヤ綅2涓嬪彂鏁版嵁">{{ item.taskSendTwo }}</el-descriptions-item>
-                <el-descriptions-item label="鍒�">{{ item.bay }}</el-descriptions-item>
-                <el-descriptions-item label="灞�">{{ item.lev }}</el-descriptions-item>
-                <el-descriptions-item label="杞借揣鍙板畾浣�">{{ item.liftPos }}</el-descriptions-item>
-                <el-descriptions-item label="璧拌鍦ㄥ畾浣�">{{ item.walkPos }}</el-descriptions-item>
-                <el-descriptions-item label="璧拌閫熷害锛坢/min)">{{ item.xspeed }}</el-descriptions-item>
-                <el-descriptions-item label="鍗囬檷閫熷害锛坢/min)">{{ item.yspeed }}</el-descriptions-item>
-                <el-descriptions-item label="鍙夌墮閫熷害锛坢/min)">{{ item.zspeed }}</el-descriptions-item>
-                <el-descriptions-item label="璧拌璺濈(Km)">{{ item.xdistance }}</el-descriptions-item>
-                <el-descriptions-item label="鍗囬檷璺濈(Km)">{{ item.ydistance }}</el-descriptions-item>
-                <el-descriptions-item label="璧拌鏃堕暱(H)">{{ item.xduration }}</el-descriptions-item>
-                <el-descriptions-item label="鍗囬檷鏃堕暱(H)">{{ item.yduration }}</el-descriptions-item>
-                <el-descriptions-item label="鎵╁睍鏁版嵁">{{ item.extend }}</el-descriptions-item>
-              </el-descriptions>
-            </el-collapse-item>
-          </el-collapse>
+      </div>
+
+      <div class="mc-collapse">
+        <div
+          v-for="item in displayCrnList"
+          :key="item.crnNo"
+          :class="['mc-item', { 'is-open': isActive(item.crnNo) }]"
+        >
+          <button type="button" class="mc-head" @click="toggleItem(item)">
+            <div class="mc-head-main">
+              <div class="mc-head-title">{{ item.crnNo }}鍙峰弻宸ヤ綅鍫嗗灈鏈�</div>
+              <div class="mc-head-subtitle">宸ヤ綅1 {{ orDash(item.taskNo) }} | 宸ヤ綅2 {{ orDash(item.taskNoTwo) }}</div>
+            </div>
+            <div class="mc-head-right">
+              <span :class="['mc-badge', 'is-' + getStatusTone(item)]">{{ getStatusLabel(item) }}</span>
+              <span class="mc-chevron">{{ isActive(item.crnNo) ? '鈻�' : '鈻�' }}</span>
+            </div>
+          </button>
+
+          <div v-if="isActive(item.crnNo)" class="mc-body">
+            <div class="mc-inline-actions" v-if="!readOnly">
+              <button type="button" class="mc-link" @click.stop="editTaskNo(item, 1)">缂栬緫宸ヤ綅1浠诲姟鍙�</button>
+              <button type="button" class="mc-link" @click.stop="editTaskNo(item, 2)">缂栬緫宸ヤ綅2浠诲姟鍙�</button>
+            </div>
+            <div class="mc-detail-grid">
+              <div v-for="entry in buildDetailEntries(item)" :key="entry.label" class="mc-detail-cell">
+                <div class="mc-detail-label">{{ entry.label }}</div>
+                <div class="mc-detail-value">{{ entry.value }}</div>
+              </div>
+            </div>
+          </div>
         </div>
-        <div style="display:flex; justify-content:flex-end; margin-top:8px;">
-          <el-pagination
-            @current-change="handlePageChange"
-            @size-change="handleSizeChange"
-            :current-page="currentPage"
-            :page-size="pageSize"
-            :page-sizes="[10,20,50,100]"
-            layout="total, prev, pager, next"
-            :total="crnList.length">
-          </el-pagination>
-        </div>
+
+        <div v-if="displayCrnList.length === 0" class="mc-empty">褰撳墠娌℃湁鍙睍绀虹殑鍙屽伐浣嶅爢鍨涙満鏁版嵁</div>
+      </div>
+
+      <div class="mc-footer">
+        <button type="button" class="mc-page-btn" :disabled="currentPage <= 1" @click="handlePageChange(currentPage - 1)">涓婁竴椤�</button>
+        <span>{{ currentPage }} / {{ totalPages }}</span>
+        <button type="button" class="mc-page-btn" :disabled="currentPage >= totalPages" @click="handlePageChange(currentPage + 1)">涓嬩竴椤�</button>
+      </div>
     </div>
   `,
   props: {
-    param: {
-      type: Object,
-      default: () => ({})
-    },
-    autoRefresh: {
-      type: Boolean,
-      default: true
-    },
-    readOnly: {
-      type: Boolean,
-      default: false
-    }
+    param: { type: Object, default: function () { return {}; } },
+    items: { type: Array, default: null },
+    autoRefresh: { type: Boolean, default: true },
+    readOnly: { type: Boolean, default: false }
   },
-  data() {
+  data: function () {
     return {
       crnList: [],
       activeNames: "",
@@ -132,251 +103,212 @@
         crnNo: "",
         sourceLocNo: "",
         targetLocNo: "",
-        station: 1,
+        station: 1
       },
-      pageSize: 25,
+      pageSize: 12,
       currentPage: 1,
       timer: null
     };
   },
-  created() {
-    if (this.autoRefresh) {
-      this.timer = setInterval(() => {
-        this.getDualCrnStateInfo();
-      }, 1000);
-    }
-  },
-  beforeDestroy() {
-    if (this.timer) {
-      clearInterval(this.timer);
-    }
-  },
   computed: {
-    displayCrnList() {
-      const start = (this.currentPage - 1) * this.pageSize;
-      const end = start + this.pageSize;
-      return this.crnList.slice(start, end);
+    sourceList: function () {
+      return Array.isArray(this.items) ? this.items : this.crnList;
+    },
+    filteredCrnList: function () {
+      var keyword = String(this.searchCrnNo || "").trim();
+      if (!keyword) {
+        return this.sourceList;
+      }
+      return this.sourceList.filter(function (item) {
+        return String(item.crnNo) === keyword;
+      });
+    },
+    displayCrnList: function () {
+      var start = (this.currentPage - 1) * this.pageSize;
+      return this.filteredCrnList.slice(start, start + this.pageSize);
+    },
+    totalPages: function () {
+      return Math.max(1, Math.ceil(this.filteredCrnList.length / this.pageSize) || 1);
     }
   },
   watch: {
+    items: function () {
+      this.afterDataRefresh();
+    },
     param: {
-      handler(newVal) {
-        if (newVal && newVal.crnNo && newVal.crnNo != 0) {
-          this.activeNames = newVal.crnNo;
-          this.searchCrnNo = newVal.crnNo;
-          const idx = this.crnList.findIndex(i => i.crnNo == newVal.crnNo);
-          if (idx >= 0) { this.currentPage = Math.floor(idx / this.pageSize) + 1; }
-        }
-      },
       deep: true,
       immediate: true,
-    },
+      handler: function (newVal) {
+        if (newVal && newVal.crnNo && newVal.crnNo !== 0) {
+          this.focusCrn(newVal.crnNo);
+        }
+      }
+    }
+  },
+  created: function () {
+    MonitorCardKit.ensureStyles();
+    if (this.autoRefresh) {
+      this.timer = setInterval(this.getDualCrnStateInfo, 1000);
+    }
+  },
+  beforeDestroy: function () {
+    if (this.timer) {
+      clearInterval(this.timer);
+      this.timer = null;
+    }
   },
   methods: {
-    handlePageChange(page) {
+    orDash: function (value) {
+      return MonitorCardKit.orDash(value);
+    },
+    getStatusLabel: function (item) {
+      return MonitorCardKit.deviceStatusLabel(item && item.deviceStatus);
+    },
+    getStatusTone: function (item) {
+      return MonitorCardKit.statusTone(this.getStatusLabel(item));
+    },
+    isActive: function (crnNo) {
+      return String(this.activeNames) === String(crnNo);
+    },
+    toggleItem: function (item) {
+      var next = String(item.crnNo);
+      this.activeNames = this.activeNames === next ? "" : next;
+    },
+    focusCrn: function (crnNo) {
+      this.searchCrnNo = String(crnNo);
+      var index = this.filteredCrnList.findIndex(function (item) {
+        return String(item.crnNo) === String(crnNo);
+      });
+      this.currentPage = index >= 0 ? Math.floor(index / this.pageSize) + 1 : 1;
+      this.activeNames = String(crnNo);
+    },
+    afterDataRefresh: function () {
+      if (this.currentPage > this.totalPages) {
+        this.currentPage = this.totalPages;
+      }
+      if (this.activeNames) {
+        var exists = this.filteredCrnList.some(function (item) {
+          return String(item.crnNo) === String(this.activeNames);
+        }, this);
+        if (!exists) {
+          this.activeNames = "";
+        }
+      }
+    },
+    handlePageChange: function (page) {
+      if (page < 1 || page > this.totalPages) {
+        return;
+      }
       this.currentPage = page;
     },
-    handleSizeChange(size) {
-      this.pageSize = size;
-      this.currentPage = 1;
+    getDualCrnStateInfo: function () {
+      if (this.$root && this.$root.sendWs) {
+        this.$root.sendWs(JSON.stringify({
+          url: "/dualcrn/table/crn/state",
+          data: {}
+        }));
+      }
     },
-    openControl() {
+    setDualCrnList: function (res) {
+      if (res && res.code === 200) {
+        this.crnList = res.data || [];
+        this.afterDataRefresh();
+      }
+    },
+    openControl: function () {
       this.showControl = !this.showControl;
     },
-    editTaskNo(item, station) {
-      let that = this;
-      const isStationOne = station === 1;
-      const fieldName = isStationOne ? "taskNo" : "taskNoTwo";
-      const stationName = isStationOne ? "宸ヤ綅1" : "宸ヤ綅2";
-      const currentTaskNo = item[fieldName] == null ? "" : String(item[fieldName]);
-      that.$prompt("璇疯緭鍏�" + stationName + "浠诲姟鍙�", "缂栬緫浠诲姟鍙�", {
+    buildDetailEntries: function (item) {
+      return [
+        { label: "妯″紡", value: this.orDash(item.mode) },
+        { label: "寮傚父鐮�", value: this.orDash(item.warnCode) },
+        { label: "宸ヤ綅1浠诲姟鍙�", value: this.orDash(item.taskNo) },
+        { label: "宸ヤ綅2浠诲姟鍙�", value: this.orDash(item.taskNoTwo) },
+        { label: "璁惧宸ヤ綅1浠诲姟鍙�", value: this.orDash(item.deviceTaskNo) },
+        { label: "璁惧宸ヤ綅2浠诲姟鍙�", value: this.orDash(item.deviceTaskNoTwo) },
+        { label: "宸ヤ綅1鐘舵��", value: this.orDash(item.status) },
+        { label: "宸ヤ綅2鐘舵��", value: this.orDash(item.statusTwo) },
+        { label: "宸ヤ綅1鏄惁鏈夌墿", value: MonitorCardKit.yesNo(item.loading) },
+        { label: "宸ヤ綅2鏄惁鏈夌墿", value: MonitorCardKit.yesNo(item.loadingTwo) },
+        { label: "宸ヤ綅1璐у弶瀹氫綅", value: this.orDash(item.forkOffset) },
+        { label: "宸ヤ綅2璐у弶瀹氫綅", value: this.orDash(item.forkOffsetTwo) },
+        { label: "宸ヤ綅1浠诲姟鎺ユ敹", value: this.orDash(item.taskReceive) },
+        { label: "宸ヤ綅2浠诲姟鎺ユ敹", value: this.orDash(item.taskReceiveTwo) },
+        { label: "宸ヤ綅1涓嬪彂鏁版嵁", value: this.orDash(item.taskSend) },
+        { label: "宸ヤ綅2涓嬪彂鏁版嵁", value: this.orDash(item.taskSendTwo) },
+        { label: "鍒�", value: this.orDash(item.bay) },
+        { label: "灞�", value: this.orDash(item.lev) },
+        { label: "杞借揣鍙板畾浣�", value: this.orDash(item.liftPos) },
+        { label: "璧拌瀹氫綅", value: this.orDash(item.walkPos) },
+        { label: "璧拌閫熷害", value: this.orDash(item.xspeed) },
+        { label: "鍗囬檷閫熷害", value: this.orDash(item.yspeed) },
+        { label: "鍙夌墮閫熷害", value: this.orDash(item.zspeed) },
+        { label: "鎵╁睍鏁版嵁", value: this.orDash(item.extend) }
+      ];
+    },
+    postControl: function (url, payload) {
+      $.ajax({
+        url: baseUrl + url,
+        headers: {
+          token: localStorage.getItem("token")
+        },
+        contentType: "application/json",
+        method: "post",
+        data: JSON.stringify(payload),
+        success: function (res) {
+          if (res && res.code === 200) {
+            MonitorCardKit.showMessage(this, res.msg || "鎿嶄綔鎴愬姛", "success");
+          } else {
+            MonitorCardKit.showMessage(this, (res && res.msg) || "鎿嶄綔澶辫触", "warning");
+          }
+        }.bind(this)
+      });
+    },
+    editTaskNo: function (item, station) {
+      var currentValue = station === 1 ? item.taskNo : item.taskNoTwo;
+      this.$prompt("璇疯緭鍏ュ伐浣�" + station + "浠诲姟鍙�", "缂栬緫浠诲姟鍙�", {
         confirmButtonText: "纭畾",
         cancelButtonText: "鍙栨秷",
-        inputValue: currentTaskNo,
+        inputValue: currentValue == null ? "" : String(currentValue),
         inputPattern: /^\d+$/,
-        inputErrorMessage: "浠诲姟鍙峰繀椤绘槸闈炶礋鏁存暟",
-      }).then(({ value }) => {
-        const taskNo = Number(value);
+        inputErrorMessage: "浠诲姟鍙峰繀椤绘槸闈炶礋鏁存暟"
+      }).then(function (result) {
         $.ajax({
           url: baseUrl + "/dualcrn/command/updateTaskNo",
           headers: {
-            token: localStorage.getItem("token"),
+            token: localStorage.getItem("token")
           },
           contentType: "application/json",
           method: "post",
           data: JSON.stringify({
             crnNo: item.crnNo,
             station: station,
-            taskNo: taskNo,
+            taskNo: Number(result.value)
           }),
-          success: (res) => {
-            if (res.code == 200) {
-              item[fieldName] = taskNo;
-              that.$message({
-                message: stationName + "浠诲姟鍙锋洿鏂版垚鍔�",
-                type: "success",
-              });
-              that.getDualCrnStateInfo();
+          success: function (res) {
+            if (res && res.code === 200) {
+              MonitorCardKit.showMessage(this, "浠诲姟鍙锋洿鏂版垚鍔�", "success");
             } else {
-              that.$message({
-                message: res.msg,
-                type: "warning",
-              });
+              MonitorCardKit.showMessage(this, (res && res.msg) || "浠诲姟鍙锋洿鏂板け璐�", "warning");
             }
-          },
+          }.bind(this)
         });
-      }).catch(() => {});
+      }.bind(this)).catch(function () {});
     },
-    getDualCrnStateInfo() {
-      if (this.$root.sendWs) {
-        this.$root.sendWs(JSON.stringify({
-          "url": "/dualcrn/table/crn/state",
-          "data": {}
-        }));
-      }
+    controlCommandTransport: function () {
+      this.postControl("/dualcrn/command/take", this.controlParam);
     },
-    controlCommandTransport() {
-      let that = this;
-      $.ajax({
-        url: baseUrl + "/dualcrn/command/take",
-        headers: {
-          token: localStorage.getItem("token"),
-        },
-        contentType: "application/json",
-        method: "post",
-        data: JSON.stringify(that.controlParam),
-        success: (res) => {
-          if (res.code == 200) {
-            that.$message({
-              message: res.msg,
-              type: "success",
-            });
-          } else {
-            that.$message({
-              message: res.msg,
-              type: "warning",
-            });
-          }
-        },
-      });
+    controlCommandPickup: function () {
+      this.postControl("/dualcrn/command/pick", this.controlParam);
     },
-    controlCommandPickup() {
-      let that = this;
-      $.ajax({
-        url: baseUrl + "/dualcrn/command/pick",
-        headers: {
-          token: localStorage.getItem("token"),
-        },
-        contentType: "application/json",
-        method: "post",
-        data: JSON.stringify(that.controlParam),
-        success: (res) => {
-          if (res.code == 200) {
-            that.$message({
-              message: res.msg,
-              type: "success",
-            });
-          } else {
-            that.$message({
-              message: res.msg,
-              type: "warning",
-            });
-          }
-        },
-      });
+    controlCommandPutdown: function () {
+      this.postControl("/dualcrn/command/put", this.controlParam);
     },
-    controlCommandPutdown() {
-      let that = this;
-      $.ajax({
-        url: baseUrl + "/dualcrn/command/put",
-        headers: {
-          token: localStorage.getItem("token"),
-        },
-        contentType: "application/json",
-        method: "post",
-        data: JSON.stringify(that.controlParam),
-        success: (res) => {
-          if (res.code == 200) {
-            that.$message({
-              message: res.msg,
-              type: "success",
-            });
-          } else {
-            that.$message({
-              message: res.msg,
-              type: "warning",
-            });
-          }
-        },
-      });
+    controlCommandMove: function () {
+      this.postControl("/dualcrn/command/move", this.controlParam);
     },
-    controlCommandMove() {
-      let that = this;
-      $.ajax({
-        url: baseUrl + "/dualcrn/command/move",
-        headers: {
-          token: localStorage.getItem("token"),
-        },
-        contentType: "application/json",
-        method: "post",
-        data: JSON.stringify(that.controlParam),
-        success: (res) => {
-          if (res.code == 200) {
-            that.$message({
-              message: res.msg,
-              type: "success",
-            });
-          } else {
-            that.$message({
-              message: res.msg,
-              type: "warning",
-            });
-          }
-        },
-      });
-    },
-    controlCommandTaskComplete() {
-      let that = this;
-      $.ajax({
-        url: baseUrl + "/dualcrn/command/taskComplete",
-        headers: {
-          token: localStorage.getItem("token"),
-        },
-        contentType: "application/json",
-        method: "post",
-        data: JSON.stringify(that.controlParam),
-        success: (res) => {
-          if (res.code == 200) {
-            that.$message({
-              message: res.msg,
-              type: "success",
-            });
-          } else {
-            that.$message({
-              message: res.msg,
-              type: "warning",
-            });
-          }
-        },
-      });
-    },
-    setDualCrnList(res) {
-      let that = this;
-      if (res.code == 200) {
-        let list = res.data;
-        if (that.searchCrnNo == "") {
-          that.crnList = list;
-        } else {
-          let tmp = [];
-          list.forEach((item) => {
-            if (item.crnNo == that.searchCrnNo) {
-              tmp.push(item);
-            }
-          });
-          that.crnList = tmp;
-          that.currentPage = 1;
-        }
-      }
-    },
-  },
+    controlCommandTaskComplete: function () {
+      this.postControl("/dualcrn/command/taskComplete", this.controlParam);
+    }
+  }
 });
diff --git a/src/main/webapp/components/WatchRgvCard.js b/src/main/webapp/components/WatchRgvCard.js
index 5c00b7d..3c19e76 100644
--- a/src/main/webapp/components/WatchRgvCard.js
+++ b/src/main/webapp/components/WatchRgvCard.js
@@ -1,83 +1,86 @@
 Vue.component("watch-rgv-card", {
   template: `
-    <div>
-        <div style="display: flex;margin-bottom: 10px;">
-            <div style="width: 100%;">RGV鐩戞帶</div>
-            <div style="width: 100%;text-align: right;display: flex;">
-              <el-input size="mini" v-model="searchRgvNo" placeholder="璇疯緭鍏GV鍙�"></el-input>
-              <el-button @click="getRgvStateInfo" size="mini">鏌ヨ</el-button>
+    <div class="mc-root">
+      <div class="mc-toolbar">
+        <div class="mc-title">RGV鐩戞帶</div>
+        <div class="mc-search">
+          <input class="mc-input" v-model="searchRgvNo" placeholder="璇疯緭鍏GV鍙�" />
+          <button type="button" class="mc-btn mc-btn-ghost" @click="getRgvStateInfo">鏌ヨ</button>
+        </div>
+      </div>
+
+      <div v-if="!readOnly" class="mc-control-toggle">
+        <button type="button" class="mc-btn mc-btn-ghost" @click="openControl">
+          {{ showControl ? '鏀惰捣鎺у埗涓績' : '鎵撳紑鎺у埗涓績' }}
+        </button>
+      </div>
+
+      <div v-if="showControl" class="mc-control">
+        <div class="mc-control-grid">
+          <label class="mc-field">
+            <span class="mc-field-label">RGV鍙�</span>
+            <input class="mc-input" v-model="controlParam.rgvNo" placeholder="渚嬪 1" />
+          </label>
+          <label class="mc-field">
+            <span class="mc-field-label">婧愮偣</span>
+            <input class="mc-input" v-model="controlParam.sourcePos" placeholder="杈撳叆婧愮偣" />
+          </label>
+          <label class="mc-field mc-span-2">
+            <span class="mc-field-label">鐩爣鐐�</span>
+            <input class="mc-input" v-model="controlParam.targetPos" placeholder="杈撳叆鐩爣鐐�" />
+          </label>
+          <div class="mc-action-row">
+            <button type="button" class="mc-btn" @click="controlCommandTransport">鍙栨斁璐�</button>
+            <button type="button" class="mc-btn mc-btn-ghost" @click="controlCommandMove">绉诲姩</button>
+            <button type="button" class="mc-btn mc-btn-soft" @click="controlCommandTaskComplete">浠诲姟瀹屾垚</button>
+          </div>
+        </div>
+      </div>
+
+      <div class="mc-collapse">
+        <div
+          v-for="item in displayRgvList"
+          :key="item.rgvNo"
+          :class="['mc-item', { 'is-open': isActive(item.rgvNo) }]"
+        >
+          <button type="button" class="mc-head" @click="toggleItem(item)">
+            <div class="mc-head-main">
+              <div class="mc-head-title">{{ item.rgvNo }}鍙稲GV</div>
+              <div class="mc-head-subtitle">杞ㄩ亾浣� {{ orDash(item.trackSiteNo) }} | 浠诲姟 {{ orDash(item.taskNo) }}</div>
             </div>
-        </div>
-        <div style="margin-bottom: 10px;" v-if="!readOnly">
-            <div style="margin-bottom: 5px;">
-               <el-button v-if="showControl" @click="openControl" size="mini">鍏抽棴鎺у埗涓績</el-button>
-               <el-button v-else @click="openControl" size="mini">鎵撳紑鎺у埗涓績</el-button>
+            <div class="mc-head-right">
+              <span :class="['mc-badge', 'is-' + getStatusTone(item)]">{{ getStatusLabel(item) }}</span>
+              <span class="mc-chevron">{{ isActive(item.rgvNo) ? '鈻�' : '鈻�' }}</span>
             </div>
-            <div v-if="showControl" style="display: flex;justify-content: space-between;flex-wrap: wrap;">
-                <div style="margin-bottom: 10px;width: 33%;"><el-input size="mini" v-model="controlParam.rgvNo" placeholder="RGV鍙�"></el-input></div>
-                <div style="margin-bottom: 10px;width: 33%;"><el-input size="mini" v-model="controlParam.sourcePos" placeholder="婧愮偣"></el-input></div>
-                <div style="margin-bottom: 10px;width: 33%;"><el-input size="mini" v-model="controlParam.targetPos" placeholder="鐩爣鐐�"></el-input></div>
-                <div style="margin-bottom: 10px;"><el-button @click="controlCommandTransport()" size="mini">鍙栨斁璐�</el-button></div>
-                <div style="margin-bottom: 10px;"><el-button @click="controlCommandMove()" size="mini">绉诲姩</el-button></div>
-                <div style="margin-bottom: 10px;"><el-button @click="controlCommandTaskComplete()" size="mini">浠诲姟瀹屾垚</el-button></div>
+          </button>
+
+          <div v-if="isActive(item.rgvNo)" class="mc-body">
+            <div class="mc-detail-grid">
+              <div v-for="entry in buildDetailEntries(item)" :key="entry.label" class="mc-detail-cell">
+                <div class="mc-detail-label">{{ entry.label }}</div>
+                <div class="mc-detail-value">{{ entry.value }}</div>
+              </div>
             </div>
+          </div>
         </div>
-        <div style="max-height: 55vh; overflow:auto;">
-          <el-collapse v-model="activeNames" accordion>
-            <el-collapse-item v-for="(item) in displayRgvList" :name="item.rgvNo">
-            <template slot="title">
-                <div style="width: 100%;display: flex;">
-                   <div style="width: 50%;">{{ item.rgvNo }}鍙稲GV</div>
-                   <div style="width: 50%;text-align: right;">
-                      <el-tag v-if="item.deviceStatus === 'AUTO'" type="success" size="small">鑷姩</el-tag>
-                      <el-tag v-else-if="item.deviceStatus === 'WORKING'" size="small">浣滀笟涓�</el-tag>
-                      <el-tag v-else-if="item.deviceStatus === 'ERROR'" type="danger" size="small">鎶ヨ</el-tag>
-                      <el-tag v-else type="warning" size="small">绂荤嚎</el-tag>
-                   </div>
-                </div>
-            </template>
-            <el-descriptions border direction="vertical">
-                <el-descriptions-item label="缂栧彿">{{ item.rgvNo }}</el-descriptions-item>
-                <el-descriptions-item label="宸ヤ綔鍙�">{{ item.taskNo }}</el-descriptions-item>
-                <el-descriptions-item label="妯″紡">{{ item.mode }}</el-descriptions-item>
-                <el-descriptions-item label="鐘舵��">{{ item.status }}</el-descriptions-item>
-                <el-descriptions-item label="杞ㄩ亾浣�">{{ item.trackSiteNo }}</el-descriptions-item>
-                <el-descriptions-item label="鏄惁鏈夌墿">{{ item.loading }}</el-descriptions-item>
-                <el-descriptions-item label="鏁呴殰浠g爜">{{ item.warnCode }}</el-descriptions-item>
-                <el-descriptions-item label="鏁呴殰鎻忚堪">{{ item.alarm }}</el-descriptions-item>
-                <el-descriptions-item label="鎵╁睍鏁版嵁">{{ item.extend }}</el-descriptions-item>
-            </el-descriptions>
-            </el-collapse-item>
-          </el-collapse>
-        </div>
-        <div style="display:flex; justify-content:flex-end; margin-top:8px;">
-          <el-pagination
-            @current-change="handlePageChange"
-            @size-change="handleSizeChange"
-            :current-page="currentPage"
-            :page-size="pageSize"
-            :page-sizes="[10,20,50,100]"
-            layout="total, prev, pager, next"
-            :total="rgvList.length">
-          </el-pagination>
-        </div>
+
+        <div v-if="displayRgvList.length === 0" class="mc-empty">褰撳墠娌℃湁鍙睍绀虹殑RGV鏁版嵁</div>
+      </div>
+
+      <div class="mc-footer">
+        <button type="button" class="mc-page-btn" :disabled="currentPage <= 1" @click="handlePageChange(currentPage - 1)">涓婁竴椤�</button>
+        <span>{{ currentPage }} / {{ totalPages }}</span>
+        <button type="button" class="mc-page-btn" :disabled="currentPage >= totalPages" @click="handlePageChange(currentPage + 1)">涓嬩竴椤�</button>
+      </div>
     </div>
-    `,
+  `,
   props: {
-    param: {
-      type: Object,
-      default: () => ({})
-    },
-    autoRefresh: {
-      type: Boolean,
-      default: true
-    },
-    readOnly: {
-      type: Boolean,
-      default: false
-    }
+    param: { type: Object, default: function () { return {}; } },
+    items: { type: Array, default: null },
+    autoRefresh: { type: Boolean, default: true },
+    readOnly: { type: Boolean, default: false }
   },
-  data() {
+  data: function () {
     return {
       rgvList: [],
       activeNames: "",
@@ -88,155 +91,158 @@
         sourcePos: "",
         targetPos: ""
       },
-      pageSize: 25,
+      pageSize: 12,
       currentPage: 1,
       timer: null
     };
   },
-  created() {
-    if (this.autoRefresh) {
-      this.timer = setInterval(() => {
-        this.getRgvStateInfo();
-      }, 1000);
-    }
-  },
-  beforeDestroy() {
-    if (this.timer) {
-      clearInterval(this.timer);
-    }
-  },
   computed: {
-    displayRgvList() {
-      const start = (this.currentPage - 1) * this.pageSize;
-      const end = start + this.pageSize;
-      return this.rgvList.slice(start, end);
+    sourceList: function () {
+      return Array.isArray(this.items) ? this.items : this.rgvList;
+    },
+    filteredRgvList: function () {
+      var keyword = String(this.searchRgvNo || "").trim();
+      if (!keyword) {
+        return this.sourceList;
+      }
+      return this.sourceList.filter(function (item) {
+        return String(item.rgvNo) === keyword;
+      });
+    },
+    displayRgvList: function () {
+      var start = (this.currentPage - 1) * this.pageSize;
+      return this.filteredRgvList.slice(start, start + this.pageSize);
+    },
+    totalPages: function () {
+      return Math.max(1, Math.ceil(this.filteredRgvList.length / this.pageSize) || 1);
     }
   },
   watch: {
-    param: {
-      handler(newVal) {
-        if (newVal && newVal.rgvNo && newVal.rgvNo != 0) {
-          this.activeNames = newVal.rgvNo;
-          this.searchRgvNo = newVal.rgvNo;
-          const idx = this.rgvList.findIndex(i => i.rgvNo == newVal.rgvNo);
-          if (idx >= 0) { this.currentPage = Math.floor(idx / this.pageSize) + 1; }
-        }
-      },
-      deep: true,
-      immediate: true
+    items: function () {
+      this.afterDataRefresh();
     },
+    param: {
+      deep: true,
+      immediate: true,
+      handler: function (newVal) {
+        if (newVal && newVal.rgvNo && newVal.rgvNo !== 0) {
+          this.focusRgv(newVal.rgvNo);
+        }
+      }
+    }
+  },
+  created: function () {
+    MonitorCardKit.ensureStyles();
+    if (this.autoRefresh) {
+      this.timer = setInterval(this.getRgvStateInfo, 1000);
+    }
+  },
+  beforeDestroy: function () {
+    if (this.timer) {
+      clearInterval(this.timer);
+      this.timer = null;
+    }
   },
   methods: {
-    handlePageChange(page) {
+    orDash: function (value) {
+      return MonitorCardKit.orDash(value);
+    },
+    getStatusLabel: function (item) {
+      return MonitorCardKit.deviceStatusLabel(item && item.deviceStatus);
+    },
+    getStatusTone: function (item) {
+      return MonitorCardKit.statusTone(this.getStatusLabel(item));
+    },
+    isActive: function (rgvNo) {
+      return String(this.activeNames) === String(rgvNo);
+    },
+    toggleItem: function (item) {
+      var next = String(item.rgvNo);
+      this.activeNames = this.activeNames === next ? "" : next;
+    },
+    focusRgv: function (rgvNo) {
+      this.searchRgvNo = String(rgvNo);
+      var index = this.filteredRgvList.findIndex(function (item) {
+        return String(item.rgvNo) === String(rgvNo);
+      });
+      this.currentPage = index >= 0 ? Math.floor(index / this.pageSize) + 1 : 1;
+      this.activeNames = String(rgvNo);
+    },
+    afterDataRefresh: function () {
+      if (this.currentPage > this.totalPages) {
+        this.currentPage = this.totalPages;
+      }
+      if (this.activeNames) {
+        var exists = this.filteredRgvList.some(function (item) {
+          return String(item.rgvNo) === String(this.activeNames);
+        }, this);
+        if (!exists) {
+          this.activeNames = "";
+        }
+      }
+    },
+    handlePageChange: function (page) {
+      if (page < 1 || page > this.totalPages) {
+        return;
+      }
       this.currentPage = page;
     },
-    handleSizeChange(size) {
-      this.pageSize = size;
-      this.currentPage = 1;
-    },
-    getRgvStateInfo() {
-      if (this.$root.sendWs) {
+    getRgvStateInfo: function () {
+      if (this.$root && this.$root.sendWs) {
         this.$root.sendWs(JSON.stringify({
-          "url": "/rgv/table/rgv/state",
-          "data": {}
+          url: "/rgv/table/rgv/state",
+          data: {}
         }));
       }
     },
-    setRgvList(res) {
-      let that = this;
-      if (res.code == 200) {
-        let list = res.data || [];
-        if (that.searchRgvNo == "") {
-          that.rgvList = list;
-        } else {
-          let tmp = [];
-          list.forEach((item) => {
-            if (item.rgvNo == that.searchRgvNo) {
-              tmp.push(item);
-            }
-          });
-          that.rgvList = tmp;
-          that.currentPage = 1;
-        }
+    setRgvList: function (res) {
+      if (res && res.code === 200) {
+        this.rgvList = res.data || [];
+        this.afterDataRefresh();
       }
     },
-    openControl() {
+    openControl: function () {
       this.showControl = !this.showControl;
     },
-    controlCommandTransport() {
-      let that = this;
+    buildDetailEntries: function (item) {
+      return [
+        { label: "缂栧彿", value: this.orDash(item.rgvNo) },
+        { label: "宸ヤ綔鍙�", value: this.orDash(item.taskNo) },
+        { label: "妯″紡", value: this.orDash(item.mode) },
+        { label: "鐘舵��", value: this.orDash(item.status) },
+        { label: "杞ㄩ亾浣�", value: this.orDash(item.trackSiteNo) },
+        { label: "鏄惁鏈夌墿", value: MonitorCardKit.yesNo(item.loading) },
+        { label: "鏁呴殰浠g爜", value: this.orDash(item.warnCode) },
+        { label: "鏁呴殰鎻忚堪", value: this.orDash(item.alarm) },
+        { label: "鎵╁睍鏁版嵁", value: this.orDash(item.extend) }
+      ];
+    },
+    postControl: function (url, payload) {
       $.ajax({
-        url: baseUrl + "/rgv/command/transport",
+        url: baseUrl + url,
         headers: {
-          token: localStorage.getItem("token"),
+          token: localStorage.getItem("token")
         },
         contentType: "application/json",
         method: "post",
-        data: JSON.stringify(that.controlParam),
-        success: (res) => {
-          if (res.code == 200) {
-            that.$message({
-              message: res.msg,
-              type: "success",
-            });
+        data: JSON.stringify(payload),
+        success: function (res) {
+          if (res && res.code === 200) {
+            MonitorCardKit.showMessage(this, res.msg || "鎿嶄綔鎴愬姛", "success");
           } else {
-            that.$message({
-              message: res.msg,
-              type: "warning",
-            });
+            MonitorCardKit.showMessage(this, (res && res.msg) || "鎿嶄綔澶辫触", "warning");
           }
-        },
+        }.bind(this)
       });
     },
-    controlCommandMove() {
-      let that = this;
-      $.ajax({
-        url: baseUrl + "/rgv/command/move",
-        headers: {
-          token: localStorage.getItem("token"),
-        },
-        contentType: "application/json",
-        method: "post",
-        data: JSON.stringify(that.controlParam),
-        success: (res) => {
-          if (res.code == 200) {
-            that.$message({
-              message: res.msg,
-              type: "success",
-            });
-          } else {
-            that.$message({
-              message: res.msg,
-              type: "warning",
-            });
-          }
-        },
-      });
+    controlCommandTransport: function () {
+      this.postControl("/rgv/command/transport", this.controlParam);
     },
-    controlCommandTaskComplete() {
-      let that = this;
-      $.ajax({
-        url: baseUrl + "/rgv/command/taskComplete",
-        headers: {
-          token: localStorage.getItem("token"),
-        },
-        contentType: "application/json",
-        method: "post",
-        data: JSON.stringify(that.controlParam),
-        success: (res) => {
-          if (res.code == 200) {
-            that.$message({
-              message: res.msg,
-              type: "success",
-            });
-          } else {
-            that.$message({
-              message: res.msg,
-              type: "warning",
-            });
-          }
-        },
-      });
+    controlCommandMove: function () {
+      this.postControl("/rgv/command/move", this.controlParam);
     },
-  },
+    controlCommandTaskComplete: function () {
+      this.postControl("/rgv/command/taskComplete", this.controlParam);
+    }
+  }
 });
diff --git a/src/main/webapp/static/js/watch/stationColorConfig.js b/src/main/webapp/static/js/watch/stationColorConfig.js
new file mode 100644
index 0000000..d2450ea
--- /dev/null
+++ b/src/main/webapp/static/js/watch/stationColorConfig.js
@@ -0,0 +1,127 @@
+var app = new Vue({
+    el: '#app',
+    data: {
+        loading: false,
+        saving: false,
+        items: [],
+        predefineColors: [
+            '#78FF81',
+            '#FA51F6',
+            '#C4C400',
+            '#30BFFC',
+            '#18C7B8',
+            '#97B400',
+            '#E69138',
+            '#B8B8B8',
+            '#FF6B6B',
+            '#FFD166',
+            '#06D6A0',
+            '#118AB2'
+        ]
+    },
+    mounted: function () {
+        this.reloadData();
+    },
+    methods: {
+        reloadData: function () {
+            var that = this;
+            this.loading = true;
+            $.ajax({
+                url: baseUrl + '/watch/stationColor/config/auth',
+                headers: { token: localStorage.getItem('token') },
+                method: 'GET',
+                success: function (res) {
+                    that.loading = false;
+                    if (res.code === 200) {
+                        var items = (res.data && res.data.items) ? res.data.items : [];
+                        that.items = items.map(function (item) {
+                            return {
+                                status: item.status,
+                                name: item.name,
+                                desc: item.desc,
+                                color: that.normalizeColor(item.color || item.defaultColor),
+                                defaultColor: that.normalizeColor(item.defaultColor)
+                            };
+                        });
+                    } else if (res.code === 403) {
+                        top.location.href = baseUrl + '/';
+                    } else {
+                        that.$message.error(res.msg || '鍔犺浇绔欑偣棰滆壊閰嶇疆澶辫触');
+                    }
+                },
+                error: function () {
+                    that.loading = false;
+                    that.$message.error('鍔犺浇绔欑偣棰滆壊閰嶇疆澶辫触');
+                }
+            });
+        },
+        resetDefaults: function () {
+            this.items = this.items.map(function (item) {
+                return Object.assign({}, item, {
+                    color: item.defaultColor
+                });
+            });
+            this.$message.success('宸叉仮澶嶉粯璁ら鑹�');
+        },
+        applyDefaultColor: function (item) {
+            item.color = item.defaultColor;
+        },
+        handleColorInput: function (item) {
+            var normalized = this.normalizeColor(item.color);
+            if (normalized !== String(item.color || '').trim().toUpperCase()) {
+                this.$message.warning('棰滆壊鏍煎紡宸茶嚜鍔ㄤ慨姝d负鍗佸叚杩涘埗');
+            }
+            item.color = normalized;
+        },
+        saveConfig: function () {
+            var that = this;
+            this.saving = true;
+            $.ajax({
+                url: baseUrl + '/watch/stationColor/config/save/auth',
+                headers: { token: localStorage.getItem('token') },
+                method: 'POST',
+                contentType: 'application/json;charset=UTF-8',
+                dataType: 'json',
+                data: JSON.stringify({
+                    items: this.items.map(function (item) {
+                        return {
+                            status: item.status,
+                            color: that.normalizeColor(item.color)
+                        };
+                    })
+                }),
+                success: function (res) {
+                    that.saving = false;
+                    if (res.code === 200) {
+                        that.$message.success('绔欑偣棰滆壊閰嶇疆宸蹭繚瀛�');
+                        that.reloadData();
+                    } else if (res.code === 403) {
+                        top.location.href = baseUrl + '/';
+                    } else {
+                        that.$message.error(res.msg || '淇濆瓨绔欑偣棰滆壊閰嶇疆澶辫触');
+                    }
+                },
+                error: function () {
+                    that.saving = false;
+                    that.$message.error('淇濆瓨绔欑偣棰滆壊閰嶇疆澶辫触');
+                }
+            });
+        },
+        normalizeColor: function (color) {
+            var value = String(color || '').trim();
+            if (!value) {
+                return '#B8B8B8';
+            }
+            if (/^#[0-9a-fA-F]{6}$/.test(value)) {
+                return value.toUpperCase();
+            }
+            if (/^#[0-9a-fA-F]{3}$/.test(value)) {
+                return ('#' + value.charAt(1) + value.charAt(1) + value.charAt(2) + value.charAt(2) + value.charAt(3) + value.charAt(3)).toUpperCase();
+            }
+            if (/^0x[0-9a-fA-F]{6}$/.test(value)) {
+                return ('#' + value.substring(2)).toUpperCase();
+            }
+            return '#B8B8B8';
+        }
+    }
+});
diff --git a/src/main/webapp/views/config/config.html b/src/main/webapp/views/config/config.html
index 3479aff..a0d92fb 100644
--- a/src/main/webapp/views/config/config.html
+++ b/src/main/webapp/views/config/config.html
@@ -67,4 +67,4 @@
 
 </body>
 
-</html>
\ No newline at end of file
+</html>
diff --git a/src/main/webapp/views/deviceLogs/deviceLogs.html b/src/main/webapp/views/deviceLogs/deviceLogs.html
index 56f8d78..ed5c392 100644
--- a/src/main/webapp/views/deviceLogs/deviceLogs.html
+++ b/src/main/webapp/views/deviceLogs/deviceLogs.html
@@ -206,10 +206,11 @@
 <script type="text/javascript" src="../../static/js/common.js" charset="utf-8"></script>
 <script src="../../static/vue/js/vue.min.js"></script>
 <script src="../../static/vue/element/element.js"></script>
+<script src="../../components/MonitorCardKit.js"></script>
 <script src="../../components/WatchCrnCard.js"></script>
 <script src="../../components/WatchRgvCard.js"></script>
 <script src="../../components/WatchDualCrnCard.js"></script>
 <script src="../../components/DevpCard.js"></script>
 <script type="text/javascript" src="../../static/js/deviceLogs/deviceLogs.js" charset="utf-8"></script>
 </body>
-</html>
\ No newline at end of file
+</html>
diff --git a/src/main/webapp/views/watch/console.html b/src/main/webapp/views/watch/console.html
index 3dbcc3c..a1229ae 100644
--- a/src/main/webapp/views/watch/console.html
+++ b/src/main/webapp/views/watch/console.html
@@ -4,8 +4,8 @@
 				<meta charset="UTF-8">
 				<title>WCS鎺у埗涓績</title>
 				<link rel="stylesheet" href="../../static/css/animate.min.css">
-				<link rel="stylesheet" href="../../static/vue/element/element.css">
 				<link rel="stylesheet" href="../../static/css/watch/console_vue.css">
+				<link rel="stylesheet" href="../../static/vue/element/element.css">
 				<style>
 					html, body, #app {
 						width: 100%;
@@ -13,11 +13,503 @@
 						margin: 0;
 						overflow: hidden;
 					}
+					body {
+						background: linear-gradient(180deg, #eef4f8 0%, #e7edf4 100%);
+					}
+					#app {
+						position: relative;
+					}
+					.monitor-shell {
+						position: relative;
+						width: 100%;
+						height: 100%;
+						overflow: hidden;
+					}
+					.monitor-map {
+						width: 100%;
+						height: 100%;
+					}
+					.monitor-panel-wrap {
+						position: absolute;
+						top: 18px;
+						left: 18px;
+						bottom: 18px;
+						z-index: 40;
+						pointer-events: none;
+					}
+					.monitor-panel {
+						width: min(max(360px, 30vw), calc(100vw - 92px));
+						max-width: calc(100vw - 92px);
+						height: calc(100vh - 36px);
+						display: flex;
+						flex-direction: column;
+						border-radius: 20px;
+						border: 1px solid rgba(255, 255, 255, 0.42);
+						background: rgba(248, 251, 253, 0.94);
+						box-shadow: 0 10px 24px rgba(88, 110, 136, 0.08);
+						overflow: hidden;
+						pointer-events: auto;
+						transform-origin: left center;
+						will-change: transform, opacity;
+						backface-visibility: hidden;
+						contain: layout paint style;
+						transition: transform .26s cubic-bezier(0.22, 1, 0.36, 1), opacity .2s ease, box-shadow .2s ease, border-color .2s ease;
+					}
+					.monitor-panel.is-collapsed {
+						opacity: 0;
+						transform: translate3d(calc(-100% - 14px), 0, 0);
+						border-color: transparent;
+						box-shadow: 0 6px 16px rgba(88, 110, 136, 0.02);
+						pointer-events: none;
+					}
+					.monitor-panel-header {
+						padding: 14px 16px 10px;
+						border-bottom: 1px solid rgba(226, 232, 240, 0.72);
+						background: rgba(255, 255, 255, 0.24);
+					}
+					.monitor-panel-title {
+						font-size: 15px;
+						font-weight: 600;
+						color: #243447;
+						line-height: 1.2;
+					}
+					.monitor-panel-desc {
+						margin-top: 4px;
+						font-size: 12px;
+						color: #6b7b8d;
+					}
+					.monitor-panel-body {
+						flex: 1;
+						min-height: 0;
+						padding: 12px;
+						overflow: hidden;
+					}
+					.monitor-panel-body {
+						flex: 1;
+						display: flex;
+						flex-direction: column;
+						gap: 10px;
+					}
+					.monitor-card-host {
+						flex: 1;
+						min-height: 0;
+						display: flex;
+						align-items: stretch;
+						width: 100%;
+					}
+					.wb-root {
+						position: relative;
+						flex: 1;
+						min-width: 0;
+						display: flex;
+						flex-direction: column;
+						height: 100%;
+						gap: 10px;
+						color: #395066;
+					}
+					.wb-main {
+						flex: 1;
+						min-height: 0;
+						display: flex;
+						gap: 10px;
+					}
+					.wb-side {
+						flex: 0 0 38%;
+						min-width: 220px;
+						max-width: 44%;
+						display: flex;
+						flex-direction: column;
+						gap: 10px;
+						min-height: 0;
+					}
+					.wb-side-title {
+						font-size: 11px;
+						font-weight: 700;
+						letter-spacing: 0.06em;
+						text-transform: uppercase;
+						color: #7d8fa2;
+						margin-bottom: 8px;
+					}
+					.wb-list-card,
+					.wb-detail-panel {
+						min-height: 0;
+						display: flex;
+						flex-direction: column;
+					}
+					.wb-detail-panel {
+						flex: 1 1 62%;
+						min-width: 0;
+					}
+					.wb-tabs {
+						display: grid;
+						grid-template-columns: repeat(4, 1fr);
+						gap: 6px;
+						padding: 6px;
+						border-radius: 14px;
+						background: rgba(242, 246, 250, 0.78);
+						border: 1px solid rgba(224, 232, 239, 0.9);
+					}
+					.wb-tab {
+						height: 34px;
+						border: none;
+						border-radius: 10px;
+						background: transparent;
+						color: #73869b;
+						font-size: 12px;
+						font-weight: 600;
+						cursor: pointer;
+						transition: all .16s ease;
+					}
+					.wb-tab.is-active {
+						background: rgba(255, 255, 255, 0.92);
+						color: #24405c;
+						box-shadow: 0 8px 16px rgba(148, 163, 184, 0.08);
+					}
+					.wb-toolbar {
+						display: flex;
+						align-items: center;
+						gap: 8px;
+					}
+					.wb-toolbar-actions {
+						display: flex;
+						gap: 8px;
+						flex-shrink: 0;
+					}
+					.wb-input,
+					.wb-select {
+						width: 100%;
+						height: 36px;
+						padding: 0 12px;
+						border-radius: 10px;
+						border: 1px solid rgba(224, 232, 239, 0.96);
+						background: rgba(255, 255, 255, 0.76);
+						color: #334155;
+						box-sizing: border-box;
+						outline: none;
+						transition: border-color .16s ease, box-shadow .16s ease, background .16s ease;
+					}
+					.wb-input:focus,
+					.wb-select:focus {
+						border-color: rgba(128, 168, 208, 0.66);
+						box-shadow: 0 0 0 3px rgba(128, 168, 208, 0.1);
+						background: rgba(255, 255, 255, 0.92);
+					}
+					.wb-btn {
+						height: 36px;
+						padding: 0 14px;
+						border: none;
+						border-radius: 10px;
+						background: #6f95bd;
+						color: #fff;
+						font-size: 12px;
+						font-weight: 600;
+						cursor: pointer;
+						box-shadow: 0 6px 14px rgba(111, 149, 189, 0.18);
+						transition: transform .16s ease, box-shadow .16s ease, border-color .16s ease, background .16s ease;
+					}
+					.wb-btn:hover {
+						transform: translateY(-1px);
+					}
+					.wb-btn-primary {
+						background: linear-gradient(135deg, #5e89b4 0%, #6f95bd 100%);
+						box-shadow: 0 10px 20px rgba(111, 149, 189, 0.22);
+					}
+					.wb-btn.wb-btn-ghost {
+						background: rgba(255, 255, 255, 0.76);
+						color: #4c6177;
+						border: 1px solid rgba(224, 232, 239, 0.96);
+						box-shadow: none;
+					}
+					.wb-btn.wb-btn-soft {
+						background: rgba(230, 237, 244, 0.92);
+						color: #4c6177;
+						border: 1px solid rgba(210, 221, 232, 0.98);
+						box-shadow: none;
+					}
+					.wb-control-card,
+					.wb-list-card,
+					.wb-detail {
+						border-radius: 16px;
+						border: 1px solid rgba(224, 232, 239, 0.92);
+						background: rgba(255, 255, 255, 0.62);
+						box-shadow: 0 8px 18px rgba(148, 163, 184, 0.06);
+					}
+					.wb-list-card {
+						flex: 1;
+						padding: 10px 8px 8px;
+					}
+					.wb-control-card {
+						padding: 14px;
+						background: linear-gradient(180deg, rgba(255, 255, 255, 0.82) 0%, rgba(246, 250, 253, 0.78) 100%);
+						overflow: auto;
+					}
+					.wb-control-subtitle {
+						margin-top: 8px;
+						margin-bottom: 10px;
+						font-size: 11px;
+						line-height: 1.45;
+						color: #6f8194;
+					}
+					.wb-control-target {
+						padding: 7px 9px;
+						border-radius: 12px;
+						background: rgba(234, 241, 247, 0.96);
+						border: 1px solid rgba(214, 224, 234, 0.96);
+						color: #43607c;
+						font-size: 11px;
+						font-weight: 700;
+						line-height: 1.4;
+					}
+					.wb-form-grid {
+						display: grid;
+						grid-template-columns: 1fr;
+						gap: 8px;
+					}
+					.wb-field {
+						display: flex;
+						flex-direction: column;
+						gap: 5px;
+					}
+					.wb-field-label {
+						font-size: 11px;
+						font-weight: 700;
+						letter-spacing: 0.02em;
+						color: #6d8197;
+					}
+					.wb-action-row {
+						display: flex;
+						flex-wrap: wrap;
+						gap: 8px;
+						margin-top: 2px;
+						padding-top: 8px;
+						border-top: 1px dashed rgba(216, 226, 235, 0.92);
+					}
+					.wb-section-title {
+						font-size: 13px;
+						font-weight: 700;
+						color: #28425d;
+						margin-bottom: 10px;
+					}
+					.wb-list {
+						flex: 1;
+						min-height: 0;
+						padding: 6px;
+						overflow: auto;
+					}
+					.wb-list-item {
+						width: 100%;
+						display: flex;
+						flex-direction: column;
+						align-items: flex-start;
+						justify-content: flex-start;
+						gap: 7px;
+						padding: 10px;
+						margin-bottom: 8px;
+						border: 1px solid transparent;
+						border-radius: 12px;
+						background: rgba(248, 250, 252, 0.72);
+						cursor: pointer;
+						text-align: left;
+						color: inherit;
+					}
+					.wb-list-item:last-child {
+						margin-bottom: 0;
+					}
+					.wb-list-item.is-active {
+						border-color: rgba(135, 166, 198, 0.38);
+						background: rgba(236, 243, 249, 0.94);
+					}
+					.wb-list-main {
+						min-width: 0;
+					}
+					.wb-list-title {
+						font-size: 12px;
+						font-weight: 700;
+						color: #27425c;
+						line-height: 1.35;
+					}
+					.wb-list-meta {
+						margin-top: 4px;
+						font-size: 11px;
+						color: #7b8b9c;
+						line-height: 1.4;
+						display: -webkit-box;
+						-webkit-line-clamp: 2;
+						-webkit-box-orient: vertical;
+						overflow: hidden;
+					}
+					.wb-badge {
+						flex-shrink: 0;
+						padding: 3px 8px;
+						border-radius: 999px;
+						font-size: 10px;
+						font-weight: 700;
+					}
+					.wb-badge.is-success {
+						background: rgba(82, 177, 126, 0.12);
+						color: #2d7650;
+					}
+					.wb-badge.is-working {
+						background: rgba(111, 149, 189, 0.12);
+						color: #3f6286;
+					}
+					.wb-badge.is-warning {
+						background: rgba(214, 162, 94, 0.14);
+						color: #9b6a24;
+					}
+					.wb-badge.is-danger {
+						background: rgba(207, 126, 120, 0.14);
+						color: #a14e4a;
+					}
+					.wb-badge.is-muted {
+						background: rgba(148, 163, 184, 0.14);
+						color: #748397;
+					}
+					.wb-empty {
+						padding: 28px 12px;
+						text-align: center;
+						color: #8b9aad;
+						font-size: 12px;
+					}
+					.wb-detail {
+						flex: 1;
+						min-height: 0;
+						padding: 12px;
+						overflow: auto;
+					}
+					.wb-detail-empty {
+						display: flex;
+						align-items: center;
+						justify-content: center;
+					}
+					.wb-detail-header {
+						display: flex;
+						align-items: flex-start;
+						justify-content: space-between;
+						gap: 10px;
+						margin-bottom: 12px;
+					}
+					.wb-detail-subtitle {
+						margin-top: 4px;
+						font-size: 12px;
+						color: #7c8c9d;
+					}
+					.wb-detail-actions {
+						display: flex;
+						flex-wrap: wrap;
+						gap: 8px;
+					}
+					.wb-link {
+						padding: 0;
+						border: none;
+						background: transparent;
+						color: #4677a4;
+						font-size: 12px;
+						cursor: pointer;
+					}
+					.wb-detail-grid {
+						display: grid;
+						grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
+						gap: 8px;
+					}
+					.wb-detail-cell {
+						padding: 10px 12px;
+						border-radius: 12px;
+						background: rgba(247, 250, 252, 0.86);
+						border: 1px solid rgba(233, 239, 244, 0.96);
+					}
+					.wb-detail-label {
+						font-size: 11px;
+						color: #8090a2;
+					}
+					.wb-detail-value {
+						margin-top: 5px;
+						font-size: 13px;
+						color: #31485f;
+						word-break: break-all;
+					}
+					.wb-notice {
+						position: absolute;
+						right: 18px;
+						bottom: 18px;
+						padding: 10px 14px;
+						border-radius: 12px;
+						font-size: 12px;
+						font-weight: 600;
+						color: #fff;
+						box-shadow: 0 12px 24px rgba(15, 23, 42, 0.12);
+					}
+					.wb-notice.is-success {
+						background: rgba(82, 177, 126, 0.92);
+					}
+					.wb-notice.is-warning {
+						background: rgba(214, 162, 94, 0.92);
+					}
+					.wb-notice.is-danger {
+						background: rgba(207, 126, 120, 0.92);
+					}
+					.floor-switch-button {
+						min-width: 44px;
+						height: 30px;
+						padding: 0 12px;
+						border: 1px solid rgba(185, 197, 210, 0.84);
+						border-radius: 999px;
+						background: rgba(255, 255, 255, 0.82);
+						color: #4b6177;
+						font-size: 12px;
+						font-weight: 700;
+						cursor: pointer;
+					}
+					.floor-switch-button.is-active {
+						border-color: rgba(111, 149, 189, 0.4);
+						background: rgba(236, 243, 249, 0.94);
+						color: #27425c;
+					}
+					.monitor-panel-toggle {
+						position: absolute;
+						left: 0;
+						top: 50%;
+						margin-left: 0;
+						transform: translateY(-50%);
+						width: 30px;
+						min-height: 108px;
+						padding: 10px 4px;
+						border: 1px solid rgba(148, 163, 184, 0.22);
+						border-left: none;
+						border-radius: 0 14px 14px 0;
+						background: rgba(255, 255, 255, 0.96);
+						color: #3e5974;
+						box-shadow: 0 8px 18px rgba(15, 23, 42, 0.08);
+						cursor: pointer;
+						pointer-events: auto;
+						display: inline-flex;
+						flex-direction: column;
+						align-items: center;
+						justify-content: center;
+						gap: 10px;
+						font-size: 12px;
+						line-height: 1;
+						white-space: nowrap;
+						backface-visibility: hidden;
+						transition: left .26s cubic-bezier(0.22, 1, 0.36, 1), transform .26s cubic-bezier(0.22, 1, 0.36, 1), box-shadow .18s ease, background .18s ease;
+					}
+					.monitor-panel-toggle.is-panel-open {
+						left: calc(100% + 10px);
+					}
+					.monitor-panel-toggle i {
+						font-size: 14px;
+					}
+					.monitor-panel-toggle span {
+						writing-mode: vertical-rl;
+						text-orientation: mixed;
+						letter-spacing: 0.08em;
+						user-select: none;
+					}
 				</style>
 				<script type="text/javascript" src="../../static/js/jquery/jquery-3.3.1.min.js"></script>
 				<script type="text/javascript" src="../../static/layui/layui.js"></script>
 				<script type="text/javascript" src="../../static/js/handlebars/handlebars-v4.5.3.js"></script>
-				<script type="text/javascript" src="../../static/js/common.js"></script>
+			<script type="text/javascript" src="../../static/js/common.js"></script>
 			<script type="text/javascript" src="../../static/vue/js/vue.min.js"></script>
 			<script type="text/javascript" src="../../static/vue/element/element.js"></script>
 			<script src="../../static/js/gsap.min.js"></script>
@@ -25,42 +517,43 @@
 		</head>
 	<body>
 		<div id="app">
-			<div style="display: flex;margin-left: 20px;">
-				<div style="width: 20%;height: 60vh;margin-right: 20px;margin-top: 30px;">
-					<el-tabs type="border-card" v-model="activateCard" @tab-click="handleCardClick">
-						<el-tab-pane label="鍫嗗灈鏈�" name="crn">
-							<watch-crn-card ref="watchCrnCard" :param="crnParam"></watch-crn-card>
-						</el-tab-pane>
-						<el-tab-pane label="鍙屽伐浣嶅爢鍨涙満" name="dualCrn">
-							<watch-dual-crn-card ref="watchDualCrnCard" :param="dualCrnParam"></watch-dual-crn-card>
-						</el-tab-pane>
-						<el-tab-pane label="杈撻�佺珯" name="devp">
-							<devp-card ref="devpCard" :param="devpParam"></devp-card>
-						</el-tab-pane>
-						<el-tab-pane label="RGV" name="rgv">
-							<watch-rgv-card ref="watchRgvCard" :param="rgvParam"></watch-rgv-card>
-						</el-tab-pane>
-						<!-- <el-tab-pane label="鍦板浘閰嶇疆" name="mapSetting">
-							<map-setting-card :param="mapSettingParam"></map-setting-card>
-						</el-tab-pane> -->
-					</el-tabs>
-				</div>
+			<div class="monitor-shell" ref="monitorShell">
+				<map-canvas :lev="currentLev" :lev-list="levList" :crn-param="crnParam" :rgv-param="rgvParam" :devp-param="devpParam" :station-task-range="stationTaskRange" :viewport-padding="mapViewportPadding" :hud-padding="mapHudPadding" @switch-lev="switchLev" @crn-click="openCrn" @dual-crn-click="openDualCrn" @station-click="openSite" @rgv-click="openRgv" class="monitor-map"></map-canvas>
 
-				<map-canvas :lev="currentLev" :crn-param="crnParam" :rgv-param="rgvParam" :devp-param="devpParam" @crn-click="openCrn" @dual-crn-click="openDualCrn" @station-click="openSite" @rgv-click="openRgv" style="width: 80%; height: 100vh;"></map-canvas>
-
-				<div style="position: absolute;top: 15px;left: 50%;display: flex;">
-					<div v-if="levList.length > 1" v-for="(lev,index) in levList" :key="index" style="margin-right: 10px;">
-						<el-button :type="currentLev == lev ? 'primary' : ''" @click="switchLev(lev)" size="mini">{{ lev }}F</el-button>
+				<div class="monitor-panel-wrap" ref="monitorPanelWrap">
+					<div class="monitor-panel" ref="monitorPanel" :class="{ 'is-collapsed': panelCollapsed }">
+						<div class="monitor-panel-header">
+							<div class="monitor-panel-title">鐩戞帶宸ヤ綔鍙�</div>
+							<div class="monitor-panel-desc">鍥寸粫鍦板浘鍋氭搷浣滐紝璁惧鐐瑰嚮鍚庤嚜鍔ㄥ垏鎹㈠埌瀵瑰簲闈㈡澘</div>
+						</div>
+						<div class="monitor-panel-body">
+							<div class="wb-tabs" role="tablist">
+								<button type="button" :class="['wb-tab', { 'is-active': activateCard === 'crn' }]" @click="handleWorkbenchTabChange('crn')">鍫嗗灈鏈�</button>
+								<button type="button" :class="['wb-tab', { 'is-active': activateCard === 'dualCrn' }]" @click="handleWorkbenchTabChange('dualCrn')">鍙屽伐浣�</button>
+								<button type="button" :class="['wb-tab', { 'is-active': activateCard === 'devp' }]" @click="handleWorkbenchTabChange('devp')">杈撻�佺珯</button>
+								<button type="button" :class="['wb-tab', { 'is-active': activateCard === 'rgv' }]" @click="handleWorkbenchTabChange('rgv')">RGV</button>
+							</div>
+							<div class="monitor-card-host">
+								<watch-crn-card v-if="activateCard === 'crn'" ref="watchCrnCard" :param="crnParam" :items="crnStateList" :auto-refresh="false"></watch-crn-card>
+								<watch-dual-crn-card v-else-if="activateCard === 'dualCrn'" ref="watchDualCrnCard" :param="dualCrnParam" :items="dualCrnStateList" :auto-refresh="false"></watch-dual-crn-card>
+								<devp-card v-else-if="activateCard === 'devp'" ref="devpCard" :param="devpParam" :items="stationStateList" :auto-refresh="false"></devp-card>
+								<watch-rgv-card v-else ref="watchRgvCard" :param="rgvParam" :items="rgvStateList" :auto-refresh="false"></watch-rgv-card>
+							</div>
+						</div>
 					</div>
+					<button class="monitor-panel-toggle" :class="{ 'is-panel-open': !panelCollapsed }" ref="monitorToggle" @click="toggleMonitorPanel">
+						<i>{{ panelCollapsed ? '>' : '<' }}</i>
+						<span>{{ panelCollapsed ? '灞曞紑闈㈡澘' : '鏀惰捣闈㈡澘' }}</span>
+					</button>
 				</div>
 			</div>
 
 		</div>
 
+		<script src="../../components/MonitorCardKit.js"></script>
 		<script src="../../components/WatchCrnCard.js"></script>
 		<script src="../../components/WatchDualCrnCard.js"></script>
 		<script src="../../components/DevpCard.js"></script>
-		<script src="../../components/MapSettingCard.js"></script>
 		<script src="../../components/WatchRgvCard.js"></script>
 		<script src="../../components/MapCanvas.js"></script>
 		<script>
@@ -74,6 +567,22 @@
 					systemStatus: true,//绯荤粺杩愯鐘舵��
 					consoleInterval: null,//瀹氭椂鍣ㄥ瓨鍌ㄥ彉閲�
 					rgvPosition: [],
+					panelCollapsed: false,
+					mapViewportPadding: {
+						top: 0,
+						right: 0,
+						bottom: 0,
+						left: 0
+					},
+					mapHudPadding: {
+						left: 14
+					},
+					stationTaskRange: {
+						inbound: null,
+						outbound: null
+					},
+					panelTransitionTimer: null,
+					panelPollTimer: null,
 					activateCard: 'crn',
 					crnParam: {
 						crnNo: 0
@@ -81,15 +590,16 @@
 					dualCrnParam: {
 						crnNo: 0
 					},
-					mapSettingParam: {
-						zoom: 70
-					},
 					devpParam: {
 						stationId: 0
 					},
 					rgvParam: {
 						rgvNo: 0
 					},
+					crnStateList: [],
+					dualCrnStateList: [],
+					stationStateList: [],
+					rgvStateList: [],
 					locMastData: [],//搴撲綅鏁版嵁
 					wsReconnectTimer: null,
 					wsReconnectAttempts: 0,
@@ -100,10 +610,20 @@
 					this.init()
 				},
 				mounted() {
+					this.$nextTick(() => {
+						this.updateMapViewportPadding();
+					});
+					window.addEventListener('resize', this.updateMapViewportPadding);
+					this.panelPollTimer = setInterval(() => {
+						this.refreshWorkbench(this.activateCard);
+					}, 1000);
 				},
 				beforeDestroy() {
 					if (this.wsReconnectTimer) { clearTimeout(this.wsReconnectTimer); this.wsReconnectTimer = null; }
 					if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) { try { ws.close(); } catch (e) {} }
+					window.removeEventListener('resize', this.updateMapViewportPadding);
+					if (this.panelTransitionTimer) { clearTimeout(this.panelTransitionTimer); this.panelTransitionTimer = null; }
+					if (this.panelPollTimer) { clearInterval(this.panelPollTimer); this.panelPollTimer = null; }
 				},
 				watch: {
 
@@ -114,12 +634,13 @@
                             ws.send(data);
                         }
                     },
-                    webSocketOnOpen() {
-                        console.log("WebSocket杩炴帴鎴愬姛");
-                        if (this.wsReconnectTimer) { clearTimeout(this.wsReconnectTimer); this.wsReconnectTimer = null; }
-                        this.wsReconnectAttempts = 0;
-                        this.getMap();
-                    },
+	                    webSocketOnOpen() {
+	                        console.log("WebSocket杩炴帴鎴愬姛");
+	                        if (this.wsReconnectTimer) { clearTimeout(this.wsReconnectTimer); this.wsReconnectTimer = null; }
+	                        this.wsReconnectAttempts = 0;
+	                        this.getMap();
+	                        this.refreshWorkbench(this.activateCard);
+	                    },
                     webSocketOnError() {
                         console.log("WebSocket杩炴帴鍙戠敓閿欒");
                         this.scheduleWsReconnect();
@@ -128,27 +649,23 @@
                         console.log("WebSocket杩炴帴鍏抽棴");
                         this.scheduleWsReconnect();
                     },
-                    webSocketOnMessage(e) {
-                        const result = JSON.parse(e.data);
-                        if (result.url == "/crn/table/crn/state") {
-                             if(this.$refs.watchCrnCard) {
-                                 this.$refs.watchCrnCard.setCrnList(JSON.parse(result.data));
-                             }
-                        } else if (result.url == "/dualcrn/table/crn/state") {
-                             if(this.$refs.watchDualCrnCard) {
-                                 this.$refs.watchDualCrnCard.setDualCrnList(JSON.parse(result.data));
-                             }
-                        } else if (result.url == "/console/latest/data/station") {
-                             if(this.$refs.devpCard) {
-                                 this.$refs.devpCard.setStationList(JSON.parse(result.data));
-                             }
-                        } else if (result.url == "/rgv/table/rgv/state") {
-                             if(this.$refs.watchRgvCard) {
-                                 this.$refs.watchRgvCard.setRgvList(JSON.parse(result.data));
-                             }
-                        } else if (result.url == "/basMap/lev/" + this.currentLev + "/auth") {
-                            // 鍦板浘鏁版嵁
-                             let res = JSON.parse(result.data);
+	                    webSocketOnMessage(e) {
+	                        const result = JSON.parse(e.data);
+	                        if (result.url == "/crn/table/crn/state") {
+	                             const res = JSON.parse(result.data);
+	                             this.crnStateList = res && res.code === 200 ? (res.data || []) : [];
+	                        } else if (result.url == "/dualcrn/table/crn/state") {
+	                             const res = JSON.parse(result.data);
+	                             this.dualCrnStateList = res && res.code === 200 ? (res.data || []) : [];
+	                        } else if (result.url == "/console/latest/data/station") {
+	                             const res = JSON.parse(result.data);
+	                             this.stationStateList = res && res.code === 200 ? (res.data || []) : [];
+	                        } else if (result.url == "/rgv/table/rgv/state") {
+	                             const res = JSON.parse(result.data);
+	                             this.rgvStateList = res && res.code === 200 ? (res.data || []) : [];
+	                        } else if (result.url == "/basMap/lev/" + this.currentLev + "/auth") {
+	                            // 鍦板浘鏁版嵁
+	                             let res = JSON.parse(result.data);
                              if (res.code === 200) {
                                  this.map = res.data;
                              }
@@ -165,7 +682,31 @@
 
 						this.getSystemRunningStatus() //鑾峰彇绯荤粺杩愯鐘舵��
 						this.getLevList() //鑾峰彇鍦板浘灞傜骇鍒楄〃
+						this.getStationTaskRange() // 鑾峰彇鍏ュ簱/鍑哄簱宸ヤ綔鍙疯寖鍥�
 						this.getLocMastData() //鑾峰彇搴撲綅鏁版嵁
+					},
+					getStationTaskRange() {
+						this.fetchWrkLastnoRange(1, 'inbound');
+						this.fetchWrkLastnoRange(101, 'outbound');
+					},
+					fetchWrkLastnoRange(id, key) {
+						$.ajax({
+							url: baseUrl + "/wrkLastno/" + id + "/auth",
+							headers: {
+								'token': localStorage.getItem('token')
+							},
+							method: "get",
+							success: (res) => {
+								if (!res || res.code !== 200 || !res.data) { return; }
+								const data = res.data;
+								this.stationTaskRange = Object.assign({}, this.stationTaskRange, {
+									[key]: {
+										start: data.sNo,
+										end: data.eNo
+									}
+								});
+							}
+						});
 					},
 					connectWs() {
 						if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) { return; }
@@ -204,40 +745,138 @@
 						this.currentLev = lev;
 						this.getMap()
 						this.getLocMastData()
+						this.refreshWorkbench(this.activateCard)
+					},
+					handleWorkbenchTabChange(type) {
+						this.activateCard = type;
+						this.refreshWorkbench(type);
+					},
+					refreshWorkbench(type) {
+						if (!type) { return; }
+						if (type === 'crn') {
+							this.sendWs(JSON.stringify({ url: "/crn/table/crn/state", data: {} }));
+						} else if (type === 'dualCrn') {
+							this.sendWs(JSON.stringify({ url: "/dualcrn/table/crn/state", data: {} }));
+						} else if (type === 'devp') {
+							this.sendWs(JSON.stringify({ url: "/console/latest/data/station", data: {} }));
+						} else if (type === 'rgv') {
+							this.sendWs(JSON.stringify({ url: "/rgv/table/rgv/state", data: {} }));
+						}
+					},
+					updateMapViewportPadding() {
+						const shell = this.$refs.monitorShell;
+						const panelWrap = this.$refs.monitorPanelWrap;
+						if (!shell) { return; }
+						const shellRect = shell.getBoundingClientRect();
+						let leftPadding = 0;
+						let hudLeft = 14;
+						if (!this.panelCollapsed && this.$refs.monitorPanel && panelWrap) {
+							const wrapRect = panelWrap.getBoundingClientRect();
+							const panelWidth = this.$refs.monitorPanel.offsetWidth || this.$refs.monitorPanel.getBoundingClientRect().width || 0;
+							const panelLeft = Math.max(0, Math.ceil(wrapRect.left - shellRect.left));
+							const panelBaseRight = panelLeft + Math.ceil(panelWidth);
+							const overlapCompensation = Math.min(56, panelWidth * 0.18);
+							leftPadding = Math.max(0, Math.ceil(panelBaseRight - overlapCompensation));
+							hudLeft = Math.max(14, Math.ceil(panelBaseRight + 34));
+						} else {
+							leftPadding = 0;
+							hudLeft = 14;
+						}
+						this.mapViewportPadding = {
+							top: 0,
+							right: 0,
+							bottom: 0,
+							left: leftPadding
+						};
+						this.mapHudPadding = {
+							left: hudLeft
+						};
+					},
+					scheduleMapViewportPaddingUpdate(delay) {
+						if (this.panelTransitionTimer) {
+							clearTimeout(this.panelTransitionTimer);
+							this.panelTransitionTimer = null;
+						}
+						this.panelTransitionTimer = setTimeout(() => {
+							this.panelTransitionTimer = null;
+							this.updateMapViewportPadding();
+						}, delay == null ? 0 : delay);
+					},
+					toggleMonitorPanel() {
+						this.panelCollapsed = !this.panelCollapsed;
+						this.scheduleMapViewportPaddingUpdate(0);
 					},
 					openCrn(id) {
+						this.panelCollapsed = false;
 						this.crnParam.crnNo = id;
 						this.activateCard = 'crn';
+						this.scheduleMapViewportPaddingUpdate(0);
+						this.refreshWorkbench('crn');
 					},
 					openDualCrn(id) {
+						this.panelCollapsed = false;
 						this.dualCrnParam.crnNo = id;
 						this.activateCard = 'dualCrn';
+						this.scheduleMapViewportPaddingUpdate(0);
+						this.refreshWorkbench('dualCrn');
 					},
 					openRgv(id) {
+						this.panelCollapsed = false;
 						this.rgvParam.rgvNo = id;
 						this.activateCard = 'rgv';
+						this.scheduleMapViewportPaddingUpdate(0);
+						this.refreshWorkbench('rgv');
 					},
 					openSite(id) {
+						this.panelCollapsed = false;
 						this.devpParam.stationId = id;
 						this.activateCard = 'devp';
+						this.scheduleMapViewportPaddingUpdate(0);
+						this.refreshWorkbench('devp');
 					},
 					systemSwitch() {
 						// 绯荤粺寮�鍏�
-						let that = this
 						if (this.systemStatus) {
-							this.$prompt('璇疯緭鍏ュ彛浠わ紝骞跺仠姝CS绯荤粺', '鎻愮ず', {
-								confirmButtonText: '纭畾',
-								cancelButtonText: '鍙栨秷',
-							}).then(({
-								value
-							}) => {
-								that.doSwitch(0, value)
-							}).catch(() => {
-
-							});
+							const password = window.prompt('璇疯緭鍏ュ彛浠わ紝骞跺仠姝CS绯荤粺', '');
+							if (password === null) {
+								return;
+							}
+							this.doSwitch(0, password);
 						} else {
 							this.doSwitch(1)
 						}
+					},
+					showPageMessage(message, type) {
+						if (!message) {
+							return;
+						}
+						if (typeof this.$message === 'function') {
+							this.$message({
+								message: message,
+								type: type || 'info'
+							});
+							return;
+						}
+						if (window.ELEMENT && typeof window.ELEMENT.Message === 'function') {
+							window.ELEMENT.Message({
+								message: message,
+								type: type || 'info'
+							});
+							return;
+						}
+						if (window.layer && typeof window.layer.msg === 'function') {
+							const iconMap = {
+								success: 1,
+								error: 2,
+								warning: 0
+							};
+							window.layer.msg(message, {
+								icon: iconMap[type] != null ? iconMap[type] : 0,
+								time: 1800
+							});
+							return;
+						}
+						console[type === 'error' ? 'error' : 'log'](message);
 					},
 					doSwitch(operatorType, password) {
 						let that = this
@@ -267,10 +906,7 @@
 								} else if (res.code === 403) {
 									parent.location.href = baseUrl + "/login";
 								} else {
-									that.$message({
-										message: res.msg,
-										type: 'error'
-									});
+									that.showPageMessage(res.msg, 'error');
 								}
 							}
 						});
@@ -300,10 +936,7 @@
 								} else if (res.code === 403) {
 									parent.location.href = baseUrl + "/login";
 								} else {
-									that.$message({
-										message: res.msg,
-										type: 'error'
-									});
+									that.showPageMessage(res.msg, 'error');
 								}
 							}
 						});
@@ -381,9 +1014,6 @@
 							return false;
 						}
 					},
-					handleCardClick(tab, event) {
-
-					},
 					//鑾峰彇搴撲綅鏁版嵁
 					getLocMastData() {
 						let that = this;
@@ -422,8 +1052,8 @@
 							return locInfo.row1 + '-' + locInfo.bay1;
 						}
 						return '';
-					},
-				}
+						},
+					}
 			})
 			</script>
 		</body>
diff --git a/src/main/webapp/views/watch/stationColorConfig.html b/src/main/webapp/views/watch/stationColorConfig.html
new file mode 100644
index 0000000..ae24781
--- /dev/null
+++ b/src/main/webapp/views/watch/stationColorConfig.html
@@ -0,0 +1,290 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="utf-8">
+    <title>绔欑偣棰滆壊閰嶇疆</title>
+    <meta name="renderer" content="webkit">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
+    <link rel="stylesheet" href="../../static/vue/element/element.css">
+    <link rel="stylesheet" href="../../static/css/cool.css">
+    <style>
+        html, body {
+            height: 100%;
+            margin: 0;
+            background:
+                radial-gradient(circle at top left, rgba(70, 136, 214, 0.14), transparent 34%),
+                radial-gradient(circle at bottom right, rgba(37, 198, 178, 0.12), transparent 28%),
+                linear-gradient(180deg, #eef4fa 0%, #e8eef5 100%);
+        }
+        body {
+            font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
+        }
+        [v-cloak] {
+            display: none;
+        }
+        #app {
+            height: 100%;
+            padding: 22px;
+            box-sizing: border-box;
+            display: flex;
+            flex-direction: column;
+            gap: 16px;
+        }
+        .page-hero {
+            padding: 22px 24px;
+            border-radius: 20px;
+            border: 1px solid rgba(255, 255, 255, 0.78);
+            background: linear-gradient(135deg, rgba(255, 255, 255, 0.92), rgba(244, 249, 255, 0.82));
+            box-shadow: 0 18px 50px rgba(48, 74, 104, 0.08);
+        }
+        .page-title {
+            font-size: 26px;
+            font-weight: 700;
+            color: #223548;
+            letter-spacing: 0.01em;
+        }
+        .page-subtitle {
+            margin-top: 8px;
+            max-width: 860px;
+            font-size: 13px;
+            line-height: 1.8;
+            color: #66788c;
+        }
+        .page-panel {
+            flex: 1;
+            min-height: 0;
+            display: flex;
+            flex-direction: column;
+            border-radius: 22px;
+            border: 1px solid rgba(221, 231, 242, 0.92);
+            background: rgba(251, 253, 255, 0.88);
+            box-shadow: 0 20px 48px rgba(49, 76, 106, 0.08);
+            overflow: hidden;
+        }
+        .panel-toolbar {
+            padding: 18px 22px;
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+            gap: 12px;
+            border-bottom: 1px solid rgba(224, 233, 244, 0.9);
+            background: rgba(255, 255, 255, 0.72);
+        }
+        .panel-tip {
+            font-size: 12px;
+            color: #74879b;
+            line-height: 1.8;
+        }
+        .color-grid {
+            flex: 1;
+            min-height: 0;
+            padding: 20px 22px 24px;
+            overflow: auto;
+            display: grid;
+            grid-template-columns: repeat(auto-fit, minmax(290px, 1fr));
+            grid-auto-rows: max-content;
+            align-content: start;
+            gap: 16px;
+        }
+        .color-card {
+            position: relative;
+            border-radius: 18px;
+            border: 1px solid rgba(223, 232, 242, 0.94);
+            background: linear-gradient(180deg, rgba(255,255,255,0.96), rgba(246,250,255,0.92));
+            box-shadow: 0 12px 28px rgba(76, 101, 130, 0.07);
+            padding: 16px;
+            display: flex;
+            flex-direction: column;
+            gap: 10px;
+            overflow: visible;
+        }
+        .color-card::after {
+            content: "";
+            position: absolute;
+            top: -30px;
+            right: -20px;
+            width: 120px;
+            height: 120px;
+            border-radius: 50%;
+            background: rgba(255,255,255,0.42);
+            pointer-events: none;
+        }
+        .color-card-head {
+            display: flex;
+            align-items: flex-start;
+            justify-content: space-between;
+            gap: 14px;
+        }
+        .color-name {
+            font-size: 16px;
+            font-weight: 700;
+            color: #24374a;
+        }
+        .color-status {
+            margin-top: 4px;
+            font-size: 12px;
+            color: #7a8da2;
+            word-break: break-all;
+        }
+        .color-chip {
+            width: 58px;
+            height: 58px;
+            border-radius: 18px;
+            border: 1px solid rgba(89, 109, 134, 0.18);
+            box-shadow: inset 0 0 0 1px rgba(255,255,255,0.46);
+            flex-shrink: 0;
+        }
+        .color-desc {
+            font-size: 12px;
+            line-height: 1.8;
+            color: #66798d;
+        }
+        .color-editor {
+            display: flex;
+            align-items: center;
+            gap: 12px;
+        }
+        .color-picker-inline {
+            flex-shrink: 0;
+            display: flex;
+            align-items: center;
+            gap: 8px;
+            padding: 0 10px;
+            height: 40px;
+            border-radius: 12px;
+            border: 1px solid rgba(214, 225, 238, 0.95);
+            background: rgba(247, 250, 255, 0.9);
+            color: #567089;
+            font-size: 12px;
+            font-weight: 600;
+        }
+        .color-actions {
+            display: flex;
+            justify-content: flex-end;
+        }
+        .color-default {
+            font-size: 12px;
+            color: #7890a8;
+        }
+        .color-meta {
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+            gap: 10px;
+        }
+        .empty-state {
+            padding: 60px 20px;
+            text-align: center;
+            color: #7b8c9f;
+            font-size: 14px;
+        }
+        .footer-note {
+            padding: 0 22px 18px;
+            font-size: 12px;
+            color: #8696a8;
+        }
+        .el-color-picker__trigger {
+            width: 52px;
+            height: 40px;
+            padding: 0;
+            border-radius: 12px;
+            border-color: rgba(211, 223, 237, 0.95);
+            background: #fff;
+        }
+        .el-input__inner {
+            border-radius: 12px;
+            height: 40px;
+            line-height: 40px;
+        }
+        .el-button + .el-button {
+            margin-left: 10px;
+        }
+        @media (max-width: 900px) {
+            #app {
+                padding: 14px;
+            }
+            .page-hero,
+            .panel-toolbar,
+            .color-grid,
+            .footer-note {
+                padding-left: 16px;
+                padding-right: 16px;
+            }
+            .color-editor,
+            .color-meta {
+                flex-wrap: wrap;
+            }
+        }
+    </style>
+</head>
+<body>
+<div id="app" v-cloak>
+    <div class="page-hero">
+        <div class="page-title">绔欑偣棰滆壊閰嶇疆</div>
+        <div class="page-subtitle">
+            鍗曠嫭缁存姢鐩戞帶鍦板浘閲岀珯鐐圭姸鎬佺殑鏄剧ず棰滆壊銆傞鑹查�氳繃璋冭壊鐩樹换鎰忛�夋嫨锛岄厤缃繚瀛樺湪 Redis 涓紱
+            鏈厤缃椂榛樿浣跨敤褰撳墠鍦板浘浠g爜閲岀殑棰滆壊鍊笺��
+        </div>
+    </div>
+
+    <div class="page-panel">
+        <div class="panel-toolbar">
+            <div class="panel-tip">
+                寤鸿璁┾�滃惎鍔ㄥ叆搴� / 鍏ュ簱浠诲姟 / 鍑哄簱浠诲姟 / 鍫靛鈥濅繚鎸佹槑鏄惧尯鍒嗭紝閬垮厤鐜板満鍊煎畧鏃惰鍒ゃ��
+            </div>
+            <div>
+                <el-button size="small" @click="reloadData" :loading="loading">鍒锋柊</el-button>
+                <el-button size="small" @click="resetDefaults">鎭㈠榛樿</el-button>
+                <el-button type="primary" size="small" @click="saveConfig" :loading="saving">淇濆瓨閰嶇疆</el-button>
+            </div>
+        </div>
+
+        <div v-if="items.length" class="color-grid">
+            <div v-for="item in items" :key="item.status" class="color-card">
+                <div class="color-card-head">
+                    <div>
+                        <div class="color-name">{{ item.name }}</div>
+                        <div class="color-status">{{ item.status }}</div>
+                    </div>
+                    <div class="color-chip" :style="{ backgroundColor: item.color }"></div>
+                </div>
+
+                <div class="color-desc">{{ item.desc }}</div>
+
+                <div class="color-editor">
+                    <div class="color-picker-inline">
+                        <el-color-picker
+                            v-model="item.color"
+                            :predefine="predefineColors"
+                        ></el-color-picker>
+                        <span>璋冭壊鐩�</span>
+                    </div>
+                    <el-input
+                        v-model="item.color"
+                        @change="handleColorInput(item)"
+                        placeholder="#FFFFFF"
+                    ></el-input>
+                </div>
+
+                <div class="color-meta">
+                    <div class="color-default">榛樿鍊硷細{{ item.defaultColor }}</div>
+                    <div class="color-actions">
+                        <el-button size="mini" @click="applyDefaultColor(item)">鎭㈠榛樿</el-button>
+                    </div>
+                </div>
+            </div>
+        </div>
+        <div v-else class="empty-state">鏆傛棤绔欑偣棰滆壊閰嶇疆椤�</div>
+
+        <div class="footer-note">淇濆瓨鍚庯紝鏂版墦寮�鐨勭洃鎺у湴鍥句細鐩存帴璇诲彇 Redis 閰嶇疆锛涘凡鎵撳紑椤甸潰鍒锋柊鍚庡嵆鍙敓鏁堛��</div>
+    </div>
+</div>
+
+<script type="text/javascript" src="../../static/js/jquery/jquery-3.3.1.min.js"></script>
+<script type="text/javascript" src="../../static/vue/js/vue.min.js"></script>
+<script type="text/javascript" src="../../static/vue/element/element.js"></script>
+<script type="text/javascript" src="../../static/js/common.js" charset="utf-8"></script>
+<script type="text/javascript" src="../../static/js/watch/stationColorConfig.js" charset="utf-8"></script>
+</body>
+</html>

--
Gitblit v1.9.1