spring
2026-10-29 eacbd6d2f0a4fb7820fcf856f9cc025bea1ea39a
fix: 原型编制
已修改5个文件
212 ■■■■■ 文件已修改
.env.production 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/timeSensitiveTask.ts 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/manifest.json 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/index/index.vue 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/timely/detail.vue 144 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.env.production
@@ -4,7 +4,7 @@
VITE_APP_BASE_API = '/prod-api'
# VITE_APP_BASE_API = 'http://114.132.189.42:7002/mes'
# API 服务器的 URL
VITE_APP_API_URL = 'http://114.132.189.42:7002/mes'
# VITE_APP_API_URL = 'http://114.132.189.42:7002/mes'
# 山东
# VITE_APP_API_URL = 'http://192.168.100.131:7002/mes'
VITE_APP_API_URL = 'http://192.168.100.131:7002/mes'
src/api/timeSensitiveTask.ts
@@ -498,6 +498,36 @@
    setTimeout(() => resolve({ code: 200, msg, data }), 120);
  });
/* ==================== 领用统计 ==================== */
/** 领用统计行:按产品大类 + 规格型号汇总 */
export interface RequisitionStatVO {
  /** 产品大类 */
  categoryName: string;
  /** 规格型号 */
  model: string;
  /** 数量(件) */
  count: number;
}
/** 从响应里取出统计数组(兼容 data 直接是数组 / 分页对象) */
const extractStatRows = (data: any): any[] => {
  if (Array.isArray(data)) return data;
  return data?.records ?? data?.list ?? data?.rows ?? [];
};
/**
 * 归一化统计行。
 * 后端约定:产品大类 categoryName、规格型号 model、数量 count
 * (其余键名为兼容兜底,确认后端字段后可以删掉)
 */
const normalizeStatRows = (rows: any[]): RequisitionStatVO[] =>
  (rows || []).map((row) => ({
    categoryName: String(row?.categoryName ?? row?.productCategory ?? ""),
    model: String(row?.model ?? row?.specificationModel ?? ""),
    count: Number(row?.count ?? row?.itemCount ?? 0),
  }));
const mockMaterialCode = (item: any) =>
  String(item?.flowCode || item?.batchCode || item?.monofilamentNumber || "").trim();
