Files
school-developer/src/views/stuwork/stuconduct/index.vue
2026-03-09 12:03:19 +08:00

550 lines
17 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="modern-page-container">
<div class="page-wrapper">
<!-- 查询条件 -->
<el-card v-show="showSearch" class="search-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Search /></el-icon>
查询条件
</span>
</div>
</template>
<el-form :model="state.queryForm" ref="searchFormRef" :inline="true" @keyup.enter="getDataList" class="search-form">
<el-form-item label="学年" prop="schoolYear">
<el-select v-model="state.queryForm.schoolYear" placeholder="请选择学年" clearable filterable style="width: 200px">
<el-option v-for="item in schoolYearList" :key="item.year" :label="item.year" :value="item.year"> </el-option>
</el-select>
</el-form-item>
<el-form-item label="学期" prop="schoolTerm">
<el-select v-model="state.queryForm.schoolTerm" placeholder="请选择学期" clearable style="width: 200px">
<el-option v-for="item in schoolTermList" :key="item.value" :label="item.label" :value="item.value"> </el-option>
</el-select>
</el-form-item>
<el-form-item label="学院" prop="deptCode">
<el-select
v-model="state.queryForm.deptCode"
placeholder="请选择学院"
clearable
filterable
style="width: 200px"
@change="handleDeptChange"
>
<el-option v-for="item in deptList" :key="item.deptCode" :label="item.deptName" :value="item.deptCode"> </el-option>
</el-select>
</el-form-item>
<el-form-item label="班级" prop="classCode">
<el-select v-model="state.queryForm.classCode" placeholder="请选择班级" clearable filterable style="width: 200px">
<el-option v-for="item in classList" :key="item.classCode" :label="item.classNo" :value="item.classCode"> </el-option>
</el-select>
</el-form-item>
<el-form-item label="姓名" prop="realName">
<el-input v-model="state.queryForm.realName" placeholder="请输入姓名" clearable style="width: 200px" />
</el-form-item>
<el-form-item label="类型" prop="conductType">
<el-select v-model="state.queryForm.conductType" placeholder="请选择类型" clearable style="width: 200px">
<el-option v-for="item in typeList" :key="item.value" :label="item.label" :value="item.value"> </el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" plain icon="Search" @click="getDataList">查询</el-button>
<el-button icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 数据列表 -->
<el-card class="content-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Document /></el-icon>
学生行为记录列表
</span>
<div class="header-actions">
<el-button icon="Plus" type="primary" @click="formDialogRef.openDialog()"> 新增 </el-button>
<el-button icon="Upload" type="success" class="ml10" @click="handleImport"> 导入行为记录 </el-button>
<el-button icon="Download" type="primary" plain class="ml10" @click="handleDownloadConductTemplate"> 导入考核模板 </el-button>
<el-button icon="Upload" type="warning" class="ml10" @click="handleConductImport"> 导入考核 </el-button>
<right-toolbar v-model:showSearch="showSearch" class="ml10" @queryTable="getDataList">
<TableColumnControl
ref="columnControlRef"
:columns="tableColumns"
v-model="visibleColumns"
trigger-type="default"
trigger-circle
@change="handleColumnChange"
@order-change="handleColumnOrderChange"
>
<template #trigger>
<el-tooltip class="item" effect="dark" content="列设置" placement="top">
<el-button circle style="margin-left: 0">
<el-icon><Menu /></el-icon>
</el-button>
</el-tooltip>
</template>
</TableColumnControl>
</right-toolbar>
</div>
</div>
</template>
<!-- 表格 -->
<el-table
:data="state.dataList"
v-loading="state.loading"
stripe
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
class="modern-table"
>
<el-table-column type="index" label="序号" width="70" align="center">
<template #header>
<el-icon><List /></el-icon>
</template>
<template #default="{ $index }">
{{ $index + 1 + ((state.pagination?.current || 1) - 1) * (state.pagination?.size || 10) }}
</template>
</el-table-column>
<template v-for="col in visibleColumnsSorted" :key="col.prop || col.label">
<el-table-column
v-if="checkColumnVisible(col.prop || '') && col.prop !== '序号'"
:prop="col.prop"
:label="col.label"
show-overflow-tooltip
align="center"
:min-width="col.minWidth"
:width="col.width"
>
<template #header>
<el-icon><component :is="columnConfigMap[col.prop]?.icon || Calendar" /></el-icon>
<span style="margin-left: 4px">{{ col.label }}</span>
</template>
<!-- 学期格式化 -->
<template v-if="col.prop === 'schoolTerm'" #default="scope">
<el-tag size="small" type="primary" effect="plain">
{{ formatSchoolTerm(scope.row.schoolTerm) }}
</el-tag>
</template>
<!-- 类型格式化 -->
<template v-else-if="col.prop === 'conductType'" #default="scope">
<el-tag size="small" type="warning" effect="plain">
{{ formatType(scope.row.conductType) }}
</el-tag>
</template>
<!-- 记录日期格式化 -->
<template v-else-if="col.prop === 'recordDate'" #default="scope">
<span>{{ scope.row.recordDate || '-' }}</span>
</template>
<!-- 附件格式化 -->
<template v-else-if="col.prop === 'attachment'" #default="scope">
<el-button v-if="scope.row.attachment" icon="Document" text type="primary" size="small" @click="handleViewAttachment(scope.row)">
查看
</el-button>
<span v-else>-</span>
</template>
</el-table-column>
</template>
<el-table-column label="操作" width="150" align="center" fixed="right">
<template #header>
<el-icon><Setting /></el-icon>
<span style="margin-left: 4px">操作</span>
</template>
<template #default="scope">
<el-button icon="Edit" link type="primary" @click="handleEdit(scope.row)"> 编辑 </el-button>
<el-button icon="Delete" link type="danger" @click="handleDelete(scope.row)"> 删除 </el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-wrapper">
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
</div>
</el-card>
</div>
<!-- 新增/编辑对话框 -->
<form-dialog ref="formDialogRef" @refresh="getDataList" />
<!-- 导入对话框 -->
<el-dialog title="导入行为记录" v-model="importDialogVisible" :width="500" :close-on-click-modal="false" draggable>
<el-upload ref="uploadRef" :auto-upload="false" :on-change="handleFileChange" :limit="1" accept=".xlsx,.xls" drag>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">将文件拖到此处<em>点击上传</em></div>
<template #tip>
<div class="el-upload__tip">只能上传 xlsx/xls 文件</div>
</template>
</el-upload>
<template #footer>
<span class="dialog-footer">
<el-button @click="importDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleImportSubmit" :disabled="!importFile || importLoading">确认</el-button>
</span>
</template>
</el-dialog>
<!-- 导入操行考核弹窗 -->
<el-dialog title="导入操行考核数据" v-model="conductImportDialogVisible" :width="500" :close-on-click-modal="false" draggable>
<div style="margin-bottom: 15px">
<el-button icon="Download" type="success" @click="handleDownloadConductTemplate"> 下载模板 </el-button>
</div>
<el-upload :auto-upload="false" :on-change="handleConductFileChange" :limit="1" accept=".xlsx,.xls" drag>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">将文件拖到此处<em>点击上传</em></div>
<template #tip>
<div class="el-upload__tip">只能上传 xlsx/xls 文件请先下载导入模板</div>
</template>
</el-upload>
<template #footer>
<span class="dialog-footer">
<el-button @click="conductImportDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleConductImportSubmit" :disabled="!conductImportFile || conductImportLoading">确认导入</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts" name="StuConduct">
import { reactive, ref, onMounted, computed, nextTick } from 'vue';
import { useRoute } from 'vue-router';
import { BasicTableProps, useTable } from '/@/hooks/table';
import { fetchList, delObj, importExcel } from '/@/api/stuwork/stuconduct';
import { exportConductAssessmentTemplate, importConductAssessment, downloadBlobFile } from '/@/api/stuwork/file';
import { getDeptList } from '/@/api/basic/basicclass';
import { queryAllSchoolYear } from '/@/api/basic/basicyear';
import { getClassListByRole } from '/@/api/basic/basicclass';
import { getDicts } from '/@/api/admin/dict';
import { useMessage, useMessageBox } from '/@/hooks/message';
import TableColumnControl from '/@/components/TableColumnControl/index.vue';
import {
UploadFilled,
List,
CreditCard,
Calendar,
Clock,
OfficeBuilding,
Grid,
Avatar,
Collection,
Document,
Setting,
Menu,
Search,
} from '@element-plus/icons-vue';
import { useTableColumnControl } from '/@/hooks/tableColumn';
import FormDialog from './form.vue';
// 定义变量
const route = useRoute();
const formDialogRef = ref();
const columnControlRef = ref<any>();
const uploadRef = ref();
const searchFormRef = ref();
const showSearch = ref(true);
const schoolYearList = ref<any[]>([]);
const schoolTermList = ref<any[]>([]);
const deptList = ref<any[]>([]);
const classList = ref<any[]>([]);
const typeList = ref<any[]>([]);
const importDialogVisible = ref(false);
const importFile = ref<File | null>(null);
const importLoading = ref(false);
const conductImportDialogVisible = ref(false);
const conductImportFile = ref<File | null>(null);
const conductImportLoading = ref(false);
// 表格列配置
const tableColumns = [
{ prop: 'stuNo', label: '学号' },
{ prop: 'schoolYear', label: '学年' },
{ prop: 'schoolTerm', label: '学期' },
{ prop: 'deptName', label: '学院' },
{ prop: 'classNo', label: '班级' },
{ prop: 'realName', label: '姓名' },
{ prop: 'conductType', label: '类型' },
{ prop: 'recordDate', label: '记录日期', width: 120 },
{ prop: 'description', label: '描述说明', minWidth: 150 },
{ prop: 'attachment', label: '附件', width: 100 },
];
// 列配置映射
const columnConfigMap: Record<string, { icon: any }> = {
stuNo: { icon: CreditCard },
schoolYear: { icon: Calendar },
schoolTerm: { icon: Clock },
deptName: { icon: OfficeBuilding },
classNo: { icon: Grid },
realName: { icon: Avatar },
conductType: { icon: Collection },
recordDate: { icon: Calendar },
description: { icon: Document },
attachment: { icon: Document },
};
// 表格列控制hook
const { visibleColumns, visibleColumnsSorted, checkColumnVisible, handleColumnChange, handleColumnOrderChange } = useTableColumnControl(
tableColumns,
route.path
);
// 使用 useTable
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
schoolYear: '',
schoolTerm: '',
deptCode: '',
classCode: '',
realName: '',
conductType: '',
},
pageList: fetchList,
props: {
item: 'records',
totalCount: 'total',
},
createdIsNeed: true,
});
// table hook
const { getDataList, currentChangeHandle, sizeChangeHandle, tableStyle } = useTable(state);
// 格式化学期
const formatSchoolTerm = (value: string | number) => {
if (value === null || value === undefined || value === '') {
return '-';
}
const dictItem = schoolTermList.value.find((item) => item.value == value);
return dictItem ? dictItem.label : value;
};
// 格式化类型
const formatType = (value: string) => {
if (!value) return '-';
const item = typeList.value.find((item: any) => item.value === value);
return item ? item.label : value;
};
// 学院变化
const handleDeptChange = () => {
// 学院变化时清空班级选择
};
// 重置
const handleReset = () => {
searchFormRef.value?.resetFields();
state.queryForm.schoolYear = '';
state.queryForm.schoolTerm = '';
state.queryForm.deptCode = '';
state.queryForm.classCode = '';
state.queryForm.realName = '';
state.queryForm.conductType = '';
getDataList();
};
// 查看附件
const handleViewAttachment = (row: any) => {
if (row.attachment) {
const urls = typeof row.attachment === 'string' ? row.attachment.split(',') : [row.attachment];
urls.forEach((url: string) => {
if (url.trim()) {
window.open(url.trim(), '_blank');
}
});
}
};
// 导入
const handleImport = () => {
importDialogVisible.value = true;
importFile.value = null;
uploadRef.value?.clearFiles();
};
// 文件变化
const handleFileChange = (file: any) => {
importFile.value = file.raw;
};
// 提交导入
const handleImportSubmit = async () => {
if (!importFile.value) {
useMessage().warning('请先选择要上传的文件');
return;
}
importLoading.value = true;
try {
await importExcel(importFile.value);
useMessage().success('导入成功');
importDialogVisible.value = false;
importFile.value = null;
uploadRef.value?.clearFiles();
getDataList();
} catch (err: any) {
useMessage().error(err.msg || '导入失败');
} finally {
importLoading.value = false;
}
};
// 下载操行考核导入模板
const handleDownloadConductTemplate = async () => {
try {
await downloadBlobFile(exportConductAssessmentTemplate(), `操行考核导入模板_${Date.now()}.xlsx`);
} catch (err: any) {
useMessage().error(err?.msg || '下载模板失败');
}
};
// 打开操行考核导入弹窗
const handleConductImport = () => {
conductImportDialogVisible.value = true;
conductImportFile.value = null;
};
// 操行考核文件变化
const handleConductFileChange = (file: any) => {
conductImportFile.value = file.raw;
};
// 提交操行考核导入
const handleConductImportSubmit = async () => {
if (!conductImportFile.value) {
useMessage().warning('请先选择要上传的文件');
return;
}
conductImportLoading.value = true;
try {
const formData = new FormData();
formData.append('file', conductImportFile.value);
await importConductAssessment(formData);
useMessage().success('导入成功');
conductImportDialogVisible.value = false;
conductImportFile.value = null;
getDataList();
} catch (err: any) {
useMessage().error(err.msg || '导入失败');
} finally {
conductImportLoading.value = false;
}
};
// 编辑
const handleEdit = (row: any) => {
formDialogRef.value?.openDialog('edit', row);
};
// 删除
const handleDelete = async (row: any) => {
const { confirm } = useMessageBox();
try {
await confirm('确定要删除该记录吗?');
await delObj([row.id]);
useMessage().success('删除成功');
getDataList();
} catch (err: any) {
if (err !== 'cancel') {
useMessage().error(err.msg || '删除失败');
}
}
};
// 获取学年列表
const getSchoolYearList = async () => {
try {
const res = await queryAllSchoolYear();
if (res.data && Array.isArray(res.data)) {
schoolYearList.value = res.data;
} else {
schoolYearList.value = [];
}
} catch (err) {
schoolYearList.value = [];
}
};
// 获取学期字典
const getSchoolTermDict = async () => {
try {
const res = await getDicts('school_term');
if (res.data && Array.isArray(res.data)) {
schoolTermList.value = res.data.map((item: any) => ({
label: item.label || item.dictLabel || item.name,
value: item.value || item.dictValue || item.code,
}));
} else {
schoolTermList.value = [];
}
} catch (err) {
schoolTermList.value = [];
}
};
// 获取学院列表
const getDeptListData = async () => {
try {
const res = await getDeptList();
if (res.data && Array.isArray(res.data)) {
deptList.value = res.data;
} else {
deptList.value = [];
}
} catch (err) {
deptList.value = [];
}
};
// 获取班级列表
const getClassListData = async () => {
try {
const res = await getClassListByRole();
if (res.data && Array.isArray(res.data)) {
classList.value = res.data;
} else {
classList.value = [];
}
} catch (err) {
classList.value = [];
}
};
// 获取类型字典
const getTypeDict = async () => {
try {
const res = await getDicts('conduct_type');
if (res.data && Array.isArray(res.data)) {
typeList.value = res.data.map((item: any) => ({
label: item.label || item.dictLabel || item.name,
value: item.value || item.dictValue || item.code,
}));
} else {
typeList.value = [];
}
} catch (err) {
typeList.value = [];
}
};
// 初始化
onMounted(() => {
getSchoolYearList();
getSchoolTermDict();
getDeptListData();
getClassListData();
getTypeDict();
nextTick(() => {
if (visibleColumns.value.length === 0) {
}
});
});
</script>
<style scoped lang="scss">
@import '/@/assets/styles/modern-page.scss';
</style>