| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136 |
- <%@ page import="java.util.List,java.util.Map" %>
- <%@ page language="java" pageEncoding="UTF-8" isELIgnored="false" %>
- <%@ taglib uri="/ssTag" prefix="ss"%>
- <% pageContext.setAttribute(ss.page.PageC.PAGE_objName,"xy");%>
- <%pageContext.setAttribute("wdpageinformation","{'hastab':'0'}");%>
- <!DOCTYPE html>
- <html>
- <head>
- <%@ include file="/page/clip/header.jsp" %>
- <script>window.loginStatus="${empty sessionScope['ssUser']?'0':'1'}"</script>
- <link rel="stylesheet" type="text/css" href="/ss/window/theme/dhtmlxwindows.css">
- <link rel="stylesheet" type="text/css" href="/ss/window/theme/dhx_blue/dhtmlxwindows_dhx_blue.css">
- <script type="text/javascript" src="/ss/window/dhtmlxcommon.js"></script>
- <script type="text/javascript" src="/ss/window/dhtmlxwindows.js"></script>
- <script type="text/javascript" src="/ss/window/dhtmlxcontainer.js"></script>
- <script type="text/javascript" src="/ss/js/display.js"></script>
- <%-- 改。Lin
- <@script type="text/javascript" src="/${sessionScope['XMMC']}/js/yx/yx_zjz.js"></script> --%>
- <%-- 页面拍照逻辑内联处理,不再依赖 /ss/yx/yx_zjz.js。 --%>
- <%-- 改,去掉 /wd/js/ueditor/dialogs/wdimage/upload.js,改用 /wd/js/upload.js。Lin
- <script type="text/javascript" src="/wd/js/ueditor/dialogs/wdimage/upload.js"></script>
- --%><script type="text/javascript" src="/ss/js/upload.js"></script>
- <style type="text/css">
- html,
- body,
- body.env-input-body{
- /* 功能说明:锁定页面基础高度链,避免右侧长列表因父级高度不确定而外溢 by xu 20260410 */
- height: 100%;
- margin: 0;
- overflow: hidden;
- background: #f5f7fb;
- }
-
- #app.form-container{
- /* 功能说明:锁定表单容器高度,给右侧列表滚动提供确定高度基准 by xu 20260410 */
- height: 100%;
- min-height: 0;
- overflow: hidden;
- }
- .form-container .content-box{
- /* 功能说明:锁定内容容器高度,避免内部 flex/grid 按内容撑开 by xu 20260410 */
- height: 100% !important;
- min-height: 0;
- padding: 0 !important;
- overflow: hidden;
- }
- .xy-zjz-page{
- /* 功能说明:锁定页面主区域高度,避免学生列表卡片继续向外撑出 by xu 20260410 */
- height: 100%;
- min-height: 0;
- padding: 0;
- box-sizing: border-box;
- overflow: hidden;
- }
- .xy-zjz-layout{
- /* 功能说明:锁定左右布局高度,确保右侧列表只能在剩余空间内滚动 by xu 20260410 */
- height: 100%;
- min-height: 0;
- display: flex;
- gap: 0;
- overflow: hidden;
- }
- .xy-zjz-left{
- flex: 0 0 61%;
- min-width: 0;
- display: flex;
- flex-direction: column;
- align-items: center;
- background: #ffffff;
- border-radius: 4px;
- padding: 30px 46px;
- box-sizing: border-box;
- border-right: 1px solid #e2e4ec;
- }
- .xy-zjz-right{
- /* 功能说明:右侧改为固定信息区 + 分隔线 + 自适应列表区,彻底避免长名单撑出 by xu 20260410 */
- flex: 0 0 39%;
- min-width: 320px;
- min-height: 0;
- height: 100%;
- display: grid;
- grid-template-rows: 234px 1px minmax(0, 1fr);
- overflow: hidden;
- background: #f7f7f7;
- }
- .xy-zjz-info-wrapper{
- height: 234px;
- padding: 30px 35px 35px;
- box-sizing: border-box;
- overflow: hidden;
- }
- .xy-zjz-camera-toolbar{
- display: none;
- }
- .xy-zjz-preview-shell{
- width: 100%;
- flex: 1;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- #sfzImg.xy-zjz-preview{
- position: relative;
- width: min(100%, 498px);
- height: auto;
- aspect-ratio: 498/756;
- max-height: 756px;
- background: #8e8e8e;
- overflow: hidden;
- box-sizing: border-box;
- border: none;
- }
- #sfzImg.xy-zjz-preview.is-live{
- border: 1px solid #3f3f3f;
- }
- /* 功能说明:照片回显层独立控制显示状态,避免误伤蒙版层 by xu 20260410 */
- #sfzImg.xy-zjz-preview #image{
- position: absolute;
- inset: 0;
- width: 100% !important;
- height: 100% !important;
- object-fit: cover;
- display: none;
- z-index: 1;
- }
- /* 功能说明:视频预览层独立控制显示状态,避免误伤蒙版层 by xu 20260410 */
- #sfzImg.xy-zjz-preview #video{
- position: absolute;
- inset: 0;
- width: 100% !important;
- height: 100% !important;
- object-fit: cover;
- display: none;
- z-index: 1;
- }
- /* 功能说明:给视频预览叠加独立人脸定位蒙版,始终显示在最上层 by xu 20260410 */
- #sfzImg.xy-zjz-preview .xy-zjz-face-mask{
- /* 功能说明:蒙版直接铺满整个预览框,消除上下留白 by xu 20260410 */
- position: absolute;
- inset: 0;
- width: 100%;
- height: 100%;
- object-fit: fill;
- pointer-events: none;
- z-index: 4;
- display: none;
- opacity: 0.5;
- }
- /* 功能说明:未连接状态下占位层放在蒙版下方,仅保留头像引导层级 by xu 20260410 */
- #sfzImg.xy-zjz-preview .xy-zjz-camera-placeholder{
- z-index: 2;
- background: transparent;
- }
- /* 功能说明:引导头像位于蒙版下方、背景上方,符合校准视觉层级 by xu 20260410 */
- #sfzImg.xy-zjz-preview .xy-zjz-camera-placeholder::before{
- z-index: 3;
- }
- /* 功能说明:提示文案层级低于蒙版,避免影响人脸框观察 by xu 20260410 */
- #sfzImg.xy-zjz-preview .xy-zjz-camera-placeholder{
- color: rgba(255,255,255,0.72);
- }
- #canvas,
- #file{
- display: none;
- }
- .xy-zjz-camera-placeholder{
- position: absolute;
- inset: 0;
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 0 32px;
- text-align: center;
- font-size: 18px;
- line-height: 28px;
- color: rgba(255,255,255,0.88);
- background: linear-gradient(180deg, rgba(0,0,0,0.08), rgba(0,0,0,0.16));
- }
- .xy-zjz-camera-placeholder::before{
- content: "";
- position: absolute;
- width: 70%;
- height: 70%;
- background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 240'%3E%3Cpath fill='rgba(255,255,255,0.25)' d='M100 20c-25 0-45 20-45 45 0 15 7 28 18 36-20 10-35 30-40 55-2 10-3 20-3 30h140c0-10-1-20-3-30-5-25-20-45-40-55 11-8 18-21 18-36 0-25-20-45-45-45z'/%3E%3C/svg%3E") center/contain no-repeat;
- opacity: 0.8;
- }
- .xy-zjz-action-stack{
- width: min(100%, 360px);
- padding-top: 20px;
- }
- .xy-zjz-action-stack.bottom-div{
- position: static;
- display: block;
- height: auto;
- padding: 20px 0 0;
- border-top: none;
- background: transparent !important;
- width: min(100%, 360px);
- }
- .xy-zjz-action-stack.bottom-div ss-bottom-button{
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 8px;
- font-size: 16px;
- min-width: 140px;
- padding: 10px 24px;
- border-radius: 4px;
- border: 1px solid #d9d9d9;
- background: #fff;
- color: #666;
- cursor: pointer;
- transition: all 0.2s;
- }
- .xy-zjz-action-stack.bottom-div ss-bottom-button:hover{
- border-color: #409eff;
- color: #409eff;
- }
- .xy-zjz-action-stack.bottom-div ss-bottom-button::before{
- content: "📷";
- font-size: 18px;
- }
- .xy-zjz-action-bar{
- display: flex;
- justify-content: center;
- gap: 12px;
- width: 100%;
- }
- .xy-zjz-action-bar ss-bottom-button,
- .xy-zjz-action-bar #photoConnectBtn,
- .xy-zjz-action-bar #photoCaptureBtn,
- .xy-zjz-action-bar #photoRetakeBtn,
- .xy-zjz-action-bar #photoSubmitBtn{
- min-width: 140px;
- }
- .xy-zjz-card,
- .xy-zjz-list-card{
- background: #fff;
- border: 1px solid #e3e7f0;
- border-radius: 4px;
- box-sizing: border-box;
- }
- .xy-zjz-card{
- width: 100%;
- height: 100%;
- box-sizing: border-box;
- background: #ffffff;
- border: 1px solid #dddfe6;
- border-radius: 4px;
- padding: 18px 20px;
- }
- .xy-zjz-info-name{
- font-size: 20px;
- line-height: 1;
- font-weight: 500;
- color: #010101;
- margin-bottom: 18px;
- }
- .xy-zjz-info-body{
- display: flex;
- gap: 0;
- align-items: flex-end;
- }
- .xy-zjz-info-avatar{
- width: 68px;
- height: 100px;
- border: 1px solid #dddfe6;
- background: #f2f3f4;
- overflow: hidden;
- flex: 0 0 68px;
- border-radius: 4px;
- }
- .xy-zjz-info-avatar.has-image{
- border: none;
- }
- .xy-zjz-info-avatar img{
- width: 100%;
- height: 100%;
- object-fit: cover;
- display: block;
- }
- .xy-zjz-info-lines{
- flex: 1;
- padding-left: 20px;
- min-width: 0;
- display: flex;
- flex-direction: column;
- justify-content: flex-end;
- }
- .xy-zjz-info-line{
- font-size: 18px;
- line-height: 28px;
- color: #666;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- line-height: 1.4;
- }
- .xy-zjz-info-line:last-child{
- color: #000;
- }
- .xy-zjz-info-line strong{
- font-weight: normal;
- color: inherit;
- }
- .xy-zjz-right-divider{
- width: 100%;
- height: 1px;
- background: #e2e4ec;
- flex-shrink: 0;
- }
- .xy-zjz-list-card{
- /* 功能说明:学生列表卡片占满右侧剩余高度,超长时仅列表内部滚动 by xu 20260410 */
- height: 100%;
- padding: 16px 14px 12px;
- display: flex;
- flex-direction: column;
- min-height: 0;
- max-height: 100%;
- overflow: hidden;
- box-sizing: border-box;
- background: #f7f7f7;
- }
- .xy-zjz-filter-row{
- display: flex;
- gap: 10px;
- margin-bottom: 10px;
- align-items: center;
- }
- .xy-zjz-filter-class{
- font-size: 16px;
- font-weight: 500;
- color: #333;
- margin-right: auto;
- }
- .xy-zjz-filter-progress{
- font-size: 14px;
- color: #666;
- margin-right: 12px;
- }
- .xy-zjz-filter-input{
- width: 120px;
- height: 32px;
- border: 1px solid #d9d9d9;
- border-radius: 4px;
- background: #fff;
- padding: 0 12px;
- font-size: 14px;
- color: #333;
- box-sizing: border-box;
- transition: border-color 0.2s;
- }
- .xy-zjz-filter-input:focus{
- outline: none;
- border-color: #409eff;
- }
- .xy-zjz-filter-input::placeholder{
- color: #999;
- }
- .xy-zjz-filter-select{
- display: none;
- }
- .xy-zjz-filter-summary{
- display: flex;
- justify-content: flex-end;
- align-items: center;
- padding: 0 2px 10px;
- }
- .xy-zjz-class-count{
- font-size: 16px;
- line-height: 24px;
- color: #5f6b80;
- }
- .xy-zjz-student-list{
- flex: 1;
- min-height: 0;
- overflow-y: auto;
- padding-top: 2px;
- border-top: 1px solid #e8e8e8;
- }
- .xy-zjz-student-row{
- display: grid;
- grid-template-columns: 32px 1fr 1.5fr;
- align-items: center;
- gap: 12px;
- height: 40px;
- padding: 0 8px;
- font-size: 14px;
- color: #333;
- border-radius: 4px;
- box-sizing: border-box;
- cursor: pointer;
- transition: background 0.15s;
- }
- .xy-zjz-student-row:hover{
- background: #f5f7fa;
- }
- .xy-zjz-student-row + .xy-zjz-student-row{
- margin-top: 2px;
- }
- .xy-zjz-student-row.is-active{
- background: #e8eaed;
- }
- .xy-zjz-student-row.is-active .xy-zjz-student-index{
- color: #666;
- }
- .xy-zjz-student-index{
- text-align: center;
- color: #999;
- font-size: 13px;
- }
- .xy-zjz-student-name{
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- font-weight: 500;
- color: #333;
- }
- .xy-zjz-student-id{
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- color: #666;
- font-size: 13px;
- }
- .xy-zjz-empty{
- padding: 32px 0;
- text-align: center;
- font-size: 14px;
- color: #999;
- }
- </style>
- </head>
- <body class="env-input-body">
- <form id="app" class="form-container" action="<ss:serv name='xyZjz_excelSureAdd' parm='{"wdConfirmationCaptchaService":"0"}' dest='info'/>" method="post">
- <div class="content-box fit-height-content">
- <div class="content-div xy-zjz-page" ssFith="true">
- <%
- List bjcyList = (List)request.getAttribute("bjcyList");
- int bjcyCount = bjcyList == null ? 0 : bjcyList.size();
- Object firstStudent = bjcyCount > 0 ? bjcyList.get(0) : null;
- pageContext.setAttribute("bjcyCount", Integer.valueOf(bjcyCount));
- pageContext.setAttribute("firstStudent", firstStudent);
- %>
- <script>
- // 功能说明:将 JSP 侧真实班级成员数据输出给 Vue 使用,替换前端 mock 数据 by xu 20260410
- window.xyZjzRealStudentList = [
- <%
- for (int i = 0; i < bjcyCount; i++) {
- Object rowObj = bjcyList.get(i);
- if (!(rowObj instanceof Map)) {
- continue;
- }
- Map row = (Map) rowObj;
- String ryid = row.get("ryid") == null ? "" : row.get("ryid").toString().replace("\\", "\\\\").replace("'", "\\'");
- String xm = row.get("xm") == null ? "" : row.get("xm").toString().replace("\\", "\\\\").replace("'", "\\'");
- String ryh = row.get("ryh") == null ? "" : row.get("ryh").toString().replace("\\", "\\\\").replace("'", "\\'");
- String zjzwj = row.get("zjzwj") == null ? "" : row.get("zjzwj").toString().replace("\\", "\\\\").replace("'", "\\'");
- %>
- { ryid: '<%= ryid %>', xm: '<%= xm %>', ryh: '<%= ryh %>', zjzwj: '<%= zjzwj %>' }<%= i < bjcyCount - 1 ? "," : "" %>
- <%
- }
- %>
- ];
- // 功能说明:同步输出班级名称给 Vue 显示 by xu 20260410
- window.xyZjzRealBjmc = "<ss:cbTrans cb='bj' val='${bjid}'/>";
- </script>
- <input name="ryid" value="${firstStudent.ryid}" type="hidden"/>
- <input name="zjzwj" value="${firstStudent.zjzwj}" type="hidden">
- <input name='wdComponentID' type='hidden' value='xyZjz_excelAdd'/>
- <input type="file" id="file">
- <select id="videoSource" style="display:none;"></select>
- <div class="xy-zjz-layout">
- <div class="xy-zjz-left">
- <div class="xy-zjz-camera-toolbar"></div>
- <div class="xy-zjz-preview-shell">
- <div id="sfzImg" class="photo xy-zjz-preview">
- <div class="xy-zjz-camera-placeholder" id="cameraPlaceholder">点击“连接摄像头”后由浏览器弹出设备/权限选择</div>
- <%-- 功能说明:叠加人脸蒙版图,辅助拍摄时对齐位置 by xu 20260410 --%>
- <img class="xy-zjz-face-mask" src="/skin/easy/image/xy-zjz-face-mask.png" alt="人脸定位蒙版"/>
- <%-- 功能说明:修复图片标签未闭合导致 video/canvas DOM 丢失,摄像头初始化取不到元素 by xu 20260410 --%>
- <img id="image"
- src="<ss:serv name='dlByHttp' parm='{"wdConfirmationCaptchaService":"0","path":"${firstStudent.zjzwj}","type":"img"}'/>"
- onerror="this.src='data:image/svg+xml,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' width=\'100\' height=\'100\'%3E%3Crect width=\'100\' height=\'100\' fill=\'%23f0f0f0\'/%3E%3Ctext x=\'50\' y=\'55\' font-size=\'12\' fill=\'%23999\' text-anchor=\'middle\'%3E无照片%3C/text%3E%3C/svg%3E'"
- />
- <video autoplay muted playsinline id="video"></video>
- <canvas id="canvas"></canvas>
- </div>
- </div>
- <div class="xy-zjz-action-stack bottom-div">
- <div class="xy-zjz-action-bar" id="cameraConnectActions">
- <ss-bottom-button
- id="photoConnectBtn"
- text="连接摄像头"
- @click='handleConnectCamera()'
- icon-class="bottom-div-save"
- ></ss-bottom-button>
- </div>
- <div class="xy-zjz-action-bar" id="cameraCaptureActions" style="display:none;">
- <ss-bottom-button
- id="photoCaptureBtn"
- text="拍照"
- @click='handleCapturePhoto()'
- icon-class="bottom-div-save"
- ></ss-bottom-button>
- </div>
- <div class="xy-zjz-action-bar" id="cameraReviewActions" style="display:none;">
- <ss-bottom-button
- id="photoRetakeBtn"
- text="重拍"
- @click='handleRetakePhoto()'
- icon-class="bottom-div-save"
- ></ss-bottom-button>
- <ss-bottom-button
- id="photoSubmitBtn"
- text="保存并提交"
- @click='handleSubmitPhotoForm()'
- icon-class="bottom-div-save"
- ></ss-bottom-button>
- </div>
- </div>
- </div>
- <div class="xy-zjz-right">
- <div class="xy-zjz-info-wrapper">
- <div class="xy-zjz-card">
- <template v-if="currentStudent">
- <div class="xy-zjz-info-name">{{currentStudent.xm}}</div>
- <div class="xy-zjz-info-body">
- <div class="xy-zjz-info-avatar">
- <img :src="currentStudent.zjzwj ? '/service?ssServ=dlByHttp&type=img&path=' + currentStudent.zjzwj : 'data:image/svg+xml,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' width=\'68\' height=\'100\'%3E%3Crect width=\'68\' height=\'100\' fill=\'%23f2f3f4\'/%3E%3Ctext x=\'34\' y=\'55\' font-size=\'11\' fill=\'%23999\' text-anchor=\'middle\'%3E无照片%3C/text%3E%3C/svg%3E'"/>
- </div>
- <div class="xy-zjz-info-lines">
- <div v-if="currentStudent.xbmc" class="xy-zjz-info-line"><strong>性别:</strong>{{currentStudent.xbmc}}</div>
- <div v-if="currentStudent.jyfsmc" class="xy-zjz-info-line"><strong>就读方式:</strong>{{currentStudent.jyfsmc}}</div>
- <div v-if="bjmc" class="xy-zjz-info-line"><strong>班级:</strong>{{bjmc}}</div>
- <div v-if="currentStudent.ryh" class="xy-zjz-info-line"><strong>学号:</strong>{{currentStudent.ryh}}</div>
- </div>
- </div>
- </template>
- <div v-else class="xy-zjz-info-empty">
- <div class="xy-zjz-info-name">--</div>
- </div>
- </div>
- </div>
- <div class="xy-zjz-right-divider"></div>
- <div class="xy-zjz-list-card" id="studentListCard">
- <div class="xy-zjz-filter-row">
- <select class="xy-zjz-filter-select">
- <option>年级</option>
- </select>
- <select class="xy-zjz-filter-select">
- <option>班级</option>
- </select>
- <span class="xy-zjz-filter-class"><ss:cbTrans cb='bj' val='${bjid}'/></span>
- <span class="xy-zjz-filter-progress">{{currentIndex + 1}}/{{bjcyCount}}</span>
- <input class="xy-zjz-filter-input" type="text" placeholder="姓名"/>
- </div>
- <div class="xy-zjz-student-list" id="studentListBody">
- <div v-for="(item, index) in studentList" :key="index"
- :class="['xy-zjz-student-row', currentIndex === index ? 'is-active' : '']"
- @click="selectStudent(index, item)">
- <div class="xy-zjz-student-index">{{index + 1}}</div>
- <div class="xy-zjz-student-name">{{item.xm}}</div>
- <div class="xy-zjz-student-id">{{item.ryh}}</div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </form>
- <script type="text/javascript">
- (function(){
- var settings = {
- width: 1000,
- height: 1292,
- action: "/service?ssServ=ulByHttp&type=img"
- };
- var state = {
- stream: null,
- mode: "disconnected",
- enumerated: false,
- capturedBlob: null,
- capturedDataUrl: ""
- };
- var elements = {};
- function $(id){
- return document.getElementById(id);
- }
- function ensureElements(){
- elements.preview = $("sfzImg");
- elements.video = $("video");
- elements.image = $("image");
- elements.canvas = $("canvas");
- elements.file = $("file");
- elements.videoSource = $("videoSource");
- elements.placeholder = $("cameraPlaceholder");
- elements.faceMask = document.querySelector("#sfzImg .xy-zjz-face-mask");
- elements.connectActions = $("cameraConnectActions");
- elements.captureActions = $("cameraCaptureActions");
- elements.reviewActions = $("cameraReviewActions");
- return !!(elements.preview && elements.video && elements.image && elements.canvas && elements.file && elements.videoSource && elements.placeholder && elements.faceMask && elements.connectActions && elements.captureActions && elements.reviewActions);
- }
- function stopTracks(stream){
- if (!stream || !stream.getTracks) {
- return;
- }
- stream.getTracks().forEach(function(track){
- track.stop();
- });
- }
- function setPlaceholder(text){
- if (!ensureElements() || !elements.placeholder) {
- return;
- }
- elements.placeholder.textContent = text || "";
- elements.placeholder.style.display = "flex";
- }
- function hidePlaceholder(){
- if (!ensureElements()) {
- return;
- }
- elements.placeholder.style.display = "none";
- }
- function clearStream(){
- stopTracks(state.stream);
- state.stream = null;
- if (elements.video) {
- elements.video.pause();
- elements.video.srcObject = null;
- }
- }
- function renderCameraOptions(deviceInfos){
- if (!ensureElements() || !elements.videoSource) {
- return false;
- }
- while (elements.videoSource.options.length > 0) {
- elements.videoSource.remove(0);
- }
- var videoDevices = deviceInfos.filter(function(item){
- return item.kind === "videoinput";
- });
- if (!videoDevices.length) {
- var emptyOption = document.createElement("option");
- emptyOption.value = "";
- emptyOption.text = "未检测到摄像头";
- elements.videoSource.appendChild(emptyOption);
- elements.videoSource.disabled = true;
- return false;
- }
- elements.videoSource.disabled = false;
- videoDevices.forEach(function(device, index){
- var option = document.createElement("option");
- option.value = device.deviceId;
- option.text = device.label || ("摄像头" + (index + 1));
- elements.videoSource.appendChild(option);
- });
- if (elements.videoSource.options.length > 0) {
- elements.videoSource.selectedIndex = 0;
- }
- return true;
- }
- async function ensureCameraOptions(){
- if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
- alert("当前浏览器不支持摄像头访问");
- return false;
- }
- if (!ensureElements()) {
- return false;
- }
- if (state.enumerated && elements.videoSource.options.length > 0 && elements.videoSource.options[0].value !== "") {
- return true;
- }
- var permissionStream = null;
- try {
- permissionStream = await navigator.mediaDevices.getUserMedia({video: true, audio: false});
- var deviceInfos = await navigator.mediaDevices.enumerateDevices();
- state.enumerated = renderCameraOptions(deviceInfos);
- return state.enumerated;
- } catch (error) {
- handleCameraError(error);
- return false;
- } finally {
- stopTracks(permissionStream);
- }
- }
- async function startPreview(deviceId){
- if (!ensureElements()) {
- return false;
- }
- clearStream();
- var constraints = {
- video: deviceId ? {deviceId: {exact: deviceId}} : true,
- audio: false
- };
- try {
- var stream = await navigator.mediaDevices.getUserMedia(constraints);
- state.stream = stream;
- elements.video.srcObject = stream;
- await elements.video.play();
- elements.video.style.display = "block";
- elements.image.style.display = "none";
- hidePlaceholder();
- return true;
- } catch (error) {
- if (deviceId) {
- var fallbackStream = await navigator.mediaDevices.getUserMedia({video: true, audio: false});
- state.stream = fallbackStream;
- elements.video.srcObject = fallbackStream;
- await elements.video.play();
- elements.video.style.display = "block";
- elements.image.style.display = "none";
- hidePlaceholder();
- return true;
- }
- handleCameraError(error);
- return false;
- }
- }
- function setActionMode(mode){
- if (!ensureElements()) {
- return;
- }
- state.mode = mode;
- // 功能说明:连接按钮仅在首次未建立预览前显示,后续重拍不再回退到首次连接步骤 by xu 20260410
- var shouldShowConnect = mode === "disconnected" && !state.enumerated;
- elements.connectActions.style.display = shouldShowConnect ? "flex" : "none";
- elements.captureActions.style.display = mode === "live" ? "flex" : "none";
- elements.reviewActions.style.display = mode === "captured" ? "flex" : "none";
- if (mode === "live") {
- elements.preview.classList.add("is-live");
- elements.faceMask.style.display = "block";
- } else {
- elements.preview.classList.remove("is-live");
- elements.faceMask.style.display = "none";
- }
- if (mode === "disconnected") {
- clearStream();
- elements.video.style.display = "none";
- elements.image.style.display = "none";
- setPlaceholder("点击连接摄像头后由浏览器弹出设备/权限选择");
- }
- }
- function handleCameraError(error){
- console.error("Error: ", error);
- clearStream();
- if (elements.video) {
- elements.video.style.display = "none";
- }
- setPlaceholder("摄像头连接失败,请检查权限后重试");
- alert("摄像头连接失败,请检查设备权限后重试");
- }
- async function connectCamera(){
- if (!ensureElements()) {
- return false;
- }
- setPlaceholder("正在请求浏览器摄像头权限...");
- var canUseCamera = await ensureCameraOptions();
- if (!canUseCamera) {
- return false;
- }
- var connected = await startPreview(elements.videoSource.value);
- if (connected) {
- setActionMode("live");
- }
- return connected;
- }
- // 功能说明:拍照后仅截取当前画面做本地预览,不立即上传,等待保存并提交时再上传 by xu 20260410
- function capturePhoto(){
- if (!ensureElements()) {
- return;
- }
- if (!state.stream) {
- connectCamera();
- return;
- }
- var width = elements.video.videoWidth || elements.preview.clientWidth;
- var height = elements.video.videoHeight || elements.preview.clientHeight;
- elements.canvas.width = width;
- elements.canvas.height = height;
- var context = elements.canvas.getContext("2d");
- context.clearRect(0, 0, width, height);
- context.drawImage(elements.video, 0, 0, width, height);
- var imgdataBase64 = elements.canvas.toDataURL("image/jpeg", 0.92);
- state.capturedBlob = convertBase64UrlToBlob(imgdataBase64);
- state.capturedDataUrl = imgdataBase64;
- clearStream();
- elements.image.src = imgdataBase64;
- elements.image.style.display = "block";
- elements.video.style.display = "none";
- hidePlaceholder();
- document.querySelector("input[name='zjzwj']").value = "";
- setActionMode("captured");
- }
- // 功能说明:将拍照截图直接上传到图片服务,返回文件路径 by xu 20260410
- function uploadCapturedPhoto(blob){
- return new Promise(function(resolve, reject){
- var fileName = new Date().getTime() + ".jpg";
- var formData = new FormData();
- formData.append("application", "");
- formData.append("fileEdit", new File([blob], fileName, { type: "image/jpeg" }));
- $.ajax({
- url: settings.action,
- type: "POST",
- data: formData,
- processData: false,
- contentType: false,
- success: function(result){
- try {
- if (typeof result === "string") {
- result = eval("(" + result + ")");
- }
- var path = result && result.fileList && result.fileList[0] && result.fileList[0].path;
- if (path) {
- resolve(path);
- } else {
- reject(new Error("上传返回路径为空"));
- }
- } catch (error) {
- reject(error);
- }
- },
- error: function(xhr){
- reject(xhr);
- }
- });
- });
- }
- function cropConfirm(result){
- if (!ensureElements()) {
- return;
- }
- clearStream();
- var path = result.fileList[0].path;
- var url = "/service?ssServ=dlByHttp&type=img&path=" + path;
- elements.image.src = url;
- elements.image.style.display = "block";
- elements.video.style.display = "none";
- hidePlaceholder();
- document.querySelector("input[name='zjzwj']").value = path;
- setActionMode("captured");
- }
- // 功能说明:重拍时直接回到预览态,不再回到首次连接步骤 by xu 20260410
- async function retakePhoto(){
- if (!ensureElements()) {
- return;
- }
- state.capturedBlob = null;
- state.capturedDataUrl = "";
- document.querySelector("input[name='zjzwj']").value = "";
- elements.image.style.display = "none";
- var connected = await startPreview(elements.videoSource.value);
- if (connected) {
- setActionMode("live");
- }
- }
- // 功能说明:保存并提交时若存在本地截图则先上传,再回填路径并提交表单 by xu 20260410
- async function submitPhotoForm(){
- var formEl = document.getElementById("app") || document.getElementById("xyZjzPhotoForm");
- if (!formEl) {
- alert("表单不存在");
- return false;
- }
- var zjzwjInput = document.querySelector("input[name='zjzwj']");
- if (!zjzwjInput) {
- alert("缺少照片字段");
- return false;
- }
- if (!zjzwjInput.value && state.capturedBlob) {
- try {
- var uploadedPath = await uploadCapturedPhoto(state.capturedBlob);
- zjzwjInput.value = uploadedPath;
- state.capturedBlob = null;
- } catch (error) {
- console.error(error);
- alert("拍照上传失败,请重试");
- return false;
- }
- }
- if (!zjzwjInput.value) {
- alert("请先拍照");
- return false;
- }
- formEl.submit();
- return true;
- }
- // 功能说明:支持 Enter 快捷键,未连接时连摄像头、预览时截图、已截图时直接保存并提交 by xu 20260410
- function handleEnterShortcut(event){
- var target = event.target || {};
- var tagName = (target.tagName || "").toLowerCase();
- if (tagName === "input" || tagName === "textarea" || tagName === "select" || target.isContentEditable) {
- return;
- }
- if (event.key !== "Enter") {
- return;
- }
- event.preventDefault();
- if (state.mode === "disconnected") {
- connectCamera();
- return;
- }
- if (state.mode === "live") {
- capturePhoto();
- return;
- }
- if (state.mode === "captured") {
- submitPhotoForm();
- }
- }
- function convertBase64UrlToBlob(urlData) {
- var bytes = window.atob(urlData.split(",")[1]);
- var ab = new ArrayBuffer(bytes.length);
- var ia = new Uint8Array(ab);
- for (var i = 0; i < bytes.length; i++) {
- ia[i] = bytes.charCodeAt(i);
- }
- return new Blob([ab], {
- type: "image/jpeg"
- });
- }
- window.xyZjzCameraPage = {
- connectCamera: connectCamera,
- capturePhoto: capturePhoto,
- retakePhoto: retakePhoto,
- submitPhotoForm: submitPhotoForm
- };
- function initPage(){
- if (!ensureElements()) {
- return;
- }
- setActionMode("disconnected");
- // 功能说明:初始化时绑定 Enter 快捷键,仅绑定一次 by xu 20260410
- if (!window.__xyZjzEnterShortcutBound) {
- document.addEventListener("keydown", handleEnterShortcut);
- window.__xyZjzEnterShortcutBound = true;
- }
- syncStudentListHeight();
- }
- // 功能说明:按右侧真实可用高度动态限制学生列表区域,避免长名单把卡片继续撑出 by xu 20260410
- function syncStudentListHeight(){
- var rightPanel = document.querySelector(".xy-zjz-right");
- var listCard = document.getElementById("studentListCard");
- var filterRow = document.querySelector(".xy-zjz-filter-row");
- var studentList = document.getElementById("studentListBody");
- if (!rightPanel || !listCard || !studentList) {
- return;
- }
- var rightRect = rightPanel.getBoundingClientRect();
- var listCardRect = listCard.getBoundingClientRect();
- var filterRect = filterRow ? filterRow.getBoundingClientRect() : {height: 0};
- var listCardStyle = window.getComputedStyle(listCard);
- var extraHeight = parseFloat(listCardStyle.paddingTop || 0)
- + parseFloat(listCardStyle.paddingBottom || 0)
- + filterRect.height
- + 12;
- var availableHeight = Math.floor(rightRect.bottom - listCardRect.top);
- if (availableHeight > 0) {
- listCard.style.height = availableHeight + "px";
- listCard.style.maxHeight = availableHeight + "px";
- studentList.style.maxHeight = Math.max(80, Math.floor(availableHeight - extraHeight)) + "px";
- }
- }
- window.addEventListener("resize", syncStudentListHeight);
- window.addEventListener("load", syncStudentListHeight);
- setTimeout(syncStudentListHeight, 0);
- setTimeout(syncStudentListHeight, 300);
- if (document.readyState === "loading") {
- document.addEventListener("DOMContentLoaded", initPage);
- } else {
- initPage();
- }
- })();
- var file = {};
- function initCropper1(fileEle, settings, cropConfirm, fileDate) {
- file["name"] = new Date().getTime() + ".png";
- initWdCropper();
- wdCropper.topWin = wd.topWindow;
- wdCropper.thisWindow = window;
- wdCropper.settings = settings;
- wdCropper.fileEle = fileEle;
- wdCropper.cropConfirm = cropConfirm;
- wdCropper.cropper.fileName = file.name;
- wdCropper.fileObj = fileDate;
- var uploadedImageURL = URL.createObjectURL(fileDate);
- var img = new Image();
- img.onload = function() {
- wdCropper.layer.show({
- width: img.width,
- height: img.height
- });
- wdCropper.cropper.init();
- wdCropper.cropper.replace(uploadedImageURL);
- };
- img.src = uploadedImageURL;
- }
- </script>
- <script type="text/javascript">var wdRecordValue='${wdRecordValue}';</script>
- <script type="text/javascript" src="/ss/js/wdRecord.js"></script>
- <script type="text/javascript">(function(){wdRecord("xyZjz_excelAdd");})();</script>
- <script type="text/javascript" src="/ss/js/wdFitHeight.js"></script>
- <script type="text/javascript">initWdFitHeight(0)</script>
- <script type="text/javascript">initWdFitHeightFunction=function(){initWdFitHeight(0);};</script>
- <ss:equal val="${empty resizeComponent}" val2="false">
- <script>{var iframe=wd.display.getFrameOfWindow();
- if(iframe&&iframe.contentWindow==window)
- wd.display.resizeComponent(${resizeComponent.width}, ${resizeComponent.height}, ${empty resizeComponent.minHeight?'null':resizeComponent.minHeight}, ${empty resizeComponent.maxHeight?'null':resizeComponent.maxHeight});}</script>
- </ss:equal>
- <ss:help/>
- </body>
- <script type="text/javascript">
- try{wd.display.showMsgPopup('${msg}');
- }catch(err){console.error(err);}
- </script>
- <ss:equal val="${empty wdclosewindowparam}" val2="false">
- <script type="text/javascript">
- try{wd.display.setCloseWindowParam('${wdclosewindowparam}');
- }catch(err){console.error(err);}
- </script>
- </ss:equal>
- </html>
- <%@ include file="/page/clip/footer.jsp" %>
- <script>
- </script>
- <script>
- // 功能说明:Vue 初始化改为读取 JSP 注入的真实数据,移除前端 mock 数据 by xu 20260410
- (function() {
- var studentList = Array.isArray(window.xyZjzRealStudentList) ? window.xyZjzRealStudentList : [];
- var firstStudent = studentList.length ? studentList[0] : null;
- var emptyImage = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100'%3E%3Crect width='100' height='100' fill='%23f0f0f0'/%3E%3Ctext x='50' y='55' font-size='12' fill='%23999' text-anchor='middle'%3E无照片%3C/text%3E%3C/svg%3E";
- function setupHijack() {
- if (!window.SS || !window.SS.dom || !window.SS.dom.initializeFormApp) {
- setTimeout(setupHijack, 50);
- return;
- }
- var originalInit = window.SS.dom.initializeFormApp;
- window.SS.dom.initializeFormApp = function(options) {
- if (!options.data) {
- options.data = function() { return {}; };
- }
- var originalDataFn = options.data;
- options.data = function() {
- var originalData = originalDataFn.call(this);
- return Object.assign({}, originalData, {
- currentStudent: firstStudent ? Object.assign({}, firstStudent) : null,
- bjmc: window.xyZjzRealBjmc || '',
- bjcyCount: studentList.length,
- currentIndex: 0,
- studentList: studentList,
- stream: null,
- mode: 'disconnected'
- });
- };
- if (!options.methods) {
- options.methods = {};
- }
- // 功能说明:为 Vue 模板提供拍照按钮桥接方法,统一通过全局拍照对象调用 by xu 20260410
- options.methods.handleConnectCamera = function() {
- return window.xyZjzCameraPage && window.xyZjzCameraPage.connectCamera ? window.xyZjzCameraPage.connectCamera() : false;
- };
- // 功能说明:为 Vue 模板提供拍照按钮桥接方法,统一通过全局拍照对象调用 by xu 20260410
- options.methods.handleCapturePhoto = function() {
- return window.xyZjzCameraPage && window.xyZjzCameraPage.capturePhoto ? window.xyZjzCameraPage.capturePhoto() : false;
- };
- // 功能说明:为 Vue 模板提供拍照按钮桥接方法,统一通过全局拍照对象调用 by xu 20260410
- options.methods.handleRetakePhoto = function() {
- return window.xyZjzCameraPage && window.xyZjzCameraPage.retakePhoto ? window.xyZjzCameraPage.retakePhoto() : false;
- };
- // 功能说明:为 Vue 模板提供拍照按钮桥接方法,统一通过全局拍照对象调用 by xu 20260410
- options.methods.handleSubmitPhotoForm = function() {
- return window.xyZjzCameraPage && window.xyZjzCameraPage.submitPhotoForm ? window.xyZjzCameraPage.submitPhotoForm() : false;
- };
- options.methods.selectStudent = function(index, student) {
- this.currentIndex = index;
- this.currentStudent = Object.assign({}, student);
- var ryidInput = document.querySelector("input[name='ryid']");
- var zjzwjInput = document.querySelector("input[name='zjzwj']");
- if (ryidInput) ryidInput.value = student.ryid || '';
- if (zjzwjInput) zjzwjInput.value = student.zjzwj || '';
- var img = document.getElementById("image");
- if (img && student.zjzwj) {
- img.src = "/service?ssServ=dlByHttp&type=img&path=" + student.zjzwj;
- } else if (img) {
- img.src = emptyImage;
- }
-
- setTimeout(syncStudentListHeight, 0);
- };
- return originalInit.call(this, options);
- };
- }
- if (typeof SS !== 'undefined' && SS.ready) {
- SS.ready(setupHijack);
- } else {
- setTimeout(setupHijack, 100);
- }
- })();
- </script>
|