@@ -590,6 +620,22 @@
    });
  },
  /**
   * 领用统计:按产品大类 + 规格型号汇总当前已领用数量
   * POST /timeSensitiveTask/requisitionStats  { taskId, materialType }
   */
  async getRequisitionStats(taskId: number | string, materialType?: string) {
    const res = await request<BaseResult<any>>({
      url: "/timeSensitiveTask/requisitionStats",
      method: "POST",
      data: { taskId, materialType } as any,
    });
    return {
      ...res,
      data: normalizeStatRows(extractStatRows(res?.data)),
    } as BaseResult<RequisitionStatVO[]>;
  },
  create(data: TimeSensitiveTaskForm) {
    if (USE_TIME_SENSITIVE_MOCK) {
      const id = Date.now();
src/manifest.json
@@ -2,7 +2,7 @@
    "name" : "线缆上报",
    "appid" : "__UNI__F64E0A4",
    "description" : "",
    "versionName" : "1.0.37",
    "versionName" : "1.0.38",
    "versionCode" : "100",
    "transformPx" : false,
    /* 5+App特有相关 */
src/pages/index/index.vue
@@ -230,12 +230,12 @@
    url: "/pages/outbound/index",
    show: false,
  },
  {
    icon: "/static/icons/inventoryOperation.png",
    title: "库存操作",
    url: "/pages/production/inventory/index",
    show: false,
  },
  //{
  //  icon: "/static/icons/inventoryOperation.png",
  //  title: "库存操作",
  //  url: "/pages/production/inventory/index",
  //  show: false,
  //},
  {
    icon: "/static/icons/timely.png",
    title: "时效",
@@ -308,9 +308,7 @@
  const productionNav = navList.find((item) => item.url === "/pages/production/index");
  const timelyNav = navList.find((item) => item.url === "/pages/timely/index");
  // 库存操作(成品拆盘 / 绞线复绕):权限与「生产管理」一致
  const inventoryNav = navList.find(
    (item) => item.url === "/pages/production/inventory/index"
  );
  const inventoryNav = navList.find((item) => item.url === "/pages/production/inventory/index");
  if (productionNav) productionNav.show = canAccessProduction;
  if (timelyNav) timelyNav.show = canAccessTimely;
  if (inventoryNav) {
src/pages/timely/detail.vue
@@ -39,7 +39,11 @@
          </view>
          <view>
            <text class="label">已领用</text>
            {{ items.length }} 件
            <!-- 件数可点击:展开领用统计(产品大类 / 规格型号 / 数量) -->
            <view class="count-link" @click="openRequisitionStats">
              <text class="count-link-text">{{ items.length }} 件</text>
              <text class="count-link-arrow">▾</text>
            </view>
          </view>
          <view>
            <text class="label">温度</text>
@@ -169,6 +173,52 @@
        </wd-card>
      </view>
    </scroll-view>
    <wd-popup
      v-model="statsPopupVisible"
      position="bottom"
      :z-index="31"
      :close-on-click-modal="true"
      custom-style="max-height: 60vh; border-radius: 24rpx 24rpx 0 0;"
    >
      <view class="popup-layout">
        <view class="popup-header">
          <view>
            <text class="popup-title">领用统计</text>
            <text class="popup-subtitle">产品大类 / 规格型号 / 数量</text>
          </view>
          <wd-icon name="close" size="20px" @click="statsPopupVisible = false" />
        </view>
        <scroll-view class="stats-list" scroll-y>
          <view v-if="statsLoading" class="state-box">
            <wd-loading />
            <text>统计中...</text>
          </view>
          <wd-status-tip v-else-if="!statsRows.length" image="content" tip="暂无领用数据" />
          <block v-else>
            <view
              v-for="(row, index) in statsRows"
              :key="`${row.categoryName}-${row.model}-${index}`"
              class="stats-item"
            >
              <view class="stats-item-main">
                <text class="stats-category">{{ row.categoryName || "-" }}</text>
                <text class="stats-model">{{ row.model || "-" }}</text>
              </view>
              <text class="stats-count">{{ row.count }} 件</text>
            </view>
          </block>
        </scroll-view>
        <view class="popup-footer">
          <text>合计 {{ statsTotal }} 件</text>
          <wd-button custom-class="footer-action-button" @click="statsPopupVisible = false">
            关闭
          </wd-button>
        </view>
      </view>
    </wd-popup>
    <wd-popup
      v-model="rackSelectorVisible"
@@ -365,7 +415,10 @@
import type { TagType } from "wot-design-uni/components/wd-tag/types";
import CardTitle from "@/components/card-title/index.vue";
import Scan from "@/components/scan/index.vue";
import TimeSensitiveTaskApi, { type TimelinessMaterialType } from "@/api/timeSensitiveTask";
import TimeSensitiveTaskApi, {
  type RequisitionStatVO,
  type TimelinessMaterialType,
} from "@/api/timeSensitiveTask";
const toast = useToast();
const message = useMessage();
@@ -390,6 +443,33 @@
const selectedRackNos = ref<string[]>([]);
const rackSelectorVisible = ref(false);
const rackSelectionRows = ref<Array<{ label: string; value: string; selected: boolean }>>([]);
/* ==================== 领用统计 ==================== */
const statsPopupVisible = ref(false);
const statsLoading = ref(false);
const statsRows = ref<RequisitionStatVO[]>([]);
const statsTotal = computed(() =>
  statsRows.value.reduce((sum, item) => sum + Number(item.count || 0), 0)
);
/** 点击「已领用 N 件」:请求领用统计并弹出 */
const openRequisitionStats = async () => {
  statsRows.value = [];
  statsPopupVisible.value = true;
  statsLoading.value = true;
  try {
    const { code, data } = await TimeSensitiveTaskApi.getRequisitionStats(
      taskId.value,
      materialType.value
    );
    statsRows.value = code === 200 ? data || [] : [];
  } catch (error) {
    statsRows.value = [];
    toast.error("领用统计查询失败");
  } finally {
    statsLoading.value = false;
  }
};
const activeRackNo = ref("");
const candidateRackNo = ref("");
const pendingScanEvent = ref<any>(null);
@@ -1157,6 +1237,66 @@
  color: #7e8987;
}
/* 已领用件数:可点击,展开领用统计 */
.count-link {
  display: flex;
  gap: 2px;
  align-items: center;
}
.count-link-text {
  color: #0d867f;
  border-bottom: 1px solid rgba(13, 134, 127, 0.4);
}
.count-link-arrow {
  font-size: 12px;
  color: #0d867f;
}
/* 领用统计弹框 */
.stats-list {
  max-height: 40vh;
  padding: 4px 0;
}
.stats-item {
  display: flex;
  gap: 12px;
  align-items: center;
  justify-content: space-between;
  padding: 14px 16px;
  border-bottom: 1px solid #f2f4f3;
}
.stats-item-main {
  display: flex;
  flex: 1;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
}
.stats-category {
  font-size: 15px;
  font-weight: 500;
  color: #252525;
  overflow-wrap: anywhere;
}
.stats-model {
  font-size: 13px;
  color: #7e8987;
  overflow-wrap: anywhere;
}
.stats-count {
  flex-shrink: 0;
  font-size: 16px;
  font-weight: 600;
  color: #0d867f;
}
.scan-tip {
  display: flex;
  gap: 8px;