gaoluyang
5 天以前 89710438fe00a9068208e806289f2ae083fc0f47
feat(app): 补齐生产准备检查、收发存汇总入口,修正质量检验工作台归组

- 生产计划列表新增「准备检查」入口:跳转准备检查页,展示物料齐套(需求/库存/在途/缺口)
与工序设备可用性,缺口按物料逐项列出。
- 新增仓储「收发存汇总」页:按仓库、统计期间查询期初/入库/出库/期末,并给出已加载行合计,
期初+入库-出库与期末不一致时标记「账实待核」。
- 工作台把「质量管理」目录归入生产管控模块,使来料/过程/出货/退货检验、待检任务、
不合格品、批次追溯等入口在 App 可见。
- 补充库区、库位页面路由映射,仓储物流下不再有两个入口缺失。

Co-Authored-By: Claude Code <noreply@anthropic.com>
已添加2个文件
已修改6个文件
367 ■■■■■ 文件已修改
src/api/production.ts 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/wls.ts 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/config/app-routes.ts 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages.json 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/mes/pro/mps/index.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/mes/pro/mps/preparation-check.vue 123 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/wls/stocksummary/index.vue 159 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/workbench/index.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/production.ts
@@ -30,6 +30,48 @@
  createTime?: string;
}
/** ç”Ÿäº§å‡†å¤‡æ£€æŸ¥ - ç‰©æ–™é½å¥—明细 */
export interface MpsPreparationMaterial {
  itemId?: number;
  itemCode?: string;
  itemName?: string;
  specification?: string;
  unitMeasureName?: string;
  requiredQuantity?: number;
  stockQuantity?: number;
  purchaseOrderQuantity?: number;
  shortageQuantity?: number;
  sufficient?: boolean;
}
/** ç”Ÿäº§å‡†å¤‡æ£€æŸ¥ - è®¾å¤‡å¯ç”¨æ€§æ˜Žç»† */
export interface MpsPreparationEquipment {
  processId?: number;
  processName?: string;
  machineryId?: number;
  machineryCode?: string;
  machineryName?: string;
  status?: number;
  statusName?: string;
  available?: boolean;
  reason?: string;
}
/** ç”Ÿäº§å‡†å¤‡æ£€æŸ¥ç»“æžœ */
export interface MpsPreparationCheck {
  mpsId?: number;
  mpsCode?: string;
  productId?: number;
  productName?: string;
  quantity?: number;
  materialReady?: boolean;
  equipmentReady?: boolean;
  ready?: boolean;
  summary?: string;
  materials?: MpsPreparationMaterial[];
  equipments?: MpsPreparationEquipment[];
}
export interface WorkOrder {
  id?: number;
  parentId?: number;
@@ -341,6 +383,9 @@
export function cancelMps(accessToken: string, id: number) {
  return request<boolean>(`/mes/pro-mps/cancel${query({ id })}`, { method: "POST", header: header(accessToken) });
}
export function getMpsPreparationCheck(accessToken: string, id: number) {
  return request<MpsPreparationCheck>(`/mes/pro-mps/preparation-check${query({ id })}`, { header: header(accessToken) });
}
export function getWorkOrderPage(accessToken: string, params: { pageNo: number; pageSize: number; code?: string; name?: string; orderSourceCode?: string; productId?: number; clientId?: number; status?: number; type?: number }) {
  return request<PageResult<WorkOrder>>(`/mes/pro/work-order/page${query(params)}`, { header: header(accessToken) });
src/api/wls.ts
@@ -125,6 +125,21 @@
  createTime?: string;
}
/** ä»“储收发存汇总行 */
export interface StockSummary {
  itemId?: number;
  itemCode?: string;
  itemName?: string;
  specification?: string;
  unitMeasureName?: string;
  warehouseId?: number;
  warehouseName?: string;
  openingQuantity?: number;
  inQuantity?: number;
  outQuantity?: number;
  closingQuantity?: number;
}
export interface ArrivalNotice {
  id?: number;
  code?: string;
@@ -570,6 +585,9 @@
export function getTransactionPage(accessToken: string, params: { pageNo: number; pageSize: number; bizCode?: string }) {
  return request<PageResult<Transaction>>(`/mes/wm/transaction/page${query(params)}`, { header: header(accessToken) });
}
export function getStockSummaryPage(accessToken: string, params: { pageNo: number; pageSize: number; itemId?: number; warehouseId?: number; startTime?: string; endTime?: string }) {
  return request<PageResult<StockSummary>>(`/mes/wm/transaction/summary${query(params)}`, { header: header(accessToken) });
}
export function getTransferPage(accessToken: string, params: { pageNo: number; pageSize: number; code?: string; name?: string; type?: number; status?: number }) { return request<PageResult<Transfer>>(`/mes/wm/transfer/page${query(params)}`, { header: header(accessToken) }); }
export function getTransfer(accessToken: string, id: number) { return request<Transfer>(`/mes/wm/transfer/get${query({ id })}`, { header: header(accessToken) }); }
export function createTransfer(accessToken: string, data: Transfer) { return request<number>("/mes/wm/transfer/create", { method: "POST", data, header: header(accessToken) }); }
src/config/app-routes.ts
@@ -103,6 +103,9 @@
  "wls/transaction/index": "/pages/wls/transaction/index",
  "wls/warehouse/index": "/pages/wls/warehouse/index",
  "wls/materialstock/index": "/pages/wls/material-stock/index",
  "wls/stocksummary/index": "/pages/wls/stocksummary/index",
  "wls/warehouse/location/index": "/pages/wls/warehouse/location",
  "wls/warehouse/area/index": "/pages/wls/warehouse/area",
  "wls/arrivalnotice/index": "/pages/wls/arrival-notice/index",
  "wls/itemreceipt/index": "/pages/wls/item-receipt/index",
  "wls/returnvendor/index": "/pages/wls/return-vendor/index",
src/pages.json
@@ -210,6 +210,12 @@
          }
        },
        {
          "path": "stocksummary/index",
          "style": {
            "navigationBarTitleText": "收发存汇总"
          }
        },
        {
          "path": "arrival-notice/index",
          "style": {
            "navigationBarTitleText": "到货通知"
@@ -1031,6 +1037,12 @@
          }
        },
        {
          "path": "mps/preparation-check",
          "style": {
            "navigationBarTitleText": "生产准备检查"
          }
        },
        {
          "path": "workorder/index",
          "style": {
            "navigationBarTitleText": "生产订单"
src/pages/mes/pro/mps/index.vue
@@ -54,6 +54,10 @@
function goForm(id?: number, mode: "edit" | "detail" = "edit") {
  uni.navigateTo({ url: `/pages/mes/pro/mps/form${id ? `?id=${id}&mode=${mode}` : ""}` });
}
function goPreparationCheck(id?: number) {
  if (!id) return;
  uni.navigateTo({ url: `/pages/mes/pro/mps/preparation-check?id=${id}` });
}
async function confirmAction(title: string, content: string) {
  return new Promise<boolean>(resolve => uni.showModal({ title, content, success: res => resolve(res.confirm) }));
}
@@ -130,6 +134,7 @@
            <view class="actions" @click.stop>
              <button @click="goForm(row.id,'detail')">详情</button>
              <button v-if="canUpdate && row.status === 0" @click="goForm(row.id)">编辑</button>
              <button @click="goPreparationCheck(row.id)">准备检查</button>
              <button v-if="canIssue && row.status === 0" @click="issue(row)">下发</button>
              <button v-if="canCancel && row.status === 1" class="danger" @click="cancel(row)">取消</button>
              <button v-if="canDelete && row.status === 0" class="danger" @click="remove(row)">删除</button>
src/pages/mes/pro/mps/preparation-check.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,123 @@
<script setup lang="ts">
import { computed, ref } from "vue";
import { onLoad, onPullDownRefresh } from "@dcloudio/uni-app";
import { getMpsPreparationCheck, type MpsPreparationCheck } from "../../../../api/production";
import { useAuth } from "../../../../stores/auth";
const { isLoggedIn, state } = useAuth();
const mpsId = ref<number>();
const result = ref<MpsPreparationCheck>();
const loading = ref(false);
const loadError = ref("");
const canQuery = computed(() => !!state.accessToken);
const shortageRows = computed(() => (result.value?.materials || []).filter(item => !item.sufficient));
const readyMaterials = computed(() => (result.value?.materials || []).filter(item => item.sufficient));
const unavailableEquipments = computed(() => (result.value?.equipments || []).filter(item => !item.available));
/** åŽç«¯å¯èƒ½æ²¡è¿”回设备明细(产品未绑定工艺路线),此时给用户一个可执行的提示 */
const equipmentHint = computed(() => {
  if ((result.value?.equipments || []).length) return "";
  return "该产品尚未维护工艺路线或工序设备,无法自动判断设备可用性,请到「工艺路线」维护后再检查";
});
function formatNumber(value?: number) {
  const number = Number(value || 0);
  return Number.isInteger(number) ? String(number) : number.toFixed(2);
}
async function load() {
  if (!state.accessToken || !mpsId.value || loading.value) return;
  loading.value = true;
  loadError.value = "";
  try {
    result.value = await getMpsPreparationCheck(state.accessToken, mpsId.value);
  } catch (error) {
    loadError.value = error instanceof Error ? error.message : "生产准备检查失败";
  } finally {
    loading.value = false;
  }
}
function goBack() {
  uni.navigateBack();
}
onLoad(options => {
  const id = Number(options?.id);
  mpsId.value = Number.isFinite(id) && id > 0 ? id : undefined;
  if (!mpsId.value) {
    loadError.value = "缺少生产计划编号,请从生产计划列表重新进入";
    return;
  }
  load();
});
onPullDownRefresh(async () => {
  await load();
  uni.stopPullDownRefresh();
});
</script>
<template>
  <view class="page">
    <template v-if="isLoggedIn && canQuery">
      <view v-if="loadError" class="state error" @click="load">{{ loadError }},点击重试</view>
      <scroll-view v-else-if="result" class="content" scroll-y>
        <view :class="['banner', result.ready ? 'ok' : 'warn']">
          <text class="banner-title">{{ result.ready ? "准备工作已就绪" : "暂不满足开工条件" }}</text>
          <text class="banner-sub">{{ result.summary || "系统未返回检查结论" }}</text>
          <view class="banner-tags">
            <text :class="['tag', result.materialReady ? 'tag-success' : 'tag-danger']">{{ result.materialReady ? "物料齐套" : "物料短缺" }}</text>
            <text :class="['tag', result.equipmentReady ? 'tag-success' : 'tag-danger']">{{ result.equipmentReady ? "设备可用" : "设备不可用" }}</text>
          </view>
        </view>
        <view class="card">
          <view class="card-title">生产计划</view>
          <view class="grid">
            <text>计划单号:{{ result.mpsCode || "--" }}</text>
            <text>产品:{{ result.productName || "--" }}</text>
            <text>计划数量:{{ formatNumber(result.quantity) }}</text>
          </view>
        </view>
        <view v-if="shortageRows.length" class="card">
          <view class="card-title danger">物料缺口 {{ shortageRows.length }} é¡¹</view>
          <view v-for="item in shortageRows" :key="item.itemId" class="item">
            <view class="item-head"><text class="name">{{ item.itemName || "未命名物料" }}</text><text class="tag tag-danger">缺 {{ formatNumber(item.shortageQuantity) }}{{ item.unitMeasureName || "" }}</text></view>
            <view class="grid">
              <text>物料编码:{{ item.itemCode || "--" }}</text>
              <text>规格型号:{{ item.specification || "--" }}</text>
              <text>需求数量:{{ formatNumber(item.requiredQuantity) }}</text>
              <text>现有库存:{{ formatNumber(item.stockQuantity) }}</text>
              <text>在途采购:{{ formatNumber(item.purchaseOrderQuantity) }}</text>
              <text>缺口数量:{{ formatNumber(item.shortageQuantity) }}</text>
            </view>
          </view>
        </view>
        <view v-if="readyMaterials.length" class="card">
          <view class="card-title">物料齐套 {{ readyMaterials.length }} é¡¹</view>
          <view v-for="item in readyMaterials" :key="item.itemId" class="item">
            <view class="item-head"><text class="name">{{ item.itemName || "未命名物料" }}</text><text class="tag tag-success">充足</text></view>
            <view class="grid">
              <text>物料编码:{{ item.itemCode || "--" }}</text>
              <text>需求数量:{{ formatNumber(item.requiredQuantity) }}</text>
              <text>现有库存:{{ formatNumber(item.stockQuantity) }}</text>
              <text>在途采购:{{ formatNumber(item.purchaseOrderQuantity) }}</text>
            </view>
          </view>
        </view>
        <view class="card">
          <view class="card-title">设备可用性</view>
          <view v-if="equipmentHint" class="empty">{{ equipmentHint }}</view>
          <view v-for="item in result.equipments || []" :key="`${item.processId}-${item.machineryId}`" class="item">
            <view class="item-head"><text class="name">{{ item.processName || "未命名工序" }}</text><text :class="['tag', item.available ? 'tag-success' : 'tag-danger']">{{ item.available ? "可用" : "不可用" }}</text></view>
            <view class="grid">
              <text>设备编码:{{ item.machineryCode || "--" }}</text>
              <text>设备名称:{{ item.machineryName || "--" }}</text>
              <text>设备状态:{{ item.statusName || "--" }}</text>
              <text v-if="item.reason">不可用原因:{{ item.reason }}</text>
            </view>
          </view>
        </view>
      </scroll-view>
      <view v-else class="state">{{ loading ? "检查中..." : "暂无检查结果" }}</view>
    </template>
    <view v-else class="state">登录后查看生产准备检查</view>
    <button class="back" @click="goBack">返回生产计划</button>
  </view>
</template>
<style lang="scss" scoped>
.page{height:100vh;display:flex;flex-direction:column;padding:0 24rpx;box-sizing:border-box;overflow:hidden;background:#f3f6fb}.content{height:0;flex:1;padding:20rpx 0 140rpx;box-sizing:border-box}.banner{display:flex;flex-direction:column;gap:10rpx;padding:26rpx 24rpx;margin-bottom:16rpx;background:#fff;border-radius:16rpx;box-shadow:0 10rpx 24rpx rgba(43,69,107,.08);border-left:8rpx solid #17865a}.banner.warn{border-left-color:#c65050}.banner-title{color:#20334c;font-size:32rpx;font-weight:600}.banner-sub{color:#66778c;font-size:24rpx;line-height:1.6}.banner-tags{display:flex;gap:12rpx;margin-top:6rpx}.card{padding:22rpx;margin-bottom:16rpx;background:#fff;border-radius:16rpx;box-shadow:0 10rpx 24rpx rgba(43,69,107,.08)}.card-title{margin-bottom:12rpx;color:#20334c;font-size:28rpx;font-weight:600}.card-title.danger{color:#c65050}.item{padding:16rpx 0;border-top:1rpx solid #edf1f6}.item-head{display:flex;align-items:center;justify-content:space-between;gap:12rpx}.name{overflow:hidden;color:#31445c;font-size:27rpx;font-weight:600;text-overflow:ellipsis;white-space:nowrap}.tag{flex:none;display:inline-flex;align-items:center;height:40rpx;padding:0 14rpx;font-size:22rpx;border-radius:999rpx;white-space:nowrap}.tag-success{color:#17865a;background:#eafaf2;border:1rpx solid #b8efd5}.tag-danger{color:#c65050;background:#fff7f7;border:1rpx solid #e3a0a0}.grid{display:grid;grid-template-columns:1fr 1fr;gap:8rpx;margin-top:10rpx;color:#66778c;font-size:23rpx}.grid text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.empty{padding:16rpx 0;color:#8795a8;font-size:24rpx;line-height:1.6}.more,.state{display:block;padding:35rpx;color:#8795a8;font-size:26rpx;text-align:center}.error{color:#c3654d}.back{position:fixed;left:24rpx;right:24rpx;bottom:calc(24rpx + env(safe-area-inset-bottom));height:84rpx;line-height:84rpx;color:var(--color-primary, #1769d8);font-size:28rpx;background:#fff;border:1rpx solid var(--color-primary-border, #8ab3ee);border-radius:14rpx}.back::after{border:0}
</style>
src/pages/wls/stocksummary/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,159 @@
<script setup lang="ts">
import { computed, ref } from "vue";
import { onPullDownRefresh, onShow } from "@dcloudio/uni-app";
import { getStockSummaryPage, getWarehouseSimpleList, type StockSummary, type Warehouse } from "../../../api/wls";
import { useAuth } from "../../../stores/auth";
import { hasMenuComponent } from "../components/shared";
const { hasPermission, isLoggedIn, state } = useAuth();
const rows = ref<StockSummary[]>([]);
const warehouses = ref<Warehouse[]>([]);
const warehouseId = ref<number | undefined>(undefined);
const startDate = ref("");
const endDate = ref("");
const pageNo = ref(1);
const total = ref(0);
const loading = ref(false);
const loadError = ref("");
const canQuery = computed(() => hasPermission("mes:wm-transaction:query") || hasMenuComponent(state.menus, "wls/stocksummary/index"));
const hasMore = computed(() => rows.value.length < total.value);
const warehouseOptions = computed(() => ["全部仓库", ...warehouses.value.map(item => `${item.code || "--"} - ${item.name || "--"}`)]);
const warehouseLabel = computed(() => {
  const index = warehouses.value.findIndex(item => item.id === warehouseId.value);
  return index >= 0 ? warehouseOptions.value[index + 1] : "全部仓库";
});
/** å½“前已加载行的合计,方便仓管在手机上快速看收发存总量 */
const totals = computed(() => rows.value.reduce(
  (sum, row) => ({
    opening: sum.opening + Number(row.openingQuantity || 0),
    inbound: sum.inbound + Number(row.inQuantity || 0),
    outbound: sum.outbound + Number(row.outQuantity || 0),
    closing: sum.closing + Number(row.closingQuantity || 0),
  }),
  { opening: 0, inbound: 0, outbound: 0, closing: 0 }
));
function formatNumber(value?: number) {
  const number = Number(value || 0);
  return Number.isInteger(number) ? String(number) : number.toFixed(2);
}
/** æœŸåˆ + å…¥åº“ - å‡ºåº“ ä¸ŽæœŸæœ«ä¸ä¸€è‡´æ—¶ç»™å‡ºæç¤ºï¼Œé¿å…æŠŠé”™è¯¯çš„账读成准的 */
function mismatch(row: StockSummary) {
  const expected = Number(row.openingQuantity || 0) + Number(row.inQuantity || 0) - Number(row.outQuantity || 0);
  return Math.abs(expected - Number(row.closingQuantity || 0)) > 0.0001;
}
async function loadWarehouses() {
  if (!state.accessToken || warehouses.value.length) return;
  warehouses.value = await getWarehouseSimpleList(state.accessToken);
}
async function load(reset = false) {
  if (!state.accessToken || loading.value || !canQuery.value) return;
  if (reset) { pageNo.value = 1; rows.value = []; }
  loading.value = true;
  loadError.value = "";
  try {
    await loadWarehouses();
    const data = await getStockSummaryPage(state.accessToken, {
      pageNo: pageNo.value,
      pageSize: 15,
      warehouseId: warehouseId.value,
      startTime: startDate.value ? `${startDate.value} 00:00:00` : undefined,
      endTime: endDate.value ? `${endDate.value} 23:59:59` : undefined,
    });
    total.value = data.total;
    rows.value = reset ? data.list : [...rows.value, ...data.list];
    pageNo.value += 1;
  } catch (error) {
    loadError.value = error instanceof Error ? error.message : "收发存汇总加载失败";
  } finally {
    loading.value = false;
  }
}
function pickWarehouse(event: { detail: { value: string } }) {
  const index = Number(event.detail.value);
  warehouseId.value = index === 0 ? undefined : warehouses.value[index - 1]?.id;
  load(true);
}
function pickStart(event: { detail: { value: string } }) {
  startDate.value = event.detail.value;
  if (endDate.value && startDate.value > endDate.value) {
    uni.showToast({ title: `开始日期 ${startDate.value} æ™šäºŽç»“束日期 ${endDate.value},请重新选择`, icon: "none" });
    return;
  }
  load(true);
}
function pickEnd(event: { detail: { value: string } }) {
  endDate.value = event.detail.value;
  if (startDate.value && endDate.value < startDate.value) {
    uni.showToast({ title: `结束日期 ${endDate.value} æ—©äºŽå¼€å§‹æ—¥æœŸ ${startDate.value},请重新选择`, icon: "none" });
    return;
  }
  load(true);
}
function resetFilter() {
  warehouseId.value = undefined;
  startDate.value = "";
  endDate.value = "";
  load(true);
}
onShow(() => load(true));
onPullDownRefresh(async () => {
  await load(true);
  uni.stopPullDownRefresh();
});
</script>
<template>
  <view class="page">
    <template v-if="isLoggedIn">
      <view v-if="canQuery" class="filters">
        <picker :range="warehouseOptions" @change="pickWarehouse">
          <view class="picker"><text>{{ warehouseLabel }}</text><text>›</text></view>
        </picker>
        <view class="dates">
          <picker mode="date" :value="startDate" @change="pickStart">
            <view class="picker"><text>{{ startDate || "开始日期" }}</text><text>›</text></view>
          </picker>
          <text class="dash">至</text>
          <picker mode="date" :value="endDate" @change="pickEnd">
            <view class="picker"><text>{{ endDate || "结束日期" }}</text><text>›</text></view>
          </picker>
        </view>
        <view class="filter-actions">
          <text>默认统计近三个月;已加载 {{ rows.length }} / {{ total }} è¡Œ</text>
          <text class="reset" @click="resetFilter">重置</text>
        </view>
      </view>
      <view v-if="canQuery && rows.length" class="totals">
        <view><text>期初</text><text>{{ formatNumber(totals.opening) }}</text></view>
        <view><text>入库</text><text class="pos">{{ formatNumber(totals.inbound) }}</text></view>
        <view><text>出库</text><text class="neg">{{ formatNumber(totals.outbound) }}</text></view>
        <view><text>期末</text><text>{{ formatNumber(totals.closing) }}</text></view>
      </view>
      <scroll-view v-if="canQuery" class="content" scroll-y lower-threshold="80" @scrolltolower="hasMore && load()">
        <view v-if="loadError" class="state error" @click="load(true)">{{ loadError }},点击重试</view>
        <view v-else-if="rows.length">
          <view v-for="row in rows" :key="`${row.itemId}-${row.warehouseId}`" class="row">
            <view class="head">
              <view><text class="name">{{ row.itemName || "未命名物料" }}</text><text>{{ row.itemCode || "暂无编码" }} / {{ row.warehouseName || "未指定仓库" }}</text></view>
              <text :class="['status', mismatch(row) ? 'tag-danger' : 'tag-success']">{{ mismatch(row) ? "账实待核" : "账实相符" }}</text>
            </view>
            <view class="grid">
              <text>规格型号:{{ row.specification || "--" }}</text>
              <text>单位:{{ row.unitMeasureName || "--" }}</text>
              <text>期初数量:{{ formatNumber(row.openingQuantity) }}</text>
              <text>本期入库:{{ formatNumber(row.inQuantity) }}</text>
              <text>本期出库:{{ formatNumber(row.outQuantity) }}</text>
              <text>期末数量:{{ formatNumber(row.closingQuantity) }}</text>
            </view>
          </view>
          <text class="more">{{ loading ? "加载中..." : hasMore ? "上拉加载更多" : "没有更多了" }}</text>
        </view>
        <view v-else class="state">{{ loading ? "加载中..." : "所选条件下暂无收发存数据" }}</view>
      </scroll-view>
      <view v-else class="state">暂无收发存汇总查看权限</view>
    </template>
    <view v-else class="state">登录后查看收发存汇总</view>
  </view>
</template>
<style lang="scss" scoped>
.page{height:100vh;display:flex;flex-direction:column;padding:0 24rpx;box-sizing:border-box;overflow:hidden;background:#f3f6fb}.filters{flex:none;padding:20rpx;margin:20rpx 0 14rpx;background:#fff;border-radius:16rpx;box-shadow:0 8rpx 20rpx rgba(43,69,107,.06)}.picker{display:flex;align-items:center;justify-content:space-between;gap:12rpx;min-height:66rpx;padding:0 18rpx;color:#31445c;font-size:25rpx;background:#f6f8fb;border-radius:12rpx}.picker text:first-child{min-width:0;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.picker text:last-child{flex:none;color:#8092a7;font-size:32rpx}.dates{display:flex;align-items:center;gap:12rpx;margin-top:12rpx}.dates picker{flex:1;min-width:0}.dash{flex:none;color:#8795a8;font-size:24rpx}.filter-actions{display:flex;align-items:center;justify-content:space-between;margin-top:12rpx;color:#8795a8;font-size:22rpx}.filter-actions .reset{color:var(--color-primary, #1769d8);font-size:24rpx}.totals{flex:none;display:flex;margin-bottom:14rpx;background:#fff;border-radius:16rpx;box-shadow:0 8rpx 20rpx rgba(43,69,107,.06)}.totals view{flex:1;display:flex;flex-direction:column;align-items:center;gap:6rpx;padding:16rpx 0;border-right:1rpx solid #f1f4f8}.totals view:last-child{border-right:0}.totals text:first-child{color:#8795a8;font-size:22rpx}.totals text:last-child{color:#20334c;font-size:27rpx;font-weight:600}.totals .pos{color:#17865a}.totals .neg{color:#c65050}.content{height:0;flex:1;padding-bottom:80rpx;box-sizing:border-box}.row{padding:24rpx 22rpx 20rpx;margin-bottom:18rpx;background:#fff;border-radius:16rpx;box-shadow:0 10rpx 24rpx rgba(43,69,107,.08)}.head{display:flex;align-items:flex-start;justify-content:space-between;gap:16rpx}.head>view:first-child{display:flex;min-width:0;flex:1;flex-direction:column;gap:8rpx}.head>view:first-child text:last-child{color:#78879b;font-size:23rpx}.name{overflow:hidden;color:#20334c;font-size:30rpx;font-weight:600;text-overflow:ellipsis;white-space:nowrap}.status{display:inline-flex;align-items:center;flex:none;height:44rpx;padding:0 18rpx;font-size:23rpx;border-radius:999rpx}.tag-success{color:#17865a;background:#eafaf2;border:1rpx solid #b8efd5}.tag-danger{color:#c65050;background:#fff7f7;border:1rpx solid #e3a0a0}.grid{display:grid;grid-template-columns:1fr 1fr;gap:10rpx;padding-top:16rpx;margin-top:16rpx;color:#66778c;font-size:23rpx;border-top:1rpx solid #edf1f6}.grid text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.more,.state{display:block;padding:35rpx;color:#8795a8;font-size:26rpx;text-align:center}.error{color:#c3654d}
</style>
src/pages/workbench/index.vue
@@ -159,6 +159,8 @@
   */
  function resolveModuleGroup(app: WorkbenchApp): string {
    if (MODULE_GROUPS.includes(app.group)) return app.group;
    // è´¨é‡ç®¡ç†ï¼ˆæ¥æ–™/过程/出货/退货检验、不合格品、批次追溯)属于生产管控,方案里不单列模块
    if (app.group === "质量管理") return "生产管控";
    if (app.group === "基础数据") {
      return /设备类型|mes\/dv\//.test(`${app.name} ${app.component}`)
        ? "设备管理"