1
yyb
6 小时以前 c63958466b4eaef3c9314c41020a3337bdb928f5
1
已修改2个文件
1261 ■■■■ 文件已修改
src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/index.vue 598 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/index.vue 663 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/index.vue
@@ -1,8 +1,8 @@
<!--OA模块:费用报销(审批实例 listPage,businessType=17)-->
<!--OA模块:费用报销-->
<template>
  <div class="app-container">
    <div class="search_form mb20">
      <div>
      <div class="search_fields">
        <span class="search_title">申请人:</span>
        <el-input
          v-model="searchForm.applicantKeyword"
@@ -10,15 +10,40 @@
          placeholder="姓名或编号"
          clearable
          :prefix-icon="Search"
          @keyup.enter="onSearch"
          @keyup.enter="handleQuery"
        />
        <el-button type="primary" style="margin-left: 10px" @click="onSearch">搜索</el-button>
        <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>
        <el-button type="primary" @click="openAddWithTemplate">新增费用报销</el-button>
      <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"
@@ -28,123 +53,404 @@
        :isSelection="false"
        :tableLoading="tableLoading"
        :total="page.total"
        @pagination="onPagination"
        @pagination="pagination"
      />
    </div>
    <ApprovalInstanceSubmitDialog
      v-model="submitDialog.visible"
      :title="submitDialogTitle"
      :form="submitForm"
      :rules="submitFormRules"
      :fields="submitFormFields"
      :active-template="activeTemplate"
      :user-options="flowUserOptions"
      :is-edit="isSubmitEdit"
      :saving="submitSaving"
      :form-ref="submitFormRef"
      flow-attachments-only
      @submit="onSubmit"
    />
    <!-- 新增 / 编辑 -->
    <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>
    <ApprovalTemplateBindDialog
      v-model:visible="templateBindVisible"
      :module-key="APPROVAL_MODULE_KEYS.COST_REIMBURSE"
      skip-form-confirm
      @confirm="onTemplateBound"
      @closed="onTemplateBindClosed"
    />
      <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>
    <ApprovalInstanceDetailDialog
      v-model="detailDialog.visible"
      title="费用报销详情"
      :row="detailRow"
      @edit="openEditFromDetail"
    />
      <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 { Search } from "@element-plus/icons-vue";
import { ElMessage } from "element-plus";
import { onMounted, reactive } from "vue";
import ApprovalInstanceDetailDialog from "../../ApproveManage/approve-shared/components/ApprovalInstanceDetailDialog.vue";
import ApprovalInstanceSubmitDialog from "../../ApproveManage/approve-shared/components/ApprovalInstanceSubmitDialog.vue";
import ApprovalTemplateBindDialog from "../../ApproveManage/approve-shared/components/ApprovalTemplateBindDialog.vue";
import { buildInstanceTableColumns } from "../../ApproveManage/approve-shared/approvalInstanceFormConfigTable.js";
import { APPROVAL_MODULE_KEYS } from "../../ApproveManage/approve-shared/approvalModuleRegistry.js";
import { useApprovalInstanceModule } from "../../ApproveManage/approve-shared/useApprovalInstanceModule.js";
import { useFlowUserOptions } from "../../ApproveManage/approve-shared/useFlowUserOptions.js";
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 searchForm = reactive({ applicantKeyword: "" });
const mod = useApprovalInstanceModule({
  moduleKey: APPROVAL_MODULE_KEYS.COST_REIMBURSE,
  buildExtraListParams(sf) {
    const extra = {};
    const kw = (sf?.applicantKeyword || "").trim();
    if (kw && /[\u4e00-\u9fa5]/.test(kw)) extra.applicantName = kw;
    return extra;
  },
});
const cr = useCostReimburse();
const {
  tableData,
  Search,
  EXPENSE_CATEGORY_OPTIONS,
  CATEGORY_TEMPLATES,
  EXPENSE_SUBJECT_OPTIONS,
  expenseSubjectLabel,
  searchForm,
  tableLoading,
  page,
  tableData,
  tableColumn,
  importInputRef,
  formRef,
  form,
  formDialog,
  formRules,
  detailDialog,
  detailRow,
  submitDialog,
  submitForm,
  submitFormRef,
  submitSaving,
  isSubmitEdit,
  activeTemplate,
  submitFormFields,
  submitFormRules,
  submitDialogTitle,
  templateBindVisible,
  approveDialog,
  approveOpinion,
  applicantFormSearchLoading,
  applicantFormOptions,
  flowUserOptions,
  detailTotalAmount,
  approvalRuleHint,
  handleQuery,
  initModuleList,
  resetSearch,
  pagination,
  openAddWithTemplate,
  onTemplateBound,
  onTemplateBindClosed,
  openEditFromDetail,
  submitInstanceForm,
  buildTableActions,
} = mod;
const { flowUserOptions, loadFlowUsers } = useFlowUserOptions();
const tableColumn = buildInstanceTableColumns(tableData, buildTableActions);
function onSearch() {
  handleQuery(searchForm);
}
function resetSearch() {
  searchForm.applicantKeyword = "";
  onSearch();
}
function onPagination(obj) {
  pagination(obj, searchForm);
}
async function onSubmit() {
  const ok = await submitInstanceForm({ skipValidate: true });
  if (ok) ElMessage.success(isSubmitEdit.value ? "修改成功" : "提交成功");
}
onMounted(async () => {
  loadFlowUsers();
  await initModuleList(searchForm);
});
  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;
