yyb
2 小时以前 1fba2685678695cca45127adaada26c7b96eec12
src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/index.vue
@@ -1,12 +1,556 @@
<!--
  模块中文名:费用报销
  目录标识:ReimburseManage/cost-reimburse(cost-reimburse → 中文:费用报销)
  复用页面:@/views/procurementManagement/procurementLedger/index.vue(采购台账;文件名 index.vue → 入口页)
-->
<!--OA模块:费用报销-->
<template>
  <ProcurementLedger />
  <div class="app-container">
    <div class="search_form mb20">
      <div class="search_fields">
        <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.applyTimeFrom"
          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.applyTimeTo"
          type="date"
          placeholder="结束日期"
          format="YYYY-MM-DD"
          value-format="YYYY-MM-DD"
          style="width: 150px; margin-left: 8px"
          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="cost-reimburse-form-dialog"
      @closed="onFormClosed"
    >
      <el-alert type="info" show-icon :closable="false" class="mb16">
        <template #title>全品类费用报销 · 分类模板一键填报</template>
        <template #default>
          支持差旅、办公采购、业务招待、交通费、通讯费等;按金额自动匹配审批链(500元内直属上级,超5000元财务总监复核)。
        </template>
      </el-alert>
      <div v-if="!formDialog.readonly" class="template-bar mb16">
        <span class="template-label">分类模板:</span>
        <el-button
          v-for="(tpl, key) in CATEGORY_TEMPLATES"
          :key="key"
          size="small"
          :type="form.expenseCategory === key ? 'primary' : 'default'"
          plain
          @click="applyTemplate(key)"
        >
          {{ tpl.label }}
        </el-button>
      </div>
      <el-form
        ref="formRef"
        :model="form"
        :rules="formRules"
        label-width="120px"
        class="cost-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="12">
              <el-form-item label="费用类型" prop="expenseCategory">
                <el-select
                  v-model="form.expenseCategory"
                  placeholder="请选择费用类型"
                  style="width: 100%"
                  @change="onExpenseCategoryChange"
                >
                  <el-option
                    v-for="opt in EXPENSE_CATEGORY_OPTIONS"
                    :key="opt.value"
                    :label="opt.label"
                    :value="opt.value"
                  />
                </el-select>
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="报销状态">
                <el-tag
                  :type="form.approvalResult === 'approved' ? 'success' : form.approvalResult === 'rejected' ? 'danger' : 'warning'"
                  effect="plain"
                >
                  {{
                    form.approvalResult === "approved"
                      ? "已通过"
                      : form.approvalResult === "rejected"
                        ? "已驳回"
                        : "审核中"
                  }}
                </el-tag>
              </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="applyAmount">
                <div class="amount-row">
                  <el-input-number
                    v-model="form.applyAmount"
                    :min="0"
                    :precision="2"
                    controls-position="right"
                    class="amount-input"
                    @change="autoAssignApprovalFlow"
                  />
                  <el-button v-if="!formDialog.readonly" type="primary" link @click="syncApplyAmountFromDetails">
                    按明细汇总 {{ detailTotalAmount }} 元
                  </el-button>
                </div>
              </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%">
                  <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-row :gutter="20">
            <el-col :span="8">
              <el-form-item label="收款人" prop="payee">
                <el-input v-model="form.payee" placeholder="请输入收款人" maxlength="50" />
              </el-form-item>
            </el-col>
            <el-col :span="8">
              <el-form-item label="收款账号" prop="payeeAccount">
                <el-input v-model="form.payeeAccount" placeholder="银行卡号" maxlength="30" />
              </el-form-item>
            </el-col>
            <el-col :span="8">
              <el-form-item label="开户支行" prop="bankBranch">
                <el-input v-model="form.bankBranch" placeholder="开户支行全称" maxlength="100" />
              </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-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>
            <div class="card-header-row">
              <span class="card-header-title">审批流程</span>
              <el-button v-if="!formDialog.readonly" type="primary" link size="small" @click="autoAssignApprovalFlow">
                按规则重新分配
              </el-button>
            </div>
          </template>
          <el-alert type="success" :title="approvalRuleHint" show-icon :closable="false" class="mb12" />
          <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" />
      <el-divider content-position="left">审批流程</el-divider>
      <ApprovalFlowProgress :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="审批意见" required>
          <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 "../travel-reimburse/components/ApprovalFlowProgress.vue";
import DetailPanel from "./components/DetailPanel.vue";
import { useCostReimburse } from "./useCostReimburse.js";
const cr = useCostReimburse();
const {
  Search,
  EXPENSE_CATEGORY_OPTIONS,
  CATEGORY_TEMPLATES,
  EXPENSE_SUBJECT_OPTIONS,
  expenseSubjectLabel,
  searchForm,
  tableLoading,
  page,
  tableData,
  tableColumn,
  importInputRef,
  formRef,
  form,
  formDialog,
  formRules,
  detailDialog,
  detailRow,
  approveDialog,
  approveOpinion,
  applicantFormSearchLoading,
  applicantFormOptions,
  flowUserOptions,
  detailTotalAmount,
  approvalRuleHint,
  handleQuery,
  resetSearch,
  pagination,
  remoteSearchApplicantForm,
  userSelectLabel,
  onApplicantChange,
  onExpenseCategoryChange,
  applyTemplate,
  onDetailAmountChange,
  onApprovalFlowChange,
  addExpenseDetail,
  removeExpenseDetail,
  syncApplyAmountFromDetails,
  autoAssignApprovalFlow,
  openFormDialog,
  onFormClosed,
  submitForm,
  approvalActionLabel,
  submitApprove,
  handleExport,
  handleImportClick,
  onImportFile,
} = cr;
</script>
<style scoped>
.mb20 {
  margin-bottom: 20px;
}
.mb16 {
  margin-bottom: 16px;
}
.mb12 {
  margin-bottom: 12px;
}
.mt16 {
  margin-top: 16px;
}
.search_form {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.search_fields {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px;
}
.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;
}
.template-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
}
.template-label {
  font-size: 14px;
  color: var(--el-text-color-secondary);
  flex-shrink: 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;
}
.attachment-form-item {
  margin-bottom: 0;
}
.detail-table {
  margin-bottom: 0;
}
.upload-block {
  width: 100%;
}
.flow-tip {
  font-size: 12px;
  color: var(--el-text-color-secondary);
  margin-top: 8px;
}
.cost-reimburse-form-dialog :deep(.el-dialog__body) {
  padding-top: 12px;
}
.cost-reimburse-form :deep(.el-form-item) {
  margin-bottom: 18px;
}
.cost-reimburse-form :deep(.el-input-number) {
  width: 100%;
}
.cost-reimburse-form :deep(.el-row) {
  margin-bottom: 0;
}
</style>