食堂问卷调查

This commit is contained in:
yaojian
2026-03-11 11:30:51 +08:00
parent a7da30f6c4
commit 553dbe5137
22 changed files with 3589 additions and 133 deletions

View File

@@ -0,0 +1,98 @@
import request from '/@/utils/request';
/**
* 分页查询顶岗班级计划
* @param query 查询参数
*/
export const fetchList = (query?: any) => {
return request({
url: '/basic/basicpracticeclassplan/page',
method: 'get',
params: query,
});
};
/**
* 通过id查询顶岗班级计划
* @param id 主键id
*/
export const getObj = (id: string) => {
return request({
url: '/basic/basicpracticeclassplan/detail',
method: 'get',
params: { id },
});
};
/**
* 新增顶岗班级计划
* @param obj 数据对象
*/
export const addObj = (obj: any) => {
return request({
url: '/basic/basicpracticeclassplan',
method: 'post',
data: obj,
});
};
/**
* 修改顶岗班级计划
* @param obj 数据对象
*/
export const putObj = (obj: any) => {
return request({
url: '/basic/basicpracticeclassplan/edit',
method: 'post',
data: obj,
});
};
/**
* 删除顶岗班级计划
* @param ids id数组
*/
export const delObj = (ids: string[]) => {
return request({
url: '/basic/basicpracticeclassplan/delete',
method: 'post',
data: ids,
});
};
/**
* 下载导入模板
*/
export const downloadImportTemplate = () => {
return request({
url: '/basic/basicpracticeclassplan/importTemplate',
method: 'get',
responseType: 'blob',
});
};
/**
* 导入顶岗班级计划
* @param formData 文件表单数据
*/
export const importData = (formData: FormData) => {
return request({
url: '/basic/basicpracticeclassplan/import',
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data',
},
});
};
/**
* 导出模板
*/
export const exportTemplate = () => {
return request({
url: '/basic/basicpracticeclassplan/exportTemplate',
method: 'get',
responseType: 'blob',
});
};

View File

@@ -0,0 +1,61 @@
import request from '/@/utils/request';
/**
* 分页查询班主任任职/调换申请
* @param query
*/
export const fetchList = (query?: any) => {
return request({
url: '/stuwork/classmasterjobapply/page',
method: 'get',
params: query,
});
};
/**
* 获取详情
* @param id
*/
export const getDetail = (id: string) => {
return request({
url: '/stuwork/classmasterjobapply/detail',
method: 'get',
params: { id },
});
};
/**
* 新增班主任任职/调换申请
* @param data
*/
export const addObj = (data: any) => {
return request({
url: '/stuwork/classmasterjobapply/add',
method: 'post',
data,
});
};
/**
* 班主任任职/调换申请审批/撤回
* @param data
*/
export const auditObj = (data: any) => {
return request({
url: '/stuwork/classmasterjobapply/audit',
method: 'post',
data,
});
};
/**
* 删除班主任任职/调换申请
* @param ids
*/
export const delObj = (ids: string[]) => {
return request({
url: '/stuwork/classmasterjobapply/delete',
method: 'post',
data: ids,
});
};

View File

@@ -0,0 +1,66 @@
import request from '/@/utils/request';
/**
* 分页查询食堂列表
*/
export const fetchList = (query?: any) => {
return request({
url: '/stuwork/dininghall/page',
method: 'get',
params: query,
});
};
/**
* 获取食堂列表(不分页)
*/
export const getList = () => {
return request({
url: '/stuwork/dininghall/list',
method: 'get',
});
};
/**
* 获取食堂详情
*/
export const getObj = (id: string) => {
return request({
url: '/stuwork/dininghall/detail',
method: 'get',
params: { id },
});
};
/**
* 新增食堂
*/
export const addObj = (data: any) => {
return request({
url: '/stuwork/dininghall',
method: 'post',
data,
});
};
/**
* 修改食堂
*/
export const putObj = (data: any) => {
return request({
url: '/stuwork/dininghall/edit',
method: 'post',
data,
});
};
/**
* 删除食堂
*/
export const delObjs = (ids: string[]) => {
return request({
url: '/stuwork/dininghall/delete',
method: 'post',
data: ids,
});
};

View File

@@ -0,0 +1,66 @@
import request from '/@/utils/request';
/**
* 分页查询食堂调查题目列表
*/
export const fetchList = (query?: any) => {
return request({
url: '/stuwork/dininghallvote/page',
method: 'get',
params: query,
});
};
/**
* 获取食堂调查题目详情
*/
export const getObj = (id: string) => {
return request({
url: '/stuwork/dininghallvote/detail',
method: 'get',
params: { id },
});
};
/**
* 新增食堂调查题目
*/
export const addObj = (data: any) => {
return request({
url: '/stuwork/dininghallvote',
method: 'post',
data,
});
};
/**
* 修改食堂调查题目
*/
export const putObj = (data: any) => {
return request({
url: '/stuwork/dininghallvote/edit',
method: 'post',
data,
});
};
/**
* 删除食堂调查题目
*/
export const delObjs = (ids: string[]) => {
return request({
url: '/stuwork/dininghallvote/delete',
method: 'post',
data: ids,
});
};
/**
* 获取题目类型字典
*/
export const getQuestionnaireDict = () => {
return request({
url: '/admin/dict/type/questionnaire',
method: 'get',
});
};

View File

@@ -0,0 +1,22 @@
import request from '/@/utils/request';
/**
* 分页查询食堂调查明细
*/
export const fetchList = (query?: any) => {
return request({
url: '/stuwork/dininghallvoteresult/page',
method: 'get',
params: query,
});
};
/**
* 获取食堂列表
*/
export const getDiningHallList = () => {
return request({
url: '/stuwork/dininghall/list',
method: 'get',
});
};

View File

@@ -0,0 +1,23 @@
import request from '/@/utils/request';
/**
* 获取学生统计列表
*/
export const getStatisticsList = (query?: any) => {
return request({
url: '/stuwork/dininghallvoteresultanalysis/getStatisticsList',
method: 'get',
params: query,
});
};
/**
* 获取教职工统计列表
*/
export const getStatisticsListByTea = (query?: any) => {
return request({
url: '/stuwork/dininghallvoteresultanalysis/getStatisticsListByTea',
method: 'get',
params: query,
});
};

View File

@@ -0,0 +1,44 @@
import request from '/@/utils/request';
/**
* 获取部门统计列表
*/
export const getStatisticsDept = (query?: any) => {
return request({
url: '/stuwork/employmentinformationsurvey/getStatisticsDept',
method: 'get',
params: query,
});
};
/**
* 获取班级统计列表
*/
export const getStatisticsClass = (query?: any) => {
return request({
url: '/stuwork/employmentinformationsurvey/getStatisticsClass',
method: 'get',
params: query,
});
};
/**
* 获取班级学生信息
*/
export const getClassStudentInfo = (query?: any) => {
return request({
url: '/stuwork/employmentinformationsurvey/getClassStudentInfo',
method: 'get',
params: query,
});
};
/**
* 获取班主任班级列表
*/
export const queryMasterClass = () => {
return request({
url: '/basic/basicclass/queryMasterClass',
method: 'get',
});
};

View File

@@ -0,0 +1,83 @@
import request from '/@/utils/request';
/**
* 分页查询学籍异动流失配置
* @param query
*/
export const fetchList = (query?: any) => {
return request({
url: '/stuwork/stuturnoverlossconfig/page',
method: 'get',
params: query,
});
};
/**
* 获取详情
* @param id
*/
export const getDetail = (id: string) => {
return request({
url: '/stuwork/stuturnoverlossconfig/detail',
method: 'get',
params: { id },
});
};
/**
* 新增学籍异动流失配置
* @param data
*/
export const addObj = (data: any) => {
return request({
url: '/stuwork/stuturnoverlossconfig',
method: 'post',
data,
});
};
/**
* 编辑学籍异动流失配置
* @param data
*/
export const editObj = (data: any) => {
return request({
url: '/stuwork/stuturnoverlossconfig/edit',
method: 'post',
data,
});
};
/**
* 删除学籍异动流失配置
* @param ids
*/
export const delObj = (ids: string[]) => {
return request({
url: '/stuwork/stuturnoverlossconfig/delete',
method: 'post',
data: ids,
});
};
/**
* 获取所有流失类型的异动配置
*/
export const getAllLossTurnoverTypes = () => {
return request({
url: '/stuwork/stuturnoverlossconfig/getAllLossTurnoverTypes',
method: 'get',
});
};
/**
* 根据异动类型判断是否属于流失
* @param turnoverType
*/
export const isTurnoverTypeLoss = (turnoverType: string) => {
return request({
url: '/stuwork/stuturnoverlossconfig/isTurnoverTypeLoss',
method: 'get',
params: { turnoverType },
});
};

View File

