From cdb0e306d0b83902908f20da0903bd9df901c81b Mon Sep 17 00:00:00 2001
From: yyb <995253665@qq.com>
Date: 星期六, 16 五月 2026 14:24:16 +0800
Subject: [PATCH] feat(travel-reimburse): 完善差旅报销模块界面和功能
---
src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/index.vue | 625 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 618 insertions(+), 7 deletions(-)
diff --git a/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/index.vue b/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/index.vue
index b78eb7b..2e81e18 100644
--- a/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/index.vue
+++ b/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/index.vue
@@ -1,12 +1,623 @@
-<!--
- 妯″潡涓枃鍚嶏細宸梾鎶ラ攢
- 鐩綍鏍囪瘑锛歊eimburseManage/travel-reimburse锛坱ravel-reimburse 鈫� 涓枃锛氬樊鏃呮姤閿�锛�
- 澶嶇敤椤甸潰锛欯/views/procurementManagement/procurementLedger/index.vue锛堥噰璐彴璐︼紱鏂囦欢鍚� index.vue 鈫� 鍏ュ彛椤碉級
--->
+<!--OA妯″潡锛氬樊鏃呮姤閿�-->
<template>
- <ProcurementLedger />
+ <div class="app-container">
+ <div class="search_form mb20">
+ <div>
+ <span class="search_title">鐢宠浜猴細</span>
+ <el-input
+ v-model="searchForm.applicantKeyword"
+ style="width: 220px"
+ placeholder="濮撳悕鎴栫紪鍙�"
+ clearable
+ :prefix-icon="Search"
+ @keyup.enter="handleQuery"
+ />
+ <span class="search_title" style="margin-left: 12px">鍑哄樊寮�濮嬶細</span>
+ <el-date-picker
+ v-model="searchForm.travelStartFrom"
+ type="date"
+ placeholder="寮�濮嬫棩鏈�"
+ format="YYYY-MM-DD"
+ value-format="YYYY-MM-DD"
+ style="width: 150px"
+ clearable
+ />
+ <span class="search_title" style="margin-left: 8px">缁撴潫锛�</span>
+ <el-date-picker
+ v-model="searchForm.travelEndTo"
+ type="date"
+ placeholder="缁撴潫鏃ユ湡"
+ format="YYYY-MM-DD"
+ value-format="YYYY-MM-DD"
+ style="width: 150px"
+ clearable
+ />
+ <el-button type="primary" style="margin-left: 10px" @click="handleQuery">鎼滅储</el-button>
+ <el-button @click="resetSearch">閲嶇疆</el-button>
+ </div>
+ <div class="search_actions">
+ <el-button type="success" plain @click="handleImportClick">瀵煎叆</el-button>
+ <el-button type="warning" plain @click="handleExport">瀵煎嚭</el-button>
+ <el-button type="primary" @click="openFormDialog('add')">鏂板宸梾鎶ラ攢</el-button>
+ </div>
+ </div>
+
+ <input ref="importInputRef" type="file" accept="application/json,.json" class="sr-only-input" @change="onImportFile" />
+
+ <div class="table_list">
+ <PIMTable
+ rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :isSelection="false"
+ :tableLoading="tableLoading"
+ :total="page.total"
+ @pagination="pagination"
+ />
+ </div>
+
+ <!-- 鏂板 / 缂栬緫 -->
+ <el-dialog
+ v-model="formDialog.visible"
+ :title="formDialog.title"
+ width="1120px"
+ append-to-body
+ destroy-on-close
+ class="travel-reimburse-form-dialog"
+ @closed="onFormClosed"
+ >
+ <el-alert
+ v-if="budgetHint.visible"
+ :title="budgetHint.title"
+ :type="budgetHint.type"
+ :description="budgetHint.description"
+ show-icon
+ :closable="false"
+ class="mb16"
+ />
+ <el-alert v-if="overBudgetWarnings.length" type="warning" show-icon :closable="false" class="mb16">
+ <template #title>宸梾鏍囧噯瓒呮敮鎻愰啋锛堥渶鐗规壒锛�</template>
+ <ul class="warn-list">
+ <li v-for="(w, i) in overBudgetWarnings" :key="i">{{ w }}</li>
+ </ul>
+ </el-alert>
+
+ <el-form
+ ref="formRef"
+ :model="form"
+ :rules="formRules"
+ label-width="120px"
+ class="travel-reimburse-form"
+ :disabled="formDialog.readonly"
+ >
+ <el-card class="form-section" shadow="never">
+ <template #header><span class="card-header-title">鍩烘湰淇℃伅</span></template>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍛樺伐缂栧彿">
+ <el-input v-model="form.employeeNo" readonly placeholder="閫夋嫨鍛樺伐鍚庤嚜鍔ㄥ甫鍑�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍛樺伐濮撳悕" prop="applicantId">
+ <el-select
+ v-model="form.applicantId"
+ filterable
+ remote
+ clearable
+ reserve-keyword
+ placeholder="璇烽�夋嫨鎴栨悳绱㈠憳宸�"
+ style="width: 100%"
+ :remote-method="remoteSearchApplicantForm"
+ :loading="applicantFormSearchLoading"
+ @change="onApplicantChange"
+ >
+ <el-option
+ v-for="u in applicantFormOptions"
+ :key="u.userId"
+ :label="userSelectLabel(u)"
+ :value="u.userId"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="24">
+ <el-form-item label="鎶ラ攢鍘熷洜" prop="reimburseReason">
+ <el-input
+ v-model="form.reimburseReason"
+ type="textarea"
+ :rows="3"
+ placeholder="璇峰~鍐欏嚭宸強鎶ラ攢鍘熷洜"
+ maxlength="2000"
+ show-word-limit
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍑哄樊寮�濮�" prop="travelStartTime">
+ <el-date-picker
+ v-model="form.travelStartTime"
+ type="datetime"
+ placeholder="寮�濮嬫椂闂�"
+ format="YYYY-MM-DD HH:mm:ss"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ style="width: 100%"
+ @change="onTravelRangeChange"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍑哄樊缁撴潫" prop="travelEndTime">
+ <el-date-picker
+ v-model="form.travelEndTime"
+ type="datetime"
+ placeholder="缁撴潫鏃堕棿"
+ format="YYYY-MM-DD HH:mm:ss"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ style="width: 100%"
+ @change="onTravelRangeChange"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="8">
+ <el-form-item label="鍑哄樊澶╂暟">
+ <el-input :model-value="travelDaysDisplay" readonly>
+ <template #append>澶�</template>
+ </el-input>
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="鍑哄樊鍦�" prop="departurePlace">
+ <el-input v-model="form.departurePlace" placeholder="鍑哄彂鍩庡競" @blur="recalcTravelStandards" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="鐩殑鍦�" prop="destination">
+ <el-input v-model="form.destination" placeholder="鐩殑鍩庡競" @blur="recalcTravelStandards" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-card>
+
+ <el-card class="form-section" shadow="never">
+ <template #header>
+ <div class="card-header-row">
+ <span class="card-header-title">宸梾鏍囧噯</span>
+ <el-text type="info" size="small">{{ travelTierLabel }} 路 鐢熸椿琛ヨ创寤鸿 {{ suggestedLivingSubsidy }} 鍏�</el-text>
+ </div>
+ </template>
+ <el-row :gutter="20">
+ <el-col :span="8">
+ <el-form-item label="閰掑簵鏍囧噯">
+ <el-input-number
+ v-model="form.hotelStandard"
+ :min="0"
+ :precision="2"
+ controls-position="right"
+ style="width: 100%"
+ @change="recalcTravelStandards"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="浣忓澶╂暟">
+ <el-input-number
+ v-model="form.hotelDays"
+ :min="0"
+ :max="365"
+ :precision="0"
+ controls-position="right"
+ style="width: 100%"
+ @change="recalcTravelStandards"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="鐢熸椿琛ヨ创">
+ <el-input-number
+ v-model="form.livingSubsidy"
+ :min="0"
+ :precision="2"
+ controls-position="right"
+ style="width: 100%"
+ @change="recalcTravelStandards"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="8">
+ <el-form-item label="浜ら�氳ˉ璐�">
+ <el-input :model-value="String(suggestedTransportSubsidy)" readonly><template #append>鍏�</template></el-input>
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="浣忓闄愰">
+ <el-input :model-value="String(suggestedHotelLimit)" readonly><template #append>鍏�</template></el-input>
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="鐗规壒鏍囪">
+ <el-tag :type="form.needSpecialApproval ? 'danger' : 'success'" effect="plain">
+ {{ form.needSpecialApproval ? "瓒呮敮闇�鐗规壒" : "鍦ㄦ爣鍑嗚寖鍥村唴" }}
+ </el-tag>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-card>
+
+ <el-card class="form-section" shadow="never">
+ <template #header><span class="card-header-title">閲戦涓庢敹娆�</span></template>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鐢宠閲戦" prop="applyAmount">
+ <div class="amount-row">
+ <el-input-number v-model="form.applyAmount" :min="0" :precision="2" controls-position="right" class="amount-input" />
+ <el-button v-if="!formDialog.readonly" type="primary" link @click="syncApplyAmountFromDetails">
+ 鎸夋槑缁嗘眹鎬� {{ detailTotalAmount }} 鍏�
+ </el-button>
+ </div>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鏀舵浜�" prop="payee">
+ <el-input v-model="form.payee" placeholder="璇疯緭鍏ユ敹娆句汉" maxlength="50" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-card>
+
+ <el-card class="form-section" shadow="never">
+ <template #header>
+ <div class="card-header-row">
+ <span class="card-header-title">鎶ラ攢鏄庣粏</span>
+ <el-button v-if="!formDialog.readonly" type="primary" plain size="small" @click="addExpenseDetail">鏂板鏄庣粏</el-button>
+ </div>
+ </template>
+
+ <el-table :data="form.expenseDetails" border size="small" class="detail-table">
+ <el-table-column type="index" label="搴忓彿" width="55" align="center" />
+ <el-table-column label="鍙戠エ鏃ユ湡" width="150">
+ <template #default="{ row }">
+ <el-date-picker
+ v-if="!formDialog.readonly"
+ v-model="row.invoiceDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ size="small"
+ style="width: 100%"
+ />
+ <span v-else>{{ row.invoiceDate || "鈥�" }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="璐圭敤绉戠洰" width="130">
+ <template #default="{ row }">
+ <el-select
+ v-if="!formDialog.readonly"
+ v-model="row.expenseSubject"
+ size="small"
+ style="width: 100%"
+ @change="recalcTravelStandards"
+ >
+ <el-option
+ v-for="opt in EXPENSE_SUBJECT_OPTIONS"
+ :key="opt.value"
+ :label="opt.label"
+ :value="opt.value"
+ />
+ </el-select>
+ <span v-else>{{ expenseSubjectLabel(row.expenseSubject) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="閲戦" width="120">
+ <template #default="{ row }">
+ <el-input-number
+ v-if="!formDialog.readonly"
+ v-model="row.amount"
+ :min="0"
+ :precision="2"
+ size="small"
+ controls-position="right"
+ style="width: 100%"
+ @change="onDetailAmountChange"
+ />
+ <span v-else>{{ row.amount ?? "鈥�" }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎻忚堪" min-width="140">
+ <template #default="{ row }">
+ <el-input v-if="!formDialog.readonly" v-model="row.description" size="small" placeholder="璇存槑" />
+ <span v-else>{{ row.description || "鈥�" }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column v-if="!formDialog.readonly" label="鎿嶄綔" width="70" align="center">
+ <template #default="{ $index }">
+ <el-button type="danger" link size="small" @click="removeExpenseDetail($index)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-card>
+
+ <el-card class="form-section" shadow="never">
+ <template #header><span class="card-header-title">闄勪欢锛堝彂绁級</span></template>
+ <el-form-item label-width="0" class="attachment-form-item">
+ <div class="upload-block">
+ <FileUpload v-model:file-list="form.attachmentList" :limit="20" button-text="鐐瑰嚮閫夋嫨鏂囦欢" />
+ </div>
+ </el-form-item>
+ </el-card>
+
+ <el-card class="form-section" shadow="never">
+ <template #header><span class="card-header-title">瀹℃壒娴佺▼</span></template>
+ <el-form-item prop="approvalFlowNodes" label-width="0">
+ <ApprovalFlowEditor
+ v-if="!formDialog.readonly"
+ v-model="form.approvalFlowNodes"
+ :user-options="flowUserOptions"
+ @update:model-value="onApprovalFlowChange"
+ />
+ <ApprovalFlowProgress v-else :nodes="form.approvalFlowNodes" :current-index="form.currentNodeIndex" />
+ <p v-if="!formDialog.readonly" class="flow-tip">鑷冲皯淇濈暀涓�涓妭鐐癸紱瀹℃牳涓�佸凡閫氳繃鐨勫崟鎹笉鍙紪杈戙��</p>
+ </el-form-item>
+ </el-card>
+ </el-form>
+ <template #footer>
+ <el-button v-if="!formDialog.readonly" type="primary" @click="submitForm">鎻� 浜�</el-button>
+ <el-button @click="formDialog.visible = false">{{ formDialog.readonly ? "鍏� 闂�" : "鍙� 娑�" }}</el-button>
+ </template>
+ </el-dialog>
+
+ <!-- 璇︽儏 -->
+ <el-dialog v-model="detailDialog.visible" title="宸梾鎶ラ攢璇︽儏" width="900px" append-to-body destroy-on-close>
+ <DetailPanel :row="detailRow" />
+ <ApprovalFlowProgress
+ class="mt16"
+ :nodes="detailRow.approvalFlowNodes"
+ :current-index="detailRow.currentNodeIndex ?? 0"
+ />
+ <el-divider content-position="left">瀹℃壒璁板綍锛堝叏娴佺▼鐣欑棔锛�</el-divider>
+ <el-timeline v-if="detailRow.approvalRecords?.length">
+ <el-timeline-item
+ v-for="(rec, i) in detailRow.approvalRecords"
+ :key="i"
+ :type="rec.result === 'approved' ? 'success' : rec.result === 'rejected' ? 'danger' : 'primary'"
+ :timestamp="rec.time"
+ >
+ {{ rec.operatorName }} 鈥� {{ approvalActionLabel(rec.result) }}锛歿{ rec.opinion || "鏃犳剰瑙�" }}
+ </el-timeline-item>
+ </el-timeline>
+ <el-empty v-else description="鏆傛棤瀹℃壒璁板綍" :image-size="60" />
+ <template #footer>
+ <el-button type="primary" @click="detailDialog.visible = false">鍏� 闂�</el-button>
+ </template>
+ </el-dialog>
+
+ <!-- 瀹℃壒 -->
+ <el-dialog
+ v-model="approveDialog.visible"
+ title="宸梾鎶ラ攢瀹℃壒"
+ width="1000px"
+ append-to-body
+ destroy-on-close
+ @closed="approveOpinion = ''"
+ >
+ <DetailPanel :row="approveDialog.row" />
+ <el-divider content-position="left">娴佺▼杩涘害</el-divider>
+ <ApprovalFlowProgress
+ :nodes="approveDialog.row?.approvalFlowNodes"
+ :current-index="approveDialog.row?.currentNodeIndex ?? 0"
+ />
+ <el-form label-width="100px" class="mt16">
+ <el-form-item label="瀹℃壒鎰忚">
+ <el-input
+ v-model="approveOpinion"
+ type="textarea"
+ :rows="3"
+ maxlength="500"
+ show-word-limit
+ placeholder="閫氳繃鍙暀绌猴紱椹冲洖璇峰~鍐欏師鍥�"
+ />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button type="success" @click="submitApprove('approved')">閫� 杩�</el-button>
+ <el-button type="danger" @click="submitApprove('rejected')">椹� 鍥�</el-button>
+ <el-button @click="approveDialog.visible = false">鍙� 娑�</el-button>
+ </template>
+ </el-dialog>
+ </div>
</template>
<script setup>
-import ProcurementLedger from '@/views/procurementManagement/procurementLedger/index.vue'
+import FileUpload from "@/components/AttachmentUpload/file/index.vue";
+import ApprovalFlowEditor from "@/views/officeProcessAutomation/AttendManage/overtime-apply/components/ApprovalFlowEditor.vue";
+import ApprovalFlowProgress from "./components/ApprovalFlowProgress.vue";
+import DetailPanel from "./components/DetailPanel.vue";
+import { useTravelReimburse } from "./useTravelReimburse.js";
+
+const tr = useTravelReimburse();
+const {
+ Search,
+ EXPENSE_SUBJECT_OPTIONS,
+ expenseSubjectLabel,
+ searchForm,
+ tableLoading,
+ page,
+ tableData,
+ tableColumn,
+ importInputRef,
+ formRef,
+ form,
+ formDialog,
+ formRules,
+ detailDialog,
+ detailRow,
+ approveDialog,
+ approveOpinion,
+ applicantFormSearchLoading,
+ applicantFormOptions,
+ flowUserOptions,
+ travelDaysDisplay,
+ travelTierLabel,
+ suggestedLivingSubsidy,
+ suggestedTransportSubsidy,
+ suggestedHotelLimit,
+ detailTotalAmount,
+ overBudgetWarnings,
+ budgetHint,
+ handleQuery,
+ resetSearch,
+ pagination,
+ remoteSearchApplicantForm,
+ userSelectLabel,
+ onApplicantChange,
+ recalcTravelStandards,
+ onTravelRangeChange,
+ onDetailAmountChange,
+ onApprovalFlowChange,
+ addExpenseDetail,
+ removeExpenseDetail,
+ syncApplyAmountFromDetails,
+ openFormDialog,
+ onFormClosed,
+ submitForm,
+ openDetail,
+ approvalActionLabel,
+ submitApprove,
+ handleExport,
+ handleImportClick,
+ onImportFile,
+} = tr;
</script>
+
+<style scoped>
+.mb20 {
+ margin-bottom: 20px;
+}
+.mb16 {
+ margin-bottom: 16px;
+}
+.mb8 {
+ margin-bottom: 8px;
+}
+.mt16 {
+ margin-top: 16px;
+}
+.search_form {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+}
+.search_actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+.search_title {
+ font-size: 14px;
+ color: var(--el-text-color-regular);
+}
+.sr-only-input {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+}
+.form-section {
+ margin-bottom: 16px;
+ border: 1px solid var(--el-border-color-lighter);
+}
+.form-section :deep(.el-card__header) {
+ padding: 12px 16px;
+ background: var(--el-fill-color-lighter);
+}
+.form-section :deep(.el-card__body) {
+ padding: 16px 16px 4px;
+}
+.card-header-title {
+ font-size: 15px;
+ font-weight: 600;
+}
+.card-header-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+}
+.amount-row {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ width: 100%;
+}
+.amount-input {
+ flex: 1;
+ min-width: 160px;
+}
+.w-full {
+ width: 100%;
+}
+.attachment-form-item {
+ margin-bottom: 0;
+}
+.detail-table {
+ margin-bottom: 0;
+}
+.section-title {
+ font-size: 15px;
+ font-weight: 600;
+ margin: 8px 0 12px;
+ color: var(--el-text-color-primary);
+ border-left: 3px solid var(--el-color-primary);
+ padding-left: 8px;
+}
+.field-tip {
+ font-size: 12px;
+ color: var(--el-text-color-secondary);
+ margin-top: 4px;
+}
+.warn-list {
+ margin: 0;
+ padding-left: 18px;
+}
+.detail-toolbar {
+ margin-bottom: 8px;
+}
+.upload-block {
+ width: 100%;
+}
+.flow-tip {
+ font-size: 12px;
+ color: var(--el-text-color-secondary);
+ margin-top: 8px;
+}
+.sync-btn {
+ margin-top: 4px;
+}
+.travel-reimburse-form-dialog :deep(.el-dialog__body) {
+ padding-top: 12px;
+}
+.travel-reimburse-form :deep(.el-form-item) {
+ margin-bottom: 18px;
+}
+.travel-reimburse-form :deep(.el-input-number) {
+ width: 100%;
+}
+.travel-reimburse-form :deep(.el-row) {
+ margin-bottom: 0;
+}
+</style>
--
Gitblit v1.9.3