Explorar o código

Merge remote-tracking branch 'origin/dev' into test

lucw hai 1 día
pai
achega
3a8d90eb3b

+ 59 - 0
src/api/disposalStorage/index.js

@@ -0,0 +1,59 @@
+import request from '@/utils/request';
+
+/**
+ * 处置入库单分页列表
+ * @param {Object} params 查询条件
+ */
+export async function getDisposalStoragePage(params) {
+  const res = await request.get('/qms/disposalstorage/page', { params });
+  if (res.data.code == 0) {
+    return res.data.data;
+  }
+  return Promise.reject(new Error(res.data.message));
+}
+
+/**
+ * 处置入库单详情
+ * @param {Number|String} id 入库申请单ID
+ */
+export async function getDisposalStorageById(id) {
+  const res = await request.get(`/qms/disposalstorage/getById/${id}`);
+  if (res.data.code == 0) {
+    return res.data.data;
+  }
+  return Promise.reject(new Error(res.data.message));
+}
+
+/**
+ * 入库完成后更新单据状态并回写处置台账(报废品台账 / 试销品台账)
+ * @param {Number|String} id 入库申请单ID
+ * @param {Number|String} [outInId] 实际库存入库单ID,为空时由后端取单据已回写的入库单
+ */
+export async function ledgerCallback(id, outInId) {
+  const res = await request.post(`/qms/disposalstorage/ledgerCallback/${id}`, null, {
+    params: outInId ? { outInId } : {}
+  });
+  if (res.data.code == 0) {
+    return res.data.data;
+  }
+  return Promise.reject(new Error(res.data.message));
+}
+
+/**
+ * 按来源单号查询库存入库单,用于入库成功但回写失败后的重试场景
+ * @param {String} sourceBizNo 来源单号(处置入库单号)
+ */
+export async function getInStorageBySourceBizNo(sourceBizNo) {
+  const res = await request.get(`/wms/outintwo/getInfoBySourceBizNo/${encodeURIComponent(sourceBizNo)}`);
+  if (res.data.code == 0) {
+    return res.data.data;
+  }
+  return Promise.reject(new Error(res.data.message));
+}
+
+export default {
+  getDisposalStoragePage,
+  getDisposalStorageById,
+  getInStorageBySourceBizNo,
+  ledgerCallback
+};

+ 62 - 0
src/utils/dict/disposalStorage.js

@@ -0,0 +1,62 @@
+/**
+ * 处置入库单字典
+ */
+
+/** 申请类型(处置类型):3报废入库 9试销入库 */
+export const disposalTypeOptions = [
+  { label: '报废入库', value: 3 },
+  { label: '试销入库', value: 9 }
+];
+
+/** 审批状态 */
+export const approvalStatusOptions = [
+  { label: '未提交', value: 0 },
+  { label: '审核中', value: 1 },
+  { label: '审核通过', value: 2 },
+  { label: '审核不通过', value: 3 }
+];
+
+/** 单据状态 */
+export const storageStatusOptions = [
+  { label: '未入库', value: 0 },
+  { label: '已入库', value: 1 }
+];
+
+/**
+ * 取字典标签
+ * @param {Array} options 字典项
+ * @param {Number|String} value 字典值
+ * @param {String} defaultLabel 兜底文案
+ */
+export function dictLabel(options, value, defaultLabel = '-') {
+  if (value === null || value === undefined || value === '') {
+    return defaultLabel;
+  }
+  const item = options.find((i) => String(i.value) === String(value));
+  return item ? item.label : defaultLabel;
+}
+
+/** 申请类型文案 */
+export function disposalTypeLabel(value) {
+  return dictLabel(disposalTypeOptions, value);
+}
+
+/** 审批状态文案 */
+export function approvalStatusLabel(value) {
+  return dictLabel(approvalStatusOptions, value);
+}
+
+/** 单据状态文案 */
+export function storageStatusLabel(value) {
+  return dictLabel(storageStatusOptions, value);
+}
+
+export default {
+  disposalTypeOptions,
+  approvalStatusOptions,
+  storageStatusOptions,
+  dictLabel,
+  disposalTypeLabel,
+  approvalStatusLabel,
+  storageStatusLabel
+};

+ 222 - 0
src/views/disposalStorage/components/flow.vue

