<!--
|
差旅/费用报销详情展示(列表详情 / 审批详情共用)
|
-->
|
<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>
|
|
<!-- 审批流程(tasks) -->
|
<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>
|
|
<!-- 审批记录(tasks 留痕) -->
|
<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>
|