张诺
10 小时以前 bbaf8175e73c8579ef14a6b4a678bcb98177834b
src/pages/productionManagement/productionReport/index.vue
@@ -30,16 +30,16 @@
      lower-threshold="80"
      @scrolltolower="loadMore"
    >
      <view v-for="(item, index) in tableData" :key="item.id || index" class="ledger-item">
      <view v-for="(item, index) in tableData" :key="item.id || index" class="ledger-item" @click="showTransferCard(item)">
        <view class="item-header">
          <view class="item-left">
            <view class="document-icon">
              <up-icon name="file-text" size="16" color="#ffffff"></up-icon>
            </view>
            <text class="item-id">{{ item.workOrderNo }}</text>
            <text class="item-id">{{ item.workOrderNo || '无' }}</text>
          </view>
          <view class="item-right">
            <text class="item-tag tag-type">{{ item.workOrderType }}</text>
            <text class="item-tag tag-type">{{ item.workOrderType || '无' }}</text>
          </view>
        </view>
        
@@ -48,23 +48,19 @@
        <view class="item-details">
          <view class="detail-row">
            <text class="detail-label">产品名称</text>
            <text class="detail-value">{{ item.productName }}</text>
            <text class="detail-value">{{ item.productName || '无' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">规格型号</text>
            <text class="detail-value">{{ item.model }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">机台名称</text>
            <text class="detail-value">{{ item.deviceName }}</text>
            <text class="detail-value">{{ item.model || '无' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">工序名称 / 计量单位</text>
            <text class="detail-value">{{ item.processName }} / {{ item.unit }}</text>
            <text class="detail-value">{{ item.processName || '无' }} / {{ item.unit || '无' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">需求/完成数量</text>
            <text class="detail-value">{{ item.planQuantity }} / {{ item.completeQuantity }}</text>
            <text class="detail-value">{{ item.planQuantity || '无' }} / {{ item.completeQuantity || '无' }}</text>
          </view>
          
          <view class="progress-section">
@@ -77,30 +73,15 @@
              ></up-line-progress>
            </view>
          </view>
          <view class="detail-row">
            <text class="detail-label">实际开始</text>
            <text class="detail-value">{{ item.startProductTime || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">实际结束</text>
            <text class="detail-value">{{ item.endProductTime || '-' }}</text>
          </view>
        </view>
        <view class="item-actions" v-if="!item.endProductTime">
        <view class="item-actions" v-if="!isEnded(item)">
          <up-button
            text="开始报工"
            size="mini"
            type="primary"
            :disabled="!canStartProduction(item) || startSubmittingId === item.id"
            @click="handleStartProduction(item)"
          />
          <up-button
            text="结束报工"
            text="报工"
            size="mini"
            type="success"
            :disabled="!canEndProduction(item)"
            @click="openEndReport(item)"
            @click.stop="openEndReport(item)"
          />
        </view>
      </view>
@@ -114,20 +95,69 @@
    <!-- 流转卡弹窗 -->
    <up-popup :show="transferCardVisible" mode="center" @close="transferCardVisible = false" round="10">
      <view class="qr-popup">
        <text class="qr-title">工单流转卡二维码</text>
        <view class="qr-box">
          <geek-qrcode
            v-if="transferCardRowData"
            :val="String(transferCardRowData.id)"
            :size="200"
          />
        <text class="qr-info" v-if="transferCardRowData">{{ transferCardRowData.workOrderNo || '-' }}</text>
        <view class="transfer-records">
          <view class="transfer-records-header">
            <text class="transfer-records-title">报工审核记录</text>
            <text class="transfer-records-count">共 {{ transferCardRecords.length }} 条</text>
          </view>
          <view v-if="transferCardLoading" class="transfer-records-loading">加载中...</view>
          <view v-else-if="transferCardRecords.length > 0" class="transfer-records-list">
            <view
              v-for="(record, index) in transferCardRecords"
              :key="record.id || index"
              class="transfer-record-item"
            >
              <view class="transfer-record-top">
                <view class="transfer-record-top-main">
                  <text class="transfer-record-no">{{ record.productNo || `报工记录${index + 1}` }}</text>
                  <text class="transfer-record-sub">
                    {{ record.process || '-' }} / {{ record.deviceName || '-' }}
                  </text>
                </view>
                <text class="audit-status-tag" :class="`audit-status-${getAuditStatusType(record.auditStatus)}`">
                  {{ getAuditStatusLabel(record.auditStatus) }}
                </text>
              </view>
              <view class="transfer-record-grid">
                <view
                  v-for="field in transferRecordFields"
                  :key="field.prop"
                  class="transfer-record-cell"
                  :class="{ 'is-wide': field.wide }"
                >
                  <text class="transfer-record-label">{{ field.label }}</text>
                  <view v-if="field.prop === 'teamNames'" class="transfer-record-content">
                    <view v-if="getTeamNameList(record.teamNames).length > 0" class="transfer-tag-list">
                      <text
                        v-for="name in getTeamNameList(record.teamNames)"
                        :key="name"
                        class="transfer-name-tag"
                      >
                        {{ name }}
                      </text>
                    </view>
                    <text v-else class="transfer-record-value">-</text>
                  </view>
                  <view v-else-if="field.prop === 'auditStatus'" class="transfer-record-content">
                    <text class="audit-status-tag" :class="`audit-status-${getAuditStatusType(record.auditStatus)}`">
                      {{ getAuditStatusLabel(record.auditStatus) }}
                    </text>
                  </view>
                  <text v-else class="transfer-record-value">
                    {{ formatTransferRecordValue(record, field.prop) }}
                  </text>
                </view>
              </view>
            </view>
          </view>
          <view v-else class="transfer-records-empty">暂无报工审核记录</view>
        </view>
        <text class="qr-info" v-if="transferCardRowData">{{ transferCardRowData.workOrderNo }}</text>
        <up-button text="关闭" @click="transferCardVisible = false" style="margin-top: 20px;"></up-button>
      </view>
    </up-popup>
    <!-- 结束报工弹窗 -->
    <!-- 报工弹窗 -->
    <up-popup
      v-model:show="endReportVisible"
      mode="bottom"
@@ -138,7 +168,7 @@
      <view class="report-modal">
        <view class="modal-header">
          <view class="modal-header-left">
            <text class="modal-title">结束报工</text>
            <text class="modal-title">报工</text>
            <text class="modal-subtitle" v-if="endReportRow">
              {{ endReportRow.workOrderNo || '-' }} · {{ endReportRow.deviceName || '-' }}
            </text>
@@ -149,7 +179,7 @@
        </view>
        <scroll-view class="modal-content" scroll-y>
          <view class="report-summary" v-if="endReportRow">
          <!-- <view class="report-summary" v-if="endReportRow">
            <view class="summary-left">
              <text class="summary-title">{{ endReportRow.productName || '-' }}</text>
              <text class="summary-sub">{{ endReportRow.processName || '-' }} · {{ endReportRow.model || '-' }}</text>
@@ -158,7 +188,7 @@
              <text class="summary-num">{{ endReportForm.planQuantity || '0' }}</text>
              <text class="summary-label">待生产</text>
            </view>
          </view>
          </view> -->
          <up-form :model="endReportForm" labelWidth="120">
            <view class="form-section">
@@ -167,7 +197,7 @@
                <up-input v-model="endReportForm.planQuantity" disabled />
              </up-form-item>
              <up-form-item label="本次生产数量" required>
                <up-input v-model="endReportForm.quantity" disabled />
                <up-input v-model="endReportForm.quantity" />
              </up-form-item>
              <up-form-item label="补产数量">
                <up-input v-model="endReportForm.replenishQty" type="number" placeholder="请输入" />
@@ -175,10 +205,35 @@
              <up-form-item label="报废数量">
                <up-input v-model="endReportForm.scrapQty" type="number" placeholder="请输入" />
              </up-form-item>
              <up-form-item label="开始时间" required>
  <up-input
    v-model="endReportForm.startTime"
    readonly
    placeholder="请选择开始时间"
    @click="startTimePickerVisible = true"
  />
</up-form-item>
<up-form-item label="结束时间" required>
  <up-input
    v-model="endReportForm.endTime"
    readonly
    placeholder="请选择结束时间"
    @click="endTimePickerVisible = true"
  />
</up-form-item>
            </view>
            <view class="form-section">
              <text class="section-title">班组信息</text>
              <up-form-item label="机台" required>
                <up-input
                  v-model="endReportForm.deviceName"
                  readonly
                  placeholder="请选择"
                  @click="openDevicePicker"
                  suffixIcon="arrow-down"
                />
              </up-form-item>
              <up-form-item label="班组人员">
                <up-input
                  v-model="teamDisplayText"
@@ -188,10 +243,6 @@
                  suffixIcon="arrow-down"
                />
              </up-form-item>
            </view>
            <view class="form-section">
              <text class="section-title">审核信息</text>
              <up-form-item label="审核人" required>
                <up-input
                  v-model="endReportForm.auditUserName"
@@ -254,6 +305,13 @@
      @select="onAuditSelect"
      @close="auditPickerVisible = false"
    />
    <up-action-sheet
      :show="devicePickerVisible"
      :actions="deviceActions"
      title="选择机台"
      @select="onDeviceSelect"
      @close="devicePickerVisible = false"
    />
    <!-- 时间选择器 -->
    <up-datetime-picker
@@ -279,7 +337,8 @@
<script setup>
import { ref, reactive, toRefs, computed, getCurrentInstance } from "vue";
import { onShow, onLoad, onReachBottom } from "@dcloudio/uni-app";
import { productWorkOrderPage, addProductMain, startProduction, getProductWorkOrderById } from "@/api/productionManagement/productionReporting.js";
import { productWorkOrderPage, addProductMain, startProduction, getProductWorkOrderById,getProductionProductMain } from "@/api/productionManagement/productionReporting.js";
import { getDeviceLedger } from "@/api/equipmentManagement/ledger";
import { userListNoPageByTenantId } from "@/api/system/user.js";
import PageHeader from "@/components/PageHeader.vue";
import FilesDia from "./components/filesDia.vue";
@@ -292,6 +351,28 @@
const loadStatus = ref('loadmore');
const transferCardVisible = ref(false);
const transferCardRowData = ref(null);
const transferCardLoading = ref(false);
const transferCardRecords = ref([]);
const auditStatusOptions = ref([
  { label: "未审核", value: 0 },
  { label: "通过", value: 1 },
  { label: "不通过", value: 2 },
]);
const transferRecordFields = [
  { label: "报工人员", prop: "teamNames", wide: true },
  { label: "审核人", prop: "auditUserName" },
  { label: "最终审核人", prop: "sureAuditUserName" },
  { label: "工单编号", prop: "workOrderNo" },
  { label: "订单编号", prop: "salesContractNo" },
  { label: "产品名称", prop: "productName" },
  { label: "产品规格型号", prop: "productModelName" },
  { label: "产出数量", prop: "quantity" },
  { label: "报废数量", prop: "scrapQty" },
  { label: "单位", prop: "unit" },
  { label: "审核状态", prop: "auditStatus" },
  { label: "备注信息", prop: "auditOpinion", wide: true },
  { label: "创建时间", prop: "createTime", wide: true },
];
const workOrderFilesRef = ref(null);
const startSubmittingId = ref(null);
@@ -302,6 +383,8 @@
  quantity: "",
  replenishQty: "0",
  scrapQty: "0",
  deviceLedgerId: "",
  deviceName: "",
  teamList: [],
  startTime: "",
  endTime: "",
@@ -318,6 +401,10 @@
const userOptions = ref([]);
const auditPickerVisible = ref(false);
const auditActions = computed(() => userOptions.value.map(u => ({ name: u.name, value: u.value })));
const deviceOptions = ref([]);
const devicePickerVisible = ref(false);
const deviceActions = computed(() => deviceOptions.value.map(d => ({ name: d?.deviceName || "", value: d?.id })));
const teamPickerVisible = ref(false);
const teamCheckedIds = ref([]);
@@ -344,7 +431,7 @@
const data = reactive({
  searchForm: {
    workOrderNo: "",
    workOrderId: "",
  },
});
const { searchForm } = toRefs(data);
@@ -390,8 +477,17 @@
  }
};
const ensureDeviceOptions = async () => {
  if (deviceOptions.value.length > 0) return;
  try {
    const res = await getDeviceLedger();
    deviceOptions.value = res?.data || [];
  } catch {
    deviceOptions.value = [];
  }
};
const getList = () => {
  console.log(searchForm.value);
  if (loading.value) return;
  loading.value = true;
  
@@ -455,36 +551,93 @@
  return Math.round(n);
};
const showTransferCard = (row) => {
  transferCardRowData.value = row;
  transferCardVisible.value = true;
const getAuditStatusLabel = (val) => {
  const target = auditStatusOptions.value.find(item => Number(item.value) === Number(val));
  return target?.label || "未知";
};
const openWorkOrderFiles = (row) => {
  workOrderFilesRef.value?.openDialog(row);
const getAuditStatusType = (val) => {
  const typeMap = { 0: "info", 1: "success", 2: "danger" };
  return typeMap[Number(val)] || "default";
};
const getTeamNameList = (val) => {
  if (!val) return [];
  return String(val)
    .split(",")
    .map(item => item.trim())
    .filter(Boolean);
};
const formatTransferRecordValue = (record, prop) => {
  if (prop === "auditStatus") {
    return getAuditStatusLabel(record?.[prop]);
  }
  const value = record?.[prop];
  if (value === null || value === undefined || value === "") {
    return "-";
  }
  return String(value);
};
const getTransferCardParams = (row) => {
  const params = {
    current: 1,
    size: 100,
  };
  if (row?.id != null) {
    params.workOrderId = row.id;
  }
  if (row?.workOrderNo) {
    params.workOrderNo = row.workOrderNo;
  }
  if (row?.productMainId != null) {
    params.productMainId = row.productMainId;
  }
  return params;
};
const showTransferCard = async (row) => {
  transferCardRowData.value = row;
  transferCardVisible.value = true;
  transferCardRecords.value = [];
  transferCardLoading.value = true;
  try {
    const res = await getProductionProductMain(getTransferCardParams(row));
    transferCardRecords.value = res?.data?.records || res?.records || [];
  } catch {
    transferCardRecords.value = [];
    showToast("加载报工审核记录失败");
  } finally {
    transferCardLoading.value = false;
  }
};
const getPendingQty = (row) => {
  const plan = Number(row?.planQuantity) || 0;
  const complete = Number(row?.completeQuantity) || 0;
  return plan - complete;
  return Math.max(plan - complete, 0);
};
const isStarted = (row) => {
  if (row?.startProductTime && !row?.endProductTime) return true;
  if (String(row?.reportWork) === "1" && !row?.endProductTime) return true;
  return false;
  if (!row?.id) return false;
  if (Number(row?.completeQuantity) > 0) return true;
  if (row?.startProductTime) return true;
  return String(row?.reportWork) === "1";
};
const isEnded = (row) => {
  return Boolean(row?.endProductTime);
  return getPendingQty(row) <= 0;
};
const canEndProduction = (row) => {
  if (!row?.id) return false;
  if (getPendingQty(row) <= 0) return false;
  if (isEnded(row)) return false;
  if (!isStarted(row)) return false;
  return true;
};
@@ -494,14 +647,16 @@
  if (getPendingQty(row) <= 0) return false;
  if (isEnded(row)) return false;
  if (isStarted(row)) return false;
  if (!canStartProductionByUserIds(userStore.id, row)) return false;
  return true;
  return canStartProductionByUserIds(userStore.id, row);
};
// 根据userIds判断是否有用户可以报工
const canStartProductionByUserIds = (userId, row) => {
  const team = row?.userIds || "";
  return team.includes(userId);
  if (!userId) return true;
  if (!team) return true;
  return String(team).includes(String(userId));
};
const handleStartProduction = (row) => {
@@ -552,11 +707,14 @@
  endReportRow.value = row;
  await ensureUserInfo();
  await ensureUserOptions();
  await ensureDeviceOptions();
  endReportForm.planQuantity = String(getPendingQty(row));
  endReportForm.quantity = String(getPendingQty(row));
  endReportForm.replenishQty = "0";
  endReportForm.scrapQty = "0";
  endReportForm.deviceLedgerId = "";
  endReportForm.deviceName = "";
  endReportForm.teamList = [];
  teamCheckedIds.value = [];
@@ -579,6 +737,14 @@
  endTimeValue.value = Date.now();
  endReportForm.endTime = formatDateTime(endTimeValue.value);
  if (row?.deviceName) {
    const matched = deviceOptions.value.find(d => String(d?.deviceName || "") === String(row.deviceName));
    if (matched) {
      endReportForm.deviceLedgerId = String(matched.id ?? "");
      endReportForm.deviceName = String(matched.deviceName ?? "");
    }
  }
  endReportVisible.value = true;
};
@@ -596,6 +762,17 @@
  endReportForm.auditUserId = String(e?.value ?? "");
  endReportForm.auditUserName = String(e?.name ?? "");
  auditPickerVisible.value = false;
};
const openDevicePicker = async () => {
  await ensureDeviceOptions();
  devicePickerVisible.value = true;
};
const onDeviceSelect = (e) => {
  endReportForm.deviceLedgerId = String(e?.value ?? "");
  endReportForm.deviceName = String(e?.name ?? "");
  devicePickerVisible.value = false;
};
const openTeamPicker = async () => {
@@ -616,12 +793,25 @@
};
const onStartTimeConfirm = (e) => {
  endReportForm.startTime = formatDateTime(e.value);
  const start = e.value;
  endReportForm.startTime = formatDateTime(start);
  startTimePickerVisible.value = false;
  // 👉 自动给结束时间 +1小时
  const end = start + 60 * 60 * 1000;
  endTimeValue.value = end;
  endReportForm.endTime = formatDateTime(end);
};
const onEndTimeConfirm = (e) => {
  endReportForm.endTime = formatDateTime(e.value);
  const end = e.value;
  if (end <= startTimeValue.value) {
    showToast("结束时间必须大于开始时间");
    return;
  }
  endReportForm.endTime = formatDateTime(end);
  endTimePickerVisible.value = false;
};
@@ -655,9 +845,31 @@
    showToast("报废数量必须为大于等于0的整数");
    return;
  }
  if (!endReportForm.startTime) {
  showToast("请选择开始时间");
  return;
}
if (!endReportForm.endTime) {
  showToast("请选择结束时间");
  return;
}
const start = new Date(endReportForm.startTime).getTime();
const end = new Date(endReportForm.endTime).getTime();
if (end <= start) {
  showToast("结束时间必须大于开始时间");
  return;
}
  if (!endReportForm.auditUserId) {
    showToast("请选择审核人");
    return;
  }
  if (!endReportForm.deviceLedgerId) {
    showToast("请选择机台");
    return;
  }
@@ -674,17 +886,19 @@
      userName: endReportForm.userName || userStore.nickName,
      auditUserId: endReportForm.auditUserId,
      auditUserName: endReportForm.auditUserName,
      deviceLedgerId: endReportForm.deviceLedgerId,
      deviceName: endReportForm.deviceName,
    };
    const res = await addProductMain(submitData);
    if (res?.code === 200) {
      showToast("结束报工成功");
      showToast("报工成功");
      closeEndReport();
      handleQuery();
    } else {
      showToast(res?.msg || "结束报工失败");
      showToast(res?.msg || "报工失败");
    }
  } catch {
    showToast("结束报工失败");
    showToast("报工失败");
  } finally {
    uni.hideLoading();
  }
@@ -752,6 +966,164 @@
  }
}
.qr-popup {
  width: 90vw;
  max-width: 900rpx;
  max-height: 85vh;   // 原来 80 → 加大
  padding: 32rpx 28rpx;
  background: #fff;
  border-radius: 20rpx;
  display: flex;
  flex-direction: column;
  align-items: center;
  box-sizing: border-box;
}
.qr-title {
  font-size: 30rpx;
  font-weight: 600;
  color: #222;
}
.qr-box {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  margin-top: 24rpx;
}
.qr-info {
  margin-top: 16rpx;
  font-size: 26rpx;
  color: #666;
}
.transfer-records {
  width: 100%;
  margin-top: 28rpx;
}
.transfer-records-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 16rpx;
  margin-bottom: 16rpx;
}
.transfer-records-title {
  font-size: 26rpx;
  font-weight: 600;
  color: #222;
}
.transfer-records-count {
  font-size: 22rpx;
  color: #8a8a8a;
}
.transfer-records-loading,
.transfer-records-empty {
  padding: 28rpx 0;
  font-size: 24rpx;
  color: #8a8a8a;
  text-align: center;
}
.transfer-records-list {
  max-height: 500rpx;
  overflow: auto;
}
.transfer-record-item {
  padding: 20rpx 22rpx;
  border-radius: 16rpx;
  background: #f7f8fa;
}
.transfer-record-item + .transfer-record-item {
  margin-top: 16rpx;
}
.transfer-record-top {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 24rpx;
}
.transfer-record-top-main {
  display: flex;
  flex-direction: column;
  gap: 8rpx;
  min-width: 0;
}
.transfer-record-no {
  font-size: 28rpx;
  font-weight: 600;
  color: #222;
  word-break: break-all;
}
.transfer-record-sub {
  font-size: 22rpx;
  color: #8a8a8a;
  word-break: break-all;
}
.transfer-record-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 16rpx;
  margin-top: 18rpx;
}
.transfer-record-cell {
  width: 100%;   // 原来一行两个 → 改成一行一个
}
.transfer-record-cell.is-wide {
  width: 100%;
}
.transfer-record-label {
  display: block;
  margin-bottom: 8rpx;
  font-size: 22rpx;
  color: #8a8a8a;
}
.transfer-record-content {
  min-height: 36rpx;
}
.transfer-record-value {
  display: block;
  font-size: 24rpx;
  color: #222;
  word-break: break-all;
}
.transfer-tag-list {
  display: flex;
  flex-wrap: wrap;
  gap: 10rpx;
}
.transfer-name-tag {
  padding: 6rpx 14rpx;
  border-radius: 999rpx;
  background: #e9f2ff;
  color: #2f6bff;
  font-size: 22rpx;
}
.audit-status-tag {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 40rpx;
  padding: 0 16rpx;
  border-radius: 999rpx;
  font-size: 22rpx;
  box-sizing: border-box;
}
.audit-status-info {
  background: #edf1f5;
  color: #6b7280;
}
.audit-status-success {
  background: #e9f9ef;
  color: #16a34a;
}
.audit-status-danger {
  background: #feeeee;
  color: #dc2626;
}
.audit-status-default {
  background: #f4f4f5;
  color: #606266;
}
.report-modal {
  background: #fff;
  max-height: 85vh;