Selaa lähdekoodia

物品领用审批

ZGC4846 1 viikko sitten
vanhempi
commit
f65f1f551c

+ 9 - 0
src/api/main/index.js

@@ -31,4 +31,13 @@ export async function parameterGetByCode(data) {
     return res.data.data;
   }
   return Promise.reject(new Error(res.data.message));
+}
+
+/** 物品领用单详情查询 GET /main/onboarding-item-receipts/{id} */
+export async function getOnboardingItemReceiptsById(id) {
+  const res = await request.get(`/main/onboarding-item-receipts/${id}`);
+  if (res.data.code == 0) {
+    return res.data.data;
+  }
+  return Promise.reject(new Error(res.data.message));
 }

+ 477 - 0
src/views/bpm/handleTask/components/itemReceipts/itemReceiptsDetail.vue

@@ -0,0 +1,477 @@
+<template>
+  <main class="item-receipts-detail" v-loading="loading">
+    <!-- 员工信息 & 单据头 -->
+    <section class="detail-section">
+      <div class="info-card">
+        <div class="info-card__head">
+          <i class="el-icon-user-solid"></i>
+          <span>员工信息</span>
+          <span v-if="employeeInfo.documentCode" class="info-card__code">
+            单号:{{ employeeInfo.documentCode }}
+          </span>
+        </div>
+        <div class="info-card__body">
+          <div class="info-item">
+            <span class="info-item__label">姓名</span>
+            <span class="info-item__value">{{ employeeInfo.employeeName || '-' }}</span>
+          </div>
+          <div class="info-item">
+            <span class="info-item__label">工号</span>
+            <span class="info-item__value">{{ employeeInfo.jobNumber || '-' }}</span>
+          </div>
+          <div class="info-item">
+            <span class="info-item__label">岗位</span>
+            <span class="info-item__value">{{ employeeInfo.positionName || '-' }}</span>
+          </div>
+          <div class="info-item">
+            <span class="info-item__label">部门</span>
+            <span class="info-item__value">{{ employeeInfo.departmentName || '-' }}</span>
+          </div>
+          <div v-if="employeeInfo.legalEntityName" class="info-item">
+            <span class="info-item__label">所属机构</span>
+            <span class="info-item__value">{{ employeeInfo.legalEntityName }}</span>
+          </div>
+          <div class="info-item">
+            <span class="info-item__label">领用状态</span>
+            <span class="info-item__value">
+              <el-tag :type="receiptStatusTag" size="mini">{{ receiptStatusText }}</el-tag>
+            </span>
+          </div>
+          <div v-if="employeeInfo.createTime" class="info-item">
+            <span class="info-item__label">创建时间</span>
+            <span class="info-item__value">{{ formatDateTime(employeeInfo.createTime) }}</span>
+          </div>
+        </div>
+      </div>
+    </section>
+
+    <!-- 物品领用清单 -->
+    <section class="detail-section">
+      <div class="detail-heading">
+        <i class="el-icon-s-goods"></i>
+        <span>物品领用清单</span>
+        <span v-if="itemList.length" class="detail-heading__count">共 {{ itemList.length }} 项</span>
+      </div>
+
+      <el-table
+        ref="tableRef"
+        :data="itemList"
+        border
+        stripe
+        style="width: 100%"
+        :empty-text="emptyText"
+      >
+        <el-table-column
+          type="index"
+          label="序号"
+          width="60"
+          align="center"
+        />
+        <el-table-column
+          prop="itemName"
+          label="物品名称"
+          min-width="160"
+          show-overflow-tooltip
+        />
+        <el-table-column
+          prop="itemId"
+          label="资产编码"
+          width="140"
+          align="center"
+          show-overflow-tooltip
+        >
+          <template slot-scope="{ row }">
+            {{ row.itemId || '-' }}
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop="quantity"
+          label="数量"
+          width="80"
+          align="center"
+        >
+          <template slot-scope="{ row }">
+            {{ row.quantity != null ? row.quantity : '-' }}
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop="itemUnit"
+          label="单位"
+          width="80"
+          align="center"
+        >
+          <template slot-scope="{ row }">
+            {{ row.itemUnit || '-' }}
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop="specifications"
+          label="规格"
+          width="120"
+          align="center"
+          show-overflow-tooltip
+        >
+          <template slot-scope="{ row }">
+            {{ row.specifications || '-' }}
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop="itemModel"
+          label="型号"
+          width="120"
+          align="center"
+          show-overflow-tooltip
+        >
+          <template slot-scope="{ row }">
+            {{ row.itemModel || '-' }}
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop="itemType"
+          label="物品类型"
+          width="100"
+          align="center"
+        >
+          <template slot-scope="{ row }">
+            {{ row.itemTypeLabel || row.itemType || '-' }}
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop="description"
+          label="描述"
+          min-width="140"
+          show-overflow-tooltip
+        >
+          <template slot-scope="{ row }">
+            {{ row.description || '-' }}
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop="remark"
+          label="备注"
+          min-width="140"
+          show-overflow-tooltip
+        >
+          <template slot-scope="{ row }">
+            {{ row.remark || '-' }}
+          </template>
+        </el-table-column>
+      </el-table>
+    </section>
+  </main>
+</template>
+
+<script>
+import { getOnboardingItemReceiptsById } from '@/api/main';
+import { getEmployeeById } from '@/api/organization';
+
+const emptyList = () => [];
+
+/** 从多种可能的字段名中取值 */
+function pick(row, keys) {
+  if (row == null || typeof row !== 'object') return '';
+  for (let i = 0; i < keys.length; i++) {
+    const key = keys[i];
+    const val = row[key];
+    if (val !== undefined && val !== null && val !== '') return val;
+  }
+  return '';
+}
+
+/** 物品类型 → 中文展示 */
+const ITEM_TYPE_LABEL = {
+  ASSET: '资产类',
+  NON_ASSET: '非资产类'
+};
+
+/** 适配后端物品领用明细 → 前端表格字段 */
+function adaptItem(raw) {
+  const r = raw && typeof raw === 'object' ? raw : {};
+  const itemType = pick(r, ['itemType']);
+  return {
+    ...r,
+    itemName: pick(r, ['itemName']),
+    itemId: pick(r, ['itemId']),                      // 资产编码快照
+    itemModel: pick(r, ['itemModel']),                // 型号
+    itemUnit: pick(r, ['itemUnit']),                  // 单位
+    quantity: pick(r, ['quantity']),
+    specifications: pick(r, ['specifications']),     // 规格
+    itemType,                                         // 原值(ASSET / NON_ASSET)
+    itemTypeLabel: ITEM_TYPE_LABEL[itemType] || itemType, // 中文标签
+    description: pick(r, ['description']),            // 描述
+    remark: pick(r, ['remark'])
+  };
+}
+
+/** 从接口返回的整体结构中提取物品明细数组 */
+function extractItemList(data) {
+  if (data == null) return emptyList();
+  if (Array.isArray(data)) return data;
+  if (typeof data !== 'object') return emptyList();
+  const candidates = [
+    data.details,
+    data.items,
+    data.itemList,
+    data.itemReceipts,
+    data.receiptItems,
+    data.detailList,
+    data.list
+  ];
+  for (let i = 0; i < candidates.length; i++) {
+    if (Array.isArray(candidates[i])) return candidates[i];
+  }
+  return emptyList();
+}
+
+/** 从接口 data 中提取员工与单据信息 */
+function extractEmployeeInfo(data) {
+  if (!data || typeof data !== 'object') return {};
+  return {
+    id: pick(data, ['id']),
+    documentCode: pick(data, ['documentCode', 'receiptCode', 'code']),  // 物品领用单号
+    employeeId: pick(data, ['employeeId']),
+    employeeName: pick(data, ['employeeName']),
+    jobNumber: pick(data, ['jobNumber']),
+    departmentId: pick(data, ['departmentId']),
+    departmentName: pick(data, ['departmentName']),
+    legalEntityId: pick(data, ['legalEntityId']),
+    legalEntityName: pick(data, ['legalEntityName']),
+    positionId: pick(data, ['positionId', 'jobId', 'postId']),
+    positionName: pick(data, ['positionName', 'jobName', 'postName', 'empPosition']),
+    receiptStatus: pick(data, ['receiptStatus', 'status', 'state']),
+    processInstanceId: pick(data, ['processInstanceId']),
+    createTime: pick(data, ['createTime'])
+  };
+}
+
+/** 合并员工档案补查结果(档案字段优先覆盖领用单缺失值) */
+function mergeEmployeeInfo(base, profile) {
+  if (!profile) return base;
+  return {
+    ...base,
+    employeeName: base.employeeName || pick(profile, ['nickname', 'name', 'employeeName']),
+    jobNumber: base.jobNumber || pick(profile, ['jobNumber', 'employeeNo', 'empNo']),
+    departmentId: base.departmentId || pick(profile, ['departmentId', 'deptId']),
+    departmentName: base.departmentName || pick(profile, ['deptName', 'departmentName', 'dept']),
+    legalEntityId: base.legalEntityId || pick(profile, ['legalId', 'legalEntityId', 'legalEntId']),
+    legalEntityName: base.legalEntityName || pick(profile, ['legalName', 'legalEntityName', 'legalEntName']),
+    positionId: base.positionId || pick(profile, ['positionId', 'jobId', 'postId']),
+    // 岗位:以员工档案为准
+    positionName: pick(profile, ['positionName', 'postName', 'jobName', 'position', 'positionNameText']) || base.positionName
+  };
+}
+
+/** receiptStatus → 文案与 el-tag 类型 */
+const STATUS_MAP = {
+  DRAFT: { text: '草稿', tag: 'info' },
+  APPROVING: { text: '审批中', tag: 'warning' },
+  PENDING: { text: '审批中', tag: 'warning' },
+  COMPLETED: { text: '已完成', tag: 'success' },
+  RECEIVED: { text: '已领用', tag: 'success' }
+};
+
+function resolveReceiptStatusText(code) {
+  if (!code) return '';
+  const upper = String(code).toUpperCase();
+  if (STATUS_MAP[upper]) return STATUS_MAP[upper].text;
+  return String(code);
+}
+
+function resolveReceiptStatusTag(code) {
+  if (!code) return 'info';
+  const upper = String(code).toUpperCase();
+  return STATUS_MAP[upper]?.tag || 'info';
+}
+
+export default {
+  name: 'ItemReceiptsDetail',
+  props: {
+    businessId: {
+      default: ''
+    },
+    items: {
+      type: Array,
+      default: null
+    }
+  },
+  data() {
+    return {
+      loading: false,
+      itemList: emptyList(),
+      employeeInfo: {}
+    };
+  },
+  computed: {
+    emptyText() {
+      return '暂无物品领用数据';
+    },
+    receiptStatusText() {
+      return resolveReceiptStatusText(this.employeeInfo?.receiptStatus);
+    },
+    receiptStatusTag() {
+      return resolveReceiptStatusTag(this.employeeInfo?.receiptStatus);
+    }
+  },
+  watch: {
+    items: {
+      immediate: true,
+      handler(val) {
+        if (Array.isArray(val)) {
+          this.itemList = val.map(adaptItem);
+        }
+      }
+    },
+    businessId: {
+      immediate: true,
+      handler(id) {
+        if (Array.isArray(this.items)) return;
+        if (id) this.bootstrap(id);
+      }
+    }
+  },
+  methods: {
+    /** 格式化后端日期字符串(兼容 ISO 格式和毫秒时间戳) */
+    formatDateTime(val) {
+      if (!val) return '';
+      const text = String(val);
+      // 已经是 yyyy-MM-dd HH:mm:ss 格式
+      if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/.test(text)) return text.slice(0, 19);
+      // ISO 格式:2024-05-20T10:30:00
+      const isoMatch = text.match(/^(\d{4}-\d{2}-\d{2})[T ](\d{2}:\d{2}:\d{2})/);
+      if (isoMatch) return `${isoMatch[1]} ${isoMatch[2]}`;
+      // 毫秒时间戳
+      if (/^\d{10,13}$/.test(text)) {
+        const d = new Date(Number(text));
+        if (!isNaN(d.getTime())) {
+          const pad = n => String(n).padStart(2, '0');
+          return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
+        }
+      }
+      return text;
+    },
+    async bootstrap(id) {
+      this.loading = true;
+      try {
+        const data = await getOnboardingItemReceiptsById(id);
+        // 1. 先从领用单接口取基础信息
+        let empInfo = extractEmployeeInfo(data);
+
+        // 2. 用 employeeId 补查 HR 员工档案,拿到岗位等信息(老夫子产线电工这类)
+        if (empInfo.employeeId) {
+          try {
+            const profile = await getEmployeeById(empInfo.employeeId);
+            empInfo = mergeEmployeeInfo(empInfo, profile);
+          } catch (e) {
+            // 补查失败不阻塞主流程
+            if (process.env.NODE_ENV !== 'production') {
+              console.warn('[ItemReceiptsDetail] 员工档案补查失败:', e);
+            }
+          }
+        }
+
+        this.employeeInfo = empInfo;
+        const rawList = extractItemList(data);
+        this.itemList = rawList.map(adaptItem);
+      } catch (error) {
+        this.employeeInfo = {};
+        this.itemList = emptyList();
+        if (process.env.NODE_ENV !== 'production') {
+          console.warn('[ItemReceiptsDetail] 加载物品领用清单失败:', error);
+        }
+      } finally {
+        this.loading = false;
+      }
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.item-receipts-detail {
+  padding: 8px;
+}
+
+.detail-section {
+  margin-bottom: 16px;
+}
+
+.info-card {
+  border: 1px solid #ebeef5;
+  border-radius: 4px;
+  background: #fff;
+  overflow: hidden;
+
+  &__head {
+    display: flex;
+    align-items: center;
+    gap: 6px;
+    padding: 10px 16px;
+    font-size: 14px;
+    font-weight: 600;
+    color: #303133;
+    background: #f5f7fa;
+    border-bottom: 1px solid #ebeef5;
+
+    i {
+      font-size: 16px;
+      color: #409eff;
+    }
+  }
+
+  &__code {
+    margin-left: auto;
+    font-size: 12px;
+    font-weight: normal;
+    color: #606266;
+    font-family: Menlo, Monaco, Consolas, monospace;
+  }
+
+  &__body {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 12px 32px;
+    padding: 16px;
+  }
+}
+
+.info-item {
+  display: flex;
+  align-items: center;
+  min-width: 220px;
+
+  &__label {
+    flex-shrink: 0;
+    width: 72px;
+    color: #909399;
+    font-size: 13px;
+  }
+
+  &__value {
+    color: #303133;
+    font-size: 13px;
+    font-weight: 500;
+  }
+}
+
+.detail-heading {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  margin-bottom: 12px;
+  font-size: 15px;
+  font-weight: 600;
+  color: #303133;
+
+  i {
+    font-size: 16px;
+    color: #409eff;
+  }
+
+  &__count {
+    margin-left: auto;
+    font-size: 12px;
+    font-weight: normal;
+    color: #909399;
+  }
+}
+</style>

+ 165 - 0
src/views/bpm/handleTask/components/itemReceipts/submit.vue

@@ -0,0 +1,165 @@
+<template>
+  <el-col :span="16" :offset="6">
+    <el-form label-width="100px" ref="formRef" :model="form">
+      <el-form-item
+        label="审批建议"
+
+        style="margin-bottom: 20px"
+        :rules="{
+          required: true,
+          message: '请输入',
+          trigger: 'blur'
+        }"
+      >
+        <el-input
+          type="textarea"
+          v-model="form.reason"
+          placeholder="请输入审批建议"
+        />
+      </el-form-item>
+    </el-form>
+    <div style="margin-left: 10%; margin-bottom: 20px; font-size: 14px">
+      <el-button
+        icon="el-icon-edit-outline"
+        type="success"
+        size="mini"
+        v-click-once
+        @click="handleAudit(1)"
+        >通过
+      </el-button>
+      <el-button
+        icon="el-icon-circle-close"
+        type="danger"
+        size="mini"
+        v-click-once
+        @click="handleAudit(0)"
+        >驳回
+      </el-button>
+    </div>
+  </el-col>
+</template>
+
+<script>
+  import { approveTaskWithVariables } from '@/api/bpm/task';
+  import storageApi from '@/api/warehouseManagement';
+  // 流程实例的详情页,可用于审批
+  export default {
+    name: '',
+    props: {
+      businessId: {
+        default: ''
+      },
+      taskId: {
+        default: ''
+      },
+      id: {
+        default: ''
+      },
+      taskDefinitionKey: {
+        default: ''
+      }
+    },
+    data() {
+      return {
+        statusOption: [
+          {
+            label: '盘盈',
+            value: '2',
+            numKey: 'surplusQuantity'
+          },
+          {
+            label: '丢失',
+            value: '3',
+            numKey: 'loseQuantity'
+          },
+          {
+            label: '破损',
+            value: '4',
+            numKey: 'wornQuantity'
+          }
+        ],
+        form: {
+          reason: '同意'
+        },
+        userOptions: []
+      };
+    },
+    methods: {
+      async handleAudit(status) {
+        await this._approveTaskWithVariables(status);
+      },
+      getTableValue() {
+        return new Promise((resolve) => {
+          this.$emit('getTableValue', async (data) => {
+            resolve(await data);
+          });
+        });
+      },
+      async _approveTaskWithVariables(status) {
+        if (!status) {
+          // 报损报溢所有节点驳回后都恢复为可编辑、可重新提交。
+          const data = await storageApi.notProfitLoss({
+            id: this.businessId,
+            reason: this.form.reason,
+            taskId: this.taskId
+          });
+          if (data.data.code != '-1') {
+            this.$emit('handleAudit', {
+              status,
+              title: '驳回'
+            });
+          }
+          return;
+        } else if (this.taskDefinitionKey == 'Activity_1ftg2hu') {
+          // 通过(仓管审核)
+          if (!!status) {
+            console.log('通过了!!!!');
+            let infoData = await this.getTableValue();
+            let params = infoData.info[0].planDetailVOList.map((item) => {
+              let filterArr = this.statusOption.filter(
+                (ite) => ite.value == item.status
+              )[0];
+              console.log(filterArr.numKey);
+              return {
+                id: this.businessId,
+                isAdd: filterArr.numKey == 'surplusQuantity' ? true : false,
+                packingNo: item.packageNo,
+                quantity: item[filterArr.numKey],
+                categoryId: item.categoryId,
+                batchNo: item.batchNo,
+                systemBatchNo: item.systemBatchNo
+              };
+            });
+            console.log('params~~~', params);
+            try {
+              const result = await storageApi.addSubtractStockPacking(params);
+              if (result && result.code !== 0 && result.code !== '0') {
+                throw new Error(result.message || '库存调整失败');
+              }
+            } catch (error) {
+              this.$message.error('保存失败');
+              return;
+            }
+          }
+        }
+        let variables = {
+          pass: !!status
+        };
+        approveTaskWithVariables({
+          id: this.taskId,
+          reason: this.form.reason,
+          variables
+        }).then((res) => {
+          if (res.data.code != '-1') {
+            this.$emit('handleAudit', {
+              status,
+              title: status === 0 ? '驳回' : ''
+            });
+          }
+        });
+      }
+    }
+  };
+</script>
+
+<style lang="scss"></style>

+ 2 - 2
vue.config.js

@@ -38,7 +38,7 @@ module.exports = {
         // target: 'http://124.71.68.31:50001', // 测试环境
         // target: 'http://124.71.68.31:50001',
         // target: 'http://192.168.1.105:18086',
-        target: 'http://localhost:18086',
+        // target: 'http://localhost:18086',
         // target: 'http://192.168.1.116:18086',
         // target: 'http://192.168.1.3:18086',
         // target: 'http://192.168.1.251:18186',
@@ -46,7 +46,7 @@ module.exports = {
         // target: 'http://192.168.1.3:18086',
         // target: 'http://aiot.zoomwin.com.cn:51001/api',
         // target: 'http://192.168.1.3:18086',
-        // target: 'http://192.168.1.147:18086', // 陈兵
+        target: 'http://192.168.1.147:18086', // 陈兵
         // target: 'http://192.168.1.116:18086', // 赵沙金
         // target: 'http://192.168.1.113:18086', // 郭丹