|
|
@@ -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>
|