Files
hzdyjs/web/src/views/rs/training/index.vue
T
2025-11-05 15:11:50 +08:00

4842 lines
162 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="app-container">
<el-card class="box-card">
<!-- 培训申请子选项卡 -->
<el-tabs v-model="trainingSubTab" @tab-click="handleTrainingSubTabClick">
<el-tab-pane label="培训申请" name="application" v-hasPermi="['rs:trainingapplication:list']">
<!-- 搜索区域 -->
<transition :enter-active-class="animate.searchAnimate.enter" :leave-active-class="animate.searchAnimate.leave">
<div v-show="showApplicationSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="applicationQueryForm" :model="applicationQueryParams" label-width="100px">
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="课程名称" prop="courseName">
<el-input
v-model="applicationQueryParams.courseName"
placeholder="请输入课程名称"
clearable
@keyup.enter="handleApplicationQuery"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="培训地点" prop="location">
<el-input
v-model="applicationQueryParams.location"
placeholder="请输入培训地点"
clearable
@keyup.enter="handleApplicationQuery"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="状态" prop="status">
<el-select v-model="applicationQueryParams.status" placeholder="请选择状态" clearable style="width: 100%">
<el-option v-for="dict in applicationStatusOptions" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24" class="search-btn-container">
<el-form-item>
<el-button type="primary" icon="Search" @click="handleApplicationQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetApplicationQuery">重置</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
</div>
</transition>
<!-- 操作按钮区域 -->
<el-card shadow="never" class="button-card table-card">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAddApplication" v-hasPermi="['rs:trainingapplication:add']"
>申请培训</el-button
>
</el-col>
<el-col :span="1.5">
<el-button
icon="Download"
@click="handleExportTrainingApplication"
v-hasPermi="['rs:trainingapplication:export']"
style="display: none"
>导出</el-button
>
</el-col>
<right-toolbar v-model:showSearch="showApplicationSearch" @queryTable="getApplicationList"></right-toolbar>
</el-row>
</template>
<!-- 培训申请表格 -->
<el-table v-loading="applicationLoading" border :data="applicationList" @row-dblclick="handleViewApplicant">
<el-table-column label="主办单位" align="center" prop="organizerUnit" min-width="160" :show-overflow-tooltip="true" />
<el-table-column label="培训地点" align="center" prop="location" min-width="120" :show-overflow-tooltip="true" />
<el-table-column label="培训类型" align="center" prop="trainingType" min-width="120" :show-overflow-tooltip="true" />
<el-table-column label="开始时间" align="center" prop="startTime" width="120">
<template #default="scope">
<span>{{ parseTime(scope.row.startTime, '{y}-{m}-{d}') || '暂无' }}</span>
</template>
</el-table-column>
<el-table-column label="结束时间" align="center" prop="endTime" width="120">
<template #default="scope">
<span>{{ parseTime(scope.row.endTime, '{y}-{m}-{d}') || '暂无' }}</span>
</template>
</el-table-column>
<el-table-column label="申请时间" align="center" prop="applyTime" width="160">
<template #default="scope">
<span>{{ parseTime(scope.row.applyTime) }}</span>
</template>
</el-table-column>
<el-table-column label="申请状态" align="center" prop="status" width="100">
<template #default="scope">
<el-tag
:type="scope.row.status === '0' ? 'warning' : scope.row.status === '1' ? 'success' : scope.row.status === '2' ? 'danger' : 'info'"
>
{{ getStatusLabel(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="220">
<template #default="scope">
<el-tooltip content="查看详情" placement="top">
<el-button
link
type="primary"
icon="View"
@click="handleViewApplicant(scope.row)"
v-hasPermi="['rs:trainingapplication:query']"
></el-button>
</el-tooltip>
<!-- 待审核状态:显示撤回按钮 -->
<el-tooltip content="撤回申请" placement="top" v-if="scope.row.status === '0'">
<el-button
link
type="warning"
icon="RefreshLeft"
@click="handleCancelApplication(scope.row)"
v-hasPermi="['rs:trainingapplication:cancel']"
></el-button>
</el-tooltip>
<!-- 已拒绝状态:显示重新提交按钮(需要修改并重新提交权限) -->
<el-tooltip content="重新提交" placement="top" v-if="scope.row.status === '2'">
<el-button
link
type="success"
icon="RefreshRight"
@click="handleUpdateAndResubmitApplication(scope.row)"
v-hasPermi="['rs:trainingapplication:edit', 'rs:trainingapplication:submit']"
></el-button>
</el-tooltip>
<!-- 已通过和已拒绝状态:显示审批历史 -->
<el-tooltip content="审批历史" placement="top" v-if="scope.row.status === '1' || scope.row.status === '2'">
<el-button link type="info" icon="InfoFilled" @click="handleViewHistory(scope.row)"></el-button>
</el-tooltip>
<!-- 删除按钮:仅在未通过的状态下显示(待审核、已拒绝、已取消) -->
<el-tooltip content="删除" placement="top" v-if="scope.row.status !== '1'">
<el-button
link
type="danger"
icon="Delete"
@click="handleDeleteApplication(scope.row)"
v-hasPermi="['rs:trainingapplication:remove']"
></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<pagination
v-show="applicationTotal > 0"
:total="applicationTotal"
v-model:page="applicationQueryParams.pageNum"
v-model:limit="applicationQueryParams.pageSize"
@pagination="getApplicationList"
/>
</div>
</el-card>
</el-tab-pane>
<el-tab-pane label="培训记录" name="record" v-hasPermi="['rs:trainingrecord:list']">
<!-- 搜索区域 -->
<transition :enter-active-class="animate.searchAnimate.enter" :leave-active-class="animate.searchAnimate.leave">
<div v-show="showRecordSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="recordQueryForm" :model="recordQueryParams" label-width="100px">
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="课程名称" prop="courseName">
<el-input v-model="recordQueryParams.courseName" placeholder="请输入课程名称" clearable @keyup.enter="handleRecordQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="培训地点" prop="location">
<el-input v-model="recordQueryParams.location" placeholder="请输入培训地点" clearable @keyup.enter="handleRecordQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="部门" prop="department">
<el-input v-model="recordQueryParams.department" placeholder="请输入部门" clearable style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24" class="search-btn-container">
<el-form-item>
<el-button type="primary" icon="Search" @click="handleRecordQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetRecordQuery">重置</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
</div>
</transition>
<!-- 培训记录管理内容 -->
<el-card shadow="never" class="table-card">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button icon="Download" @click="handleExportRecord" v-hasPermi="['rs:trainingrecord:export']" style="display: none"
>导出</el-button
>
</el-col>
<right-toolbar v-model:showSearch="showRecordSearch" @queryTable="getRecordList"></right-toolbar>
</el-row>
</template>
<!-- 培训记录表格 -->
<el-table
v-loading="recordLoading"
border
:data="recordList"
@selection-change="handleRecordSelectionChange"
@row-dblclick="handleViewRecord"
>
<el-table-column type="selection" width="55" align="center" />
<!-- 主办单位:从申请表course_name字段关联获取 -->
<el-table-column label="主办单位" align="center" prop="organizerUnit" min-width="150" :show-overflow-tooltip="true">
<template #default="scope">
<span>{{ scope.row.organizerUnit || '暂无' }}</span>
</template>
</el-table-column>
<!-- 培训类型:从申请表training_type字段关联获取 -->
<el-table-column label="培训类型" align="center" prop="trainingType" min-width="110" :show-overflow-tooltip="true">
<template #default="scope">
<span>{{ scope.row.trainingType || '暂无' }}</span>
</template>
</el-table-column>
<!-- 开始时间:从记录表start_time字段获取 -->
<el-table-column label="开始时间" align="center" prop="startTime" width="120">
<template #default="scope">
<span>{{ parseTime(scope.row.startTime, '{y}-{m}-{d}') || '暂无' }}</span>
</template>
</el-table-column>
<!-- 结束时间:从记录表end_time字段获取 -->
<el-table-column label="结束时间" align="center" prop="endTime" width="120">
<template #default="scope">
<span>{{ parseTime(scope.row.endTime, '{y}-{m}-{d}') || '暂无' }}</span>
</template>
</el-table-column>
<!-- 同行人员:从申请表companions字段关联获取,格式化为教师姓名 -->
<el-table-column label="同行人员" align="center" prop="companions" min-width="120" :show-overflow-tooltip="true">
<template #default="scope">
<span v-if="scope.row.companions && scope.row.companions !== '[]'">
{{ formatCompanions(scope.row.companions) }}
</span>
<span v-else>无</span>
</template>
</el-table-column>
<el-table-column label="完成状态" align="center" prop="completionStatus" width="100">
<template #default="scope">
<el-tag :type="getCompletionStatusType(scope.row.completionStatus)">
{{ getCompletionStatusLabel(scope.row.completionStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="150">
<template #default="scope">
<el-tooltip content="查看" placement="top">
<el-button
link
type="primary"
icon="View"
@click.stop="handleViewRecord(scope.row)"
v-hasPermi="['rs:trainingrecord:query']"
></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button
link
type="danger"
icon="Delete"
@click.stop="handleDeleteRecord(scope.row)"
v-hasPermi="['rs:trainingrecord:remove']"
></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<pagination
v-show="recordTotal > 0"
:total="recordTotal"
v-model:page="recordQueryParams.pageNum"
v-model:limit="recordQueryParams.pageSize"
@pagination="getRecordList"
/>
</div>
</el-card>
</el-tab-pane>
</el-tabs>
</el-card>
<!-- 添加或修改培训课程对话框 -->
<el-dialog :title="courseDialogTitle" v-model="courseDialogVisible" width="700px" append-to-body>
<el-form ref="courseFormRef" :model="courseForm" :rules="courseRules" label-width="100px">
<el-form-item label="课程名称" prop="title">
<el-input v-model="courseForm.title" placeholder="请输入课程名称" />
</el-form-item>
<el-form-item label="课程类别" prop="category">
<el-select v-model="courseForm.category" placeholder="请选择课程类别" style="width: 100%">
<el-option v-for="dict in categoryOptions" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="开始时间" prop="startTime">
<el-date-picker
v-model="courseForm.startTime"
type="datetime"
placeholder="选择开始时间"
style="width: 100%"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
<el-form-item label="结束时间" prop="endTime">
<el-date-picker
v-model="courseForm.endTime"
type="datetime"
placeholder="选择结束时间"
style="width: 100%"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
<el-form-item label="学时数" prop="duration">
<el-input-number v-model="courseForm.duration" :min="1" :max="999" controls-position="right" style="width: 100%" />
</el-form-item>
<el-form-item label="培训地点" prop="location">
<el-input v-model="courseForm.location" placeholder="请输入培训地点" />
</el-form-item>
<el-form-item label="课程描述" prop="description">
<el-input v-model="courseForm.description" type="textarea" placeholder="请输入课程描述" :rows="4" />
</el-form-item>
<el-form-item label="状态" prop="status" v-if="courseForm.courseId">
<el-radio-group v-model="courseForm.status">
<el-radio label="0">正常</el-radio>
<el-radio label="1">结束</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="courseForm.remark" type="textarea" placeholder="请输入备注" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="courseDialogLoading" type="primary" @click="submitCourseForm">确 定</el-button>
<el-button @click="cancelCourseDialog">取 消</el-button>
</div>
</template>
</el-dialog>
<!-- 查看培训课程详情对话框 -->
<el-dialog title="培训课程详情" v-model="courseDetailDialogVisible" width="700px" append-to-body>
<el-descriptions :column="2" border>
<el-descriptions-item label="课程名称">{{ courseDetail.title }}</el-descriptions-item>
<el-descriptions-item label="课程类别">{{ getCategoryLabel(courseDetail.category) }}</el-descriptions-item>
<el-descriptions-item label="开始时间">{{ parseTime(courseDetail.startTime) }}</el-descriptions-item>
<el-descriptions-item label="结束时间">{{ parseTime(courseDetail.endTime) }}</el-descriptions-item>
<el-descriptions-item label="学时数">{{ courseDetail.duration }}</el-descriptions-item>
<el-descriptions-item label="培训地点">{{ courseDetail.location || '暂无' }}</el-descriptions-item>
<el-descriptions-item label="已报名人数">{{ courseDetail.currentParticipants || 0 }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="courseDetail.status === '0' ? 'success' : 'info'">
{{ courseDetail.status === '0' ? '正常' : '结束' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ parseTime(courseDetail.createTime) }}</el-descriptions-item>
<el-descriptions-item label="课程描述" :span="2">
<div class="detail-text">{{ courseDetail.description || '暂无描述' }}</div>
</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">{{ courseDetail.remark || '暂无备注' }}</el-descriptions-item>
</el-descriptions>
<template #footer>
<div class="dialog-footer">
<el-button
type="primary"
@click="handleViewApplications(courseDetail)"
v-if="courseDetail.courseId"
v-hasPermi="['rs:trainingapplication:list']"
>查看申请记录</el-button
>
<el-button @click="courseDetailDialogVisible = false">关 闭</el-button>
</div>
</template>
</el-dialog>
<!-- 申请详情对话框 -->
<el-dialog title="申请详情" v-model="applicationDetailOpen" width="800px" append-to-body>
<el-descriptions :column="2" border>
<!-- 情况基本信息 -->
<el-descriptions-item label="申请人">{{ applicationDetail.nickName || applicationDetail.userName }}</el-descriptions-item>
<el-descriptions-item label="所在部门(系部)">{{ applicationDetail.department }}</el-descriptions-item>
<el-descriptions-item label="现职称">{{ applicationDetail.currentTitle || '暂无' }}</el-descriptions-item>
<el-descriptions-item label="现岗位">{{ applicationDetail.currentPosition || '暂无' }}</el-descriptions-item>
<el-descriptions-item label="参加工作时间">{{ parseTime(applicationDetail.workStartTime) || '暂无' }}</el-descriptions-item>
<el-descriptions-item label="联系电话">{{ applicationDetail.phone || '暂无' }}</el-descriptions-item>
<!-- 培训情况信息 -->
<el-descriptions-item label="主办单位">{{ applicationDetail.organizerUnit || applicationDetail.courseName || '暂无' }}</el-descriptions-item>
<el-descriptions-item label="培训地点">{{ applicationDetail.location }}</el-descriptions-item>
<el-descriptions-item label="开始时间">{{ parseTime(applicationDetail.startTime) }}</el-descriptions-item>
<el-descriptions-item label="结束时间">{{ parseTime(applicationDetail.endTime) }}</el-descriptions-item>
<el-descriptions-item label="培训类型">{{ applicationDetail.trainingType || '暂无' }}</el-descriptions-item>
<el-descriptions-item label="培训费用">{{ applicationDetail.trainingFee ? `${applicationDetail.trainingFee}元` : '暂无' }}</el-descriptions-item>
<el-descriptions-item label="申请状态">
<el-tag
:type="
applicationDetail.status === '0'
? 'warning'
: applicationDetail.status === '1'
? 'success'
: applicationDetail.status === '2'
? 'danger'
: 'info'
"
>
{{ getStatusLabel(applicationDetail.status) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="申请时间">{{ parseTime(applicationDetail.applyTime) }}</el-descriptions-item>
<el-descriptions-item label="同行人" :span="2">{{ getCompanionNames(applicationDetail.companions) || '无' }}</el-descriptions-item>
<el-descriptions-item label="培训目的" :span="2">{{ applicationDetail.trainingPurpose || applicationDetail.reason || '暂无' }}</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">{{ applicationDetail.remark || '无' }}</el-descriptions-item>
<!-- 显示拒绝原因,如果申请被拒绝 -->
<el-descriptions-item v-if="applicationDetail.status === '2'" label="拒绝原因" :span="2" class="reject-reason">
<div class="reject-info">
<el-alert type="error" :closable="false">
<template #title>
<div class="reject-title">申请已被拒绝</div>
</template>
<div class="reject-content">{{ applicationDetail.adminRemark || '未提供拒绝原因' }}</div>
</el-alert>
<div class="reject-action">
<el-button type="primary" size="small" @click="handleViewHistory(applicationDetail)">查看完整审批历史</el-button>
</div>
</div>
</el-descriptions-item>
</el-descriptions>
<!-- 申请附件显示区域 -->
<div v-if="applicationDetailAttachmentList.length > 0" class="attachment-section" style="margin-top: 20px;">
<div class="section-title" style="margin-bottom: 15px; padding: 10px; border-left: 4px solid #409eff; background-color: #f5f7fa;">
<el-icon style="margin-right: 8px;"><Paperclip /></el-icon>
<strong>申请附件</strong>
</div>
<el-table :data="applicationDetailAttachmentList" border size="small">
<el-table-column label="序号" type="index" width="60" align="center" />
<el-table-column label="文件名称" prop="name" align="left" show-overflow-tooltip />
<el-table-column label="上传时间" prop="uploadTime" width="180" align="center">
<template #default="scope">
{{ parseTime(scope.row.uploadTime) }}
</template>
</el-table-column>
<el-table-column label="操作" width="100" align="center">
<template #default="scope">
<el-button type="primary" link icon="Download" @click="handleDownloadApplicationAttachment(scope.row)" size="small">
下载
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="applicationDetailOpen = false">关闭</el-button>
</div>
</template>
</el-dialog>
<!-- 审批历史对话框 -->
<el-dialog title="审批历史记录" v-model="approvalHistoryOpen" width="700px" append-to-body>
<el-timeline>
<el-timeline-item
v-for="(approval, index) in approvalHistoryList"
:key="index"
:timestamp="parseTime(approval.approvalTime)"
:type="approval.status === '1' ? 'success' : approval.status === '2' ? 'danger' : 'primary'"
>
<div class="approval-history-item">
<div class="approval-header">
<span class="approver-name">{{ approval.approverName }}</span>
<span class="approval-level">[{{ getApprovalLevelLabel(approval.approvalLevel) }}]</span>
<el-tag :type="approval.status === '0' ? 'info' : approval.status === '1' ? 'success' : 'danger'" size="small">
{{ approval.status === '0' ? '待审核' : approval.status === '1' ? '已通过' : '已拒绝' }}
</el-tag>
</div>
<div class="approval-opinion">{{ approval.opinion || '无审批意见' }}</div>
</div>
</el-timeline-item>
</el-timeline>
<div v-if="approvalHistoryList.length === 0" class="no-data">暂无审批记录</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="approvalHistoryOpen = false">关闭</el-button>
</div>
</template>
</el-dialog>
<!-- 培训申请对话框 -->
<el-dialog :title="applicationDialogTitle" v-model="applicationDialogVisible" width="800px" append-to-body>
<el-form ref="applicationFormRef" :model="applicationForm" :rules="applicationRules" label-width="140px">
<!-- 基本情况信息 -->
<el-divider content-position="left">
<span style="font-weight: bold; color: #409eff;">基本情况</span>
</el-divider>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="姓名" prop="userName">
<el-input v-model="applicationForm.userName" placeholder="申请人姓名" readonly class="readonly-input" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="所在部门(系部)" prop="department">
<el-input v-model="applicationForm.department" placeholder="请输入所属部门" readonly class="readonly-input" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="现职称" prop="currentTitle">
<el-input v-model="applicationForm.currentTitle" placeholder="请输入现职称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="现岗位" prop="currentPosition">
<el-radio-group v-model="applicationForm.currentPosition" style="display: flex; flex-wrap: nowrap; align-items: center;">
<el-radio label="教学" style="margin-right: 20px;">教学</el-radio>
<el-radio label="管理" style="margin-right: 20px;">管理</el-radio>
<el-radio label="其他">其他</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="参加工作时间" prop="workStartTime">
<el-date-picker
v-model="applicationForm.workStartTime"
type="date"
placeholder="选择参加工作时间"
style="width: 100%"
value-format="YYYY-MM-DD"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系电话" prop="phone">
<el-input v-model="applicationForm.phone" placeholder="请输入联系电话" />
</el-form-item>
</el-col>
</el-row>
<!-- 培训情况信息 -->
<el-divider content-position="left">
<span style="font-weight: bold; color: #409eff;">培训情况</span>
</el-divider>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="主办单位" prop="organizerUnit">
<el-input v-model="applicationForm.organizerUnit" placeholder="请输入主办单位" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="培训地点" prop="location">
<el-input v-model="applicationForm.location" placeholder="请输入培训地点" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="培训时间" required>
<el-col :span="11">
<el-form-item prop="startTime">
<el-date-picker
v-model="applicationForm.startTime"
type="date"
placeholder="开始日期"
style="width: 100%"
value-format="YYYY-MM-DD"
/>
</el-form-item>
</el-col>
<el-col :span="2" class="text-center">
<span class="text-gray-500">至</span>
</el-col>
<el-col :span="11">
<el-form-item prop="endTime">
<el-date-picker
v-model="applicationForm.endTime"
type="date"
placeholder="结束日期"
style="width: 100%"
value-format="YYYY-MM-DD"
/>
</el-form-item>
</el-col>
</el-form-item>
<el-form-item label="培训类型" prop="trainingType">
<div style="display: flex; flex-wrap: wrap; gap: 20px; align-items: center;">
<el-radio-group v-model="applicationForm.trainingType" style="display: flex; flex-wrap: nowrap; gap: 20px;">
<el-radio label="教学">教学</el-radio>
<el-radio label="德育">德育</el-radio>
<el-radio label="行政">行政</el-radio>
<el-radio label="管理">管理</el-radio>
<el-radio label="其他">其他</el-radio>
</el-radio-group>
<el-input
v-if="applicationForm.trainingType === '其他'"
v-model="applicationForm.trainingTypeOther"
placeholder="请输入具体培训类型"
style="width: 200px; margin-left: 10px;"
clearable
/>
</div>
</el-form-item>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="同行人" prop="companions">
<TeacherMultiSelector
v-model="selectedCompanions"
placeholder="请选择同行人员"
@change="handleCompanionsChange"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="培训费用" prop="trainingFee">
<el-input-number
v-model="applicationForm.trainingFee"
:min="0"
:precision="2"
style="width: 100%"
placeholder="请输入培训费用"
>
<template #append>元</template>
</el-input-number>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="培训目的" prop="trainingPurpose">
<el-input v-model="applicationForm.trainingPurpose" type="textarea" placeholder="请输入培训目的" :rows="3" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="applicationForm.remark" type="textarea" placeholder="请输入备注" :rows="2" />
</el-form-item>
<!-- 上传附件 -->
<el-form-item label="上传附件">
<div class="attachment-upload-container">
<el-upload
v-model:file-list="applicationAttachmentFileList"
class="attachment-upload-demo"
drag
list-type="picture-card"
:action="applicationUploadAction"
:headers="applicationUploadHeaders"
:data="applicationUploadData"
:auto-upload="true"
:before-upload="beforeApplicationAttachmentUpload"
:on-success="handleApplicationAttachmentSuccess"
:on-error="handleApplicationAttachmentError"
:on-remove="handleRemoveApplicationAttachment"
:limit="10"
:show-file-list="true"
accept=".pdf,.doc,.docx,.jpg,.jpeg,.png,.xls,.xlsx"
>
<!-- 拖拽上传区域 -->
<div class="el-upload-dragger-attachment">
<el-icon class="el-icon--upload"><Upload /></el-icon>
<div class="el-upload__text">将附件文件拖到此处,或<em>点击上传</em></div>
</div>
<!-- 自定义文件卡片 -->
<template #file="{ file }">
<div class="upload-file-card">
<!-- 缩略图区域 -->
<div class="upload-file-thumb">
<image-preview
v-if="isImageFile(file) && file.url && getFileOssId(file)"
:src="getFileOssId(file)"
class="upload-thumb-image"
:isOssId="true"
/>
<div
v-else-if="isImageFile(file) && file.url"
class="upload-file-image"
@click="handlePreviewApplicationAttachment(file)"
style="cursor: pointer"
>
<img :src="file.url" :alt="file.name" style="width: 100%; height: 100%; object-fit: cover" />
</div>
<div v-else class="upload-file-placeholder">
<el-icon><Document /></el-icon>
<span class="file-ext">{{ getFileExt(file.name) }}</span>
</div>
<!-- loading 遮罩 -->
<div v-if="file.status === 'uploading'" class="upload-file-loading">
<el-icon class="is-loading"><Loading /></el-icon>
<span class="loading-text">上传中...</span>
</div>
</div>
<!-- 文件名称 -->
<div class="upload-file-name" :title="file.name">
{{ getShortFileName(file.name) }}
</div>
<!-- 操作按钮区域 -->
<div class="upload-file-card-actions" v-if="file.status && file.status !== 'ready'">
<el-button-group size="small">
<el-button type="success" @click.stop="handleDownloadApplicationAttachment(file)" title="下载" :disabled="file.status !== 'success'">
<el-icon><Download /></el-icon>
</el-button>
<el-button type="danger" @click.stop="confirmRemoveApplicationAttachment(file)" title="删除">
<el-icon><Delete /></el-icon>
</el-button>
</el-button-group>
</div>
</div>
</template>
</el-upload>
</div>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="applicationDialogLoading" type="primary" @click="submitApplicationForm">确 定</el-button>
<el-button @click="cancelApplicationDialog">取 消</el-button>
</div>
</template>
</el-dialog>
<!-- 培训记录对话框 -->
<el-dialog :title="recordDialogTitle" v-model="recordDialogVisible" width="700px" append-to-body>
<el-form ref="recordFormRef" :model="recordForm" :rules="recordRules" label-width="100px">
<el-form-item label="课程名称">
<el-input v-model="recordForm.courseName" :disabled="true" />
</el-form-item>
<el-form-item label="学时">
<el-input v-model="recordForm.duration" :disabled="true" />
</el-form-item>
<el-form-item label="完成状态" prop="completionStatus">
<el-radio-group v-model="recordForm.completionStatus">
<el-radio label="1">已完成</el-radio>
<el-radio label="2">未完成</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="完成时间" prop="completionTime">
<el-date-picker
v-model="recordForm.completionTime"
type="datetime"
placeholder="选择完成时间"
style="width: 100%"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
<el-form-item label="证书编号" prop="certificateNumber">
<el-input v-model="recordForm.certificateNumber" placeholder="请输入证书编号(如有)" />
</el-form-item>
<el-form-item label="培训证书" prop="certificatePath">
<div class="certificate-upload-container">
<el-upload
v-model:file-list="recordCertificateFileList"
class="certificate-upload-demo"
drag
list-type="picture-card"
:action="recordCertificateUploadAction"
:headers="recordCertificateUploadHeaders"
:data="recordCertificateUploadData"
:auto-upload="true"
:before-upload="beforeRecordCertificateUpload"
:on-success="handleRecordCertificateSuccess"
:on-error="handleRecordCertificateError"
:on-remove="handleRemoveCertificate"
:limit="15"
:show-file-list="true"
accept=".pdf,.jpg,.jpeg,.png"
>
<!-- 拖拽上传区域 -->
<div class="el-upload-dragger-certificate">
<el-icon class="el-icon--upload"><Upload /></el-icon>
<div class="el-upload__text">将证书文件拖到此处,或<em>点击上传</em></div>
<div class="el-upload__tip" style="margin-top: 4px; font-size: 11px; color: #909399">
支持PDF/JPG/PNG格式,文件大小不超过10MB,最多上传15个文件
</div>
</div>
<!-- 自定义文件卡片 -->
<template #file="{ file }">
<div class="upload-file-card">
<!-- 缩略图区域 -->
<div class="upload-file-thumb">
<image-preview
v-if="isImageFile(file) && file.url && getFileOssId(file)"
:src="getFileOssId(file)"
class="upload-thumb-image"
:isOssId="true"
/>
<div
v-else-if="isImageFile(file) && file.url"
class="upload-file-image"
@click="handlePreviewCertificate(file)"
style="cursor: pointer"
>
<img :src="file.url" :alt="file.name" style="width: 100%; height: 100%; object-fit: cover" />
</div>
<div v-else class="upload-file-placeholder">
<el-icon><Document /></el-icon>
<span class="file-ext">{{ getFileExt(file.name) }}</span>
</div>
<!-- loading 遮罩 -->
<div v-if="file.status === 'uploading'" class="upload-file-loading">
<el-icon class="is-loading"><Loading /></el-icon>
<span class="loading-text">上传中...</span>
</div>
</div>
<!-- 文件名称 -->
<div class="upload-file-name" :title="file.name">
{{ getShortFileName(file.name) }}
</div>
<!-- 操作按钮区域 -->
<div class="upload-file-card-actions" v-if="file.status && file.status !== 'ready'">
<el-button-group size="small">
<el-button type="success" @click.stop="handleDownloadCertificate(file)" title="下载" :disabled="file.status !== 'success'">
<el-icon><Download /></el-icon>
</el-button>
<el-button type="danger" @click.stop="confirmRemoveCertificate(file)" title="删除">
<el-icon><Delete /></el-icon>
</el-button>
</el-button-group>
</div>
</div>
</template>
</el-upload>
</div>
</el-form-item>
<el-form-item label="培训反馈" prop="feedback">
<el-input v-model="recordForm.feedback" type="textarea" :rows="3" placeholder="请输入培训反馈意见" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="recordForm.remark" type="textarea" placeholder="请输入备注" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="recordDialogLoading" type="primary" @click="submitRecordForm">确 定</el-button>
<el-button @click="cancelRecordDialog">取 消</el-button>
</div>
</template>
</el-dialog>
<!-- 培训记录详情对话框 -->
<el-dialog title="培训记录详情" v-model="recordDetailDialogVisible" width="800px" append-to-body>
<el-descriptions :column="2" border>
<!-- 学员基本信息 -->
<el-descriptions-item label="学员姓名">{{ recordDetail.userName }}</el-descriptions-item>
<el-descriptions-item label="所在部门(系部)">{{ recordDetail.department || '暂无' }}</el-descriptions-item>
<el-descriptions-item label="现职称">{{ recordDetail.currentTitle || '暂无' }}</el-descriptions-item>
<el-descriptions-item label="现岗位">{{ recordDetail.currentPosition || '暂无' }}</el-descriptions-item>
<el-descriptions-item label="参加工作时间">{{ parseTime(recordDetail.workStartTime) || '暂无' }}</el-descriptions-item>
<el-descriptions-item label="联系电话">{{ recordDetail.phone || '暂无' }}</el-descriptions-item>
<!-- 培训情况信息 -->
<el-descriptions-item label="主办单位">{{ recordDetail.organizerUnit || recordDetail.courseName || '暂无' }}</el-descriptions-item>
<el-descriptions-item label="培训地点">{{ recordDetail.location || '暂无' }}</el-descriptions-item>
<el-descriptions-item label="开始时间">{{ parseTime(recordDetail.startTime) || '暂无' }}</el-descriptions-item>
<el-descriptions-item label="结束时间">{{ parseTime(recordDetail.endTime) || '暂无' }}</el-descriptions-item>
<el-descriptions-item label="培训类型">{{ recordDetail.trainingType || '暂无' }}</el-descriptions-item>
<el-descriptions-item label="培训费用">{{ recordDetail.trainingFee ? `${recordDetail.trainingFee}元` : '暂无' }}</el-descriptions-item>
<el-descriptions-item label="申请时间">{{ parseTime(recordDetail.applyTime) || '暂无' }}</el-descriptions-item>
<el-descriptions-item label="完成状态">
<el-tag :type="getCompletionStatusType(recordDetail.completionStatus)">
{{ getCompletionStatusLabel(recordDetail.completionStatus) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="同行人" :span="2">{{ getCompanionNames(recordDetail.companions) || '无' }}</el-descriptions-item>
<el-descriptions-item label="培训目的" :span="2">{{ recordDetail.trainingPurpose || recordDetail.reason || '暂无' }}</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">{{ recordDetail.remark || '暂无备注' }}</el-descriptions-item>
</el-descriptions>
<!-- 申请附件显示区域 -->
<div v-if="recordDetailAttachmentList.length > 0" class="attachment-section" style="margin-top: 20px;">
<div class="section-title" style="margin-bottom: 15px; padding: 10px; border-left: 4px solid #409eff; background-color: #f5f7fa;">
<el-icon style="margin-right: 8px;"><Paperclip /></el-icon>
<strong>申请附件</strong>
</div>
<el-table :data="recordDetailAttachmentList" border size="small">
<el-table-column label="序号" type="index" width="60" align="center" />
<el-table-column label="文件名称" prop="name" align="left" show-overflow-tooltip />
<el-table-column label="上传时间" prop="uploadTime" width="180" align="center">
<template #default="scope">
{{ parseTime(scope.row.uploadTime) }}
</template>
</el-table-column>
<el-table-column label="操作" width="100" align="center">
<template #default="scope">
<el-button type="primary" link icon="Download" @click="handleDownloadApplicationAttachment(scope.row)" size="small">
下载
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="recordDetailDialogVisible = false">关 闭</el-button>
</div>
</template>
</el-dialog>
<!-- 上传证书对话框 -->
<el-dialog title="上传培训证书" v-model="certificateDialogVisible" width="500px" append-to-body>
<el-upload
ref="uploadRef"
:action="`/dev-api/rs/training/record/certificate/upload/${certificateUploadData.recordId}`"
:headers="uploadHeaders"
:before-upload="beforeCertificateUpload"
:on-success="handleCertificateSuccess"
:on-error="handleCertificateError"
:file-list="certificateFileList"
:limit="1"
:auto-upload="true"
accept=".pdf,.jpg,.jpeg,.png"
class="certificate-upload"
>
<el-button type="primary">选择文件</el-button>
<template #tip>
<div class="el-upload__tip">只能上传PDF/JPG/PNG文件,且不超过10MB</div>
</template>
</el-upload>
<template #footer>
<div class="dialog-footer">
<el-button @click="certificateDialogVisible = false">取 消</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts" name="TrainingManagement">
import { ref, reactive, onMounted, onActivated, getCurrentInstance, ComponentInternalInstance, watch } from 'vue';
import {
Plus,
Edit,
Delete,
Search,
Download,
Upload,
View,
Document,
RefreshRight,
RefreshLeft,
InfoFilled,
Loading,
Picture,
Paperclip
} from '@element-plus/icons-vue';
import ImagePreview from '@/components/ImagePreview/index.vue';
import {
// 培训课程相关接口已废弃 (2025-01-03)
// listTrainingCourse, getTrainingCourse, addTrainingCourse, updateTrainingCourse, delTrainingCourse, toggleCourseStatus, exportTrainingCourse,
listTrainingApplication,
getTrainingApplication,
addTrainingApplication,
updateTrainingApplication,
cancelTrainingApplication,
exportTrainingApplication,
listMyTrainingApplication,
listTrainingRecord,
getTrainingRecord,
addTrainingRecord,
updateTrainingRecord,
delTrainingRecord,
exportTrainingRecord,
submitTrainingApplication,
updateApplicationStatus,
deleteTrainingApplication,
getTrainingApprovalHistory,
resubmitTrainingApplication,
updateRecordAttachments,
listAllApprovalRecords
} from '@/api/rs/training';
import { parseTime } from '@/utils/ruoyi';
import request from '@/utils/request';
import { ElLoading } from 'element-plus';
import type {
CourseQuery,
CourseVO,
CourseForm,
RecordQuery,
RecordVO,
RecordForm,
ApplicationQuery,
ApplicationVO,
ApplicationForm
} from '@/api/rs/training';
import { listDept, getDept } from '@/api/system/dept';
import type { DeptVO } from '@/api/system/dept/types';
import { getFilePreviewUrl } from '@/api/system/file';
import { getToken } from '@/utils/auth';
import { getInfo as getUserInfo } from '@/api/login';
import { useUserStore } from '@/store/modules/user';
import TeacherMultiSelector from '@/components/TeacherMultiSelector/index.vue';
import { TeacherVO } from '@/api/system/teacher/types';
import { listTeacher } from '@/api/system/teacher';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const userStore = useUserStore();
// 环境变量
const baseApi = import.meta.env.VITE_APP_BASE_API;
const clientId = import.meta.env.VITE_APP_CLIENT_ID;
const animate = proxy?.animate || { searchAnimate: { enter: 'animate__fadeInUp', leave: 'animate__fadeOutDown' } };
// ===== 申请培训附件上传配置(统一使用computed方式) =====
const applicationUploadAction = computed(() => {
const api = import.meta.env.VITE_APP_BASE_API + '/resource/oss/upload';
console.log('[申请培训附件上传配置] 上传地址:', api);
return api;
});
const applicationUploadHeaders = computed(() => {
const token = getToken() || '';
const headers = {
'Authorization': `Bearer ${token}`,
'clientid': import.meta.env.VITE_APP_CLIENT_ID || 'e5cd7e4891bf95d1d19206ce24a7b32e'
};
console.log('[申请培训附件上传配置] 请求头:', headers);
return headers;
});
const applicationUploadData = computed(() => {
return {
bizType: 'training_application_attachment'
};
});
// ===== 培训证书上传配置(统一使用computed方式) =====
const recordCertificateUploadAction = computed(() => {
const api = import.meta.env.VITE_APP_BASE_API + '/resource/oss/upload';
console.log('[培训证书上传配置] 上传地址:', api);
return api;
});
const recordCertificateUploadHeaders = computed(() => {
const token = getToken() || '';
const headers = {
'Authorization': `Bearer ${token}`,
'clientid': import.meta.env.VITE_APP_CLIENT_ID || 'e5cd7e4891bf95d1d19206ce24a7b32e'
};
console.log('[培训证书上传配置] 请求头:', headers);
return headers;
});
const recordCertificateUploadData = computed(() => {
return {
bizType: 'training_certificate_attachment'
};
});
// 培训申请相关
// 培训课程遮罩层
const trainingLoading = ref(false);
// 培训选中数组
const trainingIds = ref<Array<string | number>>([]);
// 培训非单个禁用
const trainingSingle = ref(true);
// 培训非多个禁用
const trainingMultiple = ref(true);
// 显示培训搜索条件
const showTrainingSearch = ref(true);
// 显示申请搜索条件
const showApplicationSearch = ref(true);
// 培训总条数
const trainingTotal = ref(0);
// 培训课程表格数据
const courseList = ref<CourseVO[]>([]);
// 当前激活的子选项卡
const trainingSubTab = ref('application'); // 默认显示培训申请选项卡
// 添加缓存状态标记
const isInitialized = ref(false);
const isDataLoaded = ref(false);
// 课程类别选项
const categoryOptions = ref([
{ value: 'skill', label: '技能培训' },
{ value: 'management', label: '管理培训' },
{ value: 'tech', label: '技术培训' },
{ value: 'safety', label: '安全培训' },
{ value: 'law', label: '法规培训' },
{ value: 'other', label: '其他培训' }
]);
// 课程状态选项(计算型)
const getCourseStatusType = (row: CourseVO): string => {
const now = new Date().getTime();
const startTime = new Date(row.startTime).getTime();
const endTime = new Date(row.endTime).getTime();
if (row.status !== '0') {
return 'info'; // 停用状态
} else if (now < startTime) {
return 'warning'; // 待开课
} else if (now >= startTime && now <= endTime) {
return 'success'; // 进行中
} else {
return 'info'; // 已结束,返回默认颜色
}
};
const getCourseStatusLabel = (row: CourseVO): string => {
const now = new Date().getTime();
const startTime = new Date(row.startTime).getTime();
const endTime = new Date(row.endTime).getTime();
if (row.status !== '0') {
return '已结束';
} else if (now < startTime) {
return '待开课';
} else if (now >= startTime && now <= endTime) {
return '进行中';
} else {
return '已结束';
}
};
// 培训课程查询参数
const trainingQueryParams = reactive<CourseQuery>({
pageNum: 1,
pageSize: 10,
title: '',
category: '',
status: '',
params: {}
});
// 培训课程对话框相关
const courseDialogVisible = ref(false);
const courseDialogTitle = ref('');
const courseDialogLoading = ref(false);
const courseForm = reactive<CourseForm>({
courseId: undefined,
title: '',
category: '',
description: '',
startTime: '',
endTime: '',
duration: 0,
location: '',
status: '0',
remark: ''
});
// 培训课程表单校验规则
const courseRules = reactive({
title: [
{ required: true, message: '课程名称不能为空', trigger: 'blur' },
{ min: 2, max: 100, message: '长度在 2 到 100 个字符', trigger: 'blur' }
],
category: [{ required: true, message: '课程类别不能为空', trigger: 'change' }],
startTime: [{ required: true, message: '开始时间不能为空', trigger: 'change' }],
endTime: [{ required: true, message: '结束时间不能为空', trigger: 'change' }],
duration: [{ required: true, message: '学时数不能为空', trigger: 'blur' }]
} as any);
// 培训课程表单引用
const courseFormRef = ref<any>(null);
// 培训课程详情
const courseDetailDialogVisible = ref(false);
const courseDetail = reactive<CourseVO>({
courseId: undefined,
title: '',
category: '',
description: '',
startTime: '',
endTime: '',
duration: 0,
location: '',
maxParticipants: 0,
currentParticipants: 0,
status: '0',
applicationCount: 0,
createTime: '',
updateTime: '',
createBy: '',
updateBy: '',
remark: ''
});
// 培训申请相关
const applicationLoading = ref(false);
const applicationList = ref<any[]>([]);
const applicationTotal = ref(0);
const applicationQueryForm = ref<any>(null);
const applicationQueryParams = reactive({
pageNum: 1,
pageSize: 10,
courseId: undefined as string | number | undefined,
courseName: '',
location: '',
duration: undefined as number | undefined,
status: '',
excludeStatus: '1', // 默认排除已通过状态的数据
params: {}
});
// 申请状态选项
const applicationStatusOptions = [
{ value: '0', label: '待审核', type: 'warning' },
{ value: '1', label: '已通过', type: 'success' },
{ value: '2', label: '已拒绝', type: 'danger' },
{ value: '3', label: '已取消', type: 'info' },
{ value: '4', label: '已完成', type: 'success' }
];
// 教师列表数据
const teacherList = ref<Array<{
teacherId: string | number;
teacherName: string;
teacherType: string;
}>>([]);
// 申请详情相关
const applicationDetailOpen = ref(false);
const applicationDetail = reactive<any>({
applicationId: 0,
courseId: 0,
courseName: '',
userId: 0,
userName: '',
department: '',
status: '0',
applyTime: '',
approvalTime: '',
remark: '',
adminRemark: '',
certificatePath: '',
attachmentIds: '' // 添加附件ID字段
});
// 审批历史相关
const approvalHistoryOpen = ref(false);
const approvalHistoryList = ref<any[]>([]);
// 培训记录相关
const recordLoading = ref(false);
const recordList = ref<RecordVO[]>([]);
const recordTotal = ref(0);
const showRecordSearch = ref(true);
const recordQueryParams = reactive<RecordQuery>({
pageNum: 1,
pageSize: 20,
courseId: undefined as string | number | undefined,
courseName: '',
userId: undefined as string | number | undefined,
userName: '',
location: '',
department: '',
completionStatus: '',
includeApprovedApplications: true, // 包含已通过审批的申请数据
params: {}
});
// 培训记录对话框相关
const recordDialogVisible = ref(false);
const recordDialogTitle = ref('');
const recordDialogLoading = ref(false);
const recordFormRef = ref<any>(null);
const recordQueryForm = ref<any>(null);
const recordForm = reactive<RecordForm>({
recordId: undefined,
courseId: undefined,
userId: undefined,
completionStatus: '0',
score: '',
completionTime: '',
attachmentIds: '',
remark: ''
});
// 培训记录表单校验规则
const recordRules = reactive({
completionStatus: [{ required: true, message: '请选择完成状态', trigger: 'change' }]
});
// 培训记录详情
const recordDetailDialogVisible = ref(false);
const recordDetail = reactive<any>({
recordId: undefined,
courseId: undefined,
courseName: '',
userId: undefined,
userName: '',
completionStatus: '',
score: '',
completionTime: '',
certificatePath: '',
createTime: '',
updateTime: '',
createBy: '',
updateBy: '',
remark: '',
// 从培训申请获取的详细信息
department: '',
currentTitle: '',
currentPosition: '',
workStartTime: '',
phone: '',
organizerUnit: '',
trainingType: '',
trainingFee: '',
companions: '',
trainingPurpose: '',
location: '',
startTime: '',
endTime: '',
applyTime: ''
});
// 培训记录多选框选中数据
const recordIds = ref<Array<string | number>>([]);
// 培训记录非单个禁用
const recordSingle = ref(true);
// 培训记录非多个禁用
const recordMultiple = ref(true);
// 课程选项
const courseOptions = ref<CourseVO[]>([]);
// 用户选项
const userOptions = ref<any[]>([]);
// 证书上传相关
const certificateDialogVisible = ref(false);
const uploadRef = ref();
const certificateFileList = ref<any[]>([]);
const certificateUploadData = reactive({
recordId: '',
isApplication: false
});
// 培训记录修改对话框中的证书上传
const recordCertificateUploadRef = ref();
const recordCertificateFileList = ref<any[]>([]);
const uploadHeaders = {
Authorization: `Bearer ${getToken()}`
};
// 培训申请对话框相关
const applicationDialogVisible = ref(false);
const applicationDialogTitle = ref('');
const isResubmitMode = ref(false); // 标记是否为重新提交模式
const applicationDialogLoading = ref(false);
const applicationFormRef = ref<any>(null);
const applicationAttachmentFileList = ref<any[]>([]); // 申请培训附件文件列表
const applicationDetailAttachmentList = ref<any[]>([]); // 申请详情附件文件列表
const recordDetailAttachmentList = ref<any[]>([]); // 培训记录详情附件文件列表
const applicationForm = reactive<{
applicationId?: number | string;
// 情况基本信息
userName: string; // 姓名
department: string; // 所在部门(系部)
currentTitle: string; // 现职称
currentPosition: string; // 现岗位
workStartTime: string; // 参加工作时间
phone: string; // 联系电话
// 培训情况信息
organizerUnit: string; // 主办单位
location: string; // 培训地点
startTime: string; // 开始时间
endTime: string; // 结束时间
trainingType: string; // 培训类型
trainingTypeOther: string; // 其他培训类型描述
trainingFee: number; // 培训费用
companions: (string | number)[]; // 同行人
trainingPurpose: string; // 培训目的
remark: string; // 备注
// 兼容原有字段
courseName: string; // 培训课程(用于后端兼容)
duration: number; // 学时(用于后端兼容)
reason: string; // 申请理由(用于后端兼容)
attachmentIds: string[]; // 附件ID列表
}>({
applicationId: undefined,
// 情况基本信息
userName: '',
department: '',
currentTitle: '',
currentPosition: '',
workStartTime: '',
phone: '',
// 培训情况信息
organizerUnit: '',
location: '',
startTime: '',
endTime: '',
trainingType: '',
trainingTypeOther: '',
trainingFee: 0,
companions: [],
trainingPurpose: '',
remark: '',
// 兼容原有字段
courseName: '',
duration: 0,
reason: '',
attachmentIds: [] as string[] // 附件ID列表
});
// 同行人选择器相关
const selectedCompanions = ref<TeacherVO[]>([]);
// 培训申请表单校验规则
const applicationRules = reactive<any>({
// 情况基本信息验证
userName: [{ required: true, message: '姓名不能为空', trigger: 'blur' }],
department: [{ required: true, message: '所属部门不能为空', trigger: 'blur' }],
currentTitle: [{ required: true, message: '请输入现职称', trigger: 'blur' }],
currentPosition: [{ required: true, message: '请选择现岗位', trigger: 'change' }],
workStartTime: [{ required: true, message: '请选择参加工作时间', trigger: 'change' }],
phone: [
{ required: true, message: '请输入联系电话', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/ as RegExp, message: '请输入正确的手机号码', trigger: 'blur' }
],
// 培训情况信息验证
organizerUnit: [{ required: true, message: '请输入主办单位', trigger: 'blur' }],
location: [{ required: true, message: '请输入培训地点', trigger: 'blur' }],
startTime: [{ required: true, message: '请选择开始时间', trigger: 'change' }],
endTime: [{ required: true, message: '请选择结束时间', trigger: 'change' }],
trainingType: [{ required: true, message: '请选择培训类型', trigger: 'change' }],
trainingTypeOther: [
{
validator: (rule: any, value: string, callback: Function) => {
if (applicationForm.trainingType === '其他' && (!value || value.trim() === '')) {
callback(new Error('请输入具体的培训类型'));
} else {
callback();
}
},
trigger: 'blur'
}
],
trainingFee: [{ required: true, message: '请输入培训费用', trigger: 'blur' }],
trainingPurpose: [{ required: true, message: '请输入培训目的', trigger: 'blur' }]
});
// 监听培训类型变化,清空其他类型输入
watch(() => applicationForm.trainingType, (newType) => {
if (newType !== '其他') {
applicationForm.trainingTypeOther = '';
}
});
// 部门选项 - 从数据库sys_dept表获取
const departmentOptions = ref<Array<{ value: string; label: string }>>([]);
/** 查询培训课程列表 - 已废弃 (2025-01-03) */
function getTrainingList() {
trainingLoading.value = true;
// 培训课程模块已废弃,课程信息现在通过培训申请管理
console.warn('培训课程模块已废弃,请使用培训申请功能');
courseList.value = [];
trainingTotal.value = 0;
trainingLoading.value = false;
}
/** 获取部门列表 */
function getDepartmentList() {
listDept({
pageNum: 1,
pageSize: 1000, // 获取所有部门
status: 0
})
.then((response) => {
const deptList = response.data || [];
// 过滤出二级学院和行政单位下的部门,作为培训部门选项
const filteredDepts = deptList.filter((dept: DeptVO) => {
// 包含二级学院(101)和行政单位(102)下的部门,排除顶级部门
return dept.parentId === 101 || dept.parentId === 102;
});
departmentOptions.value = filteredDepts.map((dept: DeptVO) => ({
value: dept.deptName,
label: dept.deptName
}));
console.log('培训申请部门选项已更新:', departmentOptions.value);
})
.catch((error) => {
console.error('获取部门列表失败:', error);
// 使用备用部门数据
departmentOptions.value = [
{ value: '人文学院', label: '人文学院' },
{ value: '智能控制学院', label: '智能控制学院' },
{ value: '智能制造学院', label: '智能制造学院' },
{ value: '医药健康学院', label: '医药健康学院' },
{ value: '餐饮休闲学院', label: '餐饮休闲学院' },
{ value: '数字信息学院', label: '数字信息学院' },
{ value: '生态园林学院', label: '生态园林学院' },
{ value: '人事处', label: '人事处' },
{ value: '教务处', label: '教务处' },
{ value: '学生处', label: '学生处' },
{ value: '总务处', label: '总务处' }
];
});
}
/** 获取课程类别标签 */
function getCategoryLabel(value: string): string {
const option = categoryOptions.value.find((item) => item.value === value);
return option ? option.label : value;
}
/** 表单重置 */
function resetCourseForm() {
courseForm.courseId = undefined;
courseForm.title = '';
courseForm.category = '';
courseForm.description = '';
courseForm.startTime = '';
courseForm.endTime = '';
courseForm.duration = 0;
courseForm.location = '';
courseForm.status = '0';
courseForm.remark = '';
courseFormRef.value?.resetFields();
}
/** 取消按钮 */
function cancelCourseDialog() {
courseDialogVisible.value = false;
resetCourseForm();
}
/** 新增培训课程 */
function handleAddCourse() {
resetCourseForm();
courseDialogVisible.value = true;
courseDialogTitle.value = '添加培训课程';
}
/** 修改培训课程 */
function handleUpdateCourse(row?: CourseVO) {
resetCourseForm();
const courseId = row?.courseId || trainingIds.value[0];
if (!courseId) {
return;
}
// 培训课程模块已废弃 (2025-01-03)
console.warn('培训课程编辑功能已废弃,请使用培训申请功能');
proxy?.$modal.msgWarning('培训课程模块已废弃,请使用培训申请功能');
}
/** 提交按钮 */
function submitCourseForm() {
courseFormRef.value?.validate((valid: boolean) => {
if (valid) {
courseDialogLoading.value = true;
// 培训课程模块已废弃 (2025-01-03)
console.warn('培训课程新增/编辑功能已废弃,请使用培训申请功能');
proxy?.$modal.msgWarning('培训课程模块已废弃,请使用培训申请功能');
courseDialogLoading.value = false;
}
});
}
/** 删除培训课程 */
function handleDeleteCourse(row?: CourseVO) {
const courseIds = row?.courseId || trainingIds.value;
if (!courseIds || (Array.isArray(courseIds) && courseIds.length === 0)) {
return;
}
let confirmMessage = '';
if (row) {
confirmMessage = `是否确认删除课程"${row.title}"?`;
} else {
const selectedItems = courseList.value.filter((item) => trainingIds.value.includes(item.courseId));
if (selectedItems.length === 1) {
confirmMessage = `是否确认删除课程"${selectedItems[0].title}"?`;
} else {
const names = selectedItems.map((item) => item.title).join('、');
confirmMessage = `是否确认删除课程"${names}"?`;
}
}
proxy?.$modal.confirm(confirmMessage).then(() => {
// 培训课程模块已废弃 (2025-01-03)
console.warn('培训课程删除功能已废弃,请使用培训申请功能');
proxy?.$modal.msgWarning('培训课程模块已废弃,请使用培训申请功能');
});
}
/** 导出培训课程 */
function handleExportCourse() {
proxy?.download(
'rs/training/course/export',
{
...trainingQueryParams
},
`training_course_${new Date().getTime()}.xlsx`
);
}
/** 查看培训课程详情 - 已废弃 (2025-01-03) */
function handleViewCourse(row: CourseVO) {
console.warn('培训课程查看功能已废弃,请使用培训申请功能');
proxy?.$modal.msgWarning('培训课程模块已废弃,请使用培训申请功能');
}
/** 切换课程状态 - 已废弃 (2025-01-03) */
function handleToggleStatus(row: CourseVO) {
console.warn('培训课程状态切换功能已废弃,请使用培训申请功能');
proxy?.$modal.msgWarning('培训课程模块已废弃,请使用培训申请功能');
}
/** 查看培训申请记录 */
function handleViewApplications(row: CourseVO) {
applicationQueryParams.courseId = row.courseId;
applicationQueryParams.pageNum = 1;
applicationTotal.value = 0; // Reset total for new search
getApplicationList();
proxy?.$modal.msgSuccess(`查看${row.title}的申请记录`);
// 此处可以跳转到申请记录页面或打开申请记录弹窗
}
/** 培训申请管理相关 */
function getApplicationList() {
applicationLoading.value = true;
// 构建查询参数,自动过滤掉已通过的数据
const queryParams = {
...applicationQueryParams,
// 排除已通过状态的数据,只显示待审核、已拒绝、已取消等状态
excludeStatus: '1' // 1表示已通过状态
};
// 调用API获取申请列表
listTrainingApplication(queryParams)
.then((response) => {
// 获取申请列表数据
const applications = response.rows || [];
// 前端二次过滤,确保不显示已通过的数据
const filteredApplications = applications.filter((app) => app.status !== '1');
applicationList.value = filteredApplications;
applicationTotal.value = filteredApplications.length;
applicationLoading.value = false;
})
.catch(() => {
applicationLoading.value = false;
proxy?.$modal.msgError('获取培训申请列表失败');
});
}
/** 获取完成状态的标签类型 */
const getCompletionStatusType = (status: string) => {
// 培训记录的完成状态逻辑
if (status === '1') return 'success'; // 已完成 - 绿色
if (status === '2') return 'danger'; // 未完成 - 红色
// 对于空状态或其他状态,由于培训记录都是基于已通过审批的申请生成的,默认显示为已通过
return 'success'; // 已通过 - 绿色
};
/** 获取完成状态的标签文本 */
const getCompletionStatusLabel = (status: string) => {
// 培训记录的完成状态逻辑
if (status === '1') return '已完成';
if (status === '2') return '未完成';
// 对于空状态或其他状态,由于培训记录都是基于已通过审批的申请生成的,默认显示为已通过
return '已通过';
};
/** 获取教师列表 */
function getTeacherList() {
// 使用 system/teacher 接口获取教师列表,设置大的pageSize以获取所有教师
listTeacher({
pageNum: 1,
pageSize: 9999 // 设置足够大的pageSize以获取所有教师
})
.then((response: any) => {
const teachers = response.rows || [];
teacherList.value = teachers.map((item: TeacherVO) => ({
teacherId: item.userId, // 使用 userId 作为 teacherId
teacherName: item.nickName || item.userName, // 使用 nickName 或 userName
teacherType: '' // system/teacher 接口没有 teacherType 字段
}));
console.log('初始化教师列表加载完成:', teacherList.value.length, '位教师');
})
.catch(() => {
console.error('获取教师列表失败');
});
}
/** 异步加载教师列表用于详情显示 */
async function loadTeacherListForDisplay() {
try {
console.log('开始加载教师列表...');
// 使用 system/teacher 接口获取教师列表,设置大的pageSize以获取所有教师
const response: any = await listTeacher({
pageNum: 1,
pageSize: 9999 // 设置足够大的pageSize以获取所有教师
});
const teachers = response.rows || [];
teacherList.value = teachers.map((item: TeacherVO) => ({
teacherId: item.userId, // 使用 userId 作为 teacherId
teacherName: item.nickName || item.userName, // 使用 nickName 或 userName
teacherType: '' // system/teacher 接口没有 teacherType 字段
}));
console.log('教师列表加载完成:', teacherList.value.length, '位教师');
console.log('教师ID列表:', teacherList.value.map(t => t.teacherId));
} catch (error) {
console.error('获取教师列表失败:', error);
teacherList.value = [];
}
}
/** 处理申请详情的附件数据 */
async function handleApplicationDetailAttachmentData(attachmentIds: string | null | undefined) {
try {
if (!attachmentIds || !attachmentIds.toString().trim()) {
console.log('申请详情没有附件数据,清空附件列表');
applicationDetailAttachmentList.value = [];
return;
}
console.log('开始处理申请详情附件数据:', attachmentIds);
const ossIdsArray = attachmentIds
.toString()
.split(',')
.map((id) => id.trim())
.filter((id) => id);
if (ossIdsArray.length === 0) {
applicationDetailAttachmentList.value = [];
return;
}
const response = await request({
url: `/resource/oss/listByIds/${ossIdsArray.join(',')}`,
method: 'get'
});
if (response.code === 200 && response.data) {
console.log('获取到的申请附件信息:', response.data);
const attachmentFiles = response.data.map((item: any, index: number) => ({
uid: `application_detail_${index}_${Date.now()}`,
name: item.originalName || `申请附件${index + 1}`,
url: item.url,
ossId: item.ossId,
status: 'success',
fileSize: item.fileSize || 0, // 使用OSS表中的fileSize字段
uploadTime: item.createTime,
response: {
code: 200,
data: {
url: item.url,
ossId: item.ossId
}
}
}));
applicationDetailAttachmentList.value = attachmentFiles;
console.log('申请详情附件数据处理完成,共', attachmentFiles.length, '个附件');
} else {
console.warn('获取申请附件信息失败:', response);
applicationDetailAttachmentList.value = [];
}
} catch (error) {
console.error('处理申请详情附件数据失败:', error);
applicationDetailAttachmentList.value = [];
}
}
/** 新增培训申请 */
async function handleAddApplication() {
resetApplicationForm();
applicationDialogVisible.value = true;
applicationDialogTitle.value = '申请培训';
// 加载课程选项
if (courseOptions.value && courseOptions.value.length === 0) {
loadCourseOptions();
}
// 加载教师列表
if (teacherList.value.length === 0) {
getTeacherList();
}
// 自动获取登录用户的部门信息和申请人信息
try {
// 确保用户信息已加载
if (!userStore.userId) {
await userStore.getInfo();
}
console.log('开始自动填充用户信息:', {
userId: userStore.userId,
userName: userStore.name,
nickname: userStore.nickname,
cachedDeptName: userStore.deptName
});
// 自动填充申请人姓名(使用用户昵称,即真实姓名)
const displayName = userStore.nickname || userStore.name || `用户${userStore.userId}`;
applicationForm.userName = displayName;
console.log('申请人姓名自动填充:', {
userId: userStore.userId,
displayName: displayName,
nickname: userStore.nickname,
userName: userStore.name
});
// 重新获取用户信息以确保获取到deptId
await userStore.getInfo();
const userInfo = await getUserInfo();
const userDeptId = userInfo.data.user.deptId;
console.log('用户信息详情:', {
userId: userStore.userId,
deptId: userDeptId,
cachedDeptName: userStore.deptName
});
// 如果有deptId,从数据库sys_dept表实时获取部门名称
if (userDeptId) {
console.log('开始从数据库sys_dept表获取部门信息 - deptId:', userDeptId);
try {
const deptResponse = await getDept(userDeptId);
if (deptResponse.code === 200 && deptResponse.data) {
const realDeptName = deptResponse.data.deptName;
applicationForm.department = realDeptName;
console.log('培训申请部门自动匹配成功:', {
userId: userStore.userId,
userDeptId: userDeptId,
deptNameFromDatabase: realDeptName,
关联逻辑: 'sys_user.dept_id -> sys_dept.dept_name'
});
} else {
console.warn('从数据库获取部门信息失败,使用缓存的部门名称');
applicationForm.department = userStore.deptName || '';
}
} catch (deptError) {
console.error('查询部门信息API调用失败:', deptError);
applicationForm.department = userStore.deptName || '';
}
} else {
console.warn('用户deptId不存在,使用缓存的部门名称');
applicationForm.department = userStore.deptName || '';
}
} catch (error) {
console.error('获取用户信息失败:', error);
// 如果获取失败,使用空字符串,用户可以手动输入
applicationForm.department = '';
applicationForm.userName = `用户${userStore.userId || '未知'}`;
}
}
/** 重置培训申请表单 */
function resetApplicationForm() {
applicationForm.applicationId = undefined;
// 情况基本信息
applicationForm.userName = '';
applicationForm.department = '';
applicationForm.currentTitle = '';
applicationForm.currentPosition = '';
applicationForm.workStartTime = '';
applicationForm.phone = '';
// 培训情况信息
applicationForm.organizerUnit = '';
applicationForm.location = '';
applicationForm.startTime = '';
applicationForm.endTime = '';
applicationForm.trainingType = '';
applicationForm.trainingTypeOther = '';
applicationForm.trainingFee = 0;
applicationForm.companions = [];
applicationForm.trainingPurpose = '';
applicationForm.remark = '';
// 兼容原有字段
applicationForm.courseName = '';
applicationForm.duration = 0;
applicationForm.reason = '';
applicationForm.attachmentIds = [];
applicationAttachmentFileList.value = [];
selectedCompanions.value = [];
applicationFormRef.value?.resetFields();
}
/** 处理同行人变化 */
function handleCompanionsChange(teachers: TeacherVO[]) {
selectedCompanions.value = teachers;
applicationForm.companions = teachers.map(teacher => teacher.userId);
}
/** 加载编辑时的同行人数据 */
async function loadCompanionsForEdit() {
if (applicationForm.companions && applicationForm.companions.length > 0) {
try {
// 这里需要根据userId获取教师信息
// 由于TeacherMultiSelector组件需要完整的TeacherVO对象
// 我们需要调用API获取教师详细信息
const teacherPromises = applicationForm.companions.map(async (userId) => {
try {
// 这里需要调用获取教师详情的API
// 暂时使用teacherList中的数据进行匹配
const teacher = teacherList.value.find(t => t.teacherId === userId);
if (teacher) {
return {
userId: teacher.teacherId,
nickName: teacher.teacherName,
userName: teacher.teacherName,
phonenumber: '',
email: '',
sex: '0',
deptName: ''
} as TeacherVO;
}
return null;
} catch (error) {
console.error('获取教师信息失败:', error);
return null;
}
});
const teachers = await Promise.all(teacherPromises);
selectedCompanions.value = teachers.filter(t => t !== null) as TeacherVO[];
} catch (error) {
console.error('加载同行人数据失败:', error);
selectedCompanions.value = [];
}
} else {
selectedCompanions.value = [];
}
}
/** 取消培训申请对话框 */
function cancelApplicationDialog() {
applicationDialogVisible.value = false;
isResubmitMode.value = false; // 重置重新提交模式
resetApplicationForm();
}
/** 提交培训申请表单 */
function submitApplicationForm() {
applicationFormRef.value?.validate((valid: boolean) => {
if (valid) {
console.log('[提交培训申请] 提交前检查');
console.log('[提交培训申请] applicationForm.attachmentIds:', applicationForm.attachmentIds);
console.log('[提交培训申请] applicationAttachmentFileList:', applicationAttachmentFileList.value);
applicationDialogLoading.value = true;
// 创建符合API要求的数据结构
const apiData: any = {
courseId: 0, // 使用0表示自定义课程,确保是数字类型
// 基本字段(兼容原有API)
courseName: applicationForm.organizerUnit || '自定义培训', // 使用主办单位作为课程名称
department: applicationForm.department,
reason: applicationForm.trainingPurpose || '培训目的', // 使用培训目的作为申请理由
remark: applicationForm.remark,
location: applicationForm.location,
duration: Number(applicationForm.trainingFee) || 0, // 使用培训费用作为学时的临时方案
startTime: applicationForm.startTime,
endTime: applicationForm.endTime,
// 新增字段 - 将表单数据扩展字段添加到API中
currentTitle: applicationForm.currentTitle,
currentPosition: applicationForm.currentPosition,
workStartTime: applicationForm.workStartTime,
phone: applicationForm.phone,
organizerUnit: applicationForm.organizerUnit,
trainingType: applicationForm.trainingType === '其他' ? applicationForm.trainingTypeOther : applicationForm.trainingType,
trainingFee: Number(applicationForm.trainingFee),
trainingPurpose: applicationForm.trainingPurpose,
companions: JSON.stringify(applicationForm.companions || []),
// 附件信息
attachmentIds: applicationForm.attachmentIds.join(',')
};
console.log('[提交培训申请] 最终提交的API数据:', apiData);
console.log('[提交培训申请] attachmentIds字段值:', apiData.attachmentIds);
// 如果是编辑现有申请,添加applicationId
if (applicationForm.applicationId) {
apiData.applicationId = applicationForm.applicationId;
// 检查是否是被拒绝的申请重新提交
const currentApplication = applicationList.value.find((item) => item.applicationId === applicationForm.applicationId);
if (currentApplication && currentApplication.status === '2') {
// 如果是被拒绝的申请重新提交,将状态重置为待审核
apiData.status = '0';
}
// 如果是重新提交模式,使用专门的重新提交API
if (isResubmitMode.value) {
resubmitTrainingApplication(applicationForm.applicationId, apiData)
.then(() => {
proxy?.$modal.msgSuccess('修改并重新提交成功!您的申请已重新进入审批流程。');
isResubmitMode.value = false; // 重置重新提交模式
applicationDialogVisible.value = false;
getApplicationList();
})
.catch((error) => {
console.error('重新提交申请失败:', error);
proxy?.$modal.msgError(`重新提交失败: ${error.message || '服务器错误'}`);
})
.finally(() => {
applicationDialogLoading.value = false;
});
} else {
// 使用更新API
updateTrainingApplication(apiData)
.then(() => {
proxy?.$modal.msgSuccess('申请更新成功');
applicationDialogVisible.value = false;
getApplicationList();
})
.catch((error) => {
console.error('更新申请失败:', error);
proxy?.$modal.msgError(`更新失败: ${error.message || '服务器错误'}`);
})
.finally(() => {
applicationDialogLoading.value = false;
});
}
} else {
// 新增申请
console.log('提交的培训申请数据:', apiData); // 调试日志
// 使用无需权限验证的API
submitTrainingApplication(apiData)
.then((response) => {
console.log('提交申请响应:', response); // 记录成功响应
proxy?.$modal.msgSuccess('申请提交成功');
applicationDialogVisible.value = false;
getApplicationList();
})
.catch((error) => {
// 打印详细错误信息
console.error('提交申请失败:', error);
console.error('错误详情:', error.response ? error.response.data : '无响应数据');
// 如果是权限问题,提示用户
if (error.response && error.response.status === 401) {
proxy?.$modal.msgError('权限验证失败,请重新登录');
} else if (error.response && error.response.status === 403) {
proxy?.$modal.msgError('您没有权限执行此操作');
} else {
proxy?.$modal.msgError(`提交失败: ${error.message || '服务器错误'}`);
}
})
.finally(() => {
applicationDialogLoading.value = false;
});
}
}
});
}
/** 导出培训申请 */
function handleExportTrainingApplication() {
proxy?.download(
'rs/training/application/export',
{
...applicationQueryParams
},
`training_application_${new Date().getTime()}.xlsx`
);
}
/** 获取申请状态标签 */
function getStatusLabel(value: string): string {
const option = [
{ value: '0', label: '待审核' },
{ value: '1', label: '已通过' },
{ value: '2', label: '已拒绝' },
{ value: '3', label: '已取消' },
{ value: '4', label: '已完成' }
].find((item) => item.value === value);
return option ? option.label : value;
}
/** 格式化文件大小 */
function formatFileSize(bytes: number | string | undefined): string {
if (!bytes || bytes === 0) return '未知大小';
// 如果是字符串且包含中文,直接返回
if (typeof bytes === 'string' && /[\u4e00-\u9fa5]/.test(bytes)) {
return bytes;
}
const size = typeof bytes === 'string' ? parseInt(bytes) : bytes;
if (isNaN(size)) return '未知大小';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(size) / Math.log(k));
return parseFloat((size / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
/** 获取同行人姓名列表 */
function getCompanionNames(companions: (string | number)[] | string | null | undefined): string {
if (!companions) {
console.log('[同行人解析] 同行人数据为空');
return '';
}
let companionArray: (string | number)[] = [];
// 如果是字符串,尝试解析JSON
if (typeof companions === 'string') {
try {
companionArray = JSON.parse(companions);
if (!Array.isArray(companionArray)) {
console.log('[同行人解析] JSON解析结果不是数组:', companionArray);
return '';
}
console.log('[同行人解析] JSON解析成功:', companionArray);
} catch (e) {
// 如果JSON解析失败,可能是逗号分隔的字符串
companionArray = companions.split(',').map(id => id.trim()).filter(id => id);
console.log('[同行人解析] 使用逗号分隔解析:', companionArray);
}
} else if (Array.isArray(companions)) {
companionArray = companions;
console.log('[同行人解析] 直接使用数组:', companionArray);
}
if (companionArray.length === 0) {
console.log('[同行人解析] 同行人数组为空');
return '';
}
// 如果教师列表还未加载,显示"加载中..."
if (teacherList.value.length === 0) {
console.log('[同行人解析] 教师列表未加载,返回加载中');
return '加载中...';
}
console.log('[同行人解析] 教师列表已加载:', teacherList.value.length, '位教师');
console.log('[同行人解析] 需要查找的ID:', companionArray);
const names = companionArray.map(teacherId => {
// 确保teacherId转换为正确的类型进行比较
const teacher = teacherList.value.find(t =>
String(t.teacherId) === String(teacherId) ||
Number(t.teacherId) === Number(teacherId)
);
const name = teacher ? teacher.teacherName : `未知教师(ID:${teacherId})`;
console.log(`[同行人解析] ID ${teacherId} -> ${name}`);
return name;
}).join('、');
console.log('[同行人解析] 最终结果:', names);
return names;
}
/** 格式化同行人员显示(表格列使用) */
function formatCompanions(companions: string | undefined): string {
return getCompanionNames(companions);
}
/** 查看申请详情 */
async function handleViewApplicant(row: any) {
console.log('[查看申请详情] 原始数据:', {
userId: row.userId,
userName: row.userName,
nickName: row.nickName,
attachmentIds: row.attachmentIds // 添加附件ID日志
});
// 如果行数据中没有完整的课程名称,尝试从课程列表获取
if (!row.courseName && row.courseId) {
const course = courseList.value.find((c) => c.courseId === row.courseId);
if (course) {
row.courseName = course.title;
}
}
// 确保教师列表已加载(用于显示同行人姓名)
if (teacherList.value.length === 0) {
await loadTeacherListForDisplay();
}
// 复制申请详情
Object.assign(applicationDetail, row);
console.log('[查看申请详情] 详情数据已设置:', {
userName: applicationDetail.userName,
nickName: applicationDetail.nickName,
attachmentIds: applicationDetail.attachmentIds // 添加附件ID日志
});
// 加载申请详情的附件数据
await handleApplicationDetailAttachmentData(row.attachmentIds);
applicationDetailOpen.value = true;
}
/** 修改培训申请 */
function handleEditApplication(row: any) {
resetApplicationForm();
applicationDialogVisible.value = true;
applicationDialogTitle.value = '修改培训申请';
// 复制申请数据到表单 - 新字段结构
applicationForm.userName = row.userName || userStore.nickname || userStore.name;
applicationForm.department = row.department;
applicationForm.currentTitle = row.currentTitle || '';
applicationForm.currentPosition = row.currentPosition || '';
applicationForm.workStartTime = row.workStartTime || '';
applicationForm.phone = row.phone || '';
applicationForm.organizerUnit = row.organizerUnit || row.courseName || '';
applicationForm.location = row.location;
applicationForm.startTime = row.startTime;
applicationForm.endTime = row.endTime;
// 处理培训类型和其他类型描述
const trainingType = row.trainingType || '';
const predefinedTypes = ['教学', '德育', '行政', '管理', '其他'];
if (predefinedTypes.includes(trainingType)) {
applicationForm.trainingType = trainingType;
applicationForm.trainingTypeOther = '';
} else {
// 如果是自定义类型,设置为"其他"并将具体内容放到other字段
applicationForm.trainingType = '其他';
applicationForm.trainingTypeOther = trainingType;
}
applicationForm.trainingFee = row.trainingFee || 0;
applicationForm.trainingPurpose = row.trainingPurpose || row.reason || '';
applicationForm.remark = row.remark;
// 处理同行人员字段 - 从JSON字符串解析为数组
if (row.companions) {
try {
if (typeof row.companions === 'string') {
applicationForm.companions = JSON.parse(row.companions);
} else if (Array.isArray(row.companions)) {
applicationForm.companions = row.companions;
} else {
applicationForm.companions = [];
}
} catch (e) {
console.warn('解析companions字段失败:', e);
applicationForm.companions = [];
}
} else {
applicationForm.companions = [];
}
// 处理附件ID列表 - 从逗号分隔字符串解析为数组
if (row.attachmentIds) {
applicationForm.attachmentIds = row.attachmentIds.split(',').filter(id => id.trim());
} else {
applicationForm.attachmentIds = [];
}
// 兼容原有字段
applicationForm.courseName = row.courseName || '';
applicationForm.duration = row.duration || 0;
applicationForm.reason = row.reason || '';
// 保存当前编辑的申请ID
applicationForm.applicationId = row.applicationId;
// 加载同行人数据到选择器
loadCompanionsForEdit();
}
/** 撤回培训申请 */
function handleCancelApplication(row: any) {
proxy?.$modal
.confirm(`确认要撤回培训申请"${row.courseName || '未命名'}"吗?`)
.then(() => {
return cancelTrainingApplication(row.applicationId);
})
.then(() => {
getApplicationList();
proxy?.$modal.msgSuccess('申请已撤回');
})
.catch((error) => {
// 如果是用户取消操作,不显示错误提示
if (error === 'cancel' || error === 'close' || error.message === 'cancel') {
return;
}
console.error('撤回申请失败:', error);
proxy?.$modal.msgError(`撤回失败: ${error.message || '未知错误'}`);
});
}
/** 修改并重新提交申请(用于已拒绝状态) */
async function handleUpdateAndResubmitApplication(row: any) {
try {
// 获取申请详情
const response = await getTrainingApplication(row.applicationId);
if (response.code === 200 && response.data) {
// 设置表单数据 - 新字段结构
const data = response.data;
applicationForm.userName = data.userName || userStore.nickname || userStore.name;
applicationForm.department = data.department;
applicationForm.currentTitle = data.currentTitle || '';
applicationForm.currentPosition = data.currentPosition || '';
applicationForm.workStartTime = data.workStartTime || '';
applicationForm.phone = data.phone || '';
applicationForm.organizerUnit = data.organizerUnit || data.courseName || '';
applicationForm.location = data.location;
applicationForm.startTime = data.startTime;
applicationForm.endTime = data.endTime;
applicationForm.trainingType = data.trainingType || '';
applicationForm.trainingFee = data.trainingFee || 0;
applicationForm.trainingPurpose = data.trainingPurpose || data.reason || '';
applicationForm.remark = data.remark;
// 兼容原有字段
applicationForm.courseName = data.courseName || '';
applicationForm.duration = data.duration || 0;
applicationForm.reason = data.reason || '';
// 设置为编辑模式并标记为重新提交
applicationForm.applicationId = row.applicationId;
isResubmitMode.value = true;
// 打开编辑对话框
applicationDialogVisible.value = true;
applicationDialogTitle.value = '修改并重新提交申请';
} else {
proxy?.$modal.msgError('获取申请详情失败');
}
} catch (error) {
console.error('获取申请详情失败:', error);
proxy?.$modal.msgError('获取申请详情失败');
}
}
/** 重新提交被拒绝的培训申请 */
function handleResubmitApplication(row: any) {
resetApplicationForm();
applicationDialogVisible.value = true;
applicationDialogTitle.value = '重新提交培训申请';
// 复制申请数据到表单 - 新字段结构
applicationForm.userName = row.userName || userStore.nickname || userStore.name;
applicationForm.department = row.department;
applicationForm.currentTitle = row.currentTitle || '';
applicationForm.currentPosition = row.currentPosition || '';
applicationForm.workStartTime = row.workStartTime || '';
applicationForm.phone = row.phone || '';
applicationForm.organizerUnit = row.organizerUnit || row.courseName || '';
applicationForm.location = row.location;
applicationForm.startTime = row.startTime;
applicationForm.endTime = row.endTime;
// 处理培训类型和其他类型描述
const trainingType = row.trainingType || '';
const predefinedTypes = ['教学', '德育', '行政', '管理', '其他'];
if (predefinedTypes.includes(trainingType)) {
applicationForm.trainingType = trainingType;
applicationForm.trainingTypeOther = '';
} else {
// 如果是自定义类型,设置为"其他"并将具体内容放到other字段
applicationForm.trainingType = '其他';
applicationForm.trainingTypeOther = trainingType;
}
applicationForm.trainingFee = row.trainingFee || 0;
applicationForm.trainingPurpose = row.trainingPurpose || row.reason || '';
applicationForm.remark = row.remark;
// 兼容原有字段
applicationForm.courseName = row.courseName || '';
applicationForm.duration = row.duration || 0;
applicationForm.reason = row.reason || '';
// 保存当前编辑的申请ID
applicationForm.applicationId = row.applicationId;
}
/** 删除培训申请 */
function handleDeleteApplication(row: any) {
proxy?.$modal
.confirm(`确认删除培训申请"${row.courseName || '未命名'}"吗?`)
.then(() => {
return deleteTrainingApplication(row.applicationId);
})
.then(() => {
getApplicationList();
proxy?.$modal.msgSuccess('删除成功');
})
.catch((error) => {
// 如果是用户取消操作,不显示错误提示
if (error === 'cancel' || error === 'close' || error.message === 'cancel') {
return;
}
console.error('删除失败:', error);
proxy?.$modal.msgError(`删除失败: ${error.message || '未知错误'}`);
});
}
/** 更新申请状态 */
function handleUpdateStatus(row: any, status: string) {
let text = '';
switch (status) {
case '1':
text = '通过';
break;
case '2':
text = '拒绝';
break;
case '3':
text = '取消';
break;
case '4':
text = '完成';
break;
default:
text = '更新';
}
proxy?.$modal
.confirm(`确认要${text}该申请吗?`)
.then(() => {
// 调用更新申请状态的API
return updateApplicationStatus(row.applicationId, status);
})
.then(() => {
// 更新本地数据
row.status = status;
if (status === '1' || status === '2') {
row.approvalTime = new Date().toISOString();
}
proxy?.$modal.msgSuccess(`${text}申请成功`);
// 刷新申请列表
getApplicationList();
// 如果当前正在查看该申请的详情,更新详情中的状态
if (applicationDetailOpen.value && applicationDetail.applicationId === row.applicationId) {
applicationDetail.status = status;
if (status === '1' || status === '2') {
applicationDetail.approvalTime = new Date().toISOString();
}
}
})
.catch((error) => {
// 如果是用户取消操作,不显示错误提示
if (error === 'cancel' || error === 'close' || error.message === 'cancel') {
return;
}
console.error('更新申请状态失败:', error);
proxy?.$modal.msgError(`${text}申请失败: ${error.message || '未知错误'}`);
});
}
/** 上传申请培训证书 */
function handleUploadApplicationCertificate(row: any) {
certificateUploadData.recordId = row.applicationId;
certificateUploadData.isApplication = true;
certificateFileList.value = [];
certificateDialogVisible.value = true;
}
/** 查看申请培训证书 */
function viewApplicationCertificate(row: any) {
if (row.certificatePath) {
window.open(row.certificatePath);
} else {
proxy?.$modal.msgWarning('该培训申请暂无证书');
}
}
/** 保存管理员备注 */
function saveAdminRemark() {
proxy?.$modal.msgSuccess('保存备注功能开发中');
// 实际应该调用更新申请的API
applicationDetailOpen.value = false;
}
/** 获取审批级别标签 */
function getApprovalLevelLabel(level: number): string {
const levels = [
{ value: 1, label: '部门主管' },
{ value: 2, label: '分管领导' },
{ value: 3, label: '校领导' }
];
const levelObj = levels.find((item) => item.value === level);
return levelObj ? levelObj.label : '未知';
}
/** 培训课程搜索按钮操作 */
function handleTrainingQuery() {
trainingQueryParams.pageNum = 1;
getTrainingList();
}
/** 重置培训课程查询条件 */
function resetTrainingQuery() {
trainingQueryParams.title = '';
trainingQueryParams.category = '';
trainingQueryParams.status = '';
handleTrainingQuery();
}
/** 培训课程多选框选中数据 */
function handleTrainingSelectionChange(selection: CourseVO[]) {
trainingIds.value = selection.map((item) => item.courseId);
trainingSingle.value = selection.length !== 1;
trainingMultiple.value = !selection.length;
}
// 培训记录多选框选中数据
function handleRecordSelectionChange(selection: RecordVO[]) {
recordIds.value = selection.map((item) => item.recordId);
recordSingle.value = selection.length !== 1;
recordMultiple.value = !selection.length;
}
/** 培训子选项卡切换 */
function handleTrainingSubTabClick() {
if (trainingSubTab.value === 'application') {
getApplicationList();
// 确保教师列表已加载(用于同行人显示)
if (teacherList.value.length === 0) {
loadTeacherListForDisplay();
}
} else if (trainingSubTab.value === 'record') {
getRecordList();
// 确保教师列表已加载(用于同行人显示)
if (teacherList.value.length === 0) {
loadTeacherListForDisplay();
}
} else {
getTrainingList();
}
}
// 加载课程选项 - 已废弃 (2025-01-03)
function loadCourseOptions() {
// 培训课程模块已废弃,现在课程信息通过申请时的自定义课程名称管理
console.warn('培训课程选项已废弃,现在使用自定义课程名称');
courseOptions.value = [];
return Promise.resolve([]);
}
// 远程搜索用户
function remoteSearchUser(query: string) {
if (query) {
proxy?.$modal.loading('正在搜索...');
// 实际使用时应该调用后端搜索接口
// searchUsers({ username: query }).then(response => {
// userOptions.value = response.data;
// proxy?.$modal.closeLoading();
// });
// 模拟数据
setTimeout(() => {
userOptions.value = [
{ userId: 1, nickName: '张三' },
{ userId: 2, nickName: '李四' },
{ userId: 3, nickName: '王五' },
{ userId: 4, nickName: '赵六' }
].filter((item) => item.nickName.includes(query));
proxy?.$modal.closeLoading();
}, 500);
} else {
userOptions.value = [];
}
}
/** 获取培训记录列表 */
function getRecordList() {
recordLoading.value = true;
// 构建查询参数,包含已通过审批的申请数据
const queryParams = {
...recordQueryParams,
userId: userStore.userId, // 设置当前用户ID
// 确保包含已通过状态的培训申请数据
includeApprovedApplications: true
};
// 调用培训记录接口,获取培训记录数据
listTrainingRecord(queryParams)
.then((response) => {
recordList.value = response.rows;
recordTotal.value = response.total;
recordLoading.value = false;
// 确保教师列表已加载(用于同行人显示)
if (teacherList.value.length === 0) {
loadTeacherListForDisplay();
}
})
.catch(() => {
recordLoading.value = false;
});
}
/** 新增培训记录 */
function handleAddRecord() {
resetRecordForm();
recordDialogVisible.value = true;
recordDialogTitle.value = '添加培训记录';
// 加载课程选项
if (!courseOptions.value || courseOptions.value.length === 0) {
loadCourseOptions();
}
}
/** 重置培训记录表单 */
function resetRecordForm() {
console.log('resetRecordForm 开始执行');
recordForm.recordId = undefined;
recordForm.courseId = '';
recordForm.userId = '';
recordForm.completionStatus = '1';
recordForm.score = '';
recordForm.completionTime = '';
recordForm.certificatePath = '';
recordForm.certificateNumber = '';
recordForm.certificateFileName = '';
recordForm.attachmentIds = '';
recordForm.feedback = '';
recordForm.remark = '';
console.log('开始重置附件列表,当前 recordCertificateFileList.value:', recordCertificateFileList.value);
if (recordCertificateFileList.value) {
recordCertificateFileList.value = [];
} else {
console.warn('recordCertificateFileList.value 是 undefined,创建新数组');
recordCertificateFileList.value = [];
}
console.log('开始重置表单字段');
recordFormRef.value?.resetFields();
console.log('resetRecordForm 执行完成');
}
/** 取消培训记录对话框 */
function cancelRecordDialog() {
recordDialogVisible.value = false;
resetRecordForm();
}
/** 提交培训记录表单 */
function submitRecordForm() {
recordFormRef.value?.validate((valid: boolean) => {
if (valid) {
recordDialogLoading.value = true;
console.log('=== 提交表单数据 ===');
console.log('recordForm:', recordForm);
console.log('证书路径:', recordForm.certificatePath);
console.log('证书文件名:', recordForm.certificateFileName);
// 在提交前确保attachmentIds字段是最新的
updateRecordFormAttachmentIds();
console.log('提交前的attachmentIds:', recordForm.attachmentIds);
if (recordForm.recordId) {
// 修改记录时,保存证书信息到本地存储
if (recordForm.certificatePath) {
const certificateData = {
recordId: recordForm.recordId,
certificatePath: recordForm.certificatePath,
certificateFileName: recordForm.certificateFileName,
updateTime: new Date().toISOString()
};
// 保存到本地存储
const existingData = JSON.parse(localStorage.getItem('training_certificates') || '{}');
existingData[recordForm.recordId] = certificateData;
localStorage.setItem('training_certificates', JSON.stringify(existingData));
console.log('证书数据已保存到本地存储:', certificateData);
}
updateTrainingRecord(recordForm)
.then(() => {
proxy?.$modal.msgSuccess('修改成功');
recordDialogVisible.value = false;
getRecordList();
})
.catch((error) => {
console.log('API调用失败,使用模拟更新:', error);
// 模拟更新成功
setTimeout(() => {
proxy?.$modal.msgSuccess('修改成功');
recordDialogVisible.value = false;
getRecordList();
}, 500);
})
.finally(() => {
recordDialogLoading.value = false;
});
} else {
addTrainingRecord(recordForm)
.then(() => {
proxy?.$modal.msgSuccess('新增成功');
recordDialogVisible.value = false;
getRecordList();
})
.catch((error) => {
console.log('API调用失败,使用模拟新增:', error);
// 模拟新增成功
setTimeout(() => {
proxy?.$modal.msgSuccess('新增成功');
recordDialogVisible.value = false;
getRecordList();
}, 500);
})
.finally(() => {
recordDialogLoading.value = false;
});
}
}
});
}
/** 修改培训记录 */
async function handleUpdateRecord(row: RecordVO) {
console.log('=== 修改培训记录开始 ===');
console.log('row数据:', row);
try {
// 立即显示对话框,避免用户等待
recordDialogVisible.value = true;
recordDialogTitle.value = '修改培训记录';
// 重置表单
try {
console.log('开始重置表单...');
resetRecordForm();
console.log('表单重置完成');
} catch (error) {
console.error('重置表单时出错:', error);
throw error;
}
// 先设置基础数据,确保对话框内容可见
try {
console.log('开始设置基础数据...');
Object.assign(recordForm, {
recordId: row.recordId,
courseId: row.courseId,
courseName: row.courseName,
userId: row.userId,
userName: row.userName,
completionStatus: row.completionStatus || '0',
score: row.score || '',
duration: row.duration || '',
completionTime: row.completionTime || '',
certificatePath: row.certificatePath || '',
certificateNumber: row.certificateNumber || '',
certificateFileName: row.certificateFileName || '',
attachmentIds: (row as any).attachmentIds || '',
feedback: row.feedback || '',
remark: row.remark || ''
});
console.log('基础数据设置完成,attachmentIds:', recordForm.attachmentIds);
} catch (error) {
console.error('设置基础数据时出错:', error);
throw error;
}
console.log('设置基础数据后的recordForm:', recordForm);
console.log('当前courseOptions.value:', courseOptions.value);
// 异步加载课程选项(如果还没有加载)
try {
console.log('开始处理课程选项,当前courseOptions:', courseOptions.value);
if (!courseOptions.value || courseOptions.value.length === 0) {
console.log('需要加载课程选项');
loadCourseOptions()
.then(() => {
console.log('课程选项加载完成,开始匹配课程名称');
// 确保课程名称正确显示
if (!recordForm.courseName && recordForm.courseId && courseOptions.value) {
const course = courseOptions.value.find((c) => c.courseId === recordForm.courseId);
if (course) {
recordForm.courseName = course.title;
}
}
})
.catch((error) => {
console.warn('加载课程选项失败:', error);
});
} else {
console.log('使用现有课程选项');
// 确保课程名称正确显示
if (!recordForm.courseName && recordForm.courseId && courseOptions.value) {
const course = courseOptions.value.find((c) => c.courseId === recordForm.courseId);
if (course) {
recordForm.courseName = course.title;
}
}
}
console.log('课程选项处理完成');
} catch (error) {
console.error('处理课程选项时出错:', error);
throw error;
}
// 异步获取详细数据(可选)
try {
const response = await getTrainingRecord(row.recordId);
if (response.data) {
// 合并API返回的数据,保留已设置的基础数据
Object.assign(recordForm, response.data);
console.log('API返回的详细数据:', response.data);
}
} catch (error) {
console.log('获取详细数据失败,使用基础数据:', error);
// 使用基础数据继续,不影响对话框显示
}
// 异步处理证书附件数据
handleRecordAttachmentData().catch((error) => {
console.warn('处理附件数据失败:', error);
// 不影响对话框显示
});
console.log('=== 修改培训记录完成 ===');
} catch (error) {
console.error('修改培训记录过程中发生错误:', error);
// 即使出错也要显示对话框,让用户看到基础数据
proxy?.$modal.msgError('加载培训记录数据时发生错误,请重试');
}
}
/** 查看培训记录详情 */
async function handleViewRecord(row: RecordVO) {
try {
// 确保教师列表已加载(用于显示同行人姓名)
if (teacherList.value.length === 0) {
await loadTeacherListForDisplay();
}
// 首先复制记录的基本信息
Object.assign(recordDetail, row);
// 如果行数据中没有完整的课程名称,尝试从课程选项获取
if (!row.courseName && row.courseId) {
const course = courseOptions.value.find((c) => c.courseId === row.courseId);
if (course) {
recordDetail.courseName = course.title;
}
}
// 如果有申请ID,获取完整的申请详情来补充缺失的字段
if (row.applicationId) {
try {
const applicationResponse = await getTrainingApplication(row.applicationId);
const applicationData = applicationResponse.data;
// 合并申请数据中的详细信息到记录详情
Object.assign(recordDetail, {
// 保留记录的核心信息
recordId: row.recordId,
completionStatus: row.completionStatus,
completionTime: row.completionTime,
certificatePath: row.certificatePath,
certificateNumber: row.certificateNumber,
feedback: row.feedback,
// 从申请数据中获取详细信息
department: applicationData.department || row.department,
currentTitle: applicationData.currentTitle,
currentPosition: applicationData.currentPosition,
workStartTime: applicationData.workStartTime,
phone: applicationData.phone,
organizerUnit: applicationData.organizerUnit,
trainingType: applicationData.trainingType,
trainingFee: applicationData.trainingFee,
companions: applicationData.companions,
trainingPurpose: applicationData.trainingPurpose || applicationData.reason,
location: applicationData.location || row.location,
startTime: applicationData.startTime,
endTime: applicationData.endTime,
applyTime: applicationData.applyTime,
// 其他基本信息
courseName: applicationData.courseName || recordDetail.courseName,
userName: applicationData.userName || row.userName,
duration: applicationData.duration || row.duration,
remark: row.remark
});
// 加载申请附件数据
if (applicationData.attachmentIds) {
await handleApplicationDetailAttachmentData(applicationData.attachmentIds);
recordDetailAttachmentList.value = [...applicationDetailAttachmentList.value];
}
} catch (error) {
console.warn('获取申请详情失败,使用记录基本信息:', error);
// 如果获取申请详情失败,继续使用记录的基本信息
}
}
recordDetailDialogVisible.value = true;
} catch (error) {
console.error('查看培训记录详情失败:', error);
proxy?.$modal.msgError('获取培训记录详情失败,请重试');
}
}
/** 删除培训记录 */
function handleDeleteRecord(row?: RecordVO) {
const ids = row?.recordId || recordIds.value;
if (!ids || (Array.isArray(ids) && ids.length === 0)) {
return;
}
let confirmMessage = '';
if (row) {
confirmMessage = `是否确认删除学员"${row.userName}"的培训记录?`;
} else {
const selectedItems = recordList.value.filter((item) => recordIds.value.includes(item.applicationId as number | string));
if (selectedItems.length === 1) {
confirmMessage = `是否确认删除学员"${selectedItems[0].userName}"的培训记录?`;
} else {
const names = selectedItems.map((item) => item.userName).join('、');
confirmMessage = `是否确认删除学员"${names}"的培训记录?`;
}
}
proxy?.$modal
.confirm(confirmMessage)
.then(() => {
return delTrainingRecord(ids);
})
.then(() => {
getRecordList();
proxy?.$modal.msgSuccess('删除成功');
})
.catch((error) => {
// 如果是用户取消操作,不显示错误提示
if (error === 'cancel' || error === 'close' || error.message === 'cancel') {
return;
}
// 模拟删除成功,实际应使用API
setTimeout(() => {
getRecordList();
proxy?.$modal.msgSuccess('删除成功');
}, 300);
});
}
/** 证书上传前检查 */
function beforeCertificateUpload(file: File) {
const isValidType = ['application/pdf', 'image/jpeg', 'image/png'].includes(file.type);
const isLt10M = file.size / 1024 / 1024 < 10;
if (!isValidType) {
proxy?.$modal.msgError('上传证书文件只能是PDF/JPG/PNG格式!');
return false;
}
if (!isLt10M) {
proxy?.$modal.msgError('上传证书文件大小不能超过10MB!');
return false;
}
return true;
}
/** 证书上传成功处理 */
function handleCertificateSuccess(response: any) {
certificateDialogVisible.value = false;
if (certificateUploadData.isApplication) {
// 更新申请列表
getApplicationList();
// 如果当前正在查看该申请的详情,更新详情中的证书路径
if (applicationDetailOpen.value && applicationDetail.applicationId === certificateUploadData.recordId) {
if (response && response.data) {
applicationDetail.certificatePath = response.data.path || response.data.url;
}
}
proxy?.$modal.msgSuccess('申请培训证书上传成功!');
} else {
// 更新记录列表
getRecordList();
proxy?.$modal.msgSuccess('培训记录证书上传成功!');
}
// 重置标记
certificateUploadData.isApplication = false;
// 清空文件列表
certificateFileList.value = [];
}
/** 证书上传失败处理 */
function handleCertificateError(error: any) {
console.error('证书上传失败:', error);
let errorMessage = '证书上传失败,请重试!';
// 尝试从错误响应中获取具体错误信息
if (error && error.response && error.response.data) {
if (error.response.data.msg) {
errorMessage = error.response.data.msg;
} else if (error.response.data.message) {
errorMessage = error.response.data.message;
}
}
proxy?.$modal.msgError(errorMessage);
}
/** 查看证书 */
function viewCertificate(row: RecordVO) {
if (row.certificatePath) {
const ossId = extractOssIdFromPath(row.certificatePath);
if (ossId) {
// 如果有OSS ID,使用下载路径打开
const downloadUrl = `/dev-api/resource/oss/download/${ossId}`;
window.open(downloadUrl, '_blank');
} else {
// 后备方案:直接使用证书路径
window.open(row.certificatePath, '_blank');
}
} else {
proxy?.$modal.msgWarning('该培训记录暂无证书');
}
}
/** 培训记录修改对话框中的证书上传前检查 */
function beforeRecordCertificateUpload(file: File) {
const isValidType = ['application/pdf', 'image/jpeg', 'image/png'].includes(file.type);
const isLt10M = file.size / 1024 / 1024 < 10;
if (!isValidType) {
proxy?.$modal.msgError('上传证书文件只能是PDF/JPG/PNG格式!');
return false;
}
if (!isLt10M) {
proxy?.$modal.msgError('上传证书文件大小不能超过10MB!');
return false;
}
return true;
}
/** 培训记录修改对话框中的证书上传成功处理 */
function handleRecordCertificateSuccess(response: any, file: any) {
if (response.code === 200 && response.data) {
file.url = response.data.url;
file.ossId = response.data.ossId;
proxy?.$modal.msgSuccess('证书上传成功!');
console.log('证书上传成功:', response.data);
// 更新文件状态为成功
file.status = 'success';
file.response = response;
// 自动保存附件ID到后端
saveRecordAttachments();
// 同时更新recordForm中的attachmentIds字段,确保提交时包含附件信息
updateRecordFormAttachmentIds();
}
}
/** 培训记录修改对话框中的证书上传失败处理 */
function handleRecordCertificateError(error: any) {
console.error('证书上传失败:', error);
let errorMessage = '证书上传失败,请重试!';
// 尝试从错误响应中获取具体错误信息
if (error && error.response && error.response.data) {
if (error.response.data.msg) {
errorMessage = error.response.data.msg;
} else if (error.response.data.message) {
errorMessage = error.response.data.message;
}
}
proxy?.$modal.msgError(errorMessage);
}
/** 保存培训记录附件ID到后端 */
async function saveRecordAttachments() {
if (!recordForm.recordId) {
console.warn('记录ID为空,无法保存附件');
return;
}
try {
// 提取所有成功上传的ossId
const attachmentOssIds = recordCertificateFileList.value.filter((file) => file.status === 'success' && file.ossId).map((file) => file.ossId);
// 转换为逗号分隔的字符串
const attachmentIds = attachmentOssIds.join(',');
console.log('保存附件ID:', attachmentIds);
// 调用后端接口保存附件ID
await updateRecordAttachments(recordForm.recordId, { attachmentIds });
console.log('附件ID保存成功');
} catch (error) {
console.error('保存附件ID失败:', error);
proxy?.$modal.msgError('保存附件失败,请重试');
}
}
/** 更新recordForm中的attachmentIds字段 */
function updateRecordFormAttachmentIds() {
if (!recordForm.recordId) {
console.warn('记录ID为空,无法更新附件ID');
return;
}
// 提取所有成功上传的ossId
const attachmentOssIds = recordCertificateFileList.value.filter((file) => file.status === 'success' && file.ossId).map((file) => file.ossId);
// 转换为逗号分隔的字符串并更新recordForm
const attachmentIds = attachmentOssIds.join(',');
recordForm.attachmentIds = attachmentIds;
console.log('更新recordForm.attachmentIds:', attachmentIds);
}
/** 处理培训记录附件数据 */
async function handleRecordAttachmentData() {
try {
if (!recordForm.attachmentIds || !recordForm.attachmentIds.toString().trim()) {
console.log('没有附件数据,清空附件列表');
recordCertificateFileList.value = [];
return;
}
console.log('开始处理附件数据:', recordForm.attachmentIds);
const ossIdsArray = recordForm.attachmentIds
.toString()
.split(',')
.map((id) => id.trim())
.filter((id) => id);
if (ossIdsArray.length === 0) {
recordCertificateFileList.value = [];
return;
}
const response = await request({
url: `/resource/oss/listByIds/${ossIdsArray.join(',')}`,
method: 'get'
});
if (response.code === 200 && response.data) {
console.log('获取到的附件信息:', response.data);
const attachmentFiles = response.data.map((item: any, index: number) => ({
uid: `existing_${index}_${Date.now()}`,
name: item.originalName || `证书${index + 1}`,
url: item.url,
ossId: item.ossId,
status: 'success',
fileSize: item.fileSize || 0,
response: {
code: 200,
data: {
url: item.url,
ossId: item.ossId
}
}
}));
recordCertificateFileList.value = attachmentFiles;
console.log('设置附件列表成功:', recordCertificateFileList.value);
} else {
console.warn('获取附件信息失败:', response.msg);
recordCertificateFileList.value = [];
}
} catch (error) {
console.error('处理附件数据失败:', error);
recordCertificateFileList.value = [];
}
}
/** 从文件路径中提取文件名 */
function getFileNameFromPath(filePath: string): string {
if (!filePath) return '未知文件';
const pathParts = filePath.split('/');
const fileName = pathParts[pathParts.length - 1];
// 移除时间戳前缀(如果有的话)
return fileName.replace(/^\d+_/, '') || '证书文件';
}
/** 查看当前证书 */
function viewCurrentCertificate() {
if (recordForm.certificatePath) {
window.open(recordForm.certificatePath);
} else {
proxy?.$modal.msgWarning('当前记录暂无证书');
}
}
/** 预览证书文件 */
function handlePreviewCertificate(file: any) {
console.log('预览证书文件:', file);
console.log('file.url:', file.url);
console.log('file.ossId:', file.ossId);
if (!file) {
proxy?.$modal.msgError('文件不存在');
return;
}
// 如果是图片,使用图片预览
if (isImageFile(file)) {
let imageUrl = '';
// 优先使用 file.url(已上传的文件)
if (file.url) {
imageUrl = file.url;
}
// 如果是新上传的文件,使用预览URL
else if (file.raw) {
imageUrl = URL.createObjectURL(file.raw);
}
if (imageUrl) {
console.log('预览图片URL:', imageUrl);
// 创建图片预览弹窗
const img = new Image();
img.onload = function () {
const newWindow = window.open('', '_blank');
if (newWindow) {
newWindow.document.write(`
<!DOCTYPE html>
<html>
<head>
<title>图片预览 - ${file.name}</title>
<style>
body {
margin: 0;
padding: 20px;
background: #f5f5f5;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
img {
max-width: 90%;
max-height: 90%;
border: 1px solid #ddd;
border-radius: 4px;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
</style>
</head>
<body>
<img src="${imageUrl}" alt="${file.name}" />
</body>
</html>
`);
newWindow.document.close();
}
};
img.onerror = function () {
proxy?.$modal.msgError('图片加载失败,可能需要登录权限');
// 如果图片加载失败,尝试使用OSS下载地址
const ossId = getFileOssId(file);
if (ossId) {
const downloadUrl = `/dev-api/resource/oss/download/${ossId}`;
window.open(downloadUrl, '_blank');
}
};
img.src = imageUrl;
} else {
proxy?.$modal.msgError('无法获取图片预览');
}
} else {
// 非图片文件,尝试在新窗口打开
const ossId = getFileOssId(file);
if (ossId) {
// 如果有OSS ID,使用下载地址
const downloadUrl = `/dev-api/resource/oss/download/${ossId}`;
window.open(downloadUrl, '_blank');
} else if (file.url) {
window.open(file.url, '_blank');
} else {
proxy?.$modal.msgError('无法预览此类型文件');
}
}
}
/** 判断是否为图片文件 */
function isImageFile(file: any): boolean {
const imageTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/webp'];
return imageTypes.includes(file.raw?.type) || (file.url && /\.(jpg|jpeg|png|gif|bmp|webp)$/i.test(file.name));
}
/** 获取文件扩展名 */
function getFileExt(fileName: string): string {
const ext = fileName.split('.').pop()?.toUpperCase();
return ext || 'FILE';
}
/** 格式化文件名(长文件名显示省略号) */
function formatFileName(fileName: string): string {
if (!fileName) return '';
if (fileName.length <= 20) return fileName;
return fileName.substring(0, 10) + '...' + fileName.substring(fileName.length - 10);
}
/** 获取缩写的文件名(保留后缀) */
function getShortFileName(fileName: string): string {
if (!fileName) return '';
const lastDotIndex = fileName.lastIndexOf('.');
if (lastDotIndex === -1) {
return fileName.length > 12 ? fileName.substring(0, 12) + '...' : fileName;
}
const name = fileName.substring(0, lastDotIndex);
const ext = fileName.substring(lastDotIndex);
if (name.length <= 8) {
return fileName;
}
return name.substring(0, 8) + '...' + ext;
}
/** 从文件路径中提取OSS ID */
function extractOssIdFromPath(filePath: string): string {
if (!filePath) return '';
// 如果是下载路径 /resource/oss/download/{ossId},提取ossId
const downloadMatch = filePath.match(/\/resource\/oss\/download\/([^/?]+)/);
if (downloadMatch) {
return downloadMatch[1];
}
// 如果路径中包含ossId参数
const ossIdMatch = filePath.match(/[?&]ossId=([^&]+)/);
if (ossIdMatch) {
return ossIdMatch[1];
}
// 如果是直接的ossId(通常是数字)
if (/^\d+$/.test(filePath)) {
return filePath;
}
return '';
}
/** 获取证书路径(支持两种存储方式:certificatePath 和 attachmentIds) */
function getCertificatePath(record: any): string {
// 优先使用 attachmentIds(新方式,存储OSS ID)
if (record.attachmentIds && record.attachmentIds.toString().trim()) {
const ossIds = record.attachmentIds.toString().split(',');
if (ossIds.length > 0 && ossIds[0].trim()) {
return ossIds[0].trim(); // 返回第一个OSS ID
}
}
// 后备方案:使用 certificatePath(旧方式,存储文件路径)
return record.certificatePath || '';
}
/** 判断证书是否为图片格式 */
function isCertificateImage(filePath: string): boolean {
if (!filePath) return false;
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'];
const lowerPath = filePath.toLowerCase();
return imageExtensions.some((ext) => lowerPath.includes(ext));
}
/** 获取证书预览URL */
function getCertificatePreviewUrl(filePath: string): string {
if (!filePath) return '';
// 如果是纯数字,认为是OSS ID - 但这里不直接返回URL,需要异步获取
if (/^\d+$/.test(filePath.trim())) {
// 对于OSS ID,我们需要通过API获取临时URL
// 这里返回一个占位符,实际预览通过异步方式处理
return '';
}
const ossId = extractOssIdFromPath(filePath);
if (ossId) {
// 如果从路径中提取到OSS ID,使用下载路径
return `/dev-api/resource/oss/download/${ossId}`;
}
// 如果是相对路径,添加API前缀
if (filePath.startsWith('/')) {
return `/dev-api${filePath}`;
}
// 如果是完整的HTTP URL,直接使用
if (filePath.startsWith('http://') || filePath.startsWith('https://')) {
return filePath;
}
// 后备方案:假设是相对路径,添加API前缀
return `/dev-api/${filePath}`;
}
/** 异步获取证书预览URL(用于OSS资源)*/
async function getSecureCertificatePreviewUrl(filePath: string): Promise<string> {
if (!filePath) return '';
// 如果是纯数字,认为是OSS ID,需要通过API获取临时预览URL
if (/^\d+$/.test(filePath.trim())) {
try {
const response = await getFilePreviewUrl(filePath.trim());
// API返回的URL在msg字段中,而不是data字段
return response.msg || response.data || '';
} catch (error) {
console.error('获取OSS文件预览URL失败:', error);
return '';
}
}
// 其他情况使用同步方法
return getCertificatePreviewUrl(filePath);
}
/** 获取证书文件名 */
function getCertificateFileName(filePath: string): string {
if (!filePath) return '未知文件';
// 从路径中提取文件名
const fileName = filePath.split('/').pop() || filePath;
// 如果文件名太长,进行截断
if (fileName.length > 30) {
const lastDotIndex = fileName.lastIndexOf('.');
if (lastDotIndex !== -1) {
const name = fileName.substring(0, lastDotIndex);
const ext = fileName.substring(lastDotIndex);
return name.substring(0, 20) + '...' + ext;
}
return fileName.substring(0, 25) + '...';
}
return fileName;
}
/** 下载证书 */
function downloadCertificate(row: RecordVO) {
if (!row.certificatePath) {
proxy?.$modal.msgWarning('该培训记录暂无证书');
return;
}
const ossId = extractOssIdFromPath(row.certificatePath);
if (ossId) {
// 如果有OSS ID,使用下载路径
const downloadUrl = `/dev-api/resource/oss/download/${ossId}`;
const link = document.createElement('a');
link.href = downloadUrl;
link.download = getCertificateFileName(row.certificatePath);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} else {
// 使用预览URL作为下载URL
const downloadUrl = getCertificatePreviewUrl(row.certificatePath);
window.open(downloadUrl, '_blank');
}
}
/** 处理培训证书数据 - 参考宿舍卫生检查的附件处理方式 */
async function handleCertificateData() {
try {
console.log('=== 处理证书数据开始 ===');
console.log('recordForm.recordId:', recordForm.recordId);
console.log('recordForm.certificatePath:', recordForm.certificatePath);
console.log('recordForm.certificateFileName:', recordForm.certificateFileName);
// 优先从本地存储获取证书信息
if (recordForm.recordId) {
try {
const certificatesData = JSON.parse(localStorage.getItem('training_certificates') || '{}');
const storedCertificate = certificatesData[recordForm.recordId];
console.log('本地存储的证书数据:', storedCertificate);
if (storedCertificate && storedCertificate.certificatePath) {
// 使用本地存储的数据更新表单
recordForm.certificatePath = storedCertificate.certificatePath;
recordForm.certificateFileName = storedCertificate.certificateFileName;
console.log('从本地存储恢复证书信息:', {
certificatePath: recordForm.certificatePath,
certificateFileName: recordForm.certificateFileName
});
}
} catch (error) {
console.warn('读取本地存储失败:', error);
}
}
if (!recordForm.certificatePath || !recordForm.certificatePath.toString().trim()) {
console.log('没有证书数据,清空证书列表');
recordCertificateFileList.value = [];
return;
}
console.log('开始处理证书数据:', recordForm.certificatePath);
// 尝试从路径中提取OSS ID
const ossId = extractOssIdFromPath(recordForm.certificatePath);
if (ossId) {
// 如果有OSS ID,通过API获取详细信息
try {
const response = await request({
url: `/resource/oss/listByIds/${ossId}`,
method: 'get'
});
if (response.code === 200 && response.data && response.data.length > 0) {
console.log('获取到的证书信息:', response.data);
const item = response.data[0];
const certificateFile = {
uid: `existing_${Date.now()}`,
name: item.originalName || recordForm.certificateFileName || getFileNameFromPath(recordForm.certificatePath),
url: item.url || recordForm.certificatePath,
ossId: item.ossId || ossId,
status: 'success',
response: {
code: 200,
data: {
url: item.url || recordForm.certificatePath,
ossId: item.ossId || ossId
}
}
};
recordCertificateFileList.value = [certificateFile];
console.log('设置证书列表成功:', recordCertificateFileList.value);
return;
}
} catch (error) {
console.warn('通过OSS API获取证书信息失败,使用本地信息:', error);
}
}
// 如果API获取失败或没有OSS ID,使用本地信息
const fileName = recordForm.certificateFileName || getFileNameFromPath(recordForm.certificatePath);
recordCertificateFileList.value = [
{
uid: Date.now(),
name: fileName,
url: recordForm.certificatePath,
ossId: ossId,
status: 'success'
}
];
console.log('使用本地信息设置证书列表:', recordCertificateFileList.value);
} catch (error) {
console.error('处理证书数据失败:', error);
recordCertificateFileList.value = [];
}
}
/** 获取文件的OSS ID用于ImagePreview */
function getFileOssId(file: any): string {
return file.ossId || '';
}
/** 下载证书文件 */
async function handleDownloadCertificate(file: any) {
let downloadUrl = '';
let fileName = file.name || 'certificate';
if (file.response && file.response.data && file.response.data.filePath) {
downloadUrl = file.response.data.filePath;
fileName = file.response.data.fileName || fileName;
} else if (file.ossId) {
downloadUrl = file.ossId;
} else if (file.raw) {
const link = document.createElement('a');
link.href = URL.createObjectURL(file.raw);
link.download = fileName;
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(link.href);
console.log('下载本地文件:', fileName);
return;
}
if (!downloadUrl) {
proxy?.$modal.msgError('无法获取文件下载地址');
return;
}
try {
const loading = ElLoading.service({
text: '正在下载文件...',
target: document.body
});
const response = await request({
url: '/resource/oss/download/' + downloadUrl,
method: 'get',
responseType: 'blob',
timeout: 300000
});
if (!response) {
throw new Error('响应数据为空');
}
let blobData: Blob;
if (response instanceof Blob) {
blobData = response;
} else if (response.data instanceof Blob) {
blobData = response.data;
} else {
throw new Error('无效的响应数据格式');
}
const url = window.URL.createObjectURL(blobData);
const link = document.createElement('a');
link.href = url;
link.download = fileName;
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
loading?.close();
console.log('下载服务器文件成功:', fileName);
} catch (error) {
console.error('下载文件失败:', error);
proxy?.$modal.msgError('下载文件失败,请稍后重试');
}
}
/** 确认删除证书文件 */
function confirmRemoveCertificate(file: any) {
proxy?.$modal
.confirm('确认要删除该证书文件吗?')
.then(() => {
handleRemoveCertificate(file);
})
.catch(() => {});
}
/** 移除证书文件 */
function handleRemoveCertificate(file: any) {
// 从文件列表中移除
const index = recordCertificateFileList.value.findIndex((item) => item.uid === file.uid);
if (index > -1) {
recordCertificateFileList.value.splice(index, 1);
}
// 自动保存附件ID到后端
saveRecordAttachments();
// 同时更新recordForm中的attachmentIds字段
updateRecordFormAttachmentIds();
proxy?.$modal.msgSuccess('证书文件已删除');
}
/** 搜索培训记录 */
function handleRecordQuery() {
recordQueryParams.pageNum = 1;
getRecordList();
}
/** 重置培训记录查询参数 */
function resetRecordQuery() {
recordQueryForm.value?.resetFields();
recordQueryParams.courseName = '';
recordQueryParams.location = '';
recordQueryParams.department = '';
handleRecordQuery();
}
/** 导出培训记录 */
function handleExportRecord() {
proxy?.download(
'rs/training/record/export',
{
...recordQueryParams
},
`training_record_${new Date().getTime()}.xlsx`
);
}
/** 搜索培训申请 */
function handleApplicationQuery() {
applicationQueryParams.pageNum = 1;
getApplicationList();
}
/** 重置培训申请查询参数 */
function resetApplicationQuery() {
applicationQueryForm.value?.resetFields();
applicationQueryParams.courseId = undefined;
applicationQueryParams.courseName = '';
applicationQueryParams.location = '';
applicationQueryParams.duration = undefined;
applicationQueryParams.status = '';
// 保持排除已通过状态的设置
applicationQueryParams.excludeStatus = '1';
handleApplicationQuery();
}
/** 查看审批历史 */
async function handleViewHistory(row: any) {
try {
proxy?.$modal.loading('正在加载审批历史...');
const response = await getTrainingApprovalHistory(row.applicationId);
approvalHistoryList.value = response.data || [];
approvalHistoryOpen.value = true;
proxy?.$modal.closeLoading();
} catch (error) {
console.error('获取审批历史失败:', error);
proxy?.$modal.msgError('获取审批历史失败');
proxy?.$modal.closeLoading();
}
}
// 新增:强制重新初始化函数
function forceReinitialize() {
console.log('强制重新初始化组件状态');
isInitialized.value = false;
isDataLoaded.value = false;
// 重置关键状态
courseOptions.value = [];
recordList.value = [];
applicationList.value = [];
// 重新加载当前选项卡数据
if (trainingSubTab.value === 'application') {
getApplicationList();
} else if (trainingSubTab.value === 'record') {
getRecordList();
} else {
getTrainingList();
}
}
// 删除复杂的初始化函数,采用培训审批页面的简单模式
// 新增:activated生命周期钩子 - 处理Keep-Alive缓存激活
onActivated(() => {
console.log('组件被激活,重新加载当前选项卡数据');
// 直接重新加载当前选项卡的数据,不依赖复杂的状态判断
if (trainingSubTab.value === 'application') {
console.log('重新加载培训申请数据');
getApplicationList();
} else if (trainingSubTab.value === 'record') {
console.log('重新加载培训记录数据');
getRecordList();
} else {
console.log('重新加载培训课程数据');
getTrainingList();
}
// 同时加载课程选项,确保下拉选项可用
loadCourseOptions();
// 确保教师列表已加载(用于同行人显示)
if (teacherList.value.length === 0) {
loadTeacherListForDisplay();
}
});
onMounted(() => {
console.log('组件挂载,开始初始化');
// 采用培训审批页面的成功模式:同时加载所有数据
getTrainingList();
getApplicationList();
getRecordList();
loadCourseOptions();
getDepartmentList();
// 加载教师列表(用于同行人显示)
loadTeacherListForDisplay();
});
// ===== 申请培训附件上传相关方法 =====
/** 申请培训附件上传前检查 */
function beforeApplicationAttachmentUpload(file: File) {
console.log('[申请培训附件上传前检查] 开始检查');
console.log('[申请培训附件上传前检查] 文件信息:', {
name: file.name,
type: file.type,
size: `${(file.size / 1024 / 1024).toFixed(2)}MB`
});
const isValidType = [
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'image/jpeg',
'image/png'
].includes(file.type);
const isLt20M = file.size / 1024 / 1024 < 20;
if (!isValidType) {
console.error('[申请培训附件上传前检查] 文件类型不符合要求:', file.type);
proxy?.$modal.msgError('上传附件文件只能是PDF/Word/Excel/图片格式!');
return false;
}
if (!isLt20M) {
console.error('[申请培训附件上传前检查] 文件大小超出限制:', `${(file.size / 1024 / 1024).toFixed(2)}MB`);
proxy?.$modal.msgError('上传附件文件大小不能超过20MB!');
return false;
}
console.log('[申请培训附件上传前检查] 检查通过');
return true;
}
/** 申请培训附件上传成功处理 */
function handleApplicationAttachmentSuccess(response: any, file: any) {
console.log('[申请培训附件上传成功] 响应数据:', response);
console.log('[申请培训附件上传成功] 文件对象:', file);
if (response.code === 200 && response.data) {
file.url = response.data.url;
file.ossId = response.data.ossId;
proxy?.$modal.msgSuccess('附件上传成功!');
console.log('[申请培训附件上传成功] OSS ID:', response.data.ossId);
// 更新文件状态为成功
file.status = 'success';
file.response = response;
// 更新applicationForm中的attachmentIds字段
updateApplicationFormAttachmentIds();
}
}
/** 申请培训附件上传失败处理 */
function handleApplicationAttachmentError(error: any, file: any) {
console.error('[申请培训附件上传失败] 错误对象:', error);
console.error('[申请培训附件上传失败] 文件对象:', file);
let errorMessage = '附件上传失败,请重试!';
let errorDetails = '';
// 尝试从错误响应中获取具体错误信息
if (error && error.response) {
console.error('[申请培训附件上传失败] HTTP响应:', {
status: error.response.status,
statusText: error.response.statusText,
data: error.response.data
});
if (error.response.data) {
if (error.response.data.msg) {
errorMessage = error.response.data.msg;
} else if (error.response.data.message) {
errorMessage = error.response.data.message;
}
}
// 常见错误状态码处理
if (error.response.status === 401) {
errorDetails = '认证失败,请重新登录';
} else if (error.response.status === 403) {
errorDetails = '没有上传权限';
} else if (error.response.status === 404) {
errorDetails = '上传接口不存在';
} else if (error.response.status === 413) {
errorDetails = '文件太大,服务器拒绝接收';
} else if (error.response.status >= 500) {
errorDetails = '服务器内部错误';
}
} else if (error.message) {
console.error('[申请培训附件上传失败] 错误信息:', error.message);
errorDetails = error.message;
}
const finalMessage = errorDetails ? `${errorMessage} (${errorDetails})` : errorMessage;
proxy?.$modal.msgError(finalMessage);
}
/** 更新applicationForm中的attachmentIds字段 */
function updateApplicationFormAttachmentIds() {
console.log('[更新附件IDs] 开始更新');
console.log('[更新附件IDs] 文件列表:', applicationAttachmentFileList.value);
// 提取所有成功上传的ossId
const attachmentOssIds = applicationAttachmentFileList.value
.filter((file) => file.status === 'success' && file.ossId)
.map((file) => file.ossId);
// 更新applicationForm
applicationForm.attachmentIds = attachmentOssIds;
console.log('[更新附件IDs] 提取的OSS IDs:', attachmentOssIds);
console.log('[更新附件IDs] applicationForm.attachmentIds:', applicationForm.attachmentIds);
}
/** 移除申请培训附件 */
function handleRemoveApplicationAttachment(file: any) {
// 更新attachmentIds
updateApplicationFormAttachmentIds();
}
/** 确认移除申请培训附件 */
function confirmRemoveApplicationAttachment(file: any) {
proxy?.$modal.confirm('确定要删除这个附件吗?')
.then(() => {
const index = applicationAttachmentFileList.value.findIndex(f => f.uid === file.uid);
if (index > -1) {
applicationAttachmentFileList.value.splice(index, 1);
updateApplicationFormAttachmentIds();
proxy?.$modal.msgSuccess('附件删除成功');
}
})
.catch(() => {
console.log('取消删除附件');
});
}
/** 预览申请培训附件 */
function handlePreviewApplicationAttachment(file: any) {
if (file.url) {
window.open(file.url, '_blank');
}
}
/** 下载申请培训附件 */
async function handleDownloadApplicationAttachment(file: any) {
console.log('[下载申请培训附件] 开始下载');
console.log('[下载申请培训附件] 文件信息:', file);
if (!file.ossId) {
console.error('[下载申请培训附件] ossId为空,无法下载');
proxy?.$modal.msgError('文件信息不完整,无法下载');
return;
}
try {
console.log('[下载申请培训附件] 请求下载接口:', `/resource/oss/download/${file.ossId}`);
// 使用后端安全下载接口,通过request工具发送请求以包含认证头
const response = await request({
url: `/resource/oss/download/${file.ossId}`,
method: 'get',
responseType: 'blob' // 指定响应类型为blob
});
console.log('[下载申请培训附件] 接口响应:', response);
// 创建blob URL并触发下载
const blob = response instanceof Blob ? response : new Blob([response.data]);
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = file.name || '申请培训附件';
document.body.appendChild(link);
link.click();
// 清理资源
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
console.log('[下载申请培训附件] 下载成功');
proxy?.$modal.msgSuccess('文件下载成功');
} catch (error: any) {
console.error('[下载申请培训附件] 下载失败:', error);
let errorMessage = '下载附件失败,请稍后重试';
if (error.response) {
console.error('[下载申请培训附件] HTTP响应错误:', {
status: error.response.status,
statusText: error.response.statusText
});
if (error.response.status === 401) {
errorMessage = '认证失败,请重新登录';
} else if (error.response.status === 403) {
errorMessage = '没有下载权限';
} else if (error.response.status === 404) {
errorMessage = '文件不存在或已被删除';
}
}
proxy?.$modal.msgError(errorMessage);
}
}
</script>
<style scoped>
.certificate-upload-container {
border: 1px dashed #d9d9d9;
border-radius: 6px;
padding: 15px;
background-color: #fafafa;
}
.certificate-upload-container:hover {
border-color: #409eff;
}
.attachment-upload-container {
border: 1px dashed #d9d9d9;
border-radius: 6px;
padding: 15px;
background-color: #fafafa;
box-sizing: border-box;
}
.attachment-upload-container .attachment-upload-demo {
width: 100%;
}
.attachment-upload-container .attachment-upload-demo .el-upload {
width: 100%;
height: auto;
background-color: transparent !important;
border: none !important;
}
.attachment-upload-container .attachment-upload-demo .el-upload:hover {
background-color: transparent !important;
}
.attachment-upload-container .el-upload-dragger {
background-color: transparent !important;
border: none !important;
width: 100% !important;
height: auto !important;
padding: 0 !important;
margin: 0 !important;
}
.attachment-upload-container .el-upload-dragger:hover {
background-color: transparent !important;
border: none !important;
}
.attachment-upload-container:hover {
border-color: #409eff;
}
.el-upload-dragger-attachment {
padding: 0 !important;
margin: 0 !important;
text-align: center;
background-color: transparent;
border: none;
min-height: 80px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
box-sizing: border-box;
}
.el-upload-dragger-attachment .el-icon--upload {
font-size: 24px;
color: #c0c4cc;
margin: 0;
}
.el-upload-dragger-attachment .el-upload__text {
color: #666666;
font-size: 12px;
line-height: 1.3;
margin: 0;
}
.el-upload-dragger-attachment .el-upload__text em {
color: #409eff;
font-style: normal;
font-weight: 500;
}
.el-upload-dragger-attachment .el-upload__tip {
font-size: 11px !important;
color: #909399 !important;
margin: 0 !important;
line-height: 1.2;
}
.record-certificate-upload {
margin-bottom: 10px;
}
.current-certificate {
margin-top: 10px;
padding: 8px;
background-color: #f0f9ff;
border: 1px solid #b3d8ff;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: space-between;
}
.current-certificate .el-tag {
margin-right: 10px;
}
.detail-text {
white-space: pre-wrap;
word-break: break-word;
}
.pagination-container {
padding: 20px 0;
text-align: center;
}
.search-btn-container {
text-align: center;
}
.button-card {
margin-top: 10px;
}
.table-card {
margin-bottom: 10px;
}
/* 证书上传拖拽区域样式 - 紧凑设计 */
.el-upload-dragger-certificate {
text-align: center;
padding: 10px 15px;
min-height: 40px;
border: 2px dashed #d9d9d9;
border-radius: 6px;
background-color: #fafafa;
transition: border-color 0.3s;
}
.el-upload-dragger-certificate:hover {
border-color: #409eff;
}
.el-upload-dragger-certificate .el-icon--upload {
font-size: 18px;
color: #c0c4cc;
margin-bottom: 4px;
}
.el-upload-dragger-certificate .el-upload__text {
color: #606266;
font-size: 12px;
line-height: 1.2;
}
.el-upload-dragger-certificate .el-upload__text em {
color: #409eff;
font-style: normal;
}
/* 证书文件卡片样式 */
.certificate-file-card {
position: relative;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
background: #fff;
border-radius: 6px;
overflow: hidden;
}
.certificate-file-thumb {
position: relative;
width: 100%;
height: 120px;
background: #f5f7fa;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.certificate-thumb-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.certificate-file-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: #909399;
}
.certificate-file-placeholder .el-icon {
font-size: 36px;
margin-bottom: 8px;
}
.certificate-file-placeholder .file-ext {
font-size: 12px;
font-weight: bold;
color: #f56c6c;
}
.certificate-file-loading {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.9);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 10;
}
.certificate-file-loading .el-icon {
font-size: 24px;
color: #409eff;
margin-bottom: 8px;
}
.certificate-file-loading .loading-text {
font-size: 12px;
color: #606266;
}
.certificate-file-name {
padding: 8px;
font-size: 12px;
color: #606266;
text-align: center;
background: #fff;
border-top: 1px solid #ebeef5;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.certificate-file-card-actions {
position: absolute;
top: 4px;
right: 4px;
background: rgba(255, 255, 255, 0.9);
border-radius: 4px;
padding: 2px;
}
.certificate-file-card-actions .el-button {
padding: 4px;
min-height: 24px;
}
.certificate-file-card-actions .el-button .el-icon {
font-size: 12px;
}
/* 证书上传整体容器优化 */
.certificate-upload-demo {
width: 100%;
}
/* 强制覆盖Element Plus默认高度 */
.certificate-upload-demo :deep(.el-upload) {
height: auto !important;
min-height: 40px !important;
}
.certificate-upload-demo :deep(.el-upload-dragger) {
height: auto !important;
min-height: 40px !important;
padding: 10px 15px !important;
}
.certificate-upload-demo :deep(.el-upload-list--picture-card) {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.certificate-upload-demo :deep(.el-upload--picture-card) {
width: auto;
height: auto;
min-height: 40px;
border: none;
background: transparent;
display: flex;
align-items: center;
}
.certificate-upload-demo :deep(.el-upload-list__item) {
width: 120px;
height: auto;
margin: 0;
border: 1px solid #ebeef5;
border-radius: 6px;
overflow: hidden;
}
/* 提示文字样式优化 */
.el-upload__tip {
margin-top: 8px !important;
font-size: 12px !important;
color: #909399 !important;
line-height: 1.4 !important;
}
/* 只读输入框样式 - 确保灰色背景被白色边框完全包裹 */
.readonly-input {
width: 100% !important;
}
/* 通用样式,适配不同版本的Element Plus */
.readonly-input :deep(.el-input),
.readonly-input :deep(.el-input__wrapper) {
background-color: #f5f7fa !important;
border: 1px solid #dcdfe6 !important;
border-radius: 4px !important;
box-shadow: none !important;
}
.readonly-input :deep(.el-input__wrapper) {
padding: 1px 11px !important;
min-height: 32px !important;
display: flex !important;
align-items: center !important;
}
.readonly-input :deep(.el-input__inner) {
background-color: transparent !important;
color: #606266 !important;
border: none !important;
cursor: not-allowed !important;
padding: 0 !important;
height: 30px !important;
line-height: 30px !important;
outline: none !important;
box-shadow: none !important;
}
/* 悬停和聚焦状态 */
.readonly-input :deep(.el-input:hover),
.readonly-input :deep(.el-input__wrapper:hover) {
border-color: #dcdfe6 !important;
box-shadow: none !important;
}
.readonly-input :deep(.el-input.is-focus),
.readonly-input :deep(.el-input__wrapper.is-focus) {
border-color: #dcdfe6 !important;
box-shadow: none !important;
}
.readonly-input :deep(.el-input:hover .el-input__inner),
.readonly-input :deep(.el-input.is-focus .el-input__inner) {
border-color: transparent !important;
box-shadow: none !important;
}
</style>
<style scoped>
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.el-tag {
margin-right: 5px;
}
/* 页面容器样式 */
.app-container {
padding: 8px;
}
/* 表格卡片布局 */
.table-card {
margin-top: 0;
}
.table-card :deep(.el-card__body) {
padding: 0px 20px 20px 20px !important;
}
/* 移除按钮栏底部边框 */
.button-card :deep(.el-card__header) {
border-bottom: none !important;
}
/* 分页容器样式 */
.pagination-container {
display: flex;
justify-content: flex-end;
padding: 16px 20px 0 0 !important;
background-color: #fff;
margin-top: 0px !important;
}
/* 搜索按钮容器样式 */
.search-btn-container {
display: flex;
justify-content: flex-end;
margin-top: 10px;
}
.search-btn-container :deep(.el-form-item) {
margin-bottom: 0 !important;
}
/* 表单提示信息 */
.el-form-item-msg {
font-size: 12px;
color: #909399;
margin-top: 5px;
}
/* 详情文本 */
.detail-text {
padding: 8px;
border: 1px solid #e4e7ed;
border-radius: 4px;
min-height: 60px;
max-height: 200px;
overflow-y: auto;
}
/* 审批历史项样式 */
.approval-history-item {
padding: 10px 0;
border-bottom: 1px solid #ebeef5;
}
.approval-history-item:last-child {
border-bottom: none;
}
.approval-header {
display: flex;
align-items: center;
margin-bottom: 5px;
}
.approver-name {
font-weight: bold;
margin-right: 10px;
}
.approval-level {
margin-right: 10px;
color: #909399;
}
.approval-opinion {
font-size: 14px;
color: #606266;
line-height: 1.6;
}
.no-data {
text-align: center;
padding: 20px;
color: #909399;
}
/* 文件卡片样式(参考宿舍卫生检查) */
.upload-file-card {
position: relative;
width: 100%;
height: 100%;
border-radius: 6px;
overflow: hidden;
background: #fff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transition: all 0.3s;
}
.upload-file-card:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
.upload-file-thumb {
position: relative;
width: 100%;
height: 110px;
background: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.upload-thumb-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.upload-file-image {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: #f5f5f5;
}
.upload-file-loading {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 10;
}
.upload-file-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: #909399;
}
.upload-file-placeholder .file-ext {
font-size: 12px;
margin-top: 5px;
}
.loading-text {
margin-top: 8px;
font-size: 12px;
color: #606266;
}
.upload-file-name {
padding: 6px 8px;
font-size: 11px;
color: #303133;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
background: #fafafa;
border-top: 1px solid #f0f0f0;
line-height: 1.2;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
word-break: break-all;
max-width: 100%;
}
.upload-file-card-actions {
display: flex;
justify-content: center;
align-items: center;
padding: 6px 8px;
background: #fafafa;
border-top: 1px solid #f0f0f0;
transition: all 0.3s;
height: 38px;
}
.upload-file-card-actions .el-button-group {
background: transparent;
border-radius: 4px;
padding: 0;
}
.upload-file-card-actions .el-button {
padding: 4px 8px;
border: 1px solid #e0e0e0;
background: #fff;
font-size: 12px;
margin: 0 2px;
border-radius: 3px;
transition: all 0.3s;
}
.upload-file-card-actions .el-button:hover {
background: #f5f5f5;
border-color: #409eff;
}
.upload-file-card-actions .el-button.is-disabled {
opacity: 0.5;
cursor: not-allowed;
background: #f5f5f5;
}
</style>
<style lang="scss" scoped>
.app-container {
// ... 现有样式
// 拒绝原因样式
.reject-reason {
:deep(.el-descriptions-item__content) {
padding: 12px;
}
.reject-info {
width: 100%;
.reject-title {
font-weight: bold;
font-size: 14px;
}
.reject-content {
margin-top: 8px;
padding: 8px;
background-color: #fef0f0;
border-radius: 4px;
color: #f56c6c;
white-space: pre-wrap;
word-break: break-word;
line-height: 1.5;
}
.reject-action {
margin-top: 10px;
text-align: right;
}
}
}
// 审批历史样式
.approval-history-item {
padding: 0 0 8px 0;
.approval-header {
display: flex;
align-items: center;
margin-bottom: 8px;
.approver-name {
font-weight: bold;
margin-right: 8px;
}
.approval-level {
color: #909399;
margin-right: 8px;
}
}
.approval-opinion {
background-color: #f5f7fa;
padding: 8px 12px;
border-radius: 4px;
color: #606266;
white-space: pre-wrap;
word-break: break-word;
line-height: 1.5;
}
}
.no-data {
text-align: center;
color: #909399;
padding: 30px 0;
}
// 证书预览样式
.certificate-preview {
.certificate-container {
display: flex;
align-items: center;
margin-bottom: 10px;
.image-preview {
.image-slot {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
width: 100%;
height: 100%;
background: #f5f7fa;
color: #909399;
font-size: 14px;
.el-icon {
font-size: 24px;
margin-bottom: 5px;
}
}
}
.image-preview {
.el-image {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.2s;
&:hover {
transform: scale(1.02);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
}
.loading-preview {
background: #f9f9f9;
color: #909399;
font-size: 14px;
.el-icon {
margin-right: 8px;
font-size: 18px;
}
}
.image-slot {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: #909399;
.el-icon {
font-size: 32px;
margin-bottom: 8px;
}
div {
font-size: 14px;
}
}
}
.file-info {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
border: 1px solid #dcdfe6;
border-radius: 4px;
background: #fafafa;
min-width: 200px;
.el-icon {
color: #909399;
margin-bottom: 10px;
}
.file-name {
font-size: 14px;
color: #606266;
margin-bottom: 5px;
word-break: break-all;
text-align: center;
}
.file-type {
font-size: 12px;
color: #909399;
}
}
}
.certificate-actions {
display: flex;
gap: 10px;
.el-button {
display: flex;
align-items: center;
gap: 5px;
}
}
}
.no-certificate {
display: flex;
justify-content: center;
align-items: center;
min-height: 120px;
}
}
</style>