@@ -96,6 +96,29 @@
</el-table>
<div v-if="punishList.length === 0 && !punishLoading" style="text-align: center; padding: 20px; color: #909399">暂无数据</div>
</el-tab-pane>
<el-tab-pane label="顶岗计划" name="practicePlan">
<el-table :data="practicePlanList" border style="width: 100%" v-loading="practicePlanLoading">
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="practiceStartDate" label="顶岗开始时间" width="120" />
<el-table-column prop="practiceEndDate" label="顶岗结束时间" width="120" />
<el-table-column prop="status" label="状态" width="100">
<template #default="scope">
<el-tag :type="getPracticeStatusType(scope.row.status)" size="small">
{{ getPracticeStatusLabel(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="needRemind" label="是否提醒" width="100">
<template #default="scope">
<span>{{ scope.row.needRemind === '1' ? '是' : '否' }}</span>
</template>
</el-table-column>
<el-table-column prop="remindDays" label="提醒天数" width="100" />
<el-table-column prop="remarks" label="备注" show-overflow-tooltip />
</el-table>
<div v-if="practicePlanList.length === 0 && !practicePlanLoading" style="text-align: center; padding: 20px; color: #909399">暂无数据</div>
</el-tab-pane>
</el-tabs>
</div>
<template #footer>
@@ -108,13 +131,13 @@
<script setup lang="ts" name="BasicClassDetail">
import { ref } from 'vue';
import { useMessage } from '/@/hooks/message';
import { queryClassHonorByClassCode } from '/@/api/stuwork/classhonor';
import { queryDataByClassCode } from '/@/api/stuwork/classpublicity';
import { getClassRoomByClassCode } from '/@/api/stuwork/teachclassroomassign';
import { queryStuNumByClassCode } from '/@/api/basic/basicstudent';
import { fearchStuNumByClassCode } from '/@/api/stuwork/dormroomstudent';
import { queryPunlishNumByClass } from '/@/api/stuwork/stupunlish';
import { fetchList as fetchPracticePlanList } from '/@/api/basic/basicpracticeclassplan';
// 定义变量内容
const visible = ref(false);
@@ -136,6 +159,10 @@ const dormStudentLoading = ref(false);
const punishList = ref<any[]>([]);
const punishLoading = ref(false);
// 顶岗计划数据
const practicePlanList = ref<any[]>([]);
const practicePlanLoading = ref(false);
// 打开弹窗
const openDialog = async (rowData: any) => {
visible.value = true;
@@ -159,6 +186,7 @@ const loadAllData = async (classCode: string) => {
loadStudentNumData(classCode),
loadDormStudentData(classCode),
loadPunishData(classCode),
loadPracticePlanData(classCode),
]);
loading.value = false;
@@ -260,6 +288,48 @@ const loadPunishData = async (classCode: string) => {
}
};
// 加载顶岗计划数据
const loadPracticePlanData = async (classCode: string) => {
practicePlanLoading.value = true;
try {
const res = await fetchPracticePlanList({ classCode, current: 1, size: 100 });
if (res.data && res.data.records) {
practicePlanList.value = res.data.records;
} else if (Array.isArray(res.data)) {
practicePlanList.value = res.data;
} else {
practicePlanList.value = [];
}
} catch (err: any) {
console.error('获取顶岗计划失败', err);
practicePlanList.value = [];
} finally {
practicePlanLoading.value = false;
}
};
// 获取顶岗计划状态标签
const getPracticeStatusLabel = (status: string) => {
const statusMap: Record<string, string> = {
'0': '待执行',
'1': '执行中',
'2': '已完成',
'3': '已取消',
};
return statusMap[status] || '-';
};
// 获取顶岗计划状态类型
const getPracticeStatusType = (status: string) => {
const typeMap: Record<string, string> = {
'0': 'info',
'1': 'warning',
'2': 'success',
'3': 'danger',
};
return typeMap[status] || 'info';
};
// 暴露方法给父组件
defineExpose({
openDialog,

View File

@@ -61,6 +61,7 @@
<el-button icon="Link" type="success" class="ml10" @click="handleLinkRule"> 关联门禁规则 </el-button>
<el-button icon="Download" type="warning" class="ml10" @click="handleExport"> 导出 </el-button>
<el-button icon="DocumentAdd" type="info" class="ml10" @click="handleGenerateAssessment"> 生成考核班级 </el-button>
<el-button icon="Upload" type="primary" plain class="ml10" @click="handlePracticePlanImport"> 顶岗计划导入 </el-button>
<right-toolbar v-model:showSearch="showSearch" class="ml10" @queryTable="getDataList">
<TableColumnControl
ref="columnControlRef"
@@ -176,6 +177,15 @@
</span>
</template>
</el-dialog>
<!-- 顶岗计划导入对话框 -->
<upload-excel
ref="practicePlanUploadRef"
:title="'导入顶岗计划'"
:url="'/basic/basicpracticeclassplan/import'"
:temp-url="'/basic/basicpracticeclassplan/importTemplate'"
@refreshDataList="getDataList"
/>
</div>
</template>
@@ -210,6 +220,7 @@ const StatusTag = defineStatusTag(() => import('/@/components/StatusTag/index.vu
// 引入组件
const FormDialog = defineAsyncComponent(() => import('./form.vue'));
const DetailDialog = defineAsyncComponent(() => import('./detail.vue'));
const UploadExcel = defineAsyncComponent(() => import('/@/components/Upload/Excel.vue'));
// 定义变量内容
const route = useRoute();
@@ -217,6 +228,7 @@ const formDialogRef = ref();
const detailDialogRef = ref();
const searchFormRef = ref();
const columnControlRef = ref();
const practicePlanUploadRef = ref();
// 搜索变量
const showSearch = ref(true);
const deptList = ref<any[]>([]);
@@ -428,6 +440,11 @@ const getRuleListData = async () => {
}
};
// 顶岗计划导入
const handlePracticePlanImport = () => {
practicePlanUploadRef.value.show();
};
// 初始化
onMounted(() => {
getDeptListData();

View File

@@ -0,0 +1,217 @@
<template>
<el-dialog :title="dialogTitle" v-model="visible" :close-on-click-modal="false" draggable width="600px" @close="handleClose">
<el-form :model="form" :rules="rules" ref="formRef" label-width="120px" v-loading="loading">
<el-form-item label="班级" prop="classCode">
<el-select
v-model="form.classCode"
placeholder="请选择班级"
filterable
clearable
style="width: 100%"
@change="handleClassChange"
>
<el-option v-for="item in classList" :key="item.classCode" :label="`${item.classNo} - ${item.className || ''}`" :value="item.classCode">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="顶岗开始时间" prop="practiceStartDate">
<el-date-picker
v-model="form.practiceStartDate"
type="date"
placeholder="请选择顶岗开始时间"
style="width: 100%"
value-format="YYYY-MM-DD"
/>
</el-form-item>
<el-form-item label="顶岗结束时间" prop="practiceEndDate">
<el-date-picker
v-model="form.practiceEndDate"
type="date"
placeholder="请选择顶岗结束时间"
style="width: 100%"
value-format="YYYY-MM-DD"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="form.status" placeholder="请选择状态" style="width: 100%">
<el-option label="待执行" value="0" />
<el-option label="执行中" value="1" />
<el-option label="已完成" value="2" />
<el-option label="已取消" value="3" />
</el-select>
</el-form-item>
<el-form-item label="是否需要提醒" prop="needRemind">
<el-radio-group v-model="form.needRemind">
<el-radio label="1"></el-radio>
<el-radio label="0"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="提醒提前天数" prop="remindDays" v-if="form.needRemind === '1'">
<el-input-number v-model="form.remindDays" :min="1" :max="30" placeholder="请输入提醒天数" style="width: 100%" />
</el-form-item>
<el-form-item label="备注" prop="remarks">
<el-input v-model="form.remarks" type="textarea" :rows="3" placeholder="请输入备注" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false"> </el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitLoading"> </el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="BasicPracticeClassPlanForm">
import { ref, reactive, computed } from 'vue';
import { getObj, addObj, putObj } from '/@/api/basic/basicpracticeclassplan';
import { getClassListByRole } from '/@/api/basic/basicclass';
import { useMessage } from '/@/hooks/message';
// 定义变量内容
const visible = ref(false);
const loading = ref(false);
const submitLoading = ref(false);
const formRef = ref();
const classList = ref<any[]>([]);
const form = reactive({
id: '',
classCode: '',
classNo: '',
className: '',
deptCode: '',
deptName: '',
teacherNo: '',
teacherRealName: '',
practiceStartDate: '',
practiceEndDate: '',
status: '0',
remarks: '',
needRemind: '0',
remindDays: 7,
});
const rules = {
classCode: [{ required: true, message: '请选择班级', trigger: 'change' }],
practiceStartDate: [{ required: true, message: '请选择顶岗开始时间', trigger: 'change' }],
practiceEndDate: [{ required: true, message: '请选择顶岗结束时间', trigger: 'change' }],
status: [{ required: true, message: '请选择状态', trigger: 'change' }],
};
// 对话框标题
const dialogTitle = computed(() => {
return form.id ? '编辑顶岗班级计划' : '新增顶岗班级计划';
});
// 打开弹窗
const openDialog = async (id?: string) => {
visible.value = true;
resetForm();
await getClassListData();
if (id) {
loading.value = true;
try {
const res = await getObj(id);
if (res.data) {
Object.assign(form, res.data);
}
} catch (err: any) {
useMessage().error(err.msg || '获取详情失败');
} finally {
loading.value = false;
}
}
};
// 重置表单
const resetForm = () => {
Object.assign(form, {
id: '',
classCode: '',
classNo: '',
className: '',
deptCode: '',
deptName: '',
teacherNo: '',
teacherRealName: '',
practiceStartDate: '',
practiceEndDate: '',
status: '0',
remarks: '',
needRemind: '0',
remindDays: 7,
});
formRef.value?.resetFields();
};
// 班级选择变化
const handleClassChange = (classCode: string) => {
const selectedClass = classList.value.find((item) => item.classCode === classCode);
if (selectedClass) {
form.classNo = selectedClass.classNo || '';
form.className = selectedClass.className || '';
form.deptCode = selectedClass.deptCode || '';
form.deptName = selectedClass.deptName || '';
form.teacherNo = selectedClass.teacherNo || '';
form.teacherRealName = selectedClass.teacherRealName || '';
}
};
// 获取班级列表
const getClassListData = async () => {
try {
const res = await getClassListByRole();
if (res.data) {
classList.value = Array.isArray(res.data) ? res.data : [];
}
} catch (err) {
console.error('获取班级列表失败', err);
classList.value = [];
}
};
// 提交表单
const handleSubmit = async () => {
try {
await formRef.value?.validate();
submitLoading.value = true;
const submitData = { ...form };
if (submitData.needRemind !== '1') {
submitData.remindDays = null;
}
if (submitData.id) {
await putObj(submitData);
useMessage().success('修改成功');
} else {
await addObj(submitData);
useMessage().success('新增成功');
}
visible.value = false;
emit('refresh');
} catch (err: any) {
if (err !== false) {
useMessage().error(err.msg || '操作失败');
}
} finally {
submitLoading.value = false;
}
};
// 关闭弹窗
const handleClose = () => {
resetForm();
};
// 定义事件
const emit = defineEmits(['refresh']);
// 暴露方法
defineExpose({
openDialog,
});
</script>

View File

@@ -0,0 +1,355 @@
<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="searchForm" ref="searchFormRef" :inline="true" @keyup.enter="handleSearch" class="search-form">
<el-form-item label="学院" prop="deptCode">
<el-select v-model="searchForm.deptCode" placeholder="请选择学院" clearable filterable style="width: 200px">
<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="classNo">
<el-input v-model="searchForm.classNo" placeholder="请输入班号" clearable style="width: 200px" />
</el-form-item>
<el-form-item label="班主任" prop="teacherRealName">
<el-input v-model="searchForm.teacherRealName" placeholder="请输入班主任姓名" clearable style="width: 200px" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable style="width: 200px">
<el-option label="待执行" value="0" />
<el-option label="执行中" value="1" />
<el-option label="已完成" value="2" />
<el-option label="已取消" value="3" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" plain icon="Search" @click="handleSearch">查询</el-button>
<el-button icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 操作按钮 -->
<el-row>
<div class="mb8" style="width: 100%">
<el-button icon="FolderAdd" type="primary" class="ml10" @click="formDialogRef.openDialog()"> 新增 </el-button>
<el-button icon="Upload" type="success" class="ml10" @click="handleImport"> 导入 </el-button>
<right-toolbar v-model:showSearch="showSearch" class="ml10 mr20" style="float: right" @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>
</el-row>
<!-- 表格 -->
<el-table
:data="state.dataList"
v-loading="state.loading"
border
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
@sort-change="sortChangeHandle"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column type="index" label="序号" align="center" width="70">
<template #header>
<el-icon><List /></el-icon>
<span style="margin-left: 4px">序号</span>
</template>
<template #default="{ $index }">
{{ $index + 1 + ((state.pagination?.current || 1) - 1) * (state.pagination?.size || 10) }}
</template>
</el-table-column>
<el-table-column
v-for="col in visibleColumnsSorted"
:key="col.prop"
:prop="col.prop"
:label="col.label"
:width="col.width"
:min-width="col.minWidth"
:show-overflow-tooltip="col.showOverflowTooltip !== false"
:align="col.align"
>
<template #header>
<el-icon v-if="col.icon"><component :is="col.icon" /></el-icon>
<span :style="{ marginLeft: col.icon ? '4px' : '0' }">{{ col.label }}</span>
</template>
<template #default="scope" v-if="col.prop === 'status'">
<el-tag :type="getStatusType(scope.row.status)" size="small" effect="plain">
{{ getStatusLabel(scope.row.status) }}
</el-tag>
</template>
<template #default="scope" v-else-if="col.prop === 'needRemind'">
<el-tag :type="scope.row.needRemind === '1' ? 'success' : 'info'" size="small" effect="plain">
{{ scope.row.needRemind === '1' ? '是' : '否' }}
</el-tag>
</template>
<template #default="scope" v-else-if="col.prop === 'practiceStartDate' || col.prop === 'practiceEndDate'">
{{ scope.row[col.prop] || '-' }}
</template>
</el-table-column>
<el-table-column label="操作" align="center" fixed="right" width="200">
<template #header>
<el-icon><Setting /></el-icon>
<span style="margin-left: 4px">操作</span>
</template>
<template #default="scope">
<el-button icon="Edit" text type="primary" @click="formDialogRef.openDialog(scope.row.id)"> 编辑 </el-button>
<el-button icon="Delete" text type="danger" @click="handleDelete([scope.row.id])"> 删除 </el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
</div>
<!-- 编辑新增 -->
<FormDialog ref="formDialogRef" @refresh="getDataList(false)" />
<!-- 导入对话框 -->
<upload-excel
ref="uploadRef"
:title="'导入顶岗班级计划'"
:url="'/basic/basicpracticeclassplan/import'"
:temp-url="'/basic/basicpracticeclassplan/importTemplate'"
@refreshDataList="getDataList"
/>
</div>
</template>
<script setup lang="ts" name="BasicPracticeClassPlan">
import { ref, reactive, defineAsyncComponent, computed, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import {
List,
OfficeBuilding,
Grid,
Document,
UserFilled,
Calendar,
CircleCheck,
Bell,
Setting,
Menu,
Search,
} from '@element-plus/icons-vue';
import { BasicTableProps, useTable } from '/@/hooks/table';
import { fetchList, delObj } from '/@/api/basic/basicpracticeclassplan';
import { getDeptList } from '/@/api/basic/basicclass';
import { useMessage, useMessageBox } from '/@/hooks/message';
import TableColumnControl from '/@/components/TableColumnControl/index.vue';
// 引入组件
const FormDialog = defineAsyncComponent(() => import('./form.vue'));
const UploadExcel = defineAsyncComponent(() => import('/@/components/Upload/Excel.vue'));
// 定义变量内容
const route = useRoute();
const formDialogRef = ref();
const searchFormRef = ref();
const uploadRef = ref();
const columnControlRef = ref();
const showSearch = ref(true);
const deptList = ref<any[]>([]);
const selectedRows = ref<any[]>([]);
// 表格列配置
const tableColumns = [
{ prop: 'deptName', label: '学院', icon: OfficeBuilding },
{ prop: 'classNo', label: '班号', icon: Grid },
{ prop: 'className', label: '班级名称', icon: Document },
{ prop: 'teacherRealName', label: '班主任', icon: UserFilled },
{ prop: 'practiceStartDate', label: '顶岗开始时间', icon: Calendar, width: 120 },
{ prop: 'practiceEndDate', label: '顶岗结束时间', icon: Calendar, width: 120 },
{ prop: 'status', label: '状态', icon: CircleCheck, width: 100 },
{ prop: 'needRemind', label: '是否提醒', icon: Bell, width: 100 },
{ prop: 'remindDays', label: '提醒天数', icon: Bell, width: 100 },
{ prop: 'remarks', label: '备注', icon: Document },
];
// 当前显示的列
const visibleColumns = ref<string[]>([]);
// 列排序顺序
const columnOrder = ref<string[]>([]);
// 搜索表单
const searchForm = reactive({
deptCode: '',
classNo: '',
teacherRealName: '',
status: '',
});
// 配置 useTable
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: searchForm,
pageList: fetchList,
props: {
item: 'records',
totalCount: 'total',
},
createdIsNeed: true,
});
// table hook
const { getDataList, currentChangeHandle, sizeChangeHandle, sortChangeHandle, tableStyle } = useTable(state);
// 排序后的表格列
const visibleColumnsSorted = computed(() => {
const columns = tableColumns.filter((col) => {
const key = col.prop || col.label;
return visibleColumns.value.includes(key);
});
if (columnOrder.value.length > 0) {
const orderedColumns: any[] = [];
const unorderedColumns: any[] = [];
columnOrder.value.forEach((key) => {
const col = columns.find((c) => (c.prop || c.label) === key);
if (col) {
orderedColumns.push(col);
}
});
columns.forEach((col) => {
const key = col.prop || col.label;
if (!columnOrder.value.includes(key)) {
unorderedColumns.push(col);
}
});
return [...orderedColumns, ...unorderedColumns];
}
return columns;
});
// 初始化列配置
const initColumns = () => {
visibleColumns.value = tableColumns.map((col) => col.prop || col.label);
columnOrder.value = tableColumns.map((col) => col.prop || col.label);
};
// 列变化处理
const handleColumnChange = (value: string[]) => {
visibleColumns.value = value;
};
// 列排序变化处理
const handleColumnOrderChange = (order: string[]) => {
columnOrder.value = order;
};
// 查询
const handleSearch = () => {
getDataList();
};
// 重置
const handleReset = () => {
searchFormRef.value?.resetFields();
Object.assign(searchForm, {
deptCode: '',
classNo: '',
teacherRealName: '',
status: '',
});
getDataList();
};
// 表格选择变化
const handleSelectionChange = (selection: any[]) => {
selectedRows.value = selection;
};
// 删除
const handleDelete = async (ids: string[]) => {
try {
await useMessageBox().confirm('确定要删除选中的记录吗?');
await delObj(ids);
useMessage().success('删除成功');
getDataList();
} catch (err: any) {
if (err !== 'cancel') {
useMessage().error(err.msg || '删除失败');
}
}
};
// 导入
const handleImport = () => {
uploadRef.value.show();
};
// 获取状态标签
const getStatusLabel = (status: string) => {
const statusMap: Record<string, string> = {
'0': '待执行',
'1': '执行中',
'2': '已完成',
'3': '已取消',
};
return statusMap[status] || '-';
};
// 获取状态类型
const getStatusType = (status: string) => {
const typeMap: Record<string, string> = {
'0': 'info',
'1': 'warning',
'2': 'success',
'3': 'danger',
};
return typeMap[status] || 'info';
};
// 获取学院列表
const getDeptListData = async () => {
try {
const res = await getDeptList();
if (res.data) {
deptList.value = Array.isArray(res.data) ? res.data : [];
}
} catch (err) {
console.error('获取学院列表失败', err);
deptList.value = [];
}
};
// 初始化
initColumns();
onMounted(() => {
getDeptListData();
});
</script>
<style scoped lang="scss">
@import '/@/assets/styles/modern-page.scss';
</style>

View File

@@ -0,0 +1,515 @@
<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="searchForm" ref="searchFormRef" :inline="true" @keyup.enter="handleSearch" class="search-form">
<el-form-item label="申请类型" prop="applyType">
<el-select
v-model="searchForm.applyType"
placeholder="请选择申请类型"
clearable
style="width: 200px">
<el-option label="任职" value="1" />
<el-option label="调换" value="2" />
</el-select>
</el-form-item>
<el-form-item label="申请状态" prop="status">
<el-select
v-model="searchForm.status"
placeholder="请选择状态"
clearable
style="width: 200px">
<el-option label="待审核" value="0" />
<el-option label="驳回" value="1" />
<el-option label="撤回" value="2" />
<el-option label="通过" value="3" />
</el-select>
</el-form-item>
<el-form-item label="班级" prop="classCode">
<el-select
v-model="searchForm.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>
<el-button type="primary" plain icon="Search" @click="handleSearch">查询</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="handleAdd">
新增申请
</el-button>
<right-toolbar
v-model:showSearch="showSearch"
class="ml10"
@queryTable="getDataList">
</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 #default="{ $index }">
{{ $index + 1 + ((state.pagination?.current || 1) - 1) * (state.pagination?.size || 10) }}
</template>
</el-table-column>
<el-table-column prop="applyType" label="申请类型" align="center" width="100">
<template #default="scope">
<el-tag :type="scope.row.applyType === '1' ? 'success' : 'warning'" size="small">
{{ scope.row.applyType === '1' ? '任职' : '调换' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="className" label="班级" align="center" show-overflow-tooltip>
<template #default="scope">
{{ scope.row.classNo }} - {{ scope.row.className }}
</template>
</el-table-column>
<el-table-column prop="fromTeacherName" label="原班主任" align="center" show-overflow-tooltip>
<template #default="scope">
{{ scope.row.fromTeacherNo }} - {{ scope.row.fromTeacherName || '-' }}
</template>
</el-table-column>
<el-table-column prop="toTeacherName" label="拟任班主任" align="center" show-overflow-tooltip>
<template #default="scope">
{{ scope.row.toTeacherNo }} - {{ scope.row.toTeacherName || '-' }}
</template>
</el-table-column>
<el-table-column prop="reason" label="申请原因" align="center" show-overflow-tooltip min-width="150" />
<el-table-column prop="effectTime" label="期望生效时间" align="center" width="160">
<template #default="scope">
{{ formatDateTime(scope.row.effectTime) }}
</template>
</el-table-column>
<el-table-column prop="status" label="状态" align="center" width="100">
<template #default="scope">
<el-tag :type="getStatusType(scope.row.status)" size="small">
{{ getStatusLabel(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="申请时间" align="center" width="160">
<template #default="scope">
{{ formatDateTime(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column label="操作" width="200" align="center" fixed="right">
<template #default="scope">
<el-button
v-if="scope.row.status === '0'"
icon="Check"
link
type="success"
@click="handleAudit(scope.row, '3')">
审批通过
</el-button>
<el-button
v-if="scope.row.status === '0'"
icon="Close"
link
type="danger"
@click="handleAudit(scope.row, '1')">
驳回
</el-button>
<el-button
v-if="scope.row.status === '0'"
icon="RefreshLeft"
link
type="warning"
@click="handleAudit(scope.row, '2')">
撤回
</el-button>
<el-button
v-if="scope.row.status === '0'"
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>
<!-- 新增申请弹窗 -->
<el-dialog
v-model="dialogVisible"
title="新增班主任调换申请"
width="600px"
:close-on-click-modal="false">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="120px">
<el-form-item label="申请类型" prop="applyType">
<el-radio-group v-model="formData.applyType">
<el-radio label="1">任职</el-radio>
<el-radio label="2">调换</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="班级" prop="classCode">
<el-select
v-model="formData.classCode"
placeholder="请选择班级"
filterable
style="width: 100%"
@change="handleClassChange">
<el-option
v-for="item in classList"
:key="item.classCode"
:label="`${item.classNo} - ${item.className}`"
:value="item.classCode">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="拟任班主任" prop="toTeacherNo">
<el-select
v-model="formData.toTeacherNo"
placeholder="请选择拟任班主任"
filterable
style="width: 100%">
<el-option
v-for="item in teacherList"
:key="item.teacherNo"
:label="`${item.teacherNo} - ${item.teacherName}`"
:value="item.teacherNo">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="申请原因" prop="reason">
<el-input v-model="formData.reason" type="textarea" :rows="3" placeholder="请输入申请原因" />
</el-form-item>
<el-form-item label="期望生效时间" prop="effectTime">
<el-date-picker
v-model="formData.effectTime"
type="datetime"
placeholder="请选择期望生效时间"
style="width: 100%"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">提交申请</el-button>
</template>
</el-dialog>
<!-- 审批意见弹窗 -->
<el-dialog
v-model="auditDialogVisible"
:title="auditDialogTitle"
width="500px"
:close-on-click-modal="false">
<el-form
ref="auditFormRef"
:model="auditFormData"
:rules="auditFormRules"
label-width="100px">
<el-form-item label="审批意见" prop="remark">
<el-input v-model="auditFormData.remark" type="textarea" :rows="3" placeholder="请输入审批意见" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="auditDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleAuditSubmit" :loading="auditLoading">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts" name="ClassMasterJobApply">
import { reactive, ref, onMounted, computed } from 'vue'
import { BasicTableProps, useTable } from "/@/hooks/table";
import { fetchList, addObj, auditObj, delObj } from "/@/api/stuwork/classmasterjobapply";
import { list as getClassList } from "/@/api/basic/basicclass";
import { queryAllTeacher } from "/@/api/professional/professionaluser/teacherbase";
import { useMessage, useMessageBox } from "/@/hooks/message";
import { Search, Document, Plus, Check, Close, RefreshLeft, Delete } from '@element-plus/icons-vue'
// 定义变量
const searchFormRef = ref()
const formRef = ref()
const auditFormRef = ref()
const showSearch = ref(true)
const dialogVisible = ref(false)
const auditDialogVisible = ref(false)
const submitLoading = ref(false)
const auditLoading = ref(false)
const classList = ref<any[]>([])
const teacherList = ref<any[]>([])
// 搜索表单
const searchForm = reactive({
applyType: '',
status: '',
classCode: ''
})
// 表单数据
const formData = reactive({
applyType: '2',
classCode: '',
toTeacherNo: '',
reason: '',
effectTime: ''
})
// 审批表单数据
const auditFormData = reactive({
id: '',
status: '',
remark: ''
})
// 表单验证规则
const formRules = {
applyType: [{ required: true, message: '请选择申请类型', trigger: 'change' }],
classCode: [{ required: true, message: '请选择班级', trigger: 'change' }],
toTeacherNo: [{ required: true, message: '请选择拟任班主任', trigger: 'change' }],
reason: [{ required: true, message: '请输入申请原因', trigger: 'blur' }]
}
// 审批表单验证规则
const auditFormRules = {
remark: [{ required: false, message: '请输入审批意见', trigger: 'blur' }]
}
// 审批弹窗标题
const auditDialogTitle = computed(() => {
if (auditFormData.status === '1') return '驳回申请'
if (auditFormData.status === '2') return '撤回申请'
return '审批通过'
})
// 配置 useTable
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: searchForm,
pageList: fetchList,
props: {
item: 'records',
totalCount: 'total'
},
createdIsNeed: true
})
// table hook
const {
getDataList,
currentChangeHandle,
sizeChangeHandle,
tableStyle
} = useTable(state)
// 获取状态类型
const getStatusType = (status: string) => {
const map: Record<string, string> = {
'0': 'warning',
'1': 'danger',
'2': 'info',
'3': 'success'
}
return map[status] || 'info'
}
// 获取状态标签
const getStatusLabel = (status: string) => {
const map: Record<string, string> = {
'0': '待审核',
'1': '驳回',
'2': '撤回',
'3': '通过'
}
return map[status] || status
}
// 格式化日期时间
const formatDateTime = (dateTime: string) => {
if (!dateTime) return '-'
return dateTime
}
// 查询
const handleSearch = () => {
getDataList()
}
// 重置
const handleReset = () => {
searchFormRef.value?.resetFields()
searchForm.applyType = ''
searchForm.status = ''
searchForm.classCode = ''
getDataList()
}
// 获取班级列表
const getClassListData = async () => {
try {
const res = await getClassList()
if (res.data) {
classList.value = Array.isArray(res.data) ? res.data : []
}
} catch (err) {
classList.value = []
}
}
// 获取教师列表
const getTeacherListData = async () => {
try {
const res = await queryAllTeacher()
if (res.data) {
teacherList.value = Array.isArray(res.data) ? res.data.map((item: any) => ({
teacherNo: item.teacherNo || item.teacherCode,
teacherName: item.teacherName || item.realName
})) : []
}
} catch (err) {
teacherList.value = []
}
}
// 班级选择变更
const handleClassChange = (classCode: string) => {
// 可以根据班级获取该班级的原班主任信息
// 这里暂时清空拟任班主任
formData.toTeacherNo = ''
// TODO: 获取可选教师列表
}
// 新增
const handleAdd = () => {
resetForm()
dialogVisible.value = true
}
// 重置表单
const resetForm = () => {
formData.applyType = '2'
formData.classCode = ''
formData.toTeacherNo = ''
formData.reason = ''
formData.effectTime = ''
formRef.value?.resetFields()
}
// 提交
const handleSubmit = async () => {
try {
await formRef.value?.validate()
submitLoading.value = true
await addObj(formData)
useMessage().success('提交成功')
dialogVisible.value = false
getDataList()
} catch (err: any) {
if (err?.msg) {
useMessage().error(err.msg)
}
} finally {
submitLoading.value = false
}
}
// 审批
const handleAudit = (row: any, status: string) => {
auditFormData.id = row.id
auditFormData.status = status
auditFormData.remark = ''
auditDialogVisible.value = true
}
// 审批提交
const handleAuditSubmit = async () => {
try {
await auditFormRef.value?.validate()
auditLoading.value = true
await auditObj(auditFormData)
useMessage().success('操作成功')
auditDialogVisible.value = false
getDataList()
} catch (err: any) {
if (err?.msg) {
useMessage().error(err.msg)
}
} finally {
auditLoading.value = false
}
}
// 删除
const handleDelete = async (row: any) => {
try {
await useMessageBox().confirm('确定要删除该申请吗?')
await delObj([row.id])
useMessage().success('删除成功')
getDataList()
} catch (err: any) {
if (err !== 'cancel') {
useMessage().error(err.msg || '删除失败')
}
}
}
// 初始化
onMounted(() => {
getClassListData()
getTeacherListData()
})
</script>
<style scoped lang="scss">
@import '/@/assets/styles/modern-page.scss';
</style>

View File

@@ -0,0 +1,117 @@
<template>
<el-dialog :title="form.id ? '编辑' : '新增'" v-model="visible" :width="600" :close-on-click-modal="false" draggable>
<el-form ref="dataFormRef" :model="form" :rules="dataRules" label-width="100px" v-loading="loading">
<el-form-item label="食堂名称" prop="diningHallName">
<el-input v-model="form.diningHallName" placeholder="请输入食堂名称" />
</el-form-item>
<el-form-item label="食堂位置" prop="diningHallPlace">
<el-input v-model="form.diningHallPlace" placeholder="请输入食堂位置" />
</el-form-item>
<el-form-item label="学年" prop="year">
<el-input v-model="form.year" placeholder="请输入学年2024-2025" />
</el-form-item>
<el-form-item label="学期" prop="period">
<el-select v-model="form.period" placeholder="请选择学期" style="width: 100%">
<el-option label="第一学期" value="1" />
<el-option label="第二学期" value="2" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false"> </el-button>
<el-button type="primary" @click="onSubmit" :disabled="loading"> </el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="DiningHallFormDialog">
import { ref, reactive, nextTick } from 'vue';
import { useMessage } from '/@/hooks/message';
import { addObj, putObj } from '/@/api/stuwork/dininghall';
const emit = defineEmits(['refresh']);
const dataFormRef = ref();
const visible = ref(false);
const loading = ref(false);
const operType = ref('add');
const form = reactive({
id: '',
diningHallName: '',
diningHallPlace: '',
year: '',
period: '',
});
const dataRules = {
diningHallName: [{ required: true, message: '请输入食堂名称', trigger: 'blur' }],
diningHallPlace: [{ required: true, message: '请输入食堂位置', trigger: 'blur' }],
year: [{ required: true, message: '请输入学年', trigger: 'blur' }],
period: [{ required: true, message: '请选择学期', trigger: 'change' }],
};
const openDialog = (type: string = 'add', row?: any) => {
visible.value = true;
operType.value = type;
nextTick(() => {
dataFormRef.value?.resetFields();
form.id = '';
form.diningHallName = '';
form.diningHallPlace = '';
form.year = '';
form.period = '';
if (type === 'edit' && row) {
form.id = row.id;
form.diningHallName = row.diningHallName || '';
form.diningHallPlace = row.diningHallPlace || '';
form.year = row.year || '';
form.period = row.period || '';
}
});
};
const onSubmit = async () => {
if (!dataFormRef.value) return;
await dataFormRef.value.validate(async (valid: boolean) => {
if (!valid) return;
loading.value = true;
try {
if (operType.value === 'add') {
await addObj({
diningHallName: form.diningHallName,
diningHallPlace: form.diningHallPlace,
year: form.year,
period: form.period,
});
useMessage().success('新增成功');
} else {
await putObj({
id: form.id,
diningHallName: form.diningHallName,
diningHallPlace: form.diningHallPlace,
year: form.year,
period: form.period,
});
useMessage().success('编辑成功');
}
visible.value = false;
emit('refresh');
} catch (err: any) {
useMessage().error(err.msg || (operType.value === 'add' ? '新增失败' : '编辑失败'));
} finally {
loading.value = false;
}
});
};
defineExpose({
openDialog,
});
</script>

View File

@@ -0,0 +1,159 @@
<template>
<div class="modern-page-container">
<div class="page-wrapper">
<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="FolderAdd" type="primary" @click="formDialogRef.openDialog()"> </el-button>
<right-toolbar 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-form :model="searchForm" inline class="search-form">
<el-form-item label="食堂名称">
<el-input v-model="searchForm.diningHallName" placeholder="请输入食堂名称" clearable style="width: 200px" />
</el-form-item>
<el-form-item label="学年">
<el-input v-model="searchForm.year" placeholder="请输入学年" clearable style="width: 150px" />
</el-form-item>
<el-form-item>
<el-button icon="Search" type="primary" @click="getDataList(true)"> </el-button>
<el-button icon="Refresh" @click="resetSearch"> </el-button>
</el-form-item>
</el-form>
<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 #default="{ $index }">
{{ $index + 1 }}
</template>
</el-table-column>
<el-table-column prop="diningHallName" label="食堂名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="diningHallPlace" label="食堂位置" min-width="150" show-overflow-tooltip />
<el-table-column prop="year" label="学年" width="120" align="center" />
<el-table-column prop="period" label="学期" width="100" align="center">
<template #default="{ row }">
{{ row.period === '1' ? '第一学期' : row.period === '2' ? '第二学期' : '-' }}
</template>
</el-table-column>
<el-table-column label="操作" width="150" align="center" fixed="right">
<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>
<template #empty>
<el-empty description="暂无数据" :image-size="120" />
</template>
</el-table>
<div class="pagination-wrapper">
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
</div>
</el-card>
</div>
<FormDialog ref="formDialogRef" @refresh="getDataList(false)" />
</div>
</template>
<script setup lang="ts" name="DiningHall">
import { ref, reactive, defineAsyncComponent } from 'vue';
import { BasicTableProps, useTable } from '/@/hooks/table';
import { fetchList, delObjs } from '/@/api/stuwork/dininghall';
import { useMessage, useMessageBox } from '/@/hooks/message';
import TableColumnControl from '/@/components/TableColumnControl/index.vue';
import { Document, Menu } from '@element-plus/icons-vue';
import { useTableColumnControl } from '/@/hooks/tableColumn';
const FormDialog = defineAsyncComponent(() => import('./form.vue'));
const formDialogRef = ref();
const columnControlRef = ref<any>();
const tableColumns = [
{ prop: 'diningHallName', label: '食堂名称' },
{ prop: 'diningHallPlace', label: '食堂位置' },
{ prop: 'year', label: '学年' },
{ prop: 'period', label: '学期' },
];
const { visibleColumns, visibleColumnsSorted, checkColumnVisible, handleColumnChange, handleColumnOrderChange } = useTableColumnControl(tableColumns);
const searchForm = reactive({
diningHallName: '',
year: '',
});
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: searchForm,
pageList: fetchList,
props: {
item: 'records',
totalCount: 'total',
},
});
const { getDataList, currentChangeHandle, sizeChangeHandle, tableStyle } = useTable(state);
const resetSearch = () => {
searchForm.diningHallName = '';
searchForm.year = '';
getDataList(true);
};
const handleEdit = (row: any) => {
if (formDialogRef.value) {
formDialogRef.value.openDialog('edit', row);
}
};
const handleDelete = async (row: any) => {
try {
await useMessageBox().confirm('确定要删除该食堂吗?');
await delObjs([row.id]);
useMessage().success('删除成功');
getDataList();
} catch (err: any) {
if (err !== 'cancel') {
useMessage().error(err.msg || '删除失败');
}
}
};
</script>
<style scoped lang="scss">
@import '/@/assets/styles/modern-page.scss';
</style>

View File

@@ -0,0 +1,158 @@
<template>
<el-dialog :title="form.id ? '编辑' : '新增'" v-model="visible" :width="600" :close-on-click-modal="false" draggable>
<el-form ref="dataFormRef" :model="form" :rules="dataRules" label-width="100px" v-loading="loading">
<el-form-item label="食堂" prop="diningHallId">
<el-select v-model="form.diningHallId" placeholder="请选择食堂" style="width: 100%">
<el-option v-for="item in diningHallList" :key="item.id" :label="item.diningHallName" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="题目名称" prop="voteTitle">
<el-input v-model="form.voteTitle" placeholder="请输入题目名称" />
</el-form-item>
<el-form-item label="题目类型" prop="voteProjectId">
<el-select v-model="form.voteProjectId" placeholder="请选择题目类型" style="width: 100%">
<el-option v-for="item in questionnaireList" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="学年" prop="year">
<el-input v-model="form.year" placeholder="请输入学年2024-2025" />
</el-form-item>
<el-form-item label="学期" prop="period">
<el-select v-model="form.period" placeholder="请选择学期" style="width: 100%">
<el-option label="第一学期" value="1" />
<el-option label="第二学期" value="2" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false"> </el-button>
<el-button type="primary" @click="onSubmit" :disabled="loading"> </el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="DiningHallVoteFormDialog">
import { ref, reactive, nextTick } from 'vue';
import { useMessage } from '/@/hooks/message';
import { addObj, putObj, getQuestionnaireDict } from '/@/api/stuwork/dininghallvote';
import { getList as getDiningHallList } from '/@/api/stuwork/dininghall';
const props = defineProps<{
diningHallList?: any[];
}>();
const emit = defineEmits(['refresh']);
const dataFormRef = ref();
const visible = ref(false);
const loading = ref(false);
const operType = ref('add');
const diningHallList = ref<any[]>([]);
const questionnaireList = ref<any[]>([]);
const form = reactive({
id: '',
diningHallId: '',
voteTitle: '',
voteProjectId: '',
year: '',
period: '',
});
const dataRules = {
diningHallId: [{ required: true, message: '请选择食堂', trigger: 'change' }],
voteTitle: [{ required: true, message: '请输入题目名称', trigger: 'blur' }],
voteProjectId: [{ required: true, message: '请选择题目类型', trigger: 'change' }],
year: [{ required: true, message: '请输入学年', trigger: 'blur' }],
period: [{ required: true, message: '请选择学期', trigger: 'change' }],
};
const loadDiningHallList = async () => {
try {
const res = await getDiningHallList();
diningHallList.value = res.data || [];
} catch (error) {
console.error('加载食堂列表失败', error);
}
};
const loadQuestionnaireDict = async () => {
try {
const res = await getQuestionnaireDict();
questionnaireList.value = res.data || [];
} catch (error) {
console.error('加载题目类型失败', error);
}
};
const openDialog = (type: string = 'add', row?: any) => {
visible.value = true;
operType.value = type;
nextTick(() => {
dataFormRef.value?.resetFields();
form.id = '';
form.diningHallId = '';
form.voteTitle = '';
form.voteProjectId = '';
form.year = '';
form.period = '';
if (type === 'edit' && row) {
form.id = row.id;
form.diningHallId = row.diningHallId || '';
form.voteTitle = row.voteTitle || '';
form.voteProjectId = row.voteProjectId || '';
form.year = row.year || '';
form.period = row.period || '';
}
});
loadDiningHallList();
loadQuestionnaireDict();
};
const onSubmit = async () => {
if (!dataFormRef.value) return;
await dataFormRef.value.validate(async (valid: boolean) => {
if (!valid) return;
loading.value = true;
try {
if (operType.value === 'add') {
await addObj({
diningHallId: form.diningHallId,
voteTitle: form.voteTitle,
voteProjectId: form.voteProjectId,
year: form.year,
period: form.period,
});
useMessage().success('新增成功');
} else {
await putObj({
id: form.id,
diningHallId: form.diningHallId,
voteTitle: form.voteTitle,
voteProjectId: form.voteProjectId,
year: form.year,
period: form.period,
});
useMessage().success('编辑成功');
}
visible.value = false;
emit('refresh');
} catch (err: any) {
useMessage().error(err.msg || (operType.value === 'add' ? '新增失败' : '编辑失败'));
} finally {
loading.value = false;
}
});
};
defineExpose({
openDialog,
});
</script>

View File

@@ -0,0 +1,173 @@
<template>
<div class="modern-page-container">
<div class="page-wrapper">
<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="FolderAdd" type="primary" @click="formDialogRef.openDialog()"> </el-button>
<right-toolbar 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-form :model="searchForm" inline class="search-form">
<el-form-item label="食堂">
<el-select v-model="searchForm.diningHallId" placeholder="请选择食堂" clearable style="width: 200px">
<el-option v-for="item in diningHallList" :key="item.id" :label="item.diningHallName" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item>
<el-button icon="Search" type="primary" @click="getDataList(true)"> </el-button>
<el-button icon="Refresh" @click="resetSearch"> </el-button>
</el-form-item>
</el-form>
<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 #default="{ $index }">
{{ $index + 1 }}
</template>
</el-table-column>
<el-table-column prop="voteTitle" label="题目名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="diningHallName" label="所属食堂" min-width="150" show-overflow-tooltip />
<el-table-column prop="year" label="学年" width="120" align="center" />
<el-table-column prop="period" label="学期" width="100" align="center">
<template #default="{ row }">
{{ row.period === '1' ? '第一学期' : row.period === '2' ? '第二学期' : '-' }}
</template>
</el-table-column>
<el-table-column prop="voteProjectName" label="题目类型" width="150" align="center" />
<el-table-column label="操作" width="150" align="center" fixed="right">
<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>
<template #empty>
<el-empty description="暂无数据" :image-size="120" />
</template>
</el-table>
<div class="pagination-wrapper">
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
</div>
</el-card>
</div>
<FormDialog ref="formDialogRef" @refresh="getDataList(false)" />
</div>
</template>
<script setup lang="ts" name="DiningHallVote">
import { ref, reactive, defineAsyncComponent, onMounted } from 'vue';
import { BasicTableProps, useTable } from '/@/hooks/table';
import { fetchList, delObjs } from '/@/api/stuwork/dininghallvote';
import { getList as getDiningHallList } from '/@/api/stuwork/dininghall';
import { useMessage, useMessageBox } from '/@/hooks/message';
import TableColumnControl from '/@/components/TableColumnControl/index.vue';
import { Document, Menu } from '@element-plus/icons-vue';
import { useTableColumnControl } from '/@/hooks/tableColumn';
const FormDialog = defineAsyncComponent(() => import('./form.vue'));
const formDialogRef = ref();
const columnControlRef = ref<any>();
const diningHallList = ref<any[]>([]);
const tableColumns = [
{ prop: 'voteTitle', label: '题目名称' },
{ prop: 'diningHallName', label: '所属食堂' },
{ prop: 'year', label: '学年' },
{ prop: 'period', label: '学期' },
{ prop: 'voteProjectName', label: '题目类型' },
];
const { visibleColumns, visibleColumnsSorted, checkColumnVisible, handleColumnChange, handleColumnOrderChange } = useTableColumnControl(tableColumns);
const searchForm = reactive({
diningHallId: '',
});
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: searchForm,
pageList: fetchList,
props: {
item: 'records',
totalCount: 'total',
},
});
const { getDataList, currentChangeHandle, sizeChangeHandle, tableStyle } = useTable(state);
const resetSearch = () => {
searchForm.diningHallId = '';
getDataList(true);
};
const handleEdit = (row: any) => {
if (formDialogRef.value) {
formDialogRef.value.openDialog('edit', row);
}
};
const handleDelete = async (row: any) => {
try {
await useMessageBox().confirm('确定要删除该题目吗?');
await delObjs([row.id]);
useMessage().success('删除成功');
getDataList();
} catch (err: any) {
if (err !== 'cancel') {
useMessage().error(err.msg || '删除失败');
}
}
};
const loadDiningHallList = async () => {
try {
const res = await getDiningHallList();
diningHallList.value = res.data || [];
} catch (error) {
console.error('加载食堂列表失败', error);
}
};
onMounted(() => {
loadDiningHallList();
});
</script>
<style scoped lang="scss">
@import '/@/assets/styles/modern-page.scss';
</style>

View File

@@ -0,0 +1,236 @@
<template>
<div class="modern-page-container">
<div class="page-wrapper">
<el-card class="content-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><DataAnalysis /></el-icon>
食堂调查完成率统计
</span>
</div>
</template>
<!-- 标签页切换 -->
<el-tabs v-model="activeTab" @tab-change="handleTabChange">
<el-tab-pane label="学生统计" name="student">
<!-- 搜索表单 -->
<el-form :model="searchForm" inline class="search-form">
<el-form-item label="学院">
<el-select v-model="searchForm.deptCode" placeholder="请选择学院" clearable style="width: 200px">
<el-option v-for="item in deptList" :key="item.deptCode" :label="item.deptName" :value="item.deptCode" />
</el-select>
</el-form-item>
<el-form-item>
<el-button icon="Search" type="primary" @click="getStudentData(true)"> </el-button>
<el-button icon="Refresh" @click="resetStudentSearch"> </el-button>
</el-form-item>
</el-form>
<el-table :data="studentData" v-loading="studentLoading" stripe class="modern-table">
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column prop="deptName" label="学院" min-width="150" show-overflow-tooltip />
<el-table-column prop="className" label="班级" min-width="150" show-overflow-tooltip />
<el-table-column prop="classMaster" label="班主任" width="100" align="center" />
<el-table-column prop="allNum" label="总人数" width="100" align="center">
<template #default="{ row }">
<el-tag type="info">{{ row.allNum }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="completed" label="已完成" width="100" align="center">
<template #default="{ row }">
<el-tag type="success">{{ row.completed }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="noCompleted" label="未完成" width="100" align="center">
<template #default="{ row }">
<el-tag type="danger">{{ row.noCompleted }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="completionRate" label="完成率" width="120" align="center">
<template #default="{ row }">
<el-progress :percentage="parseFloat(row.completionRate) || 0" :stroke-width="15" :text-inside="true" />
</template>
</el-table-column>
<template #empty>
<el-empty description="暂无数据" :image-size="120" />
</template>
</el-table>
<div class="pagination-wrapper">
<pagination @size-change="studentSizeChange" @current-change="studentCurrentChange" v-bind="studentPagination" />
</div>
</el-tab-pane>
<el-tab-pane label="教职工统计" name="teacher">
<!-- 搜索表单 -->
<el-form :model="teacherSearchForm" inline class="search-form">
<el-form-item label="学院">
<el-select v-model="teacherSearchForm.deptCode" placeholder="请选择学院" clearable style="width: 200px">
<el-option v-for="item in deptList" :key="item.deptCode" :label="item.deptName" :value="item.deptCode" />
</el-select>
</el-form-item>
<el-form-item>
<el-button icon="Search" type="primary" @click="getTeacherData(true)"> </el-button>
<el-button icon="Refresh" @click="resetTeacherSearch"> </el-button>
</el-form-item>
</el-form>
<el-table :data="teacherData" v-loading="teacherLoading" stripe class="modern-table">
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column prop="deptName" label="学院" min-width="150" show-overflow-tooltip />
<el-table-column prop="realName" label="姓名" width="100" align="center" />
<el-table-column prop="loginName" label="账号" width="120" align="center" />
<el-table-column prop="allNum" label="总人数" width="100" align="center">
<template #default="{ row }">
<el-tag type="info">{{ row.allNum }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="completed" label="已完成" width="100" align="center">
<template #default="{ row }">
<el-tag type="success">{{ row.completed }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="noCompleted" label="未完成" width="100" align="center">
<template #default="{ row }">
<el-tag type="danger">{{ row.noCompleted }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="completionRate" label="完成率" width="120" align="center">
<template #default="{ row }">
<el-progress :percentage="parseFloat(row.completionRate) || 0" :stroke-width="15" :text-inside="true" />
</template>
</el-table-column>
<template #empty>
<el-empty description="暂无数据" :image-size="120" />
</template>
</el-table>
<div class="pagination-wrapper">
<pagination @size-change="teacherSizeChange" @current-change="teacherCurrentChange" v-bind="teacherPagination" />
</div>
</el-tab-pane>
</el-tabs>
</el-card>
</div>
</div>
</template>
<script setup lang="ts" name="DiningHallVoteStatistics">
import { ref, reactive, onMounted } from 'vue';
import { getStatisticsList, getStatisticsListByTea } from '/@/api/stuwork/dininghallvoteresultanalysis';
import { DataAnalysis } from '@element-plus/icons-vue';
const activeTab = ref('student');
const deptList = ref<any[]>([]);
// 学生统计
const searchForm = reactive({
deptCode: '',
});
const studentData = ref<any[]>([]);
const studentLoading = ref(false);
const studentPagination = reactive({
current: 1,
size: 10,
total: 0,
});
// 教职工统计
const teacherSearchForm = reactive({
deptCode: '',
});
const teacherData = ref<any[]>([]);
const teacherLoading = ref(false);
const teacherPagination = reactive({
current: 1,
size: 10,
total: 0,
});
const getStudentData = async (resetPage = false) => {
if (resetPage) {
studentPagination.current = 1;
}
studentLoading.value = true;
try {
const res = await getStatisticsList({
current: studentPagination.current,
size: studentPagination.size,
deptCode: searchForm.deptCode,
});
studentData.value = res.data?.records || [];
studentPagination.total = res.data?.total || 0;
} catch (error) {
console.error('获取学生统计数据失败', error);
} finally {
studentLoading.value = false;
}
};
const resetStudentSearch = () => {
searchForm.deptCode = '';
getStudentData(true);
};
const studentSizeChange = (size: number) => {
studentPagination.size = size;
getStudentData();
};
const studentCurrentChange = (current: number) => {
studentPagination.current = current;
getStudentData();
};
const getTeacherData = async (resetPage = false) => {
if (resetPage) {
teacherPagination.current = 1;
}
teacherLoading.value = true;
try {
const res = await getStatisticsListByTea({
current: teacherPagination.current,
size: teacherPagination.size,
deptCode: teacherSearchForm.deptCode,
});
teacherData.value = res.data || [];
teacherPagination.total = res.data?.length || 0;
} catch (error) {
console.error('获取教职工统计数据失败', error);
} finally {
teacherLoading.value = false;
}
};
const resetTeacherSearch = () => {
teacherSearchForm.deptCode = '';
getTeacherData(true);
};
const teacherSizeChange = (size: number) => {
teacherPagination.size = size;
getTeacherData();
};
const teacherCurrentChange = (current: number) => {
teacherPagination.current = current;
getTeacherData();
};
const handleTabChange = (tab: string) => {
if (tab === 'student' && studentData.value.length === 0) {
getStudentData();
} else if (tab === 'teacher' && teacherData.value.length === 0) {
getTeacherData();
}
};
onMounted(() => {
getStudentData();
});
</script>
<style scoped lang="scss">
@import '/@/assets/styles/modern-page.scss';
</style>

View File

@@ -0,0 +1,164 @@
<template>
<div class="modern-page-container">
<div class="page-wrapper">
<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">
<right-toolbar 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-form :model="searchForm" inline class="search-form">
<el-form-item label="学年">
<el-input v-model="searchForm.year" placeholder="请输入学年" clearable style="width: 150px" />
</el-form-item>
<el-form-item label="学期">
<el-select v-model="searchForm.period" placeholder="请选择学期" clearable style="width: 120px">
<el-option label="第一学期" value="1" />
<el-option label="第二学期" value="2" />
</el-select>
</el-form-item>
<el-form-item>
<el-button icon="Search" type="primary" @click="getDataList(true)"> </el-button>
<el-button icon="Refresh" @click="resetSearch"> </el-button>
</el-form-item>
</el-form>
<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 #default="{ $index }">
{{ $index + 1 }}
</template>
</el-table-column>
<el-table-column prop="loginName" label="填写人账号" width="120" align="center" />
<el-table-column prop="year" label="学年" width="120" align="center" />
<el-table-column prop="period" label="学期" width="100" align="center">
<template #default="{ row }">
{{ row.period === '1' ? '第一学期' : row.period === '2' ? '第二学期' : '-' }}
</template>
</el-table-column>
<el-table-column prop="diningHallName" label="食堂名称" min-width="150" show-overflow-tooltip />
<el-table-column prop="diningHallVoteScore" label="评分" width="80" align="center">
<template #default="{ row }">
<el-tag type="success">{{ row.diningHallVoteScore }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="isUnderstand" label="是否了解" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.isUnderstand === 1 ? 'success' : 'info'">
{{ row.isUnderstand === 1 ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="isStu" label="身份" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.isStu === 1 ? 'primary' : 'warning'">
{{ row.isStu === 1 ? '学生' : '教职工' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="mostDissatisfied" label="最不满意食堂" min-width="150" show-overflow-tooltip />
<el-table-column prop="mostVist" label="最常去食堂" min-width="150" show-overflow-tooltip />
<el-table-column prop="mostDissatisfiedLayer" label="最不满意楼层" width="120" align="center" />
<el-table-column prop="mostDissatisfiedWindow" label="最不满意窗口" min-width="150" show-overflow-tooltip />
<el-table-column prop="mostVisitLayer" label="最常去楼层" width="120" align="center" />
<el-table-column prop="createDate" label="填写时间" width="160" align="center" />
<template #empty>
<el-empty description="暂无数据" :image-size="120" />
</template>
</el-table>
<div class="pagination-wrapper">
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
</div>
</el-card>
</div>
</div>
</template>
<script setup lang="ts" name="DiningHallVoteResult">
import { ref, reactive } from 'vue';
import { BasicTableProps, useTable } from '/@/hooks/table';
import { fetchList } from '/@/api/stuwork/dininghallvoteresult';
import TableColumnControl from '/@/components/TableColumnControl/index.vue';
import { Document, Menu } from '@element-plus/icons-vue';
import { useTableColumnControl } from '/@/hooks/tableColumn';
const columnControlRef = ref<any>();
const tableColumns = [
{ prop: 'loginName', label: '填写人账号' },
{ prop: 'year', label: '学年' },
{ prop: 'period', label: '学期' },
{ prop: 'diningHallName', label: '食堂名称' },
{ prop: 'diningHallVoteScore', label: '评分' },
{ prop: 'isUnderstand', label: '是否了解' },
{ prop: 'isStu', label: '身份' },
{ prop: 'mostDissatisfied', label: '最不满意食堂' },
{ prop: 'mostVist', label: '最常去食堂' },
{ prop: 'mostDissatisfiedLayer', label: '最不满意楼层' },
{ prop: 'mostDissatisfiedWindow', label: '最不满意窗口' },
{ prop: 'mostVisitLayer', label: '最常去楼层' },
{ prop: 'createDate', label: '填写时间' },
];
const { visibleColumns, visibleColumnsSorted, checkColumnVisible, handleColumnChange, handleColumnOrderChange } = useTableColumnControl(tableColumns);
const searchForm = reactive({
year: '',
period: '',
});
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: searchForm,
pageList: fetchList,
props: {
item: 'records',
totalCount: 'total',
},
});
const { getDataList, currentChangeHandle, sizeChangeHandle, tableStyle } = useTable(state);
const resetSearch = () => {
searchForm.year = '';
searchForm.period = '';
getDataList(true);
};
</script>
<style scoped lang="scss">
@import '/@/assets/styles/modern-page.scss';
</style>

View File

@@ -0,0 +1,283 @@
<template>
<div class="modern-page-container">
<div class="page-wrapper">
<el-card class="content-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><DataAnalysis /></el-icon>
毕业调查问卷完成率-部门
</span>
</div>
</template>
<!-- 搜索表单 -->
<el-form :model="searchForm" inline class="search-form">
<el-form-item label="毕业年份">
<el-input v-model="searchForm.graduationYear" placeholder="请输入毕业年份" clearable style="width: 150px" />
</el-form-item>
<el-form-item label="学院">
<el-select v-model="searchForm.deptCode" placeholder="请选择学院" clearable style="width: 200px">
<el-option v-for="item in deptList" :key="item.deptCode" :label="item.deptName" :value="item.deptCode" />
</el-select>
</el-form-item>
<el-form-item label="是否联院">
<el-select v-model="searchForm.isUnion" placeholder="请选择" clearable style="width: 120px">
<el-option label="是" value="1" />
<el-option label="否" value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button icon="Search" type="primary" @click="getData(true)"> </el-button>
<el-button icon="Refresh" @click="resetSearch"> </el-button>
</el-form-item>
</el-form>
<el-table :data="dataList" v-loading="loading" stripe class="modern-table">
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column prop="deptName" label="学院" min-width="150" show-overflow-tooltip />
<el-table-column prop="shouldFilled" label="应填人数" width="100" align="center">
<template #default="{ row }">
<el-tag type="info">{{ row.shouldFilled }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="hasFilled" label="已填人数" width="100" align="center">
<template #default="{ row }">
<el-tag type="success">{{ row.hasFilled }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="noFilled" label="未填人数" width="100" align="center">
<template #default="{ row }">
<el-tag type="danger">{{ row.noFilled }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="completionRate" label="完成率" width="150" align="center">
<template #default="{ row }">
<el-progress
:percentage="parseFloat(row.completionRate) || 0"
:stroke-width="15"
:text-inside="true"
:color="getProgressColor(parseFloat(row.completionRate) || 0)"
/>
</template>
</el-table-column>
<el-table-column label="操作" width="150" align="center" fixed="right">
<template #default="scope">
<el-button icon="View" link type="primary" @click="handleViewClass(scope.row)"> 查看班级 </el-button>
</template>
</el-table-column>
<template #empty>
<el-empty description="暂无数据" :image-size="120" />
</template>
</el-table>
</el-card>
</div>
<!-- 班级详情对话框 -->
<el-dialog v-model="classDialogVisible" title="班级填写情况" :width="900" :close-on-click-modal="false" draggable>
<el-table :data="classData" v-loading="classLoading" stripe>
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column prop="className" label="班级名称" min-width="150" show-overflow-tooltip />
<el-table-column prop="shouldFilled" label="应填人数" width="100" align="center">
<template #default="{ row }">
<el-tag type="info">{{ row.shouldFilled }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="hasFilled" label="已填人数" width="100" align="center">
<template #default="{ row }">
<el-tag type="success">{{ row.hasFilled }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="noFilled" label="未填人数" width="100" align="center">
<template #default="{ row }">
<el-tag type="danger">{{ row.noFilled }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="completionRate" label="完成率" width="150" align="center">
<template #default="{ row }">
<el-progress
:percentage="parseFloat(row.completionRate) || 0"
:stroke-width="15"
:text-inside="true"
:color="getProgressColor(parseFloat(row.completionRate) || 0)"
/>
</template>
</el-table-column>
<el-table-column label="操作" width="100" align="center">
<template #default="scope">
<el-button icon="View" link type="primary" @click="handleViewStudent(scope.row)"> 查看学生 </el-button>
</template>
</el-table-column>
</el-table>
</el-dialog>
<!-- 学生详情对话框 -->
<el-dialog v-model="studentDialogVisible" title="学生填写情况" :width="1100" :close-on-click-modal="false" draggable>
<el-table :data="studentData" v-loading="studentLoading" stripe>
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column prop="stuNo" label="学号" width="120" align="center" />
<el-table-column prop="realName" label="姓名" width="100" align="center" />
<el-table-column prop="className" label="班级" min-width="150" show-overflow-tooltip />
<el-table-column prop="majorName" label="专业" min-width="150" show-overflow-tooltip />
<el-table-column prop="phone" label="手机号" width="120" align="center" />
<el-table-column prop="isWrite" label="是否填写" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.isWrite === 1 ? 'success' : 'danger'">
{{ row.isWrite === 1 ? '已填写' : '未填写' }}
</el-tag>
</template>
</el-table-column>
</el-table>
<div class="pagination-wrapper" style="margin-top: 15px">
<pagination @size-change="studentSizeChange" @current-change="studentCurrentChange" v-bind="studentPagination" />
</div>
</el-dialog>
</div>
</template>
<script setup lang="ts" name="EmploymentInformationSurveyCompletionRateDept">
import { ref, reactive, onMounted } from 'vue';
import { getStatisticsDept, getStatisticsClass, getClassStudentInfo } from '/@/api/stuwork/employmentinformationsurvey';
import { DataAnalysis } from '@element-plus/icons-vue';
import { useMessage } from '/@/hooks/message';
const searchForm = reactive({
graduationYear: '',
deptCode: '',
isUnion: '',
});
const dataList = ref<any[]>([]);
const loading = ref(false);
const deptList = ref<any[]>([]);
// 班级详情
const classDialogVisible = ref(false);
const classData = ref<any[]>([]);
const classLoading = ref(false);
const currentDeptCode = ref('');
// 学生详情
const studentDialogVisible = ref(false);
const studentData = ref<any[]>([]);
const studentLoading = ref(false);
const currentClassNo = ref('');
const studentPagination = reactive({
current: 1,
size: 10,
total: 0,
});
const getData = async (resetPage = false) => {
loading.value = true;
try {
const res = await getStatisticsDept({
graduationYear: searchForm.graduationYear,
deptCode: searchForm.deptCode,
isUnion: searchForm.isUnion,
});
dataList.value = res.data || [];
} catch (error) {
console.error('获取数据失败', error);
} finally {
loading.value = false;
}
};
const resetSearch = () => {
searchForm.graduationYear = '';
searchForm.deptCode = '';
searchForm.isUnion = '';
getData(true);
};
const handleViewClass = async (row: any) => {
classDialogVisible.value = true;
classLoading.value = true;
currentDeptCode.value = row.deptCode;
try {
const res = await getStatisticsClass({
deptCode: row.deptCode,
graduationYear: searchForm.graduationYear,
});
classData.value = res.data || [];
} catch (error) {
console.error('获取班级数据失败', error);
} finally {
classLoading.value = false;
}
};
const handleViewStudent = async (row: any) => {
studentDialogVisible.value = true;
studentLoading.value = true;
currentClassNo.value = row.classNo || row.classCode;
studentPagination.current = 1;
try {
const res = await getClassStudentInfo({
classCode: row.classNo || row.classCode,
year: searchForm.graduationYear,
current: studentPagination.current,
size: studentPagination.size,
});
studentData.value = res.data?.list || [];
studentPagination.total = res.data?.list?.length || 0;
} catch (error) {
console.error('获取学生数据失败', error);
} finally {
studentLoading.value = false;
}
};
const studentSizeChange = async (size: number) => {
studentPagination.size = size;
studentDialogVisible.value = true;
studentLoading.value = true;
try {
const res = await getClassStudentInfo({
classCode: currentClassNo.value,
year: searchForm.graduationYear,
current: studentPagination.current,
size: studentPagination.size,
});
studentData.value = res.data?.list || [];
} catch (error) {
console.error('获取学生数据失败', error);
} finally {
studentLoading.value = false;
}
};
const studentCurrentChange = async (current: number) => {
studentPagination.current = current;
studentDialogVisible.value = true;
studentLoading.value = true;
try {
const res = await getClassStudentInfo({
classCode: currentClassNo.value,
year: searchForm.graduationYear,
current: studentPagination.current,
size: studentPagination.size,
});
studentData.value = res.data?.list || [];
} catch (error) {
console.error('获取学生数据失败', error);
} finally {
studentLoading.value = false;
}
};
const getProgressColor = (percentage: number) => {
if (percentage >= 80) return '#67c23a';
if (percentage >= 50) return '#e6a23c';
return '#f56c6c';
};
onMounted(() => {
getData();
});
</script>
<style scoped lang="scss">
@import '/@/assets/styles/modern-page.scss';
</style>

View File

@@ -1,214 +1,437 @@
<template>
<div class="modern-page-container">
<div class="page-wrapper">
<!-- 筛选 -->
<el-card class="search-card" shadow="never">
<!-- 筛选条件 -->
<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="queryForm" :inline="true" class="search-form">
<el-form :model="searchForm" ref="searchFormRef" :inline="true" class="search-form">
<el-form-item label="毕业年份" prop="graduYear">
<el-select v-model="queryForm.graduYear" placeholder="请选择毕业年份" clearable style="width: 160px">
<el-option v-for="y in graduYearOptions" :key="y" :label="y + '年'" :value="y" />
<el-select v-model="searchForm.graduYear" placeholder="请选择毕业年份" clearable filterable style="width: 160px" @change="handleSearch">
<el-option v-for="y in graduYearOptions" :key="y" :label="y + '年'" :value="String(y)" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="loadStatistics">查询统计</el-button>
<el-button type="primary" icon="Search" @click="handleSearch">查询</el-button>
<el-button icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 汇总卡片 -->
<el-row :gutter="16" class="summary-row">
<el-col :span="6">
<!-- 统计卡片 -->
<el-row :gutter="20" class="stat-cards">
<el-col :xs="24" :sm="12" :md="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-label">毕业生总数</div>
<div class="stat-value">{{ summary.total }}</div>
<div class="stat-content">
<div class="stat-icon" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%)">
<el-icon><UserFilled /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ statistics.total }}</div>
<div class="stat-label">毕业生总数</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card stat-success">
<div class="stat-label">确认毕业</div>
<div class="stat-value">{{ summary.confirmed }}</div>
<el-col :xs="24" :sm="12" :md="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-content">
<div class="stat-icon" style="background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%)">
<el-icon><CircleCheck /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ statistics.confirmed }}</div>
<div class="stat-label">确认毕业</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card stat-warning">
<div class="stat-label">待确认</div>
<div class="stat-value">{{ summary.pending }}</div>
<el-col :xs="24" :sm="12" :md="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-content">
<div class="stat-icon" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%)">
<el-icon><Warning /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ statistics.pending }}</div>
<div class="stat-label">待确认</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card stat-danger">
<div class="stat-label">不可毕业</div>
<div class="stat-value">{{ summary.rejected }}</div>
<el-col :xs="24" :sm="12" :md="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-content">
<div class="stat-icon" style="background: linear-gradient(135deg, #eb3349 0%, #f45c43 100%)">
<el-icon><CircleClose /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ statistics.rejected }}</div>
<div class="stat-label">不可毕业</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 按学院统计表格 -->
<el-card class="content-card" shadow="never">
<!-- 图表区域 -->
<el-row :gutter="20" class="chart-row">
<el-col :xs="24" :lg="12">
<el-card shadow="never" class="chart-card">
<template #header>
<div class="card-header">
<span class="card-title">学院毕业人数分布</span>
</div>
</template>
<div ref="deptChartRef" class="chart-container"></div>
</el-card>
</el-col>
<el-col :xs="24" :lg="12">
<el-card shadow="never" class="chart-card">
<template #header>
<div class="card-header">
<span class="card-title">毕业类型分布</span>
</div>
</template>
<div ref="typeChartRef" class="chart-container"></div>
</el-card>
</el-col>
</el-row>
<!-- 详细数据表格 -->
<el-card shadow="never" class="table-card">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Document /></el-icon>
按学院统计
</span>
<span class="card-title">学院毕业详情</span>
</div>
</template>
<el-table :data="deptStats" v-loading="loading" stripe border class="modern-table">
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="deptName" label="学院" min-width="140" show-overflow-tooltip />
<el-table-column prop="total" label="应毕业人数" width="110" align="center" />
<el-table-column prop="pending" label="待确认" width="90" align="center" />
<el-table-column prop="confirmed" label="确认毕业" width="100" align="center" />
<el-table-column prop="rejected" label="不可毕业" width="100" align="center" />
<el-table-column prop="completionRate" label="完成率" width="100" align="center">
<el-table :data="deptDetailList" v-loading="loading" stripe border>
<el-table-column prop="deptName" label="学院" align="center" />
<el-table-column prop="total" label="毕业生总数" align="center" />
<el-table-column prop="confirmed" label="确认毕业" align="center">
<template #default="scope">
<span :class="completionRateClass(scope.row.completionRate)">
{{ scope.row.completionRate }}
</span>
<el-tag type="success" size="small">{{ scope.row.confirmed }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="pending" label="待确认" align="center">
<template #default="scope">
<el-tag type="warning" size="small">{{ scope.row.pending }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="rejected" label="不可毕业" align="center">
<template #default="scope">
<el-tag type="danger" size="small">{{ scope.row.rejected }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="rate" label="毕业率" align="center" width="120">
<template #default="scope">
<el-progress :percentage="scope.row.rate" :stroke-width="10" :text-inside="true" />
</template>
</el-table-column>
</el-table>
<template #empty>
<el-empty description="请选择毕业年份并点击「查询统计」" :image-size="100" />
</template>
</el-card>
</div>
</div>
</template>
<script setup lang="ts" name="GradustuAnalyse">
import { reactive, ref, computed, onMounted } from 'vue';
import { useMessage } from '/@/hooks/message';
import { ref, reactive, computed, onMounted, onUnmounted, nextTick } from 'vue';
import { Search, UserFilled, CircleCheck, CircleClose, Warning } from '@element-plus/icons-vue';
import { fetchListForAnalyse } from '/@/api/stuwork/gradustu';
import { Search, Document } from '@element-plus/icons-vue';
import * as echarts from 'echarts';
const queryForm = reactive({
graduYear: '',
const searchFormRef = ref();
const showSearch = ref(true);
const loading = ref(false);
const dataList = ref<any[]>([]);
const deptChartRef = ref<HTMLElement>();
const typeChartRef = ref<HTMLElement>();
let deptChart: echarts.ECharts | null = null;
let typeChart: echarts.ECharts | null = null;
// 搜索表单
const searchForm = reactive({
graduYear: String(new Date().getFullYear()),
});
const loading = ref(false);
const rawList = ref<any[]>([]);
// 毕业年份选项
const graduYearOptions = computed(() => {
const y = new Date().getFullYear();
return Array.from({ length: 11 }, (_, i) => y - 5 + i);
});
// 汇总:总人数、确认毕业、待确认、不可毕业
const summary = computed(() => {
const list = rawList.value;
let pending = 0;
let confirmed = 0;
let rejected = 0;
list.forEach((item: any) => {
const s = String(item.status ?? '');
if (s === '0') pending++;
else if (s === '1') confirmed++;
else if (s === '-1') rejected++;
});
return {
total: list.length,
pending,
confirmed,
rejected,
};
// 统计数据
const statistics = computed(() => {
const total = dataList.value.length;
const confirmed = dataList.value.filter((item) => item.status === '1').length;
const pending = dataList.value.filter((item) => item.status === '0').length;
const rejected = dataList.value.filter((item) => item.status === '-1').length;
return { total, confirmed, pending, rejected };
});
// 学院聚合
const deptStats = computed(() => {
const list = rawList.value;
const map: Record<string, { deptCode: string; deptName: string; total: number; pending: number; confirmed: number; rejected: number }> = {};
list.forEach((item: any) => {
const code = item.deptCode || '未知';
const name = item.deptName || item.deptCode || '未知';
if (!map[code]) {
map[code] = { deptCode: code, deptName: name, total: 0, pending: 0, confirmed: 0, rejected: 0 };
// 学院详细数据
const deptDetailList = computed(() => {
const deptMap = new Map<string, { deptName: string; total: number; confirmed: number; pending: number; rejected: number }>();
dataList.value.forEach((item) => {
const deptName = item.deptName || '未知学院';
if (!deptMap.has(deptName)) {
deptMap.set(deptName, { deptName, total: 0, confirmed: 0, pending: 0, rejected: 0 });
}
const row = map[code];
row.total++;
const s = String(item.status ?? '');
if (s === '0') row.pending++;
else if (s === '1') row.confirmed++;
else if (s === '-1') row.rejected++;
const dept = deptMap.get(deptName)!;
dept.total++;
if (item.status === '1') dept.confirmed++;
else if (item.status === '0') dept.pending++;
else if (item.status === '-1') dept.rejected++;
});
return Object.values(map).map((row) => ({
...row,
completionRate: row.total > 0 ? ((row.confirmed / row.total) * 100).toFixed(1) + '%' : '0%',
return Array.from(deptMap.values()).map((item) => ({
...item,
rate: item.total > 0 ? Math.round((item.confirmed / item.total) * 100) : 0,
}));
});
const completionRateClass = (rate: string) => {
const num = parseFloat(rate);
if (num >= 100) return 'rate-high';
if (num >= 80) return 'rate-mid';
return 'rate-low';
};
const loadStatistics = async () => {
if (!queryForm.graduYear) {
useMessage().warning('请选择毕业年份');
// 查询数据
const handleSearch = async () => {
if (!searchForm.graduYear) {
return;
}
loading.value = true;
try {
const list = await fetchListForAnalyse(queryForm.graduYear);
rawList.value = list;
} catch (err: any) {
useMessage().error(err.msg || '获取数据失败');
rawList.value = [];
const res = await fetchListForAnalyse(searchForm.graduYear);
dataList.value = res;
await nextTick();
renderCharts();
} catch (err) {
console.error('获取数据失败', err);
dataList.value = [];
} finally {
loading.value = false;
}
};
// 重置
const handleReset = () => {
searchFormRef.value?.resetFields();
searchForm.graduYear = String(new Date().getFullYear());
handleSearch();
};
// 渲染图表
const renderCharts = () => {
renderDeptChart();
renderTypeChart();
};
// 渲染学院分布图表
const renderDeptChart = () => {
if (!deptChartRef.value) return;
if (deptChart) {
deptChart.dispose();
}
deptChart = echarts.init(deptChartRef.value);
const chartData = deptDetailList.value.map((item) => ({
name: item.deptName,
value: item.total,
}));
const option: echarts.EChartsOption = {
tooltip: {
trigger: 'item',
formatter: '{b}: {c}人 ({d}%)',
},
legend: {
orient: 'vertical',
left: 'left',
top: 'center',
},
series: [
{
name: '毕业人数',
type: 'pie',
radius: ['40%', '70%'],
center: ['60%', '50%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2,
},
label: {
show: false,
position: 'center',
},
emphasis: {
label: {
show: true,
fontSize: 16,
fontWeight: 'bold',
},
},
labelLine: {
show: false,
},
data: chartData,
},
],
};
deptChart.setOption(option);
};
// 渲染毕业类型图表
const renderTypeChart = () => {
if (!typeChartRef.value) return;
if (typeChart) {
typeChart.dispose();
}
typeChart = echarts.init(typeChartRef.value);
const typeCount = {
段段清: dataList.value.filter((item) => item.type === '1').length,
正常毕业: dataList.value.filter((item) => item.type === '2').length,
未知: dataList.value.filter((item) => item.type !== '1' && item.type !== '2').length,
};
const chartData = Object.entries(typeCount)
.filter(([_, value]) => value > 0)
.map(([name, value]) => ({ name, value }));
const option: echarts.EChartsOption = {
tooltip: {
trigger: 'item',
formatter: '{b}: {c}人 ({d}%)',
},
legend: {
orient: 'vertical',
left: 'left',
top: 'center',
},
series: [
{
name: '毕业类型',
type: 'pie',
radius: '60%',
center: ['60%', '50%'],
data: chartData,
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)',
},
},
},
],
};
typeChart.setOption(option);
};
// 窗口大小变化时重绘图表
const handleResize = () => {
deptChart?.resize();
typeChart?.resize();
};
onMounted(() => {
queryForm.graduYear = String(new Date().getFullYear());
handleSearch();
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
deptChart?.dispose();
typeChart?.dispose();
});
</script>
<style scoped lang="scss">
@import '/@/assets/styles/modern-page.scss';
.summary-row {
margin-bottom: 16px;
.stat-cards {
margin-bottom: 20px;
}
.stat-card {
text-align: center;
.stat-content {
display: flex;
align-items: center;
padding: 10px 0;
}
.stat-icon {
width: 60px;
height: 60px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16px;
.el-icon {
font-size: 28px;
color: #fff;
}
}
.stat-info {
flex: 1;
}
.stat-value {
font-size: 28px;
font-weight: 600;
color: #303133;
line-height: 1.2;
}
.stat-label {
font-size: 14px;
color: var(--el-text-color-secondary);
}
.stat-value {
font-size: 24px;
font-weight: 600;
margin-top: 8px;
}
&.stat-success .stat-value {
color: var(--el-color-success);
}
&.stat-warning .stat-value {
color: var(--el-color-warning);
}
&.stat-danger .stat-value {
color: var(--el-color-danger);
color: #909399;
margin-top: 4px;
}
}
.rate-high {
color: var(--el-color-success);
font-weight: 500;
.chart-row {
margin-bottom: 20px;
}
.rate-mid {
color: var(--el-color-warning);
.chart-card {
.card-header {
display: flex;
align-items: center;
}
.card-title {
font-size: 16px;
font-weight: 500;
}
}
.rate-low {
color: var(--el-color-danger);
.chart-container {
height: 350px;
}
.table-card {
.card-header {
display: flex;
align-items: center;
}
.card-title {
font-size: 16px;
font-weight: 500;
}
}
</style>

View File

@@ -0,0 +1,306 @@
<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="searchForm" ref="searchFormRef" :inline="true" @keyup.enter="handleSearch" class="search-form">
<el-form-item label="异动类型编码" prop="turnoverType">
<el-input
v-model="searchForm.turnoverType"
placeholder="请输入异动类型编码"
clearable
style="width: 200px" />
</el-form-item>
<el-form-item label="异动类型名称" prop="turnoverTypeName">
<el-input
v-model="searchForm.turnoverTypeName"
placeholder="请输入异动类型名称"
clearable
style="width: 200px" />
</el-form-item>
<el-form-item label="是否流失" prop="isLoss">
<el-select
v-model="searchForm.isLoss"
placeholder="请选择"
clearable
style="width: 200px">
<el-option label="是" value="1" />
<el-option label="否" value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" plain icon="Search" @click="handleSearch">查询</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"><Setting /></el-icon>
异动流失配置列表
</span>
<div class="header-actions">
<el-button
icon="Plus"
type="primary"
@click="handleAdd">
新增配置
</el-button>
<right-toolbar
v-model:showSearch="showSearch"
class="ml10"
@queryTable="getDataList">
</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 #default="{ $index }">
{{ $index + 1 + ((state.pagination?.current || 1) - 1) * (state.pagination?.size || 10) }}
</template>
</el-table-column>
<el-table-column prop="turnoverType" label="异动类型编码" align="center" show-overflow-tooltip />
<el-table-column prop="turnoverTypeName" label="异动类型名称" align="center" show-overflow-tooltip />
<el-table-column prop="isLoss" label="是否流失" align="center" width="120">
<template #default="scope">
<el-tag :type="scope.row.isLoss === '1' ? 'danger' : 'success'" size="small">
{{ scope.row.isLoss === '1' ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="remarks" label="备注" align="center" show-overflow-tooltip min-width="150" />
<el-table-column prop="createTime" label="创建时间" align="center" width="180">
<template #default="scope">
{{ scope.row.createTime || '-' }}
</template>
</el-table-column>
<el-table-column label="操作" width="160" align="center" fixed="right">
<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>
<!-- 新增/编辑弹窗 -->
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="500px"
:close-on-click-modal="false">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="120px">
<el-form-item label="异动类型编码" prop="turnoverType">
<el-input v-model="formData.turnoverType" placeholder="请输入异动类型编码" />
</el-form-item>
<el-form-item label="异动类型名称" prop="turnoverTypeName">
<el-input v-model="formData.turnoverTypeName" placeholder="请输入异动类型名称" />
</el-form-item>
<el-form-item label="是否流失" prop="isLoss">
<el-radio-group v-model="formData.isLoss">
<el-radio label="1"></el-radio>
<el-radio label="0"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remarks">
<el-input v-model="formData.remarks" type="textarea" :rows="3" placeholder="请输入备注" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts" name="StuTurnoverLossConfig">
import { reactive, ref } from 'vue'
import { BasicTableProps, useTable } from "/@/hooks/table";
import { fetchList, addObj, editObj, delObj } from "/@/api/stuwork/stuturnoverlossconfig";
import { useMessage, useMessageBox } from "/@/hooks/message";
import { Search, Setting, Plus, Edit, Delete } from '@element-plus/icons-vue'
// 定义变量
const searchFormRef = ref()
const formRef = ref()
const showSearch = ref(true)
const dialogVisible = ref(false)
const dialogTitle = ref('新增配置')
const submitLoading = ref(false)
const isEdit = ref(false)
// 搜索表单
const searchForm = reactive({
turnoverType: '',
turnoverTypeName: '',
isLoss: ''
})
// 表单数据
const formData = reactive({
id: '',
turnoverType: '',
turnoverTypeName: '',
isLoss: '0',
remarks: ''
})
// 表单验证规则
const formRules = {
turnoverType: [{ required: true, message: '请输入异动类型编码', trigger: 'blur' }],
turnoverTypeName: [{ required: true, message: '请输入异动类型名称', trigger: 'blur' }],
isLoss: [{ required: true, message: '请选择是否流失', trigger: 'change' }]
}
// 配置 useTable
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: searchForm,
pageList: fetchList,
props: {
item: 'records',
totalCount: 'total'
},
createdIsNeed: true
})
// table hook
const {
getDataList,
currentChangeHandle,
sizeChangeHandle,
tableStyle
} = useTable(state)
// 查询
const handleSearch = () => {
getDataList()
}
// 重置
const handleReset = () => {
searchFormRef.value?.resetFields()
searchForm.turnoverType = ''
searchForm.turnoverTypeName = ''
searchForm.isLoss = ''
getDataList()
}
// 新增
const handleAdd = () => {
isEdit.value = false
dialogTitle.value = '新增配置'
resetForm()
dialogVisible.value = true
}
// 编辑
const handleEdit = (row: any) => {
isEdit.value = true
dialogTitle.value = '编辑配置'
resetForm()
formData.id = row.id
formData.turnoverType = row.turnoverType
formData.turnoverTypeName = row.turnoverTypeName
formData.isLoss = row.isLoss
formData.remarks = row.remarks || ''
dialogVisible.value = true
}
// 重置表单
const resetForm = () => {
formData.id = ''
formData.turnoverType = ''
formData.turnoverTypeName = ''
formData.isLoss = '0'
formData.remarks = ''
formRef.value?.resetFields()
}
// 提交
const handleSubmit = async () => {
try {
await formRef.value?.validate()
submitLoading.value = true
if (isEdit.value) {
await editObj(formData)
useMessage().success('修改成功')
} else {
await addObj(formData)
useMessage().success('新增成功')
}
dialogVisible.value = false
getDataList()
} catch (err: any) {
if (err?.msg) {
useMessage().error(err.msg)
}
} finally {
submitLoading.value = false
}
}
// 删除
const handleDelete = async (row: any) => {
try {
await useMessageBox().confirm('确定要删除该配置吗?')
await delObj([row.id])
useMessage().success('删除成功')
getDataList()
} catch (err: any) {
if (err !== 'cancel') {
useMessage().error(err.msg || '删除失败')
}
}
}
</script>
<style scoped lang="scss">
@import '/@/assets/styles/modern-page.scss';
</style>