From a9d97b150701e634bdb751eab277696abd136cca Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期二, 16 六月 2026 14:39:47 +0800
Subject: [PATCH] 君歌app 1.依照web端功能修改

---
 src/pages/oa/ReimburseManage/_components/ReimburseInstanceDetailBody.vue |  426 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 426 insertions(+), 0 deletions(-)

diff --git a/src/pages/oa/ReimburseManage/_components/ReimburseInstanceDetailBody.vue b/src/pages/oa/ReimburseManage/_components/ReimburseInstanceDetailBody.vue
new file mode 100644
index 0000000..0b270f1
--- /dev/null
+++ b/src/pages/oa/ReimburseManage/_components/ReimburseInstanceDetailBody.vue
@@ -0,0 +1,426 @@
+<!--
+  宸梾/璐圭敤鎶ラ攢璇︽儏灞曠ず锛堝垪琛ㄨ鎯� / 瀹℃壒璇︽儏鍏辩敤锛�
+-->
+<template>
+  <view class="rd-body">
+    <!-- 姒傝 -->
+    <view class="rd-hero">
+      <view class="rd-hero-top">
+        <text class="rd-bill-no">{{ billNo }}</text>
+        <text :class="['rd-status', statusCssClass]">{{ statusText }}</text>
+      </view>
+      <text class="rd-reason">{{ reasonText }}</text>
+      <view class="rd-amount-row">
+        <text class="rd-amount-label">鐢宠閲戦</text>
+        <text class="rd-amount">{{ amountText }}</text>
+      </view>
+    </view>
+
+    <!-- 鐢宠浜� -->
+    <view class="rd-section">
+      <view class="rd-section-hd">
+        <text class="rd-section-title">鐢宠浜�</text>
+      </view>
+      <view class="rd-group">
+        <view class="rd-cell">
+          <text class="rd-label">濮撳悕</text>
+          <text class="rd-value">{{ r.applicantName || "鈥�" }}</text>
+        </view>
+        <view class="rd-cell">
+          <text class="rd-label">鍛樺伐缂栧彿</text>
+          <text class="rd-value">{{ r.applicantCode || r.applicantNo || "鈥�" }}</text>
+        </view>
+        <view v-if="r.applicantDeptName || r.deptName"
+              class="rd-cell">
+          <text class="rd-label">閮ㄩ棬</text>
+          <text class="rd-value">{{ r.applicantDeptName || r.deptName }}</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 鍑哄樊 / 璐圭敤 -->
+    <view class="rd-section">
+      <view class="rd-section-hd">
+        <text class="rd-section-title">{{ isTravel ? "鍑哄樊淇℃伅" : "璐圭敤淇℃伅" }}</text>
+      </view>
+      <view class="rd-group">
+        <template v-if="isTravel">
+          <view class="rd-cell">
+            <text class="rd-label">鍑哄樊寮�濮�</text>
+            <text class="rd-value">{{ formatTime(r.travelStartTime) }}</text>
+          </view>
+          <view class="rd-cell">
+            <text class="rd-label">鍑哄樊缁撴潫</text>
+            <text class="rd-value">{{ formatTime(r.travelEndTime) }}</text>
+          </view>
+          <view class="rd-cell">
+            <text class="rd-label">鍑哄樊澶╂暟</text>
+            <text class="rd-value">{{ travelDaysText }}</text>
+          </view>
+          <view class="rd-cell">
+            <text class="rd-label">鍑哄樊鍦�</text>
+            <text class="rd-value">{{ r.departurePlace || "鈥�" }}</text>
+          </view>
+          <view class="rd-cell">
+            <text class="rd-label">鐩殑鍦�</text>
+            <text class="rd-value">{{ r.destination || "鈥�" }}</text>
+          </view>
+        </template>
+        <view v-else
+              class="rd-cell">
+          <text class="rd-label">璐圭敤绫诲瀷</text>
+          <text class="rd-value">{{ expenseTypeText }}</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 宸梾鏍囧噯 -->
+    <view v-if="isTravel && hasTravelStandard"
+          class="rd-section">
+      <view class="rd-section-hd">
+        <text class="rd-section-title">宸梾鏍囧噯</text>
+      </view>
+      <view class="rd-group">
+        <view v-if="r.hotelStandard != null"
+              class="rd-cell">
+          <text class="rd-label">閰掑簵鏍囧噯</text>
+          <text class="rd-value">{{ r.hotelStandard }} 鍏�/鏅�</text>
+        </view>
+        <view v-if="r.hotelDays != null"
+              class="rd-cell">
+          <text class="rd-label">浣忓澶╂暟</text>
+          <text class="rd-value">{{ r.hotelDays }} 澶�</text>
+        </view>
+        <view v-if="r.livingSubsidy != null"
+              class="rd-cell">
+          <text class="rd-label">鐢熸椿琛ヨ创</text>
+          <text class="rd-value">{{ r.livingSubsidy }} 鍏�</text>
+        </view>
+        <view class="rd-cell">
+          <text class="rd-label">鏍囧噯鏍囪</text>
+          <text class="rd-value">{{ r.standardTag || (r.needSpecialApproval ? "瓒呮敮闇�鐗规壒" : "鍦ㄦ爣鍑嗗唴") }}</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 鏀舵 -->
+    <view class="rd-section">
+      <view class="rd-section-hd">
+        <text class="rd-section-title">鏀舵淇℃伅</text>
+      </view>
+      <view class="rd-group">
+        <view class="rd-cell">
+          <text class="rd-label">鏀舵浜�</text>
+          <text class="rd-value">{{ r.payeeName || r.payee || "鈥�" }}</text>
+        </view>
+        <view class="rd-cell">
+          <text class="rd-label">鏀舵璐﹀彿</text>
+          <text class="rd-value">{{ r.payeeAccount || "鈥�" }}</text>
+        </view>
+        <view class="rd-cell">
+          <text class="rd-label">寮�鎴锋敮琛�</text>
+          <text class="rd-value">{{ r.payeeBank || r.bankBranch || "鈥�" }}</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 鎶ラ攢鏄庣粏 -->
+    <view class="rd-section">
+      <view class="rd-section-hd">
+        <text class="rd-section-title">鎶ラ攢鏄庣粏</text>
+        <text class="rd-section-count">鍏� {{ detailRows.length }} 鏉�</text>
+      </view>
+      <view v-if="detailRows.length"
+            class="rd-group">
+        <view v-for="(d, idx) in detailRows"
+              :key="'d-' + idx"
+              class="rd-detail-item">
+          <view class="rd-detail-head">
+            <text class="rd-detail-badge">{{ idx + 1 }}</text>
+            <text class="rd-detail-title">{{ detailSubject(d) }}</text>
+            <text class="rd-detail-amount">{{ detailAmount(d) }}</text>
+          </view>
+          <view class="rd-cell">
+            <text class="rd-label">鍙戠エ鏃ユ湡</text>
+            <text class="rd-value">{{ d.invoiceDate || "鈥�" }}</text>
+          </view>
+          <view v-if="d.description"
+                class="rd-cell">
+            <text class="rd-label">鎻忚堪</text>
+            <text class="rd-value">{{ d.description }}</text>
+          </view>
+          <view v-if="d.invoiceNo"
+                class="rd-cell">
+            <text class="rd-label">鍙戠エ鍙�</text>
+            <text class="rd-value">{{ d.invoiceNo }}</text>
+          </view>
+        </view>
+      </view>
+      <view v-else
+            class="rd-group">
+        <view class="rd-empty">鏆傛棤鎶ラ攢鏄庣粏</view>
+      </view>
+    </view>
+
+    <!-- 闄勪欢 -->
+    <view v-if="attachmentList.length"
+          class="rd-section">
+      <view class="rd-section-hd">
+        <text class="rd-section-title">鍙戠エ闄勪欢</text>
+      </view>
+      <view class="rd-group">
+        <view v-for="(f, i) in attachmentList"
+              :key="i"
+              class="rd-attach"
+              @click="openAttachment(f)">
+          {{ f.name || "闄勪欢" }}
+        </view>
+      </view>
+    </view>
+
+    <!-- 瀹℃壒娴佺▼锛坱asks锛� -->
+    <view class="rd-section">
+      <view class="rd-section-hd">
+        <text class="rd-section-title">瀹℃壒娴佺▼</text>
+        <text class="rd-section-count">{{ flowNodesList.length }} 绾�</text>
+      </view>
+      <view v-if="flowNodesList.length"
+            class="rd-group">
+        <view v-for="(node, nodeIndex) in flowNodesList"
+              :key="nodeIndex"
+              class="rd-flow-node">
+          <view class="rd-flow-line">
+            <view class="rd-flow-dot" />
+            <view v-if="nodeIndex < flowNodesList.length - 1"
+                  class="rd-flow-bar" />
+          </view>
+          <view class="rd-flow-body">
+            <text class="rd-flow-level">绗瑊{ node.levelNo }}绾� 路 {{ node.approveType === 'OR' ? '鎴栫' : '浼氱' }}</text>
+            <view v-for="(a, ai) in node.approvers"
+                  :key="ai"
+                  class="rd-flow-approver">
+              <view class="rd-flow-avatar"
+                    :style="{ backgroundColor: avatarColor(a.approverName) }">
+                {{ (a.approverName || "?").charAt(0) }}
+              </view>
+              <view class="rd-flow-approver-meta">
+                <text class="rd-flow-name">{{ a.approverName || "鈥�" }}</text>
+                <text v-if="a.taskStatus"
+                      class="rd-flow-status">{{ taskStatusLabel(a.taskStatus) }}</text>
+              </view>
+            </view>
+          </view>
+        </view>
+      </view>
+      <view v-else
+            class="rd-group">
+        <view class="rd-empty">鏆傛棤瀹℃壒鑺傜偣</view>
+      </view>
+    </view>
+
+    <!-- 瀹℃壒璁板綍锛坱asks 鐣欑棔锛� -->
+    <view class="rd-section">
+      <view class="rd-section-hd">
+        <text class="rd-section-title">瀹℃壒璁板綍</text>
+        <text class="rd-section-count">{{ approvalRecords.length }} 鏉�</text>
+      </view>
+      <view v-if="approvalRecords.length"
+            class="rd-group">
+        <view v-for="(rec, index) in approvalRecords"
+              :key="rec.id ?? index"
+              class="rd-record-item">
+          <view class="rd-record-head">
+            <text class="rd-record-operator">{{ rec.operatorName }}</text>
+            <text class="rd-record-tag"
+                  :class="'rd-record-tag--' + rec.result">{{ recordLabel(rec.result) }}</text>
+          </view>
+          <text v-if="rec.time"
+                class="rd-record-time">{{ rec.time }}</text>
+          <text class="rd-record-opinion">{{ rec.opinion || "鏃犳剰瑙�" }}</text>
+        </view>
+      </view>
+      <view v-else
+            class="rd-group">
+        <view class="rd-empty">鏆傛棤瀹℃壒璁板綍</view>
+      </view>
+    </view>
+
+    <view class="rd-section">
+      <view class="rd-group">
+        <view class="rd-cell">
+          <text class="rd-label">鍒涘缓鏃堕棿</text>
+          <text class="rd-value">{{ formatTime(r.createTime) }}</text>
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup>
+  import { computed } from "vue";
+  import { parseTime } from "@/utils/ruoyi";
+  import { isTravelReimbursementType } from "../../_utils/finReimbursementMappers.js";
+  import {
+    billStatusCssClass,
+    billStatusLabel,
+  } from "../../_utils/finReimbursementMappers.js";
+  import { expenseCategoryLabel, EXPENSE_SUBJECT_OPTIONS as COST_SUBJECTS } from "../_utils/costReimburseUtils.js";
+  import { EXPENSE_SUBJECT_OPTIONS as TRAVEL_SUBJECTS } from "../_utils/travelReimburseUtils.js";
+  import {
+    resolveExpenseSubjectLabel,
+    formatDetailAmount,
+  } from "../_utils/expenseDetailDisplay.js";
+  import { userAvatarColor } from "../../_utils/userPickerUtils.js";
+  import {
+    mapTasksToFlowNodes,
+    recordActionLabel,
+    taskStatusText,
+  } from "../../_utils/approveListUtils.js";
+  import config from "@/config.js";
+
+  const props = defineProps({
+    reimburseRow: { type: Object, default: () => ({}) },
+    moduleKey: { type: String, default: "" },
+  });
+
+  const r = computed(() => props.reimburseRow || {});
+
+  const isTravel = computed(() =>
+    isTravelReimbursementType(r.value.reimbursementType ?? props.moduleKey)
+  );
+
+  const billNo = computed(() => r.value.billNo || r.value.reimburseNo || "鈥�");
+  const statusText = computed(() =>
+    billStatusLabel(r.value.billStatus ?? r.value.status)
+  );
+  const statusCssClass = computed(() =>
+    billStatusCssClass(r.value)
+  );
+  const reasonText = computed(
+    () => r.value.reason || r.value.reimburseReason || "鈥�"
+  );
+  const amountText = computed(() =>
+    r.value.applyAmount != null ? String(r.value.applyAmount) : "鈥�"
+  );
+
+  const expenseTypeText = computed(() =>
+    expenseCategoryLabel(r.value.expenseCategory) || r.value.expenseType || "鈥�"
+  );
+
+  const travelDaysText = computed(() => {
+    const d = r.value.travelDays ?? r.value.travel?.travelDays;
+    return d != null ? `${d} 澶ー : "鈥�";
+  });
+
+  const hasTravelStandard = computed(() => {
+    const row = r.value;
+    return (
+      row.hotelStandard != null ||
+      row.hotelDays != null ||
+      row.livingSubsidy != null ||
+      row.standardTag ||
+      row.needSpecialApproval
+    );
+  });
+
+  const subjectOptions = computed(() =>
+    isTravel.value ? TRAVEL_SUBJECTS : COST_SUBJECTS
+  );
+
+  const detailRows = computed(() => {
+    const list = r.value.expenseDetails || r.value.details || [];
+    return Array.isArray(list) ? list : [];
+  });
+
+  const attachmentList = computed(() => {
+    const list =
+      r.value.attachmentList ||
+      r.value.storageBlobVOList ||
+      r.value.invoiceAttachments ||
+      [];
+    return Array.isArray(list) ? list : [];
+  });
+
+  const approvalRecords = computed(() => {
+    const list = r.value.approvalRecords || [];
+    return Array.isArray(list) ? list : [];
+  });
+
+  /** 娴佺▼灞曠ず浼樺厛鐢� enrichment 鍚庣殑 flowNodes锛堟潵鑷� tasks锛� */
+  const flowNodesList = computed(() => {
+    const row = r.value;
+    if (Array.isArray(row.flowNodes) && row.flowNodes.length) {
+      return row.flowNodes;
+    }
+    if (Array.isArray(row.tasks) && row.tasks.length) {
+      return mapTasksToFlowNodes(row.tasks);
+    }
+    return [];
+  });
+
+  function taskStatusLabel(status) {
+    return taskStatusText(status);
+  }
+
+  function recordLabel(result) {
+    return recordActionLabel(result);
+  }
+
+  function formatTime(t) {
+    if (!t) return "鈥�";
+    const s = parseTime(t, "{y}-{m}-{d} {h}:{i}");
+    return s || String(t).replace("T", " ").slice(0, 16);
+  }
+
+  function detailSubject(d) {
+    return (
+      resolveExpenseSubjectLabel(d.expenseSubject || d.expenseCategory, {
+        isTravel: isTravel.value,
+        subjectOptions: subjectOptions.value,
+      }) || "鏈�夌鐩�"
+    );
+  }
+
+  function detailAmount(d) {
+    return formatDetailAmount(d.amount) || "鈥�";
+  }
+
+  function avatarColor(name) {
+    return userAvatarColor(name);
+  }
+
+  function resolveFileUrl(f) {
+    let url = f?.url || f?.downloadURL || f?.previewURL || f?.fileUrl || "";
+    if (!url) return "";
+    if (/^https?:\/\//i.test(url)) return url;
+    const base = (config.baseUrl || "").replace(/\/+$/, "");
+    const path = url.startsWith("/") ? url : `/${url}`;
+    return `${base}${path}`;
+  }
+
+  function openAttachment(f) {
+    const url = resolveFileUrl(f);
+    if (!url) {
+      uni.showToast({ title: "鏃犳硶鎵撳紑闄勪欢", icon: "none" });
+      return;
+    }
+    // #ifdef H5
+    window.open(url, "_blank");
+    // #endif
+    // #ifndef H5
+    uni.downloadFile({
+      url,
+      success: res => {
+        if (res.statusCode === 200) {
+          uni.openDocument({ filePath: res.tempFilePath, showMenu: true });
+        }
+      },
+      fail: () => uni.showToast({ title: "闄勪欢鎵撳紑澶辫触", icon: "none" }),
+    });
+    // #endif
+  }
+</script>
+
+<style scoped lang="scss">
+  @import "../reimburse-detail/reimburse-detail.scss";
+</style>

--
Gitblit v1.9.3