“出国申请审批权限修改”

This commit is contained in:
weizhong committed 2025-10-28 14:11:39 +08:00
commit f907b9cd40
97 files changed
+18070 -2812

No files matched your search

+47
View File
@@ -0,0 +1,47 @@
import request from '@/utils/request';
import type { AxiosPromise } from 'axios';
import type { CollegeVo, CollegeQuery } from './types';
// 导出类型
export type { CollegeVo, MajorVo, CollegeQuery } from './types';
/**
* 查询学院列表(用于二级学院,parent_id = 101)
*/
export function list(query?: any): AxiosPromise<CollegeVo[]> {
return request({
url: '/xg/college/list',
method: 'get',
params: query
});
}
/**
* 获取学院和专业树形结构
*/
export function collegeTree(): AxiosPromise<CollegeVo[]> {
return request({
url: '/xg/college/tree',
method: 'get'
});
}
/**
* 根据学院ID获取专业列表
*/
export function listMajorsByCollegeId(collegeId: string | number): AxiosPromise<any[]> {
return request({
url: `/xg/college/majors/${collegeId}`,
method: 'get'
});
}
/**
* 查询学院详情
*/
export function getCollege(deptId: string | number): AxiosPromise<CollegeVo> {
return request({
url: `/xg/college/${deptId}`,
method: 'get'
});
}
+37
View File
@@ -0,0 +1,37 @@
/**
* 学院信息对象
*/
export interface CollegeVo {
deptId: number | string;
deptName: string;
parentId?: number | string;
orderNum?: number;
leader?: string;
phone?: string;
email?: string;
status?: string;
children?: CollegeVo[];
}
/**
* 专业信息对象
*/
export interface MajorVo {
deptId: number | string;
deptName: string;
parentId: number | string;
orderNum?: number;
leader?: string;
phone?: string;
email?: string;
status?: string;
}
/**
* 学院查询参数
*/
export interface CollegeQuery {
deptName?: string;
status?: string;
parentId?: number | string;
}
+117
View File
@@ -0,0 +1,117 @@
import request from '@/utils/request';
/**
* 二级学院分管领导配置对象
*/
export interface CollegeLeaderBo {
id?: number | string;
collegeId?: number | string;
collegeName?: string;
leaderId?: number | string;
leaderName?: string;
remark?: string;
}
/**
* 二级学院分管领导配置视图对象
*/
export interface CollegeLeaderVo {
id: number | string;
collegeId: number | string;
collegeName: string;
leaderId: number | string;
leaderName: string;
remark?: string;
createTime?: string;
updateTime?: string;
}
/**
* 查询二级学院分管领导配置列表
* @param query 查询参数
* @returns 返回二级学院分管领导配置列表
*/
export function listCollegeLeader(query: any) {
return request({
url: '/xg/ejxyfgld/list',
method: 'get',
params: query
});
}
/**
* 根据学院ID查询分管领导列表
* @param collegeId 学院ID
* @returns 返回分管领导列表
*/
export function listCollegeLeaderByCollege(collegeId: number | string) {
return request({
url: `/xg/ejxyfgld/listByCollege/${collegeId}`,
method: 'get'
});
}
/**
* 获取二级学院分管领导配置详情
* @param id 主键ID
* @returns 返回二级学院分管领导配置详情
*/
export function getCollegeLeader(id: number | string) {
return request({
url: `/xg/ejxyfgld/${id}`,
method: 'get'
});
}
/**
* 新增二级学院分管领导配置
* @param data 二级学院分管领导配置信息
* @returns 返回添加结果
*/
export function addCollegeLeader(data: CollegeLeaderBo) {
return request({
url: '/xg/ejxyfgld',
method: 'post',
data: data
});
}
/**
* 修改二级学院分管领导配置
* @param data 二级学院分管领导配置信息
* @returns 返回修改结果
*/
export function updateCollegeLeader(data: CollegeLeaderBo) {
return request({
url: '/xg/ejxyfgld',
method: 'put',
data: data
});
}
/**
* 删除二级学院分管领导配置
* @param ids 主键ID数组
* @returns 返回删除结果
*/
export function delCollegeLeader(ids: number | string | (number | string)[]) {
return request({
url: `/xg/ejxyfgld/${ids instanceof Array ? ids.join(',') : ids}`,
method: 'delete'
});
}
/**
* 导出二级学院分管领导配置
* @param query 查询参数
* @returns 返回导出结果
*/
export function exportCollegeLeader(query: any) {
return request({
url: '/xg/ejxyfgld/export',
method: 'post',
params: query,
responseType: 'blob'
});
}
+269
View File
@@ -0,0 +1,269 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
/**
* 学生动向信息管理(宿管员)API
* 这些接口专门用于学生动向Tab页,会自动根据宿管员权限过滤数据
*/
// ==================== 调宿退宿申请相关接口 ====================
export interface DormApplicationQuery {
pageNum?: number;
pageSize?: number;
studentName?: string;
studentNo?: string;
collegeName?: string;
majorName?: string;
className?: string;
applicationType?: string;
applicationStatus?: string;
applyTime?: string;
params?: any;
}
export interface DormApplicationVO {
id: string | number;
applicationName: string;
applicationType: string;
applicationStatus: string;
studentId: string | number;
studentNo: string;
studentName: string;
studentPhone: string;
collegeId: string | number;
collegeName: string;
majorId: string | number;
majorName: string;
classId: string | number;
className: string;
currentCampusId: string | number;
currentCampusName: string;
currentBuildingId: string | number;
currentBuildingName: string;
currentRoomId: string | number;
currentRoomNo: string;
currentBedId: string | number;
currentBedNo: string;
targetCampusId?: string | number;
targetCampusName?: string;
targetBuildingId?: string | number;
targetBuildingName?: string;
targetRoomId?: string | number;
targetRoomNo?: string;
targetBedId?: string | number;
targetBedNo?: string;
reasonDetail: string;
applyTime: string;
attachmentOssIds?: string;
remark?: string;
createTime: string;
updateTime: string;
}
/**
* 查询宿管员权限内的学生调宿退宿申请列表
*/
export function listDormApplication(query?: DormApplicationQuery): AxiosPromise<any> {
return request({
url: '/studentDynamics/dormApplicationList',
method: 'get',
params: query
});
}
/**
* 获取调宿退宿申请详细信息
*/
export function getDormApplication(id: string | number): AxiosPromise<DormApplicationVO> {
return request({
url: '/studentDynamics/dormApplication/' + id,
method: 'get'
});
}
/**
* 获取调宿退宿申请详细信息(别名)
*/
export const getDormApplicationInfo = getDormApplication;
/**
* 修改调宿退宿申请信息
*/
export function updateDormApplication(data: DormApplicationVO): AxiosPromise<any> {
return request({
url: '/studentDynamics/dormApplication',
method: 'put',
data: data
});
}
/**
* 删除调宿退宿申请
*/
export function delDormApplication(id: string | number | Array<string | number>): AxiosPromise<any> {
return request({
url: '/studentDynamics/dormApplication/' + id,
method: 'delete'
});
}
// ==================== 假期留宿相关接口 ====================
export interface HolidayStayQuery {
pageNum?: number;
pageSize?: number;
studentName?: string;
studentNo?: string;
collegeName?: string;
majorName?: string;
className?: string;
holidayType?: string;
applyStartDate?: string;
applyEndDate?: string;
status?: string;
params?: any;
}
export interface HolidayStayVO {
id: string | number;
studentId: string | number;
studentName: string;
studentNo: string;
classId: string | number;
className: string;
majorId: string | number;
majorName: string;
collegeId: string | number;
collegeName: string;
campusId: string | number;
campusName: string;
areaId?: string | number;
areaName?: string;
buildingId: string | number;
buildingName: string;
roomId: string | number;
roomName: string;
bedId: string | number;
bedName: string;
holidayType: string;
applyStartDate: string;
applyEndDate: string;
stayReason: string;
stayReasonType: string;
studentPhone: string;
emergencyContact: string;
emergencyPhone: string;
parentConsent: string;
attachmentOssIds?: string;
remark?: string;
createTime: string;
updateTime: string;
}
/**
* 查询宿管员权限内的学生假期留宿信息列表
*/
export function listHolidayStay(query?: HolidayStayQuery): AxiosPromise<any> {
return request({
url: '/studentDynamics/holidayStayList',
method: 'get',
params: query
});
}
/**
* 获取假期留宿详细信息
*/
export function getHolidayStay(id: string | number): AxiosPromise<HolidayStayVO> {
return request({
url: '/studentDynamics/holidayStay/' + id,
method: 'get'
});
}
/**
* 获取假期留宿详细信息(别名)
*/
export const getHolidayStayInfo = getHolidayStay;
// ==================== 学生请假相关接口 ====================
export interface StudentLeaveQuery {
pageNum?: number;
pageSize?: number;
studentName?: string;
studentNo?: string;
collegeName?: string;
majorName?: string;
className?: string;
leaveTypeName?: string;
approvalStatus?: string;
startTime?: string;
endTime?: string;
params?: any;
}
export interface StudentLeaveVO {
id: string | number;
studentId: string | number;
studentName: string;
studentNo: string;
classId: string | number;
className: string;
majorId: string | number;
majorName: string;
collegeId: string | number;
collegeName: string;
leaveTypeCode: string;
leaveTypeName: string;
startTime: string;
endTime: string;
leaveDuration: number;
isLeaveSchool: string;
needCancel: string;
emergencyContact: string;
emergencyPhone: string;
leaveReason: string;
leaveLocation?: string;
attachmentOssIds?: string;
approvalStatus: string;
approvalOpinion?: string;
approvalTime?: string;
approvalBy?: string | number;
approvalByName?: string;
cancelStatus?: string;
cancelTime?: string;
cancelLocation?: string;
cancelPhotoOssIds?: string;
cancelRemark?: string;
createTime: string;
updateTime: string;
}
/**
* 查询宿管员权限内的学生请假信息列表
*/
export function listStudentLeave(query?: StudentLeaveQuery): AxiosPromise<any> {
return request({
url: '/studentDynamics/studentLeaveList',
method: 'get',
params: query
});
}
/**
* 获取学生请假详细信息
*/
export function getStudentLeave(id: string | number): AxiosPromise<StudentLeaveVO> {
return request({
url: '/studentDynamics/studentLeave/' + id,
method: 'get'
});
}
/**
* 获取学生请假详细信息(别名)
*/
export const getStudentLeaveInfo = getStudentLeave;
+14 -8
View File
@@ -6,16 +6,17 @@ import { AxiosPromise } from 'axios';
*/
export interface XgSysUser {
userId: number | string;
userName: string;
userName?: string;
nickName: string;
email: string;
email?: string;
phonenumber: string;
sex: string;
status: string;
createTime: string;
updateTime: string;
deptId: number;
deptName: string;
sex?: string;
status?: string;
accountType?: string;
createTime?: string;
updateTime?: string;
deptId?: number;
deptName?: string;
}
/**
@@ -27,3 +28,8 @@ export function getActiveUserList(): AxiosPromise<XgSysUser[]> {
method: 'get'
});
}
/**
* 查询有效用户列表(别名,用于兼容)
*/
export const activeList = getActiveUserList;
@@ -106,3 +106,14 @@ export const getCollegeLeaveApprovers = (studentId: string | number): AxiosPromi
method: 'get'
});
};
/**
* 根据学生ID获取二级学院分管领导ID列表
* @param studentId 学生ID
*/
export const getCollegeLeaders = (studentId: string | number): AxiosPromise<{ data: number[] }> => {
return request({
url: `/studentMsg/studentMsg/getCollegeLeaders/${studentId}`,
method: 'get'
});
};
@@ -33,6 +33,21 @@
</template>
</el-table-column>
<el-table-column prop="message" label="审批意见" :show-overflow-tooltip="true" sortable align="center"></el-table-column>
<el-table-column prop="signature" label="手写签名" width="150" align="center">
<template #default="scope">
<el-popover v-if="scope.row.signature" placement="left" :width="350" trigger="hover">
<template #reference>
<div class="signature-thumbnail">
<img :src="scope.row.signature" alt="签名" class="signature-thumb-image" />
</div>
</template>
<div class="signature-preview-large">
<img :src="scope.row.signature" alt="签名" class="signature-large-image" />
</div>
</el-popover>
<span v-else style="color: #909399; font-size: 12px">未签名</span>
</template>
</el-table-column>
<el-table-column prop="createTime" label="开始时间" width="160" :show-overflow-tooltip="true" sortable align="center"></el-table-column>
<el-table-column prop="updateTime" label="结束时间" width="160" :show-overflow-tooltip="true" sortable align="center"></el-table-column>
<el-table-column
@@ -68,6 +83,7 @@
</div>
</template>
<script setup lang="ts">
import { ref, getCurrentInstance, toRefs, type ComponentInternalInstance } from 'vue';
import { flowHisTaskList } from '@/api/workflow/instance';
import { propTypes } from '@/utils/propTypes';
import { listByIds } from '@/api/system/oss';
@@ -198,4 +214,48 @@ defineExpose({
min-height: calc(100vh - 170px) !important;
}
}
// 列表中的签名缩略图容器
.signature-thumbnail {
display: flex;
justify-content: center;
align-items: center;
padding: 4px;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
transform: scale(1.05);
}
.signature-thumb-image {
max-width: 120px;
max-height: 50px;
border: 1px solid #e4e7ed;
border-radius: 4px;
background: #ffffff;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
object-fit: contain;
}
}
// 鼠标悬停时的大图预览
.signature-preview-large {
display: flex;
justify-content: center;
align-items: center;
padding: 16px;
background: #f5f7fa;
border-radius: 6px;
.signature-large-image {
max-width: 100%;
max-height: 200px;
border: 2px solid #dcdfe6;
border-radius: 4px;
background: #ffffff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
object-fit: contain;
}
}
</style>
@@ -31,6 +31,9 @@
<el-form-item v-if="task.flowStatus === 'waiting'" label="审批意见">
<el-input v-model="form.message" type="textarea" resize="none" />
</el-form-item>
<el-form-item v-if="task.flowStatus === 'waiting'" label="">
<SignaturePad v-model="form.signature" :width="480" :height="180" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
@@ -107,6 +110,9 @@
<el-form-item label="审批意见">
<el-input v-model="backForm.message" type="textarea" resize="none" />
</el-form-item>
<el-form-item label="">
<SignaturePad v-model="backForm.signature" :width="450" :height="180" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer" style="float: right; padding-bottom: 20px">
@@ -147,6 +153,7 @@ import {
getNextNodeList
} from '@/api/workflow/task';
import UserSelect from '@/components/UserSelect';
import SignaturePad from '@/components/SignaturePad/index.vue';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
import { UserVO } from '@/api/system/user/types';
@@ -228,6 +235,7 @@ const deleteSignatureVisible = ref(false);
const form = ref<Record<string, any>>({
taskId: undefined,
message: undefined,
signature: undefined,
assigneeMap: {},
variables: {},
messageType: ['1'],
@@ -237,6 +245,7 @@ const backForm = ref<Record<string, any>>({
taskId: undefined,
nodeCode: undefined,
message: undefined,
signature: undefined,
variables: {},
messageType: ['1']
});
@@ -248,6 +257,7 @@ const openDialog = async (id?: string) => {
form.value.fileId = undefined;
taskId.value = id;
form.value.message = undefined;
form.value.signature = undefined;
dialog.visible = true;
loading.value = true;
buttonDisabled.value = true;
@@ -322,6 +332,7 @@ const handleCompleteTask = async () => {
const handleBackProcessOpen = async () => {
backForm.value = {};
backForm.value.messageType = ['1'];
backForm.value.signature = undefined;
backVisible.value = true;
backLoading.value = true;
backButtonDisabled.value = true;
+278
View File
@@ -0,0 +1,278 @@
<template>
<div class="signature-pad-wrapper">
<div class="signature-header">
<span class="signature-title">手写签名</span>
<el-button v-if="hasSignature" size="small" type="danger" :icon="Delete" @click="handleClear">清空</el-button>
</div>
<div class="signature-canvas-container">
<canvas
ref="canvasRef"
class="signature-canvas"
@mousedown="handleMouseDown"
@mousemove="handleMouseMove"
@mouseup="handleMouseUp"
@mouseleave="handleMouseUp"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
></canvas>
<div v-if="!hasSignature" class="signature-placeholder">请在此处签名</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue';
import { Delete } from '@element-plus/icons-vue';
interface Props {
modelValue?: string; // Base64签名数据
width?: number;
height?: number;
lineWidth?: number;
lineColor?: string;
}
const props = withDefaults(defineProps<Props>(), {
modelValue: '',
width: 500,
height: 200,
lineWidth: 2,
lineColor: '#000000'
});
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void;
}>();
const canvasRef = ref<HTMLCanvasElement>();
const hasSignature = ref(false);
const isDrawing = ref(false);
let ctx: CanvasRenderingContext2D | null = null;
let resizeObserver: ResizeObserver | null = null;
onMounted(() => {
initCanvas();
// 如果有初始值,加载到画布上
if (props.modelValue) {
loadSignature(props.modelValue);
}
// 监听容器大小变化
if (canvasRef.value?.parentElement) {
resizeObserver = new ResizeObserver(() => {
handleResize();
});
resizeObserver.observe(canvasRef.value.parentElement);
}
});
onUnmounted(() => {
if (resizeObserver) {
resizeObserver.disconnect();
}
});
// 监听外部值变化
watch(
() => props.modelValue,
(newVal) => {
if (newVal && !hasSignature.value) {
loadSignature(newVal);
}
}
);
const initCanvas = () => {
if (!canvasRef.value) return;
const canvas = canvasRef.value;
const container = canvas.parentElement;
// 获取容器的实际宽度
const containerWidth = container?.clientWidth || props.width;
// 设置画布的物理尺寸(实际像素)
canvas.width = containerWidth;
canvas.height = props.height;
// 同时设置CSS样式,确保显示尺寸与物理尺寸一致
canvas.style.width = `${containerWidth}px`;
canvas.style.height = `${props.height}px`;
ctx = canvas.getContext('2d');
if (ctx) {
ctx.strokeStyle = props.lineColor;
ctx.lineWidth = props.lineWidth;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
}
};
// 处理容器大小变化
const handleResize = () => {
if (!canvasRef.value) return;
// 保存当前签名
const currentSignature = hasSignature.value ? canvasRef.value.toDataURL('image/png') : '';
// 重新初始化画布
initCanvas();
// 恢复签名
if (currentSignature && hasSignature.value) {
nextTick(() => {
loadSignature(currentSignature);
});
}
};
const startDrawing = (x: number, y: number) => {
if (!ctx || !canvasRef.value) return;
isDrawing.value = true;
const rect = canvasRef.value.getBoundingClientRect();
ctx.beginPath();
ctx.moveTo(x - rect.left, y - rect.top);
};
const draw = (x: number, y: number) => {
if (!isDrawing.value || !ctx || !canvasRef.value) return;
const rect = canvasRef.value.getBoundingClientRect();
ctx.lineTo(x - rect.left, y - rect.top);
ctx.stroke();
hasSignature.value = true;
};
const stopDrawing = () => {
if (!isDrawing.value) return;
isDrawing.value = false;
if (ctx) {
ctx.closePath();
}
// 导出签名数据
if (hasSignature.value && canvasRef.value) {
const signatureData = canvasRef.value.toDataURL('image/png');
emit('update:modelValue', signatureData);
}
};
// 鼠标事件处理
const handleMouseDown = (e: MouseEvent) => {
startDrawing(e.clientX, e.clientY);
};
const handleMouseMove = (e: MouseEvent) => {
draw(e.clientX, e.clientY);
};
const handleMouseUp = () => {
stopDrawing();
};
// 触摸事件处理(移动端支持)
const handleTouchStart = (e: TouchEvent) => {
e.preventDefault();
const touch = e.touches[0];
startDrawing(touch.clientX, touch.clientY);
};
const handleTouchMove = (e: TouchEvent) => {
e.preventDefault();
const touch = e.touches[0];
draw(touch.clientX, touch.clientY);
};
const handleTouchEnd = (e: TouchEvent) => {
e.preventDefault();
stopDrawing();
};
// 清空签名
const handleClear = () => {
if (!canvasRef.value || !ctx) return;
ctx.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height);
hasSignature.value = false;
emit('update:modelValue', '');
};
// 加载签名
const loadSignature = (base64Data: string) => {
if (!canvasRef.value || !ctx) return;
const img = new Image();
img.onload = () => {
ctx!.clearRect(0, 0, canvasRef.value!.width, canvasRef.value!.height);
ctx!.drawImage(img, 0, 0);
hasSignature.value = true;
};
img.src = base64Data;
};
// 获取签名数据
const getSignature = (): string => {
if (!canvasRef.value || !hasSignature.value) return '';
return canvasRef.value.toDataURL('image/png');
};
// 对外暴露方法
defineExpose({
clear: handleClear,
getSignature,
hasSignature: () => hasSignature.value
});
</script>
<style scoped lang="scss">
.signature-pad-wrapper {
width: 100%;
.signature-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
.signature-title {
font-size: 14px;
color: #606266;
font-weight: 500;
}
}
.signature-canvas-container {
position: relative;
width: 100%;
border: 1px solid #dcdfe6;
border-radius: 4px;
background-color: #ffffff;
overflow: hidden;
&:hover {
border-color: #c0c4cc;
}
.signature-canvas {
display: block;
width: 100%;
cursor: crosshair;
touch-action: none;
}
.signature-placeholder {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #c0c4cc;
font-size: 14px;
pointer-events: none;
user-select: none;
}
}
}
</style>
+37 -25
View File
@@ -47,7 +47,7 @@
<img :src="menu.logoUrl" :alt="menu.title" class="menu-logo" />
</div>
<span class="action-title" :class="{ 'disabled': !menu.available }">{{ menu.title }}</span>
<span class="action-desc" :class="{ 'disabled': !menu.available }">{{ menu.description }}</span>
<!-- <span class="action-desc" :class="{ 'disabled': !menu.available }">{{ menu.description }}</span> -->
<!-- 移除层级路径显示 -->
<el-tag v-if="!menu.available" type="info" size="small" class="status-tag">开发中</el-tag>
</div>
@@ -946,24 +946,36 @@ onMounted(async () => {
}
.menu-card-wrapper {
flex: 0 0 calc(20% - 16px); // 每行5个,减去间隔
min-width: 180px;
flex: 0 0 calc((100% - 140px) / 8); // 每行8个,精确计算:(100% - 7个间隔*20px) / 8
min-width: 100px;
margin-bottom: 20px;
@media (max-width: 1600px) {
flex: 0 0 calc((100% - 120px) / 7); // 中大屏幕每行7个
}
@media (max-width: 1400px) {
flex: 0 0 calc(25% - 15px); // 中等屏幕每行4个
flex: 0 0 calc((100% - 100px) / 6); // 中等屏幕每行6个
}
@media (max-width: 1200px) {
flex: 0 0 calc(33.333% - 14px); // 小屏幕每行3个
flex: 0 0 calc((100% - 80px) / 5); // 小屏幕每行5个
}
@media (max-width: 992px) {
flex: 0 0 calc((100% - 60px) / 4); // 平板每行4个
}
@media (max-width: 768px) {
flex: 0 0 calc(50% - 10px); // 移动端每行2个
flex: 0 0 calc((100% - 40px) / 3); // 移动端每行3个
}
@media (max-width: 600px) {
flex: 0 0 calc((100% - 20px) / 2); // 小手机每行2个
}
@media (max-width: 480px) {
flex: 0 0 100%; // 超小屏幕每行1个
flex: 0 0 calc(100% - 20px); // 超小屏幕每行1个
}
}
@@ -972,7 +984,7 @@ onMounted(async () => {
border: none;
transition: all 0.3s ease;
cursor: pointer;
height: 140px;
height: 110px;
display: flex;
align-items: center;
justify-content: center;
@@ -995,7 +1007,7 @@ onMounted(async () => {
.menu-action {
text-align: center;
padding: 15px 10px;
padding: 10px 8px;
width: 100%;
}
@@ -1011,15 +1023,15 @@ onMounted(async () => {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 12px;
height: 48px;
margin-bottom: 8px;
height: 36px;
}
.menu-logo {
width: 48px;
height: 48px;
width: 36px;
height: 36px;
object-fit: contain;
border-radius: 8px;
border-radius: 6px;
transition: all 0.3s ease;
filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.1));
@@ -1041,16 +1053,16 @@ onMounted(async () => {
}
.action-title {
font-size: 15px;
font-size: 14px;
font-weight: 600;
color: #333;
margin-bottom: 6px;
margin-bottom: 4px;
display: block;
line-height: 1.3;
}
.action-desc {
font-size: 12px;
font-size: 11px;
color: #666;
display: block;
line-height: 1.4;
@@ -1140,29 +1152,29 @@ onMounted(async () => {
}
.menu-action-card {
height: 120px;
height: 100px;
}
.menu-action {
padding: 10px 5px;
padding: 8px 5px;
}
.menu-logo-wrapper {
height: 36px;
margin-bottom: 8px;
height: 30px;
margin-bottom: 6px;
}
.menu-logo {
width: 36px;
height: 36px;
width: 30px;
height: 30px;
}
.action-title {
font-size: 14px;
font-size: 13px;
}
.action-desc {
font-size: 11px;
font-size: 10px;
}
.loading-state {
+322
View File
@@ -0,0 +1,322 @@
<template>
<div class="p-2">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" label-width="100px" :inline="true">
<el-form-item label="学院名称" prop="collegeName">
<el-input v-model="queryParams.collegeName" placeholder="请输入学院名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="领导姓名" prop="leaderName">
<el-input v-model="queryParams.leaderName" placeholder="请输入领导姓名" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</transition>
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['xg:ejxyfgld:add']">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['xg:ejxyfgld:remove']"
>删除</el-button
>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['xg:ejxyfgld:export']">导出</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table v-loading="loading" border :data="collegeLeaderList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="学院名称" align="center" prop="collegeName" />
<el-table-column label="领导姓名" align="center" prop="leaderName" />
<el-table-column label="备注" align="center" prop="remark" :show-overflow-tooltip="true" />
<el-table-column label="创建时间" align="center" prop="createTime" width="180" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="180">
<template #default="scope">
<el-tooltip content="修改" placement="top">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['xg:ejxyfgld:edit']"></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['xg:ejxyfgld:remove']"></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
<!-- 添加或修改二级学院分管领导对话框 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" width="600px" append-to-body>
<el-form ref="collegeLeaderFormRef" :model="form" :rules="rules" label-width="120px">
<el-form-item label="二级学院" prop="collegeId">
<el-select v-model="form.collegeId" placeholder="请选择二级学院" clearable filterable style="width: 100%" @change="handleCollegeChange">
<el-option v-for="college in collegeList" :key="college.deptId" :label="college.deptName" :value="college.deptId" />
</el-select>
</el-form-item>
<el-form-item label="分管领导" prop="leaderId">
<el-select v-model="form.leaderId" placeholder="请选择分管领导" clearable filterable style="width: 100%" @change="handleLeaderChange">
<el-option v-for="user in teacherList" :key="user.userId" :label="`${user.nickName}(${user.userName})`" :value="user.userId" />
</el-select>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注信息" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="CollegeLeader" lang="ts">
import { ref, reactive, toRefs, getCurrentInstance } from 'vue';
import type { FormInstance } from 'element-plus';
import {
listCollegeLeader,
getCollegeLeader,
addCollegeLeader,
updateCollegeLeader,
delCollegeLeader,
exportCollegeLeader,
type CollegeLeaderBo,
type CollegeLeaderVo
} from '@/api/xg/ejxyfgld';
import { list as listCollege } from '@/api/xg/college';
import { activeList as listTeacher } from '@/api/xg/sysUser';
interface ComponentInternalInstance {
proxy?: any;
}
interface DialogOption {
visible: boolean;
title: string;
}
interface QueryParams {
pageNum: number;
pageSize: number;
collegeName?: string;
leaderName?: string;
}
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const collegeLeaderList = ref<CollegeLeaderVo[]>([]);
const buttonLoading = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
// 学院列表和教师列表
const collegeList = ref<any[]>([]);
const teacherList = ref<any[]>([]);
const queryFormRef = ref<FormInstance>();
const collegeLeaderFormRef = ref<FormInstance>();
const dialog = reactive<DialogOption>({
visible: false,
title: ''
});
const initFormData: CollegeLeaderBo = {
id: undefined,
collegeId: undefined,
collegeName: undefined,
leaderId: undefined,
leaderName: undefined,
remark: undefined
};
const data = reactive({
form: { ...initFormData },
queryParams: {
pageNum: 1,
pageSize: 10,
collegeName: undefined,
leaderName: undefined
} as QueryParams,
rules: {
collegeId: [{ required: true, message: '二级学院不能为空', trigger: 'change' }],
leaderId: [{ required: true, message: '分管领导不能为空', trigger: 'change' }]
}
});
const { queryParams, form, rules } = toRefs(data);
/** 查询二级学院分管领导列表 */
const getList = async () => {
loading.value = true;
try {
const res = await listCollegeLeader(queryParams.value);
collegeLeaderList.value = res.rows;
total.value = res.total;
} finally {
loading.value = false;
}
};
/** 加载学院列表 */
const loadCollegeList = async () => {
try {
const res = await listCollege();
collegeList.value = res.data || [];
} catch (error) {
console.error('加载学院列表失败:', error);
}
};
/** 加载教师列表 */
const loadTeacherList = async () => {
try {
const res = await listTeacher();
// 后端已经过滤出教职工(account_type = 1),直接使用
teacherList.value = res.data || [];
console.log('加载教师列表成功,共', teacherList.value.length, '条数据');
} catch (error) {
console.error('加载教师列表失败:', error);
}
};
/** 学院选择变更 */
const handleCollegeChange = (val: any) => {
const college = collegeList.value.find((c: any) => c.deptId === val);
if (college) {
form.value.collegeName = college.deptName;
}
};
/** 领导选择变更 */
const handleLeaderChange = (val: any) => {
const teacher = teacherList.value.find((t: any) => t.userId === val);
if (teacher) {
form.value.leaderName = teacher.nickName;
}
};
/** 取消按钮 */
const cancel = () => {
reset();
dialog.visible = false;
};
/** 表单重置 */
const reset = () => {
form.value = { ...initFormData };
collegeLeaderFormRef.value?.resetFields();
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
};
/** 多选框选中数据 */
const handleSelectionChange = (selection: CollegeLeaderVo[]) => {
ids.value = selection.map((item) => item.id);
single.value = selection.length !== 1;
multiple.value = !selection.length;
};
/** 新增按钮操作 */
const handleAdd = async () => {
reset();
await loadCollegeList();
await loadTeacherList();
dialog.visible = true;
dialog.title = '添加二级学院分管领导';
};
/** 修改按钮操作 */
const handleUpdate = async (row?: CollegeLeaderVo) => {
reset();
await loadCollegeList();
await loadTeacherList();
const id = row?.id || ids.value[0];
const res = await getCollegeLeader(id);
Object.assign(form.value, res.data);
dialog.visible = true;
dialog.title = '修改二级学院分管领导';
};
/** 提交按钮 */
const submitForm = () => {
collegeLeaderFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
try {
if (form.value.id) {
await updateCollegeLeader(form.value);
proxy?.$modal.msgSuccess('修改成功');
} else {
await addCollegeLeader(form.value);
proxy?.$modal.msgSuccess('新增成功');
}
dialog.visible = false;
await getList();
} catch (error) {
console.error('提交失败:', error);
} finally {
buttonLoading.value = false;
}
}
});
};
/** 删除按钮操作 */
const handleDelete = async (row?: CollegeLeaderVo) => {
const deleteIds = row?.id ? [row.id] : ids.value;
await proxy?.$modal.confirm('是否确认删除选中的配置?');
try {
await delCollegeLeader(deleteIds);
await getList();
proxy?.$modal.msgSuccess('删除成功');
} catch (error) {
console.error('删除失败:', error);
}
};
/** 导出按钮操作 */
const handleExport = () => {
proxy?.$download(
'xg/ejxyfgld/export',
{
...queryParams.value
},
`二级学院分管领导_${new Date().getTime()}.xlsx`
);
};
// 初始化
getList();
</script>
<style scoped lang="scss"></style>
@@ -0,0 +1,256 @@
<template>
<el-dialog :title="dialogTitle" v-model="dialogOpen" width="800px" append-to-body @close="cancel">
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px" v-loading="loading">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="申请名称:">
<span>{{ form.applicationName }}</span>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="申请类型:">
<dict-tag :options="dorm_application_type" :value="form.applicationType" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="学生姓名:">
<span>{{ form.studentName }}</span>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="学号:">
<span>{{ form.studentNo }}</span>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="学院:">
<span>{{ form.collegeName }}</span>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="专业:">
<span>{{ form.majorName }}</span>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="班级:">
<span>{{ form.className }}</span>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系电话:">
<span>{{ form.studentPhone }}</span>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="当前宿舍:">
<span>{{ form.currentCampusName }} {{ form.currentBuildingName }} {{ form.currentRoomNo }} {{ form.currentBedNo }}</span>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20" v-if="form.applicationType === '1'">
<el-col :span="24">
<el-form-item label="目标宿舍:">
<span>{{ form.targetCampusName }} {{ form.targetBuildingName }} {{ form.targetRoomNo }} {{ form.targetBedNo }}</span>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="申请时间:">
<span>{{ form.applyTime }}</span>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="审批状态:">
<dict-tag :options="student_leave_approval_status" :value="form.applicationStatus" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="申请原因:" prop="reasonDetail">
<el-input
v-if="isEditable"
v-model="form.reasonDetail"
type="textarea"
:rows="3"
placeholder="请输入申请原因"
maxlength="500"
show-word-limit
/>
<span v-else>{{ form.reasonDetail || '无' }}</span>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="备注信息:" prop="remark">
<el-input
v-if="isEditable"
v-model="form.remark"
type="textarea"
:rows="2"
placeholder="请输入备注信息"
maxlength="200"
show-word-limit
/>
<span v-else>{{ form.remark || '无' }}</span>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancel">{{ isEditable ? '取消' : '关闭' }}</el-button>
<el-button v-if="isEditable" type="primary" @click="submitForm" :loading="submitLoading">保存</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="DormApplicationDetail">
import { getCurrentInstance, ref, toRefs, computed, type ComponentInternalInstance } from 'vue';
import { ElMessage } from 'element-plus';
import { getDormApplicationInfo, updateDormApplication, type DormApplicationVO } from '@/api/xg/studentDynamics';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { dorm_application_type, student_leave_approval_status } = toRefs(
proxy?.useDict('dorm_application_type', 'student_leave_approval_status') || {}
);
const emit = defineEmits(['refresh']);
// 弹窗状态
const dialogOpen = ref(false);
const loading = ref(false);
const submitLoading = ref(false);
// 表单数据
const form = ref<DormApplicationVO>({} as DormApplicationVO);
// 表单引用
const formRef = ref();
// 表单校验规则
const rules = {
reasonDetail: [{ required: true, message: '请输入申请原因', trigger: 'blur' }]
};
/**
* 是否可编辑
* 草稿、已撤销、已退回状态可编辑
*/
const isEditable = computed(() => {
return ['draft', 'cancel', 'back'].includes(form.value.applicationStatus || '');
});
/**
* 弹窗标题
*/
const dialogTitle = computed(() => {
return isEditable.value ? '修改调宿/退宿申请' : '调宿/退宿申请详情';
});
/**
* 打开弹窗
*/
const openDialog = (id: number) => {
reset();
getDetail(id);
dialogOpen.value = true;
};
/**
* 获取详情数据
*/
const getDetail = async (id: number) => {
loading.value = true;
try {
const response = await getDormApplicationInfo(id);
form.value = response.data;
} catch (error) {
console.error('获取调宿退宿申请详情失败:', error);
ElMessage.error('获取详情失败');
} finally {
loading.value = false;
}
};
/**
* 重置表单
*/
const reset = () => {
form.value = {} as DormApplicationVO;
formRef.value?.resetFields();
};
/**
* 提交表单
*/
const submitForm = async () => {
if (!formRef.value) return;
await formRef.value.validate(async (valid: boolean) => {
if (!valid) return;
submitLoading.value = true;
try {
await updateDormApplication(form.value);
ElMessage.success('保存成功');
dialogOpen.value = false;
emit('refresh');
} catch (error) {
console.error('保存失败:', error);
ElMessage.error('保存失败');
} finally {
submitLoading.value = false;
}
});
};
/**
* 取消操作
*/
const cancel = () => {
dialogOpen.value = false;
reset();
};
// 暴露方法
defineExpose({
open: openDialog
});
</script>
<style scoped>
.dialog-footer {
text-align: right;
}
:deep(.el-form-item__label) {
font-weight: 600;
color: #606266;
}
:deep(.el-form-item__content) {
color: #303133;
}
</style>
@@ -0,0 +1,258 @@
<template>
<div class="page-container">
<!-- 搜索表单 -->
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" style="margin-bottom: 10px">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" label-width="100px">
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="学生姓名" prop="studentName">
<el-input v-model="queryParams.studentName" placeholder="请输入学生姓名" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="学号" prop="studentNo">
<el-input v-model="queryParams.studentNo" placeholder="请输入学号" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="学院名称" prop="collegeName">
<el-input v-model="queryParams.collegeName" placeholder="请输入学院名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="专业名称" prop="majorName">
<el-input v-model="queryParams.majorName" placeholder="请输入专业名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="班级名称" prop="className">
<el-input v-model="queryParams.className" placeholder="请输入班级名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="申请类型" prop="applicationType">
<el-select v-model="queryParams.applicationType" placeholder="请选择申请类型" clearable style="width: 100%">
<el-option v-for="dict in dorm_application_type" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="申请时间" prop="applyTime">
<el-date-picker
clearable
v-model="queryParams.applyTime"
type="date"
style="width: 100%"
value-format="YYYY-MM-DD"
placeholder="请选择申请时间"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="审批状态" prop="applicationStatus">
<el-select v-model="queryParams.applicationStatus" placeholder="请选择审批状态" clearable style="width: 100%">
<el-option v-for="dict in student_leave_approval_status" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24" class="search-btn-container">
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
</div>
</transition>
<!-- 数据表格 -->
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<span class="card-title">调宿/退宿申请列表</span>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table v-loading="loading" :data="dataList" @selection-change="handleSelectionChange">
<el-table-column label="学生姓名" align="center" prop="studentName" />
<el-table-column label="学号" align="center" prop="studentNo" />
<el-table-column label="学院" align="center" prop="collegeName" show-overflow-tooltip />
<el-table-column label="专业" align="center" prop="majorName" show-overflow-tooltip />
<el-table-column label="班级" align="center" prop="className" show-overflow-tooltip />
<el-table-column label="申请类型" align="center">
<template #default="scope">
<dict-tag :options="dorm_application_type" :value="scope.row.applicationType" />
</template>
</el-table-column>
<el-table-column label="当前宿舍" align="center" show-overflow-tooltip>
<template #default="scope"> {{ scope.row.currentBuildingName }}{{ scope.row.currentRoomNo }}{{ scope.row.currentBedNo }} </template>
</el-table-column>
<el-table-column label="目标宿舍" align="center" show-overflow-tooltip>
<template #default="scope">
<span v-if="scope.row.applicationType === '1'">
{{ scope.row.targetBuildingName }}{{ scope.row.targetRoomNo }}{{ scope.row.targetBedNo }}
</span>
<span v-else>—</span>
</template>
</el-table-column>
<el-table-column label="审批状态" align="center">
<template #default="scope">
<dict-tag :options="student_leave_approval_status" :value="scope.row.applicationStatus" />
</template>
</el-table-column>
<el-table-column label="申请时间" align="center" prop="applyTime" width="180" />
<el-table-column label="操作" align="center" width="150" fixed="right">
<template #default="scope">
<el-tooltip content="查看" placement="top">
<el-button
link
type="primary"
icon="View"
@click="handleView(scope.row)"
v-hasPermi="['studentDynamics:dormApplication:query']"
></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
</div>
</template>
<script setup lang="ts" name="DormApplicationTab">
import { getCurrentInstance, ref, onMounted, toRefs, type ComponentInternalInstance } from 'vue';
import { listDormApplication, type DormApplicationQuery } from '@/api/xg/studentDynamics';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { dorm_application_type, student_leave_approval_status } = toRefs(
proxy?.useDict('dorm_application_type', 'student_leave_approval_status') || {}
);
// 数据状态
const loading = ref(true);
const showSearch = ref(true);
const dataList = ref([]);
const total = ref(0);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
// 查询参数
const queryParams = ref<DormApplicationQuery & any>({
pageNum: 1,
pageSize: 10,
studentName: undefined,
studentNo: undefined,
collegeName: undefined,
majorName: undefined,
className: undefined,
applicationType: undefined,
applicationStatus: undefined,
applyTime: undefined
});
// 表单引用
const queryFormRef = ref();
/**
* 查询列表数据
*/
const getList = async () => {
loading.value = true;
try {
const response = await listDormApplication(queryParams.value);
dataList.value = response.rows;
total.value = response.total;
} catch (error) {
console.error('获取调宿退宿申请列表失败:', error);
proxy?.$modal.msgError('获取数据失败');
} finally {
loading.value = false;
}
};
/**
* 搜索按钮操作
*/
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/**
* 重置按钮操作
*/
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
};
/**
* 多选框选中数据
*/
const handleSelectionChange = (selection: any) => {
ids.value = selection.map((item: any) => item.id);
single.value = selection.length !== 1;
multiple.value = !selection.length;
};
/**
* 查看详情
*/
const handleView = (row: any) => {
// 跳转到调宿退宿申请详情页面
proxy.$router.push({
path: `/xg/xsjcxx/xssssq/dialog`,
query: {
id: row.id,
type: 'update'
}
});
};
// 页面初始化
onMounted(() => {
getList();
});
</script>
<style scoped>
/* 页面容器样式 */
.page-container {
padding: 0;
}
.mb8 {
margin-bottom: 8px;
}
/* 搜索按钮容器样式 */
.search-btn-container {
display: flex;
justify-content: flex-end;
margin-top: 10px;
}
.search-btn-container .el-form-item {
margin-bottom: 0 !important;
}
.card-title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
</style>
@@ -0,0 +1,474 @@
<template>
<el-dialog title="假期留宿详情" v-model="dialogOpen" width="1000px" append-to-body>
<el-form :model="form" label-width="120px" v-loading="loading">
<el-row :gutter="20">
<!-- 左列:学生基本信息 -->
<el-col :span="12">
<el-form-item label="学生姓名">
<el-input v-model="form.studentName" disabled />
</el-form-item>
<el-form-item label="学号">
<el-input v-model="form.studentNo" disabled />
</el-form-item>
<el-form-item label="学生联系电话">
<el-input v-model="form.studentPhone" disabled />
</el-form-item>
<el-form-item label="学院名称">
<el-input v-model="form.collegeName" disabled />
</el-form-item>
<el-form-item label="专业名称">
<el-input v-model="form.majorName" disabled />
</el-form-item>
<el-form-item label="班级名称">
<el-input v-model="form.className" disabled />
</el-form-item>
</el-col>
<!-- 右列:宿舍信息 -->
<el-col :span="12">
<el-form-item label="校区名称">
<el-input v-model="form.campusName" disabled />
</el-form-item>
<el-form-item label="宿舍区名称">
<el-input v-model="form.areaName" disabled />
</el-form-item>
<el-form-item label="宿舍楼名称">
<el-input v-model="form.buildingName" disabled />
</el-form-item>
<el-form-item label="房间名称">
<el-input v-model="form.roomName" disabled />
</el-form-item>
<el-form-item label="床位名称">
<el-input v-model="form.bedName" disabled />
</el-form-item>
</el-col>
</el-row>
<!-- 假期申请信息 -->
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="假期类型">
<el-input v-model="holidayTypeLabel" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="留宿原因类型">
<el-input v-model="stayReasonTypeLabel" disabled />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="留宿开始日期">
<el-input v-model="form.applyStartDate" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="留宿结束日期">
<el-input v-model="form.applyEndDate" disabled />
</el-form-item>
</el-col>
</el-row>
<!-- 联系信息 -->
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="紧急联系人姓名">
<el-input v-model="form.emergencyContact" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="紧急联系人电话">
<el-input v-model="form.emergencyPhone" disabled />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="家长同意">
<el-input v-model="parentConsentLabel" disabled />
</el-form-item>
</el-col>
</el-row>
<!-- 留宿原因 -->
<el-form-item label="留宿原因">
<el-input v-model="form.stayReason" type="textarea" :rows="3" disabled />
</el-form-item>
<!-- 附件显示 -->
<el-form-item label="附件">
<div class="attachment-view-container" v-if="attachmentList.length > 0">
<div class="attachment-grid">
<div v-for="file in attachmentList" :key="file.ossId" class="attachment-item">
<!-- 缩略图区域 -->
<div class="attachment-thumb">
<image-preview v-if="isImageFile(file)" :src="file.ossId" class="thumb-image" :isOssId="true" />
<div v-else class="file-placeholder">
<el-icon><Document /></el-icon>
<span class="file-ext">{{ getFileExt(file.name) }}</span>
</div>
</div>
<!-- 文件名称 -->
<div class="attachment-name" :title="file.name">
{{ getShortFileName(file.name) }}
</div>
<!-- 操作按钮 -->
<div class="attachment-actions">
<el-button type="success" size="small" @click="handleDownload(file)" title="下载">
<el-icon><Download /></el-icon>
</el-button>
</div>
</div>
</div>
</div>
<div v-else class="no-attachment">
<el-empty description="暂无附件" :image-size="60" />
</div>
</el-form-item>
<!-- 备注信息 -->
<el-form-item label="备注信息">
<el-input v-model="form.remark" type="textarea" :rows="3" disabled />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogOpen = false">关闭</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="HolidayStayDetail">
import { ref, computed, toRefs, getCurrentInstance, type ComponentInternalInstance } from 'vue';
import ElMessage from 'element-plus/es/components/message/index';
import ElLoading from 'element-plus/es/components/loading/index';
import { Document, Download } from '@element-plus/icons-vue';
import { getHolidayStay, type HolidayStayVO } from '@/api/xg/studentDynamics';
import ImagePreview from '@/components/ImagePreview/index.vue';
import request from '@/utils/request';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
// 字典数据
const { xg_holiday_type, xg_stay_reason_type, sys_yesno } = toRefs<any>(proxy?.useDict('xg_holiday_type', 'xg_stay_reason_type', 'sys_yesno'));
// 对话框状态
const dialogOpen = ref(false);
const loading = ref(false);
// 表单数据
const form = ref<Partial<HolidayStayVO>>({
id: undefined,
studentId: undefined,
studentName: '',
studentNo: '',
classId: undefined,
className: '',
majorId: undefined,
majorName: '',
collegeId: undefined,
collegeName: '',
campusId: undefined,
campusName: '',
areaId: undefined,
areaName: '',
buildingId: undefined,
buildingName: '',
roomId: undefined,
roomName: '',
bedId: undefined,
bedName: '',
holidayType: '',
applyStartDate: '',
applyEndDate: '',
stayReason: '',
stayReasonType: '',
studentPhone: '',
emergencyContact: '',
emergencyPhone: '',
parentConsent: '',
attachmentOssIds: '',
remark: ''
});
// 附件列表
const attachmentList = ref<any[]>([]);
// 字典标签计算属性
const holidayTypeLabel = computed(() => {
if (!form.value.holidayType || !xg_holiday_type.value) return '';
const dict = xg_holiday_type.value.find((item: any) => item.value === form.value.holidayType);
return dict ? dict.label : form.value.holidayType;
});
const stayReasonTypeLabel = computed(() => {
if (!form.value.stayReasonType || !xg_stay_reason_type.value) return '';
const dict = xg_stay_reason_type.value.find((item: any) => item.value === form.value.stayReasonType);
return dict ? dict.label : form.value.stayReasonType;
});
const parentConsentLabel = computed(() => {
if (!form.value.parentConsent || !sys_yesno.value) return '';
const dict = sys_yesno.value.find((item: any) => item.value === form.value.parentConsent);
return dict ? dict.label : form.value.parentConsent;
});
/**
* 打开详情弹窗
*/
const open = async (id: string | number) => {
loading.value = true;
dialogOpen.value = true;
try {
// 获取详情数据
const response = await getHolidayStay(id);
form.value = response.data;
// 处理附件数据
await handleAttachmentData();
} catch (error) {
console.error('获取假期留宿详情失败:', error);
ElMessage.error('获取详情失败');
dialogOpen.value = false;
} finally {
loading.value = false;
}
};
/**
* 处理附件数据
*/
const handleAttachmentData = async () => {
try {
if (form.value.attachmentOssIds && form.value.attachmentOssIds.toString().trim()) {
// 将逗号分隔的字符串转换为数组
const ossIdsArray = form.value.attachmentOssIds
.toString()
.split(',')
.map((id) => id.trim())
.filter((id) => id);
// 调用接口获取附件信息
const response = await request({
url: `/resource/oss/listByIds/${ossIdsArray.join(',')}`,
method: 'get'
});
if (response.code === 200 && response.data) {
// 将附件信息转换为显示需要的格式
attachmentList.value = response.data.map((item: any) => ({
ossId: item.ossId,
name: item.originalName || '附件',
url: item.url,
type: item.contentType || ''
}));
} else {
attachmentList.value = [];
}
} else {
attachmentList.value = [];
}
} catch (error) {
console.error('处理附件数据失败:', error);
attachmentList.value = [];
}
};
/**
* 判断是否为图片文件
*/
const isImageFile = (file: any) => {
const imageTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/webp'];
return imageTypes.includes(file.type) || /\.(jpg|jpeg|png|gif|bmp|webp)$/i.test(file.name);
};
/**
* 获取文件扩展名
*/
const getFileExt = (fileName: string) => {
const ext = fileName.split('.').pop()?.toUpperCase();
return ext || 'FILE';
};
/**
* 获取缩短的文件名
*/
const getShortFileName = (fileName: string) => {
if (!fileName) return '';
const lastDotIndex = fileName.lastIndexOf('.');
if (lastDotIndex === -1) {
return fileName.length > 12 ? fileName.substring(0, 12) + '...' : fileName;
}
const name = fileName.substring(0, lastDotIndex);
const ext = fileName.substring(lastDotIndex);
if (name.length <= 8) {
return fileName;
}
return name.substring(0, 8) + '...' + ext;
};
/**
* 下载附件
*/
const handleDownload = async (file: any) => {
try {
const loading = ElLoading.service({
text: '正在下载文件...',
target: document.body
});
const response = await request({
url: '/resource/oss/download/' + file.ossId,
method: 'get',
responseType: 'blob',
timeout: 300000
});
// 验证响应数据
if (!response) {
throw new Error('响应数据为空');
}
// 检查response的结构
let blobData: Blob;
if (response instanceof Blob) {
blobData = response;
} else if (response.data instanceof Blob) {
blobData = response.data;
} else {
throw new Error('无效的响应数据格式');
}
const url = window.URL.createObjectURL(blobData);
const link = document.createElement('a');
link.href = url;
link.download = file.name;
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
loading?.close();
ElMessage.success('下载成功');
} catch (error) {
console.error('下载文件失败:', error);
ElMessage.error('下载文件失败,请稍后重试');
}
};
defineExpose({ open });
</script>
<style scoped>
/* 附件查看容器样式 */
.attachment-view-container {
width: 100%;
padding: 12px;
border: 1px solid #e4e7ed;
border-radius: 6px;
background: #fafafa;
}
.attachment-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 12px;
}
.attachment-item {
background: #fff;
border-radius: 6px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transition: all 0.3s;
}
.attachment-item:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
.attachment-thumb {
position: relative;
width: 100%;
height: 110px;
background: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.thumb-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.file-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #909399;
font-size: 14px;
}
.file-placeholder .el-icon {
font-size: 28px;
margin-bottom: 4px;
}
.file-ext {
font-size: 12px;
text-transform: uppercase;
}
.attachment-name {
padding: 6px 8px;
font-size: 11px;
color: #303133;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
background: #fafafa;
border-top: 1px solid #f0f0f0;
line-height: 1.2;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
}
.attachment-actions {
display: flex;
justify-content: center;
align-items: center;
padding: 6px 8px;
background: #fafafa;
border-top: 1px solid #f0f0f0;
height: 38px;
}
.attachment-actions .el-button {
padding: 4px 12px;
font-size: 12px;
border-radius: 4px;
}
.no-attachment {
text-align: center;
padding: 20px;
color: #909399;
}
</style>
@@ -0,0 +1,263 @@
<template>
<div class="page-container">
<!-- 搜索表单 -->
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" label-width="100px">
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="学生姓名" prop="studentName">
<el-input v-model="queryParams.studentName" placeholder="请输入学生姓名" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="学号" prop="studentNo">
<el-input v-model="queryParams.studentNo" placeholder="请输入学号" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="学院名称" prop="collegeName">
<el-input v-model="queryParams.collegeName" placeholder="请输入学院名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="专业名称" prop="majorName">
<el-input v-model="queryParams.majorName" placeholder="请输入专业名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="班级名称" prop="className">
<el-input v-model="queryParams.className" placeholder="请输入班级名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="假期类型" prop="holidayType">
<el-select v-model="queryParams.holidayType" placeholder="请选择假期类型" clearable style="width: 100%">
<el-option v-for="dict in xg_holiday_type" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="开始日期" prop="applyStartDate">
<el-date-picker
clearable
v-model="queryParams.applyStartDate"
type="date"
style="width: 100%"
value-format="YYYY-MM-DD"
placeholder="请选择开始日期"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="结束日期" prop="applyEndDate">
<el-date-picker
clearable
v-model="queryParams.applyEndDate"
type="date"
style="width: 100%"
value-format="YYYY-MM-DD"
placeholder="请选择结束日期"
/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24" class="search-btn-container">
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
</div>
</transition>
<!-- 数据表格 -->
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<span class="card-title">假期留宿信息列表</span>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table v-loading="loading" :data="dataList" @selection-change="handleSelectionChange">
<el-table-column label="学生姓名" align="center" prop="studentName" />
<el-table-column label="学号" align="center" prop="studentNo" />
<el-table-column label="学院" align="center" prop="collegeName" show-overflow-tooltip />
<el-table-column label="专业" align="center" prop="majorName" show-overflow-tooltip />
<el-table-column label="班级" align="center" prop="className" show-overflow-tooltip />
<el-table-column label="宿舍信息" align="center" show-overflow-tooltip>
<template #default="scope"> {{ scope.row.buildingName }}{{ scope.row.roomName }} </template>
</el-table-column>
<el-table-column label="假期类型" align="center" prop="holidayType">
<template #default="scope">
<dict-tag :options="xg_holiday_type" :value="scope.row.holidayType" />
</template>
</el-table-column>
<el-table-column label="留宿时间" align="center" show-overflow-tooltip>
<template #default="scope"> {{ scope.row.applyStartDate }} 至 {{ scope.row.applyEndDate }} </template>
</el-table-column>
<el-table-column label="留宿原因类型" align="center" prop="stayReasonType">
<template #default="scope">
<dict-tag :options="xg_stay_reason_type" :value="scope.row.stayReasonType" />
</template>
</el-table-column>
<el-table-column label="联系电话" align="center" prop="studentPhone" />
<el-table-column label="紧急联系人" align="center" prop="emergencyContact" />
<el-table-column label="操作" align="center" width="150" fixed="right">
<template #default="scope">
<el-tooltip content="查看" placement="top">
<el-button
link
type="primary"
icon="View"
@click="handleView(scope.row)"
v-hasPermi="['studentDynamics:holidayStay:query']"
></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
<!-- 查看详情弹窗 -->
<HolidayStayDetail ref="detailRef" @refresh="getList" />
</div>
</template>
<script setup lang="ts" name="HolidayStayTab">
import { getCurrentInstance, ref, onMounted, toRefs } from 'vue';
import { ElMessage } from 'element-plus';
import { listHolidayStay, type HolidayStayQuery } from '@/api/xg/studentDynamics';
import HolidayStayDetail from './HolidayStayDetail.vue';
const { proxy } = getCurrentInstance();
// 字典数据
const { xg_holiday_type, xg_stay_reason_type } = toRefs(proxy?.useDict('xg_holiday_type', 'xg_stay_reason_type') || {});
// 数据状态
const loading = ref(true);
const showSearch = ref(true);
const dataList = ref([]);
const total = ref(0);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
// 查询参数
const queryParams = ref<HolidayStayQuery & any>({
pageNum: 1,
pageSize: 10,
studentName: undefined,
studentNo: undefined,
collegeName: undefined,
majorName: undefined,
className: undefined,
holidayType: undefined,
applyStartDate: undefined,
applyEndDate: undefined,
status: undefined
});
// 表单引用
const queryFormRef = ref();
const detailRef = ref();
/**
* 查询列表数据
*/
const getList = async () => {
loading.value = true;
try {
const response = await listHolidayStay(queryParams.value);
dataList.value = response.rows;
total.value = response.total;
} catch (error) {
console.error('获取假期留宿信息列表失败:', error);
ElMessage.error('获取数据失败');
} finally {
loading.value = false;
}
};
/**
* 搜索按钮操作
*/
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/**
* 重置按钮操作
*/
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
};
/**
* 多选框选中数据
*/
const handleSelectionChange = (selection: any) => {
ids.value = selection.map((item: any) => item.id);
single.value = selection.length !== 1;
multiple.value = !selection.length;
};
/**
* 查看详情
*/
const handleView = (row: any) => {
detailRef.value?.open(row.id);
};
// 页面初始化
onMounted(() => {
getList();
});
</script>
<style scoped>
/* 页面容器样式 */
.page-container {
padding: 0;
}
.mb-[10px] {
margin-bottom: 10px;
}
.mb8 {
margin-bottom: 8px;
}
/* 搜索按钮容器样式 */
.search-btn-container {
display: flex;
justify-content: flex-end;
margin-top: 10px;
}
.search-btn-container .el-form-item {
margin-bottom: 0 !important;
}
.card-title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
</style>
@@ -0,0 +1,21 @@
<template>
<el-dialog title="学生请假详情" v-model="dialogOpen" width="800px" append-to-body>
<div>学生请假详情组件待实现</div>
<template #footer>
<el-button @click="dialogOpen = false">关闭</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="StudentLeaveDetail">
import { ref } from 'vue';
const dialogOpen = ref(false);
const open = (id: number) => {
console.log('打开学生请假详情,ID:', id);
dialogOpen.value = true;
};
defineExpose({ open });
</script>
@@ -0,0 +1,271 @@
<template>
<div class="page-container">
<!-- 搜索表单 -->
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" style="margin-bottom: 10px">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" label-width="100px">
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="学生姓名" prop="studentName">
<el-input v-model="queryParams.studentName" placeholder="请输入学生姓名" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="学号" prop="studentNo">
<el-input v-model="queryParams.studentNo" placeholder="请输入学号" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="学院名称" prop="collegeName">
<el-input v-model="queryParams.collegeName" placeholder="请输入学院名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="专业名称" prop="majorName">
<el-input v-model="queryParams.majorName" placeholder="请输入专业名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="班级名称" prop="className">
<el-input v-model="queryParams.className" placeholder="请输入班级名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="请假类型" prop="leaveTypeName">
<el-input v-model="queryParams.leaveTypeName" placeholder="请输入请假类型名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="开始时间" prop="startTime">
<el-date-picker
clearable
v-model="queryParams.startTime"
type="date"
style="width: 100%"
value-format="YYYY-MM-DD"
placeholder="请选择开始时间"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="结束时间" prop="endTime">
<el-date-picker
clearable
v-model="queryParams.endTime"
type="date"
style="width: 100%"
value-format="YYYY-MM-DD"
placeholder="请选择结束时间"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="审批状态" prop="approvalStatus">
<el-select v-model="queryParams.approvalStatus" placeholder="请选择审批状态" clearable style="width: 100%">
<el-option v-for="dict in student_leave_approval_status" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24" class="search-btn-container">
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
</div>
</transition>
<!-- 数据表格 -->
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<span class="card-title">学生请假信息列表</span>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table v-loading="loading" :data="dataList" @selection-change="handleSelectionChange">
<el-table-column label="学生姓名" align="center" prop="studentName" />
<el-table-column label="学号" align="center" prop="studentNo" />
<el-table-column label="学院" align="center" prop="collegeName" show-overflow-tooltip />
<el-table-column label="专业" align="center" prop="majorName" show-overflow-tooltip />
<el-table-column label="班级" align="center" prop="className" show-overflow-tooltip />
<el-table-column label="请假类型" align="center" prop="leaveTypeName" />
<el-table-column label="请假时间" align="center" show-overflow-tooltip>
<template #default="scope"> {{ scope.row.startTime }} 至 {{ scope.row.endTime }} </template>
</el-table-column>
<el-table-column label="请假时长" align="center">
<template #default="scope"> {{ scope.row.leaveDuration }}天 </template>
</el-table-column>
<el-table-column label="是否离校" align="center">
<template #default="scope">
<dict-tag :options="sys_yesno" :value="scope.row.isLeaveSchool" />
</template>
</el-table-column>
<el-table-column label="审批状态" align="center">
<template #default="scope">
<dict-tag :options="student_leave_approval_status" :value="scope.row.approvalStatus" />
</template>
</el-table-column>
<el-table-column label="销假状态" align="center">
<template #default="scope">
<dict-tag :options="xg_xsqj_cancel_status" :value="scope.row.cancelStatus" />
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="150" fixed="right">
<template #default="scope">
<el-tooltip content="查看" placement="top">
<el-button
link
type="primary"
icon="View"
@click="handleView(scope.row)"
v-hasPermi="['studentDynamics:studentLeave:query']"
></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
</div>
</template>
<script setup lang="ts" name="StudentLeaveTab">
import { getCurrentInstance, ref, onMounted, toRefs } from 'vue';
import { listStudentLeave, type StudentLeaveQuery } from '@/api/xg/studentDynamics';
const { proxy } = getCurrentInstance();
const { sys_yesno, student_leave_approval_status, xg_xsqj_cancel_status } = toRefs(
proxy?.useDict('sys_yesno', 'student_leave_approval_status', 'xg_xsqj_cancel_status') || {}
);
// 数据状态
const loading = ref(true);
const showSearch = ref(true);
const dataList = ref([]);
const total = ref(0);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
// 查询参数
const queryParams = ref<StudentLeaveQuery & any>({
pageNum: 1,
pageSize: 10,
studentName: undefined,
studentNo: undefined,
collegeName: undefined,
majorName: undefined,
className: undefined,
leaveTypeName: undefined,
approvalStatus: undefined,
startTime: undefined,
endTime: undefined
});
// 表单引用
const queryFormRef = ref();
/**
* 查询列表数据
*/
const getList = async () => {
loading.value = true;
try {
const response = await listStudentLeave(queryParams.value);
dataList.value = response.rows;
total.value = response.total;
} catch (error) {
console.error('获取学生请假信息列表失败:', error);
proxy?.$modal.msgError('获取数据失败');
} finally {
loading.value = false;
}
};
/**
* 搜索按钮操作
*/
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/**
* 重置按钮操作
*/
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
};
/**
* 多选框选中数据
*/
const handleSelectionChange = (selection: any) => {
ids.value = selection.map((item: any) => item.id);
single.value = selection.length !== 1;
multiple.value = !selection.length;
};
/**
* 查看详情
*/
const handleView = (row: any) => {
// 跳转到学生请假详情页面
proxy.$router.push({
path: `/xg/xsjcxx/xsqj/leaveEdit/index`,
query: {
id: row.id,
type: 'update'
}
});
};
// 页面初始化
onMounted(() => {
getList();
});
</script>
<style scoped>
/* 页面容器样式 */
.page-container {
padding: 0;
}
.mb8 {
margin-bottom: 8px;
}
/* 搜索按钮容器样式 */
.search-btn-container {
display: flex;
justify-content: flex-end;
margin-top: 10px;
}
.search-btn-container .el-form-item {
margin-bottom: 0 !important;
}
.card-title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
</style>
@@ -0,0 +1,80 @@
<template>
<div class="p-2">
<!-- Tab页组件 -->
<el-tabs v-model="activeTab" @tab-click="handleTabClick" class="mb-4">
<!-- 调宿/退宿信息Tab页 -->
<el-tab-pane label="调宿/退宿信息" name="dormApplication">
<DormApplicationTab v-if="activeTab === 'dormApplication'" ref="dormApplicationTabRef" />
</el-tab-pane>
<!-- 假期留宿信息Tab页 -->
<el-tab-pane label="假期留宿信息" name="holidayStay">
<HolidayStayTab v-if="activeTab === 'holidayStay'" ref="holidayStayTabRef" />
</el-tab-pane>
<!-- 请假信息Tab页 -->
<el-tab-pane label="请假信息" name="studentLeave">
<StudentLeaveTab v-if="activeTab === 'studentLeave'" ref="studentLeaveTabRef" />
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup lang="ts" name="StudentDynamics">
import { ref, onMounted } from 'vue';
import DormApplicationTab from './components/DormApplicationTab.vue';
import HolidayStayTab from './components/HolidayStayTab.vue';
import StudentLeaveTab from './components/StudentLeaveTab.vue';
// Tab页状态
const activeTab = ref('dormApplication');
// 子组件引用
const dormApplicationTabRef = ref();
const holidayStayTabRef = ref();
const studentLeaveTabRef = ref();
/**
* Tab页切换处理
*/
const handleTabClick = (tab: any) => {
console.log('切换到Tab页:', tab.name);
// 可以在这里添加Tab切换的逻辑
};
/**
* 页面初始化
*/
onMounted(() => {
console.log('学生动向信息管理页面已加载');
});
</script>
<style scoped>
.p-2 {
padding: 8px;
}
.mb-4 {
margin-bottom: 16px;
}
/* Tab页样式优化 */
:deep(.el-tabs__header) {
margin-bottom: 20px;
}
:deep(.el-tabs__item) {
font-size: 14px;
font-weight: 500;
}
:deep(.el-tabs__item.is-active) {
/* color: #409eff; */
font-weight: 600;
}
:deep(.el-tabs__content) {
padding: 0;
}
</style>
@@ -183,7 +183,7 @@
<el-table-column label="校区ID,关联xg_dorm_campus.id" align="center" prop="campusId" v-if="false" />
<el-table-column label="校区" align="center" prop="campusName" />
<el-table-column label="宿舍区ID,关联xg_dorm_area.id" align="center" prop="areaId" v-if="false" />
<el-table-column label="宿舍区" align="center" prop="areaName" />
<el-table-column label="宿舍区" align="center" prop="areaName" v-if="false" />
<el-table-column label="宿舍楼ID,关联xg_dorm_building.id" align="center" prop="buildingId" v-if="false" />
<el-table-column label="宿舍楼" align="center" prop="buildingName" />
<el-table-column label="房间ID,关联xg_dorm_room.id" align="center" prop="roomId" v-if="false" />
@@ -180,7 +180,7 @@
<el-table-column label="校区ID,关联xg_dorm_campus.id" align="center" prop="campusId" v-if="false" />
<el-table-column label="校区" align="center" prop="campusName" />
<el-table-column label="宿舍区ID,关联xg_dorm_area.id" align="center" prop="areaId" v-if="false" />
<el-table-column label="宿舍区" align="center" prop="areaName" />
<el-table-column label="宿舍区" align="center" prop="areaName" v-if="false" />
<el-table-column label="宿舍楼ID,关联xg_dorm_building.id" align="center" prop="buildingId" v-if="false" />
<el-table-column label="宿舍楼" align="center" prop="buildingName" />
<el-table-column label="房间ID,关联xg_dorm_room.id" align="center" prop="roomId" v-if="false" />
+23 -4
View File
@@ -307,7 +307,7 @@ import ApprovalButton from '@/components/Process/approvalButton.vue';
import { AxiosResponse } from 'axios';
import { StartProcessBo } from '@/api/workflow/workflowCommon/types';
import { addStudentLeave, getStudentLeave, updateStudentLeave } from '@/api/workflow/studentLeave/studentLeave';
import { getStudentMsg, getClassTeacherId, getCollegeLeaveApprovers } from '@/api/xg/xsjcxx/studentMsg/studentMsg';
import { getStudentMsg, getClassTeacherId, getCollegeLeaveApprovers, getCollegeLeaders } from '@/api/xg/xsjcxx/studentMsg/studentMsg';
import { startWorkFlow } from '@/api/workflow/task';
import { StudentLeaveForm, StudentLeaveVO } from '@/api/workflow/studentLeave/studentLeave/types';
import { StudentMsgVO } from '@/api/xg/xsjcxx/studentMsg/studentMsg/types';
@@ -890,7 +890,7 @@ const handleStartWorkFlow = async (data: StudentLeaveForm) => {
// 1. 调用接口获取班主任ID
let classTeacherId: number | null = null;
try {
const classTeacherResponse = await getClassTeacherId(data.studentId);
const classTeacherResponse: any = await getClassTeacherId(data.studentId);
if (classTeacherResponse && classTeacherResponse.data) {
classTeacherId = classTeacherResponse.data;
console.log('获取到班主任ID:', classTeacherId);
@@ -908,7 +908,7 @@ const handleStartWorkFlow = async (data: StudentLeaveForm) => {
// 2. 调用接口获取学院请假审批人列表
let collegeApprovers: number[] = [];
try {
const collegeApproversResponse = await getCollegeLeaveApprovers(data.studentId);
const collegeApproversResponse: any = await getCollegeLeaveApprovers(data.studentId);
if (collegeApproversResponse && collegeApproversResponse.data) {
collegeApprovers = collegeApproversResponse.data;
console.log('获取到学院请假审批人列表:', collegeApprovers);
@@ -923,6 +923,24 @@ const handleStartWorkFlow = async (data: StudentLeaveForm) => {
return; // 停止后续操作
}
// 3. 调用接口获取二级学院分管领导ID列表
let collegeLeaders: number[] = [];
try {
const collegeLeadersResponse: any = await getCollegeLeaders(data.studentId);
if (collegeLeadersResponse && collegeLeadersResponse.data) {
collegeLeaders = collegeLeadersResponse.data;
console.log('获取到二级学院分管领导ID列表:', collegeLeaders);
} else {
console.warn('获取二级学院分管领导ID列表失败');
ElMessage.error('获取二级学院分管领导信息失败,请稍后重试');
return; // 停止后续操作
}
} catch (error) {
console.error('调用获取二级学院分管领导接口失败:', error);
ElMessage.error('获取二级学院分管领导信息失败,请稍后重试');
return; // 停止后续操作
}
submitFormData.value.flowCode = flowCode.value;
submitFormData.value.businessId = data.id;
// 流程变量
@@ -935,7 +953,8 @@ const handleStartWorkFlow = async (data: StudentLeaveForm) => {
// userList: ['1', '3', '4'],
// 新增的流程变量
bzrspr: classTeacherId,
ejxyspr: collegeApprovers
ejxyspr: collegeApprovers,
ejxyfgld: collegeLeaders
};
submitFormData.value.variables = taskVariables.value;
const resp = await startWorkFlow(submitFormData.value);