huminmin
2026-06-01 a563ea879ef5fb6897e76d2df661e465dce2ab9b
src/views/index.vue
@@ -113,10 +113,11 @@
          <div class="panel-title-row">
            <div class="panel-title">生产订单进度</div>
            <el-radio-group v-model="orderFilter" size="small">
              <el-radio-button label="all">全部({{ orderProgressMeta.total }})</el-radio-button>
              <el-radio-button label="inProgress">进行中({{ orderProgressMeta.inProgressCount }})</el-radio-button>
              <el-radio-button label="completed">已完成({{ orderProgressMeta.completedCount }})</el-radio-button>
              <el-radio-button label="paused">已暂停({{ orderProgressMeta.pausedCount }})</el-radio-button>
              <el-radio-button :value="'all'">全部({{ orderProgressMeta.total }})</el-radio-button>
              <el-radio-button :value="'waiting'">待开始({{ orderProgressMeta.waitingCount }})</el-radio-button>
              <el-radio-button :value="'inProgress'">进行中({{ orderProgressMeta.inProgressCount }})</el-radio-button>
              <el-radio-button :value="'completed'">已完成({{ orderProgressMeta.completedCount }})</el-radio-button>
              <el-radio-button :value="'paused'">已暂停({{ orderProgressMeta.pausedCount }})</el-radio-button>
            </el-radio-group>
          </div>
          <el-table :data="filteredOrders" stripe>
@@ -181,32 +182,6 @@
          </ul>
        </div>
        <div v-if="visiblePanels.quality" class="cockpit-panel quality-panel">
          <div class="panel-title-row">
            <div class="panel-title">质量统计</div>
            <el-radio-group v-model="qualityRange" size="small" @change="qualityStatisticsInfo">
              <el-radio-button :value="1">周</el-radio-button>
              <el-radio-button :value="2">月</el-radio-button>
              <el-radio-button :value="3">季度</el-radio-button>
            </el-radio-group>
          </div>
          <div class="quality-cards">
            <div class="quality-card one">原材料已检数量 <span>{{ qualityStatisticsObject.supplierNum }}件</span></div>
            <div class="quality-card two">过程检验数量 <span>{{ qualityStatisticsObject.processNum }}件</span></div>
            <div class="quality-card three">出厂已检数量 <span>{{ qualityStatisticsObject.factoryNum }}件</span></div>
          </div>
          <Echarts
              :options="chartBaseOptions"
              :chartStyle="chartStyle"
              :grid="grid"
              :legend="barLegend"
              :series="barSeries1"
              :tooltip="tooltip"
              :xAxis="xAxis1"
              :yAxis="yAxis1"
              style="height: 270px"
          />
        </div>
      </div>
      <div v-if="hasRightPanels" class="right-column">
@@ -293,21 +268,34 @@
          </ul>
        </div>
        <div v-if="visiblePanels.receipt" class="cockpit-panel receipt-panel">
          <div class="panel-title">回款与开票分析</div>
          <Echarts
              :options="chartBaseOptions"
              :chartStyle="chartStyle"
              :grid="grid"
              :legend="lineLegend"
              :series="lineSeries"
              :tooltip="tooltipLine"
              :xAxis="xAxis2"
              :yAxis="yAxis2"
              style="height: 300px"
          />
        </div>
      </div>
      <!-- 质量统计 - 占满整行 -->
      <div v-if="visiblePanels.quality" class="cockpit-panel quality-panel full-width">
        <div class="panel-title-row">
          <div class="panel-title">质量统计</div>
          <el-radio-group v-model="qualityRange" size="small" @change="qualityStatisticsInfo">
            <el-radio-button :value="1">周</el-radio-button>
            <el-radio-button :value="2">月</el-radio-button>
            <el-radio-button :value="3">季度</el-radio-button>
          </el-radio-group>
        </div>
        <div class="quality-cards">
          <div class="quality-card one">原材料已检数量 <span>{{ qualityStatisticsObject.supplierNum }}吨</span></div>
          <div class="quality-card two">过程检验数量 <span>{{ qualityStatisticsObject.processNum }}吨</span></div>
          <div class="quality-card three">出厂已检数量 <span>{{ qualityStatisticsObject.factoryNum }}吨</span></div>
        </div>
        <Echarts
            :options="chartBaseOptions"
            :chartStyle="chartStyle"
            :grid="grid"
            :legend="barLegend"
            :series="barSeries1"
            :tooltip="tooltip"
            :xAxis="xAxis1"
            :yAxis="yAxis1"
            style="height: 270px"
        />
      </div>
    </section>
