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