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