兵马未动 粮草先行

This commit is contained in:
RISE
2026-02-08 23:47:50 +08:00
parent 00a005e65f
commit 2670340af3
47 changed files with 3909 additions and 257 deletions

View File

@@ -89,9 +89,11 @@
style="width: 100%">
<el-option
v-for="item in bedNoList"
:key="item"
:label="item"
:value="item">
:key="item.bedNo"
:value="item.bedNo">
<span :class="{ 'bed-option-occupied': item.haveStudent }">
{{ item.bedNo }}{{ item.haveStudent ? ' (有人)' : '' }}
</span>
</el-option>
</el-select>
</el-form-item>
@@ -138,7 +140,8 @@ const loading = ref(false)
const classList = ref<any[]>([])
const studentList = ref<any[]>([])
const roomList = ref<any[]>([])
const bedNoList = ref<string[]>([])
// 床位列表:支持 haveStudent 标记true=有人false=无人)
const bedNoList = ref<Array<{ bedNo: string; haveStudent: boolean }>>([])
// 提交表单数据
const form = reactive({
@@ -206,28 +209,33 @@ const handleStudentChange = (stuNo: string) => {
}
}
// 房间号变化时获取床位号列表
// 房间号变化时获取床位号列表(支持 haveStudenttrue=有人false=无人)
const handleRoomChange = async (roomNo: string) => {
if (!roomNo) {
bedNoList.value = []
form.bedNo = ''
return
}
const toBedItem = (item: any): { bedNo: string; haveStudent: boolean } => {
if (typeof item === 'number' || typeof item === 'string') {
return { bedNo: String(item), haveStudent: false }
}
return {
bedNo: String(item?.bedNo ?? item?.value ?? item ?? ''),
haveStudent: !!(item && item.haveStudent === true)
}
}
try {
const res = await fearchRoomStuNum(roomNo)
if (res.data) {
// 根据返回的数据结构处理床位号列表
// 假设返回的是数字数组或对象数组
if (Array.isArray(res.data)) {
bedNoList.value = res.data.map((item: any) => {
if (typeof item === 'number' || typeof item === 'string') {
return String(item)
}
return String(item.bedNo || item.value || item)
})
bedNoList.value = res.data.map(toBedItem).filter((b) => b.bedNo)
} else if (res.data.bedNos && Array.isArray(res.data.bedNos)) {
bedNoList.value = res.data.bedNos.map((item: any) => String(item))
bedNoList.value = res.data.bedNos.map((item: any) =>
toBedItem(typeof item === 'object' ? item : { bedNo: String(item), haveStudent: false })
)
} else {
bedNoList.value = []
}
@@ -321,3 +329,8 @@ defineExpose({
})
</script>
<style scoped lang="scss">
.bed-option-occupied {
color: var(--el-color-warning);
}
</style>

View File

@@ -211,9 +211,13 @@
<el-icon><component :is="columnConfigMap[col.prop || '']?.icon || OfficeBuilding" /></el-icon>
<span style="margin-left: 4px">{{ col.label }}</span>
</template>
<!-- 床位号列特殊模板 -->
<!-- 床位号列haveStudent true 时变色标记有人 -->
<template v-if="col.prop === 'bedNo'" #default="scope">
<el-tag v-if="scope.row.bedNo" size="small" type="info" effect="plain">
<el-tag
v-if="scope.row.bedNo"
size="small"
:type="scope.row.haveStudent ? 'warning' : 'info'"
effect="plain">
{{ scope.row.bedNo }}
</el-tag>
<span v-else>-</span>
@@ -274,6 +278,12 @@
<!-- 转宿弹窗 -->
<TransferDialog ref="transferDialogRef" @refresh="getDataList" />
<!-- 宿舍互换弹窗 -->
<SwapDialog ref="swapDialogRef" @refresh="getDataList" />
<!-- 打印宿舍卡弹窗 -->
<PrintCardDialog ref="printCardDialogRef" />
</div>
</template>
@@ -281,7 +291,7 @@
import { reactive, ref, onMounted, computed, nextTick } from 'vue'
import { useRoute } from 'vue-router'
import { BasicTableProps, useTable } from "/@/hooks/table";
import { fetchList, delObjs } from "/@/api/stuwork/dormroomstudent";
import { fetchList, delObjs, exportEmptyPeopleRoomExcel } from "/@/api/stuwork/dormroomstudent";
import { getDeptList } from "/@/api/basic/basicclass";
import { getBuildingList } from "/@/api/stuwork/dormbuilding";
import { fetchDormRoomTreeList } from "/@/api/stuwork/dormroom";
@@ -289,6 +299,8 @@ import { useMessage, useMessageBox } from "/@/hooks/message";
import TableColumnControl from '/@/components/TableColumnControl/index.vue'
import FormDialog from './form.vue';
import TransferDialog from './transfer.vue';
import SwapDialog from './swap.vue';
import PrintCardDialog from './printCard.vue';
import TreeSelect from '/@/components/TreeSelect/index.vue';
import { List, OfficeBuilding, House, Grid, UserFilled, Phone, CreditCard, Avatar, User, Setting, Menu, Search, Document } from '@element-plus/icons-vue'
import { useTableColumnControl } from '/@/hooks/tableColumn'
@@ -301,6 +313,8 @@ const route = useRoute()
const formDialogRef = ref()
const columnControlRef = ref<any>()
const transferDialogRef = ref()
const swapDialogRef = ref()
const printCardDialogRef = ref()
const searchFormRef = ref()
const showSearch = ref(true)
const deptList = ref<any[]>([])
@@ -425,29 +439,91 @@ const handleDormDataTypeChange = (dormdataType: string) => {
getDormRoomTreeListData(dormdataType)
}
// 打印宿舍卡
// 打印宿舍卡(按房间号查询后打印)
const handlePrintCard = () => {
useMessage().warning('功能开发中')
printCardDialogRef.value?.openDialog()
}
// 宿舍互换
// 宿舍互换(两名学生互换宿舍)
const handleRoomSwap = () => {
useMessage().warning('功能开发中')
const query = {
deptCode: searchForm.deptCode,
buildingNo: searchForm.buildingNo,
gender: searchForm.gender,
roomNo: searchForm.roomNo || searchForm.roomNoInput,
classNo: searchForm.classNo,
stuNo: searchForm.stuNo,
realName: searchForm.realName
}
swapDialogRef.value?.openDialog(query)
}
// 导出
const handleExport = () => {
useMessage().warning('功能开发中')
// 导出:空 n 人宿舍导出(按当前筛选条件传参)
const handleExport = async () => {
try {
const params = {
deptCode: searchForm.deptCode,
buildingNo: searchForm.buildingNo,
gender: searchForm.gender,
dormdataType: searchForm.dormdataType,
roomNo: searchForm.roomNo || searchForm.roomNoInput,
classNo: searchForm.classNo,
stuNo: searchForm.stuNo,
realName: searchForm.realName
}
const res = await exportEmptyPeopleRoomExcel(params)
const blob = res instanceof Blob ? res : new Blob([res as BlobPart], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `空宿舍导出_${Date.now()}.xlsx`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
useMessage().success('导出成功')
} catch (err: any) {
useMessage().error(err?.msg || '导出失败')
}
}
// 名单导出
const handleExportList = () => {
useMessage().warning('功能开发中')
// 名单导出:与导出共用空 n 人宿舍导出接口,文件名区分
const handleExportList = async () => {
try {
const params = {
deptCode: searchForm.deptCode,
buildingNo: searchForm.buildingNo,
gender: searchForm.gender,
dormdataType: searchForm.dormdataType,
roomNo: searchForm.roomNo || searchForm.roomNoInput,
classNo: searchForm.classNo,
stuNo: searchForm.stuNo,
realName: searchForm.realName
}
const res = await exportEmptyPeopleRoomExcel(params)
const blob = res instanceof Blob ? res : new Blob([res as BlobPart], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `住宿学生名单_${Date.now()}.xlsx`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
useMessage().success('导出成功')
} catch (err: any) {
useMessage().error(err?.msg || '导出失败')
}
}
// 编辑(与转宿共用接口 edit修改房间/床位/是否舍长)
const handleEdit = (row: any) => {
transferDialogRef.value?.openDialog(row)
}
// 转宿
const handleTransfer = (row: any) => {
transferDialogRef.value.openDialog(row)
transferDialogRef.value?.openDialog(row)
}
// 退宿

View File

@@ -0,0 +1,152 @@
<template>
<el-dialog
title="打印宿舍卡"
v-model="visible"
:close-on-click-modal="false"
width="640px"
destroy-on-close
@closed="onClosed">
<div v-loading="loading">
<el-form :inline="true" class="mb16">
<el-form-item label="房间号">
<el-input v-model="roomNo" placeholder="请输入房间号" clearable style="width: 180px" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleFetch">查询并预览</el-button>
</el-form-item>
</el-form>
<div v-if="printData.length" ref="printAreaRef" class="print-area">
<div v-for="(room, idx) in printData" :key="idx" class="room-card mb16">
<div class="room-title">房间号{{ room.roomNo }}</div>
<table class="print-table">
<thead>
<tr>
<th>姓名</th>
<th>学号</th>
<th>床位</th>
<th>是否舍长</th>
<th>班级</th>
</tr>
</thead>
<tbody>
<tr v-for="(s, i) in (room.dormRoomStudentVOList || [])" :key="i">
<td>{{ s.realName }}</td>
<td>{{ s.stuNo }}</td>
<td>{{ s.bedNo }}</td>
<td>{{ s.isLeader === '1' ? '是' : '否' }}</td>
<td>{{ s.className || s.classNo }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false"> </el-button>
<el-button type="primary" @click="handlePrint" :disabled="!printData.length"> </el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="DormRoomStudentPrintCard">
import { ref } from 'vue'
import { useMessage } from '/@/hooks/message'
import { printDormRoomData } from '/@/api/stuwork/dormroomstudent'
const visible = ref(false)
const loading = ref(false)
const roomNo = ref('')
const printData = ref<any[]>([])
const printAreaRef = ref<HTMLElement>()
const openDialog = (initialRoomNo?: string) => {
visible.value = true
roomNo.value = initialRoomNo || ''
printData.value = []
}
const onClosed = () => {
printData.value = []
}
const handleFetch = async () => {
const no = (roomNo.value || '').trim()
if (!no) {
useMessage().warning('请输入房间号')
return
}
loading.value = true
try {
const res = await printDormRoomData(no)
const data = res?.data ?? res
printData.value = Array.isArray(data) ? data : (data ? [data] : [])
if (!printData.value.length) {
useMessage().info('该房间暂无数据')
}
} catch (err: any) {
useMessage().error(err?.msg || '查询失败')
printData.value = []
} finally {
loading.value = false
}
}
const handlePrint = () => {
if (!printData.value.length) return
const win = window.open('', '_blank')
if (!win) {
useMessage().error('无法打开打印窗口')
return
}
const el = printAreaRef.value
if (el) {
win.document.write(`
<!DOCTYPE html><html><head><meta charset="utf-8"><title>宿舍卡</title>
<style>
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #333; padding: 6px 8px; text-align: center; }
.room-title { font-weight: bold; margin-bottom: 8px; }
</style>
</head><body>${el.innerHTML}</body></html>
`)
win.document.close()
win.focus()
setTimeout(() => {
win.print()
win.close()
}, 300)
}
}
defineExpose({ openDialog })
</script>
<style scoped lang="scss">
.print-area {
max-height: 60vh;
overflow: auto;
}
.room-card {
padding: 12px;
border: 1px solid var(--el-border-color);
border-radius: 4px;
}
.room-title {
margin-bottom: 8px;
}
.print-table {
width: 100%;
border-collapse: collapse;
th,
td {
border: 1px solid var(--el-border-color);
padding: 6px 8px;
text-align: center;
}
}
.mb16 {
margin-bottom: 16px;
}
</style>

View File

@@ -0,0 +1,136 @@
<template>
<el-dialog
title="宿舍互换"
v-model="visible"
:close-on-click-modal="false"
draggable
width="520px">
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="100px"
:validate-on-rule-change="false"
v-loading="loading">
<el-form-item label="学生一" prop="sourceSutNo">
<el-select
v-model="form.sourceSutNo"
placeholder="请选择学生(学号)"
clearable
filterable
style="width: 100%"
@change="onSourceChange">
<el-option
v-for="item in studentOptions"
:key="item.stuNo"
:label="`${item.realName}${item.stuNo}${item.roomNo || ''}`"
:value="item.stuNo" />
</el-select>
</el-form-item>
<el-form-item label="学生二" prop="targetStuNO">
<el-select
v-model="form.targetStuNO"
placeholder="请选择学生(学号)"
clearable
filterable
style="width: 100%"
@change="onTargetChange">
<el-option
v-for="item in studentOptions"
:key="item.stuNo"
:label="`${item.realName}${item.stuNo}${item.roomNo || ''}`"
:value="item.stuNo" />
</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" :loading="submitting"> </el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="DormRoomStudentSwapDialog">
import { ref, reactive } from 'vue'
import { useMessage } from '/@/hooks/message'
import { exchangeRoom } from '/@/api/stuwork/dormroomstudent'
import { fetchList } from '/@/api/stuwork/dormroomstudent'
const emit = defineEmits(['refresh'])
const formRef = ref()
const visible = ref(false)
const loading = ref(false)
const submitting = ref(false)
const studentOptions = ref<any[]>([])
const form = reactive({
sourceSutNo: '',
targetStuNO: ''
})
const rules = {
sourceSutNo: [{ required: true, message: '请选择学生一', trigger: 'change' }],
targetStuNO: [{ required: true, message: '请选择学生二', trigger: 'change' }]
}
const onSourceChange = () => {
if (form.sourceSutNo && form.targetStuNO && form.sourceSutNo === form.targetStuNO) {
form.targetStuNO = ''
}
}
const onTargetChange = () => {
if (form.sourceSutNo && form.targetStuNO && form.sourceSutNo === form.targetStuNO) {
form.sourceSutNo = ''
}
}
const openDialog = async (queryParams?: any) => {
visible.value = true
form.sourceSutNo = ''
form.targetStuNO = ''
loading.value = true
try {
const res = await fetchList({
current: 1,
size: 500,
...queryParams
})
const list = res?.data?.records ?? res?.records ?? []
studentOptions.value = Array.isArray(list) ? list : []
} catch (err) {
studentOptions.value = []
} finally {
loading.value = false
}
}
const onSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid: boolean) => {
if (!valid) return
if (form.sourceSutNo === form.targetStuNO) {
useMessage().warning('请选择两名不同学生')
return
}
submitting.value = true
try {
await exchangeRoom({
sourceSutNo: form.sourceSutNo,
targetStuNO: form.targetStuNO
})
useMessage().success('互换成功')
visible.value = false
emit('refresh')
} catch (err: any) {
console.error(err)
} finally {
submitting.value = false
}
})
}
defineExpose({ openDialog })
</script>

View File

@@ -41,9 +41,11 @@
style="width: 100%">
<el-option
v-for="item in bedNoList"
:key="item"
:label="item"
:value="item">
:key="item.bedNo"
:value="item.bedNo">
<span :class="{ 'bed-option-occupied': item.haveStudent }">
{{ item.bedNo }}{{ item.haveStudent ? ' (有人)' : '' }}
</span>
</el-option>
</el-select>
</el-form-item>
@@ -95,7 +97,8 @@ const dataFormRef = ref()
const visible = ref(false)
const loading = ref(false)
const roomList = ref<any[]>([])
const bedNoList = ref<string[]>([])
// 床位列表:支持 haveStudent 标记true=有人false=无人)
const bedNoList = ref<Array<{ bedNo: string; haveStudent: boolean }>>([])
// 提交表单数据
const form = reactive({
@@ -119,26 +122,33 @@ const dataRules = {
]
}
// 房间号变化时获取床位号列表
// 房间号变化时获取床位号列表(支持 haveStudenttrue=有人false=无人)
const handleRoomChange = async (roomNo: string) => {
if (!roomNo) {
bedNoList.value = []
form.bedNo = ''
return
}
const toBedItem = (item: any): { bedNo: string; haveStudent: boolean } => {
if (typeof item === 'number' || typeof item === 'string') {
return { bedNo: String(item), haveStudent: false }
}
return {
bedNo: String(item?.bedNo ?? item?.value ?? item ?? ''),
haveStudent: !!(item && item.haveStudent === true)
}
}
try {
const res = await fearchRoomStuNum(roomNo)
if (res.data) {
if (Array.isArray(res.data)) {
bedNoList.value = res.data.map((item: any) => {
if (typeof item === 'number' || typeof item === 'string') {
return String(item)
}
return String(item.bedNo || item.value || item)
})
bedNoList.value = res.data.map(toBedItem).filter((b) => b.bedNo)
} else if (res.data.bedNos && Array.isArray(res.data.bedNos)) {
bedNoList.value = res.data.bedNos.map((item: any) => String(item))
bedNoList.value = res.data.bedNos.map((item: any) =>
toBedItem(typeof item === 'object' ? item : { bedNo: String(item), haveStudent: false })
)
} else {
bedNoList.value = []
}
@@ -157,20 +167,20 @@ const openDialog = async (row: any) => {
visible.value = true
// 重置表单数据
nextTick(() => {
dataFormRef.value?.resetFields()
form.id = row.id || ''
form.roomNo = row.roomNo || ''
await nextTick()
dataFormRef.value?.resetFields()
form.id = row.id || ''
form.roomNo = row.roomNo || ''
form.bedNo = row.bedNo || ''
form.stuNo = row.stuNo || ''
form.isLeader = row.isLeader || '0'
bedNoList.value = []
// 如果有房间号,先拉取床位列表再回填床位号(避免 handleRoomChange 清空 bedNo
if (form.roomNo) {
await handleRoomChange(form.roomNo)
form.bedNo = row.bedNo || ''
form.stuNo = row.stuNo || ''
form.isLeader = row.isLeader || '0'
bedNoList.value = []
// 如果有房间号,获取床位号列表
if (form.roomNo) {
handleRoomChange(form.roomNo)
}
})
}
}
// 提交表单
@@ -224,3 +234,8 @@ defineExpose({
})
</script>
<style scoped lang="scss">
.bed-option-occupied {
color: var(--el-color-warning);
}
</style>