|
@@ -0,0 +1,394 @@
|
|
|
|
+<template>
|
|
|
|
+ <div class="project-analysis">
|
|
|
|
+ <report-header
|
|
|
|
+ title="质量监控分析"
|
|
|
|
+ v-model:dateRange="curTimeRange"
|
|
|
|
+ @timeChange="timeChange"
|
|
|
|
+ >
|
|
|
|
+ <t-select
|
|
|
|
+ style="width: 200px"
|
|
|
|
+ :options="serviceOptions"
|
|
|
|
+ v-model="serviceId"
|
|
|
|
+ :keys="{ label: 'name', value: 'id' }"
|
|
|
|
+ ></t-select>
|
|
|
|
+ </report-header>
|
|
|
|
+ <div class="page-main">
|
|
|
|
+ <div class="col1">
|
|
|
|
+ <div class="card">
|
|
|
|
+ <div class="title">
|
|
|
|
+ <span class="label">质量问题总体盘点</span>
|
|
|
|
+ <t-select
|
|
|
|
+ style="width: 100px"
|
|
|
|
+ :options="groupOptions"
|
|
|
|
+ v-model="group"
|
|
|
|
+ ></t-select>
|
|
|
|
+ </div>
|
|
|
|
+ <div class="chart-wrap">
|
|
|
|
+ <div class="chart-part">
|
|
|
|
+ <my-chart :options="overallPieOptions"></my-chart>
|
|
|
|
+ </div>
|
|
|
|
+ <div class="chart-part">
|
|
|
|
+ <my-chart :options="overallRadarOptions"></my-chart>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ <div class="col2">
|
|
|
|
+ <div class="col2-head">
|
|
|
|
+ <div class="card">
|
|
|
|
+ <div class="title">
|
|
|
|
+ <span class="label">质量问题审核进度</span>
|
|
|
|
+ </div>
|
|
|
|
+ <div class="chart-wrap p-t-7px">
|
|
|
|
+ <t-progress :percentage="auditProgress || 0" />
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ <div class="col2-body">
|
|
|
|
+ <div class="col2-row">
|
|
|
|
+ <div class="card">
|
|
|
|
+ <div class="title">
|
|
|
|
+ <span class="label">影响度供应商分布及对比</span>
|
|
|
|
+ <t-button variant="outline">
|
|
|
|
+ <template #icon><FullscreenIcon /></template>
|
|
|
|
+ </t-button>
|
|
|
|
+ </div>
|
|
|
|
+ <div class="chart-wrap">
|
|
|
|
+ <my-chart :options="options11"></my-chart>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ <div class="card">
|
|
|
|
+ <div class="title">
|
|
|
|
+ <span class="label">执行协调类归因供应商分布及对比</span>
|
|
|
|
+ <t-button variant="outline">
|
|
|
|
+ <template #icon><FullscreenIcon /></template>
|
|
|
|
+ </t-button>
|
|
|
|
+ </div>
|
|
|
|
+ <div class="chart-wrap">
|
|
|
|
+ <my-chart :options="options12"></my-chart>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ <div class="col2-row">
|
|
|
|
+ <div class="card">
|
|
|
|
+ <div class="title">
|
|
|
|
+ <span class="label">影响度大区分布及对比TOP5</span>
|
|
|
|
+ <t-button variant="outline">
|
|
|
|
+ <template #icon><FullscreenIcon /></template>
|
|
|
|
+ </t-button>
|
|
|
|
+ </div>
|
|
|
|
+ <div class="chart-wrap">
|
|
|
|
+ <my-chart :options="options21"></my-chart>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ <div class="card">
|
|
|
|
+ <div class="title">
|
|
|
|
+ <span class="label">执行协调类归因大区分布及对比TOP5</span>
|
|
|
|
+ <t-button variant="outline">
|
|
|
|
+ <template #icon><FullscreenIcon /></template>
|
|
|
|
+ </t-button>
|
|
|
|
+ </div>
|
|
|
|
+ <div class="chart-wrap">
|
|
|
|
+ <my-chart :options="options22"></my-chart>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ </div> </div
|
|
|
|
+></template>
|
|
|
|
+
|
|
|
|
+<script setup name="QualityAnalysis">
|
|
|
|
+import { ref, computed, watch } from 'vue';
|
|
|
|
+import {
|
|
|
|
+ createRingPieOption,
|
|
|
|
+ createStackingBarOption,
|
|
|
|
+ createRadarOption,
|
|
|
|
+} from '@/utils/chart';
|
|
|
|
+import { useRequest } from 'vue-request';
|
|
|
|
+import {
|
|
|
|
+ serviceServiceList,
|
|
|
|
+ qualityAnalysisPie,
|
|
|
|
+ qualityAnalysisRadar,
|
|
|
|
+ qualityAnalysisProgress,
|
|
|
|
+ qualityAnalysisInfluence,
|
|
|
|
+} from '@/api/report';
|
|
|
|
+import { FullscreenIcon } from 'tdesign-icons-vue-next';
|
|
|
|
+const curTimeRange = ref([]);
|
|
|
|
+const timeParams = computed(() => {
|
|
|
|
+ return {
|
|
|
|
+ startTime: new Date(curTimeRange.value[0]).getTime(),
|
|
|
|
+ endTime: new Date(curTimeRange.value[1]).getTime(),
|
|
|
|
+ };
|
|
|
|
+});
|
|
|
|
+const groupOptions = ref([
|
|
|
|
+ { label: '按供应商', value: 'REASON_SUPPLIER' },
|
|
|
|
+ { label: '按大区', value: 'REASON_REGION' },
|
|
|
|
+]);
|
|
|
|
+let group = ref('REASON_SUPPLIER');
|
|
|
|
+const serviceOptions = ref([]);
|
|
|
|
+const serviceId = ref('');
|
|
|
|
+const timeChange = (time) => {
|
|
|
|
+ serviceServiceList(timeParams.value).then((res) => {
|
|
|
|
+ serviceOptions.value = res || [];
|
|
|
|
+ res?.length && (serviceId.value = res[0].id);
|
|
|
|
+ });
|
|
|
|
+};
|
|
|
|
+// 总体盘点饼图
|
|
|
|
+const { data: overallPieData, run: overallPieRun } =
|
|
|
|
+ useRequest(qualityAnalysisPie);
|
|
|
|
+// 总体盘点归因雷达图
|
|
|
|
+const { data: overallRadarData, run: overallRadarRun } =
|
|
|
|
+ useRequest(qualityAnalysisRadar);
|
|
|
|
+// 总体盘点饼图
|
|
|
|
+const { data: auditProgress, run: progressRun } = useRequest(
|
|
|
|
+ qualityAnalysisProgress
|
|
|
|
+);
|
|
|
|
+// 影响度/归因柱状图
|
|
|
|
+const { data: result11, run: run11 } = useRequest(qualityAnalysisInfluence);
|
|
|
|
+const { data: result12, run: run12 } = useRequest(qualityAnalysisInfluence);
|
|
|
|
+const { data: result21, run: run21 } = useRequest(qualityAnalysisInfluence);
|
|
|
|
+const { data: result22, run: run22 } = useRequest(qualityAnalysisInfluence);
|
|
|
|
+
|
|
|
|
+watch(serviceId, (serviceUnitId) => {
|
|
|
|
+ const param = { serviceUnitId };
|
|
|
|
+ overallPieRun(param);
|
|
|
|
+ overallRadarRun({ ...param, group: group.value });
|
|
|
|
+ progressRun(param);
|
|
|
|
+ run11({ ...param, group: 'INFLUENCE_SUPPLIER' });
|
|
|
|
+ run12({ ...param, group: 'INFLUENCE_REGION' });
|
|
|
|
+ run21({ ...param, group: 'REASON_SUPPLIER' });
|
|
|
|
+ run22({ ...param, group: 'REASON_REGION' });
|
|
|
|
+});
|
|
|
|
+watch(group, () => {
|
|
|
|
+ overallRadarRun({ serviceUnitId: serviceId.value, group: group.value });
|
|
|
|
+});
|
|
|
|
+
|
|
|
|
+const buildBarData = (result = {}) => {
|
|
|
|
+ let xData = Object.keys(result);
|
|
|
|
+ let names = Object.values(result)
|
|
|
|
+ ? Object.keys(Object.values(result)[0] || {})
|
|
|
|
+ : [];
|
|
|
|
+ let sData = [];
|
|
|
|
+ for (let i = 0; i < names.length; i++) {
|
|
|
|
+ let data = [];
|
|
|
|
+ for (let j = 0; j < xData.length; j++) {
|
|
|
|
+ data.push(result[xData[j]][names[i]] || 0);
|
|
|
|
+ }
|
|
|
|
+ sData.push({
|
|
|
|
+ name: names[i],
|
|
|
|
+ data: data,
|
|
|
|
+ barWidth: '16px',
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+ return { xData, seriesData: sData };
|
|
|
|
+};
|
|
|
|
+// 总体盘点饼图
|
|
|
|
+const overallPieOptions = computed(() => {
|
|
|
|
+ const result = Object.entries(overallPieData.value || {});
|
|
|
|
+ return createRingPieOption(
|
|
|
|
+ {
|
|
|
|
+ data: result.map(([name, value]) => {
|
|
|
|
+ return {
|
|
|
|
+ name,
|
|
|
|
+ value,
|
|
|
|
+ };
|
|
|
|
+ }),
|
|
|
|
+ center: ['50%', '40%'],
|
|
|
|
+ radius: [50, 80],
|
|
|
|
+ title: '质量问题累计',
|
|
|
|
+ },
|
|
|
|
+ {
|
|
|
|
+ legend: {
|
|
|
|
+ right: 'auto',
|
|
|
|
+ top: 'auto',
|
|
|
|
+ bottom: 30,
|
|
|
|
+ orient: 'horizontal',
|
|
|
|
+ },
|
|
|
|
+ }
|
|
|
|
+ );
|
|
|
|
+});
|
|
|
|
+// 总体盘点归因雷达图
|
|
|
|
+const overallRadarOptions = computed(() => {
|
|
|
|
+ const result = overallRadarData.value || {};
|
|
|
|
+ // const result = {
|
|
|
|
+ // all: {
|
|
|
|
+ // EXEC: 4,
|
|
|
|
+ // EXEC1: 3,
|
|
|
|
+ // EXEC2: 5,
|
|
|
|
+ // EXEC3: 6,
|
|
|
|
+ // },
|
|
|
|
+ // 小熊U: {
|
|
|
|
+ // EXEC: 2,
|
|
|
|
+ // EXEC1: 4,
|
|
|
|
+ // EXEC2: 7,
|
|
|
|
+ // EXEC3: 4,
|
|
|
|
+ // },
|
|
|
|
+ // };
|
|
|
|
+ let xData = Object.keys(result);
|
|
|
|
+ let names = Object.values(result)
|
|
|
|
+ ? Object.keys(Object.values(result)[0] || {})
|
|
|
|
+ : [];
|
|
|
|
+ let sData = [];
|
|
|
|
+ for (let i = 0; i < xData.length; i++) {
|
|
|
|
+ let data = [];
|
|
|
|
+ for (let j = 0; j < names.length; j++) {
|
|
|
|
+ data.push(result[xData[i]][names[j]] || 0);
|
|
|
|
+ }
|
|
|
|
+ sData.push({
|
|
|
|
+ name: xData[i] === 'all' ? '全部' : xData[i],
|
|
|
|
+ value: data,
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+ return createRadarOption({
|
|
|
|
+ names,
|
|
|
|
+ sData,
|
|
|
|
+ });
|
|
|
|
+});
|
|
|
|
+
|
|
|
|
+// 影响度/归因柱状图
|
|
|
|
+const barExtendOption = {
|
|
|
|
+ grid: {
|
|
|
|
+ top: 50,
|
|
|
|
+ },
|
|
|
|
+ yAxis: {
|
|
|
|
+ name: '单位:件',
|
|
|
|
+ nameTextStyle: {
|
|
|
|
+ color: '#595959',
|
|
|
|
+ },
|
|
|
|
+ nameGap: 28,
|
|
|
|
+ },
|
|
|
|
+};
|
|
|
|
+const options11 = computed(() => {
|
|
|
|
+ return createStackingBarOption(
|
|
|
|
+ buildBarData(result11.value || {}),
|
|
|
|
+ barExtendOption
|
|
|
|
+ );
|
|
|
|
+});
|
|
|
|
+const options12 = computed(() => {
|
|
|
|
+ return createStackingBarOption(
|
|
|
|
+ buildBarData(result12.value || {}),
|
|
|
|
+ barExtendOption
|
|
|
|
+ );
|
|
|
|
+});
|
|
|
|
+const options21 = computed(() => {
|
|
|
|
+ return createStackingBarOption(
|
|
|
|
+ buildBarData(result21.value || {}),
|
|
|
|
+ barExtendOption
|
|
|
|
+ );
|
|
|
|
+});
|
|
|
|
+const options22 = computed(() => {
|
|
|
|
+ return createStackingBarOption(
|
|
|
|
+ buildBarData(result22.value || {}),
|
|
|
|
+ barExtendOption
|
|
|
|
+ );
|
|
|
|
+});
|
|
|
|
+</script>
|
|
|
|
+
|
|
|
|
+<style lang="less" scoped>
|
|
|
|
+.project-analysis {
|
|
|
|
+ .page-main {
|
|
|
|
+ height: calc(100vh - 57px);
|
|
|
|
+ padding: 16px;
|
|
|
|
+ overflow: auto;
|
|
|
|
+ display: flex;
|
|
|
|
+
|
|
|
|
+ .col1 {
|
|
|
|
+ height: 100%;
|
|
|
|
+ width: 360px;
|
|
|
|
+ margin-right: 16px;
|
|
|
|
+ flex-grow: 0;
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
+ .card {
|
|
|
|
+ height: 100%;
|
|
|
|
+ }
|
|
|
|
+ .chart-wrap {
|
|
|
|
+ position: relative;
|
|
|
|
+
|
|
|
|
+ &::after {
|
|
|
|
+ content: '';
|
|
|
|
+ display: block;
|
|
|
|
+ position: absolute;
|
|
|
|
+ top: 50%;
|
|
|
|
+ border-bottom: 1px solid #e5e5e5;
|
|
|
|
+ left: 14px;
|
|
|
|
+ right: 14px;
|
|
|
|
+ z-index: 9;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ .chart-part {
|
|
|
|
+ height: 50%;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ .col2 {
|
|
|
|
+ flex-grow: 2;
|
|
|
|
+ display: flex;
|
|
|
|
+ flex-direction: column;
|
|
|
|
+ height: 100%;
|
|
|
|
+
|
|
|
|
+ .col2-head {
|
|
|
|
+ flex-grow: 0;
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
+ }
|
|
|
|
+ .col2-body {
|
|
|
|
+ flex-grow: 2;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ .col2-row {
|
|
|
|
+ padding-top: 16px;
|
|
|
|
+ display: flex;
|
|
|
|
+ justify-content: stretch;
|
|
|
|
+ height: 50%;
|
|
|
|
+
|
|
|
|
+ .card {
|
|
|
|
+ width: calc(50% - 8px);
|
|
|
|
+ &:first-child {
|
|
|
|
+ margin-right: 8px;
|
|
|
|
+ }
|
|
|
|
+ &:last-child {
|
|
|
|
+ margin-left: 8px;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ .card {
|
|
|
|
+ padding: 16px;
|
|
|
|
+ background-color: #fff;
|
|
|
|
+ border: 1px solid #e5e5e5;
|
|
|
|
+ border-radius: 4px;
|
|
|
|
+ display: flex;
|
|
|
|
+ flex-direction: column;
|
|
|
|
+
|
|
|
|
+ .title {
|
|
|
|
+ display: flex;
|
|
|
|
+ justify-content: space-between;
|
|
|
|
+ align-items: center;
|
|
|
|
+ flex-grow: 0;
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
+ .label {
|
|
|
|
+ height: 22px;
|
|
|
|
+ font-size: 14px;
|
|
|
|
+ font-weight: 500;
|
|
|
|
+ color: #262626;
|
|
|
|
+ line-height: 22px;
|
|
|
|
+ }
|
|
|
|
+ .t-button {
|
|
|
|
+ padding: 0;
|
|
|
|
+ border: none !important;
|
|
|
|
+ outline: none;
|
|
|
|
+ background-color: transparent !important;
|
|
|
|
+ height: auto;
|
|
|
|
+ color: #595959;
|
|
|
|
+ :deep(div) {
|
|
|
|
+ display: none;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ .chart-wrap {
|
|
|
|
+ flex-grow: 2;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+</style>
|