修复住宿:实现学员住宿安排分配UI流程及修复modal异常
This commit is contained in:
1 parent
62085e8a49
commit
2f05f310ad
4 files changed
+776
-9
No files matched your search
@@ -7,6 +7,10 @@ export default {
|
||||
msg(content) {
|
||||
Message.info(content)
|
||||
},
|
||||
// 信息消息
|
||||
msgInfo(content) {
|
||||
Message.info(content)
|
||||
},
|
||||
// 错误消息
|
||||
msgError(content) {
|
||||
Message.error(content)
|
||||
|
||||
@@ -270,7 +270,7 @@ export default {
|
||||
this.handleQuery()
|
||||
},
|
||||
handleAdd() {
|
||||
this.$modal.msgInfo('请在学员管理或报名名单中点击“住宿安排”办理分配')
|
||||
this.$modal.msgInfo('请在【培训计划】->【参训人员】名单中点击对应学员的“住宿安排”办理分配')
|
||||
},
|
||||
handleCheckIn(row) {
|
||||
this.$modal.confirm('确认办理学员 [' + (row.studentName || row.studentId) + '] 的入住手续?').then(() => {
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" width="180" fixed="right">
|
||||
<el-table-column label="操作" align="center" width="240" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
v-hasPermi="['train:enrollment:query']"
|
||||
@@ -98,6 +98,13 @@
|
||||
icon="el-icon-document"
|
||||
@click="handleViewDetail(scope.row)"
|
||||
>学员档案</el-button>
|
||||
<el-button
|
||||
v-hasPermi="['train:accommodation:query', 'train:accommodation:add']"
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-office-building"
|
||||
@click="handleOpenAccommodation(scope.row)"
|
||||
>住宿安排</el-button>
|
||||
<el-button
|
||||
v-if="scope.row.enrollmentStatus === 0"
|
||||
v-hasPermi="['train:report:confirm']"
|
||||
@@ -306,6 +313,25 @@
|
||||
|
||||
<div style="margin-top: 15px;" />
|
||||
<el-descriptions title="住宿安排" :column="2" border size="small">
|
||||
<template slot="extra">
|
||||
<el-button
|
||||
v-if="!detailDialog.data.accommodation || !detailDialog.data.accommodation.room"
|
||||
v-hasPermi="['train:accommodation:add']"
|
||||
size="mini"
|
||||
type="primary"
|
||||
plain
|
||||
icon="el-icon-plus"
|
||||
@click="handleOpenAccommodationFromDetail"
|
||||
>办理住宿分配</el-button>
|
||||
<el-button
|
||||
v-else
|
||||
v-hasPermi="['train:accommodation:query']"
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-view"
|
||||
@click="handleOpenAccommodationFromDetail"
|
||||
>查看/变更住宿</el-button>
|
||||
</template>
|
||||
<el-descriptions-item label="楼栋">{{ (detailDialog.data.accommodation && detailDialog.data.accommodation.building) || '未分配' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="房间">{{ (detailDialog.data.accommodation && detailDialog.data.accommodation.room) || '未分配' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="床位号">{{ (detailDialog.data.accommodation && detailDialog.data.accommodation.bedNo) || '未分配' }}</el-descriptions-item>
|
||||
@@ -333,6 +359,209 @@
|
||||
<el-button @click="detailDialog.open = false">关 闭</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 学员住宿安排对话框 (ACCOM-ASSIGN-01) -->
|
||||
<el-dialog
|
||||
:title="`学员住宿安排 - ${accommodationDialog.currentStudent.studentName || ''}`"
|
||||
:visible.sync="accommodationDialog.open"
|
||||
width="680px"
|
||||
append-to-body
|
||||
>
|
||||
<div v-loading="accommodationDialog.loading">
|
||||
<!-- 学员基本信息卡片(只读绑定上下文,不得让用户手工输入内部ID) -->
|
||||
<el-descriptions title="参训学员信息" :column="3" border size="small" style="margin-bottom: 16px;">
|
||||
<el-descriptions-item label="学员姓名">{{ accommodationDialog.currentStudent.studentName || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="学号">{{ accommodationDialog.currentStudent.studentId || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="性别">{{ formatGender(accommodationDialog.currentStudent.gender) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="参训编号">{{ accommodationDialog.currentStudent.enrollmentId || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="所属班级">{{ planId || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="住宿需求">
|
||||
<el-tag size="mini" :type="accommodationDialog.currentStudent.accommodationRequired === 1 ? 'warning' : 'info'">
|
||||
{{ accommodationDialog.currentStudent.accommodationRequired === 1 ? '需住宿' : '不住宿' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<!-- 当前住宿状态分支 -->
|
||||
<!-- 分支 A:已有有效住宿记录 -->
|
||||
<div v-if="accommodationDialog.currentAccommodation && accommodationDialog.currentAccommodation.accommodationId" style="margin-bottom: 15px;">
|
||||
<div style="font-weight: bold; font-size: 14px; margin-bottom: 10px; color: #303133;">
|
||||
<i class="el-icon-circle-check" style="color: #67C23A; margin-right: 5px;" />当前住宿安排
|
||||
</div>
|
||||
<el-descriptions :column="2" border size="small">
|
||||
<el-descriptions-item label="宿舍楼栋">{{ accommodationDialog.currentAccommodation.dormBuilding || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="房间号">{{ accommodationDialog.currentAccommodation.dormRoom || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="床位号">{{ accommodationDialog.currentAccommodation.bedNo || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="住宿状态">
|
||||
<el-tag v-if="accommodationDialog.currentAccommodation.accommodationStatus === 1" type="success" size="mini">在住</el-tag>
|
||||
<el-tag v-else-if="accommodationDialog.currentAccommodation.accommodationStatus === 0" type="info" size="mini">预分配</el-tag>
|
||||
<el-tag v-else-if="accommodationDialog.currentAccommodation.accommodationStatus === 2" type="danger" size="mini">已退宿</el-tag>
|
||||
<el-tag v-else size="mini">{{ accommodationDialog.currentAccommodation.accommodationStatus }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="分配时间" :span="2">{{ parseTime(accommodationDialog.currentAccommodation.assignedTime, '{y}-{m}-{d} {h}:{i}') || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="备注说明" :span="2">{{ accommodationDialog.currentAccommodation.remark || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<!-- 快速操作区 -->
|
||||
<div style="margin-top: 15px; display: flex; justify-content: flex-end; gap: 10px;">
|
||||
<el-button
|
||||
v-if="accommodationDialog.currentAccommodation.accommodationStatus === 0"
|
||||
v-hasPermi="['train:accommodation:edit']"
|
||||
size="small"
|
||||
type="success"
|
||||
icon="el-icon-check"
|
||||
@click="handleCheckInFromDialog"
|
||||
>办理入住</el-button>
|
||||
<el-button
|
||||
v-if="accommodationDialog.currentAccommodation.accommodationStatus === 0"
|
||||
v-hasPermi="['train:accommodation:remove']"
|
||||
size="small"
|
||||
type="danger"
|
||||
plain
|
||||
icon="el-icon-delete"
|
||||
@click="handleDeleteFromDialog"
|
||||
>删除预分配</el-button>
|
||||
<el-button
|
||||
v-if="accommodationDialog.currentAccommodation.accommodationStatus === 1"
|
||||
v-hasPermi="['train:accommodation:edit']"
|
||||
size="small"
|
||||
type="warning"
|
||||
icon="el-icon-close"
|
||||
@click="handleCheckOutFromDialog"
|
||||
>办理退宿</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分支 B:暂无住宿,显示分配表单 -->
|
||||
<div v-else>
|
||||
<div style="font-weight: bold; font-size: 14px; margin-bottom: 12px; color: #303133;">
|
||||
<i class="el-icon-office-building" style="color: #409EFF; margin-right: 5px;" />办理住宿分配
|
||||
</div>
|
||||
|
||||
<el-form ref="accommodationForm" :model="accommodationDialog.form" :rules="accommodationDialog.rules" label-width="100px" size="small">
|
||||
<el-form-item label="选择宿舍" prop="dormId">
|
||||
<el-select
|
||||
v-model="accommodationDialog.form.dormId"
|
||||
placeholder="请选择目标宿舍房间"
|
||||
filterable
|
||||
style="width: 100%;"
|
||||
>
|
||||
<el-option
|
||||
v-for="dorm in accommodationDialog.dormOptions"
|
||||
:key="dorm.dormId"
|
||||
:label="`${dorm.dormBuilding} ${dorm.dormRoom} (余位:${dorm.remainingBeds != null ? dorm.remainingBeds : dorm.capacity}/${dorm.capacity}, ${genderLabel(dorm.genderType)})`"
|
||||
:value="dorm.dormId"
|
||||
:disabled="dorm.dormStatus === 0 || (dorm.remainingBeds !== null && dorm.remainingBeds <= 0)"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 选定宿舍后提示 -->
|
||||
<div v-if="selectedDormInfo" style="margin-left: 100px; margin-bottom: 15px;">
|
||||
<el-alert
|
||||
:title="`楼栋: ${selectedDormInfo.dormBuilding} | 房间: ${selectedDormInfo.dormRoom} | 剩余床位: ${selectedDormInfo.remainingBeds != null ? selectedDormInfo.remainingBeds : selectedDormInfo.capacity} / ${selectedDormInfo.capacity} | 适用性别: ${genderLabel(selectedDormInfo.genderType)}`"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
<el-alert
|
||||
v-if="isGenderMismatch"
|
||||
title="提示:学员性别与该宿舍适用性别不一致,提交可能触发系统性别隔离拦截!"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
style="margin-top: 5px;"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-form-item label="床位号" prop="bedNo">
|
||||
<el-input
|
||||
v-model="accommodationDialog.form.bedNo"
|
||||
placeholder="请输入床位号,如 1号床、2号床 或 A床"
|
||||
clearable
|
||||
>
|
||||
<template slot="append">
|
||||
<el-dropdown trigger="click" @command="val => accommodationDialog.form.bedNo = val">
|
||||
<span class="el-dropdown-link" style="cursor: pointer;">
|
||||
常用推荐<i class="el-icon-arrow-down el-icon--right" />
|
||||
</span>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item command="1号床">1号床</el-dropdown-item>
|
||||
<el-dropdown-item command="2号床">2号床</el-dropdown-item>
|
||||
<el-dropdown-item command="3号床">3号床</el-dropdown-item>
|
||||
<el-dropdown-item command="4号床">4号床</el-dropdown-item>
|
||||
<el-dropdown-item command="A床">A床</el-dropdown-item>
|
||||
<el-dropdown-item command="B床">B床</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="分配备注" prop="remark">
|
||||
<el-input
|
||||
v-model="accommodationDialog.form.remark"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
placeholder="选填住宿分配备注说明"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 历史履历收折面板 -->
|
||||
<div style="margin-top: 15px; border-top: 1px dashed #DCDFE6; padding-top: 12px;">
|
||||
<el-button
|
||||
type="text"
|
||||
size="mini"
|
||||
:icon="accommodationDialog.showHistory ? 'el-icon-arrow-up' : 'el-icon-time'"
|
||||
@click="toggleHistory"
|
||||
>
|
||||
{{ accommodationDialog.showHistory ? '收起历史住宿记录' : '查看学员历史住宿记录' }}
|
||||
</el-button>
|
||||
|
||||
<div v-show="accommodationDialog.showHistory" style="margin-top: 10px;">
|
||||
<el-table :data="accommodationDialog.historyList" size="mini" border empty-text="暂无历史住宿记录">
|
||||
<el-table-column label="宿舍房间" min-width="120" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ scope.row.dormBuilding }} {{ scope.row.dormRoom }} ({{ scope.row.bedNo }}床)</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="住宿状态" width="85" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.accommodationStatus === 1" type="success" size="mini">在住</el-tag>
|
||||
<el-tag v-else-if="scope.row.accommodationStatus === 0" type="info" size="mini">预分配</el-tag>
|
||||
<el-tag v-else-if="scope.row.accommodationStatus === 2" type="danger" size="mini">已退宿</el-tag>
|
||||
<el-tag v-else size="mini">{{ scope.row.accommodationStatus }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="分配时间" width="135" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ parseTime(scope.row.assignedTime, '{y}-{m}-{d} {h}:{i}') || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="退宿时间" width="135" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ parseTime(scope.row.checkOutTime, '{y}-{m}-{d} {h}:{i}') || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button
|
||||
v-if="!accommodationDialog.currentAccommodation || !accommodationDialog.currentAccommodation.accommodationId"
|
||||
type="primary"
|
||||
:loading="accommodationDialog.submitting"
|
||||
@click="submitAccommodationForm"
|
||||
>确认分配</el-button>
|
||||
<el-button @click="accommodationDialog.open = false">关 闭</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -347,6 +576,15 @@ import {
|
||||
confirmReport
|
||||
} from '@/api/train/enrollment'
|
||||
import { listStudent } from '@/api/train/student'
|
||||
import {
|
||||
getActiveAccommodation,
|
||||
getAccommodationHistory,
|
||||
addAccommodation,
|
||||
checkInAccommodation,
|
||||
checkOutAccommodation,
|
||||
delAccommodation
|
||||
} from '@/api/train/accommodation'
|
||||
import { listDormitory } from '@/api/train/dormitory'
|
||||
|
||||
export default {
|
||||
name: 'EnrollmentPanel',
|
||||
@@ -414,6 +652,26 @@ export default {
|
||||
open: false,
|
||||
loading: false,
|
||||
data: {}
|
||||
},
|
||||
// 学员住宿安排对话框
|
||||
accommodationDialog: {
|
||||
open: false,
|
||||
loading: false,
|
||||
submitting: false,
|
||||
currentStudent: {},
|
||||
currentAccommodation: null,
|
||||
dormOptions: [],
|
||||
showHistory: false,
|
||||
historyList: [],
|
||||
form: {
|
||||
dormId: '',
|
||||
bedNo: '',
|
||||
remark: ''
|
||||
},
|
||||
rules: {
|
||||
dormId: [{ required: true, message: '请选择目标宿舍房间', trigger: 'change' }],
|
||||
bedNo: [{ required: true, message: '请输入床位号', trigger: 'blur' }]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -427,6 +685,23 @@ export default {
|
||||
const name = (s.sName || s.studentName || '').trim()
|
||||
const org = s.orgName ? ` - ${s.orgName}` : ''
|
||||
return name ? `${name}${org}` : '-'
|
||||
},
|
||||
selectedDormInfo() {
|
||||
if (!this.accommodationDialog.form.dormId) return null
|
||||
return this.accommodationDialog.dormOptions.find(d => d.dormId === this.accommodationDialog.form.dormId) || null
|
||||
},
|
||||
isGenderMismatch() {
|
||||
if (!this.selectedDormInfo) return false
|
||||
const dormGender = this.selectedDormInfo.genderType
|
||||
if (dormGender === 0 || dormGender == null) return false
|
||||
const studentGender = this.accommodationDialog.currentStudent.gender
|
||||
if (studentGender === '男' || studentGender === '0' || studentGender === 0) {
|
||||
return dormGender === 2
|
||||
}
|
||||
if (studentGender === '女' || studentGender === '1' || studentGender === 1) {
|
||||
return dormGender === 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -476,14 +751,23 @@ export default {
|
||||
handleViewDetail(row) {
|
||||
this.detailDialog.loading = true
|
||||
this.detailDialog.open = true
|
||||
getEnrollmentDetail(row.enrollmentId)
|
||||
.then(res => {
|
||||
this.detailDialog.data = res.data || row
|
||||
})
|
||||
.catch(() => {
|
||||
Promise.all([
|
||||
getEnrollmentDetail(row.enrollmentId),
|
||||
getActiveAccommodation(row.enrollmentId).catch(() => ({ data: null }))
|
||||
]).then(([detailRes, accRes]) => {
|
||||
const data = Object.assign({}, detailRes.data || row)
|
||||
if (accRes && accRes.data) {
|
||||
data.accommodation = {
|
||||
building: accRes.data.dormBuilding,
|
||||
room: accRes.data.dormRoom,
|
||||
bedNo: accRes.data.bedNo,
|
||||
authStatus: accRes.data.authStatus || 0
|
||||
}
|
||||
}
|
||||
this.detailDialog.data = data
|
||||
}).catch(() => {
|
||||
this.detailDialog.data = row
|
||||
})
|
||||
.finally(() => {
|
||||
}).finally(() => {
|
||||
this.detailDialog.loading = false
|
||||
})
|
||||
},
|
||||
@@ -658,6 +942,125 @@ export default {
|
||||
this.$modal.msgError((err && err.message) || '报到失败')
|
||||
}
|
||||
})
|
||||
},
|
||||
// ---- 学员住宿安排与分配 (ACCOM-ASSIGN-01) ----
|
||||
genderLabel(val) {
|
||||
if (val === 1) return '男'
|
||||
if (val === 2) return '女'
|
||||
return '不限'
|
||||
},
|
||||
handleOpenAccommodation(row) {
|
||||
this.accommodationDialog.currentStudent = Object.assign({}, row)
|
||||
this.accommodationDialog.currentAccommodation = null
|
||||
this.accommodationDialog.showHistory = false
|
||||
this.accommodationDialog.historyList = []
|
||||
this.accommodationDialog.form = {
|
||||
dormId: '',
|
||||
bedNo: '1号床',
|
||||
remark: ''
|
||||
}
|
||||
this.accommodationDialog.open = true
|
||||
this.accommodationDialog.loading = true
|
||||
Promise.all([
|
||||
this.loadDormOptions(),
|
||||
this.loadStudentAccommodation(row.enrollmentId)
|
||||
]).finally(() => {
|
||||
this.accommodationDialog.loading = false
|
||||
})
|
||||
},
|
||||
handleOpenAccommodationFromDetail() {
|
||||
if (!this.detailDialog.data || !this.detailDialog.data.enrollmentId) return
|
||||
this.handleOpenAccommodation(this.detailDialog.data)
|
||||
},
|
||||
loadDormOptions() {
|
||||
return listDormitory({ pageNum: 1, pageSize: 100, dormStatus: 1 }).then(res => {
|
||||
this.accommodationDialog.dormOptions = res.rows || []
|
||||
}).catch(() => {
|
||||
this.accommodationDialog.dormOptions = []
|
||||
})
|
||||
},
|
||||
loadStudentAccommodation(enrollmentId) {
|
||||
return getActiveAccommodation(enrollmentId).then(res => {
|
||||
this.accommodationDialog.currentAccommodation = res.data || null
|
||||
}).catch(() => {
|
||||
this.accommodationDialog.currentAccommodation = null
|
||||
})
|
||||
},
|
||||
submitAccommodationForm() {
|
||||
this.$refs.accommodationForm.validate(valid => {
|
||||
if (!valid) return
|
||||
this.accommodationDialog.submitting = true
|
||||
const payload = {
|
||||
enrollmentId: this.accommodationDialog.currentStudent.enrollmentId,
|
||||
dormId: this.accommodationDialog.form.dormId,
|
||||
bedNo: this.accommodationDialog.form.bedNo,
|
||||
remark: this.accommodationDialog.form.remark
|
||||
}
|
||||
addAccommodation(payload).then(() => {
|
||||
this.$modal.msgSuccess('住宿分配成功')
|
||||
return this.loadStudentAccommodation(payload.enrollmentId)
|
||||
}).then(() => {
|
||||
this.getList()
|
||||
if (this.detailDialog.open && this.detailDialog.data.enrollmentId === payload.enrollmentId) {
|
||||
getEnrollmentDetail(payload.enrollmentId).then(res => {
|
||||
this.detailDialog.data = res.data || this.detailDialog.data
|
||||
})
|
||||
}
|
||||
}).catch(err => {
|
||||
const msg = (err && (err.msg || err.message)) || '分配住宿失败'
|
||||
this.$modal.msgError(msg)
|
||||
}).finally(() => {
|
||||
this.accommodationDialog.submitting = false
|
||||
})
|
||||
})
|
||||
},
|
||||
handleCheckInFromDialog() {
|
||||
const acc = this.accommodationDialog.currentAccommodation
|
||||
if (!acc) return
|
||||
this.$modal.confirm(`确认办理学员【${this.accommodationDialog.currentStudent.studentName}】的入住手续?`).then(() => {
|
||||
return checkInAccommodation(acc.accommodationId)
|
||||
}).then(() => {
|
||||
this.$modal.msgSuccess('入住办理成功')
|
||||
this.loadStudentAccommodation(this.accommodationDialog.currentStudent.enrollmentId)
|
||||
this.getList()
|
||||
}).catch(() => {})
|
||||
},
|
||||
handleCheckOutFromDialog() {
|
||||
const acc = this.accommodationDialog.currentAccommodation
|
||||
if (!acc) return
|
||||
this.$modal.confirm(`确认办理学员【${this.accommodationDialog.currentStudent.studentName}】的退宿手续?`).then(() => {
|
||||
return checkOutAccommodation(acc.accommodationId)
|
||||
}).then(() => {
|
||||
this.$modal.msgSuccess('退宿办理成功')
|
||||
this.loadStudentAccommodation(this.accommodationDialog.currentStudent.enrollmentId)
|
||||
this.getList()
|
||||
}).catch(() => {})
|
||||
},
|
||||
handleDeleteFromDialog() {
|
||||
const acc = this.accommodationDialog.currentAccommodation
|
||||
if (!acc) return
|
||||
this.$modal.confirm(`确定要删除学员【${this.accommodationDialog.currentStudent.studentName}】的预分配住宿记录吗?`).then(() => {
|
||||
return delAccommodation(acc.accommodationId)
|
||||
}).then(() => {
|
||||
this.$modal.msgSuccess('删除成功')
|
||||
this.loadStudentAccommodation(this.accommodationDialog.currentStudent.enrollmentId)
|
||||
this.getList()
|
||||
if (this.detailDialog.open && this.detailDialog.data.enrollmentId === this.accommodationDialog.currentStudent.enrollmentId) {
|
||||
getEnrollmentDetail(this.accommodationDialog.currentStudent.enrollmentId).then(res => {
|
||||
this.detailDialog.data = res.data || this.detailDialog.data
|
||||
})
|
||||
}
|
||||
}).catch(() => {})
|
||||
},
|
||||
toggleHistory() {
|
||||
this.accommodationDialog.showHistory = !this.accommodationDialog.showHistory
|
||||
if (this.accommodationDialog.showHistory && this.accommodationDialog.currentStudent.enrollmentId) {
|
||||
getAccommodationHistory(this.accommodationDialog.currentStudent.enrollmentId).then(res => {
|
||||
this.accommodationDialog.historyList = res.data || []
|
||||
}).catch(() => {
|
||||
this.accommodationDialog.historyList = []
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,360 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
ACCOM-ASSIGN-01 Product Remediation E2E Test Suite
|
||||
Verifies:
|
||||
A. Entry test: 参训名单存在明确「住宿安排」入口
|
||||
B. Dialog test: 点击入口,无 JavaScript error,弹窗打开,展示正确学员上下文
|
||||
C. Successful assignment: 有效资源分配,addAccommodation 发送并成功,UI刷新显示住宿信息
|
||||
D. Persistence: 重新打开页面/弹窗,持久化住宿信息依然存在
|
||||
E. Conflict: 构造相同宿舍床位时间冲突,后端拦截触发,前端显示清晰业务错误提示,无非法记录
|
||||
F. No regression: 住宿管理页面点击分配住宿不再抛 TypeError,提示清晰
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import json
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
def load_qa_env():
|
||||
env_file = os.path.expanduser('~/.railwaysystem-qa.env')
|
||||
env = {}
|
||||
if os.path.exists(env_file):
|
||||
with open(env_file, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and not line.startswith('#') and '=' in line:
|
||||
k, v = line.split('=', 1)
|
||||
env[k.strip()] = v.strip()
|
||||
return env
|
||||
|
||||
def get_captcha_code_from_redis(uuid):
|
||||
res = subprocess.check_output(['redis-cli', 'get', f'captcha_codes:{uuid}']).decode('utf-8').strip()
|
||||
return res.strip('\"')
|
||||
|
||||
def run_tests():
|
||||
env = load_qa_env()
|
||||
username = env.get('QA_ADMIN_USERNAME', 'admin')
|
||||
password = env.get('QA_ADMIN_PASSWORD')
|
||||
base_url = 'http://127.0.0.1:1024'
|
||||
|
||||
print("===================================================================")
|
||||
print("=== Starting ACCOM-ASSIGN-01 Product Remediation E2E Verification ===")
|
||||
print("===================================================================")
|
||||
|
||||
page_errors = []
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(channel='chrome', headless=True)
|
||||
context = browser.new_context(viewport={'width': 1440, 'height': 900})
|
||||
page = context.new_page()
|
||||
|
||||
page.on('pageerror', lambda err: page_errors.append(str(err)))
|
||||
|
||||
last_captcha_uuid = [None]
|
||||
token_holder = [None]
|
||||
|
||||
def on_response(response):
|
||||
if 'captchaImage' in response.url:
|
||||
try:
|
||||
data = response.json()
|
||||
last_captcha_uuid[0] = data.get('uuid')
|
||||
except Exception:
|
||||
pass
|
||||
elif '/login' in response.url and response.request.method == 'POST':
|
||||
try:
|
||||
data = response.json()
|
||||
if data.get('code') == 200:
|
||||
token_holder[0] = data.get('token')
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
page.on('response', on_response)
|
||||
|
||||
# 1. 登录系统
|
||||
print("\n[Step 1] Logging in as admin...")
|
||||
page.goto(f"{base_url}/login", wait_until='domcontentloaded')
|
||||
for _ in range(50):
|
||||
if last_captcha_uuid[0]:
|
||||
break
|
||||
page.wait_for_timeout(200)
|
||||
|
||||
uuid = last_captcha_uuid[0]
|
||||
code = get_captcha_code_from_redis(uuid)
|
||||
page.wait_for_selector('input[type="password"]', timeout=10000)
|
||||
page.fill('input[placeholder="账号"]', username)
|
||||
page.fill('input[placeholder="密码"]', password)
|
||||
page.fill('input[placeholder="验证码"]', code)
|
||||
page.click('button:has-text("登 录")')
|
||||
page.wait_for_timeout(3000)
|
||||
print("✔ Login succeeded.")
|
||||
|
||||
# ========================================================
|
||||
# TEST F: No regression on accommodation/index.vue (TypeError fixed)
|
||||
# ========================================================
|
||||
print("\n[Step 2] Testing F: Accommodation page defect remediation (no TypeError: this.$modal.msgInfo)...")
|
||||
page_errors.clear()
|
||||
page.goto(f"{base_url}/train/accommodation", wait_until='domcontentloaded')
|
||||
page.wait_for_selector('.el-table', timeout=10000)
|
||||
print("Accommodation page loaded.")
|
||||
|
||||
# 点击「分配住宿」按钮
|
||||
add_btn = page.locator('button:has-text("分配住宿")')
|
||||
assert add_btn.is_visible(), "F FAILED: '分配住宿' button not found on accommodation page"
|
||||
add_btn.click()
|
||||
page.wait_for_timeout(1000)
|
||||
|
||||
# 验证没有发生 TypeError: this.$modal.msgInfo is not a function
|
||||
msginfo_errors = [e for e in page_errors if 'msgInfo' in e]
|
||||
assert len(msginfo_errors) == 0, f"F FAILED: msgInfo TypeError observed: {msginfo_errors}"
|
||||
assert len(page_errors) == 0, f"F FAILED: page errors observed: {page_errors}"
|
||||
|
||||
# 检查是否弹出指引消息
|
||||
msg_locator = page.locator('.el-message:has-text("住宿安排")')
|
||||
assert msg_locator.is_visible(), "F FAILED: Guideline message not displayed upon clicking 分配住宿"
|
||||
print(f"✔ F PASS: Clicked '分配住宿' on accommodation page -> No TypeError, guideline message shown: '{msg_locator.inner_text().strip()}'")
|
||||
|
||||
# ========================================================
|
||||
# TEST A & B: Entry & Dialog in EnrollmentPanel.vue
|
||||
# ========================================================
|
||||
print("\n[Step 3] Testing A & B: Enrollment list entry test and accommodation dialog opening...")
|
||||
page.goto(f"{base_url}/coursemgr/plan", wait_until='domcontentloaded')
|
||||
page.wait_for_selector('.el-table__body-wrapper tbody tr', timeout=15000)
|
||||
|
||||
# 打开第一个培训计划的计划详情(Element UI 具有 fixed-right 操作列)
|
||||
plan_rows = page.locator('.el-table__body-wrapper tbody tr')
|
||||
assert plan_rows.count() > 0, "No plans available in plan table"
|
||||
first_plan_row = plan_rows.first
|
||||
plan_name = first_plan_row.locator('td').first.inner_text().strip()
|
||||
print(f"Opening plan detail for: {plan_name}")
|
||||
|
||||
# 兼容定位 fixed-right 列中的计划详情按钮或可见按钮
|
||||
detail_btn = page.locator('.el-table__fixed-right button:has-text("计划详情"), button:has-text("计划详情"):visible').first
|
||||
if not detail_btn.is_visible():
|
||||
# 也可以点击参训人数直接打开参训人员Tab
|
||||
first_plan_row.locator('td').nth(8).locator('button').click()
|
||||
else:
|
||||
detail_btn.click()
|
||||
|
||||
page.wait_for_selector('.el-dialog:has-text("培训计划详情")', timeout=5000)
|
||||
plan_dialog = page.locator('.el-dialog:has-text("培训计划详情")')
|
||||
|
||||
# 切换到「参训人员」Tab
|
||||
plan_dialog.locator('.el-tabs__item:has-text("参训人员")').click()
|
||||
page.wait_for_selector('.enrollment-panel .el-table__body-wrapper tbody tr', timeout=15000)
|
||||
|
||||
enrollment_rows = plan_dialog.locator('.enrollment-panel .el-table__body-wrapper tbody tr')
|
||||
enrollment_count = enrollment_rows.count()
|
||||
print(f"Enrollment rows loaded: {enrollment_count}")
|
||||
assert enrollment_count >= 1, "A FAILED: No students enrolled in this plan"
|
||||
|
||||
# 验证 Test A: 参训名单存在明确「住宿安排」入口
|
||||
first_student_row = enrollment_rows.first
|
||||
# 在 Element UI 中固定列渲染在 .el-table__fixed-right 中
|
||||
accom_btn = page.locator('.enrollment-panel .el-table__fixed-right button:has-text("住宿安排")').first
|
||||
assert accom_btn.is_visible(), "A FAILED: '住宿安排' button is not visible in student table row"
|
||||
print("✔ A PASS: '住宿安排' button is clearly visible in enrollment list row.")
|
||||
|
||||
# 获取第一个学员基本信息
|
||||
student_name = first_student_row.locator('td').nth(1).inner_text().strip()
|
||||
print(f"Target student: {student_name}")
|
||||
|
||||
# 验证 Test B: 点击入口,无 JavaScript error,弹窗打开,展示正确学员上下文
|
||||
page_errors.clear()
|
||||
accom_btn.click()
|
||||
page.wait_for_selector('.el-dialog:has-text("学员住宿安排")', timeout=5000)
|
||||
accom_dialog = page.locator('.el-dialog:has-text("学员住宿安排")')
|
||||
page.wait_for_timeout(1000)
|
||||
|
||||
assert len(page_errors) == 0, f"B FAILED: Page errors observed when opening dialog: {page_errors}"
|
||||
|
||||
# 检查上下文展示
|
||||
dialog_text = accom_dialog.inner_text()
|
||||
assert student_name in dialog_text, f"B FAILED: Student name '{student_name}' not shown in accommodation dialog"
|
||||
assert "参训学员信息" in dialog_text, "B FAILED: Student context section missing"
|
||||
print(f"✔ B PASS: Dialog opened without JS error, correctly showing student context for '{student_name}'.")
|
||||
|
||||
# ========================================================
|
||||
# TEST C: Successful accommodation assignment
|
||||
# ========================================================
|
||||
print("\n[Step 4] Testing C: Successful accommodation assignment...")
|
||||
# 检查当前是否已有有效住宿安排。如果有,先测试或删除预分配以进行全新的分配测试
|
||||
delete_btn = accom_dialog.locator('button:has-text("删除预分配")')
|
||||
checkout_btn = accom_dialog.locator('button:has-text("办理退宿")')
|
||||
|
||||
if checkout_btn.is_visible():
|
||||
print("Student currently checked in, checking out first for assignment test...")
|
||||
checkout_btn.click()
|
||||
page.wait_for_selector('.el-message-box:has-text("退宿手续")', timeout=3000)
|
||||
page.locator('.el-message-box:has-text("退宿手续") button:has-text("确定")').click()
|
||||
page.wait_for_timeout(2000)
|
||||
elif delete_btn.is_visible():
|
||||
print("Student currently has preallocation, deleting for clean test...")
|
||||
delete_btn.click()
|
||||
page.wait_for_selector('.el-message-box:has-text("删除学员")', timeout=3000)
|
||||
page.locator('.el-message-box:has-text("删除学员") button:has-text("确定")').click()
|
||||
page.wait_for_timeout(2000)
|
||||
|
||||
# 此时应显示「办理住宿分配」表单
|
||||
page.wait_for_selector('.el-form-item:has-text("选择宿舍")', timeout=5000)
|
||||
print("Assignment form is ready.")
|
||||
|
||||
# 选择宿舍
|
||||
dorm_select = accom_dialog.locator('.el-form-item:has-text("选择宿舍") .el-select')
|
||||
dorm_select.click()
|
||||
page.wait_for_timeout(500)
|
||||
|
||||
# 选择第一个可用宿舍选项
|
||||
dorm_options = page.locator('.el-select-dropdown:visible .el-select-dropdown__item:not(.is-disabled)')
|
||||
assert dorm_options.count() > 0, "C FAILED: No available dormitory rooms found"
|
||||
target_dorm_label = dorm_options.first.inner_text().strip()
|
||||
target_dorm_name = target_dorm_label.split('(')[0].strip()
|
||||
print(f"Selecting dormitory: {target_dorm_label} (room identifier: '{target_dorm_name}')")
|
||||
dorm_options.first.click()
|
||||
page.wait_for_timeout(500)
|
||||
|
||||
# 填写床位号(使用固定床位号)
|
||||
test_bed_no = f"测试{int(time.time())%1000}号床"
|
||||
bed_input = accom_dialog.locator('.el-form-item:has-text("床位号") input')
|
||||
bed_input.fill(test_bed_no)
|
||||
|
||||
# 监听 POST /train/accommodation 请求
|
||||
post_request_captured = [None]
|
||||
def on_accom_request(req):
|
||||
if '/train/accommodation' in req.url and req.method == 'POST' and not req.url.endswith('/checkin') and not req.url.endswith('/checkout'):
|
||||
try:
|
||||
post_request_captured[0] = json.loads(req.post_data)
|
||||
except Exception:
|
||||
pass
|
||||
page.on('request', on_accom_request)
|
||||
|
||||
# 点击「确认分配」
|
||||
confirm_btn = accom_dialog.locator('button:has-text("确认分配")')
|
||||
confirm_btn.click(force=True)
|
||||
page.wait_for_timeout(2000)
|
||||
|
||||
assert post_request_captured[0] is not None, "C FAILED: addAccommodation POST request was not sent"
|
||||
print(f"Captured POST payload: {post_request_captured[0]}")
|
||||
assert post_request_captured[0].get('bedNo') == test_bed_no, f"C FAILED: BedNo mismatch in payload"
|
||||
assert post_request_captured[0].get('enrollmentId') is not None, "C FAILED: enrollmentId missing in payload"
|
||||
|
||||
# 检查成功提示
|
||||
success_msg = page.locator('.el-message--success:has-text("住宿分配成功")')
|
||||
assert success_msg.is_visible(), "C FAILED: Success message '住宿分配成功' not displayed"
|
||||
print("✔ Success message displayed.")
|
||||
|
||||
# 检查弹窗是否自动刷新并展示当前住宿安排
|
||||
page.wait_for_timeout(1000)
|
||||
assert "当前住宿安排" in accom_dialog.inner_text(), "C FAILED: Accommodation dialog did not refresh to show assigned accommodation"
|
||||
assert test_bed_no in accom_dialog.inner_text(), f"C FAILED: Assigned bedNo {test_bed_no} not shown in dialog"
|
||||
print(f"✔ C PASS: addAccommodation request sent successfully, UI refreshed displaying assigned bed '{test_bed_no}'.")
|
||||
|
||||
# ========================================================
|
||||
# TEST D: Persistence test
|
||||
# ========================================================
|
||||
print("\n[Step 5] Testing D: Persistence verification...")
|
||||
# 关闭弹窗
|
||||
accom_dialog.locator('button:has-text("关 闭")').click()
|
||||
page.wait_for_timeout(1000)
|
||||
|
||||
# 重新点击该学员的「住宿安排」
|
||||
page.locator('.enrollment-panel .el-table__fixed-right button:has-text("住宿安排")').first.click()
|
||||
page.wait_for_selector('.el-dialog:has-text("学员住宿安排")', timeout=5000)
|
||||
page.wait_for_timeout(1000)
|
||||
|
||||
reopened_text = accom_dialog.inner_text()
|
||||
assert "当前住宿安排" in reopened_text, "D FAILED: Persisted accommodation section not shown on reopen"
|
||||
assert test_bed_no in reopened_text, f"D FAILED: Persisted bedNo {test_bed_no} not found on reopen"
|
||||
print(f"✔ D PASS: Reopened dialog correctly loaded persisted accommodation with bed '{test_bed_no}'.")
|
||||
|
||||
# 同时在学员档案全景中也验证住宿信息
|
||||
accom_dialog.locator('button:has-text("关 闭")').click()
|
||||
page.wait_for_timeout(500)
|
||||
page.locator('.enrollment-panel .el-table__fixed-right button:has-text("学员档案")').first.click()
|
||||
page.wait_for_selector('.el-dialog:has-text("参训档案全景")', timeout=5000)
|
||||
detail_diag = page.locator('.el-dialog:has-text("参训档案全景")')
|
||||
page.wait_for_timeout(1000)
|
||||
detail_text = detail_diag.inner_text()
|
||||
assert test_bed_no in detail_text, f"D FAILED: Student profile detail does not reflect bed {test_bed_no}"
|
||||
print(f"✔ D PASS: Student detail profile also correctly reflects persisted bed '{test_bed_no}'.")
|
||||
detail_diag.locator('button:has-text("关 闭")').click()
|
||||
page.wait_for_timeout(500)
|
||||
|
||||
# ========================================================
|
||||
# TEST E: Bed conflict behavior remains intact
|
||||
# ========================================================
|
||||
print("\n[Step 6] Testing E: Bed conflict detection and error feedback...")
|
||||
if enrollment_count >= 2:
|
||||
second_student_row = enrollment_rows.nth(1)
|
||||
second_name = second_student_row.locator('td').nth(1).inner_text().strip()
|
||||
print(f"Testing conflict on second student: {second_name}")
|
||||
|
||||
sec_accom_btn = page.locator('.enrollment-panel .el-table__fixed-right button:has-text("住宿安排")').nth(1)
|
||||
sec_accom_btn.click()
|
||||
page.wait_for_selector('.el-dialog:has-text("学员住宿安排")', timeout=5000)
|
||||
page.wait_for_timeout(1000)
|
||||
|
||||
# 如果第二名学员已有住宿,先删除预分配以测试分配冲突
|
||||
sec_del_btn = accom_dialog.locator('button:has-text("删除预分配")')
|
||||
if sec_del_btn.is_visible():
|
||||
sec_del_btn.click()
|
||||
page.locator('.el-message-box:has-text("删除学员") button:has-text("确定")').click()
|
||||
page.wait_for_timeout(1500)
|
||||
|
||||
# 选择与第一名学员相同的宿舍和相同的床位号
|
||||
page.wait_for_selector('.el-form-item:has-text("选择宿舍")', timeout=5000)
|
||||
sec_dorm_select = accom_dialog.locator('.el-form-item:has-text("选择宿舍") .el-select')
|
||||
sec_dorm_select.click()
|
||||
page.wait_for_timeout(500)
|
||||
|
||||
sec_dorm_opt = page.locator(f'.el-select-dropdown:visible .el-select-dropdown__item:has-text("{target_dorm_name}")')
|
||||
assert sec_dorm_opt.count() > 0, f"Cannot find matching dorm option for '{target_dorm_name}'"
|
||||
print(f"Selecting exact same dorm: {sec_dorm_opt.first.inner_text().strip()}")
|
||||
sec_dorm_opt.first.click()
|
||||
page.wait_for_timeout(500)
|
||||
|
||||
# 填入完全相同的床位号
|
||||
sec_bed_input = accom_dialog.locator('.el-form-item:has-text("床位号") input')
|
||||
sec_bed_input.fill(test_bed_no)
|
||||
|
||||
# 点击确认分配
|
||||
accom_dialog.locator('button:has-text("确认分配")').click(force=True)
|
||||
page.wait_for_timeout(2000)
|
||||
|
||||
# 验证触发后端冲突业务错误提示
|
||||
error_msg_locator = page.locator('.el-message--error').first
|
||||
assert error_msg_locator.is_visible(), "E FAILED: Business conflict error message not shown on UI"
|
||||
conflict_error_text = error_msg_locator.inner_text().strip()
|
||||
print(f"✔ Visible conflict error displayed: '{conflict_error_text}'")
|
||||
assert "已被占用" in conflict_error_text or "冲突" in conflict_error_text or "400" in conflict_error_text or "FAIL" in conflict_error_text, \
|
||||
f"E FAILED: Unexpected error message content: {conflict_error_text}"
|
||||
|
||||
accom_dialog.locator('button:has-text("关 闭")').click()
|
||||
page.wait_for_timeout(500)
|
||||
print("✔ E PASS: Conflicting bed assignment was blocked by backend rule and displayed clear business error.")
|
||||
else:
|
||||
print("Notice: Plan has only 1 student, skipping second student conflict test (already verified backend rule).")
|
||||
|
||||
# 清理测试住宿(保持测试环境干净)
|
||||
print("\n[Step 7] Cleaning up test accommodation record...")
|
||||
page.locator('.enrollment-panel .el-table__fixed-right button:has-text("住宿安排")').first.click()
|
||||
page.wait_for_selector('.el-dialog:has-text("学员住宿安排")', timeout=5000)
|
||||
page.wait_for_timeout(1000)
|
||||
clean_del = accom_dialog.locator('button:has-text("删除预分配")')
|
||||
if clean_del.is_visible():
|
||||
clean_del.click()
|
||||
page.locator('.el-message-box:has-text("删除学员") button:has-text("确定")').click()
|
||||
page.wait_for_timeout(1500)
|
||||
print("Test preallocation cleaned up successfully.")
|
||||
accom_dialog.locator('button:has-text("关 闭")').click()
|
||||
|
||||
browser.close()
|
||||
|
||||
print("\n===================================================================")
|
||||
print("ALL ACCOM-ASSIGN-01 ACCEPTANCE CRITERIA VERIFIED AND PASSED!")
|
||||
print("===================================================================")
|
||||
|
||||
if __name__ == '__main__':
|
||||
run_tests()
|
||||
Reference in new issue
Block a user