@@ -319,7 +307,7 @@
      <div class="process-selection-wrapper">
        <el-checkbox-group v-model="tempProcessIds">
          <div class="process-grid">
            <el-checkbox v-for="item in processOptions" :key="item.id" :label="item.id" border>
            <el-checkbox v-for="item in processOptions" :key="item.id" :value="item.id" border>
              {{ item.name }}
            </el-checkbox>
          </div>
@@ -359,7 +347,6 @@
import useUserStore from "@/store/modules/user.js";
import {
  analysisCustomerContractAmounts,
  getAmountHalfYear,
  getBusiness,
  homeTodos,
  processDataProductionStatistics,
@@ -367,7 +354,6 @@
  productionOverview,
  productionRealtimeBoard,
  qualityInspectionStatistics,
  statisticsReceivablePayable,
  todayProductionPlan,
} from "@/api/viewIndex.js";
import { list } from "@/api/productionManagement/productionProcess";
@@ -433,6 +419,8 @@
  monthSaleHaveMoney: 0,
  monthSaleMoney: 0,
  todayInventoryNum: 0,
  lastMonthSaleMoney: 0,
  lastMonthPurchaseMoney: 0,
});
const qualityStatisticsObject = ref({
@@ -454,10 +442,13 @@
});
const orderProgressMeta = ref({
  status: "all",
  tab: "all",
  bizDate: null,
  total: 0,
  pageNum: 1,
  pageSize: 10,
  waitingCount: 0,
  inProgressCount: 0,
  completedCount: 0,
  pausedCount: 0,
@@ -577,74 +568,6 @@
    itemStyle: { color: "#f59e0b", borderRadius: [6, 6, 0, 0] },
    emphasis: { focus: "series" },
    data: [],
  },
]);
const lineLegend = {
  show: true,
  textStyle: { color: axisTextColor },
  data: ["开票", "回款"],
};
const xAxis2 = ref([
  {
    type: "category",
    data: [],
    axisLine: { lineStyle: { color: axisLineColor } },
    axisLabel: {
      color: axisTextColor,
      interval: 0,
      formatter: (value) => value.replace(/~/g, "\n"),
    },
  },
]);
const yAxis2 = ref([
  {
    type: "value",
    splitLine: { lineStyle: { color: splitLineColor } },
    axisLine: { lineStyle: { color: axisLineColor } },
    axisLabel: { color: axisTextColor },
  },
]);
const tooltipLine = {
  trigger: "axis",
  backgroundColor: "rgba(255, 255, 255, 0.97)",
  borderColor: "rgba(148, 163, 184, 0.26)",
  textStyle: { color: "#334155" },
};
const lineSeries = ref([
  {
    name: "开票",
    type: "line",
    data: [],
    smooth: true,
    itemStyle: { color: "#2563eb" },
    lineStyle: { width: 2, color: "#2563eb" },
    showSymbol: true,
    areaStyle: {
      color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
        { offset: 0, color: "rgba(37, 99, 235, 0.24)" },
        { offset: 1, color: "rgba(37, 99, 235, 0.02)" },
      ]),
    },
  },
  {
    name: "回款",
    type: "line",
    data: [],
    smooth: true,
    itemStyle: { color: "#14b8a6" },
    lineStyle: { width: 2, color: "#14b8a6" },
    showSymbol: true,
    areaStyle: {
      color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
        { offset: 0, color: "rgba(20, 184, 166, 0.2)" },
        { offset: 1, color: "rgba(20, 184, 166, 0.02)" },
      ]),
    },
  },
]);
@@ -780,15 +703,29 @@
  });
});
// 计算环比增长率
const calculateGrowthRate = (current, last) => {
  const currentVal = parseFloat(current) || 0;
  const lastVal = parseFloat(last) || 0;
  if (lastVal === 0) return currentVal > 0 ? 100 : 0;
  return ((currentVal - lastVal) / lastVal * 100).toFixed(1);
};
const dashboardCards = computed(() => [
  {
    key: "sales",
    title: "销售数据",
    desc: "本月销售额(元)",
    value: formatNumber(businessInfo.value.monthSaleMoney),
    subLabel: "未开票金额",
    subValue: formatNumber(businessInfo.value.monthSaleHaveMoney),
    trend: `占比 ${ratioText(businessInfo.value.monthSaleHaveMoney, businessInfo.value.monthSaleMoney)}`,
    subLabel: "上月销售",
    subValue: formatNumber(businessInfo.value.lastMonthSaleMoney),
    trend: (() => {
      const rate = calculateGrowthRate(businessInfo.value.monthSaleMoney, businessInfo.value.lastMonthSaleMoney);
      const num = parseFloat(rate);
      if (num > 0) return `环比 ↑ ${rate}%`;
      if (num < 0) return `环比 ↓ ${Math.abs(num)}%`;
      return "环比持平";
    })(),
    icon: DataLine,
    visible: visibleModules.value.sales,
  },
@@ -797,12 +734,15 @@
    title: "采购数据",
    desc: "本月采购额(元)",
    value: formatNumber(businessInfo.value.monthPurchaseMoney),
    subLabel: "待付款金额",
    subValue: formatNumber(businessInfo.value.monthPurchaseHaveMoney),
    trend: `占比 ${ratioText(
        businessInfo.value.monthPurchaseHaveMoney,
        businessInfo.value.monthPurchaseMoney
    )}`,
    subLabel: "上月采购",
    subValue: formatNumber(businessInfo.value.lastMonthPurchaseMoney),
    trend: (() => {
      const rate = calculateGrowthRate(businessInfo.value.monthPurchaseMoney, businessInfo.value.lastMonthPurchaseMoney);
      const num = parseFloat(rate);
      if (num > 0) return `环比 ↑ ${rate}%`;
      if (num < 0) return `环比 ↓ ${Math.abs(num)}%`;
      return "环比持平";
    })(),
    icon: ShoppingCartFull,
    visible: visibleModules.value.procurement,
  },
@@ -817,23 +757,40 @@
    icon: Box,
    visible: visibleModules.value.inventory,
  },
  {
    key: "production",
    title: "生产总览",
    desc: "累计产出(件)",
    value: formatNumber(productionOverviewData.value.totalOutput),
    subLabel: "累计报废",
    subValue: formatNumber(productionOverviewData.value.totalScrap),
    trend: `良率 ${Number(productionOverviewData.value.yieldRate || 0).toFixed(2)}%`,
    icon: Operation,
    visible: visibleModules.value.production,
  },
].filter((item) => item.visible));
const productionOrders = ref([]);
const orderFilterOptions = ["all", "waiting", "inProgress", "completed", "paused"];
const orderFilterAliasMap = {
  1: "waiting",
  2: "inProgress",
  3: "completed",
  4: "paused",
};
const orderFilter = ref("all");
const filteredOrders = computed(() => productionOrders.value);
const normalizeOrderFilter = (value, fallback = "all") => {
  const safeFallback = orderFilterOptions.includes(fallback) ? fallback : "all";
  const text = String(value ?? "").trim();
  if (orderFilterAliasMap[text]) {
    return orderFilterAliasMap[text];
  }
  return orderFilterOptions.includes(text) ? text : safeFallback;
};
const parseCount = (value) => {
  if (value === null || value === undefined || value === "") return null;
  const num = Number(value);
  return Number.isFinite(num) ? num : null;
};
const resolveProgressCount = (rawValue, currentStatus, targetStatus, total) => {
  const count = parseCount(rawValue);
  if (count !== null) return count;
  return currentStatus === targetStatus ? total : 0;
};
const getCompareTrend = (value) => {
  const num = Number(value || 0);
@@ -1038,16 +995,16 @@
  realtime: visibleModules.value.production,
  quick: quickEntries.value.length > 0,
  plan: visibleModules.value.production,
  receipt: visibleModules.value.sales || visibleModules.value.finance,
}));
const hasLeftPanels = computed(
    () => visiblePanels.value.process || visiblePanels.value.order || visiblePanels.value.contract || visiblePanels.value.quality
    () => visiblePanels.value.process || visiblePanels.value.order || visiblePanels.value.contract
);
const hasRightPanels = computed(
    () => visiblePanels.value.todo || visiblePanels.value.realtime || visiblePanels.value.quick || visiblePanels.value.plan || visiblePanels.value.receipt
    () => visiblePanels.value.todo || visiblePanels.value.realtime || visiblePanels.value.quick || visiblePanels.value.plan
);
const hasVisiblePanels = computed(() => hasLeftPanels.value || hasRightPanels.value);
const hasVisiblePanels = computed(() => hasLeftPanels.value || hasRightPanels.value || visiblePanels.value.quality);
const quickEntries = computed(() =>
    quickEntryConfigs
@@ -1198,27 +1155,36 @@
const refreshProductionOrderProgress = async () => {
  try {
    const res = await productionOrderProgress({
      status: orderFilter.value,
      tab: orderFilter.value,
      pageNum: 1,
      pageSize: 10,
    });
    const data = res?.data || {};
    const statusValue = normalizeOrderFilter(data.status, orderFilter.value);
    const total = Number(data.total || 0);
    orderProgressMeta.value = {
      status: statusValue,
      tab: data.tab || orderFilter.value,
      total: Number(data.total || 0),
      bizDate: data.bizDate || null,
      total,
      pageNum: Number(data.pageNum || 1),
      pageSize: Number(data.pageSize || 10),
      inProgressCount: Number(data.inProgressCount || 0),
      completedCount: Number(data.completedCount || 0),
      pausedCount: Number(data.pausedCount || 0),
      waitingCount: resolveProgressCount(data.waitingCount, statusValue, "waiting", total),
      inProgressCount: resolveProgressCount(data.inProgressCount, statusValue, "inProgress", total),
      completedCount: resolveProgressCount(data.completedCount, statusValue, "completed", total),
      pausedCount: resolveProgressCount(data.pausedCount, statusValue, "paused", total),
    };
    productionOrders.value = (data.records || []).map(mapOrderProgressRecord);
  } catch {
    orderProgressMeta.value = {
      status: orderFilter.value,
      tab: orderFilter.value,
      bizDate: null,
      total: 0,
      pageNum: 1,
      pageSize: 10,
      waitingCount: 0,
      inProgressCount: 0,
      completedCount: 0,
      pausedCount: 0,
@@ -1229,7 +1195,10 @@
const refreshTodayProductionPlan = async () => {
  try {
    const res = await todayProductionPlan({ limit: 4 });
    const res = await todayProductionPlan({
      limit: 4,
      planDate: nowDate.value,
    });
    const data = res?.data || {};
    todayPlanTotal.value = Number(data.total || 0);
    todayPlanList.value = (data.records || []).map(mapTodayPlanRecord);
@@ -1261,45 +1230,22 @@
  todoList.value = res.data || [];
};
const statisticsReceivable = async () => {
  const res = await statisticsReceivablePayable({ type: 1 });
  barSeries.value[0].data = [
    { value: res.data.payableMoney, itemStyle: { color: barColors2[0] } },
    { value: res.data.receivableMoney, itemStyle: { color: barColors2[1] } },
  ];
};
const qualityStatisticsInfo = async () => {
  const res = await qualityInspectionStatistics({ type: qualityRange.value });
  xAxis1.value[0].data = [];
  barSeries1.value[0].data = [];
  barSeries1.value[1].data = [];
  barSeries1.value[2].data = [];
  (res.data.item || []).forEach((item) => {
  const data = res.data || {};
  (data.item || []).forEach((item) => {
    xAxis1.value[0].data.push(item.date);
    barSeries1.value[0].data.push(item.supplierNum);
    barSeries1.value[1].data.push(item.processNum);
    barSeries1.value[2].data.push(item.factoryNum);
  });
  qualityStatisticsObject.value.supplierNum = res.data.supplierNum;
  qualityStatisticsObject.value.processNum = res.data.processNum;
  qualityStatisticsObject.value.factoryNum = res.data.factoryNum;
};
const getAmountHalfYearNum = async () => {
  const res = await getAmountHalfYear();
  const monthName = [];
  const receiptAmount = [];
  const invoiceAmount = [];
  (res.data || []).forEach((item) => {
    monthName.push(item.month);
    receiptAmount.push(item.receiptAmount);
    invoiceAmount.push(item.invoiceAmount);
  });
  xAxis2.value[0].data = monthName.map((item) => item.replace(/~/g, "\n~"));
  lineSeries.value[0].data = invoiceAmount;
  lineSeries.value[1].data = receiptAmount;
  qualityStatisticsObject.value.supplierNum = data.supplierNum || 0;
  qualityStatisticsObject.value.processNum = data.processNum || 0;
  qualityStatisticsObject.value.factoryNum = data.factoryNum || 0;
};
const getProcessList = async () => {
@@ -1373,10 +1319,6 @@
  }
  if (visiblePanels.value.quality) {
    qualityStatisticsInfo();
  }
  if (visiblePanels.value.receipt) {
    statisticsReceivable();
    getAmountHalfYearNum();
  }
  if (visiblePanels.value.process) {
    getProcessList();
@@ -1508,7 +1450,7 @@
.stats-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 14px;
}
@@ -1700,7 +1642,7 @@
  display: flex;
  flex-direction: column;
  gap: 8px;
  max-height: 230px;
  max-height: 380px;
  overflow-y: auto;
}
@@ -1749,18 +1691,31 @@
}
.main-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 400px;
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  align-items: start;
}
.left-column,
.left-column {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 16px;
}
.right-column {
  width: 400px;
  display: flex;
  flex-direction: column;
  gap: 16px;
  min-width: 0;
}
.full-width {
  width: 100%;
  flex-basis: 100%;
}
.process-panel {
@@ -2149,7 +2104,7 @@
}
.quick-panel {
  min-height: 0;
  min-height: 280px;
}
.table-progress {
@@ -2191,10 +2146,6 @@
.quality-panel {
  min-height: 0;
}
.receipt-panel {
  min-height: 340px;
}
.plan-list {
@@ -2349,24 +2300,31 @@
}
@media (max-width: 1600px) {
  .main-grid {
    grid-template-columns: minmax(0, 1fr) 380px;
  .right-column {
    width: 380px;
  }
}
@media (max-width: 1366px) {
  .main-grid {
    grid-template-columns: minmax(0, 1fr) 340px;
  .right-column {
    width: 340px;
  }
  .stats-grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}
@media (max-width: 992px) {
  .stats-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}
@media (max-width: 1200px) {
  .main-grid {
    grid-template-columns: 1fr;
  .left-column,
  .right-column {
    width: 100%;
  }
  .right-column {