xyZjz_excelAdd.jsp 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907
  1. <%@ page import="java.util.List" %>
  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. body{
  25. background: #f5f7fb;
  26. }
  27. #app.form-container{
  28. height: 100%;
  29. }
  30. .form-container .content-box{
  31. height: 100% !important;
  32. padding: 0 !important;
  33. }
  34. .xy-zjz-page{
  35. height: 100%;
  36. padding: 0;
  37. box-sizing: border-box;
  38. }
  39. .xy-zjz-layout{
  40. height: 100%;
  41. display: flex;
  42. gap: 0;
  43. }
  44. .xy-zjz-left{
  45. flex: 0 0 61%;
  46. min-width: 0;
  47. display: flex;
  48. flex-direction: column;
  49. align-items: center;
  50. background: #ffffff;
  51. border-radius: 4px;
  52. padding: 30px 46px;
  53. box-sizing: border-box;
  54. border-right: 1px solid #e2e4ec;
  55. }
  56. .xy-zjz-right{
  57. flex: 0 0 39%;
  58. min-width: 320px;
  59. display: flex;
  60. flex-direction: column;
  61. background: #f7f7f7;
  62. }
  63. .xy-zjz-info-wrapper{
  64. height: 234px;
  65. padding: 30px 35px 35px;
  66. box-sizing: border-box;
  67. }
  68. .xy-zjz-camera-toolbar{
  69. display: none;
  70. }
  71. .xy-zjz-preview-shell{
  72. width: 100%;
  73. flex: 1;
  74. display: flex;
  75. align-items: center;
  76. justify-content: center;
  77. }
  78. #sfzImg.xy-zjz-preview{
  79. position: relative;
  80. width: min(100%, 498px);
  81. height: auto;
  82. aspect-ratio: 498/756;
  83. max-height: 756px;
  84. background: #8e8e8e;
  85. overflow: hidden;
  86. box-sizing: border-box;
  87. border: none;
  88. }
  89. #sfzImg.xy-zjz-preview.is-live{
  90. border: 1px solid #3f3f3f;
  91. }
  92. #sfzImg.xy-zjz-preview img,
  93. #sfzImg.xy-zjz-preview video{
  94. position: absolute;
  95. inset: 0;
  96. width: 100% !important;
  97. height: 100% !important;
  98. object-fit: cover;
  99. display: none;
  100. }
  101. #canvas,
  102. #file{
  103. display: none;
  104. }
  105. .xy-zjz-camera-placeholder{
  106. position: absolute;
  107. inset: 0;
  108. display: flex;
  109. align-items: center;
  110. justify-content: center;
  111. padding: 0 32px;
  112. text-align: center;
  113. font-size: 18px;
  114. line-height: 28px;
  115. color: rgba(255,255,255,0.88);
  116. background: linear-gradient(180deg, rgba(0,0,0,0.08), rgba(0,0,0,0.16));
  117. }
  118. .xy-zjz-camera-placeholder::before{
  119. content: "";
  120. position: absolute;
  121. width: 70%;
  122. height: 70%;
  123. 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;
  124. opacity: 0.8;
  125. }
  126. .xy-zjz-action-stack{
  127. width: min(100%, 360px);
  128. padding-top: 20px;
  129. }
  130. .xy-zjz-action-stack.bottom-div{
  131. position: static;
  132. display: block;
  133. height: auto;
  134. padding: 20px 0 0;
  135. border-top: none;
  136. background: transparent !important;
  137. width: min(100%, 360px);
  138. }
  139. .xy-zjz-action-stack.bottom-div ss-bottom-button{
  140. display: flex;
  141. align-items: center;
  142. justify-content: center;
  143. gap: 8px;
  144. font-size: 16px;
  145. min-width: 140px;
  146. padding: 10px 24px;
  147. border-radius: 4px;
  148. border: 1px solid #d9d9d9;
  149. background: #fff;
  150. color: #666;
  151. cursor: pointer;
  152. transition: all 0.2s;
  153. }
  154. .xy-zjz-action-stack.bottom-div ss-bottom-button:hover{
  155. border-color: #409eff;
  156. color: #409eff;
  157. }
  158. .xy-zjz-action-stack.bottom-div ss-bottom-button::before{
  159. content: "📷";
  160. font-size: 18px;
  161. }
  162. .xy-zjz-action-bar{
  163. display: flex;
  164. justify-content: center;
  165. gap: 12px;
  166. width: 100%;
  167. }
  168. .xy-zjz-action-bar ss-bottom-button,
  169. .xy-zjz-action-bar #photoConnectBtn,
  170. .xy-zjz-action-bar #photoCaptureBtn,
  171. .xy-zjz-action-bar #photoRetakeBtn,
  172. .xy-zjz-action-bar #photoSubmitBtn{
  173. min-width: 140px;
  174. }
  175. .xy-zjz-card,
  176. .xy-zjz-list-card{
  177. background: #fff;
  178. border: 1px solid #e3e7f0;
  179. border-radius: 4px;
  180. box-sizing: border-box;
  181. }
  182. .xy-zjz-card{
  183. width: 100%;
  184. height: 100%;
  185. box-sizing: border-box;
  186. background: #ffffff;
  187. border: 1px solid #dddfe6;
  188. border-radius: 4px;
  189. padding: 18px 20px;
  190. }
  191. .xy-zjz-info-name{
  192. font-size: 20px;
  193. line-height: 1;
  194. font-weight: 500;
  195. color: #010101;
  196. margin-bottom: 18px;
  197. }
  198. .xy-zjz-info-body{
  199. display: flex;
  200. gap: 0;
  201. align-items: flex-end;
  202. }
  203. .xy-zjz-info-avatar{
  204. width: 68px;
  205. height: 100px;
  206. border: 1px solid #dddfe6;
  207. background: #f2f3f4;
  208. overflow: hidden;
  209. flex: 0 0 68px;
  210. border-radius: 4px;
  211. }
  212. .xy-zjz-info-avatar.has-image{
  213. border: none;
  214. }
  215. .xy-zjz-info-avatar img{
  216. width: 100%;
  217. height: 100%;
  218. object-fit: cover;
  219. display: block;
  220. }
  221. .xy-zjz-info-lines{
  222. flex: 1;
  223. padding-left: 20px;
  224. min-width: 0;
  225. display: flex;
  226. flex-direction: column;
  227. justify-content: flex-end;
  228. }
  229. .xy-zjz-info-line{
  230. font-size: 18px;
  231. line-height: 28px;
  232. color: #666;
  233. white-space: nowrap;
  234. overflow: hidden;
  235. text-overflow: ellipsis;
  236. line-height: 1.4;
  237. }
  238. .xy-zjz-info-line:last-child{
  239. color: #000;
  240. }
  241. .xy-zjz-info-line strong{
  242. font-weight: normal;
  243. color: inherit;
  244. }
  245. .xy-zjz-right-divider{
  246. width: 100%;
  247. height: 1px;
  248. background: #e2e4ec;
  249. flex-shrink: 0;
  250. }
  251. .xy-zjz-list-card{
  252. flex: 1;
  253. padding: 16px 14px 12px;
  254. display: flex;
  255. flex-direction: column;
  256. min-height: 0;
  257. max-height: none;
  258. background: #f7f7f7;
  259. }
  260. .xy-zjz-filter-row{
  261. display: flex;
  262. gap: 10px;
  263. margin-bottom: 10px;
  264. align-items: center;
  265. }
  266. .xy-zjz-filter-class{
  267. font-size: 16px;
  268. font-weight: 500;
  269. color: #333;
  270. margin-right: auto;
  271. }
  272. .xy-zjz-filter-progress{
  273. font-size: 14px;
  274. color: #666;
  275. margin-right: 12px;
  276. }
  277. .xy-zjz-filter-input{
  278. width: 120px;
  279. height: 32px;
  280. border: 1px solid #d9d9d9;
  281. border-radius: 4px;
  282. background: #fff;
  283. padding: 0 12px;
  284. font-size: 14px;
  285. color: #333;
  286. box-sizing: border-box;
  287. transition: border-color 0.2s;
  288. }
  289. .xy-zjz-filter-input:focus{
  290. outline: none;
  291. border-color: #409eff;
  292. }
  293. .xy-zjz-filter-input::placeholder{
  294. color: #999;
  295. }
  296. .xy-zjz-filter-select{
  297. display: none;
  298. }
  299. .xy-zjz-filter-summary{
  300. display: flex;
  301. justify-content: flex-end;
  302. align-items: center;
  303. padding: 0 2px 10px;
  304. }
  305. .xy-zjz-class-count{
  306. font-size: 16px;
  307. line-height: 24px;
  308. color: #5f6b80;
  309. }
  310. .xy-zjz-student-list{
  311. flex: 1;
  312. min-height: 0;
  313. overflow-y: auto;
  314. padding-top: 2px;
  315. border-top: 1px solid #e8e8e8;
  316. }
  317. .xy-zjz-student-row{
  318. display: grid;
  319. grid-template-columns: 32px 1fr 1.5fr;
  320. align-items: center;
  321. gap: 12px;
  322. height: 40px;
  323. padding: 0 8px;
  324. font-size: 14px;
  325. color: #333;
  326. border-radius: 4px;
  327. box-sizing: border-box;
  328. cursor: pointer;
  329. transition: background 0.15s;
  330. }
  331. .xy-zjz-student-row:hover{
  332. background: #f5f7fa;
  333. }
  334. .xy-zjz-student-row + .xy-zjz-student-row{
  335. margin-top: 2px;
  336. }
  337. .xy-zjz-student-row.is-active{
  338. background: #e8eaed;
  339. }
  340. .xy-zjz-student-row.is-active .xy-zjz-student-index{
  341. color: #666;
  342. }
  343. .xy-zjz-student-index{
  344. text-align: center;
  345. color: #999;
  346. font-size: 13px;
  347. }
  348. .xy-zjz-student-name{
  349. white-space: nowrap;
  350. overflow: hidden;
  351. text-overflow: ellipsis;
  352. font-weight: 500;
  353. color: #333;
  354. }
  355. .xy-zjz-student-id{
  356. white-space: nowrap;
  357. overflow: hidden;
  358. text-overflow: ellipsis;
  359. color: #666;
  360. font-size: 13px;
  361. }
  362. .xy-zjz-empty{
  363. padding: 32px 0;
  364. text-align: center;
  365. font-size: 14px;
  366. color: #999;
  367. }
  368. </style>
  369. </head>
  370. <body class="env-input-body">
  371. <form id="app" class="form-container" action="<ss:serv name='xyZjz_excelSureAdd' parm='{"wdConfirmationCaptchaService":"0"}' dest='info'/>" method="post">
  372. <div class="content-box fit-height-content">
  373. <div class="content-div xy-zjz-page" ssFith="true">
  374. <%
  375. List bjcyList = (List)request.getAttribute("bjcyList");
  376. int bjcyCount = bjcyList == null ? 0 : bjcyList.size();
  377. Object firstStudent = bjcyCount > 0 ? bjcyList.get(0) : null;
  378. pageContext.setAttribute("bjcyCount", Integer.valueOf(bjcyCount));
  379. pageContext.setAttribute("firstStudent", firstStudent);
  380. %>
  381. <input name="ryid" value="${firstStudent.ryid}" type="hidden"/>
  382. <input name="zjzwj" value="${firstStudent.zjzwj}" type="hidden">
  383. <input name='wdComponentID' type='hidden' value='xyZjz_excelAdd'/>
  384. <input type="file" id="file">
  385. <select id="videoSource" style="display:none;"></select>
  386. <div class="xy-zjz-layout">
  387. <div class="xy-zjz-left">
  388. <div class="xy-zjz-camera-toolbar"></div>
  389. <div class="xy-zjz-preview-shell">
  390. <div id="sfzImg" class="photo xy-zjz-preview">
  391. <div class="xy-zjz-camera-placeholder" id="cameraPlaceholder">点击“连接摄像头”后由浏览器弹出设备/权限选择</div>
  392. <img id="image"
  393. src="<ss:serv name='dlByHttp' parm='{"wdConfirmationCaptchaService":"0","path":"${firstStudent.zpwj}","type":"img"}'/>"
  394. 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'"
  395. <video autoplay muted playsinline id="video"></video>
  396. <canvas id="canvas"></canvas>
  397. </div>
  398. </div>
  399. <div class="xy-zjz-action-stack bottom-div">
  400. <div class="xy-zjz-action-bar" id="cameraConnectActions">
  401. <ss-bottom-button
  402. id="photoConnectBtn"
  403. text="连接摄像头"
  404. @click='xyZjzCameraPage.connectCamera()'
  405. icon-class="bottom-div-save"
  406. ></ss-bottom-button>
  407. </div>
  408. <div class="xy-zjz-action-bar" id="cameraCaptureActions" style="display:none;">
  409. <ss-bottom-button
  410. id="photoCaptureBtn"
  411. text="拍照"
  412. @click='xyZjzCameraPage.capturePhoto()'
  413. icon-class="bottom-div-save"
  414. ></ss-bottom-button>
  415. </div>
  416. <div class="xy-zjz-action-bar" id="cameraReviewActions" style="display:none;">
  417. <ss-bottom-button
  418. id="photoRetakeBtn"
  419. text="重拍"
  420. @click='xyZjzCameraPage.retakePhoto()'
  421. icon-class="bottom-div-close"
  422. ></ss-bottom-button>
  423. <ss-bottom-button
  424. id="photoSubmitBtn"
  425. text="保存并提交"
  426. @click='xyZjzCameraPage.submitPhotoForm()'
  427. icon-class="bottom-div-save"
  428. ></ss-bottom-button>
  429. </div>
  430. </div>
  431. </div>
  432. <div class="xy-zjz-right">
  433. <div class="xy-zjz-info-wrapper">
  434. <div class="xy-zjz-card">
  435. <template v-if="currentStudent">
  436. <div class="xy-zjz-info-name">{{currentStudent.xm}}</div>
  437. <div class="xy-zjz-info-body">
  438. <div class="xy-zjz-info-avatar">
  439. <img :src="currentStudent.zpwj ? '/service?ssServ=dlByHttp&type=img&path=' + currentStudent.zpwj : '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'"/>
  440. </div>
  441. <div class="xy-zjz-info-lines">
  442. <div v-if="currentStudent.xbmc" class="xy-zjz-info-line"><strong>性别:</strong>{{currentStudent.xbmc}}</div>
  443. <div v-if="currentStudent.jyfsmc" class="xy-zjz-info-line"><strong>就读方式:</strong>{{currentStudent.jyfsmc}}</div>
  444. <div v-if="bjmc" class="xy-zjz-info-line"><strong>班级:</strong>{{bjmc}}</div>
  445. <div v-if="currentStudent.ryh" class="xy-zjz-info-line"><strong>学号:</strong>{{currentStudent.ryh}}</div>
  446. </div>
  447. </div>
  448. </template>
  449. <div v-else class="xy-zjz-info-empty">
  450. <div class="xy-zjz-info-name">--</div>
  451. </div>
  452. </div>
  453. </div>
  454. <div class="xy-zjz-right-divider"></div>
  455. <div class="xy-zjz-list-card">
  456. <div class="xy-zjz-filter-row">
  457. <select class="xy-zjz-filter-select">
  458. <option>年级</option>
  459. </select>
  460. <select class="xy-zjz-filter-select">
  461. <option>班级</option>
  462. </select>
  463. <span class="xy-zjz-filter-class"><ss:cbTrans cb='bj' val='${bjid}'/></span>
  464. <span class="xy-zjz-filter-progress">{{currentIndex + 1}}/{{bjcyCount}}</span>
  465. <input class="xy-zjz-filter-input" type="text" placeholder="姓名"/>
  466. </div>
  467. <div class="xy-zjz-student-list">
  468. <div v-for="(item, index) in studentList" :key="index"
  469. :class="['xy-zjz-student-row', currentIndex === index ? 'is-active' : '']"
  470. @click="selectStudent(index, item)">
  471. <div class="xy-zjz-student-index">{{index + 1}}</div>
  472. <div class="xy-zjz-student-name">{{item.xm}}</div>
  473. <div class="xy-zjz-student-id">{{item.sfzh}}</div>
  474. </div>
  475. </div>
  476. </div>
  477. </div>
  478. </div>
  479. </div>
  480. </div>
  481. </form>
  482. <script type="text/javascript">
  483. (function(){
  484. var settings = {
  485. width: 1000,
  486. height: 1292,
  487. action: "/service?ssServ=ulByHttp&type=img"
  488. };
  489. var state = {
  490. stream: null,
  491. mode: "disconnected",
  492. enumerated: false
  493. };
  494. var elements = {};
  495. function $(id){
  496. return document.getElementById(id);
  497. }
  498. function ensureElements(){
  499. elements.preview = $("sfzImg");
  500. elements.video = $("video");
  501. elements.image = $("image");
  502. elements.canvas = $("canvas");
  503. elements.file = $("file");
  504. elements.videoSource = $("videoSource");
  505. elements.placeholder = $("cameraPlaceholder");
  506. elements.connectActions = $("cameraConnectActions");
  507. elements.captureActions = $("cameraCaptureActions");
  508. elements.reviewActions = $("cameraReviewActions");
  509. return !!(elements.preview && elements.video && elements.image && elements.canvas && elements.file && elements.videoSource && elements.placeholder && elements.connectActions && elements.captureActions && elements.reviewActions);
  510. }
  511. function stopTracks(stream){
  512. if (!stream || !stream.getTracks) {
  513. return;
  514. }
  515. stream.getTracks().forEach(function(track){
  516. track.stop();
  517. });
  518. }
  519. function setPlaceholder(text){
  520. if (!ensureElements() || !elements.placeholder) {
  521. return;
  522. }
  523. elements.placeholder.textContent = text || "";
  524. elements.placeholder.style.display = "flex";
  525. }
  526. function hidePlaceholder(){
  527. if (!ensureElements()) {
  528. return;
  529. }
  530. elements.placeholder.style.display = "none";
  531. }
  532. function clearStream(){
  533. stopTracks(state.stream);
  534. state.stream = null;
  535. if (elements.video) {
  536. elements.video.pause();
  537. elements.video.srcObject = null;
  538. }
  539. }
  540. function renderCameraOptions(deviceInfos){
  541. if (!ensureElements() || !elements.videoSource) {
  542. return false;
  543. }
  544. while (elements.videoSource.options.length > 0) {
  545. elements.videoSource.remove(0);
  546. }
  547. var videoDevices = deviceInfos.filter(function(item){
  548. return item.kind === "videoinput";
  549. });
  550. if (!videoDevices.length) {
  551. var emptyOption = document.createElement("option");
  552. emptyOption.value = "";
  553. emptyOption.text = "未检测到摄像头";
  554. elements.videoSource.appendChild(emptyOption);
  555. elements.videoSource.disabled = true;
  556. return false;
  557. }
  558. elements.videoSource.disabled = false;
  559. videoDevices.forEach(function(device, index){
  560. var option = document.createElement("option");
  561. option.value = device.deviceId;
  562. option.text = device.label || ("摄像头" + (index + 1));
  563. elements.videoSource.appendChild(option);
  564. });
  565. if (elements.videoSource.options.length > 0) {
  566. elements.videoSource.selectedIndex = 0;
  567. }
  568. return true;
  569. }
  570. async function ensureCameraOptions(){
  571. if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
  572. alert("当前浏览器不支持摄像头访问");
  573. return false;
  574. }
  575. if (!ensureElements()) {
  576. return false;
  577. }
  578. if (state.enumerated && elements.videoSource.options.length > 0 && elements.videoSource.options[0].value !== "") {
  579. return true;
  580. }
  581. var permissionStream = null;
  582. try {
  583. permissionStream = await navigator.mediaDevices.getUserMedia({video: true, audio: false});
  584. var deviceInfos = await navigator.mediaDevices.enumerateDevices();
  585. state.enumerated = renderCameraOptions(deviceInfos);
  586. return state.enumerated;
  587. } catch (error) {
  588. handleCameraError(error);
  589. return false;
  590. } finally {
  591. stopTracks(permissionStream);
  592. }
  593. }
  594. async function startPreview(deviceId){
  595. if (!ensureElements()) {
  596. return false;
  597. }
  598. clearStream();
  599. var constraints = {
  600. video: deviceId ? {deviceId: {exact: deviceId}} : true,
  601. audio: false
  602. };
  603. try {
  604. var stream = await navigator.mediaDevices.getUserMedia(constraints);
  605. state.stream = stream;
  606. elements.video.srcObject = stream;
  607. await elements.video.play();
  608. elements.video.style.display = "block";
  609. elements.image.style.display = "none";
  610. hidePlaceholder();
  611. return true;
  612. } catch (error) {
  613. if (deviceId) {
  614. var fallbackStream = await navigator.mediaDevices.getUserMedia({video: true, audio: false});
  615. state.stream = fallbackStream;
  616. elements.video.srcObject = fallbackStream;
  617. await elements.video.play();
  618. elements.video.style.display = "block";
  619. elements.image.style.display = "none";
  620. hidePlaceholder();
  621. return true;
  622. }
  623. handleCameraError(error);
  624. return false;
  625. }
  626. }
  627. function setActionMode(mode){
  628. if (!ensureElements()) {
  629. return;
  630. }
  631. state.mode = mode;
  632. elements.connectActions.style.display = mode === "disconnected" ? "flex" : "none";
  633. elements.captureActions.style.display = mode === "live" ? "flex" : "none";
  634. elements.reviewActions.style.display = mode === "captured" ? "flex" : "none";
  635. if (mode === "live") {
  636. elements.preview.classList.add("is-live");
  637. } else {
  638. elements.preview.classList.remove("is-live");
  639. }
  640. if (mode === "disconnected") {
  641. clearStream();
  642. elements.video.style.display = "none";
  643. elements.image.style.display = "none";
  644. setPlaceholder("点击连接摄像头后由浏览器弹出设备/权限选择");
  645. }
  646. }
  647. function handleCameraError(error){
  648. console.error("Error: ", error);
  649. clearStream();
  650. if (elements.video) {
  651. elements.video.style.display = "none";
  652. }
  653. setPlaceholder("摄像头连接失败,请检查权限后重试");
  654. alert("摄像头连接失败,请检查设备权限后重试");
  655. }
  656. async function connectCamera(){
  657. if (!ensureElements()) {
  658. return false;
  659. }
  660. setPlaceholder("正在请求浏览器摄像头权限...");
  661. var canUseCamera = await ensureCameraOptions();
  662. if (!canUseCamera) {
  663. return false;
  664. }
  665. var connected = await startPreview(elements.videoSource.value);
  666. if (connected) {
  667. setActionMode("live");
  668. }
  669. return connected;
  670. }
  671. function capturePhoto(){
  672. if (!ensureElements()) {
  673. return;
  674. }
  675. if (!state.stream) {
  676. connectCamera();
  677. return;
  678. }
  679. var width = elements.video.videoWidth || elements.preview.clientWidth;
  680. var height = elements.video.videoHeight || elements.preview.clientHeight;
  681. elements.canvas.width = width;
  682. elements.canvas.height = height;
  683. var context = elements.canvas.getContext("2d");
  684. context.clearRect(0, 0, width, height);
  685. context.drawImage(elements.video, 0, 0, width, height);
  686. var imgdataBase64 = elements.canvas.toDataURL("image/png");
  687. initCropper1(elements.file, settings, cropConfirm, convertBase64UrlToBlob(imgdataBase64));
  688. }
  689. function cropConfirm(result){
  690. if (!ensureElements()) {
  691. return;
  692. }
  693. clearStream();
  694. var path = result.fileList[0].path;
  695. var url = "/service?ssServ=dlByHttp&type=img&path=" + path;
  696. elements.image.src = url;
  697. elements.image.style.display = "block";
  698. elements.video.style.display = "none";
  699. hidePlaceholder();
  700. document.querySelector("input[name='zjzwj']").value = path;
  701. setActionMode("captured");
  702. }
  703. function retakePhoto(){
  704. if (!ensureElements()) {
  705. return;
  706. }
  707. elements.image.style.display = "none";
  708. connectCamera();
  709. }
  710. function submitPhotoForm(){
  711. var formEl = document.getElementById("app") || document.getElementById("xyZjzPhotoForm");
  712. if (!formEl) {
  713. alert("表单不存在");
  714. return false;
  715. }
  716. if (!document.querySelector("input[name='zjzwj']").value) {
  717. alert("请先拍照");
  718. return false;
  719. }
  720. formEl.submit();
  721. return true;
  722. }
  723. function convertBase64UrlToBlob(urlData) {
  724. var bytes = window.atob(urlData.split(",")[1]);
  725. var ab = new ArrayBuffer(bytes.length);
  726. var ia = new Uint8Array(ab);
  727. for (var i = 0; i < bytes.length; i++) {
  728. ia[i] = bytes.charCodeAt(i);
  729. }
  730. return new Blob([ab], {
  731. type: "image/jpeg"
  732. });
  733. }
  734. window.xyZjzCameraPage = {
  735. connectCamera: connectCamera,
  736. capturePhoto: capturePhoto,
  737. retakePhoto: retakePhoto,
  738. submitPhotoForm: submitPhotoForm
  739. };
  740. function initPage(){
  741. if (!ensureElements()) {
  742. return;
  743. }
  744. setActionMode("disconnected");
  745. }
  746. if (document.readyState === "loading") {
  747. document.addEventListener("DOMContentLoaded", initPage);
  748. } else {
  749. initPage();
  750. }
  751. })();
  752. var file = {};
  753. function initCropper1(fileEle, settings, cropConfirm, fileDate) {
  754. file["name"] = new Date().getTime() + ".png";
  755. initWdCropper();
  756. wdCropper.topWin = wd.topWindow;
  757. wdCropper.thisWindow = window;
  758. wdCropper.settings = settings;
  759. wdCropper.fileEle = fileEle;
  760. wdCropper.cropConfirm = cropConfirm;
  761. wdCropper.cropper.fileName = file.name;
  762. wdCropper.fileObj = fileDate;
  763. var uploadedImageURL = URL.createObjectURL(fileDate);
  764. var img = new Image();
  765. img.onload = function() {
  766. wdCropper.layer.show({
  767. width: img.width,
  768. height: img.height
  769. });
  770. wdCropper.cropper.init();
  771. wdCropper.cropper.replace(uploadedImageURL);
  772. };
  773. img.src = uploadedImageURL;
  774. }
  775. </script>
  776. <script type="text/javascript">var wdRecordValue='${wdRecordValue}';</script>
  777. <script type="text/javascript" src="/ss/js/wdRecord.js"></script>
  778. <script type="text/javascript">(function(){wdRecord("xyZjz_excelAdd");})();</script>
  779. <script type="text/javascript" src="/ss/js/wdFitHeight.js"></script>
  780. <script type="text/javascript">initWdFitHeight(0)</script>
  781. <script type="text/javascript">initWdFitHeightFunction=function(){initWdFitHeight(0);};</script>
  782. <ss:equal val="${empty resizeComponent}" val2="false">
  783. <script>{var iframe=wd.display.getFrameOfWindow();
  784. if(iframe&&iframe.contentWindow==window)
  785. wd.display.resizeComponent(${resizeComponent.width}, ${resizeComponent.height}, ${empty resizeComponent.minHeight?'null':resizeComponent.minHeight}, ${empty resizeComponent.maxHeight?'null':resizeComponent.maxHeight});}</script>
  786. </ss:equal>
  787. <ss:help/>
  788. </body>
  789. <script type="text/javascript">
  790. try{wd.display.showMsgPopup('${msg}');
  791. }catch(err){console.error(err);}
  792. </script>
  793. <ss:equal val="${empty wdclosewindowparam}" val2="false">
  794. <script type="text/javascript">
  795. try{wd.display.setCloseWindowParam('${wdclosewindowparam}');
  796. }catch(err){console.error(err);}
  797. </script>
  798. </ss:equal>
  799. </html>
  800. <%@ include file="/page/clip/footer.jsp" %>
  801. <script>
  802. </script>
  803. <script>
  804. // 写死测试数据,先调UI
  805. // 策略:劫持 SS.dom.initializeFormApp,在创建 Vue 时自动注入数据
  806. (function() {
  807. const studentList = [
  808. {xm: '张三', xbmc: '男', jyfsmc: '走读', ryh: '2024001', zpwj: '', zjzwj: '', ryid: '1001', sfzh: '110101200001011234'},
  809. {xm: '李四', xbmc: '女', jyfsmc: '寄宿', ryh: '2024002', zpwj: '', zjzwj: '', ryid: '1002', sfzh: '110101200002021245'},
  810. {xm: '王五', xbmc: '男', jyfsmc: '走读', ryh: '2024003', zpwj: '', zjzwj: '', ryid: '1003', sfzh: '110101200003031256'},
  811. {xm: '赵六', xbmc: '女', jyfsmc: '寄宿', ryh: '2024004', zpwj: '', zjzwj: '', ryid: '1004', sfzh: '110101200004041267'},
  812. {xm: '钱七', xbmc: '男', jyfsmc: '走读', ryh: '2024005', zpwj: '', zjzwj: '', ryid: '1005', sfzh: '110101200005051278'},
  813. ];
  814. const firstStudent = studentList[0];
  815. // 等待 SS.ready 后劫持 initializeFormApp
  816. function setupHijack() {
  817. if (!window.SS || !window.SS.dom || !window.SS.dom.initializeFormApp) {
  818. setTimeout(setupHijack, 50);
  819. return;
  820. }
  821. const originalInit = window.SS.dom.initializeFormApp;
  822. window.SS.dom.initializeFormApp = function(options) {
  823. // 合并数据到 options
  824. if (!options.data) {
  825. options.data = function() { return {}; };
  826. }
  827. const originalDataFn = options.data;
  828. options.data = function() {
  829. const originalData = originalDataFn.call(this);
  830. return {
  831. ...originalData,
  832. currentStudent: { ...firstStudent },
  833. bjmc: '一年级1班',
  834. bjcyCount: studentList.length,
  835. currentIndex: 0,
  836. studentList: studentList,
  837. stream: null,
  838. mode: 'disconnected'
  839. };
  840. };
  841. // 合并方法
  842. if (!options.methods) {
  843. options.methods = {};
  844. }
  845. options.methods.selectStudent = function(index, student) {
  846. this.currentIndex = index;
  847. this.currentStudent = { ...student };
  848. const ryidInput = document.querySelector("input[name='ryid']");
  849. const zjzwjInput = document.querySelector("input[name='zjzwj']");
  850. if (ryidInput) ryidInput.value = student.ryid || '';
  851. if (zjzwjInput) zjzwjInput.value = student.zjzwj || '';
  852. const img = document.getElementById("image");
  853. if (img && student.zpwj) {
  854. img.src = "/service?ssServ=dlByHttp&type=img&path=" + student.zpwj;
  855. } else if (img) {
  856. img.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";
  857. }
  858. };
  859. console.log('[Vue] 数据已合并到初始化选项');
  860. return originalInit.call(this, options);
  861. };
  862. console.log('[Vue] initializeFormApp 已劫持');
  863. }
  864. // 如果 SS 还没准备好,等它
  865. if (typeof SS !== 'undefined' && SS.ready) {
  866. SS.ready(setupHijack);
  867. } else {
  868. setTimeout(setupHijack, 100);
  869. }
  870. })();
  871. </script>