|
|
@@ -0,0 +1,449 @@
|
|
|
+<template>
|
|
|
+ <div class="p-2 page-container">
|
|
|
+ <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
|
|
|
+ <div v-show="showSearch" class="mb-[10px]">
|
|
|
+ <el-card shadow="hover">
|
|
|
+ <el-form ref="queryFormRef" :model="queryParams" label-width="120px">
|
|
|
+ <el-row :gutter="20">
|
|
|
+ <el-col :span="6">
|
|
|
+ <el-form-item label="学生姓名" prop="studentName">
|
|
|
+ <el-input v-model="queryParams.studentName" placeholder="请输入学生姓名" clearable @keyup.enter="handleQuery" />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="6">
|
|
|
+ <el-form-item label="学号" prop="studentNo">
|
|
|
+ <el-input v-model="queryParams.studentNo" placeholder="请输入学号" clearable @keyup.enter="handleQuery" />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="6">
|
|
|
+ <el-form-item label="班级" prop="classId">
|
|
|
+ <el-select v-model="queryParams.classId" placeholder="请选择班级" clearable style="width: 100%">
|
|
|
+ <el-option v-for="classItem in classOptions" :key="classItem.value" :label="classItem.label" :value="classItem.value" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="6">
|
|
|
+ <el-form-item label="实习状态" prop="internshipStatus">
|
|
|
+ <el-select v-model="queryParams.internshipStatus" placeholder="请选择实习状态" clearable style="width: 100%">
|
|
|
+ <el-option v-for="dict in internship_status" :key="dict.value" :label="dict.label" :value="dict.value" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ <el-row :gutter="20">
|
|
|
+ <el-col :span="6">
|
|
|
+ <el-form-item label="实习公司" prop="internshipCompany">
|
|
|
+ <el-input v-model="queryParams.internshipCompany" placeholder="请输入实习公司名称" clearable @keyup.enter="handleQuery" />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="实习开始时间">
|
|
|
+ <el-date-picker
|
|
|
+ v-model="dateRange"
|
|
|
+ style="width: 100%"
|
|
|
+ value-format="YYYY-MM-DD"
|
|
|
+ type="daterange"
|
|
|
+ range-separator="-"
|
|
|
+ start-placeholder="开始日期"
|
|
|
+ end-placeholder="结束日期"
|
|
|
+ />
|
|
|
+ </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="handleQuery">搜索</el-button>
|
|
|
+ <el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </el-form>
|
|
|
+ </el-card>
|
|
|
+ </div>
|
|
|
+ </transition>
|
|
|
+
|
|
|
+ <!-- 统计卡片 -->
|
|
|
+ <el-card shadow="never" class="mb-[10px]" v-if="statisticsData">
|
|
|
+ <el-row :gutter="20" class="statistics-row">
|
|
|
+ <el-col :span="6">
|
|
|
+ <div class="statistics-card total-students">
|
|
|
+ <div class="statistics-content">
|
|
|
+ <div class="statistics-number">{{ statisticsData.totalCount || 0 }}</div>
|
|
|
+ <div class="statistics-label">总学生数</div>
|
|
|
+ </div>
|
|
|
+ <div class="statistics-icon">
|
|
|
+ <el-icon><User /></el-icon>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="6">
|
|
|
+ <div class="statistics-card not-internship">
|
|
|
+ <div class="statistics-content">
|
|
|
+ <div class="statistics-number">{{ statisticsData.noInternshipCount || 0 }}</div>
|
|
|
+ <div class="statistics-label">未实习</div>
|
|
|
+ </div>
|
|
|
+ <div class="statistics-icon">
|
|
|
+ <el-icon><Clock /></el-icon>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="6">
|
|
|
+ <div class="statistics-card internshiping">
|
|
|
+ <div class="statistics-content">
|
|
|
+ <div class="statistics-number">{{ statisticsData.internshipCount || 0 }}</div>
|
|
|
+ <div class="statistics-label">实习中</div>
|
|
|
+ </div>
|
|
|
+ <div class="statistics-icon">
|
|
|
+ <el-icon><Loading /></el-icon>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="6">
|
|
|
+ <div class="statistics-card completed">
|
|
|
+ <div class="statistics-content">
|
|
|
+ <div class="statistics-number">{{ statisticsData.internshipCount || 0 }}</div>
|
|
|
+ <div class="statistics-label">已完成</div>
|
|
|
+ <div class="statistics-rate">实习率:{{ statisticsData.internshipRate || '0.00%' }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="statistics-icon">
|
|
|
+ <el-icon><CircleCheck /></el-icon>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <el-card shadow="never" class="button-card table-card">
|
|
|
+ <template #header>
|
|
|
+ <el-row :gutter="10" class="mb8">
|
|
|
+ <el-col :span="1.5">
|
|
|
+ <el-button icon="Download" @click="handleExport" v-hasPermi="['zsjy:internship:export']">导出</el-button>
|
|
|
+ </el-col>
|
|
|
+ <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
|
|
|
+ </el-row>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <el-table v-loading="loading" border :data="internshipList" @row-dblclick="handleView">
|
|
|
+ <el-table-column label="学号" align="center" prop="studentNo" width="120" />
|
|
|
+ <el-table-column label="姓名" align="center" prop="studentName" width="100" />
|
|
|
+ <el-table-column label="性别" align="center" prop="gender" width="80" />
|
|
|
+ <el-table-column label="班级" align="center" prop="className" width="180" />
|
|
|
+ <el-table-column label="班主任" align="center" prop="classMasterName" width="100" />
|
|
|
+ <el-table-column label="实习状态" align="center" prop="internshipStatus" width="100">
|
|
|
+ <template #default="scope">
|
|
|
+ <dict-tag :options="internship_status" :value="scope.row.internshipStatus" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="实习公司" align="center" prop="internshipCompany" min-width="150" show-overflow-tooltip />
|
|
|
+ <el-table-column label="实习岗位" align="center" prop="internshipPosition" width="120" show-overflow-tooltip />
|
|
|
+ <el-table-column label="实习开始时间" align="center" prop="internshipStartTime" width="120">
|
|
|
+ <template #default="scope">
|
|
|
+ <span v-if="scope.row.internshipStartTime">{{ parseTime(scope.row.internshipStartTime, '{y}-{m}-{d}') }}</span>
|
|
|
+ <span v-else>-</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="实习地点" align="center" prop="internshipLocation" width="120" show-overflow-tooltip />
|
|
|
+ <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="120">
|
|
|
+ <template #default="scope">
|
|
|
+ <el-tooltip content="查看详情" placement="top">
|
|
|
+ <el-button
|
|
|
+ link
|
|
|
+ type="primary"
|
|
|
+ icon="View"
|
|
|
+ size="default"
|
|
|
+ style="font-size: 16px; color: blue"
|
|
|
+ @click="handleView(scope.row)"
|
|
|
+ v-hasPermi="['zsjy:internship:query']"
|
|
|
+ ></el-button>
|
|
|
+ </el-tooltip>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+
|
|
|
+ <div class="pagination-container">
|
|
|
+ <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <!-- 学生实习详情对话框 -->
|
|
|
+ <el-dialog title="学生实习详情" v-model="dialog.visible" width="800px" append-to-body>
|
|
|
+ <div v-if="studentDetail" class="student-detail">
|
|
|
+ <el-descriptions title="基本信息" :column="2" border>
|
|
|
+ <el-descriptions-item label="学号">{{ studentDetail.studentNo }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="姓名">{{ studentDetail.studentName }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="性别">{{ studentDetail.gender }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="手机号">{{ studentDetail.mobile }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="学院">{{ studentDetail.collegeName }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="专业">{{ studentDetail.majorName }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="班级">{{ studentDetail.className }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="班主任">{{ studentDetail.classMasterName }}</el-descriptions-item>
|
|
|
+ </el-descriptions>
|
|
|
+
|
|
|
+ <el-descriptions title="实习信息" :column="2" border class="mt-4">
|
|
|
+ <el-descriptions-item label="实习状态">
|
|
|
+ <dict-tag :options="internship_status" :value="studentDetail.internshipStatus" />
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="实习公司">{{ studentDetail.internshipCompany || '-' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="实习岗位">{{ studentDetail.internshipPosition || '-' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="实习地点">{{ studentDetail.internshipLocation || '-' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="实习开始时间">
|
|
|
+ <span v-if="studentDetail.internshipStartTime">{{ parseTime(studentDetail.internshipStartTime, '{y}-{m}-{d}') }}</span>
|
|
|
+ <span v-else>-</span>
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="实习结束时间">
|
|
|
+ <span v-if="studentDetail.internshipEndTime">{{ parseTime(studentDetail.internshipEndTime, '{y}-{m}-{d}') }}</span>
|
|
|
+ <span v-else>-</span>
|
|
|
+ </el-descriptions-item>
|
|
|
+ </el-descriptions>
|
|
|
+ </div>
|
|
|
+ <template #footer>
|
|
|
+ <div class="dialog-footer">
|
|
|
+ <el-button @click="cancel">关 闭</el-button>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup name="InternshipInfo" lang="ts">
|
|
|
+import { listInternshipInfo, getInternshipInfo, exportInternshipInfo, getClassOptions, getInternshipStatistics } from '@/api/zsjy/internship';
|
|
|
+import { InternshipInfoVO, InternshipInfoQuery, ClassInfoVO, InternshipStatisticsVO } from '@/api/zsjy/types';
|
|
|
+import { ComponentInternalInstance, getCurrentInstance, reactive, ref, toRefs } from 'vue';
|
|
|
+import { parseTime } from '@/utils/ruoyi';
|
|
|
+import { download } from '@/utils/request';
|
|
|
+
|
|
|
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
|
|
+const { internship_status } = toRefs<any>(proxy?.useDict('internship_status'));
|
|
|
+
|
|
|
+const internshipList = ref<InternshipInfoVO[]>([]);
|
|
|
+const classOptions = ref<ClassInfoVO[]>([]);
|
|
|
+const statisticsData = ref<InternshipStatisticsVO>({
|
|
|
+ totalCount: 0,
|
|
|
+ internshipCount: 0,
|
|
|
+ noInternshipCount: 0,
|
|
|
+ internshipRate: '0.00%'
|
|
|
+});
|
|
|
+const loading = ref(true);
|
|
|
+const showSearch = ref(true);
|
|
|
+const total = ref(0);
|
|
|
+const dateRange = ref<[string, string]>([]);
|
|
|
+const studentDetail = ref<InternshipInfoVO>();
|
|
|
+
|
|
|
+const data = reactive({
|
|
|
+ queryParams: {
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 10,
|
|
|
+ studentName: '',
|
|
|
+ studentNo: '',
|
|
|
+ classId: '',
|
|
|
+ internshipStatus: '',
|
|
|
+ internshipCompany: '',
|
|
|
+ startTimeBegin: '',
|
|
|
+ startTimeEnd: ''
|
|
|
+ } as InternshipInfoQuery,
|
|
|
+ dialog: {
|
|
|
+ visible: false,
|
|
|
+ title: ''
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+const { queryParams, dialog } = toRefs(data);
|
|
|
+
|
|
|
+/** 查询实习信息列表 */
|
|
|
+const getList = async () => {
|
|
|
+ loading.value = true;
|
|
|
+ // 处理日期范围
|
|
|
+ if (dateRange.value && dateRange.value.length === 2) {
|
|
|
+ queryParams.value.startTimeBegin = dateRange.value[0];
|
|
|
+ queryParams.value.startTimeEnd = dateRange.value[1];
|
|
|
+ } else {
|
|
|
+ queryParams.value.startTimeBegin = '';
|
|
|
+ queryParams.value.startTimeEnd = '';
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const res = await listInternshipInfo(queryParams.value);
|
|
|
+ internshipList.value = res.rows;
|
|
|
+ total.value = res.total;
|
|
|
+ loading.value = false;
|
|
|
+ } catch (error) {
|
|
|
+ loading.value = false;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+/** 获取班级选项 */
|
|
|
+const getClassOptionsList = async () => {
|
|
|
+ try {
|
|
|
+ const res = await getClassOptions();
|
|
|
+ classOptions.value = res.data;
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取班级选项失败:', error);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+/** 获取统计数据 */
|
|
|
+const getStatistics = async () => {
|
|
|
+ try {
|
|
|
+ const res = await getInternshipStatistics(queryParams.value);
|
|
|
+ if (res.code === 200 && res.data) {
|
|
|
+ statisticsData.value = res.data;
|
|
|
+ } else {
|
|
|
+ console.warn('获取统计数据异常:', res.msg || '未知错误');
|
|
|
+ // 设置默认统计数据
|
|
|
+ statisticsData.value = {
|
|
|
+ totalCount: 0,
|
|
|
+ internshipCount: 0,
|
|
|
+ noInternshipCount: 0,
|
|
|
+ internshipRate: '0.00%'
|
|
|
+ };
|
|
|
+ }
|
|
|
+ } catch (error: any) {
|
|
|
+ console.error('获取统计数据失败:', error);
|
|
|
+ // 设置默认统计数据
|
|
|
+ statisticsData.value = {
|
|
|
+ totalCount: 0,
|
|
|
+ internshipCount: 0,
|
|
|
+ noInternshipCount: 0,
|
|
|
+ internshipRate: '0.00%'
|
|
|
+ };
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+/** 搜索按钮操作 */
|
|
|
+const handleQuery = () => {
|
|
|
+ queryParams.value.pageNum = 1;
|
|
|
+ getList();
|
|
|
+ getStatistics();
|
|
|
+};
|
|
|
+
|
|
|
+/** 重置按钮操作 */
|
|
|
+const resetQuery = () => {
|
|
|
+ dateRange.value = [];
|
|
|
+ proxy?.resetForm('queryFormRef');
|
|
|
+ handleQuery();
|
|
|
+};
|
|
|
+
|
|
|
+/** 查看详情按钮操作 */
|
|
|
+const handleView = async (row: InternshipInfoVO) => {
|
|
|
+ try {
|
|
|
+ const res = await getInternshipInfo(row.studentId!);
|
|
|
+ studentDetail.value = res.data;
|
|
|
+ dialog.value.visible = true;
|
|
|
+ dialog.value.title = '学生实习详情';
|
|
|
+ } catch (error) {
|
|
|
+ proxy?.$modal.msgError('获取详情失败');
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+/** 导出按钮操作 */
|
|
|
+const handleExport = () => {
|
|
|
+ proxy?.$modal.confirm('是否确认导出所有实习信息数据项?').then(async () => {
|
|
|
+ try {
|
|
|
+ const res = await exportInternshipInfo(queryParams.value);
|
|
|
+ download.excel(res, '实习信息数据.xlsx');
|
|
|
+ } catch (error) {
|
|
|
+ proxy?.$modal.msgError('导出失败');
|
|
|
+ }
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+/** 取消按钮 */
|
|
|
+const cancel = () => {
|
|
|
+ dialog.value.visible = false;
|
|
|
+ studentDetail.value = undefined;
|
|
|
+};
|
|
|
+
|
|
|
+// 初始化
|
|
|
+(() => {
|
|
|
+ getList();
|
|
|
+ getClassOptionsList();
|
|
|
+ getStatistics();
|
|
|
+})();
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.page-container {
|
|
|
+ .search-btn-container {
|
|
|
+ text-align: right;
|
|
|
+ margin-top: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .statistics-row {
|
|
|
+ .statistics-card {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding: 20px;
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
|
|
+ color: white;
|
|
|
+ position: relative;
|
|
|
+ overflow: hidden;
|
|
|
+
|
|
|
+ .statistics-content {
|
|
|
+ flex: 1;
|
|
|
+
|
|
|
+ .statistics-number {
|
|
|
+ font-size: 28px;
|
|
|
+ font-weight: bold;
|
|
|
+ margin-bottom: 5px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .statistics-label {
|
|
|
+ font-size: 14px;
|
|
|
+ opacity: 0.9;
|
|
|
+ }
|
|
|
+
|
|
|
+ .statistics-rate {
|
|
|
+ font-size: 12px;
|
|
|
+ opacity: 0.8;
|
|
|
+ margin-top: 2px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .statistics-icon {
|
|
|
+ font-size: 40px;
|
|
|
+ opacity: 0.8;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.total-students {
|
|
|
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
+ }
|
|
|
+
|
|
|
+ &.not-internship {
|
|
|
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
|
|
+ }
|
|
|
+
|
|
|
+ &.internshiping {
|
|
|
+ background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
|
|
+ }
|
|
|
+
|
|
|
+ &.completed {
|
|
|
+ background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .button-card {
|
|
|
+ .mb8 {
|
|
|
+ margin-bottom: 8px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .table-card {
|
|
|
+ .pagination-container {
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ margin-top: 20px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .student-detail {
|
|
|
+ .mt-4 {
|
|
|
+ margin-top: 20px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|