Explorar el Código

task-971:质量管理首页看板优化03

lucw hace 4 días
padre
commit
ce1bdc5b57

+ 22 - 4
src/api/aps/index.js

@@ -70,13 +70,31 @@ export async function warehousing(data) {
   return Promise.reject(new Error(res.data.message));
 }
 
-// 工厂列表
+// 工厂列表(兼容 { code, data } 包装与直接返回数组两种后端实现)
 export async function getFactoryList(params) {
   const res = await request.get(`/main/factoryarea/getFactoryList`, { params });
-  if (res.data.code == 0) {
-    return res.data.data;
+  const body = res.data;
+  if (Array.isArray(body)) {
+    return body;
   }
-  return Promise.reject(new Error(res.data.message));
+  if (body && body.code == 0) {
+    return body.data;
+  }
+  return Promise.reject(new Error((body && body.message) || '获取工厂列表失败'));
+}
+
+// 当前登录用户所属工厂ID(雪花ID以字符串返回,取不到时为 null)
+// 兼容:包装体 { code, data } 与直接返回裸值两种后端实现
+export async function getCurrentUserFactoryId() {
+  const res = await request.get(`/main/factoryarea/getCurrentUserFactoryId`);
+  const body = res.data;
+  if (body && typeof body === 'object') {
+    if (body.code == 0) {
+      return body.data;
+    }
+    return Promise.reject(new Error(body.message || '获取当前用户所属工厂失败'));
+  }
+  return body;
 }
 
 // aps生产计划获取产品的多个bom版本

+ 36 - 0
src/api/qmsDashboard/index.js

@@ -37,6 +37,23 @@ export function getDefectTypeDist(data) {
   return post('/qms/dashboard/defectTypeDist', data);
 }
 
+/**
+ * 不良类型占比:复用质量首页接口
+ * 入参 { factoryId, createTimeStart, createTimeEnd },时间格式 yyyy-MM(不传默认当年)
+ * 后端为 @RequestParam,参数必须走 query,不能放在 JSON body
+ */
+export async function getDefectTypeProportion(data) {
+  const res = await request({
+    url: '/qms/quality_work_order/queryProportionDefectiveTypes',
+    method: 'post',
+    params: data || {}
+  });
+  if (res.data.code == 0) {
+    return res.data.data;
+  }
+  return Promise.reject(new Error(res.data.message));
+}
+
 // 记录方法分布(原检验方式分布)
 export function getRecordingMethodDist(data) {
   return post('/qms/dashboard/recordingMethodDist', data);
@@ -61,3 +78,22 @@ export function getDisposalDist(data) {
 export function getAlertList(data) {
   return post('/qms/dashboard/alertList', data);
 }
+
+/**
+ * 工厂 / 班组 / 产线合格率:复用质量首页既有接口
+ * 入参 { timeType: 1本周(近7天) 2本月(近31天) 3本年(近12个月) },注意与看板主 timeType(本季度) 语义不同
+ */
+// 工厂合格率
+export function getFactoryRate(data) {
+  return post('/qms/quality_work_order/queryFactory', data);
+}
+
+// 班组合格率
+export function getTeamRate(data) {
+  return post('/qms/quality_work_order/queryTeam', data);
+}
+
+// 产线合格率
+export function getProductionLineRate(data) {
+  return post('/qms/quality_work_order/queryProductionLine', data);
+}

+ 15 - 1
src/views/qualityBoard/components/DrillDialog.vue

@@ -17,6 +17,8 @@
           {{ config.title }}
         </span>
         <span class="qb-drill-source">{{ config.source }}</span>
+        <!-- 继承看板顶部工厂筛选时给出显式提示,避免误读为全厂数据 -->
+        <span v-if="factoryName" class="qb-drill-factory">工厂:{{ factoryName }}</span>
       </div>
       <div class="qb-drill-head-right">
         <button class="qb-drill-btn" :disabled="exporting || loading" @click="exportCsv">
@@ -148,7 +150,9 @@ export default {
     drillKey: { type: String, default: '' },
     timeRange: { type: Array, default: () => [] },
     /** 图表点击带入的附加条件,如 { qualityType: 1 } / { supplierName: 'xx' } */
-    extraWhere: { type: Object, default: () => ({}) }
+    extraWhere: { type: Object, default: () => ({}) },
+    /** 看板顶部选中的工厂名称,为空表示全部工厂 */
+    factoryName: { type: String, default: '' }
   },
   data() {
     return {
@@ -396,6 +400,16 @@ export default {
   font-size: 12px;
   color: #8aa0bd;
 }
+/* 工厂筛选提示:与标题同一行,用边框胶囊与口径说明区分 */
+.qb-drill-factory {
+  margin-top: 4px;
+  margin-left: 10px;
+  padding: 1px 10px;
+  font-size: 12px;
+  color: #7fe3c8;
+  border: 1px solid rgba(37, 208, 176, 0.45);
+  border-radius: 10px;
+}
 /* 导出 / 关闭按钮组:绝对定位到标题栏右上角,不受标题宽度影响 */
 .qb-drill-head-right {
   position: absolute;

+ 463 - 42
src/views/qualityBoard/index.vue

@@ -4,9 +4,20 @@
     <div class="qb-header">
       <div class="qb-title">
         <span class="qb-title-main">质量管理看板</span>
-        <span class="qb-title-sub">Quality Management Dashboard</span>
       </div>
       <div class="qb-header-right">
+        <!-- 工厂维度:除“工厂合格率”外的所有图表、KPI、明细弹窗都按此工厂筛选 -->
+        <el-select
+          v-model="factoryId"
+          class="qb-factory"
+          size="mini"
+          placeholder="全部工厂"
+          clearable
+          popper-class="qb-factory-popper"
+          @change="changeFactory"
+        >
+          <el-option v-for="item in factoryList" :key="item.id" :label="item.name" :value="item.id" />
+        </el-select>
         <div class="qb-range">
           <span
             v-for="item in rangeList"
@@ -59,7 +70,35 @@
       </div>
     </div>
 
-    <!-- 行2 -->
+    <!-- 行2:合格率三视图(工厂 / 班组 / 产线),时间维度统一跟随页面顶部 本月 / 本季度 / 本年 -->
+    <div class="qb-row">
+      <div class="qb-card qb-col-4">
+        <div class="qb-card-head">
+          <span title="合格率 = 合格数量 ÷ 检验数量 × 100%">工厂合格率</span>
+          <span class="qb-tip">{{ rateRangeLabel }}</span>
+        </div>
+        <v-chart v-if="factoryRateOption" class="qb-chart" :option="factoryRateOption" autoresize />
+        <div v-else class="qb-empty">暂无数据</div>
+      </div>
+      <div class="qb-card qb-col-4">
+        <div class="qb-card-head">
+          <span title="合格率 = 合格数量 ÷ 检验数量 × 100%">班组合格率</span>
+          <span class="qb-tip">{{ rateRangeLabel }}</span>
+        </div>
+        <v-chart v-if="teamRateOption" class="qb-chart" :option="teamRateOption" autoresize />
+        <div v-else class="qb-empty">暂无数据</div>
+      </div>
+      <div class="qb-card qb-col-4">
+        <div class="qb-card-head">
+          <span title="合格率 = 合格数量 ÷ 检验数量 × 100%">产线合格率</span>
+          <span class="qb-tip">{{ rateRangeLabel }}</span>
+        </div>
+        <v-chart v-if="lineRateOption" class="qb-chart" :option="lineRateOption" autoresize />
+        <div v-else class="qb-empty">暂无数据</div>
+      </div>
+    </div>
+
+    <!-- 行3 -->
     <div class="qb-row">
       <div class="qb-card qb-col-3">
         <div class="qb-card-head">
@@ -90,17 +129,24 @@
         <v-chart v-if="trendOption" class="qb-chart" :option="trendOption" autoresize />
         <div v-else class="qb-empty">暂无数据</div>
       </div>
-      <div class="qb-card qb-col-3">
+      <div class="qb-card qb-col-4">
         <div class="qb-card-head">
-          <span>不良类型分布</span>
+          <span title="不良类型占比 = 该类型不合格数量 ÷ 各类不合格数量合计">不良类型占比</span>
           <span class="qb-link" @click="openKpiDrill('defect')">明细</span>
         </div>
-        <v-chart v-if="defectOption" class="qb-chart" :option="defectOption" autoresize @click="handleDefectTypeDrill" />
+        <div v-if="defectOption" class="qb-pie-wrap">
+          <v-chart class="qb-chart" :option="defectOption" autoresize @click="handleDefectTypeDrill" />
+          <!-- 中心合计用覆盖层实现,不依赖 echarts graphic 组件,窄卡片下也不会错位 -->
+          <div class="qb-pie-center">
+            <span class="qb-pie-center-label">不良次数</span>
+            <span class="qb-pie-center-value">{{ defectTotal }}</span>
+          </div>
+        </div>
         <div v-else class="qb-empty">暂无数据</div>
       </div>
     </div>
 
-    <!-- 行3 -->
+    <!-- 行4 -->
     <div class="qb-row">
       <div class="qb-card qb-col-4">
         <div class="qb-card-head">
@@ -139,7 +185,7 @@
       </div>
     </div>
 
-    <!-- 行4 -->
+    <!-- 行5 -->
     <div class="qb-row">
       <div class="qb-card qb-col-4">
         <div class="qb-card-head">
@@ -226,12 +272,20 @@
       :drill-key="drillKey"
       :time-range="drillRange"
       :extra-where="drillExtra"
+      :factory-name="factoryName"
     />
   </div>
 </template>
 
 <script>
-import { getOverview } from '@/api/qmsDashboard';
+import {
+  getOverview,
+  getFactoryRate,
+  getTeamRate,
+  getProductionLineRate,
+  getDefectTypeProportion
+} from '@/api/qmsDashboard';
+import { getFactoryList, getCurrentUserFactoryId } from '@/api/aps';
 import DrillDialog from './components/DrillDialog';
 import { getDefaultRange, DRILL_CONFIG } from './components/drillConfig';
 import { use } from 'echarts/core';
@@ -252,6 +306,9 @@ export default {
     return {
       loading: false,
       timeType: 1,
+      /* 工厂维度:'' 表示全部工厂;工厂 id 为雪花 id,统一用字符串避免精度丢失 */
+      factoryId: '',
+      factoryList: [],
       drillVisible: false,
       drillKey: '',
       drillExtra: {},
@@ -263,12 +320,17 @@ export default {
       kpi: {},
       qualityTypeDist: [],
       trend: [],
-      defectTypeDist: [],
+      /* 不良类型占比:来自质量首页同款接口,字段 { name, value } */
+      defectTypeProportion: [],
       recordingMethodDist: [],
       flowStatus: [],
       supplierRank: [],
       disposalDist: [],
-      alertList: []
+      alertList: [],
+      /* 工厂 / 班组 / 产线合格率:时间维度统一跟随页面顶部 timeType,不再各自维护 */
+      factoryRateList: [],
+      teamRateList: [],
+      lineRateList: []
     };
   },
   computed: {
@@ -283,6 +345,16 @@ export default {
     drillRange() {
       return getDefaultRange(this.timeType);
     },
+    /** 三张合格率卡片右上角提示:与页面顶部时间维度保持一致的文案 */
+    rateRangeLabel() {
+      const hit = (this.rangeList || []).find((item) => item.value === this.timeType);
+      return hit ? hit.label : '';
+    },
+    /** 当前选中的工厂名称,明细弹窗头部展示用;未选工厂时为空 */
+    factoryName() {
+      const hit = (this.factoryList || []).find((item) => item.id === this.factoryId);
+      return hit ? hit.name : '';
+    },
     kpiList() {
       const kpi = this.kpi || {};
       /* 待处理异常没有现成比例,用 待处理 / 不良品总数 作为环形进度 */
@@ -489,8 +561,121 @@ export default {
         ]
       };
     },
+    /** 不良次数合计(环形中心展示) */
+    defectTotal() {
+      return this.defectTypeProportion.reduce(
+        (sum, item) => sum + (Number(item.value) || 0),
+        0
+      );
+    },
+    /**
+     * 不良类型占比:沿用质量首页口径(按不合格数量统计、图例带次数、环形中心展示合计),
+     * 仅配色与字号适配看板深色风格
+     */
     defectOption() {
-      return this.donutOption(this.defectTypeDist, '不良类型');
+      const list = this.defectTypeProportion;
+      if (!list || !list.length) return null;
+      return {
+        color: PALETTE,
+        tooltip: {
+          trigger: 'item',
+          formatter: (params) => `${params.name}<br/>次数:${params.value}(${params.percent}%)`
+        },
+        legend: {
+          type: 'scroll',
+          orient: 'vertical',
+          right: 0,
+          top: 'center',
+          itemWidth: 10,
+          itemHeight: 10,
+          textStyle: { color: AXIS_COLOR, fontSize: 12 },
+          formatter: (name) => {
+            const hit = list.find((item) => item.name === name);
+            return `${name}  |  ${hit ? hit.value : 0}`;
+          }
+        },
+        series: [
+          {
+            name: '不良类型占比',
+            type: 'pie',
+            radius: ['52%', '72%'],
+            center: ['32%', '50%'],
+            avoidLabelOverlap: true,
+            itemStyle: { borderColor: '#0e1626', borderWidth: 2 },
+            label: { show: false },
+            labelLine: { show: false },
+            data: list.map((item, index) => ({
+              name: item.name,
+              value: item.value,
+              itemStyle: { color: PALETTE[index % PALETTE.length] }
+            }))
+          }
+        ]
+      };
+    },
+    /**
+     * 工厂合格率:环形饼展示各工厂,图例文字直接带合格率数值,避免只看到占比
+     */
+    factoryRateOption() {
+      const list = this.factoryRateList || [];
+      if (!list.length) return null;
+      const nameOf = (item) => item.factoryName || '未命名工厂';
+      return {
+        color: PALETTE,
+        tooltip: {
+          trigger: 'item',
+          formatter: (params) => `${params.name}<br/>合格率:${params.value}%`
+        },
+        legend: {
+          type: 'scroll',
+          orient: 'vertical',
+          right: 0,
+          top: 'center',
+          itemWidth: 10,
+          itemHeight: 10,
+          textStyle: { color: AXIS_COLOR, fontSize: 12 },
+          formatter: (name) => {
+            const hit = list.find((item) => nameOf(item) === name);
+            return hit ? `${name}  ${this.toPercent(hit.qualificationRate)}%` : name;
+          }
+        },
+        series: [
+          {
+            name: '工厂合格率',
+            type: 'pie',
+            radius: ['48%', '72%'],
+            center: ['34%', '50%'],
+            label: { show: false },
+            labelLine: { show: false },
+            itemStyle: { borderRadius: 6 },
+            data: list.map((item, index) => ({
+              name: nameOf(item),
+              value: this.toPercent(item.qualificationRate),
+              itemStyle: { color: PALETTE[index % PALETTE.length] }
+            }))
+          }
+        ]
+      };
+    },
+    /** 班组合格率:柱状图,柱顶标合格率 */
+    teamRateOption() {
+      const list = this.teamRateList || [];
+      if (!list.length) return null;
+      return this.rateBarOption(
+        list.map((item) => item.teamName || '未命名班组'),
+        list.map((item) => this.toPercent(item.qualificationRate)),
+        PALETTE[1]
+      );
+    },
+    /** 产线合格率:柱状图,柱顶标合格率 */
+    lineRateOption() {
+      const list = this.lineRateList || [];
+      if (!list.length) return null;
+      return this.rateBarOption(
+        list.map((item) => item.productionLineName || '未命名产线'),
+        list.map((item) => this.toPercent(item.qualificationRate)),
+        PALETTE[0]
+      );
     },
     /**
      * 记录方法分布:柱长表示次数,柱顶同时标注次数与占比(需求要求显示占比)
@@ -617,23 +802,203 @@ export default {
       };
     }
   },
-  mounted() {
+  async mounted() {
+    /* 默认工厂确定后再取数,避免先按“全部工厂”查一遍再按工厂查一遍 */
+    await this.loadFactoryList();
     this.fetchData();
   },
   methods: {
+    /**
+     * 工厂下拉数据(后端 /main/factoryarea/getFactoryList 只返回 type=1 的工厂)
+     * 默认选中当前登录人所属工厂;取不到(或已不在工厂列表中)时取第一个工厂
+     */
+    async loadFactoryList() {
+      /* 两个接口互不影响:任一失败都不应该让工厂下拉变成“无数据” */
+      const toResult = (p) => p.then((v) => ({ ok: true, v }), (e) => ({ ok: false, e }));
+      const [listRes, userRes] = await Promise.all([
+        toResult(getFactoryList()),
+        toResult(getCurrentUserFactoryId())
+      ]);
+      let list = [];
+      if (listRes.ok) {
+        /* 兼容后端返回数组 / 分页对象 / 二次包装三种形态 */
+        const raw = listRes.v;
+        let arr = [];
+        if (Array.isArray(raw)) {
+          arr = raw;
+        } else if (raw && typeof raw === 'object') {
+          const keys = ['list', 'records', 'content', 'rows', 'data'];
+          const hit = keys.find((key) => Array.isArray(raw[key]));
+          arr = hit ? raw[hit] : [];
+        }
+        list = arr
+          .filter((item) => item && item.id !== undefined && item.id !== null)
+          .map((item) => ({ id: String(item.id), name: item.name }));
+      } else {
+        console.warn('[质量看板] 工厂列表接口异常:', listRes.e);
+      }
+      if (!userRes.ok) {
+        console.warn('[质量看板] 当前用户所属工厂接口异常:', userRes.e);
+      }
+      this.factoryList = list;
+      /* 当前用户工厂归一化:null / '' / undefined / 空数组 都视为“没取到” */
+      let userFactoryId = userRes.ok ? userRes.v : null;
+      if (Array.isArray(userFactoryId)) {
+        userFactoryId = userFactoryId.length ? userFactoryId[0] : null;
+      } else if (userFactoryId && typeof userFactoryId === 'object') {
+        userFactoryId = userFactoryId.id !== undefined ? userFactoryId.id : null;
+      }
+      if (userFactoryId === '' || userFactoryId === undefined) {
+        userFactoryId = null;
+      }
+      const matched = userFactoryId !== null
+        ? list.find((item) => item.id === String(userFactoryId))
+        : null;
+      /* 取不到(或不在工厂列表中)时默认选中列表第一条;列表为空才保持“全部工厂” */
+      this.factoryId = matched ? matched.id : (list.length ? list[0].id : '');
+    },
+    /** 切换工厂:除“工厂合格率”外的所有区块按新工厂重新取数 */
+    changeFactory() {
+      this.fetchData();
+    },
     changeRange(type) {
       if (this.timeType === type) return;
       this.timeType = type;
       this.fetchData();
     },
+    /**
+     * 合格率接口的时间参数:跟随页面顶部 本月 / 本季度 / 本年,换算成具体起止时间
+     * timeType = 4 表示按调用方传入的区间取数(后端 quality_work_order 支持)
+     */
+    buildRateRange() {
+      const now = new Date();
+      let start;
+      if (this.timeType === 1) {
+        start = new Date(now.getFullYear(), now.getMonth(), 1);
+      } else if (this.timeType === 2) {
+        /* 本季度首月:0~2 月为 Q1,以此类推 */
+        start = new Date(now.getFullYear(), Math.floor(now.getMonth() / 3) * 3, 1);
+      } else {
+        start = new Date(now.getFullYear(), 0, 1);
+      }
+      return { timeType: 4, startTime: start.getTime(), endTime: now.getTime() };
+    },
+    /**
+     * 三个合格率接口共用同一时间条件,各自独立请求,单个失败只影响自己那张卡片
+     * 工厂合格率本身按工厂分组对比,不传工厂;班组 / 产线合格率按顶部工厂筛选
+     */
+    fetchRateData() {
+      const timeParam = this.buildRateRange();
+      const factoryParam = this.factoryId ? { factoriesId: this.factoryId } : {};
+      return Promise.all([
+        getFactoryRate(timeParam)
+          .then((list) => {
+            this.factoryRateList = list || [];
+          })
+          .catch(() => {
+            this.factoryRateList = [];
+          }),
+        getTeamRate({ ...timeParam, ...factoryParam })
+          .then((list) => {
+            this.teamRateList = list || [];
+          })
+          .catch(() => {
+            this.teamRateList = [];
+          }),
+        getProductionLineRate({ ...timeParam, ...factoryParam })
+          .then((list) => {
+            this.lineRateList = list || [];
+          })
+          .catch(() => {
+            this.lineRateList = [];
+          })
+      ]);
+    },
+    /**
+     * 把看板 timeType(1 本月 / 2 本季度 / 3 本年)换算成后台要求的 yyyy-MM 区间
+     * 后端 queryProportionDefectiveTypes 按 createTimeStart ~ createTimeEnd 的月份取数
+     */
+    buildDefectRange() {
+      const now = new Date();
+      const year = now.getFullYear();
+      const month = now.getMonth() + 1;
+      let startMonth = 1;
+      if (this.timeType === 1) {
+        startMonth = month;
+      } else if (this.timeType === 2) {
+        /* 本季度:1~3 月为 Q1,以此类推 */
+        startMonth = Math.floor((month - 1) / 3) * 3 + 1;
+      }
+      const fmt = (m) => `${year}-${String(m).padStart(2, '0')}`;
+      const params = { createTimeStart: fmt(startMonth), createTimeEnd: fmt(month) };
+      if (this.factoryId) {
+        params.factoryId = this.factoryId;
+      }
+      return params;
+    },
+    /** 不良类型占比:接口失败只清空本卡片,不影响其他区块 */
+    async fetchDefectTypeProportion() {
+      try {
+        const list = await getDefectTypeProportion(this.buildDefectRange());
+        this.defectTypeProportion = (list || [])
+          .map((item) => ({
+            name: item.categoryLevelClassName || '未分类',
+            value: Number(item.unQualifiedNumber) || 0
+          }))
+          .filter((item) => item.value > 0);
+      } catch (e) {
+        this.defectTypeProportion = [];
+      }
+    },
+    /** 合格率柱状图(班组 / 产线通用):柱顶标数值,Y 轴固定 0~100 便于横向对比 */
+    rateBarOption(names, values, color) {
+      return {
+        grid: { left: 10, right: 16, top: 26, bottom: 4, containLabel: true },
+        tooltip: {
+          trigger: 'axis',
+          axisPointer: { type: 'shadow' },
+          formatter: (params) => `${params[0].name}<br/>合格率:${params[0].value}%`
+        },
+        xAxis: {
+          type: 'category',
+          data: names,
+          axisTick: { show: false },
+          axisLine: { lineStyle: { color: SPLIT_COLOR } },
+          axisLabel: {
+            color: AXIS_COLOR,
+            interval: 0,
+            fontSize: 11,
+            rotate: names.length > 5 ? 25 : 0
+          }
+        },
+        yAxis: {
+          type: 'value',
+          max: 100,
+          splitLine: { lineStyle: { color: SPLIT_COLOR } },
+          axisLabel: { color: AXIS_COLOR, formatter: '{value}%' }
+        },
+        series: [
+          {
+            type: 'bar',
+            barMaxWidth: 26,
+            itemStyle: { color, borderRadius: [8, 8, 0, 0] },
+            label: { show: true, position: 'top', color: '#cfe2ff', fontSize: 11, formatter: '{c}%' },
+            data: values
+          }
+        ]
+      };
+    },
     async fetchData() {
       this.loading = true;
+      /* 工厂 / 班组 / 产线合格率跟随顶部 本月/本季度/本年,与主看板数据并行加载 */
+      this.fetchRateData();
+      /* 不良类型占比走质量首页同款接口,时间维度跟随看板 本月/本季度/本年 */
+      this.fetchDefectTypeProportion();
       try {
-        const data = await getOverview({ timeType: this.timeType });
+        const data = await getOverview({ timeType: this.timeType, factoryId: this.factoryId || null });
         this.kpi = data.kpi || {};
         this.qualityTypeDist = data.qualityTypeDist || [];
         this.trend = data.trend || [];
-        this.defectTypeDist = data.defectTypeDist || [];
         this.recordingMethodDist = data.recordingMethodDist || [];
         this.flowStatus = data.flowStatus || [];
         this.supplierRank = (data.supplierRank || []).slice(0, 5);
@@ -665,32 +1030,6 @@ export default {
       }
       return Math.round(num * 10) / 10;
     },
-    donutOption(list, name) {
-      if (!list || !list.length) return null;
-      return {
-        color: PALETTE,
-        tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
-        legend: {
-          type: 'scroll',
-          orient: 'vertical',
-          right: 0,
-          top: 'center',
-          textStyle: { color: AXIS_COLOR, fontSize: 12 }
-        },
-        series: [
-          {
-            name,
-            type: 'pie',
-            radius: ['45%', '68%'],
-            center: ['38%', '50%'],
-            avoidLabelOverlap: true,
-            itemStyle: { borderColor: '#0e1626', borderWidth: 2 },
-            label: { show: false },
-            data: list.map((item) => ({ name: item.name, value: item.value || 0 }))
-          }
-        ]
-      };
-    },
     /**
      * 打开明细弹窗(不跳转路由),配置见 components/drillConfig.js
      * @param {String} key 弹窗配置 key
@@ -702,7 +1041,11 @@ export default {
         return;
       }
       this.drillKey = key;
-      this.drillExtra = extraWhere ? { ...extraWhere } : {};
+      /* 明细继承看板顶部工厂筛选(工单台账与不良品台账均按 factoriesId 过滤) */
+      this.drillExtra = { ...(extraWhere || {}) };
+      if (this.factoryId) {
+        this.drillExtra.factoriesId = this.factoryId;
+      }
       this.drillVisible = true;
     },
     /** 三类检验:点柱条按质检类型下钻(来料 1 / 生产 2 / 成品 3) */
@@ -725,7 +1068,7 @@ export default {
       }
       this.openKpiDrill('completion', { recordingMethod: Number(item.code) });
     },
-    /** 不良类型分布:台账接口无不良类型字段,暂只打开不良明细 */
+    /** 不良类型占比:台账主表无不良类型字段,点击扇区只打开不良明细 */
     handleDefectTypeDrill() {
       this.openKpiDrill('defect');
       this.$message.info('不良类型暂不支持作为台账筛选条件,已展示筛选期间内全部不良明细');
@@ -785,6 +1128,33 @@ export default {
   display: flex;
   align-items: center;
 }
+/* 工厂下拉:与时间维度胶囊同一视觉语言(深色透明底 + 细边框) */
+.qb-factory {
+  width: 140px;
+  margin-right: 12px;
+}
+.qb-factory ::v-deep .el-input__inner {
+  height: 28px;
+  line-height: 28px;
+  padding: 0 30px 0 12px;
+  font-size: 12px;
+  color: #cfe2ff;
+  background: rgba(80, 140, 200, 0.12);
+  border: 1px solid rgba(80, 140, 200, 0.35);
+  border-radius: 15px;
+}
+.qb-factory ::v-deep .el-input__inner::placeholder {
+  color: #8aa0bd;
+}
+.qb-factory ::v-deep .el-input__suffix {
+  line-height: 28px;
+}
+.qb-factory ::v-deep .el-select__caret {
+  color: #8aa0bd;
+}
+.qb-factory ::v-deep .el-input__icon {
+  line-height: 28px;
+}
 .qb-range {
   display: flex;
   border: 1px solid rgba(80, 140, 200, 0.35);
@@ -973,6 +1343,31 @@ export default {
   height: 220px;
   width: 100%;
 }
+/* 不良类型占比:环形中心合计文字用覆盖层定位,left 与饼图 center 保持一致 */
+.qb-pie-wrap {
+  position: relative;
+}
+.qb-pie-center {
+  position: absolute;
+  top: 50%;
+  left: 32%;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  pointer-events: none;
+  transform: translate(-50%, -50%);
+}
+.qb-pie-center-label {
+  font-size: 11px;
+  color: #8aa0bd;
+}
+.qb-pie-center-value {
+  margin-top: 2px;
+  font-size: 16px;
+  font-weight: 600;
+  color: #cfe2ff;
+  font-family: 'DIN Alternate', Arial, sans-serif;
+}
 /* 三类检验卡片底部有累计统计条,图高相应收一点,保持与同行卡片等高 */
 .qb-chart-type,
 .qb-empty.qb-empty-type {
@@ -1318,3 +1713,29 @@ export default {
   }
 }
 </style>
+
+<!-- 工厂下拉浮层挂在 body 上,scoped 样式无法命中,单独声明全局样式 -->
+<style>
+.qb-factory-popper {
+  border: 1px solid rgba(80, 140, 200, 0.35) !important;
+  background: #0f1a2c !important;
+}
+.qb-factory-popper .el-select-dropdown__item {
+  height: 30px;
+  line-height: 30px;
+  font-size: 12px;
+  color: #b6c9e0;
+}
+.qb-factory-popper .el-select-dropdown__item.hover,
+.qb-factory-popper .el-select-dropdown__item:hover {
+  background: rgba(80, 140, 200, 0.18);
+}
+.qb-factory-popper .el-select-dropdown__item.selected {
+  color: #25d0b0;
+  font-weight: 600;
+}
+.qb-factory-popper .popper__arrow,
+.qb-factory-popper .popper__arrow::after {
+  display: none;
+}
+</style>