@@ -153,8 +459,98 @@
  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>
src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/index.vue
@@ -1,4 +1,4 @@
<!--OA模块:差旅报销(审批实例 listPage,businessType=16)-->
<!--OA模块:差旅报销-->
<template>
  <div class="app-container">
    <div class="search_form mb20">
@@ -10,15 +10,40 @@
          placeholder="姓名或编号"
          clearable
          :prefix-icon="Search"
          @keyup.enter="onSearch"
          @keyup.enter="handleQuery"
        />
        <el-button type="primary" style="margin-left: 10px" @click="onSearch">搜索</el-button>
        <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>
        <el-button type="primary" @click="openAddWithTemplate">新增差旅报销</el-button>
      <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"
@@ -28,123 +53,462 @@
        :isSelection="false"
        :tableLoading="tableLoading"
        :total="page.total"
        @pagination="onPagination"
        @pagination="pagination"
      />
    </div>
    <ApprovalInstanceSubmitDialog
      v-model="submitDialog.visible"
      :title="submitDialogTitle"
      :form="submitForm"
      :rules="submitFormRules"
      :fields="submitFormFields"
      :active-template="activeTemplate"
      :user-options="flowUserOptions"
      :is-edit="isSubmitEdit"
      :saving="submitSaving"
      :form-ref="submitFormRef"
      flow-attachments-only
      @submit="onSubmit"
    />
    <!-- 新增 / 编辑 -->
    <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>
    <ApprovalTemplateBindDialog
      v-model:visible="templateBindVisible"
      :module-key="APPROVAL_MODULE_KEYS.TRAVEL_REIMBURSE"
      skip-form-confirm
      @confirm="onTemplateBound"
      @closed="onTemplateBindClosed"
    />
      <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>
    <ApprovalInstanceDetailDialog
      v-model="detailDialog.visible"
      title="差旅报销详情"
      :row="detailRow"
      @edit="openEditFromDetail"
    />
        <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 { Search } from "@element-plus/icons-vue";
import { ElMessage } from "element-plus";
import { onMounted, reactive } from "vue";
import ApprovalInstanceDetailDialog from "../../ApproveManage/approve-shared/components/ApprovalInstanceDetailDialog.vue";
import ApprovalInstanceSubmitDialog from "../../ApproveManage/approve-shared/components/ApprovalInstanceSubmitDialog.vue";
import ApprovalTemplateBindDialog from "../../ApproveManage/approve-shared/components/ApprovalTemplateBindDialog.vue";
import { buildInstanceTableColumns } from "../../ApproveManage/approve-shared/approvalInstanceFormConfigTable.js";
import { APPROVAL_MODULE_KEYS } from "../../ApproveManage/approve-shared/approvalModuleRegistry.js";
import { useApprovalInstanceModule } from "../../ApproveManage/approve-shared/useApprovalInstanceModule.js";
import { useFlowUserOptions } from "../../ApproveManage/approve-shared/useFlowUserOptions.js";
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 searchForm = reactive({ applicantKeyword: "" });
const mod = useApprovalInstanceModule({
  moduleKey: APPROVAL_MODULE_KEYS.TRAVEL_REIMBURSE,
  buildExtraListParams(sf) {
    const extra = {};
    const kw = (sf?.applicantKeyword || "").trim();
    if (kw && /[\u4e00-\u9fa5]/.test(kw)) extra.applicantName = kw;
    return extra;
  },
});
const tr = useTravelReimburse();
const {
  tableData,
  Search,
  EXPENSE_SUBJECT_OPTIONS,
  expenseSubjectLabel,
  searchForm,
  tableLoading,
  page,
  tableData,
  tableColumn,
  importInputRef,
  formRef,
  form,
  formDialog,
  formRules,
  detailDialog,
  detailRow,
  submitDialog,
  submitForm,
  submitFormRef,
  submitSaving,
  isSubmitEdit,
  activeTemplate,
  submitFormFields,
  submitFormRules,
  submitDialogTitle,
  templateBindVisible,
  approveDialog,
  approveOpinion,
  applicantFormSearchLoading,
  applicantFormOptions,
  flowUserOptions,
  travelDaysDisplay,
  travelTierLabel,
  suggestedLivingSubsidy,
  suggestedTransportSubsidy,
  suggestedHotelLimit,
  detailTotalAmount,
  overBudgetWarnings,
  budgetHint,
  handleQuery,
  initModuleList,
  resetSearch,
  pagination,
  openAddWithTemplate,
  onTemplateBound,
  onTemplateBindClosed,
  openEditFromDetail,
  submitInstanceForm,
  buildTableActions,
} = mod;
const { flowUserOptions, loadFlowUsers } = useFlowUserOptions();
const tableColumn = buildInstanceTableColumns(tableData, buildTableActions);
function onSearch() {
  handleQuery(searchForm);
}
function resetSearch() {
  searchForm.applicantKeyword = "";
  onSearch();
}
function onPagination(obj) {
  pagination(obj, searchForm);
}
async function onSubmit() {
  const ok = await submitInstanceForm({ skipValidate: true });
  if (ok) ElMessage.success(isSubmitEdit.value ? "修改成功" : "提交成功");
}
onMounted(async () => {
  loadFlowUsers();
  await initModuleList(searchForm);
});
  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;
@@ -153,8 +517,107 @@
  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>