@@ -0,0 +1,222 @@
+<!-- 处置入库单 流程查看 -->
+<template>
+  <el-dialog
+    title="流程"
+    :before-close="handleClose"
+    :visible.sync="dialogVisible"
+    :close-on-click-modal="false"
+    :append-to-body="true"
+    width="80%"
+  >
+    <div class="app-container" style="padding: 15px">
+      <!-- 审批记录 -->
+      <el-card class="box-card" v-loading="tasksLoad">
+        <div slot="header" class="clearfix">
+          <span class="el-icon-picture-outline">审批记录</span>
+        </div>
+        <el-col :span="16" :offset="4">
+          <div class="block">
+            <el-timeline>
+              <el-timeline-item
+                v-for="(item, index) in tasks"
+                :key="index"
+                :icon="getTimelineItemIcon(item)"
+                :type="getTimelineItemType(item)"
+              >
+                <p style="font-weight: 700">任务:{{ item.name }}</p>
+                <el-card :body-style="{ padding: '10px' }">
+                  <label
+                    v-if="item.assigneeUser"
+                    style="font-weight: normal; margin-right: 30px"
+                  >
+                    审批人:{{ item.assigneeUser.nickname }}
+                    <el-tag type="info" size="mini">
+                      {{ item.assigneeUser.deptName }}
+                    </el-tag>
+                  </label>
+                  <label v-if="item.createTime" style="font-weight: normal">
+                    创建时间:
+                  </label>
+                  <label v-if="item.createTime" style="color: #8a909c; font-weight: normal">
+                    {{ item.createTime }}
+                  </label>
+                  <label v-if="item.endTime" style="margin-left: 30px; font-weight: normal">
+                    审批时间:
+                  </label>
+                  <label v-if="item.endTime" style="color: #8a909c; font-weight: normal">
+                    {{ item.endTime }}
+                  </label>
+                  <label v-if="item.durationInMillis" style="margin-left: 30px; font-weight: normal">
+                    耗时:
+                  </label>
+                  <label v-if="item.durationInMillis" style="color: #8a909c; font-weight: normal">
+                    {{ getDateStar(item.durationInMillis) }}
+                  </label>
+                  <p v-if="item.reason">
+                    <el-tag :type="getTimelineItemType(item)">{{ item.reason }}</el-tag>
+                  </p>
+                </el-card>
+              </el-timeline-item>
+            </el-timeline>
+          </div>
+        </el-col>
+      </el-card>
+
+      <!-- 高亮流程图 -->
+      <el-card class="box-card" v-loading="processInstanceLoading">
+        <div slot="header" class="clearfix">
+          <span class="el-icon-picture-outline">流程图</span>
+        </div>
+        <my-process-viewer
+          key="designer"
+          v-model="bpmnXML"
+          v-bind="bpmnControlForm"
+          :activityData="activityList"
+          :processInstanceData="processInstance"
+          :taskData="tasks"
+        />
+      </el-card>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+  import {
+    getProcessDefinitionBpmnXML,
+    getProcessInstance,
+    getTaskListByProcessInstanceId,
+    getActivityList
+  } from '@/api/bpm/index';
+  import { getDate } from '@/utils/dateUtils';
+
+  export default {
+    name: 'disposalStorageFlow',
+    data() {
+      return {
+        processInstanceLoading: true,
+        dialogVisible: false,
+        id: undefined,
+        processInstance: {},
+        bpmnXML: null,
+        bpmnControlForm: {
+          prefix: 'flowable'
+        },
+        activityList: [],
+        tasksLoad: true,
+        tasks: []
+      };
+    },
+    methods: {
+      /**
+       * 打开流程弹窗
+       * @param {String} id 流程实例ID
+       */
+      open(id) {
+        this.id = id;
+        this.dialogVisible = true;
+        this.getDetail();
+      },
+      /** 获得流程实例 */
+      getDetail() {
+        this.processInstanceLoading = true;
+        getProcessInstance(this.id).then((response) => {
+          if (!response) {
+            this.$message.error('查询不到流程信息!');
+            this.processInstanceLoading = false;
+            return;
+          }
+          this.processInstance = response;
+          getProcessDefinitionBpmnXML(this.processInstance.processDefinition.id).then(
+            (res) => {
+              this.bpmnXML = res;
+            }
+          );
+          getActivityList({ processInstanceId: this.processInstance.id }).then((res) => {
+            this.activityList = res;
+          });
+          this.processInstanceLoading = false;
+        });
+
+        // 获得流程任务列表(审批记录)
+        this.tasksLoad = true;
+        getTaskListByProcessInstanceId(this.id).then((response) => {
+          this.tasks = [];
+          // 移除已取消的审批
+          (response || []).forEach((task) => {
+            if (task.result !== 4) {
+              this.tasks.push(task);
+            }
+          });
+          // 排序,将未完成的排在前面,已完成的排在后面
+          this.tasks.sort((a, b) => {
+            if (a.endTime && b.endTime) {
+              return b.endTime - a.endTime;
+            } else if (a.endTime) {
+              return 1;
+            } else if (b.endTime) {
+              return -1;
+            } else {
+              return b.createTime - a.createTime;
+            }
+          });
+          this.tasksLoad = false;
+        });
+      },
+      getDateStar(ms) {
+        return getDate(ms);
+      },
+      getTimelineItemIcon(item) {
+        if (item.result === 1) {
+          return 'el-icon-time';
+        }
+        if (item.result === 2) {
+          return 'el-icon-check';
+        }
+        if (item.result === 3) {
+          return 'el-icon-close';
+        }
+        if (item.result === 4) {
+          return 'el-icon-remove-outline';
+        }
+        if (item.result === 5) {
+          return 'el-icon-back';
+        }
+        return '';
+      },
+      getTimelineItemType(item) {
+        if (item.result === 1) {
+          return 'primary';
+        }
+        if (item.result === 2) {
+          return 'success';
+        }
+        if (item.result === 3) {
+          return 'danger';
+        }
+        if (item.result === 4) {
+          return 'info';
+        }
+        if (item.result === 5) {
+          return 'warning';
+        }
+        if (item.result === 6) {
+          return 'default';
+        }
+        return '';
+      },
+      handleClose() {
+        this.dialogVisible = false;
+      }
+    }
+  };
+</script>
+
+<style lang="scss">
+  .my-process-designer {
+    height: calc(100vh - 200px);
+  }
+  .box-card {
+    width: 100%;
+    margin-bottom: 20px;
+  }
+</style>

+ 189 - 0
src/views/disposalStorage/components/search.vue

