Bläddra i källkod

首页-控制模式: 扩到 16 种模式 + 配色抽 controlModeColors.js + 图例 >8 条无缝滚动 hover 暂停

画安 3 veckor sedan
förälder
incheckning
1687ad9e86
4 ändrade filer med 166 tillägg och 33 borttagningar
  1. 52 7
      src/components/ui/TickDonutChart.vue
  2. 95 0
      src/config/controlModeColors.js
  3. 3 1
      src/mock/api.js
  4. 16 25
      src/mock/mock_data.json

+ 52 - 7
src/components/ui/TickDonutChart.vue

@@ -1,13 +1,39 @@
 <template>
   <div class="tick-donut-wrapper">
     <div class="custom-legend">
-      <div class="legend-item" v-for="(item, index) in chartData" :key="index">
-        <span class="color-dot" :style="{ backgroundColor: item.color }"></span>
-        <span class="legend-name" :title="item.name">{{ truncateName(item.name) }}</span>
-        <span class="legend-value" v-if="item.value !== undefined && item.value !== null" :style="{ color: item.color }">
-          {{ item.value }}
-        </span>
-      </div>
+      <!-- 数据 ≤ legendLimit 直接列出, 不引入滚动开销; > legendLimit 走无缝滚动 + hover 停 -->
+      <SeamlessScroll
+        v-if="chartData.length > legendLimit"
+        :data="chartData"
+        :limit="legendLimit"
+        :speed="0.3"
+        measureSelector=".legend-rows"
+      >
+        <template #default="{ list }">
+          <div class="legend-rows">
+            <div
+              class="legend-item"
+              v-for="(item, index) in list"
+              :key="(item._clone_id || 'orig') + '_' + (item._originalIndex !== undefined ? item._originalIndex : index)"
+            >
+              <span class="color-dot" :style="{ backgroundColor: item.color }"></span>
+              <span class="legend-name" :title="item.name">{{ truncateName(item.name) }}</span>
+              <span class="legend-value" v-if="item.value !== undefined && item.value !== null" :style="{ color: item.color }">
+                {{ item.value }}
+              </span>
+            </div>
+          </div>
+        </template>
+      </SeamlessScroll>
+      <template v-else>
+        <div class="legend-item" v-for="(item, index) in chartData" :key="index">
+          <span class="color-dot" :style="{ backgroundColor: item.color }"></span>
+          <span class="legend-name" :title="item.name">{{ truncateName(item.name) }}</span>
+          <span class="legend-value" v-if="item.value !== undefined && item.value !== null" :style="{ color: item.color }">
+            {{ item.value }}
+          </span>
+        </div>
+      </template>
     </div>
 
     <div class="echarts-container" ref="chartRef"></div>
@@ -18,10 +44,12 @@
 import * as echarts from 'echarts';
 // 引入你项目的自适应 Mixin 和 px转换工具
 import echartsResize, { px2echarts } from '@/mixins/echartsResize.js';
