|
|
@@ -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>
|