@@ -0,0 +1,189 @@
+<!-- 处置入库单 搜索表单 -->
+<template>
+  <el-form
+    label-width="90px"
+    class="ele-form-search"
+    @keyup.enter.native="search"
+    @submit.native.prevent
+  >
+    <el-row :gutter="15">
+      <el-col v-bind="styleResponsive ? { lg: 5, md: 10 } : { span: 5 }">
+        <el-form-item label="单号:">
+          <el-input clearable v-model="where.code" placeholder="请输入" />
+        </el-form-item>
+      </el-col>
+
+      <el-col v-bind="styleResponsive ? { lg: 5, md: 10 } : { span: 5 }">
+        <el-form-item label="明细编码:">
+          <el-input clearable v-model="where.sourceDetailCode" placeholder="请输入" />
+        </el-form-item>
+      </el-col>
+
+      <el-col v-bind="styleResponsive ? { lg: 5, md: 10 } : { span: 5 }">
+        <el-form-item label="不良品单号:">
+          <el-input clearable v-model="where.sourceBillCode" placeholder="请输入" />
+        </el-form-item>
+      </el-col>
+
+      <el-col v-bind="styleResponsive ? { lg: 5, md: 10 } : { span: 5 }">
+        <el-form-item label="物品名称:">
+          <el-input clearable v-model="where.categoryName" placeholder="请输入" />
+        </el-form-item>
+      </el-col>
+
+      <el-col v-bind="styleResponsive ? { lg: 5, md: 10 } : { span: 5 }">
+        <el-form-item label="物品编码:">
+          <el-input clearable v-model="where.categoryCode" placeholder="请输入" />
+        </el-form-item>
+      </el-col>
+
+      <el-col v-bind="styleResponsive ? { lg: 5, md: 10 } : { span: 5 }">
+        <el-form-item label="批次号:">
+          <el-input clearable v-model="where.batchNo" placeholder="请输入" />
+        </el-form-item>
+      </el-col>
+
+      <el-col v-bind="styleResponsive ? { lg: 5, md: 10 } : { span: 5 }">
+        <el-form-item label="申请类型:">
+          <el-select clearable v-model="where.disposalType" placeholder="请选择">
+            <el-option
+              v-for="item in disposalTypeOptions"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            />
+          </el-select>
+        </el-form-item>
+      </el-col>
+
+      <el-col v-bind="styleResponsive ? { lg: 5, md: 10 } : { span: 5 }">
+        <el-form-item label="审批状态:">
+          <el-select clearable v-model="where.approvalStatus" placeholder="请选择">
+            <el-option
+              v-for="item in approvalStatusOptions"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            />
+          </el-select>
+        </el-form-item>
+      </el-col>
+
+      <el-col v-bind="styleResponsive ? { lg: 5, md: 10 } : { span: 5 }">
+        <el-form-item label="单据状态:">
+          <el-select clearable v-model="where.status" placeholder="请选择">
+            <el-option
+              v-for="item in storageStatusOptions"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            />
+          </el-select>
+        </el-form-item>
+      </el-col>
+
+      <el-col v-bind="styleResponsive ? { lg: 5, md: 10 } : { span: 5 }">
+        <el-form-item label="仓库名称:">
+          <el-input clearable v-model="where.warehouseName" placeholder="请输入" />
+        </el-form-item>
+      </el-col>
+
+      <el-col v-bind="styleResponsive ? { lg: 10, md: 24 } : { span: 10 }">
+        <el-form-item label="创建时间:">
+          <el-date-picker
+            v-model="dateRange"
+            type="datetimerange"
+            range-separator="至"
+            start-placeholder="开始时间"
+            end-placeholder="结束时间"
+            value-format="yyyy-MM-dd HH:mm:ss"
+            clearable
+          />
+        </el-form-item>
+      </el-col>
+
+      <el-col
+        style="display: flex; justify-content: flex-end"
+        v-bind="styleResponsive ? { lg: 5, md: 24 } : { span: 5 }"
+      >
+        <el-form-item label-width="0px">
+          <el-button
+            type="primary"
+            icon="el-icon-search"
+            class="ele-btn-icon"
+            @click="search"
+          >
+            查询
+          </el-button>
+          <el-button @click="reset" icon="el-icon-refresh-left" type="primary">
+            重置
+          </el-button>
+        </el-form-item>
+      </el-col>
+    </el-row>
+  </el-form>
+</template>
+
+<script>
+  import {
+    disposalTypeOptions,
+    approvalStatusOptions,
+    storageStatusOptions
+  } from '@/utils/dict/disposalStorage';
+
+  export default {
+    props: [],
+    data() {
+      // 默认表单数据
+      const defaultWhere = {
+        code: '',
+        sourceDetailCode: '',
+        sourceBillCode: '',
+        categoryName: '',
+        categoryCode: '',
+        batchNo: '',
+        disposalType: null,
+        approvalStatus: null,
+        status: null,
+        warehouseName: ''
+      };
+      return {
+        // 表单数据
+        where: { ...defaultWhere },
+        dateRange: [],
+        defaultWhere,
+        disposalTypeOptions,
+        approvalStatusOptions,
+        storageStatusOptions
+      };
+    },
+    computed: {
+      // 是否开启响应式布局
+      styleResponsive() {
+        return this.$store.state.theme.styleResponsive;
+      }
+    },
+    methods: {
+      /* 搜索 */
+      search() {
+        const where = { ...this.where };
+        if (this.dateRange && this.dateRange.length === 2) {
+          where.startTime = this.dateRange[0];
+          where.endTime = this.dateRange[1];
+        } else {
+          where.startTime = null;
+          where.endTime = null;
+        }
+        this.$emit('search', where);
+      },
+      /* 重置 */
+      reset() {
+        this.where = { ...this.defaultWhere };
+        this.dateRange = [];
+        this.search();
+      }
+    }
+  };
+</script>
+
+<style lang="scss" scoped></style>

+ 334 - 0
src/views/disposalStorage/components/storage.vue