+import SeamlessScroll from '@/components/ui/SeamlessScroll.vue';
 
 export default {
   name: 'TickDonutChart',
   mixins: [echartsResize],
+  components: { SeamlessScroll },
   props: {
     // 传入的图表数据,格式如:[{ name: '定周期控制', value: 400, color: '#33ccff' }, ...]
     chartData: {
@@ -37,6 +65,11 @@ export default {
     centerSubTitle: {
       type: String,
       default: ''
+    },
+    // 左侧图例最多展示多少行 (超过此数自动滚动, 鼠标 hover 暂停)
+    legendLimit: {
+      type: Number,
+      default: 8
     }
   },
   watch: {
@@ -174,6 +207,18 @@ export default {
   justify-content: center;
   width: 45%; /* 图例区占据左侧 45% */
   gap: 2px;
+  /* 最多 8 行: 每行 line-height 18px + 7 个 gap 2px = 158px
+     - ≤ 8 条: 内容自然居中, 不会撑爆
+     - > 8 条: SeamlessScroll 限定在这 158px 内自动滚动, hover 暂停 */
+  max-height: 158px;
+  overflow: hidden;
+}
+
+/* SeamlessScroll 内部承载多份克隆数据的行容器, measureSelector=".legend-rows" */
+.legend-rows {
+  display: flex;
+  flex-direction: column;
+  gap: 2px;
 }
 
 .legend-item {

+ 95 - 0
src/config/controlModeColors.js

@@ -0,0 +1,95 @@
+/**
+ * 控制模式配色注册表
+ *
+ * 设计原则:
+ *   - 暗色大屏背景, 全部色相亮度 50-65% (饱和、不刺眼)
+ *   - 同业务族用同色系: 算法类蓝色, 协调网络绿色, 人工/中央紫色, 预警黄/橙, 紧急红, 关闭/异常灰
+ *   - 后端只返 { name, value }, 前端按 name 查表注入 color
+ *   - 未注册的 name 走 FALLBACK_PALETTE 按 hash 兜底
+ *
+ * 用法:
+ *   import { resolveControlModeColor, getControlModeStyle } from '@/config/controlModeColors'
+ *   const color = resolveControlModeColor('定周期控制')          // -> '#33a3ff'
+ *   const item = getControlModeStyle({ name, value })            // -> { name, value, color }
+ */
+
+// 16 种业务模式 → 颜色
+// 排序按业务族, 不影响 API 返回顺序
+export const CONTROL_MODE_COLORS = {
+  // ── 算法类 (冷色蓝) ───────────────────────────────
+  '定周期控制':   '#33a3ff',
+  '感应控制':     '#e6734d',  // 现状保留 (历史橙)
+  '干线协调':     '#10b981',  // 现状保留 (绿)
+  '自适应控制':   '#2dd4bf',  // 现状保留 (青)
+
+  // ── 协调/网络 (绿色) ─────────────────────────────
+  '区域协调':     '#14b8a6',
+  '公交优先':     '#06b6d4',
+  '行人请求':     '#84cc16',
+
+  // ── 人工/中央 (紫色) ─────────────────────────────
+  '中心计划':     '#a78bfa',
+  '手动控制':     '#c084fc',
+
+  // ── 预警类 (黄/橙) ───────────────────────────────
+  '黄闪控制':     '#eab308',  // 现状保留 (物理黄灯)
+  '降级模式':     '#f59e0b',
+  '感应降级':     '#fb923c',
+
+  // ── 紧急类 (红色) ─────────────────────────────────
+  '紧急抢占':     '#f43f5e',
+  '全红控制':     '#dc2626',  // 物理全红灯
+  '应急疏散':     '#ef4444',
+
+  // ── 关闭/异常 (灰色) ─────────────────────────────
+  '关灯':         '#6b7280',
+}
+
+// 未在表内的 name 走这套兜底色板 (按 hash 落位)
+const FALLBACK_PALETTE = [
+  '#7dd3fc', '#fda4af', '#fcd34d', '#a3e635', '#67e8f9',
+  '#c4b5fd', '#fb7185', '#fde047', '#86efac', '#f0abfc',
+]
+
+function hashCode(s) {
+  let h = 0
+  for (let i = 0; i < s.length; i++) {
+    h = ((h << 5) - h) + s.charCodeAt(i)
+    h |= 0
+  }
+  return Math.abs(h)
+}
+
+/**
+ * 根据模式名取颜色
+ * @param {string} name
+ * @returns {string} hex color
+ */
+export function resolveControlModeColor(name) {
+  if (!name) return FALLBACK_PALETTE[0]
+  if (CONTROL_MODE_COLORS[name]) return CONTROL_MODE_COLORS[name]
+  return FALLBACK_PALETTE[hashCode(name) % FALLBACK_PALETTE.length]
+}
+
+/**
+ * 把后端 { name, value } 列表注入颜色
+ * @param {Array<{name: string, value: number}>} list
+ * @returns {Array<{name: string, value: number, color: string}>}
+ */
+export function applyControlModeColors(list) {
+  if (!Array.isArray(list)) return []
+  return list.map(item => ({
+    ...item,
+    color: item.color || resolveControlModeColor(item.name),
+  }))
+}
+
+/**
+ * 单条注入 (给单 item 用)
+ */
+export function getControlModeStyle(item) {
+  return {
+    ...item,
+    color: item.color || resolveControlModeColor(item.name),
+  }
+}

+ 3 - 1
src/mock/api.js

@@ -14,6 +14,7 @@
 
 import mockData from './mock_data.json'
 import { simulateMaxband } from './_simulateMaxband'
+import { applyControlModeColors } from '@/config/controlModeColors'
 
 // ── 静态资源(模拟 CDN / 后端返回的资源 URL)─────────────────────
 
@@ -722,7 +723,8 @@ export async function apiGetControlModeStats() {
   // 修正总数:将差值补到第一项(定周期控制),保证总数 = 信号机在线数
   const currentSum = fluctuated.reduce((s, m) => s + m.value, 0)
   fluctuated[0].value = Math.max(0, fluctuated[0].value + (onlineTotal - currentSum))
-  return ok(fluctuated)
+  // 按 name 注入色 (后端只返 { name, value }, 前端统一治理配色)
+  return ok(applyControlModeColors(fluctuated))
 }
 
 /**

+ 16 - 25
src/mock/mock_data.json

@@ -15480,31 +15480,22 @@
       "fault": 0
     },
     "controlModes": [
-      {
-        "name": "定周期控制",
-        "value": 396,
-        "color": "#33a3ff"
-      },
-      {
-        "name": "感应控制",
-        "value": 57,
-        "color": "#e6734d"
-      },
-      {
-        "name": "干线协调",
-        "value": 180,
-        "color": "#10b981"
-      },
-      {
-        "name": "黄闪控制",
-        "value": 14,
-        "color": "#eab308"
-      },
-      {
-        "name": "自适应控制(学习型干线协调)",
-        "value": 71,
-        "color": "#2dd4bf"
-      }
+      { "name": "定周期控制", "value": 396 },
+      { "name": "感应控制", "value": 57 },
+      { "name": "干线协调", "value": 180 },
+      { "name": "黄闪控制", "value": 14 },
+      { "name": "自适应控制", "value": 71 },
+      { "name": "区域协调", "value": 42 },
+      { "name": "公交优先", "value": 33 },
+      { "name": "行人请求", "value": 18 },
+      { "name": "中心计划", "value": 56 },
+      { "name": "手动控制", "value": 9 },
+      { "name": "降级模式", "value": 22 },
+      { "name": "感应降级", "value": 11 },
+      { "name": "紧急抢占", "value": 6 },
+      { "name": "全红控制", "value": 4 },
+      { "name": "应急疏散", "value": 2 },
+      { "name": "关灯", "value": 5 }
     ],
     "keyIntersections": [
       {