zhangwencui
3 天以前 3e7bafbaf57c7a10baa0494bf8a551c4382a0dac
feat(app): 生产领料按任务工序投入物料(BOM)选择

新增 work-order-bom/list-by-task-id 接口,领料行物料收窄为当前任务工序BOM范围
已修改2个文件
37 ■■■■■ 文件已修改
src/api/production.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/wls/components/WlsFlowDocForm.vue 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/production.ts
@@ -109,6 +109,7 @@
export interface WorkOrderBom {
  id?: number;
  workOrderId?: number;
  itemId?: number;
  itemCode?: string;
  itemName?: string;
  itemSpecification?: string;
@@ -400,6 +401,11 @@
  return request<WorkOrderBom[]>(`/mes/pro/work-order-bom/item-list-by-work-order-id${query({ workOrderId })}`, { header: header(accessToken) });
}
/** 查询生产任务所在工序的投入物料(领料单行按此范围选择物料) */
export function getWorkOrderBomListByTaskId(accessToken: string, taskId: number) {
  return request<WorkOrderBom[]>(`/mes/pro/work-order-bom/list-by-task-id${query({ taskId })}`, { header: header(accessToken) });
}
export function getTaskPage(accessToken: string, params: { pageNo: number; pageSize: number; code?: string; name?: string; workOrderId?: number; workstationId?: number; routeId?: number; processId?: number; status?: number; statuses?: number[] }) {
  return request<PageResult<ProTask>>(`/mes/pro/task/page${query(params)}`, { header: header(accessToken) });
}
src/pages/wls/components/WlsFlowDocForm.vue
@@ -72,6 +72,7 @@
  type WarehouseArea,
  type WarehouseLocation,
} from "../../../api/wls";
import { getWorkOrderBomListByTaskId, type WorkOrderBom } from "../../../api/production";
import { useAuth } from "../../../stores/auth";
import { dateText, dayText, materialOptionText } from "./shared";
@@ -85,6 +86,8 @@
const lines = ref<WlsDocLine[]>([]);
const details = ref<Record<number, WlsDocDetail[]>>({});
const materials = ref<MaterialItem[]>([]);
// 生产领料:当前任务所在工序的投入物料(BOM),行物料只能在此范围内选择
const taskBoms = ref<WorkOrderBom[]>([]);
const stocks = ref<MaterialStock[]>([]);
const workOrders = ref<WlsWorkOrder[]>([]);
const tasks = ref<WlsTask[]>([]);
@@ -126,6 +129,17 @@
const moduleName = computed(() => config.value.module as "product-issue" | "product-receipt" | "sales-notice" | "return-issue" | "product-sales" | "return-sales");
const detailModule = computed(() => config.value.module as "product-issue" | "product-receipt" | "return-issue" | "product-sales" | "return-sales");
const materialOptions = computed(() => materials.value.map(materialOptionText));
// 生产领料:选中任务后,行物料只能从该任务工序的投入物料(BOM)中选择
const issueMaterialOptions = computed(() => {
  if (props.kind !== "product-issue") return materialOptions.value;
  if (!form.taskId) return [];
  return taskBoms.value.map(item => `${item.itemCode || "--"} - ${item.itemName || "--"} / ${item.itemSpecification || "无规格"} / ${item.unitMeasureName || "无单位"}`);
});
const issueMaterialPlaceholder = computed(() => {
  if (props.kind !== "product-issue") return "请选择物料";
  if (!form.taskId) return "请先选择生产任务";
  return taskBoms.value.length ? "请选择当前工序物料" : "当前工序未维护投入物料";
});
const stockOptions = computed(() => stocks.value.map(item => `${item.itemCode || "--"} - ${item.itemName || "--"} / ${item.specification || "无规格"} / ${item.unitMeasureName || "无单位"} / ${item.batchCode || "无批次"} / ${item.warehouseName || "--"}-${item.locationName || "--"}-${item.areaName || "--"} / 可用${item.availableQuantity ?? item.quantity ?? 0}`));
const workOrderOptions = computed(() => workOrders.value.map(item => `${item.code || "--"} - ${item.name || item.itemName || "--"}`));
const taskOptions = computed(() => tasks.value.map(item => `${item.taskCode || item.code || "--"} - ${item.taskName || item.name || "--"} / ${item.workstationName || "无工作站"}`));
@@ -221,6 +235,7 @@
            : await getReturnSales(state.accessToken, id.value);
  clearForm(); Object.assign(form, data);
  if (form.workOrderId) await loadTasks();
  if (form.taskId) await loadTaskBoms(form.taskId);
  if (form.saleOrderId) await loadSaleOrderItems(form.saleOrderId);
  if (form.noticeId) await loadNoticeLines();
  const dataLines = await getWlsLinePage(state.accessToken, moduleName.value, { pageNo: 1, pageSize: 100, [lineOwnerKey.value]: id.value });