@@ -0,0 +1,334 @@
+<!-- 处置入库单 入库执行弹窗 -->
+<template>
+  <el-dialog
+    title="处置入库执行"
+    :visible.sync="visible"
+    width="1100px"
+    :close-on-click-modal="false"
+    append-to-body
+    @closed="handleClosed"
+  >
+    <div v-loading="loading">
+      <el-descriptions :column="3" border size="small" class="doc-info">
+        <el-descriptions-item label="单号">{{ row.code }}</el-descriptions-item>
+        <el-descriptions-item label="申请类型">{{ disposalTypeText }}</el-descriptions-item>
+        <el-descriptions-item label="入库仓库">{{ row.warehouseName }}</el-descriptions-item>
+        <el-descriptions-item label="物品编码">{{ row.categoryCode }}</el-descriptions-item>
+        <el-descriptions-item label="物品名称">{{ row.categoryName }}</el-descriptions-item>
+        <el-descriptions-item label="规格型号">
+          {{ row.specification }} / {{ row.modelType }}
+        </el-descriptions-item>
+      </el-descriptions>
+
+      <el-alert
+        class="batch-tip"
+        type="info"
+        show-icon
+        :closable="false"
+        title="批次号已按处置来源批次预填,入库前请核对;批次号为必填项,修改后将以填写的批次号写入库存台账。"
+      />
+
+      <el-table :data="detailList" border size="small" class="detail-table">
+        <el-table-column type="index" label="序号" width="55" align="center" />
+        <el-table-column prop="categoryName" label="物品名称" min-width="120" show-overflow-tooltip />
+        <el-table-column label="批次号" width="180">
+          <template slot-scope="scope">
+            <el-input
+              v-model="scope.row.batchNo"
+              size="mini"
+              placeholder="请输入批次号"
+              maxlength="50"
+            />
+          </template>
+        </el-table-column>
+        <el-table-column prop="systemBatchNo" label="系统批次号" width="130" show-overflow-tooltip />
+        <el-table-column label="包装数量" width="90" align="center">
+          <template slot-scope="scope">
+            {{ scope.row.packingCount }} {{ scope.row.packingUnit }}
+          </template>
+        </el-table-column>
+        <el-table-column label="计量数量" width="110" align="center">
+          <template slot-scope="scope">
+            {{ scope.row.quantity }} {{ scope.row.measuringUnit }}
+          </template>
+        </el-table-column>
+        <el-table-column label="重量" width="110" align="center">
+          <template slot-scope="scope">
+            {{ scope.row.packingWeight }} {{ scope.row.weightUnit }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="warehouseName" label="入库仓库" width="110" show-overflow-tooltip />
+        <el-table-column label="库位" width="230" required>
+          <template slot-scope="scope">
+            <div class="house-cell" @click="chooseHouse(scope.$index)">
+              <span v-if="scope.row.house">{{ scope.row.house.position }}</span>
+              <span v-else class="house-required">---(必填)</span>
+            </div>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <el-form :model="form" label-width="80px" size="small" class="remark-form">
+        <el-form-item label="备注:">
+          <el-input
+            v-model="form.remark"
+            type="textarea"
+            :rows="2"
+            maxlength="200"
+            show-word-limit
+            placeholder="请输入入库备注"
+          />
+        </el-form-item>
+      </el-form>
+    </div>
+
+    <wareHouseDialog ref="wareHouseDialogRef" @confirm="onHouseConfirm" />
+
+    <div slot="footer">
+      <el-button @click="visible = false">取消</el-button>
+      <el-button type="primary" :loading="isLoading" @click="submit">确认入库</el-button>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+  import storageApi from '@/api/warehouseManagement';
+  import { getDisposalStorageById, getInStorageBySourceBizNo, ledgerCallback } from '@/api/disposalStorage';
+  import { disposalTypeLabel } from '@/utils/dict/disposalStorage';
+  import wareHouseDialog from './wareHouseDialog.vue';
+
+  export default {
+    name: 'disposalStorageExecution',
+    components: { wareHouseDialog },
+    data() {
+      return {
+        visible: false,
+        loading: false,
+        isLoading: false,
+        row: {},
+        detailList: [],
+        form: {
+          remark: ''
+        }
+      };
+    },
+    computed: {
+      disposalTypeText() {
+        return disposalTypeLabel(this.row.disposalType);
+      }
+    },
+    methods: {
+      /**
+       * 打开入库弹窗
+       * @param {Object} row 列表行数据
+       */
+      async open(row) {
+        this.row = row || {};
+        this.detailList = (this.row.detailList || []).map((item) => ({ ...item, house: null }));
+        this.form.remark = '';
+        this.visible = true;
+        this.loading = true;
+        try {
+          const detail = await getDisposalStorageById(this.row.id);
+          if (detail) {
+            this.row = { ...this.row, ...detail };
+            this.detailList = (detail.detailList || []).map((item) => ({ ...item, house: null }));
+          }
+        } catch (e) {
+          this.$message.error(e.message || '获取处置入库单明细失败');
+        } finally {
+          this.loading = false;
+        }
+      },
+      /**
+       * 组装库存入库报文
+       */
+      buildPayload() {
+        const detailList = this.detailList.map((item) => {
+          const batchNo = (item.batchNo || '').trim();
+          const measureQuantity = Number(item.quantity || 0);
+          const packingQuantity = Number(item.packingCount || 0);
+          const weight = Number(item.packingWeight || 0);
+          // 库位在入库弹窗中手工选择,优先取所选库位对应的仓库
+          const house = item.house || {};
+          const warehouseId = house.warehouseId || item.warehouseId || this.row.warehouseId;
+          const warehouseName = house.warehouseName || item.warehouseName || this.row.warehouseName;
+          const record = {
+            id: 0,
+            outInId: 0,
+            outInDetailId: 0,
+            outType: 1,
+            categoryId: item.categoryId,
+            batchNo,
+            systemBatchNo: item.systemBatchNo,
+            packingQuantity,
+            packingUnit: item.packingUnit,
+            measureQuantity,
+            measureUnit: item.measuringUnit,
+            weight,
+            weightUnit: item.weightUnit,
+            warehouseId,
+            warehouseName,
+            // 库区/货架/货位,缺失会导致入库台账无库位
+            areaId: house.areaId,
+            areaName: house.areaName,
+            goodsShelfId: house.shelfId,
+            goodsShelfName: house.shelfCode,
+            goodsAllocationId: house.cargoSpaceId,
+            goodsAllocationName: house.cargoSpaceCode,
+            supplierCode: item.supplierCode,
+            result: 2,
+            status: 2
+          };
+          return {
+            id: 0,
+            outInId: 0,
+            outType: 1,
+            categoryId: item.categoryId,
+            categoryCode: item.categoryCode,
+            categoryName: item.categoryName,
+            categoryModel: item.modelType || item.specification || '-',
+            batchNo,
+            systemBatchNo: item.systemBatchNo,
+            minPackingQuantity: packingQuantity,
+            packingQuantity,
+            packingUnit: item.packingUnit,
+            measureQuantity,
+            measureUnit: item.measuringUnit,
+            singleWeight: packingQuantity > 0 ? weight / packingQuantity : weight,
+            weight,
+            weightUnit: item.weightUnit,
+            warehouseId,
+            warehouseName,
+            supplierId: item.supplierId,
+            supplierCode: item.supplierCode,
+            supplierName: item.supplierName,
+            qualityControl: 2,
+            result: 2,
+            outInDetailRecordRequestList: [record]
+          };
+        });
+        const warehouseIds = [];
+        detailList.forEach((item) => {
+          if (item.warehouseId && warehouseIds.indexOf(item.warehouseId) === -1) {
+            warehouseIds.push(item.warehouseId);
+          }
+        });
+        return {
+          type: 1,
+          // 生产入库
+          bizType: 1,
+          sourceBizNo: this.row.code,
+          // 处置入库单已在生产入库申请审批流中审批通过,跳过仓储内部审批直接增加库存
+          isSkip: 1,
+          remark: this.form.remark,
+          extInfo: { assetType: this.row.rootCategoryLevelId },
+          warehouseIds,
+          outInDetailList: detailList
+        };
+      },
+      /** 选择库位 */
+      chooseHouse(index) {
+        this.$refs.wareHouseDialogRef.open(index, this.detailList[index]);
+      },
+      /** 库位选择回填 */
+      onHouseConfirm(house, index) {
+        const row = this.detailList[index];
+        this.$set(row, 'house', house);
+        // 仓库以所选库位为准,同步回表格避免展示与实际入库仓库不一致
+        this.$set(row, 'warehouseId', house.warehouseId);
+        this.$set(row, 'warehouseName', house.warehouseName);
+      },
+      /** 入库校验 */
+      validate() {
+        if (!this.detailList || this.detailList.length === 0) {
+          this.$message.error('该单据没有可入库的明细!');
+          return false;
+        }
+        for (let i = 0; i < this.detailList.length; i++) {
+          const item = this.detailList[i];
+          if (!(item.batchNo || '').trim()) {
+            this.$message.error(`第 ${i + 1} 行明细的批次号不能为空!`);
+            return false;
+          }
+          if (!(Number(item.quantity || 0) > 0)) {
+            this.$message.error(`第 ${i + 1} 行明细的计量数量必须大于 0!`);
+            return false;
+          }
+          if (!(item.warehouseId || this.row.warehouseId)) {
+            this.$message.error(`第 ${i + 1} 行明细未指定入库仓库!`);
+            return false;
+          }
+          if (!item.house || !item.house.cargoSpaceId) {
+            this.$message.error(`第 ${i + 1} 行明细未选择库位!`);
+            return false;
+          }
+        }
+        return true;
+      },
+      /** 提交入库 */
+      async submit() {
+        if (!this.validate()) {
+          return;
+        }
+        this.isLoading = true;
+        try {
+          // 入库前复核单据状态:已入库的不再重复入库,只补做台账回写,避免重复增加库存
+          const latest = await getDisposalStorageById(this.row.id);
+          let outInId = latest ? latest.outInId : this.row.outInId;
+          const stored = latest ? latest.status === 1 : this.row.status === 1;
+          if (!stored) {
+            // 1. 写入库存台账,返回实际入库单ID;同一业务单号重复提交时库存侧幂等,可能返回空
+            const inStorageResult = await storageApi.storage(this.buildPayload());
+            const outInIds = inStorageResult ? inStorageResult.data : null;
+            if (Array.isArray(outInIds) && outInIds.length > 0) {
+              outInId = outInIds[0];
+            }
+            // 库存侧已存在同来源单的入库记录时补齐入库单ID,避免本次入库被幂等拦截后无法回写台账
+            if (!outInId) {
+              const existed = await getInStorageBySourceBizNo(this.row.code);
+              outInId = existed ? existed.id : undefined;
+            }
+          }
+          // 2. 更新单据状态为已入库并回写处置台账(报废品/试销品),服务端会校验实际入库单号
+          await ledgerCallback(this.row.id, outInId);
+          this.$message.success('入库成功!');
+          this.visible = false;
+          this.$emit('success');
+        } catch (e) {
+          this.$message.error(e.message || '入库失败');
+        } finally {
+          this.isLoading = false;
+        }
+      },
+      handleClosed() {
+        this.form.remark = '';
+      }
+    }
+  };
+</script>
+
+<style lang="scss" scoped>
+  .doc-info {
+    margin-bottom: 12px;
+  }
+  .batch-tip {
+    margin-bottom: 12px;
+  }
+  .detail-table {
+    margin-bottom: 12px;
+  }
+  .remark-form {
+    margin-bottom: 0;
+  }
+  .house-cell {
+    cursor: pointer;
+    color: #606266;
+    &:hover {
+      color: #409eff;
+    }
+  }
+  .house-required {
+    color: #ff4949;
+  }
+</style>

+ 164 - 0
src/views/disposalStorage/components/tgDetails.vue

@@ -0,0 +1,164 @@
+<!-- 处置入库单 明细抽屉 -->
+<template>
+  <el-drawer
+    :visible.sync="visible"
+    :title="`处置入库单明细 - ${row.code || ''}`"
+    direction="rtl"
+    size="720px"
+    :wrapper-closable="true"
+    append-to-body
+  >
+    <div class="details-body" v-loading="loading">
+      <el-form label-width="100px" size="small">
+        <el-row :gutter="12">
+          <el-col :span="12">
+            <el-form-item label="单号:">
+              <el-input v-model="row.code" disabled />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="申请类型:">
+              <el-input v-model="disposalTypeText" disabled />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="明细编码:">
+              <el-input v-model="row.sourceDetailCode" disabled />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="台账编号:">
+              <el-input v-model="row.unqualifiedProductsCode" disabled />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="物品编码:">
+              <el-input v-model="row.categoryCode" disabled />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="物品名称:">
+              <el-input v-model="row.categoryName" disabled />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="物品分类:">
+              <el-input v-model="row.categoryLevelName" disabled />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="入库仓库:">
+              <el-input v-model="row.warehouseName" disabled />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="规格:">
+              <el-input v-model="row.specification" disabled />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="型号:">
+              <el-input v-model="row.modelType" disabled />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="牌号:">
+              <el-input v-model="row.brandNum" disabled />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="创建时间:">
+              <el-input v-model="row.createTime" disabled />
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+
+      <div class="detail-title">入库明细</div>
+      <el-table
+        :data="detailList"
+        border
+        size="small"
+        height="calc(100vh - 420px)"
+      >
+        <el-table-column type="index" label="序号" width="55" align="center" />
+        <el-table-column prop="batchNo" label="批次号" min-width="110" show-overflow-tooltip />
+        <el-table-column prop="systemBatchNo" label="系统批次号" min-width="110" show-overflow-tooltip />
+        <el-table-column prop="packingCount" label="包装数量" width="90" align="center" />
+        <el-table-column prop="packingUnit" label="包装单位" width="90" align="center" />
+        <el-table-column label="计量数量" width="110" align="center">
+          <template slot-scope="scope">
+            {{ scope.row.quantity }} {{ scope.row.measuringUnit }}
+          </template>
+        </el-table-column>
+        <el-table-column label="重量" width="110" align="center">
+          <template slot-scope="scope">
+            {{ scope.row.packingWeight }} {{ scope.row.weightUnit }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="supplierName" label="供应商" min-width="110" show-overflow-tooltip />
+      </el-table>
+    </div>
+  </el-drawer>
+</template>
+
+<script>
+  import { getDisposalStorageById } from '@/api/disposalStorage';
+  import { disposalTypeLabel } from '@/utils/dict/disposalStorage';
+
+  export default {
+    data() {
+      return {
+        visible: false,
+        loading: false,
+        row: {},
+        detailList: []
+      };
+    },
+    computed: {
+      disposalTypeText() {
+        return disposalTypeLabel(this.row.disposalType);
+      }
+    },
+    methods: {
+      /**
+       * 打开明细抽屉
+       * @param {Object} row 列表行数据
+       */
+      async open(row) {
+        this.row = row || {};
+        this.detailList = (this.row.detailList || []).slice();
+        this.visible = true;
+        this.loading = true;
+        try {
+          const detail = await getDisposalStorageById(this.row.id);
+          if (detail) {
+            this.row = { ...this.row, ...detail };
+            this.detailList = (detail.detailList || []).slice();
+          }
+        } catch (e) {
+          this.$message.error(e.message || '获取处置入库单明细失败');
+        } finally {
+          this.loading = false;
+        }
+      },
+      handleClose() {
+        this.visible = false;
+      }
+    }
+  };
+</script>
+
+<style lang="scss" scoped>
+  .details-body {
+    padding: 0 20px 20px 20px;
+  }
+  .detail-title {
+    font-size: 14px;
+    font-weight: 600;
+    color: #303133;
+    margin: 4px 0 10px 0;
+    padding-left: 10px;
+    border-left: 3px solid #1890ff;
+  }
+</style>

+ 308 - 0
src/views/disposalStorage/components/wareHouseDialog.vue

@@ -0,0 +1,308 @@
+<!-- 处置入库 库位选择弹窗 -->
+<template>
+  <el-dialog
+    title="选择库位"
+    :visible.sync="visible"
+    width="960px"
+    append-to-body
+    @closed="handleClosed"
+  >
+    <el-form :model="form" label-width="80px" size="small">
+      <el-row>
+        <el-col :span="8">
+          <el-form-item label="选择仓库">
+            <el-select
+              filterable
+              v-model="warehouseId"
+              placeholder="请选择仓库"
+              style="width: 100%"
+              @change="warehouseChange"
+            >
+              <el-option
+                v-for="item in warehouseList"
+                :key="item.id"
+                :label="item.name"
+                :value="item.id"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="8">
+          <el-form-item label="选择库区">
+            <el-select
+              filterable
+              v-model="areaId"
+              placeholder="请选择库区"
+              style="width: 100%"
+              :disabled="!warehouseId"
+              @change="areaChange"
+            >
+              <el-option
+                v-for="item in areaList"
+                :key="item.id"
+                :label="item.name"
+                :value="item.id"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="8">
+          <el-form-item label="选择货架">
+            <el-select
+              filterable
+              v-model="shelfId"
+              placeholder="请选择货架"
+              style="width: 100%"
+              :disabled="!areaId"
+              @change="shelfChange"
+            >
+              <el-option
+                v-for="item in shelfList"
+                :key="item.id"
+                :label="item.goodsshelvesCode"
+                :value="item.id"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </el-form>
+
+    <div class="status-legend">
+      <div v-for="item in locationStatus" :key="item.code">
+        <span :style="{ 'background-color': item.color }"></span>{{ item.label }}
+      </div>
+    </div>
+
+    <div class="content-box" v-loading="loading">
+      <div
+        v-for="p in locationList"
+        :key="p.id"
+        class="box"
+        :class="{ active: selected && selected.cargoSpaceId == p.id }"
+        :style="{
+          'background-color': statusColor(p.allocationStatus),
+          cursor: isSelectable(p.allocationStatus) ? 'pointer' : 'not-allowed'
+        }"
+        @click="selectLocation(p)"
+      >
+        {{ p.goodsAllocationCode }}
+      </div>
+      <div v-if="!locationList.length" class="empty-tip">
+        请选择仓库、库区、货架后选取货位
+      </div>
+    </div>
+
+    <div class="selected-tip" v-if="selected">
+      已选库位:{{ selected.position }}
+    </div>
+
+    <div slot="footer">
+      <el-button @click="visible = false">取消</el-button>
+      <el-button type="primary" @click="confirm">确认</el-button>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+  import warehouseDefinition from '@/api/warehouseManagement/warehouseDefinition';
+
+  export default {
+    name: 'disposalStorageHouseDialog',
+    data() {
+      return {
+        visible: false,
+        loading: false,
+        index: -1,
+        row: {},
+        warehouseId: '',
+        areaId: '',
+        shelfId: '',
+        warehouseList: [],
+        areaList: [],
+        shelfList: [],
+        locationList: [],
+        selected: null,
+        form: {},
+        locationStatus: [
+          { code: 1, label: '空置', color: 'rgba(202, 249, 130, 1)' },
+          { code: 2, label: '在用', color: 'rgba(129, 211, 248, 1)' },
+          { code: 3, label: '已满', color: 'rgba(255, 14, 14, 1)' },
+          { code: 4, label: '失效', color: 'rgba(215, 215, 215, 1)' }
+        ]
+      };
+    },
+    methods: {
+      /**
+       * 打开库位选择
+       * @param {Number} index 明细行下标
+       * @param {Object} row 明细行数据
+       */
+      async open(index, row) {
+        this.index = index;
+        this.row = row || {};
+        this.selected = null;
+        this.areaId = '';
+        this.shelfId = '';
+        this.areaList = [];
+        this.shelfList = [];
+        this.locationList = [];
+        this.visible = true;
+        this.loading = true;
+        try {
+          if (!this.warehouseList.length) {
+            const res = await warehouseDefinition.list({});
+            this.warehouseList = (res || []).map((item) => ({
+              ...item,
+              name: item.factoryName ? `${item.factoryName}-${item.name}` : item.name
+            }));
+          }
+          // 默认定位到明细的入库仓库,减少一次选择
+          this.warehouseId = this.row.warehouseId || '';
+          if (this.warehouseId) {
+            this.areaList = (await warehouseDefinition.getListByWarehouseId(this.warehouseId)) || [];
+          }
+        } catch (e) {
+          this.$message.error(e.message || '获取仓库信息失败');
+        } finally {
+          this.loading = false;
+        }
+      },
+      async warehouseChange(id) {
+        this.areaId = '';
+        this.shelfId = '';
+        this.areaList = [];
+        this.shelfList = [];
+        this.locationList = [];
+        this.selected = null;
+        if (!id) {
+          return;
+        }
+        this.loading = true;
+        try {
+          this.areaList = (await warehouseDefinition.getListByWarehouseId(id)) || [];
+        } finally {
+          this.loading = false;
+        }
+      },
+      async areaChange(id) {
+        this.shelfId = '';
+        this.shelfList = [];
+        this.locationList = [];
+        this.selected = null;
+        if (!id) {
+          return;
+        }
+        this.loading = true;
+        try {
+          this.shelfList = (await warehouseDefinition.getListByAreaId(id)) || [];
+        } finally {
+          this.loading = false;
+        }
+      },
+      async shelfChange(id) {
+        this.locationList = [];
+        this.selected = null;
+        if (!id) {
+          return;
+        }
+        this.loading = true;
+        try {
+          this.locationList = (await warehouseDefinition.getListByGoodId(id)) || [];
+        } finally {
+          this.loading = false;
+        }
+      },
+      isSelectable(status) {
+        return [1, 2].includes(status);
+      },
+      statusColor(status) {
+        const item = this.locationStatus.find((i) => i.code == status);
+        return item ? item.color : 'rgba(215, 215, 215, 1)';
+      },
+      selectLocation(location) {
+        if (!this.isSelectable(location.allocationStatus)) {
+          return this.$message.warning('该货位不可用,请选择空置或在用的货位');
+        }
+        const warehouse = this.warehouseList.find((i) => i.id == this.warehouseId) || {};
+        const area = this.areaList.find((i) => i.id == this.areaId) || {};
+        const shelf = this.shelfList.find((i) => i.id == this.shelfId) || {};
+        const position = `${warehouse.name || ''}-${area.name || ''}-${
+          shelf.goodsshelvesCode || ''
+        }-${location.goodsAllocationCode || ''}`;
+        this.selected = {
+          warehouseId: warehouse.id,
+          warehouseName: warehouse.name,
+          areaId: area.id,
+          areaName: area.name,
+          shelfId: shelf.id,
+          shelfCode: shelf.goodsshelvesCode,
+          cargoSpaceId: location.id,
+          cargoSpaceCode: location.goodsAllocationCode,
+          position
+        };
+      },
+      confirm() {
+        if (!this.selected) {
+          return this.$message.warning('请选择库位');
+        }
+        this.$emit('confirm', this.selected, this.index);
+        this.visible = false;
+      },
+      handleClosed() {
+        this.index = -1;
+        this.row = {};
+        this.selected = null;
+      }
+    }
+  };
+</script>
+
+<style lang="scss" scoped>
+  .status-legend {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    margin: 8px 0;
+    div {
+      margin: 0 12px;
+    }
+    span {
+      display: inline-block;
+      width: 13px;
+      height: 13px;
+      margin: 0 4px;
+    }
+  }
+  .content-box {
+    display: flex;
+    justify-content: flex-start;
+    flex-wrap: wrap;
+    max-height: 40vh;
+    min-height: 120px;
+    overflow: auto;
+    .box {
+      cursor: pointer;
+      width: 134px;
+      height: 64px;
+      background-clip: content-box;
+      margin: 0 4px 8px 0;
+      line-height: 64px;
+      text-align: center;
+      &:hover,
+      &.active {
+        box-shadow: 2px 2px 2px 2px #ccc;
+      }
+    }
+    .empty-tip {
+      width: 100%;
+      line-height: 120px;
+      text-align: center;
+      color: #909399;
+    }
+  }
+  .selected-tip {
+    margin-top: 8px;
+    color: #409eff;
+  }
+</style>

+ 357 - 0
src/views/disposalStorage/index.vue

@@ -0,0 +1,357 @@
+<template>
+  <div class="ele-body">
+    <el-card shadow="never" v-loading="loading">
+      <search @search="reload" ref="searchRef"> </search>
+
+      <!-- 数据表格 -->
+      <ele-pro-table
+        ref="table"
+        :columns="columns"
+        :datasource="datasource"
+        cache-key="disposalStorageIndex"
+        :pageSize="20"
+        :height="tableHeight"
+        @fullscreen-change="fullscreenChange"
+      >
+        <template v-slot:code="{ row }">
+          <el-link type="primary" :underline="false" @click="details(row)">
+            {{ row.code }}
+          </el-link>
+        </template>
+
+        <template v-slot:disposalType="{ row }">
+          <el-tag
+            :type="row.disposalType === 9 ? 'warning' : 'danger'"
+            size="mini"
+            effect="light"
+          >
+            {{ disposalTypeLabel(row.disposalType) }}
+          </el-tag>
+        </template>
+
+        <template v-slot:batchNos="{ row }">
+          {{ row.batchNos || row.batchNo }}
+        </template>
+
+        <template v-slot:totalCount="{ row }">
+          {{ row.totalCount }} {{ row.measuringUnit }}
+        </template>
+
+        <template v-slot:totalPackage="{ row }">
+          {{ row.totalPackage
+          }}{{
+            row.detailList && row.detailList.length
+              ? row.detailList[0].packingUnit
+              : ''
+          }}
+        </template>
+
+        <template v-slot:totalWeight="{ row }">
+          {{ row.totalWeight }} {{ row.weightUnit }}
+        </template>
+
+        <template v-slot:approvalStatus="{ row }">
+          <span :class="approvalStatusClass(row.approvalStatus)">
+            {{ approvalStatusLabel(row.approvalStatus) }}
+          </span>
+        </template>
+
+        <template v-slot:status="{ row }">
+          <span :class="row.status == 1 ? 'ele-text-success' : 'ele-text-warning'">
+            {{ storageStatusLabel(row.status) }}
+          </span>
+        </template>
+
+        <template v-slot:action="{ row }">
+          <el-link
+            type="primary"
+            :underline="false"
+            v-if="canStorage(row)"
+            @click="storage(row)"
+          >
+            入库
+          </el-link>
+
+          <el-link
+            type="primary"
+            :underline="false"
+            @click="handleFlow(row)"
+            v-if="row.processInstanceId"
+          >
+            流程
+          </el-link>
+        </template>
+      </ele-pro-table>
+    </el-card>
+
+    <tgDetails ref="tgDetailsRefs"></tgDetails>
+    <flow ref="flowRef"></flow>
+    <storage ref="storageRefs" @success="reload"></storage>
+  </div>
+</template>
+
+<script>
+  import { getDisposalStoragePage } from '@/api/disposalStorage';
+  import {
+    approvalStatusLabel,
+    disposalTypeLabel,
+    storageStatusLabel
+  } from '@/utils/dict/disposalStorage';
+  import search from './components/search.vue';
+  import tgDetails from './components/tgDetails.vue';
+  import flow from './components/flow.vue';
+  import storage from './components/storage.vue';
+
+  export default {
+    components: {
+      search,
+      tgDetails,
+      flow,
+      storage
+    },
+    data() {
+      return {
+        loading: false,
+        tableHeight: 'calc(100vh - 300px)'
+      };
+    },    computed: {
+      // 表格列配置
+      columns() {
+        return [
+          {
+            columnKey: 'index',
+            label: '序号',
+            type: 'index',
+            width: 55,
+            align: 'center',
+            showOverflowTooltip: true,
+            fixed: 'left'
+          },
+          {
+            prop: 'code',
+            slot: 'code',
+            label: '单号',
+            align: 'center',
+            minWidth: 140,
+            showOverflowTooltip: true
+          },
+          {
+            prop: 'sourceDetailCode',
+            label: '不良品明细编码',
+            align: 'center',
+            minWidth: 150,
+            showOverflowTooltip: true
+          },
+          {
+            prop: 'unqualifiedProductsCode',
+            label: '不良品台账编号',
+            align: 'center',
+            minWidth: 150,
+            showOverflowTooltip: true
+          },
+          {
+            prop: 'disposalType',
+            slot: 'disposalType',
+            label: '申请类型',
+            align: 'center',
+            minWidth: 100,
+            showOverflowTooltip: true
+          },
+          {
+            prop: 'categoryCode',
+            label: '物品编码',
+            align: 'center',
+            minWidth: 110,
+            showOverflowTooltip: true
+          },
+          {
+            prop: 'categoryName',
+            label: '物品名称',
+            align: 'center',
+            minWidth: 130,
+            showOverflowTooltip: true
+          },
+          {
+            prop: 'categoryLevelName',
+            label: '物品分类',
+            align: 'center',
+            minWidth: 110,
+            showOverflowTooltip: true
+          },
+          {
+            prop: 'specification',
+            label: '规格',
+            align: 'center',
+            minWidth: 120,
+            showOverflowTooltip: true
+          },
+          {
+            prop: 'modelType',
+            label: '型号',
+            align: 'center',
+            minWidth: 100,
+            showOverflowTooltip: true
+          },
+          {
+            prop: 'brandNum',
+            label: '牌号',
+            align: 'center',
+            minWidth: 90,
+            showOverflowTooltip: true
+          },
+          {
+            prop: 'batchNos',
+            slot: 'batchNos',
+            label: '批次号',
+            align: 'center',
+            minWidth: 120,
+            showOverflowTooltip: true
+          },
+          {
+            prop: 'totalCount',
+            slot: 'totalCount',
+            label: '数量',
+            align: 'center',
+            minWidth: 110,
+            showOverflowTooltip: true
+          },
+          {
+            prop: 'totalPackage',
+            slot: 'totalPackage',
+            label: '总包装',
+            align: 'center',
+            minWidth: 90,
+            showOverflowTooltip: true
+          },
+          {
+            prop: 'totalWeight',
+            slot: 'totalWeight',
+            label: '总重量',
+            align: 'center',
+            minWidth: 100,
+            showOverflowTooltip: true
+          },
+          {
+            prop: 'warehouseName',
+            label: '入库仓库',
+            align: 'center',
+            minWidth: 120,
+            showOverflowTooltip: true
+          },
+          {
+            prop: 'approvalStatus',
+            slot: 'approvalStatus',
+            label: '审批状态',
+            align: 'center',
+            minWidth: 100,
+            showOverflowTooltip: true
+          },
+          {
+            prop: 'status',
+            slot: 'status',
+            label: '单据状态',
+            align: 'center',
+            minWidth: 100,
+            showOverflowTooltip: true
+          },
+          {
+            prop: 'approvalUserName',
+            label: '审核人',
+            align: 'center',
+            minWidth: 90,
+            showOverflowTooltip: true
+          },
+          {
+            prop: 'executorName',
+            label: '执行人',
+            align: 'center',
+            minWidth: 100,
+            showOverflowTooltip: true
+          },
+          {
+            prop: 'createTime',
+            label: '创建时间',
+            align: 'center',
+            minWidth: 150,
+            showOverflowTooltip: true
+          },
+          {
+            columnKey: 'action',
+            label: '操作',
+            width: 120,
+            align: 'center',
+            resizable: false,
+            fixed: 'right',
+            slot: 'action',
+            showOverflowTooltip: true
+          }
+        ];
+      }
+    },
+    methods: {
+      disposalTypeLabel,
+      approvalStatusLabel,
+      storageStatusLabel,
+      /**
+       * 审批状态语义色
+       */
+      approvalStatusClass(status) {
+        if (status == 1) {
+          return 'ele-text-warning';
+        }
+        if (status == 2) {
+          return 'ele-text-success';
+        }
+        if (status == 3) {
+          return 'ele-text-danger';
+        }
+        return '';
+      },
+      /**
+       * 是否允许执行入库:审批通过且单据未入库。
+       * 审批通过是入库前置条件,入库动作完成即单据终态(审批通过 + 已入库)。
+       */
+      canStorage(row) {
+        return row.approvalStatus == 2 && row.status == 0 && !row.outInId;
+      },
+      /* 表格数据源 */
+      datasource({ page, limit, where }) {
+        return getDisposalStoragePage({
+          pageNum: page,
+          size: limit,
+          ...where
+        });
+      },
+      /* 明细 */
+      details(row) {
+        this.$refs.tgDetailsRefs.open(row);
+      },
+      /* 入库 */
+      storage(row) {
+        this.$refs.storageRefs.open(row);
+      },
+      /* 流程 */
+      handleFlow(row) {
+        if (!row.processInstanceId) {
+          return this.$message.warning('暂无流程图');
+        }
+        this.$refs.flowRef.open(row.processInstanceId);
+      },
+      /* 刷新表格 */
+      reload(where) {
+        this.$nextTick(() => {
+          this.$refs.table.reload({ page: 1, where });
+        });
+      },
+      fullscreenChange(fullscreen) {
+        if (fullscreen) {
+          this.tableHeight = 'calc(100vh - 120px)';
+        } else {
+          this.tableHeight = 'calc(100vh - 300px)';
+        }
+      }
+    }
+  };
+</script>
+
+<style lang="scss" scoped></style>