| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873 |
- <%@ page language="java" pageEncoding="UTF-8" isELIgnored="false" %>
- <%@ taglib uri="/ssTag" prefix="ss"%>
- <% pageContext.setAttribute(ss.page.PageC.PAGE_objName,"ksap");%>
- <%pageContext.setAttribute("wdpageinformation","{'hastab':'0'}");%>
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>考试编排</title>
- <script src="/js/Sortable/Sortable.min.js"></script> <%-- ="/newUI/ss/js/Sortable.min.js"。Lin(新UI) --%>
- <script src="/js/load.js"></script> <%-- ="/newUI/ss/js/base.js"。Lin(新UI) --%>
- <style>
- :root {
- --primary-text: #333;
- --regular-text: #606266;
- --secondary-text: #909399;
- --border-color: #dcdfe6;
- --bg-color: #f5f7fa;
- --card-active-border: #dcdee5;
- --header-default-bg: #dddfe6;
- --header-active-bg: #b1b4bb;
- --delete-btn-bg: #5f6c7b;
- --section-bg-gray: #f8f9fb;
- --line-color: #ebeef5;
- }
- input[type="number"]::-webkit-inner-spin-button,
- input[type="number"]::-webkit-outer-spin-button {
- -webkit-appearance: none;
- margin: 0;
- }
- * {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
- outline: none;
- }
- body {
- font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif;
- background-color: #ffffff;
- font-size: 16px;
- /* 全局字体 16px */
- color: var(--primary-text);
- height: 100vh;
- display: flex;
- overflow: hidden;
- }
- /* SVG 图标通用样式 */
- .icon {
- width: 22px;
- height: 22px;
- fill: currentColor;
- vertical-align: middle;
- }
- #app {
- width: 100%;
- height: 100%;
- }
- .content {
- display: flex;
- width: 100%;
- height: calc(100% - 60px);
- }
- /* --- 左侧区域 --- */
- .left-container {
- flex: 1;
- display: flex;
- flex-direction: column;
- padding: 0 20px;
- overflow: hidden;
- background-color: #fff;
- }
- .header-info {
- padding: 16px 0;
- flex-shrink: 0;
- display: flex;
- align-items: center;
- gap: 20px;
- border-bottom: 1px solid #e2e4ec;
- }
- .info-items {
- display: flex;
- gap: 24px;
- font-size: 16px;
- color: var(--primary-text);
- }
- .info-label {
- /* color: var(--secondary-text); */
- margin-right: 4px;
- }
- .btn {
- padding: 6px 16px;
- border: 1px solid #dcdfe6;
- background: white;
- border-radius: 4px;
- cursor: pointer;
- font-size: 13px;
- color: #606266;
- transition: all 0.3s;
- white-space: nowrap;
- }
- .btn:hover {
- color: #409eff;
- border-color: #c6e2ff;
- background-color: #ecf5ff;
- }
- .card-container {
- flex: 1;
- display: flex;
- gap: 20px;
- overflow-x: auto;
- overflow-y: hidden;
- padding: 20px;
- align-items: flex-start;
- border: 2px dashed transparent;
- border-radius: 10px;
- transition: border-color 0.2s, background 0.2s;
- }
- .card-container.drop-active {
- border-color: #d3d6dc;
- background: rgba(211, 214, 220, 0.35);
- }
- /* --- 考室卡片 --- */
- .room-card {
- width: 270px;
- height: 100%;
- max-height: calc(100vh - 100px);
- background: #fff;
- border: 1px solid #e2e4ec;
- display: flex;
- flex-direction: column;
- flex-shrink: 0;
- transition: box-shadow 0.2s;
- position: relative;
- box-sizing: border-box;
- }
- .room-card:hover {
- box-shadow: 0 0 0 8px var(--card-active-border);
- z-index: 1;
- }
- .room-card.active {
- box-shadow: 0 0 0 8px var(--card-active-border);
- border-color: var(--card-active-border);
- z-index: 2;
- }
- /* 卡片头部 */
- .room-header {
- height: 55px;
- padding: 0 22px;
- font-size: 22px;
- /* font-weight: 500; */
- color: #4d4d4d;
- flex-shrink: 0;
- display: flex;
- align-items: center;
- border-bottom: 1px solid var(--line-color);
- /* 修复:确保有下边框 */
- background: #fff;
- position: relative;
- }
- /* 删除按钮 */
- .btn-close {
- position: absolute;
- top: 0;
- right: 0;
- width: 55px;
- height: 55px;
- background: transparent;
- border: none;
- cursor: pointer;
- display: none;
- align-items: center;
- justify-content: center;
- z-index: 10;
- color: #909399;
- transition: background-color 0.2s;
- }
- .room-card:hover .btn-close {
- display: flex;
- }
- .btn-close:hover {
- background-color: #565d6d;
- color: #fff;
- }
- /* 容纳人数行 (灰色背景) */
- .capacity-row {
- display: flex;
- align-items: stretch;
- height: 48px;
- background-color: var(--section-bg-gray);
- border-bottom: 1px solid var(--line-color);
- /* 修复:确保有下边框 */
- flex-shrink: 0;
- }
- .capacity-label {
- padding: 0 12px 0 22px;
- font-size: 20px;
- color: #333333;
- border-right: 1px solid var(--line-color);
- display: flex;
- align-items: center;
- }
- .capacity-input-wrapper {
- flex: 1;
- display: flex;
- align-items: center;
- position: relative;
- }
- .capacity-alert {
- position: absolute;
- left: 0;
- bottom: 0;
- padding: 0 14px;
- color: #f56c6c;
- font-size: 12px;
- border-bottom: 1px solid #f56c6c;
- pointer-events: none;
- width: 100%;
- }
- .capacity-input:placeholder-shown {
- border-left: 2px solid #f56c6c;
- }
- .capacity-input {
- width: 100%;
- height: 100%;
- border: none;
- padding: 0 12px;
- font-size: 20px;
- background: transparent;
- color: #333333;
- }
- .card-scroll-area {
- flex: 1;
- overflow-y: auto;
- display: flex;
- flex-direction: column;
- }
- /* 监考员区域 (灰色背景) */
- .supervisor-section {
- display: flex;
- flex-direction: column;
- border-bottom: 1px solid var(--line-color);
- /* 修复:确保有下边框 */
- background-color: var(--section-bg-gray);
- }
- .supervisor-section .section-header,
- .supervisor-section .section-list {
- background-color: var(--section-bg-gray);
- }
- /* 考生区域 (白色背景) */
- .student-section {
- display: flex;
- flex-direction: column;
- background-color: #fff;
- flex: 1;
- /* 占满剩余空间 */
- }
- .section-header {
- padding: 0 22px;
- height: 48px;
- display: flex;
- align-items: center;
- justify-content: space-between;
- color: var(--primary-text);
- font-weight: 500;
- font-size: 20px;
- }
- .section-header-left {
- display: flex;
- align-items: center;
- gap: 20px;
- }
- .section-icon {
- display: inline-flex;
- font-size: 20px;
- color: #5e6470;
- }
- .section-count {
- font-size: 18px;
- color: #999999;
- font-weight: normal;
- }
- .section-list {
- min-height: 40px;
- display: flex;
- flex-direction: column;
- align-items: flex-end;
- margin-bottom: 10px;
- border: 2px dashed transparent;
- border-radius: 6px;
- transition: border-color 0.2s, background 0.2s;
- }
- .section-list.drop-active {
- border-color: #d3d6dc;
- background: rgba(211, 214, 220, 0.35);
- }
- .empty-tip {
- display: none;
- }
- /* 列表项 */
- .list-item {
- width: 75%;
- height: 36px;
- margin-right: 10px;
- padding-left: 10px;
- display: flex;
- align-items: center;
- justify-content: space-between;
- font-size: 18px;
- /* 确保列表项也是 16px */
- color: var(--primary-text);
- cursor: grab;
- position: relative;
- /* 为 absolute button 提供定位上下文 */
- border-radius: 0;
- box-sizing: border-box;
- }
- .list-item:hover {
- background-color: #edf2f5;
- }
- .list-item-name {
- text-align: left;
- flex: 1;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .list-item-code {
- color: #909399;
- font-size: 16px;
- /* 工号稍微小一点点,或者也 16px */
- margin-right: 8px;
- /* 稍微离右边远一点,避免贴着删除按钮区域 */
- text-align: right;
- }
- /* 列表项删除按钮 */
- .item-delete-btn {
- display: none;
- position: absolute;
- /* 绝对定位 */
- top: 0;
- right: 0;
- bottom: 0;
- /* 撑满高度 */
- width: 36px;
- /* 固定宽度 */
- /* height: 100%; 由 top/bottom 决定 */
- color: #909399;
- cursor: pointer;
- align-items: center;
- justify-content: center;
- font-size: 18px;
- line-height: 1;
- border-radius: 0;
- /* 贴边无圆角 */
- z-index: 5;
- }
- .list-item:hover .item-delete-btn {
- display: flex;
- }
- .item-delete-btn:hover {
- background-color: #565d6d;
- color: #fff;
- }
- /* --- 右侧资源面板 --- */
- .right-panel {
- width: 350px;
- background: #f2f3f5;
- display: flex;
- flex-direction: column;
- border-left: 1px solid #dcdfe6;
- flex-shrink: 0;
- height: 100%;
- overflow: hidden;
- }
- .panel {
- background: #fff;
- display: flex;
- flex-direction: column;
- flex: none;
- border: none;
- border-bottom: 1px solid transparent;
- }
- .panel-header {
- width: calc(100% - 6px);
- margin: 0 auto;
- height: 48px;
- box-sizing: border-box;
- padding: 10px 18px;
- display: flex;
- align-items: center;
- cursor: default;
- user-select: none;
- background: var(--header-default-bg);
- color: #303133;
- transition: background 0.2s;
- flex-shrink: 0;
- }
- .panel-header.can-resize {
- cursor: ns-resize;
- }
- .panel-header.resizing {
- background: var(--header-active-bg);
- cursor: ns-resize;
- }
- .panel-header-left {
- display: flex;
- align-items: center;
- gap: 12px;
- flex: 1;
- }
- .panel-header.dark {
- background: var(--header-dark-bg);
- /* 深色背景暂未定义,假设不需要或沿用 dark 逻辑 */
- color: #fff;
- }
- /* 左侧组:图标+标题+箭头 */
- .panel-header-group {
- display: flex;
- align-items: center;
- gap: 12px;
- }
- .panel-icon {
- display: inline-flex;
- color: #5e6470;
- font-size: 22px;
- }
- .panel-title {
- font-size: 20px;
- /* font-weight: 500; */
- }
- .panel-arrow {
- transition: color 0.2s ease;
- color: #606266;
- width: 22px;
- height: 22px;
- pointer-events: none;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- }
- .panel-toggle {
- background: transparent;
- border: none;
- cursor: pointer;
- width: 28px;
- height: 28px;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 22px;
- margin-left: 12px;
- }
- .panel-actions {
- display: flex;
- align-items: center;
- gap: 6px;
- margin-left: auto;
- }
- .panel-resize-indicator {
- background: transparent;
- border: none;
- cursor: ns-resize;
- width: 28px;
- height: 28px;
- display: flex;
- align-items: center;
- justify-content: center;
- color: #5e6470;
- }
- .panel-resize-icon {
- font-size: 18px;
- color: #5e6470;
- }
-
- .panel-body {
- display: flex;
- flex-direction: column;
- flex: none;
- overflow: hidden;
- min-height: 0;
- background: #f7f7f7;
- }
- .panel-search {
- padding: 12px !important;
- display: flex;
- justify-content: flex-end;
- gap: 8px;
- /* background: #fff; */
- flex-shrink: 0;
- height: auto !important;
- gap: 8px !important;
- /* border-bottom: 1px solid #ebeef5; */
- }
- .search-select {
- width: 138px;
- height: 34px;
- padding: 4px;
- border: 1px solid #dcdfe6;
- border-radius: 2px;
- font-size: 16px;
- color: #606266;
- }
- .search-input {
- width: 106px;
- height: 34px;
- padding: 4px 15px;
- border: 1px solid #dcdfe6;
- border-radius: 2px;
- font-size: 16px;
- color: #999999;
- }
- .panel-list {
- flex: 1;
- overflow-y: auto;
- /* background: #fff; */
- padding: 0 12px 8px 0;
- min-height: 0;
- box-sizing: border-box;
- display: flex;
- flex-direction: column;
- align-items: flex-end;
- }
- .panel-list::-webkit-scrollbar {
- width: 6px;
- }
- .panel-list-item {
- width: 280px;
- /* margin-left: auto; */
- /* margin-right: 12px; */
- flex-shrink: 0;
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 0 6px;
- height: 34px;
- font-size: 18px;
- color: var(--primary-text);
- cursor: move;
- border-bottom: 1px solid #e2e4ec;
- box-sizing: border-box;
- }
- .panel-list-item:hover {
- background-color: #ecf5ff;
- }
- .panel-item-name {
- text-align: left;
- }
- .panel-item-code {
- text-align: right;
- /* color: #909399; */
- }
- .panel-room-building {
- /* color: #909399; */
- /* font-size: 18; */
- margin-left: 12px;
- text-align: right;
- }
- ::-webkit-scrollbar {
- width: 6px;
- height: 6px;
- }
- ::-webkit-scrollbar-thumb {
- background: #c0c4cc;
- border-radius: 3px;
- }
- ::-webkit-scrollbar-track {
- background: transparent;
- }
- </style>
- </head>
- <body>
- <div id="app">
- <div class="content">
- <!-- 左侧区域容器 -->
- <div class="left-container">
- <!-- 头部信息栏 -->
- <div class="header-info">
- <div class="info-items">
-
- <div><span class="info-label">课程:</span>{{ ksjh.kcmc }}</div>
- <div><span class="info-label">年级:</span>{{ ksjh.njmc }}</div>
- <div><span class="info-label">学期:</span>{{ ksjh.xqmc }}</div>
- <div><span class="info-label">试室数:</span>{{ ksjh.ssCount }} 间</div>
- <div><span class="info-label">总容纳人数:</span>{{ ksjh.totalCapacity }} 人</div>
- <div><span class="info-label">已编排考生数:</span>{{ ksjh.arrangedCount }} 人</div>
- </div>
- <!-- <button class="btn" @click="autoArrange">自动编排</button> -->
-
- <ss-search-button
- text="自动编排"
- @click.stop="autoArrange"
- ></ss-search-button>
-
- </div>
- <!-- 考室卡片区域 (X轴滚动) -->
- <div class="card-container" ref="leftContent"
- :class="{ 'drop-active': dropIndicators.room }"
- @dragover.prevent="handleRoomDragOver"
- @dragleave="handleRoomDragLeave"
- @drop.prevent="handleRoomDrop">
- <div v-for="room in ksapList" :key="room.ksapid" class="room-card"
- :class="{ active: selectedKsapid === room.ksapid }" :data-ksapid="room.ksapid"
- @click.stop="selectRoom(room.ksapid)">
- <button class="btn-close" @click.stop="deleteRoom(room)" title="删除">
- <ss-bottom-div-icon class="bottom-div-close"></ss-bottom-div-icon>
- </button>
- <div class="room-header">{{ room.ssmc || '未选择' }}</div>
- <div class="capacity-row" style="margin-bottom: 12px;">
- <div class="capacity-label">容纳人数</div>
- <div class="capacity-input-wrapper">
- <input type="number" class="capacity-input" v-model="room.zdrs" min="0"
- @input="handleCapacityInput(room)"
- placeholder="请输入">
- <div class="capacity-alert" v-if="room.showCapacityAlert">请先输入容纳人数</div>
- </div>
- </div>
- <!-- 内部滚动区域 -->
- <div class="card-scroll-area">
- <!-- 监考员区域 -->
- <div class="card-section supervisor-section">
- <div class="section-header">
- <div class="section-header-left">
- <ss-common-icon class="section-icon common-icon-renyuan2"></ss-common-icon>
- <span>监考员</span>
- </div>
- </div>
- <div class="section-divider"></div>
- <div class="section-list"
- :class="{ 'drop-active': dropIndicators.supervisor === room.ksapid }"
- @dragover.prevent="handleSupervisorDragOver(room, $event)"
- @dragleave="handleSupervisorDragLeave(room)"
- @drop.prevent.stop="handleSupervisorDrop(room)">
- <div v-if="room.zjkry" class="list-item">
- <span class="list-item-name">{{ room.zjkry.xm }}</span>
- <span class="item-delete-btn"
- @click.stop="removeSupervisor(room, room.zjkry)">
- <ss-bottom-div-icon class="bottom-div-close"></ss-bottom-div-icon>
- </span>
- </div>
- <div v-for="person in room.jkcyList" :key="person.ryid" class="list-item">
- <span class="list-item-name">{{ person.xm }}</span>
- <span class="item-delete-btn"
- @click.stop="removeSupervisor(room, person)">
- <ss-bottom-div-icon class="bottom-div-close"></ss-bottom-div-icon>
- </span>
- </div>
- <div v-if="!room.zjkry && room.jkcyList.length === 0" class="empty-tip">
- 暂无监考员
- </div>
- </div>
- </div>
- <!-- 考生区域 -->
- <div class="card-section student-section">
- <div class="section-header">
- <div class="section-header-left">
- <ss-common-icon class="section-icon common-icon-xueyuan"></ss-common-icon>
- <span>考生</span>
- </div>
- <span class="section-count">{{ room.dqrs }}/{{ room.zdrs || 0 }}</span>
- </div>
- <div class="section-divider"></div>
- <div class="section-list"
- :class="{ 'drop-active': dropIndicators.student === room.ksapid }"
- @dragover.prevent="handleStudentDragOver(room, $event)"
- @dragleave="handleStudentDragLeave(room)"
- @drop.prevent.stop="handleStudentDrop(room)">
- <div v-for="student in room.kscyList" :key="student.ryid" class="list-item">
- <span>{{ student.xm }}</span>
- <span class="item-delete-btn"
- @click.stop="removeStudent(room, student)">
- <ss-bottom-div-icon class="bottom-div-close"></ss-bottom-div-icon>
- </span>
- </div>
- <div v-if="room.kscyList.length === 0" class="empty-tip">
- 暂无考生
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- <!-- 右侧资源面板 -->
- <div class="right-panel" ref="rightPanel">
- <!-- 可安排的课室 -->
- <div class="panel">
- <div class="panel-header" :class="{ 'can-resize': canResize('room'), resizing: isResizing('room') }"
- @mousedown="handlePanelPress('room', $event)" @mouseup="handlePanelRelease"
- @mouseleave="handlePanelRelease" ref="roomHeader">
- <div class="panel-header-group">
- <ss-common-icon class="panel-icon common-icon-jiaoshi"></ss-common-icon>
- <span class="panel-title">可安排的课室</span>
- </div>
- <button type="button" class="panel-toggle" @click.stop="togglePanel('room')" @mousedown.stop
- @mouseup.stop>
- <ss-common-icon :class="['panel-arrow', getPanelToggleIcon('room')]"></ss-common-icon>
- </button>
- <button v-if="isResizing('room')" class="panel-resize-indicator" title="拖动调整高度"
- style="margin-left: auto;">
- <ss-common-icon class="panel-resize-icon common-icon-paixu"></ss-common-icon>
- </button>
- </div>
- <div class="panel-body" v-show="!panels.room.collapsed" :style="getPanelBodyStyle('room')"
- ref="roomBody">
- <div class="search-bar-contaienr panel-search " ref="roomSearch">
- <ss-objp v-model="searchFilters.room.building" name="jzwid" :opt="bmidOption"
- placeholder="建筑物" width="150px" inp="true"
- url="/service?ssServ=loadObjpOpt&objectpickerdropdown1=1" cb="jzw"></ss-objp>
- <ss-search-input name="roomnmae" placeholder="名称" v-model="searchFilters.room.name"
- width="100px" @search="search">
- </ss-search-input>
- </div>
- <div class="panel-list" ref="roomList">
- <div v-for="room in filteredRooms" :key="room.ssid" class="panel-list-item"
- draggable="true"
- @dragstart="handleDragStart('room', room, $event)"
- @dragend="handleDragEnd">
- <span class="panel-item-name">{{ room.ssmc }}</span>
- <span class="panel-item-code">{{ room.jzwid || room.ssid }}</span>
- </div>
- </div>
- </div>
- </div>
- <!-- 未编排的考生 -->
- <div class="panel">
- <div class="panel-header"
- :class="{ 'can-resize': canResize('student'), resizing: isResizing('student') }"
- @mousedown="handlePanelPress('student', $event)" @mouseup="handlePanelRelease"
- @mouseleave="handlePanelRelease" ref="studentHeader">
- <div class="panel-header-group">
- <ss-common-icon class="panel-icon common-icon-xueyuan"></ss-common-icon>
- <span class="panel-title">未编排的考生</span>
- </div>
- <button type="button" class="panel-toggle" @click.stop="togglePanel('student')" @mousedown.stop
- @mouseup.stop>
- <ss-common-icon :class="['panel-arrow', getPanelToggleIcon('student')]"></ss-common-icon>
- </button>
- <button v-if="isResizing('student')" class="panel-resize-indicator" title="拖动调整高度"
- style="margin-left: auto;">
- <ss-common-icon class="panel-resize-icon common-icon-paixu"></ss-common-icon>
- </button>
- </div>
- <div class="panel-body" v-show="!panels.student.collapsed" :style="getPanelBodyStyle('student')"
- ref="studentBody">
- <div class="search-bar-contaienr panel-search " ref="studentSearch">
- <ss-objp v-model="searchFilters.student.class" name="bjid" :opt="bjidOption"
- placeholder="班级" width="150px" inp="true"
- url="/service?ssServ=loadObjpOpt&objectpickerdropdown1=1" cb="bj"></ss-objp>
- <ss-search-input name="roomnmae" placeholder="姓名" v-model="searchFilters.student.name"
- width="100px" @search="search">
- </ss-search-input>
- </div>
-
- <div class="panel-list" ref="studentList">
- <div v-for="student in filteredStudents" :key="student.ryid" class="panel-list-item"
- draggable="true"
- @dragstart="handleDragStart('student', student, $event)"
- @dragend="handleDragEnd">
- <span>{{ student.xm }}</span>
- <span class="panel-item-code">{{ student.displayId }}</span>
- </div>
- </div>
- </div>
- </div>
- <!-- 可安排的监考人员 -->
- <div class="panel">
- <div class="panel-header"
- :class="{ 'can-resize': canResize('supervisor'), resizing: isResizing('supervisor') }"
- @mousedown="handlePanelPress('supervisor', $event)" @mouseup="handlePanelRelease"
- @mouseleave="handlePanelRelease" ref="supervisorHeader">
- <div class="panel-header-group">
- <ss-common-icon class="panel-icon common-icon-renyuan2"></ss-common-icon>
- <span class="panel-title">可安排的监考人员</span>
- </div>
- <button type="button" class="panel-toggle" @click.stop="togglePanel('supervisor')"
- @mousedown.stop @mouseup.stop>
- <ss-common-icon :class="['panel-arrow', getPanelToggleIcon('supervisor')]"></ss-common-icon>
- </button>
- <button v-if="isResizing('supervisor')" class="panel-resize-indicator" title="拖动调整高度"
- style="margin-left: auto;">
- <ss-common-icon class="panel-resize-icon common-icon-paixu"></ss-common-icon>
- </button>
- </div>
- <div class="panel-body" v-show="!panels.supervisor.collapsed"
- :style="getPanelBodyStyle('supervisor')" ref="supervisorBody">
- <div class="search-bar-contaienr panel-search " ref="supervisorSearch">
- <ss-objp v-model="searchFilters.supervisor.dept" name="bmid" :opt="bjidOption"
- placeholder="部门" width="150px" inp="true"
- url="/service?ssServ=loadObjpOpt&objectpickerdropdown1=1" cb="bm"></ss-objp>
- <ss-search-input name="roomnmae" placeholder="姓名" v-model="searchFilters.supervisor.name"
- width="100px" @search="search">
- </ss-search-input>
- </div>
-
- <div class="panel-list" ref="supervisorList">
- <div v-for="person in filteredSupervisors" :key="person.ryid" class="panel-list-item"
- draggable="true"
- @dragstart="handleDragStart('supervisor', person, $event)"
- @dragend="handleDragEnd">
- <span>{{ person.xm }}</span>
- <span class="panel-item-code">{{ person.displayId }}</span>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- <div class='bottom-div'>
- <ss-bottom-button text="取消" @click="handleCancel" icon-class="bottom-div-close"></ss-bottom-button>
- <ss-bottom-button text="保存" @click="submitForm" icon-class="bottom-div-save"></ss-bottom-button>
- </div>
- </div>
- <script>
- /*
- * 以下脚本负责考室编排页的所有动态行为:
- * - 通过后端接口加载实际数据
- * - 通过 Vue 的 data/computed/methods 管理状态、拖拽、校验与保存
- */
- SS.ready(function () {
- window.SS.dom.initializeFormApp({
- el: "#app",
- data() {
- const normalizeStr = (val) => {
- if (val === undefined || val === null) return '';
- const text = String(val);
- return text === 'null' ? '' : text;
- };
- const initialKsjh = {
- ksjhid: normalizeStr("${ksjh.ksjhid}"),
- kcid: normalizeStr("${ksjh.kcid}"),
- kcmc: normalizeStr("<ss:cbTrans cb='kc' val='${ksjh.kcid}'/>"),
- njm: normalizeStr("${ksjh.njm}"),
- njmc: normalizeStr("<ss:cbTrans cb='nj' val='${ksjh.njm}'/>"),
- xqm: normalizeStr("${ksjh.xqm}"),
- xqmc: normalizeStr("<ss:cbTrans cb='xq' val='${ksjh.xqm}'/>"),
- rxnd: normalizeStr("${ksjh.rxnd}"),
- ssCount: 0,
- totalCapacity: 0,
- arrangedCount: 0
- };
- return {
- ksjh: initialKsjh,
- ksapList: [],
- kbpSsList: [],
- kbpJkcyList: [],
- wbpKscyList: [],
- selectedKsapid: '',
- panels: {
- room: { collapsed: false },
- student: { collapsed: false },
- supervisor: { collapsed: false }
- },
- panelHeights: {
- room: 260,
- student: 280,
- supervisor: 240
- },
- panelOrder: ['room', 'student', 'supervisor'],
- resizeState: {
- timer: null,
- active: false,
- pair: null,
- startY: 0,
- startUpper: 0,
- totalHeight: 0
- },
- searchFilters: {
- supervisor: { dept: '', name: '' },
- room: { building: '', name: '' },
- student: { class: '', name: '' }
- },
- dragState: { type: '', item: null },
- dropIndicators: {
- room: false,
- supervisor: '',
- student: ''
- },
- newRoomSerial: 1,
- loading: false
- };
- },
- // 计算属性用于生成面板列表展示与筛选选项
- computed: {
- filteredSupervisors() {
- const keyword = (this.searchFilters.supervisor.name || '').trim();
- const deptFilter = this.normalizeId(this.searchFilters.supervisor.dept);
- return this.kbpJkcyList.filter(person => {
- if (deptFilter) {
- const personDept = this.normalizeId(person.bmid || person.deptId || person.dept);
- if (personDept !== deptFilter) {
- return false;
- }
- }
- if (!keyword) return true;
- const matchName = person.xm && person.xm.includes(keyword);
- const matchId = person.displayId && String(person.displayId).includes(keyword);
- return matchName || matchId;
- });
- },
- filteredRooms() {
- const keyword = (this.searchFilters.room.name || '').trim();
- const buildingFilter = this.normalizeId(this.searchFilters.room.building);
- return this.kbpSsList.filter(room => {
- if (buildingFilter) {
- const roomBuilding = this.normalizeId(room.jzwid || room.building);
- if (roomBuilding !== buildingFilter) {
- return false;
- }
- }
- if (!keyword) return true;
- const matchName = room.ssmc && room.ssmc.includes(keyword);
- const matchCode = room.jzwid && String(room.jzwid).includes(keyword);
- return matchName || matchCode;
- });
- },
- filteredStudents() {
- const keyword = (this.searchFilters.student.name || '').trim();
- const classFilter = this.normalizeId(this.searchFilters.student.class);
- return this.wbpKscyList.filter(student => {
- if (classFilter) {
- const studentClass = this.normalizeId(student.bjid || student.classId);
- if (studentClass !== classFilter) {
- return false;
- }
- }
- if (!keyword) return true;
- const matchName = student.xm && student.xm.includes(keyword);
- const matchId = student.displayId && String(student.displayId).includes(keyword);
- return matchName || matchId;
- });
- },
- roomBuildingOptions() {
- const set = new Set();
- this.kbpSsList.forEach(room => {
- if (room.building) set.add(room.building);
- });
- return Array.from(set);
- }
- },
- // methods 里按功能拆分:交互控制、面板高度、拖拽编排、删除/回流、校验与保存
- methods: {
- search() {},
- fetchInitialData() {
- if (!this.ksjh.ksjhid) {
- this.updateSummaryStats();
- return;
- }
- const _this = this;
- const ksapUrl = "<ss:serv name='ksap_loadArr' parm='{\"wdConfirmationCaptchaService\":\"0\"}'/>";
- $.ajax({
- url: ksapUrl,
- data: { ksjhid: this.ksjh.ksjhid },
- async: true,
- type: "POST",
- dataType: "json",
- success(result) {
- if (result && result.ssCode === 0 && result.ssData) {
- _this.applyServerData(result.ssData);
- } else {
- alert((result && result.ssMsg) || '数据加载失败');
- }
- },
- error() {
- alert('数据加载失败,请稍后重试');
- }
- });
- },
- applyServerData(ssData) {
- const rooms = Array.isArray(ssData.ksapList) ? ssData.ksapList.map(item => this.normalizeServerRoom(item)) : [];
- this.ksapList = rooms;
- this.selectedKsapid = rooms.length ? rooms[0].ksapid : '';
- this.kbpSsList = Array.isArray(ssData.kbpSsList)
- ? ssData.kbpSsList.map(ss => ({
- ssid: ss.ssid || ss.cdid || '',
- ssmc: ss.ssmc || ss.mc || '',
- building: ss.building || '',
- capacity: ss.zdrs || ss.rnrs || 0,
- jzwid: ss.jzwid || ''
- }))
- : [];
- this.kbpJkcyList = Array.isArray(ssData.kbpJkcyList)
- ? ssData.kbpJkcyList.map(person => ({
- ...person,
- ryid: person.ryid || person.jkryid || person.rybh || '',
- xm: person.xm || person.name || '',
- displayId: person.ryid || person.rybh || ''
- }))
- : [];
- this.wbpKscyList = Array.isArray(ssData.wbpKscyList)
- ? ssData.wbpKscyList.map(student => ({
- ...student,
- displayId: student.ryh || ''
- }))
- : [];
- this.newRoomSerial = this.ksapList.length + 1;
- if (ssData.ksjh) {
- this.ksjh = Object.assign({}, this.ksjh, ssData.ksjh);
- }
- this.updateSummaryStats();
- },
- normalizeServerRoom(item) {
- const monitors = Array.isArray(item.jkcyList) ? item.jkcyList : [];
- const students = Array.isArray(item.kscyList) ? item.kscyList : [];
- const baseCapacity = item.zdrs || item.capacity || item.rnrs || '';
- return {
- ksapid: item.ksapid || ('xz-' + (this.newRoomSerial++)),
- xh: item.xh || 0,
- dqrs: item.dqrs || students.length,
- zdrs: item.zdrs || baseCapacity || '',
- ssid: item.ssid || item.cdid || '',
- ssmc: item.ssmc || '',
- building: item.building || '',
- defaultCapacity: baseCapacity ? String(baseCapacity) : '',
- zjkry: item.zjkryid ? { ryid: item.zjkryid, xm: item.zjkryxm || '' } : null,
- jkcyList: monitors.map(p => ({
- ryid: p.ryid,
- xm: p.xm,
- displayId: p.ryid || p.rybh || ''
- })),
- kscyList: students.map(s => ({
- ryid: s.ryid,
- xm: s.xm,
- displayId: s.ryh || ''
- })),
- showCapacityAlert: false
- };
- },
- updateSummaryStats() {
- const leftCapacity = this.ksapList.reduce((sum, room) => {
- const cap = Number(room.zdrs || room.defaultCapacity || 0);
- if (Number.isNaN(cap) || cap <= 0) return sum;
- return sum + cap;
- }, 0);
- const rightCapacity = this.kbpSsList.reduce((sum, room) => {
- const cap = Number(room.capacity || room.rnrs || 0);
- if (Number.isNaN(cap) || cap <= 0) return sum;
- return sum + cap;
- }, 0);
- const totalCapacity = leftCapacity + rightCapacity;
- const totalRoomCount = this.ksapList.length + this.kbpSsList.length;
- const arranged = this.ksapList.reduce((sum, room) => sum + (room.kscyList ? room.kscyList.length : 0), 0);
- this.ksjh.ssCount = totalRoomCount;
- this.ksjh.totalCapacity = totalCapacity;
- this.ksjh.arrangedCount = arranged;
- },
- collectAssignedMonitorIds() {
- const ids = [];
- const normalizeId = this.normalizeId;
- this.ksapList.forEach(room => {
- if (room.zjkry && room.zjkry.ryid) {
- ids.push(normalizeId(room.zjkry.ryid));
- }
- room.jkcyList.forEach(p => {
- if (p.ryid) {
- ids.push(normalizeId(p.ryid));
- }
- });
- });
- return ids;
- },
- collectAssignedStudentIds() {
- const ids = [];
- const normalizeId = this.normalizeId;
- this.ksapList.forEach(room => {
- room.kscyList.forEach(s => {
- if (s.ryid) {
- ids.push(normalizeId(s.ryid));
- }
- });
- });
- return ids;
- },
- distributeStudents(studentList) {
- if (!studentList || !studentList.length) return;
- const pending = studentList.slice();
- this.ksapList.forEach(room => {
- const capacity = this.resolveRoomCapacity(room);
- if (!capacity) return;
- while (pending.length && room.kscyList.length < capacity) {
- const student = pending.shift();
- if (!student) break;
- if (student.ryid && this.isStudentAssigned(student.ryid)) {
- continue;
- }
- const normalized = this.normalizeStudentEntry(student);
- room.kscyList.push(normalized);
- }
- room.dqrs = room.kscyList.length;
- });
- this.wbpKscyList = pending;
- this.updateSummaryStats();
- },
- normalizeStudentEntry(student) {
- if (!student) return null;
- return {
- ...student,
- displayId: student.displayId || student.ryh || ''
- };
- },
- resolveRoomCapacity(room) {
- if (!room) return 0;
- let value = Number(room.zdrs);
- if (!value || Number.isNaN(value) || value <= 0) {
- if (room.defaultCapacity) {
- room.zdrs = room.defaultCapacity;
- value = Number(room.zdrs);
- }
- }
- if (!value || Number.isNaN(value) || value <= 0) {
- room.showCapacityAlert = true;
- return 0;
- }
- room.showCapacityAlert = false;
- return value;
- },
- shuffleStudents(list) {
- if (!Array.isArray(list)) return [];
- const arr = list.slice();
- for (let i = arr.length - 1; i > 0; i -= 1) {
- const j = Math.floor(Math.random() * (i + 1));
- const temp = arr[i];
- arr[i] = arr[j];
- arr[j] = temp;
- }
- return arr;
- },
- normalizeId(value) {
- if (value === undefined || value === null) return '';
- return String(value);
- },
- prepareSavePayload() {
- const normalizeId = this.normalizeId;
- return this.ksapList.map((room, index) => {
- const monitorIds = [];
- if (room.zjkry && room.zjkry.ryid) {
- monitorIds.push(normalizeId(room.zjkry.ryid));
- }
- room.jkcyList.forEach(person => {
- if (person.ryid) {
- monitorIds.push(normalizeId(person.ryid));
- }
- });
- const studentIds = room.kscyList
- .map(student => student.ryid)
- .filter(id => !!id)
- .map(id => normalizeId(id));
- return {
- xh: String(index + 1),
- zdrs: String(room.zdrs || ''),
- ssid: normalizeId(room.ssid),
- jkryidList: monitorIds,
- kscyidList: studentIds
- };
- });
- },
- buildConflictMessage(conflictData) {
- if (!conflictData) return '';
- const wrapDetail = (text) => (text ? '(' + text + ')' : '');
- const parts = [];
- (conflictData.jxry || []).forEach(item => {
- parts.push('教师冲突:' + (item.xm || '') + wrapDetail(item.ms));
- });
- (conflictData.jxrc || []).forEach(item => {
- parts.push('教师日程冲突:' + (item.xm || '') + wrapDetail(item.ms));
- });
- (conflictData.jxcd || []).forEach(item => {
- parts.push('场地冲突:' + (item.cdmc || '') + wrapDetail(item.ms));
- });
- return parts.join('\n');
- },
- // 选中左侧卡片,更新高亮状态便于用户确认当前操作对象
- selectRoom(id) {
- this.selectedKsapid = id;
- },
- /* ---------- 拖拽相关逻辑(右侧 → 左侧) ---------- */
- // 统一记录拖拽起点及数据,便于 drop 阶段判断拖拽来源
- handleDragStart(type, item, event) {
- this.dragState = { type, item };
- if (event?.dataTransfer) {
- event.dataTransfer.effectAllowed = 'copyMove';
- event.dataTransfer.setData('text/plain', type);
- }
- },
- // 拖拽结束或取消时统一清理拖拽状态和高亮
- handleDragEnd() {
- this.dragState = { type: '', item: null };
- this.resetDropIndicators();
- },
- // 关闭所有 drop 高亮提示,防止状态残留
- resetDropIndicators() {
- this.dropIndicators.room = false;
- this.dropIndicators.supervisor = '';
- this.dropIndicators.student = '';
- },
- // 课室容器允许投放课室时显示灰色虚线提示
- handleRoomDragOver(event) {
- if (this.dragState?.type === 'room') {
- event.preventDefault();
- if (event?.dataTransfer) {
- event.dataTransfer.dropEffect = 'copy';
- }
- this.dropIndicators.room = true;
- }
- },
- // 鼠标离开容器时即时取消高亮
- handleRoomDragLeave() {
- this.dropIndicators.room = false;
- },
- // 课室拖拽到左侧容器,自动创建考室卡片并滚动到最右侧
- handleRoomDrop() {
- if (!this.dragState || this.dragState.type !== 'room') return;
- this.dropIndicators.room = false;
- const roomData = this.dragState.item || {};
- if (roomData.ssid && this.ksapList.some(room => room.ssid === roomData.ssid)) {
- alert('该课室已在左侧。');
- this.handleDragEnd();
- return;
- }
- const initialCapacity = roomData.capacity || roomData.rnrs || '';
- const newRoom = {
- ksapid: roomData.ksapid || ('xz-' + (this.newRoomSerial++)),
- xh: this.ksapList.length + 1,
- dqrs: 0,
- zdrs: '',
- ssid: roomData.ssid || '',
- ssmc: roomData.ssmc || '新增考室',
- building: roomData.building || '',
- defaultCapacity: initialCapacity === '' ? '' : String(initialCapacity),
- zjkry: null,
- jkcyList: [],
- kscyList: [],
- showCapacityAlert: false
- };
- this.ksapList.push(newRoom);
- if (roomData.ssid) {
- this.kbpSsList = this.kbpSsList.filter(r => r.ssid !== roomData.ssid);
- }
- this.ksjh.ssCount = this.ksapList.length;
- this.updateSummaryStats();
- this.$nextTick(() => this.scrollToLatestRoom());
- this.handleDragEnd();
- },
- // 新增考室后将卡片容器滚动至最右,保证最新卡片可见
- scrollToLatestRoom() {
- const container = this.$refs.leftContent;
- if (container) {
- const maxLeft = Math.max(container.scrollWidth - container.clientWidth, 0);
- container.scrollTo({
- left: maxLeft,
- behavior: 'smooth'
- });
- }
- },
- // 监考区在可投放状态下提示高亮
- handleSupervisorDragOver(room, event) {
- if (this.dragState?.type === 'supervisor') {
- if (event?.dataTransfer) {
- event.dataTransfer.dropEffect = 'copy';
- }
- this.dropIndicators.supervisor = room.ksapid;
- }
- },
- // 拖拽离开监考区时重置高亮
- handleSupervisorDragLeave(room) {
- if (this.dropIndicators.supervisor === room.ksapid) {
- this.dropIndicators.supervisor = '';
- }
- },
- // 监考员拖拽到考室后,若未重复,优先填充主监考,再追加到监考列表
- handleSupervisorDrop(room) {
- if (!this.dragState || this.dragState.type !== 'supervisor') return;
- const person = this.dragState.item;
- if (!person) return;
- if (this.isSupervisorAssigned(person.ryid)) {
- this.handleDragEnd();
- return;
- }
- if (!room.zjkry) {
- room.zjkry = { ...person };
- } else if (!room.jkcyList.some(p => p.ryid === person.ryid)) {
- room.jkcyList.push({ ...person });
- }
- this.kbpJkcyList = this.kbpJkcyList.filter(p => p.ryid !== person.ryid);
- this.dropIndicators.supervisor = '';
- this.handleDragEnd();
- },
- // 考生区在可投放状态下提示高亮
- handleStudentDragOver(room, event) {
- if (this.dragState?.type === 'student') {
- if (event?.dataTransfer) {
- event.dataTransfer.dropEffect = 'copy';
- }
- this.dropIndicators.student = room.ksapid;
- }
- },
- // 拖拽离开考生区时重置高亮
- handleStudentDragLeave(room) {
- if (this.dropIndicators.student === room.ksapid) {
- this.dropIndicators.student = '';
- }
- },
- // 考生拖拽到考室时,验证容量与重复后才允许落入
- handleStudentDrop(room) {
- if (!this.dragState || this.dragState.type !== 'student') return;
- const student = this.dragState.item;
- if (!student) return;
- // Ⅰ. 同一个考生只能存在于一个考室中
- if (this.isStudentAssigned(student.ryid)) {
- this.handleDragEnd();
- return;
- }
- // Ⅱ. 未设置容纳人数时直接拒绝
- if (room.zdrs === '' || room.zdrs === null || room.zdrs === undefined) {
- room.showCapacityAlert = true;
- this.handleDragEnd();
- return;
- }
- const capacity = Number(room.zdrs);
- // Ⅲ. 容纳人数需为正且未超员
- if (Number.isNaN(capacity) || capacity <= 0 || room.kscyList.length >= capacity) {
- if (!capacity || Number.isNaN(capacity) || capacity <= 0) {
- room.showCapacityAlert = true;
- }
- this.handleDragEnd();
- return;
- }
- // Ⅳ. 将考生写入考室列表,并同步右侧未编排列表
- room.kscyList.push({ ...student });
- room.dqrs = room.kscyList.length;
- this.wbpKscyList = this.wbpKscyList.filter(s => s.ryid !== student.ryid);
- this.dropIndicators.student = '';
- this.updateSummaryStats();
- this.handleDragEnd();
- },
- handleCapacityInput(room) {
- const value = Number(room.zdrs);
- if (value && !Number.isNaN(value) && value > 0) {
- room.showCapacityAlert = false;
- }
- },
- /* ---------- 底部按钮:取消 / 保存 ---------- */
- // 点击“取消”按钮时默认回退,若无历史则尝试关闭窗口
- handleCancel() {
- if (window.history?.back) {
- window.history.back();
- } else {
- window.close?.();
- }
- },
- // 点击“保存”按钮:先校验,后输出格式化数据
- submitForm() {
- if (!this.validateBeforeSubmit()) return;
- if (!this.ksjh.ksjhid) {
- alert('缺少考试计划信息,无法保存');
- return;
- }
- const payload = this.prepareSavePayload();
- const monitorIds = this.collectAssignedMonitorIds();
- const ssIds = payload.map(item => item.ssid).filter(id => !!id);
- const checkUrl = "<ss:serv name='ksap_chkArr' parm='{\"wdConfirmationCaptchaService\":\"0\"}'/>";
- const _this = this;
- $.ajax({
- url: checkUrl,
- data: {
- ksjhid: this.ksjh.ksjhid,
- allJxry: monitorIds.join(','),
- allSs: ssIds.join(',')
- },
- type: "POST",
- dataType: "json",
- success(result) {
- const message = _this.buildConflictMessage(result);
- if (message) {
- if (confirm('发现冲突:\n' + message + '\n是否继续保存?')) {
- _this.savePayload(payload);
- }
- } else {
- _this.savePayload(payload);
- }
- },
- error() {
- alert('保存前校验失败,请稍后重试');
- }
- });
- },
- // 保存前统一校验:课室、主监考、容量、人员重复等
- validateBeforeSubmit() {
- const errors = [];
- const monitorIds = new Set();
- const studentIds = new Set();
- this.ksapList.forEach((room, idx) => {
- const label = room.ssmc || ('第' + (idx + 1) + '间考室');
- const capacity = Number(room.zdrs);
- if (!room.ssid) {
- errors.push(label + ' 未选择课室。');
- }
- if (!room.zjkry) {
- errors.push(label + ' 需要至少一名主监考。');
- }
- if (!room.zdrs || Number.isNaN(capacity) || capacity <= 0) {
- errors.push(label + ' 的容纳人数必须为大于 0 的数字。');
- } else {
- if (room.kscyList.length === 0) {
- errors.push(label + ' 至少需要分配 1 名考生。');
- }
- if (room.kscyList.length > capacity) {
- errors.push(label + ' 的考生数量超过容纳人数。');
- }
- }
- if (room.zjkry) {
- if (monitorIds.has(room.zjkry.ryid)) {
- errors.push(room.zjkry.xm + ' 被重复分配。');
- } else {
- monitorIds.add(room.zjkry.ryid);
- }
- }
- room.jkcyList.forEach(person => {
- if (monitorIds.has(person.ryid)) {
- errors.push(person.xm + ' 被重复分配。');
- } else {
- monitorIds.add(person.ryid);
- }
- });
- room.kscyList.forEach(student => {
- if (studentIds.has(student.ryid)) {
- errors.push(student.xm + ' 被重复分配。');
- } else {
- studentIds.add(student.ryid);
- }
- });
- });
- if (errors.length) {
- alert(errors.join('\n'));
- return false;
- }
- return true;
- },
- savePayload(payload) {
- const saveUrl = "<ss:serv name='ksap_saveArr' parm='{\"wdConfirmationCaptchaService\":\"0\"}'/>";
- const _this = this;
- $.ajax({
- url: saveUrl,
- data: {
- "requestParentViewObject":"ksjh",
- ksjhid: this.ksjh.ksjhid,
- ksapList: JSON.stringify(payload)
- },
- type: "POST",
- dataType: "json",
- success(result) {
- if (result && (result.flag === true || result.flag === "true")) {
- alert(result.msg || '保存成功');
- _this.fetchInitialData();
- } else {
- alert((result && result.msg) || '保存失败');
- }
- },
- error() {
- alert('保存失败,请稍后重试');
- }
- });
- },
- // 判断监考员是否已在任一考室中,避免重复
- isSupervisorAssigned(ryid) {
- return this.ksapList.some(room => {
- if (room.zjkry && room.zjkry.ryid === ryid) return true;
- return room.jkcyList.some(p => p.ryid === ryid);
- });
- },
- // 判断考生是否已在任一考室中,避免重复
- isStudentAssigned(ryid) {
- return this.ksapList.some(room =>
- room.kscyList.some(s => s.ryid === ryid)
- );
- },
- // 删除整张考室卡片时,课室/监考/考生三类资源全部退回右侧
- deleteRoom(room) {
- if (confirm('确定要删除该考室吗?')) {
- if (room.zjkry) {
- if (!this.kbpJkcyList.some(p => p.ryid === room.zjkry.ryid)) {
- this.kbpJkcyList.push({ ...room.zjkry });
- }
- }
- room.jkcyList.forEach(p => {
- if (!this.kbpJkcyList.some(item => item.ryid === p.ryid)) {
- this.kbpJkcyList.push({ ...p });
- }
- });
- room.kscyList.forEach(s => {
- if (!this.wbpKscyList.some(item => item.ryid === s.ryid)) {
- this.wbpKscyList.push({ ...s });
- }
- });
- if (room.ssid) {
- const exists = this.kbpSsList.find(r => r.ssid === room.ssid);
- if (!exists) {
- this.kbpSsList.push({
- ssid: room.ssid,
- ssmc: room.ssmc,
- building: room.building || '',
- capacity: room.zdrs
- });
- }
- }
- const index = this.ksapList.findIndex(r => r.ksapid === room.ksapid);
- if (index > -1) this.ksapList.splice(index, 1);
- this.ksjh.ssCount = this.ksapList.length;
- this.updateSummaryStats();
- }
- },
- // 从考室中移除监考,顺便放回右侧待选列
- removeSupervisor(room, person) {
- if (room.zjkry && room.zjkry.ryid === person.ryid) {
- room.zjkry = null;
- } else {
- const index = room.jkcyList.findIndex(p => p.ryid === person.ryid);
- if (index > -1) room.jkcyList.splice(index, 1);
- }
- if (!this.kbpJkcyList.some(p => p.ryid === person.ryid)) {
- this.kbpJkcyList.push({ ...person });
- }
- },
- // 从考室中移除考生,顺便放回右侧待编排列表
- removeStudent(room, student) {
- const index = room.kscyList.findIndex(s => s.ryid === student.ryid);
- if (index > -1) {
- room.kscyList.splice(index, 1);
- room.dqrs = room.kscyList.length;
- }
- if (!this.wbpKscyList.some(s => s.ryid === student.ryid)) {
- this.wbpKscyList.push({ ...student });
- }
- this.updateSummaryStats();
- },
- // 折叠/展开右侧面板,同时保持高度分布
- togglePanel(type) {
- this.panels[type].collapsed = !this.panels[type].collapsed;
- if (!this.panels[type].collapsed) {
- const minHeight = this.getPanelMinHeight(type);
- this.panelHeights[type] = Math.max(this.panelHeights[type] || minHeight, minHeight);
- }
- this.normalizePanelHeights();
- },
- // 根据折叠状态返回对应的箭头图标类名
- getPanelToggleIcon(type) {
- return this.panels[type].collapsed ? 'common-icon-arrow-bottom' : 'common-icon-arrow-top';
- },
- getPanelBodyStyle(type) {
- const height = this.panels[type].collapsed ? 0 : this.panelHeights[type];
- return {
- height: String(height) + 'px'
- };
- },
- // 长按面板标题后开启高度拖拽功能,配合延迟防止误触
- handlePanelPress(type, event) {
- if ((event.button !== 0 && event.button !== undefined) || this.resizeState.active) return;
- const pair = this.getResizePair(type);
- if (!pair) return;
- event.preventDefault();
- this.cancelResizeTimer();
- const startY = event.clientY;
- this.resizeState.timer = setTimeout(() => {
- this.startPanelResize(pair, startY);
- }, 250);
- },
- // 鼠标提前抬起时取消进入拖拽状态
- handlePanelRelease() {
- if (!this.resizeState.active) {
- this.cancelResizeTimer();
- }
- },
- // 真正进入拖拽时记录初始高度,以便后续计算
- startPanelResize(pair, startY) {
- const { upper, lower } = pair;
- if (this.panels[upper].collapsed || this.panels[lower].collapsed) return;
- this.resizeState.active = true;
- this.resizeState.pair = pair;
- this.resizeState.startY = startY;
- this.resizeState.startUpper = this.panelHeights[upper];
- this.resizeState.totalHeight = this.panelHeights[upper] + this.panelHeights[lower];
- document.addEventListener('mousemove', this.onPanelMouseMove);
- document.addEventListener('mouseup', this.onDocumentMouseUp);
- },
- // 拖拽过程中动态更新上下两个面板高度,保持总高度不变
- onPanelMouseMove(event) {
- if (!this.resizeState.active || !this.resizeState.pair) return;
- const delta = event.clientY - this.resizeState.startY;
- const { upper, lower } = this.resizeState.pair;
- const minUpper = this.getPanelMinHeight(upper);
- const minLower = this.getPanelMinHeight(lower);
- const total = this.resizeState.totalHeight;
- let nextUpper = this.resizeState.startUpper + delta;
- const maxUpper = total - minLower;
- nextUpper = Math.min(Math.max(nextUpper, minUpper), maxUpper);
- const nextLower = total - nextUpper;
- this.panelHeights[upper] = nextUpper;
- this.panelHeights[lower] = nextLower;
- },
- // 鼠标抬起后收尾:停止监听并重置拖拽状态
- onDocumentMouseUp() {
- if (this.resizeState.active) {
- this.stopPanelResize();
- } else {
- this.cancelResizeTimer();
- }
- },
- stopPanelResize() {
- document.removeEventListener('mousemove', this.onPanelMouseMove);
- document.removeEventListener('mouseup', this.onDocumentMouseUp);
- this.resizeState.active = false;
- this.resizeState.pair = null;
- this.resizeState.startY = 0;
- this.resizeState.startUpper = 0;
- this.resizeState.totalHeight = 0;
- this.cancelResizeTimer();
- },
- // 清理长按定时器,防止普通点击也触发拖拽
- cancelResizeTimer() {
- if (this.resizeState.timer) {
- clearTimeout(this.resizeState.timer);
- this.resizeState.timer = null;
- }
- },
- // 判断某个面板当前是否处于“被拖拽”的状态,用于控制提示图标
- isResizing(type) {
- return this.resizeState.active && this.resizeState.pair && this.resizeState.pair.lower === type;
- },
- // 计算面板最小高度:至少展示搜索区域+一行列表
- getPanelMinHeight(type) {
- const search = this.getRefEl(type + 'Search');
- const list = this.getRefEl(type + 'List');
- const searchHeight = search ? search.offsetHeight : 0;
- let listHeight = 0;
- if (list) {
- const firstItem = list.querySelector('.panel-list-item');
- if (firstItem && this.getPanelDataLength(type) > 0) {
- listHeight = firstItem.offsetHeight;
- }
- }
- const base = searchHeight + listHeight + 24;
- return Math.max(base, 120);
- },
- // 提供给高度约束逻辑,用来判断某面板当前列表数据条数
- getPanelDataLength(type) {
- if (type === 'room') return this.filteredRooms.length;
- if (type === 'student') return this.filteredStudents.length;
- if (type === 'supervisor') return this.filteredSupervisors.length;
- return 0;
- },
- // 折叠/展开后重新按比例分配高度,保持用户调整后的整体比例
- normalizePanelHeights() {
- if (this.resizeState.active) return;
- this.$nextTick(() => {
- this.updatePanelHeightsToContainer();
- });
- },
- // 根据容器总高度、面板最小高度,重新计算可用空间分配
- updatePanelHeightsToContainer() {
- const container = this.$refs.rightPanel;
- if (!container) return;
- const visiblePanels = this.panelOrder.filter(type => !this.panels[type].collapsed);
- if (!visiblePanels.length) return;
- const headersHeight = this.getHeadersHeight();
- const available = container.clientHeight - headersHeight;
- if (available <= 0) return;
- const ratioBase = visiblePanels.reduce((sum, type) => sum + (this.panelHeights[type] || 1), 0) || visiblePanels.length;
- let remaining = available;
- const newHeights = {};
- visiblePanels.forEach(type => {
- const min = this.getPanelMinHeight(type);
- newHeights[type] = min;
- remaining -= min;
- });
- if (remaining < 0) {
- remaining = 0;
- }
- visiblePanels.forEach(type => {
- const ratio = ratioBase ? (this.panelHeights[type] || 1) / ratioBase : 1 / visiblePanels.length;
- const extra = remaining * ratio;
- newHeights[type] += extra;
- });
- visiblePanels.forEach(type => {
- this.panelHeights[type] = newHeights[type];
- });
- },
- // 统计所有面板标题栏高度,用于扣除后计算剩余内容高度
- getHeadersHeight() {
- return this.panelOrder.reduce((sum, type) => {
- const header = this.getRefEl(type + 'Header');
- return sum + (header ? header.offsetHeight : 0);
- }, 0);
- },
- // 自动填充考生:调用后端自动编排接口或使用本地候选列表
- autoArrange() {
- if (!this.ksapList.length) {
- alert('请先添加考室');
- return;
- }
- if (!this.ksjh.ksjhid) {
- alert('缺少考试计划信息,无法自动编排');
- return;
- }
- if (!this.wbpKscyList.length) {
- return;
- }
- const randomized = this.shuffleStudents(this.wbpKscyList);
- this.distributeStudents(randomized);
- },
- // 离开页面前统一卸载事件并重置状态,避免内存泄漏
- cleanupResizeListeners() {
- document.removeEventListener('mousemove', this.onPanelMouseMove);
- document.removeEventListener('mouseup', this.onDocumentMouseUp);
- this.cancelResizeTimer();
- this.resizeState.active = false;
- this.resizeState.pair = null;
- this.resizeState.startY = 0;
- this.resizeState.startUpper = 0;
- this.resizeState.totalHeight = 0;
- if (this.$_panelResizeHandler) {
- window.removeEventListener('resize', this.$_panelResizeHandler);
- this.$_panelResizeHandler = null;
- }
- },
- // 兼容模板中 ref 可能成数组的情况,统一获取原生 DOM
- getRefEl(name) {
- const el = this.$refs[name];
- if (Array.isArray(el)) {
- return el[0] || null;
- }
- return el || null;
- },
- // 根据面板类型找到其上一个未折叠的面板,组成拖拽调整的上下组合
- getResizePair(type) {
- if (this.panels[type].collapsed) return null;
- const idx = this.panelOrder.indexOf(type);
- if (idx === -1) return null;
- const upper = this.getPrevExpandedPanel(idx - 1);
- if (!upper) return null;
- return { upper, lower: type };
- },
- getPrevExpandedPanel(fromIndex) {
- for (let i = fromIndex; i >= 0; i -= 1) {
- const candidate = this.panelOrder[i];
- if (!this.panels[candidate].collapsed) {
- return candidate;
- }
- }
- return null;
- },
- // 判断某面板是否允许被上下拖拽(上方需有展开的面板)
- canResize(type) {
- if (this.panels[type].collapsed) return false;
- const idx = this.panelOrder.indexOf(type);
- if (idx <= 0) return false;
- return !!this.getPrevExpandedPanel(idx - 1);
- }
- },
- beforeUnmount() {
- this.cleanupResizeListeners();
- },
- beforeDestroy() {
- this.cleanupResizeListeners();
- },
- mounted() {
- console.log('UI Refreshed', this.$data);
- // 进入页面后立即根据容器高度分配三块面板的初始高度
- this.normalizePanelHeights();
- // 监听窗口尺寸变化,保持右侧面板高度自适应
- this.$_panelResizeHandler = () => this.normalizePanelHeights();
- window.addEventListener('resize', this.$_panelResizeHandler);
- this.fetchInitialData();
- }
- });
- });
- </script>
- </body>
- </html>
|