@@ -269,10 +284,16 @@
  await loadTasks();
  await loadLineSources();
}
async function loadTaskBoms(taskId?: number) {
  taskBoms.value = [];
  if (!state.accessToken || props.kind !== "product-issue" || !taskId) return;
  taskBoms.value = await getWorkOrderBomListByTaskId(state.accessToken, taskId);
}
function selectTask(event: { detail: { value: number } }) {
  const row = tasks.value[Number(event.detail.value)];
  form.taskId = row?.id; form.taskCode = row?.taskCode || row?.code; form.taskName = row?.taskName || row?.name;
  form.workstationId = row?.workstationId; form.workstationCode = row?.workstationCode; form.workstationName = row?.workstationName;
  void loadTaskBoms(form.taskId);
}
function selectWorkstation(event: { detail: { value: number } }) {
  const row = workstations.value[Number(event.detail.value)];
@@ -294,6 +315,12 @@
  await loadNoticeLines();
}
function selectMaterial(event: { detail: { value: number } }, line: WlsDocLine) {
  // 生产领料且已选任务:物料来源于当前任务工序投入物料(BOM)
  if (props.kind === "product-issue" && form.taskId) {
    const bom = taskBoms.value[Number(event.detail.value)];
    Object.assign(line, { itemId: bom?.itemId, itemCode: bom?.itemCode, itemName: bom?.itemName, specification: bom?.itemSpecification, unitMeasureName: bom?.unitMeasureName });
    return;
  }
  const row = materials.value[Number(event.detail.value)];
  Object.assign(line, { itemId: row?.id, itemCode: row?.code, itemName: row?.name, specification: row?.specification, unitMeasureName: row?.unitMeasureName });
}
@@ -493,7 +520,7 @@
        <view v-else-if="kind === 'product-receipt'" class="field"><text>库存记录 <text class="required">*</text></text><picker v-if="!readonly" :range="stockOptions" @change="selectStockLine($event,line)"><view class="picker"><text>{{ line.materialStockId ? `${lineMaterialText(line)} / ${line.batchCode || '无批次'}` : '请选择库存记录' }}</text><text>›</text></view></picker><text v-else class="value">{{ line.materialStockId ? lineMaterialText(line) : '--' }}</text></view>
        <view v-else-if="kind === 'return-issue' && returnableLines.length" class="field"><text>领料记录 <text class="required">*</text></text><picker v-if="!readonly" :range="returnableLineOptions" @change="selectReturnableLine($event,line)"><view class="picker"><text>{{ line.issueLineId ? `${lineMaterialText(line)} / ${line.batchCode || '无批次'}` : '请选择领料记录' }}</text><text>›</text></view></picker><text v-else class="value">{{ line.issueLineId ? lineMaterialText(line) : '--' }}</text></view>
        <view v-else-if="kind === 'return-sales' && saleOrderItems.length" class="field"><text>物料 <text class="required">*</text></text><picker v-if="!readonly" :range="saleOrderItems.map(i => `${i.productCode || '--'} / ${i.productName || '--'} / ${i.productSpecification || '无规格'} / ${i.productUnitName || '无单位'}`)" @change="selectSaleOrderItem($event,line)"><view class="picker"><text>{{ lineMaterialText(line) }}</text><text>›</text></view></picker><text v-else class="value">{{ line.itemId ? lineMaterialText(line) : '--' }}</text></view>
        <view v-else class="field"><text>{{ lineMaterialLabel() }} <text class="required">*</text></text><picker v-if="!readonly" :range="materialOptions" @change="selectMaterial($event,line)"><view class="picker"><text>{{ lineMaterialText(line) }}</text><text>›</text></view></picker><text v-else class="value">{{ line.itemId ? lineMaterialText(line) : '--' }}</text></view>
        <view v-else class="field"><text>{{ lineMaterialLabel() }} <text class="required">*</text></text><picker v-if="!readonly" :range="issueMaterialOptions" :disabled="kind === 'product-issue' && !issueMaterialOptions.length" @change="selectMaterial($event,line)"><view class="picker"><text :class="{ 'picker-placeholder': !line.itemId }">{{ line.itemId ? lineMaterialText(line) : issueMaterialPlaceholder }}</text><text>›</text></view></picker><text v-else class="value">{{ line.itemId ? lineMaterialText(line) : '--' }}</text></view>
        <view class="field"><text>规格型号</text><text class="value">{{ line.specification || '--' }}</text></view>
        <view class="field"><text>单位</text><text class="value">{{ line.unitMeasureName || '--' }}</text></view>
        <view class="field"><text>{{ config.quantity }} <text class="required">*</text></text><input :value="String(line.quantity || '')" :disabled="readonly" type="digit" :placeholder="`请输入${config.quantity}`" @input="line.quantity=num($event.detail.value)" /></view>
