Merge branch 'developer'

This commit is contained in:
吴红兵
2026-03-11 22:22:50 +08:00
42 changed files with 5736 additions and 630 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

@@ -460,7 +460,7 @@ export const queryStuBaseByNo = (obj: string | number) => {
*/
export const queryStuindex = (query?: any) => {
return request({
url: '/basic/basicstudent/queryStuindex',
url: '/basic/basicstudentinfo/queryXJDataByPage',
method: 'get',
params: query,
});

View File

@@ -6,7 +6,7 @@ import request from '/@/utils/request';
*/
export const fetchList = (query?: any) => {
return request({
url: '/basic/basicstudent/avatar/list',
url: '/basic/basicstudent/avatar/page',
method: 'get',
params: query,
});

View File

@@ -437,3 +437,12 @@ export function getSupplementFilesByApplyId(applyId: string) {
params: { purchaseId: applyId },
});
}
export function exportPurchaseApply(params?: any) {
return request({
url: '/purchase/purchasingapply/export',
method: 'get',
params,
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

@@ -0,0 +1,83 @@
import request from '/@/utils/request';
/**
* 分页查询异动规则配置列表
* @param query
*/
export const fetchList = (query?: any) => {
return request({
url: '/stuwork/stuturnoverrule/page',
method: 'get',
params: query,
});
};
/**
* 新增异动规则配置
* @param data
*/
export const addObj = (data: any) => {
return request({
url: '/stuwork/stuturnoverrule',
method: 'post',
data,
});
};
/**
* 获取详情
* @param id
*/
export const getDetail = (id: string) => {
return request({
url: '/stuwork/stuturnoverrule/detail',
method: 'get',
params: { id },
});
};
/**
* 编辑异动规则配置
* @param data
*/
export const editObj = (data: any) => {
return request({
url: '/stuwork/stuturnoverrule/edit',
method: 'post',
data,
});
};
/**
* 删除异动规则配置
* @param ids
*/
export const delObj = (ids: string[]) => {
return request({
url: '/stuwork/stuturnoverrule/delete',
method: 'post',
data: ids,
});
};
/**
* 获取所有启用的异动规则
*/
export const getAllActiveRules = () => {
return request({
url: '/stuwork/stuturnoverrule/getAllActiveRules',
method: 'get',
});
};
/**
* 根据异动类型获取规则
* @param turnoverType 异动类型
*/
export const getRulesByTurnoverType = (turnoverType: string) => {
return request({
url: '/stuwork/stuturnoverrule/getRulesByTurnoverType',
method: 'get',
params: { turnoverType },
});
};

View File

@@ -1,14 +1,86 @@
import request from '/@/utils/request';
/**
* 查看水电明细
* @param roomNo 宿舍号
* 分页查询宿舍水电明细
* @param query
*/
export const lookDetails = (roomNo: string) => {
export const fetchList = (query?: any) => {
return request({
url: '/stuwork/watermonthreport/page',
method: 'get',
params: query,
});
};
/**
* 新增宿舍水电月明细
* @param data
*/
export const addObj = (data: any) => {
return request({
url: '/stuwork/watermonthreport',
method: 'post',
data,
});
};
/**
* 获取详情
* @param id
*/
export const getDetail = (id: string) => {
return request({
url: '/stuwork/watermonthreport/detail',
method: 'get',
params: { id },
});
};
/**
* 编辑宿舍水电月明细
* @param data
*/
export const editObj = (data: any) => {
return request({
url: '/stuwork/watermonthreport/edit',
method: 'post',
data,
});
};
/**
* 删除宿舍水电月明细
* @param ids
*/
export const delObj = (ids: string[]) => {
return request({
url: '/stuwork/watermonthreport/delete',
method: 'post',
data: ids,
});
};
/**
* 查看水电明细
* @param params 查询参数
*/
export const lookDetails = (params: any) => {
return request({
url: '/stuwork/watermonthreport/lookDetails',
method: 'get',
params: { roomNo },
params,
});
};
/**
* 根据角色查看明细
* @param params 查询参数
*/
export const lookDetail = (params: any) => {
return request({
url: '/stuwork/watermonthreport/lookDetail',
method: 'get',
params,
});
};

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

@@ -168,7 +168,10 @@
</el-table-column>
</el-table>
<pagination v-show="state.total > 0" :total="state.total" v-model:page="state.page" v-model:limit="state.limit" @pagination="getDataList" />
<!-- 分页 -->
<div class="pagination-wrapper">
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
</div>
</el-card>
</div>
</template>
@@ -231,16 +234,14 @@ const state: BasicTableProps = reactive<BasicTableProps>({
createdIsNeed: true,
});
const { getDataList, sortChangeHandle, tableStyle } = useTable(state);
const { getDataList, sortChangeHandle, sizeChangeHandle, currentChangeHandle, tableStyle } = useTable(state);
const handleSearch = () => {
state.page = 1;
getDataList();
};
const handleReset = () => {
searchFormRef.value?.resetFields();
state.page = 1;
getDataList();
};
</script>
@@ -252,4 +253,9 @@ const handleReset = () => {
.mb12 {
margin-bottom: 12px;
}
.pagination-wrapper {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
</style>

View File

@@ -150,44 +150,10 @@ const searchForm = reactive({
classCode: '',
});
// 配置 useTable - 接口返回的数据结构是 { classes: [], students: [] }
// 配置 useTable - 标准分页查询
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: searchForm,
pageList: async (queryParams: any) => {
const res = await fetchList(queryParams);
// 接口返回的数据结构是 { classes: [], students: [] }
// 需要将 students 数组转换为表格数据,并关联班级信息
if (res.data && res.data.students) {
const students = res.data.students || [];
const classes = res.data.classes || [];
const classMap = new Map();
classes.forEach((cls: any) => {
classMap.set(cls.classCode, cls);
});
// 将学生数据与班级信息合并
const dataList = students.map((stu: any) => {
const classInfo = classMap.get(stu.classCode);
return {
...stu,
className: classInfo ? classInfo.classNo : stu.className || '',
classNo: classInfo ? classInfo.classNo : '',
};
});
return {
...res,
data: {
records: dataList,
total: dataList.length,
current: 1,
size: dataList.length,
pages: 1,
},
};
}
return res;
},
pageList: fetchList,
props: {
item: 'records',
totalCount: 'total',

View File

@@ -75,7 +75,7 @@
<el-divider content-position="left">审核流程</el-divider>
<flow-comment-timeline v-if="flowInstId" :flow-inst-id="flowInstId" />
<FlowCommentTimeline v-if="flowInstId" :curr-job="{ flowInstId }" />
</template>
<template #footer>
@@ -91,11 +91,13 @@
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { ref, defineAsyncComponent } from 'vue';
import { useMessage } from '/@/hooks/message';
import { getDetail } from '/@/api/purchase/purchasingcontract';
import { previewFileById, downloadFileById } from '/@/api/purchase/purchasingrequisition';
const FlowCommentTimeline = defineAsyncComponent(() => import('/@/views/jsonflow/comment/timeline.vue'));
interface FileItem {
id: string;
name: string;

View File

@@ -16,7 +16,7 @@
<el-row :gutter="20">
<el-col :span="12" class="mb12">
<el-form-item label="合同编号" prop="contractNo">
<el-input v-model="formData.contractNo" placeholder="请输入合同编号" />
<el-input v-model="formData.contractNo" placeholder="请输入合同编号" disabled />
</el-form-item>
</el-col>
<el-col :span="12" class="mb12">
@@ -178,12 +178,17 @@ const open = async (id: string, openMode: 'add' | 'edit' | 'view' = 'add') => {
const res = await getObj(id);
applyData.value = res?.data || {};
// 新增时,合同编号默认为采购编号
if (mode.value === 'add') {
formData.value.contractNo = applyData.value.purchaseNo || '';
}
const contractRes = await getByPurchaseId(id);
const contractData = contractRes?.data;
if (contractData) {
formData.value = {
purchaseId: id,
contractNo: contractData.contractNo || '',
contractNo: contractData.contractNo || applyData.value.purchaseNo || '',
contractName: contractData.contractName || '',
money: contractData.money,
isBidding: contractData.isBidding || '0',
@@ -215,7 +220,7 @@ const open = async (id: string, openMode: 'add' | 'edit' | 'view' = 'add') => {
console.error('获取数据失败', e);
useMessage().error('获取数据失败');
}
};
};
const resetForm = () => {
formData.value = {

View File

@@ -33,24 +33,24 @@
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="160px" :disabled="isViewMode">
<el-row :gutter="20">
<el-col :span="12">
<el-col :span="12" class="mb12">
<el-form-item label="合同编号" prop="contractNo">
<el-input v-model="formData.contractNo" placeholder="请输入合同编号" />
<el-input v-model="formData.contractNo" placeholder="请输入合同编号" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-col :span="12" class="mb12">
<el-form-item label="合同名称" prop="contractName">
<el-input v-model="formData.contractName" placeholder="请输入合同名称" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-col :span="12" class="mb12">
<el-form-item label="合同金额(元)" prop="money">
<el-input-number v-model="formData.money" :precision="2" :min="0" :controls="false" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-col :span="12" class="mb12">
<el-form-item label="是否需要招标" prop="isBidding">
<el-radio-group v-model="formData.isBidding">
<el-radio label="0"></el-radio>
@@ -60,7 +60,7 @@
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-col :span="12" class="mb12">
<el-form-item label="是否需要法律顾问" prop="isLegalAdviser">
<el-radio-group v-model="formData.isLegalAdviser">
<el-radio label="0"></el-radio>
@@ -68,7 +68,7 @@
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-col :span="12" class="mb12">
<el-form-item label="是否涉及多个部门" prop="isDepts">
<el-radio-group v-model="formData.isDepts">
<el-radio label="0"></el-radio>
@@ -78,7 +78,7 @@
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-col :span="12" class="mb12">
<el-form-item label="是否全校合同" prop="isSchool">
<el-radio-group v-model="formData.isSchool">
<el-radio label="0"></el-radio>
@@ -150,7 +150,7 @@ import { useRoute } from 'vue-router';
import { Document, Tickets, FolderOpened } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
import { getObj } from '/@/api/purchase/purchasingrequisition';
import { getByPurchaseId, updateContract } from '/@/api/purchase/purchasingcontract';
import { getByPurchaseId, getByFlowInstId, updateContract } from '/@/api/purchase/purchasingcontract';
import { previewFileById, downloadFileById } from '/@/api/purchase/purchasingrequisition';
import { currElTabIsSave } from '/@/api/order/order-key-vue';
@@ -208,6 +208,13 @@ const previewVisible = ref(false);
const previewTitle = ref('');
const previewUrl = ref('');
const effectiveFlowInstId = computed(() => {
if (props.currJob?.flowInstId) {
return props.currJob.flowInstId;
}
return route.query.flowInstId ? Number(route.query.flowInstId) : null;
});
const effectivePurchaseId = computed(() => {
if (props.currJob?.orderId) {
return String(props.currJob.orderId);
@@ -232,41 +239,76 @@ const loadApplyData = async () => {
};
const loadContractData = async () => {
if (!effectivePurchaseId.value) return;
let contractData: any = null;
let purchaseId = effectivePurchaseId.value;
// 优先使用 flowInstId 获取合同数据(审核流程嵌入时)
if (effectiveFlowInstId.value) {
try {
fileLoading.value = true;
const res = await getByPurchaseId(effectivePurchaseId.value);
const res = await getByFlowInstId(effectiveFlowInstId.value);
if (res.code === 0 && res.data) {
const data = res.data;
formData.value = {
purchaseId: effectivePurchaseId.value,
contractNo: data.contractNo || '',
contractName: data.contractName || '',
money: data.money,
isBidding: data.isBidding || '0',
isLegalAdviser: data.isLegalAdviser || '0',
legalAdviserOpinion: data.legalAdviserOpinion || '',
isDepts: data.isDepts || '0',
isSchool: data.isSchool || '0',
remarks: data.remarks || '',
contractFileIds: [],
supplementFileIds: [],
};
(formData.value as any).flowStatus = data.flowStatus;
if (data.contractFiles && data.contractFiles.length > 0) {
contractFiles.value = data.contractFiles;
}
if (data.supplementFiles && data.supplementFiles.length > 0) {
supplementFiles.value = data.supplementFiles;
}
contractData = res.data;
purchaseId = res.data.purchaseId || purchaseId;
}
} catch (e: any) {
ElMessage.error(e?.msg || '加载合同信息失败');
} finally {
fileLoading.value = false;
}
}
// 如果通过 flowInstId 没有获取到数据,则使用 purchaseId 获取
if (!contractData && purchaseId) {
try {
fileLoading.value = true;
const res = await getByPurchaseId(purchaseId);
if (res.code === 0 && res.data) {
contractData = res.data;
}
} catch (e: any) {
ElMessage.error(e?.msg || '加载合同信息失败');
} finally {
fileLoading.value = false;
}
}
if (contractData) {
formData.value = {
purchaseId: purchaseId,
contractNo: contractData.contractNo || '',
contractName: contractData.contractName || '',
money: contractData.money,
isBidding: contractData.isBidding || '0',
isLegalAdviser: contractData.isLegalAdviser || '0',
legalAdviserOpinion: contractData.legalAdviserOpinion || '',
isDepts: contractData.isDepts || '0',
isSchool: contractData.isSchool || '0',
remarks: contractData.remarks || '',
contractFileIds: [],
supplementFileIds: [],
};
(formData.value as any).flowStatus = contractData.flowStatus;
if (contractData.contractFiles && contractData.contractFiles.length > 0) {
contractFiles.value = contractData.contractFiles;
}
if (contractData.supplementFiles && contractData.supplementFiles.length > 0) {
supplementFiles.value = contractData.supplementFiles;
}
// 如果有 purchaseId加载采购申请信息
if (purchaseId && !applyData.value.id) {
try {
const applyRes = await getObj(purchaseId);
if (applyRes.code === 0 && applyRes.data) {
applyData.value = applyRes.data;
}
} catch (e) {
console.error('加载采购申请信息失败', e);
}
}
}
};
const handlePreview = async (row: any) => {
@@ -313,7 +355,7 @@ const handleFlowSave = async () => {
await updateContract({
...formData.value,
purchaseId: effectivePurchaseId.value,
purchaseId: formData.value.purchaseId || effectivePurchaseId.value,
contractFileIds,
supplementFileIds,
});

View File

@@ -39,9 +39,52 @@
<el-option label="是" value="1" />
</el-select>
</el-form-item>
<el-form-item label="是否特殊" prop="isSpecial">
<el-select v-model="state.queryForm.isSpecial" 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="purchaseMode">
<el-select
v-model="state.queryForm.purchaseMode"
placeholder="请选择采购形式"
clearable
style="width: 200px"
@change="handlePurchaseModeChange"
>
<el-option label="部门自行采购" value="1" />
<el-option label="学校统一采购" value="2" />
</el-select>
</el-form-item>
<el-form-item v-if="state.queryForm.purchaseMode === '1'" label="采购途径" prop="purchaseChannel">
<el-select
v-model="state.queryForm.purchaseChannel"
placeholder="请选择采购途径"
clearable
style="width: 200px"
@change="handlePurchaseChannelChange"
>
<el-option label="自行采购" value="1" />
<el-option label="委托采购中心采购" value="2" />
</el-select>
</el-form-item>
<el-form-item v-if="showPurchaseTypeSelect" label="采购方式" prop="purchaseType">
<el-select v-model="state.queryForm.purchaseType" placeholder="请选择采购方式" clearable style="width: 200px">
<el-option v-for="item in purchaseTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="需求部门" prop="deptId">
<el-select v-model="state.queryForm.deptCode" placeholder="请选择需求部门" clearable filterable style="width: 200px">
<el-option v-for="item in secondDeptList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="getDataList">查询</el-button>
<el-button icon="Refresh" @click="handleReset">重置</el-button>
<el-button type="success" icon="Download" @click="handleExport">导出</el-button>
</el-form-item>
</el-form>
</el-card>
@@ -206,7 +249,9 @@
>
</el-tag>
<el-tooltip
v-if="scope.row.status === '1' && scope.row.purchaseMode === '1' && scope.row.purchaseChannel === '1' && scope.row.purchaseType !=='6'"
v-if="
scope.row.status === '1' && scope.row.purchaseMode === '1' && scope.row.purchaseChannel === '1' && scope.row.purchaseType !== '6'
"
:content="getSupplementTooltip(scope.row)"
placement="top"
>
@@ -302,7 +347,7 @@
</el-tag>
<el-tag v-else type="warning" class="status-tag-clickable" @click="handleShowDocAudit(scope.row)">未知</el-tag>
</el-tooltip>
<el-tag v-else type="info">未发起</el-tag>
<el-tag v-else type="info">未发起</el-tag>
</template>
<span v-else>-</span>
</template>
@@ -319,16 +364,34 @@
<template #default="scope">
<template v-if="scope.row.status === '1'">
<el-tooltip v-if="scope.row.contractFlowStatus" content="点击查看合同详情" placement="top">
<el-tag v-if="scope.row.contractFlowStatus === '0'" type="warning" class="status-tag-clickable" @click="handleShowContractDetail(scope.row)"
<el-tag
v-if="scope.row.contractFlowStatus === '-1'"
type="info"
class="status-tag-clickable"
@click="handleEditContract(scope.row)"
>已上传
</el-tag>
<el-tag
v-else-if="scope.row.contractFlowStatus === '0'"
type="warning"
class="status-tag-clickable"
@click="handleShowContractDetail(scope.row)"
>运行中
</el-tag>
<el-tag v-else-if="scope.row.contractFlowStatus === '1'" type="success" class="status-tag-clickable" @click="handleShowContractDetail(scope.row)"
<el-tag
v-else-if="scope.row.contractFlowStatus === '1'"
type="success"
class="status-tag-clickable"
@click="handleShowContractDetail(scope.row)"
>已完成
</el-tag>
<el-tag v-else-if="scope.row.contractFlowStatus === '2'" type="info" class="status-tag-clickable" @click="handleShowContractDetail(scope.row)"
<el-tag
v-else-if="scope.row.contractFlowStatus === '2'"
type="info"
class="status-tag-clickable"
@click="handleShowContractDetail(scope.row)"
>已作废
</el-tag>
<el-tag v-else type="info" class="status-tag-clickable" @click="handleShowContractDetail(scope.row)">{{ scope.row.contractFlowStatus }}</el-tag>
</el-tooltip>
<el-button v-else type="primary" link size="small" @click="handleAddContract(scope.row)">添加合同</el-button>
</template>
@@ -482,6 +545,7 @@ import {
saveRepresentor,
listDownloadUrls,
updateFiles,
exportPurchaseApply,
} from '/@/api/purchase/purchasingrequisition';
import { useMessage, useMessageBox } from '/@/hooks/message';
import { useAuth } from '/@/hooks/auth';
@@ -515,6 +579,7 @@ import {
import other from '/@/utils/other';
import { Session } from '/@/utils/storage';
import { getByPurchaseId } from '/@/api/purchase/purchasingcontract';
import { getDeptListByLevelTwo } from '/@/api/basic/basicdept';
// 角色常量
const PURCHASE_DEPT_AUDIT_ROLE_CODE = 'PURCHASE_DEPT_AUDIT';
@@ -548,6 +613,53 @@ const dictData = ref({
categoryTreeData: [] as any[],
});
// 搜索条件相关数据
const purchaseTypeDeptDelegationList = ref<any[]>([]); // 委托采购中心采购方式字典
const secondDeptList = ref<any[]>([]); // 二级部门列表
// 是否显示采购方式选择框
const showPurchaseTypeSelect = computed(() => {
const mode = state.queryForm.purchaseMode;
if (mode === '2') {
// 学校统一采购:显示采购方式
return true;
} else if (mode === '1') {
// 部门自行采购:需要先选择采购途径
return !!state.queryForm.purchaseChannel;
}
return false;
});
// 采购方式选项(根据采购形式和采购途径动态变化)
const purchaseTypeOptions = computed(() => {
const mode = state.queryForm.purchaseMode;
if (mode === '2') {
// 学校统一采购:使用 UNION_PURCHASE_TYPE 字典
return dictData.value.purchaseTypeUnionList;
} else if (mode === '1') {
const channel = state.queryForm.purchaseChannel;
if (channel === '1') {
// 自行采购:使用 DEPT_PURCHASE_TYPE 字典
return dictData.value.purchaseTypeDeptList;
} else if (channel === '2') {
// 委托采购中心采购:使用 PURCHASE_TYPE_DEPT_DELEGATION 字典
return purchaseTypeDeptDelegationList.value;
}
}
return [];
});
// 采购形式变化时清空采购途径和采购方式
const handlePurchaseModeChange = () => {
state.queryForm.purchaseChannel = '';
state.queryForm.purchaseType = '';
};
// 采购途径变化时清空采购方式
const handlePurchaseChannelChange = () => {
state.queryForm.purchaseType = '';
};
// 定义变量内容
const router = useRouter();
const tableRef = ref();
@@ -639,6 +751,11 @@ const state: BasicTableProps = reactive<BasicTableProps>({
projectType: '',
status: '',
isCentralized: '',
isSpecial: '',
purchaseMode: '',
purchaseChannel: '',
purchaseType: '',
deptCode: '',
},
createdIsNeed: true,
});
@@ -747,6 +864,16 @@ const handleAddContract = (row: any) => {
purchaseContractDialogRef.value?.open(String(id), 'add');
};
/** 点击已上传合同:打开采购合同编辑弹窗 */
const handleEditContract = (row: any) => {
const id = row?.id ?? row?.purchaseId;
if (!id) {
useMessage().warning('无法获取采购申请ID');
return;
}
purchaseContractDialogRef.value?.open(String(id), 'edit');
};
/** 点击采购合同状态:打开采购合同详情弹窗 */
const handleShowContractDetail = (row: any) => {
const id = row?.id ?? row?.purchaseId;
@@ -1072,8 +1199,16 @@ const handleArchive = (row: any) => {
// 获取字典数据和品目树数据
const loadDictData = async () => {
try {
const [fundSourceRes, isCentralizedRes, isSpecialRes, purchaseTypeDeptRes, purchaseModeSchoolRes, purchaseTypeUnionRes, categoryTreeRes] =
await Promise.all([
const [
fundSourceRes,
isCentralizedRes,
isSpecialRes,
purchaseTypeDeptRes,
purchaseModeSchoolRes,
purchaseTypeUnionRes,
categoryTreeRes,
purchaseTypeDeptDelegationRes,
] = await Promise.all([
getDicts('PURCHASE_FUND_SOURCE'),
getDicts('PURCHASE_IS_CEN'),
getDicts('PURCHASE_IS_SPEC'),
@@ -1081,6 +1216,7 @@ const loadDictData = async () => {
getDicts('PURCHASE_MODE_SCHOOL'),
getDicts('PURCHASE_TYPE_UNION'),
getTree(),
getDicts('PURCHASE_TYPE_DEPT_DELEGATION'),
]);
// 处理资金来源字典
@@ -1162,6 +1298,14 @@ const loadDictData = async () => {
}));
}
// 处理委托采购中心采购方式字典
if (purchaseTypeDeptDelegationRes.data && Array.isArray(purchaseTypeDeptDelegationRes.data)) {
purchaseTypeDeptDelegationList.value = purchaseTypeDeptDelegationRes.data.map((item: any) => ({
label: item.label || item.dictLabel || item.name,
value: item.value || item.dictValue || item.code,
}));
}
// 处理品目树数据
if (categoryTreeRes.data && Array.isArray(categoryTreeRes.data)) {
dictData.value.categoryTreeData = categoryTreeRes.data;
@@ -1202,9 +1346,47 @@ const loadDictData = async () => {
}
};
// 获取二级部门列表
const loadSecondDeptList = async () => {
try {
const res = await getDeptListByLevelTwo();
if (res.data && Array.isArray(res.data)) {
secondDeptList.value = res.data.map((item: any) => ({
id: item.id || item.deptId,
name: item.name || item.deptName,
}));
}
} catch (err) {
console.error('加载二级部门列表失败', err);
secondDeptList.value = [];
}
};
// 导出功能
const handleExport = async () => {
try {
const res: any = await exportPurchaseApply(state.queryForm);
downloadFile(res, '采购申请列表.xlsx');
} catch (err: any) {
useMessage().error(err.msg || '导出失败');
}
};
const downloadFile = (blob: Blob, fileName: string) => {
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = fileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
};
// 页面加载时获取字典数据和品目树数据
onMounted(() => {
loadDictData();
loadSecondDeptList();
});
</script>

View File

@@ -140,6 +140,13 @@
</el-tag>
<span v-else>-</span>
</template>
<!-- 学期班费结余列特殊模板 -->
<template v-else-if="col.prop === 'termBalance'" #default="scope">
<el-tag v-if="scope.row.termBalance !== null && scope.row.termBalance !== undefined" :type="scope.row.termBalance >= 0 ? 'success' : 'danger'" size="small" effect="plain" round>
¥{{ scope.row.termBalance.toFixed(2) }}
</el-tag>
<span v-else>-</span>
</template>
<!-- 附件列特殊模板 -->
<template v-else-if="col.prop === 'attachment'" #default="scope">
<el-button v-if="scope.row.attachment" icon="Document" link type="primary" size="small" @click="handleViewAttachment(scope.row)">
@@ -328,6 +335,7 @@ const tableColumns = [
{ prop: 'operatTime', label: '发生时间', icon: Calendar, width: 180 },
{ prop: 'type', label: '类型', icon: Collection },
{ prop: 'money', label: '金额', icon: Money },
{ prop: 'termBalance', label: '学期班费结余', icon: Money, width: 120 },
{ prop: 'operator', label: '经办人', icon: User },
{ prop: 'purpose', label: '用途', icon: Document, minWidth: 150 },
{ prop: 'attachment', label: '附件', width: 100 },
@@ -345,7 +353,41 @@ const state: BasicTableProps = reactive<BasicTableProps>({
classCode: '',
type: '',
},
pageList: fetchList,
pageList: async (params: any) => {
const res = await fetchList(params);
// 后端返回数据结构IPage<ClassFeeLogRelationVO>
// ClassFeeLogRelationVO 包含 moneyTotal 和 classFeeLogVOList
if (res.data && res.data.records) {
// 展开所有班级的班费记录
const allRecords: any[] = [];
let totalMoney: number = 0;
res.data.records.forEach((item: any) => {
if (item.classFeeLogVOList && Array.isArray(item.classFeeLogVOList)) {
item.classFeeLogVOList.forEach((log: any) => {
allRecords.push({
...log,
moneyTotal: item.moneyTotal // 添加班费结余到每条记录
});
});
}
// 记录总班费结余
if (item.moneyTotal !== undefined) {
totalMoney = item.moneyTotal;
}
});
return {
...res,
data: {
records: allRecords,
total: allRecords.length,
moneyTotal: totalMoney // 保留总结余
}
};
}
return res;
},
props: {
item: 'records',
totalCount: 'total',

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-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 class="stat-value">{{ summary.total }}</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card stat-success">
<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 class="stat-value">{{ summary.confirmed }}</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card stat-warning">
<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 class="stat-value">{{ summary.pending }}</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card stat-danger">
<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 class="stat-value">{{ summary.rejected }}</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">
<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">
<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">学院毕业详情</span>
</div>
</template>
<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);
.chart-row {
margin-bottom: 20px;
}
.chart-card {
.card-header {
display: flex;
align-items: center;
}
.card-title {
font-size: 16px;
font-weight: 500;
}
}
.rate-mid {
color: var(--el-color-warning);
.chart-container {
height: 350px;
}
.rate-low {
color: var(--el-color-danger);
.table-card {
.card-header {
display: flex;
align-items: center;
}
.card-title {
font-size: 16px;
font-weight: 500;
}
}
</style>

View File

@@ -1,9 +1,17 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<!-- 搜索表单 -->
<el-row v-show="showSearch">
<el-form :model="queryForm" ref="searchFormRef" :inline="true" @keyup.enter="getDataList">
<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="queryForm" ref="searchFormRef" :inline="true" @keyup.enter="getDataList" class="search-form">
<el-form-item label="学年" prop="schoolYear">
<el-select
v-model="queryForm.schoolYear"
@@ -54,15 +62,25 @@
<el-button type="warning" icon="Bell" @click="handleSendWarning" :loading="warningLoading">发送预警</el-button>
</el-form-item>
</el-form>
</el-row>
</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>
</template>
<!-- 统计表格 -->
<el-row style="margin-bottom: 20px">
<el-table
:data="statisticsData"
v-loading="loading"
border
style="width: 100%"
stripe
style="width: 100%; margin-bottom: 20px"
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
>
@@ -73,50 +91,52 @@
<el-table-column prop="pass" label="及格" min-width="100" align="center" />
<el-table-column prop="fail" label="不及格" min-width="100" align="center" />
</el-table>
</el-row>
<!-- 学生列表表格 -->
<el-row>
<el-table
:data="studentList"
v-loading="loading"
border
:max-height="600"
stripe
style="width: 100%"
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="stuNo" label="学号" show-overflow-tooltip align="center" />
<el-table-column prop="realName" label="姓名" show-overflow-tooltip align="center" />
<el-table-column prop="score" label="学期总评" show-overflow-tooltip align="center">
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column prop="stuNo" label="学号" min-width="120" show-overflow-tooltip align="center" />
<el-table-column prop="realName" label="姓名" min-width="100" show-overflow-tooltip align="center" />
<!-- 各月份分数 -->
<el-table-column v-for="(month, index) in monthColumns" :key="index" :label="month.label" min-width="70" align="center">
<template #default="scope">
<span>{{ scope.row.score !== null && scope.row.score !== undefined ? scope.row.score.toFixed(2) : '-' }}</span>
<span>{{ formatScore(scope.row[month.prop]) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="150" align="center" fixed="right">
<!-- 学期总评 -->
<el-table-column prop="scoreOneTerm" label="学期总评" min-width="100" align="center">
<template #default="scope">
<el-button icon="View" text type="primary" @click="handleView(scope.row)"> 查看 </el-button>
<el-tag :type="getScoreType(scope.row.scoreOneTerm)" size="small" effect="plain">
{{ formatScore(scope.row.scoreOneTerm) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="100" align="center" fixed="right">
<template #default="scope">
<el-button icon="View" link type="primary" @click="handleView(scope.row)">查看</el-button>
</template>
</el-table-column>
</el-table>
</el-row>
</el-card>
</div>
<!-- 查看详情弹窗接口queryDataByStuNo 通过学年学号查看详情按当前学期筛选 -->
<el-dialog
v-model="viewDialogVisible"
title="学期操行考核详情"
width="800px"
destroy-on-close
@close="viewDetailList = []">
<!-- 查看详情弹窗 -->
<el-dialog v-model="viewDialogVisible" title="学期操行考核详情" width="900px" destroy-on-close @close="viewDetailList = []">
<div v-if="viewRow" class="view-summary">
<el-descriptions :column="2" border size="small">
<el-descriptions :column="3" border size="small">
<el-descriptions-item label="学号">{{ viewRow.stuNo }}</el-descriptions-item>
<el-descriptions-item label="姓名">{{ viewRow.realName }}</el-descriptions-item>
<el-descriptions-item label="学">{{ queryForm.schoolYear }}</el-descriptions-item>
<el-descriptions-item label="学期">{{ formatSchoolTerm(queryForm.schoolTerm) }}</el-descriptions-item>
<el-descriptions-item label="学期总评" :span="2">
{{ viewRow.score != null && viewRow.score !== undefined ? Number(viewRow.score).toFixed(2) : '-' }}
<el-descriptions-item label="学期总评">
<el-tag :type="getScoreType(viewRow.scoreOneTerm)" size="small">
{{ formatScore(viewRow.scoreOneTerm) }}
</el-tag>
</el-descriptions-item>
</el-descriptions>
</div>
@@ -124,14 +144,13 @@
<el-table
:data="viewDetailList"
v-loading="viewLoading"
border
stripe
size="small"
max-height="400"
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle">
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="schoolTerm" label="期" width="80" align="center" show-overflow-tooltip />
<el-table-column prop="recordDate" label="考核日期" width="110" align="center" show-overflow-tooltip />
<el-table-column prop="recordDate" label="考核日期" width="120" align="center" show-overflow-tooltip />
<el-table-column prop="conductType" label="类型" width="80" align="center">
<template #default="scope">
<el-tag :type="scope.row.conductType === '1' ? 'success' : 'danger'" size="small">
@@ -144,8 +163,8 @@
{{ scope.row.score != null && scope.row.score !== undefined ? Number(scope.row.score) : '-' }}
</template>
</el-table-column>
<el-table-column prop="description" label="情况记录" min-width="140" show-overflow-tooltip />
<el-table-column prop="remarks" label="备注" min-width="100" show-overflow-tooltip />
<el-table-column prop="description" label="情况记录" min-width="150" show-overflow-tooltip />
<el-table-column prop="remarks" label="备注" min-width="120" show-overflow-tooltip />
</el-table>
<template v-if="viewDetailList.length === 0 && !viewLoading">
<el-empty description="暂无考核记录" :image-size="80" />
@@ -156,16 +175,17 @@
<script setup lang="ts" name="StuConductTerm">
import { reactive, ref, onMounted, computed } from 'vue'
import { getStuConductTerm, queryDataByStuNo, sendConductWarning } from "/@/api/stuwork/stuconduct";
import { getStuConductTerm, sendConductWarning, queryDataByStuNo } from "/@/api/stuwork/stuconduct";
import { getClassListByRole } from "/@/api/basic/basicclass";
import { queryAllSchoolYear } from "/@/api/basic/basicyear";
import { getDicts } from "/@/api/admin/dict";
import { useMessage, useMessageBox } from "/@/hooks/message";
import { Search, Document } from '@element-plus/icons-vue';
// 表格样式 - 在组件内部定义,不从外部导入
// 表格样式
const tableStyle = {
cellStyle: { padding: '8px 0' },
headerCellStyle: { background: '#f5f7fa', color: '#606266', fontWeight: 'bold' },
cellStyle: { padding: '8px 0', textAlign: 'center' },
headerCellStyle: { background: '#f5f7fa', color: '#606266', fontWeight: 'bold', textAlign: 'center' },
};
// 定义变量内容
@@ -189,28 +209,73 @@ const queryForm = reactive({
classCode: '',
});
// 根据学期动态生成月份列
const monthColumns = computed(() => {
// 第一学期9月、10月、11月、12月、1月
// 第二学期2月、3月、4月、5月、6月
if (queryForm.schoolTerm === '1') {
return [
{ label: '9月', prop: 'scoreOneMonth' },
{ label: '10月', prop: 'scoreTwoMonth' },
{ label: '11月', prop: 'scoreThreeMonth' },
{ label: '12月', prop: 'scoreFourMonth' },
{ label: '1月', prop: 'scoreFiveMonth' },
];
} else if (queryForm.schoolTerm === '2') {
return [
{ label: '2月', prop: 'scoreSixMonth' },
{ label: '3月', prop: 'scoreSevenMonth' },
{ label: '4月', prop: 'scoreEightMonth' },
{ label: '5月', prop: 'scoreNineMonth' },
{ label: '6月', prop: 'scoreTenMonth' },
];
}
// 默认返回第一学期
return [
{ label: '9月', prop: 'scoreOneMonth' },
{ label: '10月', prop: 'scoreTwoMonth' },
{ label: '11月', prop: 'scoreThreeMonth' },
{ label: '12月', prop: 'scoreFourMonth' },
{ label: '1月', prop: 'scoreFiveMonth' },
];
});
// 格式化分数
const formatScore = (score: any) => {
if (score === null || score === undefined || score === '') return '-';
return Number(score).toFixed(2);
};
// 根据分数获取标签类型
const getScoreType = (score: any) => {
if (score === null || score === undefined) return 'info';
const num = Number(score);
if (num >= 90) return 'success';
if (num >= 80) return 'primary';
if (num >= 60) return 'warning';
return 'danger';
};
// 统计表格数据
const statisticsData = computed(() => {
if (studentList.value.length === 0) {
return [];
}
// 计算各等级人数
// 优秀:>=90良好80-89及格60-79不及格<60
let excellent = 0; // 优秀
let good = 0; // 良好
let pass = 0; // 及格
let fail = 0; // 不及格
let excellent = 0;
let good = 0;
let pass = 0;
let fail = 0;
const total = studentList.value.length;
studentList.value.forEach((student: any) => {
const score = student.score;
const score = student.scoreOneTerm;
if (score !== null && score !== undefined) {
if (score >= 90) {
if (Number(score) >= 90) {
excellent++;
} else if (score >= 80) {
} else if (Number(score) >= 80) {
good++;
} else if (score >= 60) {
} else if (Number(score) >= 60) {
pass++;
} else {
fail++;
@@ -218,52 +283,19 @@ const statisticsData = computed(() => {
}
});
// 计算比率
const excellentRate = total > 0 ? ((excellent / total) * 100).toFixed(2) + '%' : '0%';
const goodRate = total > 0 ? ((good / total) * 100).toFixed(2) + '%' : '0%';
const passRate = total > 0 ? ((pass / total) * 100).toFixed(2) + '%' : '0%';
const failRate = total > 0 ? ((fail / total) * 100).toFixed(2) + '%' : '0%';
const excellentGoodRate = total > 0 ? (((excellent + good) / total) * 100).toFixed(2) + '%' : '0%';
// 优良率 = (优秀 + 良好) / 总人数
const excellentGoodCount = excellent + good;
const excellentGoodRate = total > 0 ? ((excellentGoodCount / total) * 100).toFixed(2) + '%' : '0%';
// 获取班级名称
const classNo = studentList.value.length > 0 ? studentList.value[0].classNo || '-' : '-';
return [
{
label: '人数',
classNo: classNo,
excellent: excellent,
good: good,
pass: pass,
fail: fail,
},
{
label: '比率',
classNo: classNo,
excellent: excellentRate,
good: goodRate,
pass: passRate,
fail: failRate,
},
{
label: '优良率',
classNo: classNo,
excellent: excellentGoodRate,
good: '-',
pass: '-',
fail: '-',
},
{
label: '备注',
classNo: classNo,
excellent: '-',
good: '-',
pass: '-',
fail: '-',
},
{ label: '人数', classNo, excellent, good, pass, fail },
{ label: '比率', classNo, excellent: excellentRate, good: goodRate, pass: passRate, fail: failRate },
{ label: '优良率', classNo, excellent: excellentGoodRate, good: '-', pass: '-', fail: '-' },
{ label: '备注', classNo, excellent: '-', good: '-', pass: '-', fail: '-' },
];
});
@@ -283,34 +315,19 @@ const getDataList = async () => {
});
if (res.data && Array.isArray(res.data)) {
// 处理返回数据提取学生列表
// 根据API文档返回的是StuConductTermVO数组
// 返回数据提取学生列表
const tempList: any[] = [];
res.data.forEach((item: any) => {
// 如果返回的数据结构中有basicStudentVOList需要展开
if (item.basicStudentVOList && Array.isArray(item.basicStudentVOList) && item.basicStudentVOList.length > 0) {
if (item.basicStudentVOList && Array.isArray(item.basicStudentVOList)) {
item.basicStudentVOList.forEach((student: any) => {
tempList.push({
stuNo: student.stuNo || item.stuNo,
realName: student.realName || item.realName,
score: item.score, // 学期总评分数
...student,
classNo: item.classNo,
classCode: item.classCode,
});
});
} else {
// 直接使用item作为学生信息
tempList.push({
stuNo: item.stuNo,
realName: item.realName,
score: item.score,
classNo: item.classNo,
classCode: item.classCode,
});
}
});
studentList.value = tempList;
} else {
studentList.value = [];
@@ -338,7 +355,7 @@ const handleReset = () => {
studentList.value = [];
};
// 查看详情接口GET /stuwork/stuconduct/queryDataByStuNo按当前学年+学号拉取后筛本学期记录)
// 查看详情
const handleView = async (row: any) => {
if (!queryForm.schoolYear || !row.stuNo) {
useMessage().warning('缺少学年或学号');
@@ -354,6 +371,7 @@ const handleView = async (row: any) => {
stuNo: row.stuNo,
});
const list = Array.isArray(res.data) ? res.data : [];
// 筛选当前学期的记录
const term = queryForm.schoolTerm;
viewDetailList.value = term ? list.filter((r: any) => String(r.schoolTerm) === String(term)) : list;
} catch (_err) {
@@ -373,9 +391,7 @@ const handleSendWarning = async () => {
const { confirm } = useMessageBox();
try {
await confirm(
`确定要发送${queryForm.schoolYear}学年第${
queryForm.schoolTerm === '1' ? '一' : '二'
}学期操行考核预警吗将向班主任推送不及格学生低于60分的预警通知。`
`确定要发送${queryForm.schoolYear}学年第${queryForm.schoolTerm === '1' ? '一' : '二'}学期操行考核预警吗将向班主任推送不及格学生低于60分的预警通知。`
);
warningLoading.value = true;
@@ -394,11 +410,7 @@ const handleSendWarning = async () => {
const getSchoolYearList = async () => {
try {
const res = await queryAllSchoolYear();
if (res.data && Array.isArray(res.data)) {
schoolYearList.value = res.data;
} else {
schoolYearList.value = [];
}
schoolYearList.value = res.data && Array.isArray(res.data) ? res.data : [];
} catch (err) {
schoolYearList.value = [];
}
@@ -425,11 +437,7 @@ const getSchoolTermDict = async () => {
const getClassListData = async () => {
try {
const res = await getClassListByRole();
if (res.data && Array.isArray(res.data)) {
classList.value = res.data;
} else {
classList.value = [];
}
classList.value = res.data && Array.isArray(res.data) ? res.data : [];
} catch (err) {
classList.value = [];
}
@@ -444,15 +452,7 @@ onMounted(() => {
</script>
<style scoped lang="scss">
.layout-padding {
.layout-padding-auto {
.layout-padding-view {
.el-row {
margin-bottom: 20px;
}
}
}
}
@import '/@/assets/styles/modern-page.scss';
.view-summary {
margin-bottom: 16px;
@@ -462,19 +462,4 @@ onMounted(() => {
font-weight: 600;
color: #303133;
}
// 确保页面可以滚动
.layout-padding {
height: 100%;
overflow-y: auto;
.layout-padding-auto {
height: 100%;
.layout-padding-view {
height: 100%;
overflow-y: auto;
}
}
}
</style>

View File

@@ -1,9 +1,17 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<!-- 搜索表单 -->
<el-row v-show="showSearch">
<el-form :model="queryForm" ref="searchFormRef" :inline="true" @keyup.enter="getDataList">
<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="queryForm" ref="searchFormRef" :inline="true" @keyup.enter="getDataList" class="search-form">
<el-form-item label="学年" prop="schoolYear">
<el-select v-model="queryForm.schoolYear" placeholder="请选择学年" clearable filterable style="width: 200px">
<el-option v-for="item in schoolYearList" :key="item.year" :label="item.year" :value="item.year"> </el-option>
@@ -19,15 +27,25 @@
<el-button icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-row>
</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>
</template>
<!-- 统计表格 -->
<el-row style="margin-bottom: 20px">
<el-table
:data="statisticsData"
v-loading="loading"
border
style="width: 100%"
stripe
style="width: 100%; margin-bottom: 20px"
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
>
@@ -38,60 +56,106 @@
<el-table-column prop="pass" label="及格" min-width="100" align="center" />
<el-table-column prop="fail" label="不及格" min-width="100" align="center" />
</el-table>
</el-row>
<!-- 学生列表表格 -->
<el-row>
<el-table
:data="studentList"
v-loading="loading"
border
:max-height="600"
stripe
style="width: 100%"
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="stuNo" label="学号" show-overflow-tooltip align="center" />
<el-table-column prop="realName" label="姓名" show-overflow-tooltip align="center" />
<el-table-column prop="score" label="学年总评" show-overflow-tooltip align="center">
<el-table-column type="index" label="序号" width="70" align="center" fixed="left" />
<el-table-column prop="stuNo" label="学号" min-width="120" show-overflow-tooltip align="center" fixed="left" />
<el-table-column prop="realName" label="姓名" min-width="80" show-overflow-tooltip align="center" fixed="left" />
<!-- 第一学期月份 -->
<el-table-column label="第一学期" align="center">
<el-table-column v-for="(month, index) in firstTermMonths" :key="'first-' + index" :label="month.label" min-width="60" align="center">
<template #default="scope">
<span>{{ scope.row.score !== null && scope.row.score !== undefined ? scope.row.score.toFixed(2) : '-' }}</span>
<span>{{ formatScore(scope.row[month.prop]) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="150" align="center" fixed="right">
<el-table-column label="学期评" min-width="70" align="center">
<template #default="scope">
<el-button icon="View" text type="primary" @click="handleView(scope.row)"> 查看 </el-button>
<el-tag :type="getScoreType(scope.row.scoreOneTerm)" size="small" effect="plain">
{{ formatScore(scope.row.scoreOneTerm) }}
</el-tag>
</template>
</el-table-column>
</el-table-column>
<!-- 第二学期月份 -->
<el-table-column label="第二学期" align="center">
<el-table-column v-for="(month, index) in secondTermMonths" :key="'second-' + index" :label="month.label" min-width="60" align="center">
<template #default="scope">
<span>{{ formatScore(scope.row[month.prop]) }}</span>
</template>
</el-table-column>
<el-table-column label="学期评" min-width="70" align="center">
<template #default="scope">
<el-tag :type="getScoreType(scope.row.scoreTwoTerm)" size="small" effect="plain">
{{ formatScore(scope.row.scoreTwoTerm) }}
</el-tag>
</template>
</el-table-column>
</el-table-column>
<!-- 学年总评 -->
<el-table-column prop="scoreYear" label="学年总评" min-width="100" align="center" fixed="right">
<template #default="scope">
<el-tag :type="getScoreType(scope.row.scoreYear)" size="small" effect="dark">
{{ formatScore(scope.row.scoreYear) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="100" align="center" fixed="right">
<template #default="scope">
<el-button icon="View" link type="primary" @click="handleView(scope.row)">查看</el-button>
</template>
</el-table-column>
</el-table>
</el-row>
</el-card>
</div>
<!-- 查看详情弹窗接口queryDataByStuNo 通过学年学号查看详情 -->
<el-dialog v-model="viewDialogVisible" title="学年操行考核详情" width="800px" destroy-on-close @close="viewDetailList = []">
<!-- 查看详情弹窗 -->
<el-dialog v-model="viewDialogVisible" title="学年操行考核详情" width="900px" destroy-on-close @close="viewDetailList = []">
<div v-if="viewRow" class="view-summary">
<el-descriptions :column="2" border size="small">
<el-descriptions :column="3" border size="small">
<el-descriptions-item label="学号">{{ viewRow.stuNo }}</el-descriptions-item>
<el-descriptions-item label="姓名">{{ viewRow.realName }}</el-descriptions-item>
<el-descriptions-item label="学年">{{ queryForm.schoolYear }}</el-descriptions-item>
<el-descriptions-item label="学年总评">
{{ viewRow.score != null && viewRow.score !== undefined ? Number(viewRow.score).toFixed(2) : '-' }}
<el-tag :type="getScoreType(viewRow.scoreYear)" size="small">
{{ formatScore(viewRow.scoreYear) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="第一学期">
<el-tag :type="getScoreType(viewRow.scoreOneTerm)" size="small" effect="plain">
{{ formatScore(viewRow.scoreOneTerm) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="第二学期">
<el-tag :type="getScoreType(viewRow.scoreTwoTerm)" size="small" effect="plain">
{{ formatScore(viewRow.scoreTwoTerm) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="学年">{{ queryForm.schoolYear }}</el-descriptions-item>
</el-descriptions>
</div>
<div class="view-detail-title">考核记录</div>
<el-table
:data="viewDetailList"
v-loading="viewLoading"
border
stripe
size="small"
max-height="400"
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
>
:header-cell-style="tableStyle.headerCellStyle">
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="schoolTerm" label="学期" width="80" align="center" show-overflow-tooltip />
<el-table-column prop="recordDate" label="考核日期" width="110" align="center" show-overflow-tooltip />
<el-table-column prop="schoolTerm" label="学期" width="80" align="center">
<template #default="scope">
<el-tag size="small" effect="plain">{{ scope.row.schoolTerm === '1' ? '第一学期' : '第二学期' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="recordDate" label="考核日期" width="120" align="center" show-overflow-tooltip />
<el-table-column prop="conductType" label="类型" width="80" align="center">
<template #default="scope">
<el-tag :type="scope.row.conductType === '1' ? 'success' : 'danger'" size="small">
@@ -104,8 +168,8 @@
{{ scope.row.score != null && scope.row.score !== undefined ? Number(scope.row.score) : '-' }}
</template>
</el-table-column>
<el-table-column prop="description" label="情况记录" min-width="140" show-overflow-tooltip />
<el-table-column prop="remarks" label="备注" min-width="100" show-overflow-tooltip />
<el-table-column prop="description" label="情况记录" min-width="150" show-overflow-tooltip />
<el-table-column prop="remarks" label="备注" min-width="120" show-overflow-tooltip />
</el-table>
<template v-if="viewDetailList.length === 0 && !viewLoading">
<el-empty description="暂无考核记录" :image-size="80" />
@@ -120,11 +184,12 @@ import { getStuConductYear, queryDataByStuNo } from '/@/api/stuwork/stuconduct';
import { getClassListByRole } from '/@/api/basic/basicclass';
import { queryAllSchoolYear } from '/@/api/basic/basicyear';
import { useMessage } from '/@/hooks/message';
import { Search, Document } from '@element-plus/icons-vue';
// 表格样式 - 在组件内部定义,不从外部导入
// 表格样式
const tableStyle = {
cellStyle: { padding: '8px 0' },
headerCellStyle: { background: '#f5f7fa', color: '#606266', fontWeight: 'bold' },
cellStyle: { padding: '8px 0', textAlign: 'center' },
headerCellStyle: { background: '#f5f7fa', color: '#606266', fontWeight: 'bold', textAlign: 'center' },
};
// 定义变量内容
@@ -145,28 +210,60 @@ const queryForm = reactive({
classCode: '',
});
// 第一学期月份列9月-1月
const firstTermMonths = [
{ label: '9月', prop: 'scoreOneMonth' },
{ label: '10月', prop: 'scoreTwoMonth' },
{ label: '11月', prop: 'scoreThreeMonth' },
{ label: '12月', prop: 'scoreFourMonth' },
{ label: '1月', prop: 'scoreFiveMonth' },
];
// 第二学期月份列2月-6月
const secondTermMonths = [
{ label: '2月', prop: 'scoreSixMonth' },
{ label: '3月', prop: 'scoreSevenMonth' },
{ label: '4月', prop: 'scoreEightMonth' },
{ label: '5月', prop: 'scoreNineMonth' },
{ label: '6月', prop: 'scoreTenMonth' },
];
// 格式化分数
const formatScore = (score: any) => {
if (score === null || score === undefined || score === '') return '-';
return Number(score).toFixed(2);
};
// 根据分数获取标签类型
const getScoreType = (score: any) => {
if (score === null || score === undefined) return 'info';
const num = Number(score);
if (num >= 90) return 'success';
if (num >= 80) return 'primary';
if (num >= 60) return 'warning';
return 'danger';
};
// 统计表格数据
const statisticsData = computed(() => {
if (studentList.value.length === 0) {
return [];
}
// 计算各等级人数
// 优秀:>=90良好80-89及格60-79不及格<60
let excellent = 0; // 优秀
let good = 0; // 良好
let pass = 0; // 及格
let fail = 0; // 不及格
let excellent = 0;
let good = 0;
let pass = 0;
let fail = 0;
const total = studentList.value.length;
studentList.value.forEach((student: any) => {
const score = student.score;
const score = student.scoreYear;
if (score !== null && score !== undefined) {
if (score >= 90) {
if (Number(score) >= 90) {
excellent++;
} else if (score >= 80) {
} else if (Number(score) >= 80) {
good++;
} else if (score >= 60) {
} else if (Number(score) >= 60) {
pass++;
} else {
fail++;
@@ -174,52 +271,19 @@ const statisticsData = computed(() => {
}
});
// 计算比率
const excellentRate = total > 0 ? ((excellent / total) * 100).toFixed(2) + '%' : '0%';
const goodRate = total > 0 ? ((good / total) * 100).toFixed(2) + '%' : '0%';
const passRate = total > 0 ? ((pass / total) * 100).toFixed(2) + '%' : '0%';
const failRate = total > 0 ? ((fail / total) * 100).toFixed(2) + '%' : '0%';
const excellentGoodRate = total > 0 ? (((excellent + good) / total) * 100).toFixed(2) + '%' : '0%';
// 优良率 = (优秀 + 良好) / 总人数
const excellentGoodCount = excellent + good;
const excellentGoodRate = total > 0 ? ((excellentGoodCount / total) * 100).toFixed(2) + '%' : '0%';
// 获取班级名称
const classNo = studentList.value.length > 0 ? studentList.value[0].classNo || '-' : '-';
return [
{
label: '人数',
classNo: classNo,
excellent: excellent,
good: good,
pass: pass,
fail: fail,
},
{
label: '比率',
classNo: classNo,
excellent: excellentRate,
good: goodRate,
pass: passRate,
fail: failRate,
},
{
label: '优良率',
classNo: classNo,
excellent: excellentGoodRate,
good: '-',
pass: '-',
fail: '-',
},
{
label: '备注',
classNo: classNo,
excellent: '-',
good: '-',
pass: '-',
fail: '-',
},
{ label: '人数', classNo, excellent, good, pass, fail },
{ label: '比率', classNo, excellent: excellentRate, good: goodRate, pass: passRate, fail: failRate },
{ label: '优良率', classNo, excellent: excellentGoodRate, good: '-', pass: '-', fail: '-' },
{ label: '备注', classNo, excellent: '-', good: '-', pass: '-', fail: '-' },
];
});
@@ -238,34 +302,18 @@ const getDataList = async () => {
});
if (res.data && Array.isArray(res.data)) {
// 处理返回的数据,提取学生列表
// 根据API文档返回的是StuConductYearVO数组
const tempList: any[] = [];
res.data.forEach((item: any) => {
// 如果返回的数据结构中有basicStudentVOList需要展开
if (item.basicStudentVOList && Array.isArray(item.basicStudentVOList) && item.basicStudentVOList.length > 0) {
if (item.basicStudentVOList && Array.isArray(item.basicStudentVOList)) {
item.basicStudentVOList.forEach((student: any) => {
tempList.push({
stuNo: student.stuNo || item.stuNo,
realName: student.realName || item.realName,
score: item.score, // 学年总评分数
...student,
classNo: item.classNo,
classCode: item.classCode,
});
});
} else {
// 直接使用item作为学生信息
tempList.push({
stuNo: item.stuNo,
realName: item.realName,
score: item.score,
classNo: item.classNo,
classCode: item.classCode,
});
}
});
studentList.value = tempList;
} else {
studentList.value = [];
@@ -285,7 +333,7 @@ const handleReset = () => {
studentList.value = [];
};
// 查看详情接口GET /stuwork/stuconduct/queryDataByStuNo通过学年学号查看详情
// 查看详情
const handleView = async (row: any) => {
if (!queryForm.schoolYear || !row.stuNo) {
useMessage().warning('缺少学年或学号');
@@ -312,11 +360,7 @@ const handleView = async (row: any) => {
const getSchoolYearList = async () => {
try {
const res = await queryAllSchoolYear();
if (res.data && Array.isArray(res.data)) {
schoolYearList.value = res.data;
} else {
schoolYearList.value = [];
}
schoolYearList.value = res.data && Array.isArray(res.data) ? res.data : [];
} catch (err) {
schoolYearList.value = [];
}
@@ -326,11 +370,7 @@ const getSchoolYearList = async () => {
const getClassListData = async () => {
try {
const res = await getClassListByRole();
if (res.data && Array.isArray(res.data)) {
classList.value = res.data;
} else {
classList.value = [];
}
classList.value = res.data && Array.isArray(res.data) ? res.data : [];
} catch (err) {
classList.value = [];
}
@@ -344,15 +384,7 @@ onMounted(() => {
</script>
<style scoped lang="scss">
.layout-padding {
.layout-padding-auto {
.layout-padding-view {
.el-row {
margin-bottom: 20px;
}
}
}
}
@import '/@/assets/styles/modern-page.scss';
.view-summary {
margin-bottom: 16px;
@@ -362,19 +394,4 @@ onMounted(() => {
font-weight: 600;
color: #303133;
}
// 确保页面可以滚动
.layout-padding {
height: 100%;
overflow-y: auto;
.layout-padding-auto {
height: 100%;
.layout-padding-view {
height: 100%;
overflow-y: auto;
}
}
}
</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>

View File

@@ -0,0 +1,271 @@
<template>
<el-dialog
:title="form.id ? '编辑异动规则' : '新增异动规则'"
v-model="visible"
:close-on-click-modal="false"
draggable
width="600px">
<el-form
ref="dataFormRef"
:model="form"
:rules="dataRules"
label-width="120px"
v-loading="loading">
<el-row :gutter="24">
<el-col :span="24" class="mb20">
<el-form-item label="规则名称" prop="ruleName">
<el-input
v-model="form.ruleName"
placeholder="请输入规则名称"
clearable
maxlength="100" />
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="异动类型" prop="turnoverType">
<el-select
v-model="form.turnoverType"
placeholder="请选择异动类型"
clearable
style="width: 100%">
<el-option
v-for="item in turnoverTypeList"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="最短间隔" prop="minIntervalDays">
<el-input-number
v-model="form.minIntervalDays"
:min="0"
:max="365"
placeholder="天"
style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="最长持续" prop="maxDurationDays">
<el-input-number
v-model="form.maxDurationDays"
:min="0"
:max="3650"
placeholder="天"
style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="提醒天数" prop="remindDays">
<el-input-number
v-model="form.remindDays"
:min="0"
:max="365"
placeholder="天"
style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="排序" prop="sort">
<el-input-number
v-model="form.sort"
:min="0"
:max="999"
style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="是否启用" prop="isActive">
<el-switch
v-model="form.isActive"
active-value="1"
inactive-value="0"
active-text="启用"
inactive-text="禁用" />
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="规则描述" prop="ruleDescription">
<el-input
v-model="form.ruleDescription"
type="textarea"
:rows="3"
placeholder="请输入规则描述"
maxlength="500"
show-word-limit />
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="备注" prop="remarks">
<el-input
v-model="form.remarks"
type="textarea"
:rows="2"
placeholder="请输入备注"
maxlength="200" />
</el-form-item>
</el-col>
</el-row>
</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="StuTurnoverRuleFormDialog">
import { ref, reactive, nextTick, onMounted } from 'vue'
import { useMessage } from '/@/hooks/message'
import { addObj, editObj, getDetail } from '/@/api/stuwork/stuturnoverrule'
import { getDicts } from '/@/api/admin/dict'
const emit = defineEmits(['refresh'])
// 定义变量内容
const dataFormRef = ref()
const visible = ref(false)
const loading = ref(false)
const turnoverTypeList = ref<any[]>([])
// 提交表单数据
const form = reactive({
id: '',
ruleName: '',
turnoverType: '',
minIntervalDays: 0,
maxDurationDays: 0,
remindDays: 0,
ruleDescription: '',
isActive: '1',
sort: 0,
remarks: ''
})
// 定义校验规则
const dataRules = reactive({
ruleName: [
{ required: true, message: '请输入规则名称', trigger: 'blur' }
],
turnoverType: [
{ required: true, message: '请选择异动类型', trigger: 'change' }
]
})
// 打开弹窗
const openDialog = async (row?: any) => {
visible.value = true
// 重置表单数据
nextTick(() => {
dataFormRef.value?.resetFields()
form.id = ''
form.ruleName = ''
form.turnoverType = ''
form.minIntervalDays = 0
form.maxDurationDays = 0
form.remindDays = 0
form.ruleDescription = ''
form.isActive = '1'
form.sort = 0
form.remarks = ''
// 编辑时填充数据
if (row?.id) {
loading.value = true
getDetail(row.id).then((res: any) => {
if (res.data) {
form.id = res.data.id
form.ruleName = res.data.ruleName || ''
form.turnoverType = res.data.turnoverType || ''
form.minIntervalDays = res.data.minIntervalDays || 0
form.maxDurationDays = res.data.maxDurationDays || 0
form.remindDays = res.data.remindDays || 0
form.ruleDescription = res.data.ruleDescription || ''
form.isActive = res.data.isActive || '1'
form.sort = res.data.sort || 0
form.remarks = res.data.remarks || ''
}
}).finally(() => {
loading.value = false
})
}
})
}
// 提交表单
const onSubmit = async () => {
if (!dataFormRef.value) return
await dataFormRef.value.validate(async (valid: boolean) => {
if (!valid) return
loading.value = true
try {
const submitData = {
id: form.id || undefined,
ruleName: form.ruleName,
turnoverType: form.turnoverType,
minIntervalDays: form.minIntervalDays,
maxDurationDays: form.maxDurationDays,
remindDays: form.remindDays,
ruleDescription: form.ruleDescription,
isActive: form.isActive,
sort: form.sort,
remarks: form.remarks
}
if (form.id) {
await editObj(submitData)
useMessage().success('编辑成功')
} else {
await addObj(submitData)
useMessage().success('新增成功')
}
visible.value = false
emit('refresh')
} catch (err: any) {
useMessage().error(err.msg || (form.id ? '编辑失败' : '新增失败'))
} finally {
loading.value = false
}
})
}
// 获取异动类型字典
const getTurnoverTypeDict = async () => {
try {
const res = await getDicts('turnover_type')
if (res.data) {
turnoverTypeList.value = Array.isArray(res.data) ? res.data.map((item: any) => ({
label: item.label || item.dictLabel || item.name,
value: item.value || item.dictValue || item.code
})) : []
}
} catch (err) {
turnoverTypeList.value = []
}
}
// 初始化
onMounted(() => {
getTurnoverTypeDict()
})
// 暴露方法给父组件
defineExpose({
openDialog
})
</script>

View File

@@ -0,0 +1,337 @@
<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="ruleName">
<el-input
v-model="searchForm.ruleName"
placeholder="请输入规则名称"
clearable
style="width: 200px" />
</el-form-item>
<el-form-item label="异动类型" prop="turnoverType">
<el-select
v-model="searchForm.turnoverType"
placeholder="请选择异动类型"
clearable
style="width: 200px">
<el-option
v-for="item in turnoverTypeList"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="是否启用" prop="isActive">
<el-select
v-model="searchForm.isActive"
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"
v-auth="'stuwork_stuturnoverrule_add'"
@click="formDialogRef.openDialog()">
新增规则
</el-button>
<right-toolbar
v-model:showSearch="showSearch"
class="ml10"
@queryTable="getDataList">
<TableColumnControl
ref="columnControlRef"
:columns="tableColumns"
v-model="visibleColumns"
trigger-type="default"
trigger-circle
@change="handleColumnChange"
@order-change="handleColumnOrderChange"
>
<template #trigger>
<el-tooltip class="item" effect="dark" content="列设置" placement="top">
<el-button circle style="margin-left: 0;">
<el-icon><Menu /></el-icon>
</el-button>
</el-tooltip>
</template>
</TableColumnControl>
</right-toolbar>
</div>
</div>
</template>
<!-- 表格 -->
<el-table
:data="state.dataList"
v-loading="state.loading"
stripe
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
class="modern-table">
<el-table-column type="index" label="序号" width="70" align="center">
<template #header>
<el-icon><List /></el-icon>
</template>
<template #default="{ $index }">
{{ $index + 1 + ((state.pagination?.current || 1) - 1) * (state.pagination?.size || 10) }}
</template>
</el-table-column>
<template v-for="col in visibleColumnsSorted" :key="col.prop || col.label">
<el-table-column
v-if="checkColumnVisible(col.prop || '') && col.prop !== '操作'"
:prop="col.prop"
:label="col.label"
show-overflow-tooltip
align="center"
:min-width="col.minWidth"
:width="col.width">
<template #header>
<el-icon><component :is="columnConfigMap[col.prop]?.icon || Document" /></el-icon>
<span style="margin-left: 4px">{{ col.label }}</span>
</template>
<!-- 异动类型列特殊模板 -->
<template v-if="col.prop === 'turnoverType'" #default="scope">
<el-tag size="small" type="warning" effect="plain">
{{ formatTurnoverType(scope.row.turnoverType) }}
</el-tag>
</template>
<!-- 是否启用列特殊模板 -->
<template v-else-if="col.prop === 'isActive'" #default="scope">
<el-tag size="small" :type="scope.row.isActive === '1' ? 'success' : 'danger'" effect="plain">
{{ scope.row.isActive === '1' ? '启用' : '禁用' }}
</el-tag>
</template>
<!-- 天数相关列 -->
<template v-else-if="col.prop === 'minIntervalDays'" #default="scope">
<span>{{ scope.row.minIntervalDays ?? '-' }} </span>
</template>
<template v-else-if="col.prop === 'maxDurationDays'" #default="scope">
<span>{{ scope.row.maxDurationDays ?? '-' }} </span>
</template>
<template v-else-if="col.prop === 'remindDays'" #default="scope">
<span>{{ scope.row.remindDays ?? '-' }} </span>
</template>
</el-table-column>
</template>
<el-table-column label="操作" width="160" align="center" fixed="right">
<template #header>
<el-icon><Setting /></el-icon>
<span style="margin-left: 4px">操作</span>
</template>
<template #default="scope">
<el-button
icon="Edit"
link
type="primary"
v-auth="'stuwork_stuturnoverrule_edit'"
@click="handleEdit(scope.row)">
编辑
</el-button>
<el-button
icon="Delete"
link
type="danger"
v-auth="'stuwork_stuturnoverrule_del'"
@click="handleDelete(scope.row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-wrapper">
<pagination
@size-change="sizeChangeHandle"
@current-change="currentChangeHandle"
v-bind="state.pagination" />
</div>
</el-card>
</div>
<!-- 新增/编辑表单弹窗 -->
<form-dialog ref="formDialogRef" @refresh="getDataList" />
</div>
</template>
<script setup lang="ts" name="StuTurnoverRule">
import { reactive, ref, onMounted, nextTick } from 'vue'
import { useRoute } from 'vue-router'
import { BasicTableProps, useTable } from "/@/hooks/table";
import { fetchList, delObj } from "/@/api/stuwork/stuturnoverrule";
import { getDicts } from "/@/api/admin/dict";
import { useMessage, useMessageBox } from "/@/hooks/message";
import TableColumnControl from '/@/components/TableColumnControl/index.vue'
import FormDialog from './form.vue'
import { List, Setting, Document, Collection, Menu, Search, Clock } from '@element-plus/icons-vue'
import { useTableColumnControl } from '/@/hooks/tableColumn'
// 定义变量内容
const route = useRoute()
const searchFormRef = ref()
const columnControlRef = ref<any>()
const showSearch = ref(true)
const turnoverTypeList = ref<any[]>([])
const formDialogRef = ref()
// 表格列配置
const tableColumns = [
{ prop: 'ruleName', label: '规则名称', minWidth: 150 },
{ prop: 'turnoverType', label: '异动类型', width: 120 },
{ prop: 'minIntervalDays', label: '最短间隔', width: 100 },
{ prop: 'maxDurationDays', label: '最长持续', width: 100 },
{ prop: 'remindDays', label: '提醒天数', width: 100 },
{ prop: 'sort', label: '排序', width: 80 },
{ prop: 'isActive', label: '状态', width: 80 },
{ prop: 'ruleDescription', label: '规则描述', minWidth: 200 },
{ prop: 'remarks', label: '备注', minWidth: 150 }
]
// 列配置映射(用于图标显示)
const columnConfigMap: Record<string, { icon: any }> = {
ruleName: { icon: Document },
turnoverType: { icon: Collection },
minIntervalDays: { icon: Clock },
maxDurationDays: { icon: Clock },
remindDays: { icon: Clock },
sort: { icon: Setting },
isActive: { icon: Setting },
ruleDescription: { icon: Document },
remarks: { icon: Document }
}
// 使用表格列控制hook
const {
visibleColumns,
visibleColumnsSorted,
checkColumnVisible,
handleColumnChange,
handleColumnOrderChange
} = useTableColumnControl(tableColumns, route.path)
// 搜索表单
const searchForm = reactive({
ruleName: '',
turnoverType: '',
isActive: ''
})
// 配置 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 formatTurnoverType = (value: string) => {
if (!value) return '-'
const item = turnoverTypeList.value.find((item: any) => item.value === value)
return item ? item.label : value
}
// 查询
const handleSearch = () => {
getDataList()
}
// 重置
const handleReset = () => {
searchFormRef.value?.resetFields()
searchForm.ruleName = ''
searchForm.turnoverType = ''
searchForm.isActive = ''
getDataList()
}
// 编辑
const handleEdit = (row: any) => {
formDialogRef.value?.openDialog(row)
}
// 删除
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 || '删除失败')
}
}
}
// 获取异动类型字典
const getTurnoverTypeDict = async () => {
try {
const res = await getDicts('turnover_type')
if (res.data) {
turnoverTypeList.value = Array.isArray(res.data) ? res.data.map((item: any) => ({
label: item.label || item.dictLabel || item.name,
value: item.value || item.dictValue || item.code
})) : []
}
} catch (err) {
turnoverTypeList.value = []
}
}
// 初始化
onMounted(() => {
getTurnoverTypeDict()
nextTick(() => {
if (visibleColumns.value.length === 0) {
}
})
})
</script>
<style scoped lang="scss">
@import '/@/assets/styles/modern-page.scss';
</style>

View File

@@ -20,10 +20,10 @@
filterable
style="width: 200px">
<el-option
v-for="item in schoolYearList"
:key="item.year"
:label="item.year"
:value="item.year">
v-for="(item, index) in schoolYearList"
:key="item.year || item.id || index"
:label="item.year || item.schoolYear || item.name || item"
:value="item.year || item.schoolYear || item.name || item">
</el-option>
</el-select>
</el-form-item>
@@ -303,23 +303,38 @@ const searchForm = reactive({
dateRange: null as [string, string] | null
})
// 配置 useTable接口返回 data.tableData.records / data.tableData.total,需包装以适配 hook 的 res.data[props.item] 取数
// 配置 useTable接口返回 data.tableData.records / data.tableData.total
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: searchForm,
pageList: async (params: any) => {
const res = await fetchList(params)
const data = res?.data
const tableData = data?.tableData
// 处理查询参数
const queryParams: any = { ...params }
// 处理日期范围
if (searchForm.dateRange && searchForm.dateRange.length === 2) {
queryParams.dateRangeStr = `${searchForm.dateRange[0]},${searchForm.dateRange[1]}`
}
delete queryParams.dateRange
// 处理学年数组
if (queryParams.schoolYear) {
queryParams.schoolYear = [queryParams.schoolYear]
}
const res = await fetchList(queryParams)
// 将嵌套的 tableData 提升到 data 层级,适配 useTable hook
if (res.data && res.data.tableData) {
return {
...res,
data: {
'tableData.records': tableData?.records ?? [],
'tableData.total': tableData?.total ?? data?.total ?? 0
records: res.data.tableData.records || [],
total: res.data.tableData.total || 0
}
}
}
return res
},
props: {
item: 'tableData.records',
totalCount: 'tableData.total'
item: 'records',
totalCount: 'total'
},
createdIsNeed: true
})
@@ -334,20 +349,6 @@ const {
// 查询
const handleSearch = () => {
// 处理日期范围
const params: any = { ...searchForm }
if (searchForm.dateRange && searchForm.dateRange.length === 2) {
params.dateRangeStr = `${searchForm.dateRange[0]},${searchForm.dateRange[1]}`
}
delete params.dateRange
// 处理学年数组
if (params.schoolYear) {
params.schoolYear = [params.schoolYear]
}
// 更新查询参数
Object.assign(searchForm, params)
getDataList()
}
@@ -394,10 +395,20 @@ const getDeptListData = async () => {
const getSchoolYearList = async () => {
try {
const res = await queryAllSchoolYear()
if (res.data && Array.isArray(res.data)) {
if (res.data) {
// 兼容多种数据格式
if (Array.isArray(res.data)) {
schoolYearList.value = res.data
} else if (res.data.records && Array.isArray(res.data.records)) {
schoolYearList.value = res.data.records
} else {
schoolYearList.value = []
}
} else {
schoolYearList.value = []
}
} catch (err) {
console.error('获取学年列表失败', err)
schoolYearList.value = []
}
}

View File

@@ -19,10 +19,10 @@
filterable
style="width: 100%">
<el-option
v-for="item in schoolYearList"
:key="item.year"
:label="item.year"
:value="item.year">
v-for="(item, index) in schoolYearList"
:key="item.year || item.id || index"
:label="item.year || item.schoolYear || item.name || item"
:value="item.year || item.schoolYear || item.name || item">
</el-option>
</el-select>
</el-form-item>
@@ -34,9 +34,9 @@
style="width: 100%">
<el-option
v-for="item in schoolTermList"
:key="item.value"
:label="item.label"
:value="item.value">
:key="item.value || item.dictValue"
:label="item.label || item.dictLabel || item.name"
:value="item.value || item.dictValue">
</el-option>
</el-select>
</el-form-item>
@@ -50,16 +50,18 @@
<el-form-item label="教师" prop="teacherNo">
<el-select
v-model="form.teacherNo"
placeholder="请选择教师"
placeholder="请输入姓名或工号搜索"
clearable
filterable
style="width: 100%"
@search="handleTeacherSearch">
remote
:remote-method="handleTeacherSearch"
:loading="teacherLoading"
style="width: 100%">
<el-option
v-for="item in teacherList"
:key="item.teacherNo"
:label="`${item.realName}(${item.teacherNo})`"
:value="item.teacherNo">
:key="item.teacherNo || item.id"
:label="`${item.realName || item.name}(${item.teacherNo || item.id})`"
:value="item.teacherNo || item.id">
</el-option>
</el-select>
</el-form-item>
@@ -87,6 +89,7 @@ const emit = defineEmits(['refresh'])
const dataFormRef = ref()
const visible = ref(false)
const loading = ref(false)
const teacherLoading = ref(false)
const schoolYearList = ref<any[]>([])
const schoolTermList = ref<any[]>([])
const teacherList = ref<any[]>([])
@@ -117,17 +120,29 @@ const dataRules = {
// 教师搜索
const handleTeacherSearch = async (keyword: string) => {
if (!keyword) {
if (!keyword || keyword.length < 1) {
teacherList.value = []
return
}
teacherLoading.value = true
try {
const res = await getTeacherInfoCommon({ searchKeywords: keyword })
if (res.data && Array.isArray(res.data)) {
if (res.data) {
if (Array.isArray(res.data)) {
teacherList.value = res.data
} else if (res.data.records && Array.isArray(res.data.records)) {
teacherList.value = res.data.records
} else {
teacherList.value = []
}
} else {
teacherList.value = []
}
} catch (err) {
console.error('获取教师列表失败', err)
teacherList.value = []
} finally {
teacherLoading.value = false
}
}
@@ -174,10 +189,19 @@ const onSubmit = async () => {
const getSchoolYearList = async () => {
try {
const res = await queryAllSchoolYear()
if (res.data && Array.isArray(res.data)) {
if (res.data) {
if (Array.isArray(res.data)) {
schoolYearList.value = res.data
} else if (res.data.records && Array.isArray(res.data.records)) {
schoolYearList.value = res.data.records
} else {
schoolYearList.value = []
}
} else {
schoolYearList.value = []
}
} catch (err) {
console.error('获取学年列表失败', err)
schoolYearList.value = []
}
}
@@ -187,9 +211,15 @@ const getSchoolTermList = async () => {
try {
const res = await getDicts('school_term')
if (res.data && Array.isArray(res.data)) {
schoolTermList.value = res.data
schoolTermList.value = res.data.map((item: any) => ({
label: item.label ?? item.dictLabel ?? item.name,
value: item.value ?? item.dictValue
}))
} else {
schoolTermList.value = []
}
} catch (err) {
console.error('获取学期列表失败', err)
schoolTermList.value = []
}
}

View File

@@ -0,0 +1,262 @@
<template>
<el-dialog
:title="form.id ? '编辑水电月明细' : '新增水电月明细'"
v-model="visible"
:close-on-click-modal="false"
draggable
width="550px">
<el-form
ref="dataFormRef"
:model="form"
:rules="dataRules"
label-width="100px"
v-loading="loading">
<el-row :gutter="24">
<el-col :span="24" class="mb20">
<el-form-item label="房间号" prop="roomNo">
<el-input
v-model="form.roomNo"
placeholder="请输入房间号"
clearable
maxlength="50" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="年份" prop="year">
<el-date-picker
v-model="form.year"
type="year"
placeholder="选择年份"
value-format="YYYY"
style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="月份" prop="month">
<el-select
v-model="form.month"
placeholder="请选择月份"
style="width: 100%">
<el-option v-for="m in 12" :key="m" :label="m + '月'" :value="m" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="类型" prop="flag">
<el-select
v-model="form.flag"
placeholder="请选择类型"
style="width: 100%"
@change="handleFlagChange">
<el-option label="用电" :value="2" />
<el-option label="用水" :value="4" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="水表类型" prop="meterNum">
<el-select
v-model="form.meterNum"
placeholder="请选择水表类型"
style="width: 100%"
:disabled="form.flag !== 4">
<el-option label="冷水" :value="10" />
<el-option label="热水" :value="11" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="用量" prop="subiMonthSum">
<el-input-number
v-model="form.subiMonthSum"
:min="0"
:precision="2"
:controls="false"
placeholder="请输入用量"
style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="费用" prop="subWatFlagSum">
<el-input-number
v-model="form.subWatFlagSum"
:min="0"
:precision="2"
:controls="false"
placeholder="请输入费用"
style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="日期" prop="waterDate">
<el-date-picker
v-model="form.waterDate"
type="date"
placeholder="选择日期"
value-format="YYYY-MM-DD"
style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="备注" prop="remarks">
<el-input
v-model="form.remarks"
type="textarea"
:rows="3"
placeholder="请输入备注"
maxlength="200" />
</el-form-item>
</el-col>
</el-row>
</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="WaterMonthReportFormDialog">
import { ref, reactive, nextTick } from 'vue'
import { useMessage } from '/@/hooks/message'
import { addObj, editObj, getDetail } from '/@/api/stuwork/watermonthreport'
const emit = defineEmits(['refresh'])
// 定义变量内容
const dataFormRef = ref()
const visible = ref(false)
const loading = ref(false)
// 提交表单数据
const form = reactive({
id: '',
roomNo: '',
year: '',
month: '',
flag: undefined as number | undefined,
meterNum: undefined as number | undefined,
subiMonthSum: undefined as number | undefined,
subWatFlagSum: undefined as number | undefined,
waterDate: '',
remarks: ''
})
// 定义校验规则
const dataRules = reactive({
roomNo: [
{ required: true, message: '请输入房间号', trigger: 'blur' }
],
year: [
{ required: true, message: '请选择年份', trigger: 'change' }
],
month: [
{ required: true, message: '请选择月份', trigger: 'change' }
],
flag: [
{ required: true, message: '请选择类型', trigger: 'change' }
]
})
// 类型变化处理
const handleFlagChange = () => {
if (form.flag !== 4) {
form.meterNum = undefined
}
}
// 打开弹窗
const openDialog = async (row?: any) => {
visible.value = true
// 重置表单数据
nextTick(() => {
dataFormRef.value?.resetFields()
form.id = ''
form.roomNo = ''
form.year = ''
form.month = ''
form.flag = undefined
form.meterNum = undefined
form.subiMonthSum = undefined
form.subWatFlagSum = undefined
form.waterDate = ''
form.remarks = ''
// 编辑时填充数据
if (row?.id) {
loading.value = true
getDetail(row.id).then((res: any) => {
if (res.data) {
form.id = res.data.id
form.roomNo = res.data.roomNo || ''
form.year = res.data.year ? String(res.data.year) : ''
form.month = res.data.month
form.flag = res.data.flag
form.meterNum = res.data.meterNum
form.subiMonthSum = res.data.subiMonthSum
form.subWatFlagSum = res.data.subWatFlagSum
form.waterDate = res.data.waterDate || ''
form.remarks = res.data.remarks || ''
}
}).finally(() => {
loading.value = false
})
}
})
}
// 提交表单
const onSubmit = async () => {
if (!dataFormRef.value) return
await dataFormRef.value.validate(async (valid: boolean) => {
if (!valid) return
loading.value = true
try {
const submitData = {
id: form.id || undefined,
roomNo: form.roomNo,
year: form.year ? parseInt(form.year) : undefined,
month: form.month,
flag: form.flag,
meterNum: form.meterNum,
subiMonthSum: form.subiMonthSum,
subWatFlagSum: form.subWatFlagSum,
waterDate: form.waterDate,
remarks: form.remarks
}
if (form.id) {
await editObj(submitData)
useMessage().success('编辑成功')
} else {
await addObj(submitData)
useMessage().success('新增成功')
}
visible.value = false
emit('refresh')
} catch (err: any) {
useMessage().error(err.msg || (form.id ? '编辑失败' : '新增失败'))
} finally {
loading.value = false
}
})
}
// 暴露方法给父组件
defineExpose({
openDialog
})
</script>

View File

@@ -0,0 +1,328 @@
<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="roomNo">
<el-input
v-model="searchForm.roomNo"
placeholder="请输入房间号"
clearable
style="width: 200px" />
</el-form-item>
<el-form-item label="年份" prop="year">
<el-date-picker
v-model="searchForm.year"
type="year"
placeholder="选择年份"
value-format="YYYY"
style="width: 200px" />
</el-form-item>
<el-form-item label="月份" prop="month">
<el-select
v-model="searchForm.month"
placeholder="请选择月份"
clearable
style="width: 200px">
<el-option v-for="m in 12" :key="m" :label="m + '月'" :value="m" />
</el-select>
</el-form-item>
<el-form-item label="类型" prop="flag">
<el-select
v-model="searchForm.flag"
placeholder="请选择类型"
clearable
style="width: 200px">
<el-option label="用电" :value="2" />
<el-option label="用水" :value="4" />
</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"
v-auth="'stuwork_watermonthreport_add'"
@click="formDialogRef.openDialog()">
新增
</el-button>
<right-toolbar
v-model:showSearch="showSearch"
class="ml10"
@queryTable="getDataList">
<TableColumnControl
ref="columnControlRef"
:columns="tableColumns"
v-model="visibleColumns"
trigger-type="default"
trigger-circle
@change="handleColumnChange"
@order-change="handleColumnOrderChange"
>
<template #trigger>
<el-tooltip class="item" effect="dark" content="列设置" placement="top">
<el-button circle style="margin-left: 0;">
<el-icon><Menu /></el-icon>
</el-button>
</el-tooltip>
</template>
</TableColumnControl>
</right-toolbar>
</div>
</div>
</template>
<!-- 表格 -->
<el-table
:data="state.dataList"
v-loading="state.loading"
stripe
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
class="modern-table">
<el-table-column type="index" label="序号" width="70" align="center">
<template #header>
<el-icon><List /></el-icon>
</template>
<template #default="{ $index }">
{{ $index + 1 + ((state.pagination?.current || 1) - 1) * (state.pagination?.size || 10) }}
</template>
</el-table-column>
<template v-for="col in visibleColumnsSorted" :key="col.prop || col.label">
<el-table-column
v-if="checkColumnVisible(col.prop || '') && col.prop !== '操作'"
:prop="col.prop"
:label="col.label"
show-overflow-tooltip
align="center"
:min-width="col.minWidth"
:width="col.width">
<template #header>
<el-icon><component :is="columnConfigMap[col.prop]?.icon || Document" /></el-icon>
<span style="margin-left: 4px">{{ col.label }}</span>
</template>
<!-- 类型列 -->
<template v-if="col.prop === 'flag'" #default="scope">
<el-tag size="small" :type="scope.row.flag === 2 ? 'warning' : 'primary'" effect="plain">
{{ scope.row.flag === 2 ? '用电' : '用水' }}
</el-tag>
</template>
<!-- 水表类型列 -->
<template v-else-if="col.prop === 'meterNum'" #default="scope">
<span>{{ formatMeterNum(scope.row.meterNum) }}</span>
</template>
<!-- 用量列 -->
<template v-else-if="col.prop === 'subiMonthSum'" #default="scope">
<span>{{ scope.row.subiMonthSum ?? '-' }} {{ scope.row.flag === 2 ? '度' : '吨' }}</span>
</template>
<!-- 费用列 -->
<template v-else-if="col.prop === 'subWatFlagSum'" #default="scope">
<span class="text-primary">¥{{ scope.row.subWatFlagSum ?? '0' }}</span>
</template>
<!-- 月份列 -->
<template v-else-if="col.prop === 'month'" #default="scope">
<span>{{ scope.row.month }}</span>
</template>
</el-table-column>
</template>
<el-table-column label="操作" width="160" align="center" fixed="right">
<template #header>
<el-icon><Setting /></el-icon>
<span style="margin-left: 4px">操作</span>
</template>
<template #default="scope">
<el-button
icon="Edit"
link
type="primary"
v-auth="'stuwork_watermonthreport_edit'"
@click="handleEdit(scope.row)">
编辑
</el-button>
<el-button
icon="Delete"
link
type="danger"
v-auth="'stuwork_watermonthreport_del'"
@click="handleDelete(scope.row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-wrapper">
<pagination
@size-change="sizeChangeHandle"
@current-change="currentChangeHandle"
v-bind="state.pagination" />
</div>
</el-card>
</div>
<!-- 新增/编辑表单弹窗 -->
<form-dialog ref="formDialogRef" @refresh="getDataList" />
</div>
</template>
<script setup lang="ts" name="WaterMonthReport">
import { reactive, ref, onMounted, nextTick } from 'vue'
import { useRoute } from 'vue-router'
import { BasicTableProps, useTable } from "/@/hooks/table";
import { fetchList, delObj } from "/@/api/stuwork/watermonthreport";
import { useMessage, useMessageBox } from "/@/hooks/message";
import TableColumnControl from '/@/components/TableColumnControl/index.vue'
import FormDialog from './form.vue'
import { List, Setting, Document, HomeFilled, Calendar, Clock, Coin, Menu, Search } from '@element-plus/icons-vue'
import { useTableColumnControl } from '/@/hooks/tableColumn'
// 定义变量内容
const route = useRoute()
const searchFormRef = ref()
const columnControlRef = ref<any>()
const showSearch = ref(true)
const formDialogRef = ref()
// 表格列配置
const tableColumns = [
{ prop: 'roomNo', label: '房间号', width: 120 },
{ prop: 'year', label: '年份', width: 100 },
{ prop: 'month', label: '月份', width: 100 },
{ prop: 'flag', label: '类型', width: 100 },
{ prop: 'meterNum', label: '水表类型', width: 100 },
{ prop: 'subiMonthSum', label: '用量', width: 120 },
{ prop: 'subWatFlagSum', label: '费用', width: 120 },
{ prop: 'waterDate', label: '日期', width: 120 },
{ prop: 'remarks', label: '备注', minWidth: 150 }
]
// 列配置映射(用于图标显示)
const columnConfigMap: Record<string, { icon: any }> = {
roomNo: { icon: HomeFilled },
year: { icon: Calendar },
month: { icon: Calendar },
flag: { icon: Clock },
meterNum: { icon: Clock },
subiMonthSum: { icon: Coin },
subWatFlagSum: { icon: Coin },
waterDate: { icon: Calendar },
remarks: { icon: Document }
}
// 使用表格列控制hook
const {
visibleColumns,
visibleColumnsSorted,
checkColumnVisible,
handleColumnChange,
handleColumnOrderChange
} = useTableColumnControl(tableColumns, route.path)
// 搜索表单
const searchForm = reactive({
roomNo: '',
year: '',
month: '',
flag: ''
})
// 格式化水表类型
const formatMeterNum = (value: number) => {
if (value === 10) return '冷水'
if (value === 11) 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 handleSearch = () => {
getDataList()
}
// 重置
const handleReset = () => {
searchFormRef.value?.resetFields()
searchForm.roomNo = ''
searchForm.year = ''
searchForm.month = ''
searchForm.flag = ''
getDataList()
}
// 编辑
const handleEdit = (row: any) => {
formDialogRef.value?.openDialog(row)
}
// 删除
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(() => {
nextTick(() => {
if (visibleColumns.value.length === 0) {
}
})
})
</script>
<style scoped lang="scss">
@import '/@/assets/styles/modern-page.scss';
.text-primary {
color: #409eff;
font-weight: 500;
}
</style>