xyZjz_excelAdd.jsp 35 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136
  1. <%@ page import="java.util.List,java.util.Map" %>
  2. <%@ page language="java" pageEncoding="UTF-8" isELIgnored="false" %>
  3. <%@ taglib uri="/ssTag" prefix="ss"%>
  4. <% pageContext.setAttribute(ss.page.PageC.PAGE_objName,"xy");%>
  5. <%pageContext.setAttribute("wdpageinformation","{'hastab':'0'}");%>
  6. <!DOCTYPE html>
  7. <html>
  8. <head>
  9. <%@ include file="/page/clip/header.jsp" %>
  10. <script>window.loginStatus="${empty sessionScope['ssUser']?'0':'1'}"</script>
  11. <link rel="stylesheet" type="text/css" href="/ss/window/theme/dhtmlxwindows.css">
  12. <link rel="stylesheet" type="text/css" href="/ss/window/theme/dhx_blue/dhtmlxwindows_dhx_blue.css">
  13. <script type="text/javascript" src="/ss/window/dhtmlxcommon.js"></script>
  14. <script type="text/javascript" src="/ss/window/dhtmlxwindows.js"></script>
  15. <script type="text/javascript" src="/ss/window/dhtmlxcontainer.js"></script>
  16. <script type="text/javascript" src="/ss/js/display.js"></script>
  17. <%-- 改。Lin
  18. <@script type="text/javascript" src="/${sessionScope['XMMC']}/js/yx/yx_zjz.js"></script> --%>
  19. <%-- 页面拍照逻辑内联处理,不再依赖 /ss/yx/yx_zjz.js。 --%>
  20. <%-- 改,去掉 /wd/js/ueditor/dialogs/wdimage/upload.js,改用 /wd/js/upload.js。Lin
  21. <script type="text/javascript" src="/wd/js/ueditor/dialogs/wdimage/upload.js"></script>
  22. --%><script type="text/javascript" src="/ss/js/upload.js"></script>
  23. <style type="text/css">
  24. html,
  25. body,
  26. body.env-input-body{
  27. /* 功能说明:锁定页面基础高度链,避免右侧长列表因父级高度不确定而外溢 by xu 20260410 */
  28. height: 100%;
  29. margin: 0;
  30. overflow: hidden;
  31. background: #f5f7fb;
  32. }
  33. #app.form-container{
  34. /* 功能说明:锁定表单容器高度,给右侧列表滚动提供确定高度基准 by xu 20260410 */
  35. height: 100%;
  36. min-height: 0;
  37. overflow: hidden;
  38. }
  39. .form-container .content-box{
  40. /* 功能说明:锁定内容容器高度,避免内部 flex/grid 按内容撑开 by xu 20260410 */
  41. height: 100% !important;
  42. min-height: 0;
  43. padding: 0 !important;
  44. overflow: hidden;
  45. }
  46. .xy-zjz-page{
  47. /* 功能说明:锁定页面主区域高度,避免学生列表卡片继续向外撑出 by xu 20260410 */
  48. height: 100%;
  49. min-height: 0;
  50. padding: 0;
  51. box-sizing: border-box;
  52. overflow: hidden;
  53. }
  54. .xy-zjz-layout{
  55. /* 功能说明:锁定左右布局高度,确保右侧列表只能在剩余空间内滚动 by xu 20260410 */
  56. height: 100%;
  57. min-height: 0;
  58. display: flex;
  59. gap: 0;
  60. overflow: hidden;
  61. }
  62. .xy-zjz-left{
  63. flex: 0 0 61%;
  64. min-width: 0;
  65. display: flex;
  66. flex-direction: column;
  67. align-items: center;
  68. background: #ffffff;
  69. border-radius: 4px;
  70. padding: 30px 46px;
  71. box-sizing: border-box;
  72. border-right: 1px solid #e2e4ec;
  73. }
  74. .xy-zjz-right{
  75. /* 功能说明:右侧改为固定信息区 + 分隔线 + 自适应列表区,彻底避免长名单撑出 by xu 20260410 */
  76. flex: 0 0 39%;
  77. min-width: 320px;
  78. min-height: 0;
  79. height: 100%;
  80. display: grid;
  81. grid-template-rows: 234px 1px minmax(0, 1fr);
  82. overflow: hidden;
  83. background: #f7f7f7;
  84. }
  85. .xy-zjz-info-wrapper{
  86. height: 234px;
  87. padding: 30px 35px 35px;
  88. box-sizing: border-box;
  89. overflow: hidden;
  90. }
  91. .xy-zjz-camera-toolbar{
  92. display: none;
  93. }
  94. .xy-zjz-preview-shell{
  95. width: 100%;
  96. flex: 1;
  97. display: flex;
  98. align-items: center;
  99. justify-content: center;
  100. }
  101. #sfzImg.xy-zjz-preview{
  102. position: relative;
  103. width: min(100%, 498px);
  104. height: auto;
  105. aspect-ratio: 498/756;
  106. max-height: 756px;
  107. background: #8e8e8e;
  108. overflow: hidden;
  109. box-sizing: border-box;
  110. border: none;
  111. }
  112. #sfzImg.xy-zjz-preview.is-live{
  113. border: 1px solid #3f3f3f;
  114. }
  115. /* 功能说明:照片回显层独立控制显示状态,避免误伤蒙版层 by xu 20260410 */
  116. #sfzImg.xy-zjz-preview #image{
  117. position: absolute;
  118. inset: 0;
  119. width: 100% !important;
  120. height: 100% !important;
  121. object-fit: cover;
  122. display: none;
  123. z-index: 1;
  124. }
  125. /* 功能说明:视频预览层独立控制显示状态,避免误伤蒙版层 by xu 20260410 */
  126. #sfzImg.xy-zjz-preview #video{
  127. position: absolute;
  128. inset: 0;
  129. width: 100% !important;
  130. height: 100% !important;
  131. object-fit: cover;
  132. display: none;
  133. z-index: 1;
  134. }
  135. /* 功能说明:给视频预览叠加独立人脸定位蒙版,始终显示在最上层 by xu 20260410 */
  136. #sfzImg.xy-zjz-preview .xy-zjz-face-mask{
  137. /* 功能说明:蒙版直接铺满整个预览框,消除上下留白 by xu 20260410 */
  138. position: absolute;
  139. inset: 0;
  140. width: 100%;
  141. height: 100%;
  142. object-fit: fill;
  143. pointer-events: none;
  144. z-index: 4;
  145. display: none;
  146. opacity: 0.5;
  147. }
  148. /* 功能说明:未连接状态下占位层放在蒙版下方,仅保留头像引导层级 by xu 20260410 */
  149. #sfzImg.xy-zjz-preview .xy-zjz-camera-placeholder{
  150. z-index: 2;
  151. background: transparent;
  152. }
  153. /* 功能说明:引导头像位于蒙版下方、背景上方,符合校准视觉层级 by xu 20260410 */
  154. #sfzImg.xy-zjz-preview .xy-zjz-camera-placeholder::before{
  155. z-index: 3;
  156. }
  157. /* 功能说明:提示文案层级低于蒙版,避免影响人脸框观察 by xu 20260410 */
  158. #sfzImg.xy-zjz-preview .xy-zjz-camera-placeholder{
  159. color: rgba(255,255,255,0.72);
  160. }
  161. #canvas,
  162. #file{
  163. display: none;
  164. }
  165. .xy-zjz-camera-placeholder{
  166. position: absolute;
  167. inset: 0;
  168. display: flex;
  169. align-items: center;
  170. justify-content: center;
  171. padding: 0 32px;
  172. text-align: center;
  173. font-size: 18px;
  174. line-height: 28px;
  175. color: rgba(255,255,255,0.88);
  176. background: linear-gradient(180deg, rgba(0,0,0,0.08), rgba(0,0,0,0.16));
  177. }
  178. .xy-zjz-camera-placeholder::before{
  179. content: "";
  180. position: absolute;
  181. width: 70%;
  182. height: 70%;
  183. 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;
  184. opacity: 0.8;
  185. }
  186. .xy-zjz-action-stack{
  187. width: min(100%, 360px);
  188. padding-top: 20px;
  189. }
  190. .xy-zjz-action-stack.bottom-div{
  191. position: static;
  192. display: block;
  193. height: auto;
  194. padding: 20px 0 0;
  195. border-top: none;
  196. background: transparent !important;
  197. width: min(100%, 360px);
  198. }
  199. .xy-zjz-action-stack.bottom-div ss-bottom-button{
  200. display: flex;
  201. align-items: center;
  202. justify-content: center;
  203. gap: 8px;
  204. font-size: 16px;
  205. min-width: 140px;
  206. padding: 10px 24px;
  207. border-radius: 4px;
  208. border: 1px solid #d9d9d9;
  209. background: #fff;
  210. color: #666;
  211. cursor: pointer;
  212. transition: all 0.2s;
  213. }
  214. .xy-zjz-action-stack.bottom-div ss-bottom-button:hover{
  215. border-color: #409eff;
  216. color: #409eff;
  217. }
  218. .xy-zjz-action-stack.bottom-div ss-bottom-button::before{
  219. content: "📷";
  220. font-size: 18px;
  221. }
  222. .xy-zjz-action-bar{
  223. display: flex;
  224. justify-content: center;
  225. gap: 12px;
  226. width: 100%;
  227. }
  228. .xy-zjz-action-bar ss-bottom-button,
  229. .xy-zjz-action-bar #photoConnectBtn,
  230. .xy-zjz-action-bar #photoCaptureBtn,
  231. .xy-zjz-action-bar #photoRetakeBtn,
  232. .xy-zjz-action-bar #photoSubmitBtn{
  233. min-width: 140px;
  234. }
  235. .xy-zjz-card,
  236. .xy-zjz-list-card{
  237. background: #fff;
  238. border: 1px solid #e3e7f0;
  239. border-radius: 4px;
  240. box-sizing: border-box;
  241. }
  242. .xy-zjz-card{
  243. width: 100%;
  244. height: 100%;
  245. box-sizing: border-box;
  246. background: #ffffff;
  247. border: 1px solid #dddfe6;
  248. border-radius: 4px;
  249. padding: 18px 20px;
  250. }
  251. .xy-zjz-info-name{
  252. font-size: 20px;
  253. line-height: 1;
  254. font-weight: 500;
  255. color: #010101;
  256. margin-bottom: 18px;
  257. }
  258. .xy-zjz-info-body{
  259. display: flex;
  260. gap: 0;
  261. align-items: flex-end;
  262. }
  263. .xy-zjz-info-avatar{
  264. width: 68px;
  265. height: 100px;
  266. border: 1px solid #dddfe6;
  267. background: #f2f3f4;
  268. overflow: hidden;
  269. flex: 0 0 68px;
  270. border-radius: 4px;
  271. }
  272. .xy-zjz-info-avatar.has-image{
  273. border: none;
  274. }
  275. .xy-zjz-info-avatar img{
  276. width: 100%;
  277. height: 100%;
  278. object-fit: cover;
  279. display: block;
  280. }
  281. .xy-zjz-info-lines{
  282. flex: 1;
  283. padding-left: 20px;
  284. min-width: 0;
  285. display: flex;
  286. flex-direction: column;
  287. justify-content: flex-end;
  288. }
  289. .xy-zjz-info-line{
  290. font-size: 18px;
  291. line-height: 28px;
  292. color: #666;
  293. white-space: nowrap;
  294. overflow: hidden;
  295. text-overflow: ellipsis;
  296. line-height: 1.4;
  297. }
  298. .xy-zjz-info-line:last-child{
  299. color: #000;
  300. }
  301. .xy-zjz-info-line strong{
  302. font-weight: normal;
  303. color: inherit;
  304. }
  305. .xy-zjz-right-divider{
  306. width: 100%;
  307. height: 1px;
  308. background: #e2e4ec;
  309. flex-shrink: 0;
  310. }
  311. .xy-zjz-list-card{
  312. /* 功能说明:学生列表卡片占满右侧剩余高度,超长时仅列表内部滚动 by xu 20260410 */
  313. height: 100%;
  314. padding: 16px 14px 12px;
  315. display: flex;
  316. flex-direction: column;
  317. min-height: 0;
  318. max-height: 100%;
  319. overflow: hidden;
  320. box-sizing: border-box;
  321. background: #f7f7f7;
  322. }
  323. .xy-zjz-filter-row{
  324. display: flex;
  325. gap: 10px;
  326. margin-bottom: 10px;
  327. align-items: center;
  328. }
  329. .xy-zjz-filter-class{
  330. font-size: 16px;
  331. font-weight: 500;
  332. color: #333;
  333. margin-right: auto;
  334. }
  335. .xy-zjz-filter-progress{
  336. font-size: 14px;
  337. color: #666;
  338. margin-right: 12px;
  339. }
  340. .xy-zjz-filter-input{
  341. width: 120px;
  342. height: 32px;
  343. border: 1px solid #d9d9d9;
  344. border-radius: 4px;
  345. background: #fff;
  346. padding: 0 12px;
  347. font-size: 14px;
  348. color: #333;
  349. box-sizing: border-box;
  350. transition: border-color 0.2s;
  351. }
  352. .xy-zjz-filter-input:focus{
  353. outline: none;
  354. border-color: #409eff;
  355. }
  356. .xy-zjz-filter-input::placeholder{
  357. color: #999;
  358. }
  359. .xy-zjz-filter-select{
  360. display: none;
  361. }
  362. .xy-zjz-filter-summary{
  363. display: flex;
  364. justify-content: flex-end;
  365. align-items: center;
  366. padding: 0 2px 10px;
  367. }
  368. .xy-zjz-class-count{
  369. font-size: 16px;
  370. line-height: 24px;
  371. color: #5f6b80;
  372. }
  373. .xy-zjz-student-list{
  374. flex: 1;
  375. min-height: 0;
  376. overflow-y: auto;
  377. padding-top: 2px;
  378. border-top: 1px solid #e8e8e8;
  379. }
  380. .xy-zjz-student-row{
  381. display: grid;
  382. grid-template-columns: 32px 1fr 1.5fr;
  383. align-items: center;
  384. gap: 12px;
  385. height: 40px;
  386. padding: 0 8px;
  387. font-size: 14px;
  388. color: #333;
  389. border-radius: 4px;
  390. box-sizing: border-box;
  391. cursor: pointer;
  392. transition: background 0.15s;
  393. }
  394. .xy-zjz-student-row:hover{
  395. background: #f5f7fa;
  396. }
  397. .xy-zjz-student-row + .xy-zjz-student-row{
  398. margin-top: 2px;
  399. }
  400. .xy-zjz-student-row.is-active{
  401. background: #e8eaed;
  402. }
  403. .xy-zjz-student-row.is-active .xy-zjz-student-index{
  404. color: #666;
  405. }
  406. .xy-zjz-student-index{
  407. text-align: center;
  408. color: #999;
  409. font-size: 13px;
  410. }
  411. .xy-zjz-student-name{
  412. white-space: nowrap;
  413. overflow: hidden;
  414. text-overflow: ellipsis;
  415. font-weight: 500;
  416. color: #333;
  417. }
  418. .xy-zjz-student-id{
  419. white-space: nowrap;
  420. overflow: hidden;
  421. text-overflow: ellipsis;
  422. color: #666;
  423. font-size: 13px;
  424. }
  425. .xy-zjz-empty{
  426. padding: 32px 0;
  427. text-align: center;
  428. font-size: 14px;
  429. color: #999;
  430. }
  431. </style>
  432. </head>
  433. <body class="env-input-body">
  434. <form id="app" class="form-container" action="<ss:serv name='xyZjz_excelSureAdd' parm='{"wdConfirmationCaptchaService":"0"}' dest='info'/>" method="post">
  435. <div class="content-box fit-height-content">
  436. <div class="content-div xy-zjz-page" ssFith="true">
  437. <%
  438. List bjcyList = (List)request.getAttribute("bjcyList");
  439. int bjcyCount = bjcyList == null ? 0 : bjcyList.size();
  440. Object firstStudent = bjcyCount > 0 ? bjcyList.get(0) : null;
  441. pageContext.setAttribute("bjcyCount", Integer.valueOf(bjcyCount));
  442. pageContext.setAttribute("firstStudent", firstStudent);
  443. %>
  444. <script>
  445. // 功能说明:将 JSP 侧真实班级成员数据输出给 Vue 使用,替换前端 mock 数据 by xu 20260410
  446. window.xyZjzRealStudentList = [
  447. <%
  448. for (int i = 0; i < bjcyCount; i++) {
  449. Object rowObj = bjcyList.get(i);
  450. if (!(rowObj instanceof Map)) {
  451. continue;
  452. }
  453. Map row = (Map) rowObj;
  454. String ryid = row.get("ryid") == null ? "" : row.get("ryid").toString().replace("\\", "\\\\").replace("'", "\\'");
  455. String xm = row.get("xm") == null ? "" : row.get("xm").toString().replace("\\", "\\\\").replace("'", "\\'");
  456. String ryh = row.get("ryh") == null ? "" : row.get("ryh").toString().replace("\\", "\\\\").replace("'", "\\'");
  457. String zjzwj = row.get("zjzwj") == null ? "" : row.get("zjzwj").toString().replace("\\", "\\\\").replace("'", "\\'");
  458. %>
  459. { ryid: '<%= ryid %>', xm: '<%= xm %>', ryh: '<%= ryh %>', zjzwj: '<%= zjzwj %>' }<%= i < bjcyCount - 1 ? "," : "" %>
  460. <%
  461. }
  462. %>
  463. ];
  464. // 功能说明:同步输出班级名称给 Vue 显示 by xu 20260410
  465. window.xyZjzRealBjmc = "<ss:cbTrans cb='bj' val='${bjid}'/>";
  466. </script>
  467. <input name="ryid" value="${firstStudent.ryid}" type="hidden"/>
  468. <input name="zjzwj" value="${firstStudent.zjzwj}" type="hidden">
  469. <input name='wdComponentID' type='hidden' value='xyZjz_excelAdd'/>
  470. <input type="file" id="file">
  471. <select id="videoSource" style="display:none;"></select>
  472. <div class="xy-zjz-layout">
  473. <div class="xy-zjz-left">
  474. <div class="xy-zjz-camera-toolbar"></div>
  475. <div class="xy-zjz-preview-shell">
  476. <div id="sfzImg" class="photo xy-zjz-preview">
  477. <div class="xy-zjz-camera-placeholder" id="cameraPlaceholder">点击“连接摄像头”后由浏览器弹出设备/权限选择</div>
  478. <%-- 功能说明:叠加人脸蒙版图,辅助拍摄时对齐位置 by xu 20260410 --%>
  479. <img class="xy-zjz-face-mask" src="/skin/easy/image/xy-zjz-face-mask.png" alt="人脸定位蒙版"/>
  480. <%-- 功能说明:修复图片标签未闭合导致 video/canvas DOM 丢失,摄像头初始化取不到元素 by xu 20260410 --%>
  481. <img id="image"
  482. src="<ss:serv name='dlByHttp' parm='{"wdConfirmationCaptchaService":"0","path":"${firstStudent.zjzwj}","type":"img"}'/>"
  483. 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'"
  484. />
  485. <video autoplay muted playsinline id="video"></video>
  486. <canvas id="canvas"></canvas>
  487. </div>
  488. </div>
  489. <div class="xy-zjz-action-stack bottom-div">
  490. <div class="xy-zjz-action-bar" id="cameraConnectActions">
  491. <ss-bottom-button
  492. id="photoConnectBtn"
  493. text="连接摄像头"
  494. @click='handleConnectCamera()'
  495. icon-class="bottom-div-save"
  496. ></ss-bottom-button>
  497. </div>
  498. <div class="xy-zjz-action-bar" id="cameraCaptureActions" style="display:none;">
  499. <ss-bottom-button
  500. id="photoCaptureBtn"
  501. text="拍照"
  502. @click='handleCapturePhoto()'
  503. icon-class="bottom-div-save"
  504. ></ss-bottom-button>
  505. </div>
  506. <div class="xy-zjz-action-bar" id="cameraReviewActions" style="display:none;">
  507. <ss-bottom-button
  508. id="photoRetakeBtn"
  509. text="重拍"
  510. @click='handleRetakePhoto()'
  511. icon-class="bottom-div-save"
  512. ></ss-bottom-button>
  513. <ss-bottom-button
  514. id="photoSubmitBtn"
  515. text="保存并提交"
  516. @click='handleSubmitPhotoForm()'
  517. icon-class="bottom-div-save"
  518. ></ss-bottom-button>
  519. </div>
  520. </div>
  521. </div>
  522. <div class="xy-zjz-right">
  523. <div class="xy-zjz-info-wrapper">
  524. <div class="xy-zjz-card">
  525. <template v-if="currentStudent">
  526. <div class="xy-zjz-info-name">{{currentStudent.xm}}</div>
  527. <div class="xy-zjz-info-body">
  528. <div class="xy-zjz-info-avatar">
  529. <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'"/>
  530. </div>
  531. <div class="xy-zjz-info-lines">
  532. <div v-if="currentStudent.xbmc" class="xy-zjz-info-line"><strong>性别:</strong>{{currentStudent.xbmc}}</div>
  533. <div v-if="currentStudent.jyfsmc" class="xy-zjz-info-line"><strong>就读方式:</strong>{{currentStudent.jyfsmc}}</div>
  534. <div v-if="bjmc" class="xy-zjz-info-line"><strong>班级:</strong>{{bjmc}}</div>
  535. <div v-if="currentStudent.ryh" class="xy-zjz-info-line"><strong>学号:</strong>{{currentStudent.ryh}}</div>
  536. </div>
  537. </div>
  538. </template>
  539. <div v-else class="xy-zjz-info-empty">
  540. <div class="xy-zjz-info-name">--</div>
  541. </div>
  542. </div>
  543. </div>
  544. <div class="xy-zjz-right-divider"></div>
  545. <div class="xy-zjz-list-card" id="studentListCard">
  546. <div class="xy-zjz-filter-row">
  547. <select class="xy-zjz-filter-select">
  548. <option>年级</option>
  549. </select>
  550. <select class="xy-zjz-filter-select">
  551. <option>班级</option>
  552. </select>
  553. <span class="xy-zjz-filter-class"><ss:cbTrans cb='bj' val='${bjid}'/></span>
  554. <span class="xy-zjz-filter-progress">{{currentIndex + 1}}/{{bjcyCount}}</span>
  555. <input class="xy-zjz-filter-input" type="text" placeholder="姓名"/>
  556. </div>
  557. <div class="xy-zjz-student-list" id="studentListBody">
  558. <div v-for="(item, index) in studentList" :key="index"
  559. :class="['xy-zjz-student-row', currentIndex === index ? 'is-active' : '']"
  560. @click="selectStudent(index, item)">
  561. <div class="xy-zjz-student-index">{{index + 1}}</div>
  562. <div class="xy-zjz-student-name">{{item.xm}}</div>
  563. <div class="xy-zjz-student-id">{{item.ryh}}</div>
  564. </div>
  565. </div>
  566. </div>
  567. </div>
  568. </div>
  569. </div>
  570. </div>
  571. </form>
  572. <script type="text/javascript">
  573. (function(){
  574. var settings = {
  575. width: 1000,
  576. height: 1292,
  577. action: "/service?ssServ=ulByHttp&type=img"
  578. };
  579. var state = {
  580. stream: null,
  581. mode: "disconnected",
  582. enumerated: false,
  583. capturedBlob: null,
  584. capturedDataUrl: ""
  585. };
  586. var elements = {};
  587. function $(id){
  588. return document.getElementById(id);
  589. }
  590. function ensureElements(){
  591. elements.preview = $("sfzImg");
  592. elements.video = $("video");
  593. elements.image = $("image");
  594. elements.canvas = $("canvas");
  595. elements.file = $("file");
  596. elements.videoSource = $("videoSource");
  597. elements.placeholder = $("cameraPlaceholder");
  598. elements.faceMask = document.querySelector("#sfzImg .xy-zjz-face-mask");
  599. elements.connectActions = $("cameraConnectActions");
  600. elements.captureActions = $("cameraCaptureActions");
  601. elements.reviewActions = $("cameraReviewActions");
  602. return !!(elements.preview && elements.video && elements.image && elements.canvas && elements.file && elements.videoSource && elements.placeholder && elements.faceMask && elements.connectActions && elements.captureActions && elements.reviewActions);
  603. }
  604. function stopTracks(stream){
  605. if (!stream || !stream.getTracks) {
  606. return;
  607. }
  608. stream.getTracks().forEach(function(track){
  609. track.stop();
  610. });
  611. }
  612. function setPlaceholder(text){
  613. if (!ensureElements() || !elements.placeholder) {
  614. return;
  615. }
  616. elements.placeholder.textContent = text || "";
  617. elements.placeholder.style.display = "flex";
  618. }
  619. function hidePlaceholder(){
  620. if (!ensureElements()) {
  621. return;
  622. }
  623. elements.placeholder.style.display = "none";
  624. }
  625. function clearStream(){
  626. stopTracks(state.stream);
  627. state.stream = null;
  628. if (elements.video) {
  629. elements.video.pause();
  630. elements.video.srcObject = null;
  631. }
  632. }
  633. function renderCameraOptions(deviceInfos){
  634. if (!ensureElements() || !elements.videoSource) {
  635. return false;
  636. }
  637. while (elements.videoSource.options.length > 0) {
  638. elements.videoSource.remove(0);
  639. }
  640. var videoDevices = deviceInfos.filter(function(item){
  641. return item.kind === "videoinput";
  642. });
  643. if (!videoDevices.length) {
  644. var emptyOption = document.createElement("option");
  645. emptyOption.value = "";
  646. emptyOption.text = "未检测到摄像头";
  647. elements.videoSource.appendChild(emptyOption);
  648. elements.videoSource.disabled = true;
  649. return false;
  650. }
  651. elements.videoSource.disabled = false;
  652. videoDevices.forEach(function(device, index){
  653. var option = document.createElement("option");
  654. option.value = device.deviceId;
  655. option.text = device.label || ("摄像头" + (index + 1));
  656. elements.videoSource.appendChild(option);
  657. });
  658. if (elements.videoSource.options.length > 0) {
  659. elements.videoSource.selectedIndex = 0;
  660. }
  661. return true;
  662. }
  663. async function ensureCameraOptions(){
  664. if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
  665. alert("当前浏览器不支持摄像头访问");
  666. return false;
  667. }
  668. if (!ensureElements()) {
  669. return false;
  670. }
  671. if (state.enumerated && elements.videoSource.options.length > 0 && elements.videoSource.options[0].value !== "") {
  672. return true;
  673. }
  674. var permissionStream = null;
  675. try {
  676. permissionStream = await navigator.mediaDevices.getUserMedia({video: true, audio: false});
  677. var deviceInfos = await navigator.mediaDevices.enumerateDevices();
  678. state.enumerated = renderCameraOptions(deviceInfos);
  679. return state.enumerated;
  680. } catch (error) {
  681. handleCameraError(error);
  682. return false;
  683. } finally {
  684. stopTracks(permissionStream);
  685. }
  686. }
  687. async function startPreview(deviceId){
  688. if (!ensureElements()) {
  689. return false;
  690. }
  691. clearStream();
  692. var constraints = {
  693. video: deviceId ? {deviceId: {exact: deviceId}} : true,
  694. audio: false
  695. };
  696. try {
  697. var stream = await navigator.mediaDevices.getUserMedia(constraints);
  698. state.stream = stream;
  699. elements.video.srcObject = stream;
  700. await elements.video.play();
  701. elements.video.style.display = "block";
  702. elements.image.style.display = "none";
  703. hidePlaceholder();
  704. return true;
  705. } catch (error) {
  706. if (deviceId) {
  707. var fallbackStream = await navigator.mediaDevices.getUserMedia({video: true, audio: false});
  708. state.stream = fallbackStream;
  709. elements.video.srcObject = fallbackStream;
  710. await elements.video.play();
  711. elements.video.style.display = "block";
  712. elements.image.style.display = "none";
  713. hidePlaceholder();
  714. return true;
  715. }
  716. handleCameraError(error);
  717. return false;
  718. }
  719. }
  720. function setActionMode(mode){
  721. if (!ensureElements()) {
  722. return;
  723. }
  724. state.mode = mode;
  725. // 功能说明:连接按钮仅在首次未建立预览前显示,后续重拍不再回退到首次连接步骤 by xu 20260410
  726. var shouldShowConnect = mode === "disconnected" && !state.enumerated;
  727. elements.connectActions.style.display = shouldShowConnect ? "flex" : "none";
  728. elements.captureActions.style.display = mode === "live" ? "flex" : "none";
  729. elements.reviewActions.style.display = mode === "captured" ? "flex" : "none";
  730. if (mode === "live") {
  731. elements.preview.classList.add("is-live");
  732. elements.faceMask.style.display = "block";
  733. } else {
  734. elements.preview.classList.remove("is-live");
  735. elements.faceMask.style.display = "none";
  736. }
  737. if (mode === "disconnected") {
  738. clearStream();
  739. elements.video.style.display = "none";
  740. elements.image.style.display = "none";
  741. setPlaceholder("点击连接摄像头后由浏览器弹出设备/权限选择");
  742. }
  743. }
  744. function handleCameraError(error){
  745. console.error("Error: ", error);
  746. clearStream();
  747. if (elements.video) {
  748. elements.video.style.display = "none";
  749. }
  750. setPlaceholder("摄像头连接失败,请检查权限后重试");
  751. alert("摄像头连接失败,请检查设备权限后重试");
  752. }
  753. async function connectCamera(){
  754. if (!ensureElements()) {
  755. return false;
  756. }
  757. setPlaceholder("正在请求浏览器摄像头权限...");
  758. var canUseCamera = await ensureCameraOptions();
  759. if (!canUseCamera) {
  760. return false;
  761. }
  762. var connected = await startPreview(elements.videoSource.value);
  763. if (connected) {
  764. setActionMode("live");
  765. }
  766. return connected;
  767. }
  768. // 功能说明:拍照后仅截取当前画面做本地预览,不立即上传,等待保存并提交时再上传 by xu 20260410
  769. function capturePhoto(){
  770. if (!ensureElements()) {
  771. return;
  772. }
  773. if (!state.stream) {
  774. connectCamera();
  775. return;
  776. }
  777. var width = elements.video.videoWidth || elements.preview.clientWidth;
  778. var height = elements.video.videoHeight || elements.preview.clientHeight;
  779. elements.canvas.width = width;
  780. elements.canvas.height = height;
  781. var context = elements.canvas.getContext("2d");
  782. context.clearRect(0, 0, width, height);
  783. context.drawImage(elements.video, 0, 0, width, height);
  784. var imgdataBase64 = elements.canvas.toDataURL("image/jpeg", 0.92);
  785. state.capturedBlob = convertBase64UrlToBlob(imgdataBase64);
  786. state.capturedDataUrl = imgdataBase64;
  787. clearStream();
  788. elements.image.src = imgdataBase64;
  789. elements.image.style.display = "block";
  790. elements.video.style.display = "none";
  791. hidePlaceholder();
  792. document.querySelector("input[name='zjzwj']").value = "";
  793. setActionMode("captured");
  794. }
  795. // 功能说明:将拍照截图直接上传到图片服务,返回文件路径 by xu 20260410
  796. function uploadCapturedPhoto(blob){
  797. return new Promise(function(resolve, reject){
  798. var fileName = new Date().getTime() + ".jpg";
  799. var formData = new FormData();
  800. formData.append("application", "");
  801. formData.append("fileEdit", new File([blob], fileName, { type: "image/jpeg" }));
  802. $.ajax({
  803. url: settings.action,
  804. type: "POST",
  805. data: formData,
  806. processData: false,
  807. contentType: false,
  808. success: function(result){
  809. try {
  810. if (typeof result === "string") {
  811. result = eval("(" + result + ")");
  812. }
  813. var path = result && result.fileList && result.fileList[0] && result.fileList[0].path;
  814. if (path) {
  815. resolve(path);
  816. } else {
  817. reject(new Error("上传返回路径为空"));
  818. }
  819. } catch (error) {
  820. reject(error);
  821. }
  822. },
  823. error: function(xhr){
  824. reject(xhr);
  825. }
  826. });
  827. });
  828. }
  829. function cropConfirm(result){
  830. if (!ensureElements()) {
  831. return;
  832. }
  833. clearStream();
  834. var path = result.fileList[0].path;
  835. var url = "/service?ssServ=dlByHttp&type=img&path=" + path;
  836. elements.image.src = url;
  837. elements.image.style.display = "block";
  838. elements.video.style.display = "none";
  839. hidePlaceholder();
  840. document.querySelector("input[name='zjzwj']").value = path;
  841. setActionMode("captured");
  842. }
  843. // 功能说明:重拍时直接回到预览态,不再回到首次连接步骤 by xu 20260410
  844. async function retakePhoto(){
  845. if (!ensureElements()) {
  846. return;
  847. }
  848. state.capturedBlob = null;
  849. state.capturedDataUrl = "";
  850. document.querySelector("input[name='zjzwj']").value = "";
  851. elements.image.style.display = "none";
  852. var connected = await startPreview(elements.videoSource.value);
  853. if (connected) {
  854. setActionMode("live");
  855. }
  856. }
  857. // 功能说明:保存并提交时若存在本地截图则先上传,再回填路径并提交表单 by xu 20260410
  858. async function submitPhotoForm(){
  859. var formEl = document.getElementById("app") || document.getElementById("xyZjzPhotoForm");
  860. if (!formEl) {
  861. alert("表单不存在");
  862. return false;
  863. }
  864. var zjzwjInput = document.querySelector("input[name='zjzwj']");
  865. if (!zjzwjInput) {
  866. alert("缺少照片字段");
  867. return false;
  868. }
  869. if (!zjzwjInput.value && state.capturedBlob) {
  870. try {
  871. var uploadedPath = await uploadCapturedPhoto(state.capturedBlob);
  872. zjzwjInput.value = uploadedPath;
  873. state.capturedBlob = null;
  874. } catch (error) {
  875. console.error(error);
  876. alert("拍照上传失败,请重试");
  877. return false;
  878. }
  879. }
  880. if (!zjzwjInput.value) {
  881. alert("请先拍照");
  882. return false;
  883. }
  884. formEl.submit();
  885. return true;
  886. }
  887. // 功能说明:支持 Enter 快捷键,未连接时连摄像头、预览时截图、已截图时直接保存并提交 by xu 20260410
  888. function handleEnterShortcut(event){
  889. var target = event.target || {};
  890. var tagName = (target.tagName || "").toLowerCase();
  891. if (tagName === "input" || tagName === "textarea" || tagName === "select" || target.isContentEditable) {
  892. return;
  893. }
  894. if (event.key !== "Enter") {
  895. return;
  896. }
  897. event.preventDefault();
  898. if (state.mode === "disconnected") {
  899. connectCamera();
  900. return;
  901. }
  902. if (state.mode === "live") {
  903. capturePhoto();
  904. return;
  905. }
  906. if (state.mode === "captured") {
  907. submitPhotoForm();
  908. }
  909. }
  910. function convertBase64UrlToBlob(urlData) {
  911. var bytes = window.atob(urlData.split(",")[1]);
  912. var ab = new ArrayBuffer(bytes.length);
  913. var ia = new Uint8Array(ab);
  914. for (var i = 0; i < bytes.length; i++) {
  915. ia[i] = bytes.charCodeAt(i);
  916. }
  917. return new Blob([ab], {
  918. type: "image/jpeg"
  919. });
  920. }
  921. window.xyZjzCameraPage = {
  922. connectCamera: connectCamera,
  923. capturePhoto: capturePhoto,
  924. retakePhoto: retakePhoto,
  925. submitPhotoForm: submitPhotoForm
  926. };
  927. function initPage(){
  928. if (!ensureElements()) {
  929. return;
  930. }
  931. setActionMode("disconnected");
  932. // 功能说明:初始化时绑定 Enter 快捷键,仅绑定一次 by xu 20260410
  933. if (!window.__xyZjzEnterShortcutBound) {
  934. document.addEventListener("keydown", handleEnterShortcut);
  935. window.__xyZjzEnterShortcutBound = true;
  936. }
  937. syncStudentListHeight();
  938. }
  939. // 功能说明:按右侧真实可用高度动态限制学生列表区域,避免长名单把卡片继续撑出 by xu 20260410
  940. function syncStudentListHeight(){
  941. var rightPanel = document.querySelector(".xy-zjz-right");
  942. var listCard = document.getElementById("studentListCard");
  943. var filterRow = document.querySelector(".xy-zjz-filter-row");
  944. var studentList = document.getElementById("studentListBody");
  945. if (!rightPanel || !listCard || !studentList) {
  946. return;
  947. }
  948. var rightRect = rightPanel.getBoundingClientRect();
  949. var listCardRect = listCard.getBoundingClientRect();
  950. var filterRect = filterRow ? filterRow.getBoundingClientRect() : {height: 0};
  951. var listCardStyle = window.getComputedStyle(listCard);
  952. var extraHeight = parseFloat(listCardStyle.paddingTop || 0)
  953. + parseFloat(listCardStyle.paddingBottom || 0)
  954. + filterRect.height
  955. + 12;
  956. var availableHeight = Math.floor(rightRect.bottom - listCardRect.top);
  957. if (availableHeight > 0) {
  958. listCard.style.height = availableHeight + "px";
  959. listCard.style.maxHeight = availableHeight + "px";
  960. studentList.style.maxHeight = Math.max(80, Math.floor(availableHeight - extraHeight)) + "px";
  961. }
  962. }
  963. window.addEventListener("resize", syncStudentListHeight);
  964. window.addEventListener("load", syncStudentListHeight);
  965. setTimeout(syncStudentListHeight, 0);
  966. setTimeout(syncStudentListHeight, 300);
  967. if (document.readyState === "loading") {
  968. document.addEventListener("DOMContentLoaded", initPage);
  969. } else {
  970. initPage();
  971. }
  972. })();
  973. var file = {};
  974. function initCropper1(fileEle, settings, cropConfirm, fileDate) {
  975. file["name"] = new Date().getTime() + ".png";
  976. initWdCropper();
  977. wdCropper.topWin = wd.topWindow;
  978. wdCropper.thisWindow = window;
  979. wdCropper.settings = settings;
  980. wdCropper.fileEle = fileEle;
  981. wdCropper.cropConfirm = cropConfirm;
  982. wdCropper.cropper.fileName = file.name;
  983. wdCropper.fileObj = fileDate;
  984. var uploadedImageURL = URL.createObjectURL(fileDate);
  985. var img = new Image();
  986. img.onload = function() {
  987. wdCropper.layer.show({
  988. width: img.width,
  989. height: img.height
  990. });
  991. wdCropper.cropper.init();
  992. wdCropper.cropper.replace(uploadedImageURL);
  993. };
  994. img.src = uploadedImageURL;
  995. }
  996. </script>
  997. <script type="text/javascript">var wdRecordValue='${wdRecordValue}';</script>
  998. <script type="text/javascript" src="/ss/js/wdRecord.js"></script>
  999. <script type="text/javascript">(function(){wdRecord("xyZjz_excelAdd");})();</script>
  1000. <script type="text/javascript" src="/ss/js/wdFitHeight.js"></script>
  1001. <script type="text/javascript">initWdFitHeight(0)</script>
  1002. <script type="text/javascript">initWdFitHeightFunction=function(){initWdFitHeight(0);};</script>
  1003. <ss:equal val="${empty resizeComponent}" val2="false">
  1004. <script>{var iframe=wd.display.getFrameOfWindow();
  1005. if(iframe&&iframe.contentWindow==window)
  1006. wd.display.resizeComponent(${resizeComponent.width}, ${resizeComponent.height}, ${empty resizeComponent.minHeight?'null':resizeComponent.minHeight}, ${empty resizeComponent.maxHeight?'null':resizeComponent.maxHeight});}</script>
  1007. </ss:equal>
  1008. <ss:help/>
  1009. </body>
  1010. <script type="text/javascript">
  1011. try{wd.display.showMsgPopup('${msg}');
  1012. }catch(err){console.error(err);}
  1013. </script>
  1014. <ss:equal val="${empty wdclosewindowparam}" val2="false">
  1015. <script type="text/javascript">
  1016. try{wd.display.setCloseWindowParam('${wdclosewindowparam}');
  1017. }catch(err){console.error(err);}
  1018. </script>
  1019. </ss:equal>
  1020. </html>
  1021. <%@ include file="/page/clip/footer.jsp" %>
  1022. <script>
  1023. </script>
  1024. <script>
  1025. // 功能说明:Vue 初始化改为读取 JSP 注入的真实数据,移除前端 mock 数据 by xu 20260410
  1026. (function() {
  1027. var studentList = Array.isArray(window.xyZjzRealStudentList) ? window.xyZjzRealStudentList : [];
  1028. var firstStudent = studentList.length ? studentList[0] : null;
  1029. 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";
  1030. function setupHijack() {
  1031. if (!window.SS || !window.SS.dom || !window.SS.dom.initializeFormApp) {
  1032. setTimeout(setupHijack, 50);
  1033. return;
  1034. }
  1035. var originalInit = window.SS.dom.initializeFormApp;
  1036. window.SS.dom.initializeFormApp = function(options) {
  1037. if (!options.data) {
  1038. options.data = function() { return {}; };
  1039. }
  1040. var originalDataFn = options.data;
  1041. options.data = function() {
  1042. var originalData = originalDataFn.call(this);
  1043. return Object.assign({}, originalData, {
  1044. currentStudent: firstStudent ? Object.assign({}, firstStudent) : null,
  1045. bjmc: window.xyZjzRealBjmc || '',
  1046. bjcyCount: studentList.length,
  1047. currentIndex: 0,
  1048. studentList: studentList,
  1049. stream: null,
  1050. mode: 'disconnected'
  1051. });
  1052. };
  1053. if (!options.methods) {
  1054. options.methods = {};
  1055. }
  1056. // 功能说明:为 Vue 模板提供拍照按钮桥接方法,统一通过全局拍照对象调用 by xu 20260410
  1057. options.methods.handleConnectCamera = function() {
  1058. return window.xyZjzCameraPage && window.xyZjzCameraPage.connectCamera ? window.xyZjzCameraPage.connectCamera() : false;
  1059. };
  1060. // 功能说明:为 Vue 模板提供拍照按钮桥接方法,统一通过全局拍照对象调用 by xu 20260410
  1061. options.methods.handleCapturePhoto = function() {
  1062. return window.xyZjzCameraPage && window.xyZjzCameraPage.capturePhoto ? window.xyZjzCameraPage.capturePhoto() : false;
  1063. };
  1064. // 功能说明:为 Vue 模板提供拍照按钮桥接方法,统一通过全局拍照对象调用 by xu 20260410
  1065. options.methods.handleRetakePhoto = function() {
  1066. return window.xyZjzCameraPage && window.xyZjzCameraPage.retakePhoto ? window.xyZjzCameraPage.retakePhoto() : false;
  1067. };
  1068. // 功能说明:为 Vue 模板提供拍照按钮桥接方法,统一通过全局拍照对象调用 by xu 20260410
  1069. options.methods.handleSubmitPhotoForm = function() {
  1070. return window.xyZjzCameraPage && window.xyZjzCameraPage.submitPhotoForm ? window.xyZjzCameraPage.submitPhotoForm() : false;
  1071. };
  1072. options.methods.selectStudent = function(index, student) {
  1073. this.currentIndex = index;
  1074. this.currentStudent = Object.assign({}, student);
  1075. var ryidInput = document.querySelector("input[name='ryid']");
  1076. var zjzwjInput = document.querySelector("input[name='zjzwj']");
  1077. if (ryidInput) ryidInput.value = student.ryid || '';
  1078. if (zjzwjInput) zjzwjInput.value = student.zjzwj || '';
  1079. var img = document.getElementById("image");
  1080. if (img && student.zjzwj) {
  1081. img.src = "/service?ssServ=dlByHttp&type=img&path=" + student.zjzwj;
  1082. } else if (img) {
  1083. img.src = emptyImage;
  1084. }
  1085. setTimeout(syncStudentListHeight, 0);
  1086. };
  1087. return originalInit.call(this, options);
  1088. };
  1089. }
  1090. if (typeof SS !== 'undefined' && SS.ready) {
  1091. SS.ready(setupHijack);
  1092. } else {
  1093. setTimeout(setupHijack, 100);
  1094. }
  1095. })();
  1096. </script>