@@ -538,5 +565,5 @@
</template>
<style lang="scss" scoped>
.page{min-height:100vh;padding:20rpx 24rpx 60rpx;box-sizing:border-box;background:#f3f6fb}.form-card{margin-bottom:18rpx;background:#fff;border-radius:16rpx;overflow:hidden}.field{display:flex;align-items:center;min-height:86rpx;padding:0 22rpx;border-bottom:1rpx solid #edf1f6;box-sizing:border-box}.field>text:first-child{flex:none;width:180rpx;color:#10233f;font-size:26rpx}.field>picker{display:block;flex:1;min-width:0}.field input,.field textarea,.value,.picker{flex:1;min-width:0;color:#10233f;font-size:26rpx;text-align:right}.field textarea{height:120rpx;padding:22rpx 0}.field.column{align-items:flex-start;flex-direction:column;padding:22rpx}.field.column>text:first-child{width:auto;margin-bottom:12rpx}.field.column textarea{text-align:left;width:100%;height:120rpx;padding:0}.required{color:#e23b3b}.picker{display:flex;width:100%;align-items:center;justify-content:space-between;gap:10rpx}.picker text:first-child{min-width:0;flex:1;overflow:hidden;text-align:right;text-overflow:ellipsis;white-space:nowrap}.picker text:last-child{flex:none;width:24rpx;text-align:right}.code-field input{text-align:left}.code-field button,.section button,.line-head button,.detail-actions button{height:52rpx;min-height:52rpx;padding:0 16rpx;margin:0;color:var(--color-primary, #1769d8);font-size:23rpx;line-height:50rpx;background:var(--color-primary-soft, #f5f8ff);border:1rpx solid var(--color-primary-border, #8ab3ee);border-radius:8rpx}.code-field button::after,.section button::after,.line-head button::after,.detail-actions button::after,.save::after,.submit::after,.modal-actions button::after{border:0}.section{display:flex;align-items:center;justify-content:space-between;height:82rpx;padding:0 22rpx;color:#10233f;font-size:30rpx;font-weight:600}.line-card{margin:0 18rpx 18rpx;border:1rpx solid #edf1f6;border-radius:12rpx;overflow:hidden}.line-head{display:flex;align-items:center;justify-content:space-between;padding:16rpx 18rpx;color:#10233f;font-size:27rpx;font-weight:600;background:#f9fbff}.line-head>view{display:flex;gap:10rpx}.danger{color:#c65050!important;background:#fff7f7!important;border-color:#e3a0a0!important}.detail-list{padding:10rpx 18rpx 16rpx;border-top:1rpx solid #edf1f6}.detail-row{display:flex;align-items:flex-start;justify-content:space-between;gap:14rpx;padding:14rpx 0;color:#66778c;font-size:23rpx;border-bottom:1rpx solid #f2f4f8}.detail-info{display:flex;min-width:0;flex:1;flex-direction:column;gap:8rpx}.detail-info text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.detail-actions{display:flex;flex:none;align-items:center;justify-content:flex-end;gap:10rpx}.detail-actions button{width:84rpx;padding:0}.empty{padding:34rpx;color:#8795a8;font-size:25rpx;text-align:center}.save,.submit{width:100%;height:78rpx;margin:18rpx 0 0;color:#fff;font-size:28rpx;line-height:78rpx;background:var(--color-primary, #1769d8);border-radius:10rpx}.submit{background:#16a36f}.mask{position:fixed;left:0;right:0;top:0;bottom:0;z-index:60;display:flex;align-items:flex-end;background:rgba(0,0,0,.35)}.modal{width:100%;max-height:86vh;padding:0 24rpx 28rpx;box-sizing:border-box;background:#fff;border-radius:24rpx 24rpx 0 0}.modal-title{height:88rpx;line-height:88rpx;color:#10233f;font-size:30rpx;font-weight:600;text-align:center}.modal-actions{display:flex;gap:18rpx;padding-top:24rpx}.modal-actions button{flex:1;height:72rpx;line-height:72rpx;font-size:27rpx;border-radius:10rpx}.modal-actions .primary{color:#fff;background:var(--color-primary, #1769d8)}
.page{min-height:100vh;padding:20rpx 24rpx 60rpx;box-sizing:border-box;background:#f3f6fb}.form-card{margin-bottom:18rpx;background:#fff;border-radius:16rpx;overflow:hidden}.field{display:flex;align-items:center;min-height:86rpx;padding:0 22rpx;border-bottom:1rpx solid #edf1f6;box-sizing:border-box}.field>text:first-child{flex:none;width:180rpx;color:#10233f;font-size:26rpx}.field>picker{display:block;flex:1;min-width:0}.field input,.field textarea,.value,.picker{flex:1;min-width:0;color:#10233f;font-size:26rpx;text-align:right}.field textarea{height:120rpx;padding:22rpx 0}.field.column{align-items:flex-start;flex-direction:column;padding:22rpx}.field.column>text:first-child{width:auto;margin-bottom:12rpx}.field.column textarea{text-align:left;width:100%;height:120rpx;padding:0}.required{color:#e23b3b}.picker{display:flex;width:100%;align-items:center;justify-content:space-between;gap:10rpx}.picker text:first-child{min-width:0;flex:1;overflow:hidden;text-align:right;text-overflow:ellipsis;white-space:nowrap}.picker text:last-child{flex:none;width:24rpx;text-align:right}.code-field input{text-align:left}.code-field button,.section button,.line-head button,.detail-actions button{height:52rpx;min-height:52rpx;padding:0 16rpx;margin:0;color:var(--color-primary, #1769d8);font-size:23rpx;line-height:50rpx;background:var(--color-primary-soft, #f5f8ff);border:1rpx solid var(--color-primary-border, #8ab3ee);border-radius:8rpx}.code-field button::after,.section button::after,.line-head button::after,.detail-actions button::after,.save::after,.submit::after,.modal-actions button::after{border:0}.section{display:flex;align-items:center;justify-content:space-between;height:82rpx;padding:0 22rpx;color:#10233f;font-size:30rpx;font-weight:600}.line-card{margin:0 18rpx 18rpx;border:1rpx solid #edf1f6;border-radius:12rpx;overflow:hidden}.line-head{display:flex;align-items:center;justify-content:space-between;padding:16rpx 18rpx;color:#10233f;font-size:27rpx;font-weight:600;background:#f9fbff}.line-head>view{display:flex;gap:10rpx}.danger{color:#c65050!important;background:#fff7f7!important;border-color:#e3a0a0!important}.detail-list{padding:10rpx 18rpx 16rpx;border-top:1rpx solid #edf1f6}.detail-row{display:flex;align-items:flex-start;justify-content:space-between;gap:14rpx;padding:14rpx 0;color:#66778c;font-size:23rpx;border-bottom:1rpx solid #f2f4f8}.detail-info{display:flex;min-width:0;flex:1;flex-direction:column;gap:8rpx}.detail-info text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.detail-actions{display:flex;flex:none;align-items:center;justify-content:flex-end;gap:10rpx}.detail-actions button{width:84rpx;padding:0}.empty{padding:34rpx;color:#8795a8;font-size:25rpx;text-align:center}.save,.submit{width:100%;height:78rpx;margin:18rpx 0 0;color:#fff;font-size:28rpx;line-height:78rpx;background:var(--color-primary, #1769d8);border-radius:10rpx}.submit{background:#16a36f}.mask{position:fixed;left:0;right:0;top:0;bottom:0;z-index:60;display:flex;align-items:flex-end;background:rgba(0,0,0,.35)}.modal{width:100%;max-height:86vh;padding:0 24rpx 28rpx;box-sizing:border-box;background:#fff;border-radius:24rpx 24rpx 0 0}.modal-title{height:88rpx;line-height:88rpx;color:#10233f;font-size:30rpx;font-weight:600;text-align:center}.modal-actions{display:flex;gap:18rpx;padding-top:24rpx}.modal-actions button{flex:1;height:72rpx;line-height:72rpx;font-size:27rpx;border-radius:10rpx}.modal-actions .primary{color:#fff;background:var(--color-primary, #1769d8)}.picker-placeholder{color:#9aa6b5}
</style>