BSWYZ пре 3 дана
родитељ
комит
c651afc83d
1 измењених фајлова са 668 додато и 0 уклоњено
  1. 668 0
      imcs-ui/src/views/zuihou/statisticalAnalysis/rhythmDisplay/Index.vue

+ 668 - 0
imcs-ui/src/views/zuihou/statisticalAnalysis/rhythmDisplay/Index.vue

@@ -0,0 +1,668 @@
+<template>
+  <div class="rhythm-board">
+    <transition name="boot-fade">
+      <div
+        v-if="isLoading"
+        class="boot-loader"
+      >
+        <div class="loader-visual">
+          <div class="loader-ring ring-outer" />
+          <div class="loader-ring ring-inner" />
+          <div class="loader-core">
+            <i class="el-icon-data-line" />
+          </div>
+        </div>
+        <div class="loader-eyebrow">
+          PRODUCTION TAKT SYSTEM
+        </div>
+        <h2>节拍数据加载中</h2>
+        <p>正在初始化四节点生产节拍...</p>
+        <div class="loader-progress">
+          <span :style="{ width: loadingProgress + '%' }" />
+        </div>
+        <strong class="loader-percent">{{ loadingProgress }}%</strong>
+      </div>
+    </transition>
+
+    <header class="board-header">
+      <div class="brand">
+        <div class="brand-mark">
+          <i class="el-icon-data-line" />
+        </div>
+        <div>
+          <!-- <div class="eyebrow">
+            AUTOMATED PRODUCTION TAKT
+          </div> -->
+          <h1>生产节拍看板</h1>
+          <!-- <p>上料 · 加工 · 清洗 · 下料,四节点标准节拍展示</p> -->
+        </div>
+      </div>
+      <div class="header-side">
+        <div class="running-badge">
+          <i class="pulse-dot" />
+          24H 自动化生产
+        </div>
+        <el-input
+          v-model.trim="keyword"
+          class="search-input"
+          prefix-icon="el-icon-search"
+          placeholder="搜索产品或工序"
+          clearable
+        />
+      </div>
+    </header>
+
+    <section class="metrics">
+      <div class="metric metric-primary">
+        <div class="metric-icon">
+          <i class="el-icon-box" />
+        </div>
+        <div>
+          <span>产品种类</span>
+          <strong>{{ products.length }}<small> 种</small></strong>
+        </div>
+      </div>
+      <div class="metric">
+        <div class="metric-icon">
+          <i class="el-icon-s-operation" />
+        </div>
+        <div>
+          <span>覆盖工序</span>
+          <strong>{{ processCount }}<small> 道</small></strong>
+        </div>
+      </div>
+      <div class="metric">
+        <div class="metric-icon">
+          <i class="el-icon-connection" />
+        </div>
+        <div>
+          <span>节拍节点</span>
+          <strong>4<small> 个</small></strong>
+        </div>
+      </div>
+      <div class="metric metric-accent">
+        <div class="metric-icon">
+          <i class="el-icon-timer" />
+        </div>
+        <div>
+          <span>平均单件节拍</span>
+          <strong>{{ averageTakt }}<small> min</small></strong>
+        </div>
+      </div>
+    </section>
+
+    <div class="section-heading">
+      <div>
+        <h2>产品节拍总览</h2>
+        <p>以一次装夹为计算批次,点击产品查看每道工序的四节点节拍</p>
+      </div>
+      <div class="data-note">
+        <i class="el-icon-info" />
+        上下料、清洗时间按现场估算
+      </div>
+    </div>
+
+    <section
+      v-if="filteredProducts.length"
+      class="product-grid"
+    >
+      <button
+        v-for="(product, index) in filteredProducts"
+        :key="product.id"
+        type="button"
+        class="product-card"
+        @click="openDetail(product)"
+      >
+        <div class="card-top">
+          <span class="product-index">{{ formatIndex(index + 1) }}</span>
+          <span class="process-count">{{ product.processes.length }} 道工序</span>
+        </div>
+        <h3>{{ product.name }}</h3>
+        <div class="takt-main">
+          <span>综合单件节拍</span>
+          <div>
+            <strong>{{ formatNumber(productTakt(product)) }}</strong>
+            <small>min / 件</small>
+          </div>
+        </div>
+        <div class="card-detail">
+          <div>
+            <span>一次装夹</span>
+            <strong>{{ product.processes[0].batchSize }} 件</strong>
+          </div>
+          <div>
+            <span>整批节拍</span>
+            <strong>{{ formatDuration(productBatchTakt(product)) }}</strong>
+          </div>
+        </div>
+        <div class="mini-flow">
+          <span
+            v-for="node in nodeLegend"
+            :key="node.key"
+          >
+            <i :class="node.icon" />
+            {{ node.label }}
+          </span>
+        </div>
+        <div class="view-action">
+          查看节拍详情 <i class="el-icon-right" />
+        </div>
+      </button>
+    </section>
+
+    <div
+      v-else
+      class="empty-state"
+    >
+      <i class="el-icon-search" />
+      <h3>未找到相关产品</h3>
+      <p>请尝试搜索其他产品名称或工序名称</p>
+    </div>
+
+    <el-dialog
+      v-if="activeProduct"
+      :visible.sync="dialogVisible"
+      :title="activeProduct.name + ' · 节拍详情'"
+      width="88%"
+      top="5vh"
+      custom-class="takt-dialog"
+      append-to-body
+    >
+      <div class="dialog-summary">
+        <div>
+          <span>综合单件节拍</span>
+          <strong>{{ formatNumber(productTakt(activeProduct)) }}<small> min/件</small></strong>
+        </div>
+        <div>
+          <span>工序数量</span>
+          <strong>{{ activeProduct.processes.length }}<small> 道</small></strong>
+        </div>
+        <div>
+          <span>节点数量</span>
+          <strong>{{ activeProduct.processes.length * 4 }}<small> 个</small></strong>
+        </div>
+        <div>
+          <span>流转方式</span>
+          <strong class="mode-value">整批流转</strong>
+        </div>
+      </div>
+
+      <div class="process-list">
+        <article
+          v-for="(process, processIndex) in activeProduct.processes"
+          :key="process.name"
+          class="process-row"
+        >
+          <div class="process-heading">
+            <span class="step-index">{{ processIndex + 1 }}</span>
+            <div>
+              <label>第 {{ processIndex + 1 }} 道工序</label>
+              <h3>{{ process.name }}</h3>
+            </div>
+            <div class="process-takt">
+              <span>单件节拍</span>
+              <strong>{{ formatNumber(processTakt(process)) }} min</strong>
+            </div>
+            <div class="batch-info">
+              <span>一次装夹</span>
+              <strong>{{ process.batchSize }} 件</strong>
+            </div>
+          </div>
+
+          <div class="node-flow">
+            <template v-for="(node, nodeIndex) in processNodes(process)">
+              <div
+                :key="node.key"
+                class="takt-node"
+                :class="'node-' + node.key"
+              >
+                <div class="node-icon">
+                  <i :class="node.icon" />
+                </div>
+                <div class="node-copy">
+                  <span>{{ node.label }}</span>
+                  <strong>{{ node.minutes }}<small> min</small></strong>
+                  <em v-if="node.simulated">估算值</em>
+                  <em v-else>提供数据</em>
+                </div>
+                <div class="node-share">
+                  <span :style="{ width: nodeShare(node.minutes, process) + '%' }" />
+                </div>
+              </div>
+              <div
+                v-if="nodeIndex < 3"
+                :key="node.key + '-arrow'"
+                class="flow-arrow"
+              >
+                <i class="el-icon-right" />
+              </div>
+            </template>
+          </div>
+
+          <div class="process-total">
+            <span>上料至下料完整周期</span>
+            <strong>{{ formatDuration(processBatchTakt(process)) }}</strong>
+          </div>
+        </article>
+      </div>
+
+      <span slot="footer">
+        <el-button @click="dialogVisible = false">关闭窗口</el-button>
+      </span>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+const PRODUCTS = [
+  {
+    id: 1,
+    name: "Z509左后翼转接座",
+    processes: [
+      { name: "数铣1", loading: 10, processing: 233, cleaning: 15, unloading: 10, batchSize: 4 }
+    ]
+  },
+  {
+    id: 2,
+    name: "Z509左前翼转接座",
+    processes: [
+      { name: "数铣1", loading: 10, processing: 193, cleaning: 15, unloading: 10, batchSize: 3 }
+    ]
+  },
+  {
+    id: 3,
+    name: "Z509右后翼转接座",
+    processes: [
+      { name: "数铣1", loading: 10, processing: 233, cleaning: 15, unloading: 10, batchSize: 4 }
+    ]
+  },
+  {
+    id: 4,
+    name: "Z509右前翼转接座",
+    processes: [
+      { name: "数铣1", loading: 10, processing: 193, cleaning: 15, unloading: 10, batchSize: 3 }
+    ]
+  },
+  {
+    id: 5,
+    name: "Z509支撑座",
+    processes: [
+      { name: "数铣1", loading: 10, processing: 112, cleaning: 15, unloading: 10, batchSize: 2 }
+    ]
+  },
+  {
+    id: 6,
+    name: "Z509舵翼本体",
+    processes: [
+      { name: "数铣1", loading: 10, processing: 171, cleaning: 15, unloading: 10, batchSize: 2 },
+      { name: "数铣2", loading: 10, processing: 110, cleaning: 15, unloading: 10, batchSize: 2 },
+      { name: "数铣3", loading: 10, processing: 330, cleaning: 15, unloading: 10, batchSize: 2 },
+      { name: "数铣4", loading: 10, processing: 309, cleaning: 15, unloading: 10, batchSize: 2 }
+    ]
+  },
+  {
+    id: 7,
+    name: "Z509制导舱体",
+    processes: [
+      { name: "数铣1", loading: 10, processing: 153, cleaning: 15, unloading: 10, batchSize: 1 }
+    ]
+  },
+  {
+    id: 8,
+    name: "Z509控制舱体",
+    processes: [
+      { name: "数铣1", loading: 10, processing: 195, cleaning: 15, unloading: 10, batchSize: 1 }
+    ]
+  }
+];
+
+export default {
+  name: "RhythmDisplay",
+  data() {
+    return {
+      isLoading: true,
+      loadingProgress: 0,
+      loadingTimer: null,
+      hideTimer: null,
+      keyword: "",
+      dialogVisible: false,
+      activeProduct: null,
+      products: PRODUCTS,
+      nodeLegend: [
+        { key: "loading", label: "上料", icon: "el-icon-upload2" },
+        { key: "processing", label: "加工", icon: "el-icon-setting" },
+        { key: "cleaning", label: "清洗", icon: "el-icon-magic-stick" },
+        { key: "unloading", label: "下料", icon: "el-icon-download" }
+      ]
+    };
+  },
+  computed: {
+    filteredProducts() {
+      if (!this.keyword) return this.products;
+      const value = this.keyword.toLowerCase();
+      return this.products.filter(product => {
+        return product.name.toLowerCase().indexOf(value) > -1 ||
+          product.processes.some(process => process.name.toLowerCase().indexOf(value) > -1);
+      });
+    },
+    processCount() {
+      return this.products.reduce((total, product) => total + product.processes.length, 0);
+    },
+    averageTakt() {
+      const total = this.products.reduce((sum, product) => sum + this.productTakt(product), 0);
+      return this.formatNumber(total / this.products.length);
+    }
+  },
+  mounted() {
+    this.startLoading();
+  },
+  beforeDestroy() {
+    clearInterval(this.loadingTimer);
+    clearTimeout(this.hideTimer);
+  },
+  methods: {
+    startLoading() {
+      this.loadingTimer = setInterval(() => {
+        const step = this.loadingProgress < 70 ? 8 : 4;
+        this.loadingProgress = Math.min(100, this.loadingProgress + step);
+        if (this.loadingProgress === 100) {
+          clearInterval(this.loadingTimer);
+          this.hideTimer = setTimeout(() => {
+            this.isLoading = false;
+          }, 260);
+        }
+      }, 65);
+    },
+    openDetail(product) {
+      this.activeProduct = product;
+      this.dialogVisible = true;
+    },
+    formatIndex(value) {
+      return value < 10 ? "0" + value : value;
+    },
+    processBatchTakt(process) {
+      return process.loading + process.processing + process.cleaning + process.unloading;
+    },
+    processTakt(process) {
+      return this.processBatchTakt(process) / process.batchSize;
+    },
+    productTakt(product) {
+      return product.processes.reduce((sum, process) => sum + this.processTakt(process), 0);
+    },
+    productBatchTakt(product) {
+      return product.processes.reduce((sum, process) => sum + this.processBatchTakt(process), 0);
+    },
+    processNodes(process) {
+      return this.nodeLegend.map(node => ({
+        key: node.key,
+        label: node.label,
+        icon: node.icon,
+        minutes: process[node.key],
+        simulated: node.key !== "processing"
+      }));
+    },
+    nodeShare(minutes, process) {
+      return Math.max(4, Math.round(minutes / this.processBatchTakt(process) * 100));
+    },
+    formatNumber(value) {
+      return Number(value).toFixed(1).replace(/\.0$/, "");
+    },
+    formatDuration(minutes) {
+      if (minutes < 60) return minutes + " min";
+      const hours = Math.floor(minutes / 60);
+      const rest = minutes % 60;
+      return rest ? hours + "h " + rest + "min" : hours + "h";
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.rhythm-board {
+  --navy: #14213b;
+  --blue: #2f73f1;
+  --cyan: #17a8bd;
+  --muted: #75839a;
+  position: relative;
+  min-height: calc(100vh - 84px);
+  padding: 30px;
+  color: var(--navy);
+  background:
+    radial-gradient(circle at 8% 0, rgba(47, 115, 241, .12), transparent 24%),
+    radial-gradient(circle at 96% 4%, rgba(23, 168, 189, .09), transparent 24%),
+    #f4f7fb;
+  box-sizing: border-box;
+}
+.boot-loader {
+  position: absolute; z-index: 30; inset: 0; min-height: calc(100vh - 84px);
+  display: flex; flex-direction: column; align-items: center; justify-content: center;
+  color: #14213b;
+  background:
+    radial-gradient(circle at center, rgba(47, 115, 241, .12), transparent 31%),
+    linear-gradient(145deg, #f5f8fc, #edf3fb);
+}
+.loader-visual { position: relative; width: 108px; height: 108px; margin-bottom: 25px; }
+.loader-ring { position: absolute; border-radius: 50%; }
+.ring-outer {
+  inset: 0; border: 2px solid rgba(47, 115, 241, .15); border-top-color: #2f73f1;
+  border-right-color: #17a8bd; animation: loader-spin 1.15s linear infinite;
+}
+.ring-inner {
+  inset: 12px; border: 1px dashed rgba(47, 115, 241, .45);
+  animation: loader-spin-reverse 1.8s linear infinite;
+}
+.loader-core {
+  position: absolute; inset: 25px; display: flex; align-items: center; justify-content: center;
+  border-radius: 50%; color: #fff; font-size: 24px;
+  background: linear-gradient(145deg, #2d70ec, #18a6bd);
+  box-shadow: 0 12px 28px rgba(47, 115, 241, .25);
+}
+.loader-eyebrow { color: #2f73f1; font-size: 10px; font-weight: 700; letter-spacing: 2px; }
+.boot-loader h2 { margin-top: 9px; font-size: 24px; }
+.boot-loader p { margin-top: 8px; color: #75839a; font-size: 13px; }
+.loader-progress {
+  width: 260px; height: 5px; margin-top: 24px; overflow: hidden;
+  border-radius: 4px; background: #dce5f2;
+}
+.loader-progress span {
+  display: block; height: 100%; border-radius: 4px;
+  background: linear-gradient(90deg, #2f73f1, #17a8bd);
+  transition: width .16s ease;
+}
+.loader-percent { margin-top: 9px; color: #557199; font-size: 12px; }
+.boot-fade-leave-active { transition: opacity .35s ease, transform .35s ease; }
+.boot-fade-leave-to { opacity: 0; transform: scale(1.015); }
+@keyframes loader-spin {
+  to { transform: rotate(360deg); }
+}
+@keyframes loader-spin-reverse {
+  to { transform: rotate(-360deg); }
+}
+.board-header {
+  display: flex; align-items: center; justify-content: space-between; gap: 24px; margin-bottom: 24px;
+}
+.brand { display: flex; align-items: center; gap: 17px; }
+.brand-mark {
+  width: 58px; height: 58px; display: flex; align-items: center; justify-content: center;
+  border-radius: 16px; color: #fff; font-size: 27px;
+  background: linear-gradient(145deg, #2869ed, #19a8c1);
+  box-shadow: 0 13px 30px rgba(47, 115, 241, .24);
+}
+.eyebrow { margin-bottom: 5px; color: var(--blue); font-size: 12px; font-weight: 700; letter-spacing: 1.7px; }
+h1, h2, h3, p { margin: 0; }
+h1 { font-size: 29px; line-height: 1.25; }
+.brand p { margin-top: 6px; color: var(--muted); font-size: 14px; }
+.header-side { display: flex; align-items: center; gap: 16px; }
+.running-badge {
+  display: flex; align-items: center; padding: 10px 14px; border-radius: 22px;
+  color: #187a63; font-size: 13px; font-weight: 700; background: #e5f7f1;
+}
+.pulse-dot {
+  width: 8px; height: 8px; margin-right: 8px; border-radius: 50%; background: #21b88e;
+  box-shadow: 0 0 0 5px rgba(33, 184, 142, .13);
+}
+.search-input { width: 230px; }
+.search-input ::v-deep .el-input__inner {
+  height: 42px; border-color: #dfe6f1; border-radius: 11px; background: rgba(255,255,255,.9);
+}
+.metrics { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 28px; }
+.metric {
+  min-height: 94px; display: flex; align-items: center; gap: 15px; padding: 20px 22px;
+  border: 1px solid rgba(225,232,243,.95); border-radius: 16px; background: rgba(255,255,255,.9);
+  box-shadow: 0 10px 30px rgba(34, 58, 96, .055);
+}
+.metric-icon {
+  width: 47px; height: 47px; display: flex; align-items: center; justify-content: center;
+  border-radius: 13px; color: #3974d7; font-size: 21px; background: #eaf2ff;
+}
+.metric:nth-child(2) .metric-icon { color: #168da1; background: #e6f8fa; }
+.metric:nth-child(3) .metric-icon { color: #745ccc; background: #f0ecff; }
+.metric-accent .metric-icon { color: #168766; background: #e5f7f1; }
+.metric span { display: block; color: var(--muted); font-size: 13px; }
+.metric strong { display: block; margin-top: 5px; font-size: 29px; line-height: 1; }
+.metric small { color: #8995a8; font-size: 13px; font-weight: 500; }
+.section-heading {
+  display: flex; align-items: flex-end; justify-content: space-between; gap: 20px; margin: 0 2px 15px;
+}
+.section-heading h2 { font-size: 21px; }
+.section-heading p { margin-top: 6px; color: var(--muted); font-size: 13px; }
+.data-note { color: #8a6a2a; font-size: 12px; }
+.data-note i { margin-right: 4px; }
+.product-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
+.product-card {
+  position: relative; min-width: 0; min-height: 286px; padding: 20px;
+  overflow: hidden; border: 1px solid #e1e8f3; border-radius: 17px; color: inherit;
+  text-align: left; background: rgba(255,255,255,.92); cursor: pointer;
+  box-shadow: 0 10px 30px rgba(34, 58, 96, .055);
+  transition: transform .22s ease, box-shadow .22s ease, border-color .22s ease;
+}
+.product-card::before {
+  content: ""; position: absolute; top: 0; left: 0; width: 100%; height: 4px;
+  background: linear-gradient(90deg, #2f73f1, #19a7bd);
+}
+.product-card:hover {
+  transform: translateY(-3px); border-color: #c8d8f0; box-shadow: 0 18px 42px rgba(34, 58, 96, .11);
+}
+.card-top { display: flex; align-items: center; justify-content: space-between; }
+.product-index {
+  width: 37px; height: 37px; display: flex; align-items: center; justify-content: center;
+  border-radius: 10px; color: #326bc8; font-size: 13px; font-weight: 700; background: #edf3ff;
+}
+.process-count { padding: 5px 10px; border-radius: 18px; color: #60718d; font-size: 11px; background: #f0f3f7; }
+.product-card h3 { margin-top: 14px; overflow: hidden; font-size: 18px; text-overflow: ellipsis; white-space: nowrap; }
+.takt-main {
+  display: flex; align-items: flex-end; justify-content: space-between; margin-top: 18px;
+  padding: 15px 17px; border-radius: 12px; background: linear-gradient(135deg, #edf4ff, #f0fbfc);
+}
+.takt-main > span { color: #66758d; font-size: 12px; }
+.takt-main div { text-align: right; }
+.takt-main strong { color: #205fc8; font-size: 31px; line-height: 1; }
+.takt-main small { display: block; margin-top: 4px; color: #75849a; font-size: 10px; }
+.card-detail { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 13px; }
+.card-detail > div { padding: 10px 12px; border-left: 2px solid #d9e4f5; }
+.card-detail span { display: block; color: #8a96a8; font-size: 10px; }
+.card-detail strong { display: block; margin-top: 5px; color: #42536d; font-size: 13px; }
+.mini-flow { display: flex; align-items: center; justify-content: space-between; margin-top: 16px; color: #76859b; font-size: 10px; }
+.mini-flow span { display: flex; align-items: center; gap: 4px; }
+.mini-flow i { color: #4780db; }
+.view-action { margin-top: 17px; color: #376fc9; font-size: 12px; text-align: right; }
+.empty-state {
+  min-height: 400px; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center;
+}
+.empty-state > i { color: #94a8ca; font-size: 42px; }
+.empty-state h3 { margin-top: 15px; font-size: 18px; }
+.empty-state p { margin-top: 7px; color: var(--muted); font-size: 13px; }
+.dialog-summary { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 18px; }
+.dialog-summary > div { padding: 18px 20px; border: 1px solid #e4eaf3; border-radius: 12px; background: #f8faff; }
+.dialog-summary span { display: block; color: #7d8aa0; font-size: 12px; }
+.dialog-summary strong { display: block; margin-top: 8px; color: #263a5a; font-size: 25px; }
+.dialog-summary small { color: #8190a5; font-size: 12px; font-weight: 500; }
+.dialog-summary .mode-value { color: #2572d7; font-size: 19px; }
+.process-list { display: flex; flex-direction: column; gap: 15px; max-height: 58vh; padding-right: 4px; overflow-y: auto; }
+.process-row { padding: 18px; border: 1px solid #e2e9f3; border-radius: 14px; background: #fbfcfe; }
+.process-heading { display: grid; grid-template-columns: 42px minmax(150px, 1fr) 150px 110px; align-items: center; gap: 13px; }
+.step-index {
+  width: 38px; height: 38px; display: flex; align-items: center; justify-content: center;
+  border-radius: 11px; color: #fff; font-size: 15px; font-weight: 700; background: #3475de;
+}
+.process-heading label { color: #8b97aa; font-size: 10px; }
+.process-heading h3 { margin-top: 3px; font-size: 17px; }
+.process-takt, .batch-info { text-align: right; }
+.process-takt span, .batch-info span { display: block; color: #8b97aa; font-size: 10px; }
+.process-takt strong, .batch-info strong { display: block; margin-top: 4px; color: #2e5fae; font-size: 15px; }
+.node-flow { display: flex; align-items: stretch; margin-top: 16px; }
+.takt-node {
+  flex: 1; min-width: 145px; padding: 15px; border: 1px solid #e4eaf3; border-radius: 12px; background: #fff;
+}
+.node-icon {
+  width: 34px; height: 34px; display: flex; align-items: center; justify-content: center;
+  border-radius: 10px; color: #3473d7; font-size: 17px; background: #eaf2ff;
+}
+.node-processing .node-icon { color: #9a641c; background: #fff3dc; }
+.node-cleaning .node-icon { color: #168b9d; background: #e5f8fa; }
+.node-unloading .node-icon { color: #1b8a68; background: #e5f7f1; }
+.node-copy { position: relative; margin-top: 11px; }
+.node-copy > span { display: block; color: #77869c; font-size: 11px; }
+.node-copy strong { display: block; margin-top: 3px; color: #263a58; font-size: 22px; }
+.node-copy small { color: #8793a5; font-size: 10px; font-weight: 500; }
+.node-copy em {
+  position: absolute; right: 0; bottom: 2px; color: #99a4b4; font-size: 8px; font-style: normal;
+}
+.node-share { height: 4px; margin-top: 12px; overflow: hidden; border-radius: 3px; background: #edf1f6; }
+.node-share span { display: block; height: 100%; border-radius: 3px; background: #4a80dd; }
+.node-processing .node-share span { background: #e3a13f; }
+.node-cleaning .node-share span { background: #34aabc; }
+.node-unloading .node-share span { background: #35a982; }
+.flow-arrow { flex: 0 0 38px; display: flex; align-items: center; justify-content: center; color: #8da5ca; }
+.process-total {
+  display: flex; align-items: center; justify-content: flex-end; gap: 12px;
+  margin-top: 13px; color: #77859a; font-size: 11px;
+}
+.process-total strong { color: #315f9f; font-size: 14px; }
+
+@media (max-width: 1200px) {
+  .product-grid { grid-template-columns: repeat(2, 1fr); }
+  .node-flow { overflow-x: auto; padding-bottom: 7px; }
+  .takt-node { flex: 0 0 175px; }
+}
+@media (max-width: 850px) {
+  .rhythm-board { padding: 20px 16px 34px; }
+  .board-header, .section-heading { align-items: flex-start; flex-direction: column; }
+  .header-side { width: 100%; justify-content: space-between; }
+  .metrics { grid-template-columns: repeat(2, 1fr); }
+  .dialog-summary { grid-template-columns: repeat(2, 1fr); }
+}
+@media (max-width: 560px) {
+  .brand-mark, .eyebrow { display: none; }
+  .header-side { align-items: stretch; flex-direction: column; }
+  .search-input { width: 100%; }
+  .metrics, .product-grid { grid-template-columns: 1fr; }
+  .process-heading { grid-template-columns: 42px 1fr; }
+  .process-takt, .batch-info { text-align: left; }
+  .dialog-summary { grid-template-columns: 1fr; }
+}
+@media (prefers-reduced-motion: reduce) {
+  .ring-outer, .ring-inner { animation: none; }
+  .loader-progress span, .boot-fade-leave-active { transition: none; }
+}
+</style>
+
+<style lang="scss">
+.takt-dialog {
+  min-width: 760px;
+  overflow: hidden;
+  border-radius: 16px;
+  box-shadow: 0 28px 80px rgba(25, 45, 80, .2);
+
+  .el-dialog__header { padding: 21px 25px 18px; border-bottom: 1px solid #e6ebf3; }
+  .el-dialog__title { color: #14213b; font-size: 20px; font-weight: 700; }
+  .el-dialog__body { padding: 22px 25px 14px; }
+  .el-dialog__footer { padding: 11px 25px 20px; }
+}
+@media (max-width: 820px) {
+  .takt-dialog {
+    width: calc(100% - 24px) !important;
+    min-width: 0;
+  }
+}
+</style>