Login.vue 36 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214
  1. <template>
  2. <div class="home">
  3. <header class="header">
  4. <div class="school-logo">
  5. <img
  6. v-show="logoPath"
  7. class="logo-size"
  8. :src="logoPath"
  9. alt="school logo"
  10. style="
  11. background: linear-gradient(to bottom, #38f6f5 0%, #8efdf4 100%);
  12. "
  13. @load="(e) => (e.target.style = '')"
  14. />
  15. </div>
  16. <a
  17. class="close"
  18. style="border-bottom-left-radius: 6px;"
  19. @click="closeApp"
  20. >
  21. 关闭
  22. </a>
  23. </header>
  24. <div class="center">
  25. <div class="content">
  26. <div style="display: flex;">
  27. <a
  28. v-if="allowLoginType.includes('STUDENT_CODE')"
  29. key="STUDENT_CODE"
  30. :class="[
  31. 'qm-big-text',
  32. 'login-type',
  33. loginType === 'STUDENT_CODE' && 'active-type',
  34. allowLoginType.length === 1 && 'single-login-type',
  35. ]"
  36. style="border-top-left-radius: 6px;"
  37. @click="loginType = 'STUDENT_CODE'"
  38. >
  39. {{ QECSConfig.STUDENT_CODE_LOGIN_ALIAS }}
  40. </a>
  41. <a
  42. v-if="allowLoginType.includes('IDENTITY_NUMBER')"
  43. key="IDENTITY_NUMBER"
  44. :class="[
  45. 'qm-big-text',
  46. 'login-type',
  47. loginType !== 'STUDENT_CODE' && 'active-type',
  48. allowLoginType.length === 1 && 'single-login-type',
  49. ]"
  50. style="border-top-right-radius: 6px;"
  51. @click="loginType = 'STUDENT_IDENTITY_NUMBER'"
  52. >
  53. {{ QECSConfig.IDENTITY_NUMBER_LOGIN_ALIAS }}
  54. </a>
  55. <a
  56. v-if="allowLoginType.length === 0"
  57. key="loading"
  58. :class="['qm-big-text', 'login-type']"
  59. >
  60. loading...
  61. </a>
  62. </div>
  63. <div class="qm-title-text" style="margin: 40px 0 20px 0;">
  64. {{ productName }}
  65. </div>
  66. <div style="margin: 0 40px 40px 40px;">
  67. <i-form ref="loginForm" :model="loginForm" :rules="loginFormRule">
  68. <i-form-item
  69. prop="accountValue"
  70. style="margin-bottom: 30px; height: 42px;"
  71. >
  72. <i-input
  73. v-model="loginForm.accountValue"
  74. type="text"
  75. size="large"
  76. @on-blur="loginForm.accountValue && (initVerifyCode = true)"
  77. >
  78. <i-icon slot="prepend" type="ios-person"></i-icon>
  79. </i-input>
  80. </i-form-item>
  81. <i-form-item
  82. prop="password"
  83. style="margin-bottom: 30px; height: 42px;"
  84. >
  85. <i-input
  86. v-model="loginForm.password"
  87. type="password"
  88. size="large"
  89. @on-enter="loginForuser"
  90. @on-blur="loginForm.accountValue && (initVerifyCode = true)"
  91. >
  92. <i-icon slot="prepend" type="ios-lock"></i-icon>
  93. </i-input>
  94. </i-form-item>
  95. <i-form-item
  96. prop="verifyCode"
  97. style="margin-bottom: 30px; height: 42px;"
  98. >
  99. <VerifyCode
  100. :account-value="loginForm.accountValue"
  101. :init="initVerifyCode"
  102. @calcVerify="calcVerify"
  103. />
  104. </i-form-item>
  105. <i-form-item style="position: relative;">
  106. <div
  107. v-if="errorInfo !== ''"
  108. style="position: absolute; top: -37px; width: 100%;"
  109. >
  110. <i-alert type="error" show-icon>{{ errorInfo }}</i-alert>
  111. </div>
  112. <i-button
  113. size="large"
  114. class="qm-primary-button"
  115. style="margin-top: 10px;"
  116. long
  117. :disabled="disableLoginBtn"
  118. :loading="loginBtnLoading"
  119. @click="loginForuser"
  120. >
  121. {{ newVersionAvailable ? "点击更新版本" : "登录" }}
  122. </i-button>
  123. </i-form-item>
  124. </i-form>
  125. </div>
  126. </div>
  127. </div>
  128. <footer class="footer">
  129. <div style="position: absolute; right: 20px; bottom: 20px;">
  130. 版本: {{ VUE_APP_GIT_REPO_VERSION }}
  131. </div>
  132. <DevTools />
  133. </footer>
  134. </div>
  135. </template>
  136. <script>
  137. import moment from "moment";
  138. import { mapMutations } from "vuex";
  139. import { FACE_API_MODEL_PATH } from "@/constants/constants";
  140. import DevTools from "./DevTools.vue";
  141. import nativeExe, { fileExists } from "@/utils/nativeExe";
  142. import {
  143. EPCC_DOMAIN,
  144. VUE_APP_CONFIG_FILE_SEVER_URL,
  145. } from "@/constants/constants";
  146. import UA, { chromeUA } from "@/utils/ua.js";
  147. import {
  148. createLog,
  149. createUserDetailLog,
  150. createEncryptLog,
  151. } from "@/utils/logger";
  152. import VerifyCode from "./VerifyCode.vue";
  153. // 检测devtools. 仅在chrome 72+ 有效。
  154. let element = new Image();
  155. Object.defineProperty(element, "id", {
  156. get: function () {
  157. // console.log("trigger devtools log");
  158. window._hmt.push([
  159. "_trackEvent",
  160. "控制台打开",
  161. window.location.href,
  162. "|||" +
  163. localStorage.getItem("domain") +
  164. "|||" +
  165. localStorage.getItem("key"),
  166. ]);
  167. return null;
  168. },
  169. });
  170. /**
  171. * 在任何组件需要强制退出,做以下步骤
  172. * 1. this.$Message.info()
  173. * 2. this.$router.push("/login"+domain);
  174. * 因为在/login里会删除localStorage的token,而在router.beforeEach会检查是否有token,达到退出的目的。
  175. */
  176. export default {
  177. name: "Login",
  178. components: {
  179. DevTools,
  180. VerifyCode,
  181. },
  182. data() {
  183. return {
  184. domainInUrl:
  185. this.$route.params.domain ||
  186. window.location.hostname.split(".")[0] + ".ecs.qmth.com.cn",
  187. QECSConfig: {},
  188. loginType: "STUDENT_CODE",
  189. errorInfo: "",
  190. loginForm: {
  191. accountValue: "",
  192. password: "",
  193. verifyCode: "",
  194. tid: "",
  195. // result: "",
  196. },
  197. loginFormRule: {
  198. accountValue: [
  199. {
  200. required: true,
  201. message: "请填写登录账号",
  202. trigger: "blur",
  203. },
  204. ],
  205. password: [
  206. {
  207. required: true,
  208. message: "请填写密码",
  209. trigger: "blur",
  210. },
  211. ],
  212. verifyCode: [
  213. {
  214. required: true,
  215. message: "请填写计算结果",
  216. trigger: "blur",
  217. type: "number",
  218. },
  219. {
  220. type: "number",
  221. asyncValidator: (rule, value) => {
  222. return new Promise((resolve, reject) => {
  223. if (value < -999 || value > 999) {
  224. reject("验证码长度不对"); // reject with error message
  225. } else {
  226. resolve();
  227. }
  228. });
  229. },
  230. message: "验证码长度不对",
  231. trigger: "blur",
  232. },
  233. ],
  234. },
  235. loginBtnLoading: false,
  236. isElectron: true,
  237. disableLoginBtnBecauseRemoteApp: true,
  238. disableLoginBtnBecauseVCam: true,
  239. disableLoginBtnBecauseAppVersionChecker: false,
  240. disableLoginBtnBecauseRefreshServiceWorker: false,
  241. disableLoginBtnBecauseNotAllowedNative: true,
  242. newVersionAvailable: false,
  243. VUE_APP_GIT_REPO_VERSION: process.env.VUE_APP_GIT_REPO_VERSION,
  244. initVerifyCode: false,
  245. };
  246. },
  247. computed: {
  248. logoPath() {
  249. // "!/progressive/true/format/webp"
  250. return this.QECSConfig.LOGO_FILE_URL ? this.QECSConfig.LOGO_FILE_URL : "";
  251. },
  252. productName() {
  253. return this.QECSConfig.OE_STUDENT_SYS_NAME || "远程教育网络考试";
  254. },
  255. allowLoginType() {
  256. return (
  257. (this.QECSConfig.LOGIN_TYPE && this.QECSConfig.LOGIN_TYPE.split(",")) ||
  258. []
  259. );
  260. },
  261. schoolDomain() {
  262. const domain = this.domainInUrl;
  263. if (!domain || !domain.includes("qmth.com.cn")) {
  264. this.$Message.error({
  265. content: "机构地址出错,请关闭程序后再登录。",
  266. duration: 15,
  267. closable: true,
  268. });
  269. }
  270. return domain;
  271. },
  272. isEPCC() {
  273. return this.schoolDomain === EPCC_DOMAIN;
  274. },
  275. usernameInputPlaceholder() {
  276. if (this.loginType === "STUDENT_CODE") {
  277. return "请输入学号";
  278. } else {
  279. return "请输入身份证号";
  280. }
  281. },
  282. passwordInputPlaceholder() {
  283. if (this.loginType === "STUDENT_CODE") {
  284. return "初始密码为身份证号后6位";
  285. } else {
  286. return "初始密码为身份证号后6位";
  287. }
  288. },
  289. disableLoginBtn() {
  290. // console.log(
  291. // this.isElectron,
  292. // this.disableLoginBtnBecauseRemoteApp,
  293. // this.disableLoginBtnBecauseVCam,
  294. // this.disableLoginBtnBecauseAppVersionChecker
  295. // );
  296. return (
  297. (this.isElectron &&
  298. (this.disableLoginBtnBecauseRemoteApp ||
  299. this.disableLoginBtnBecauseVCam)) ||
  300. this.disableLoginBtnBecauseAppVersionChecker ||
  301. this.disableLoginBtnBecauseRefreshServiceWorker ||
  302. this.disableLoginBtnBecauseNotAllowedNative
  303. );
  304. },
  305. },
  306. async mounted() {
  307. // this.testServiceWorker();
  308. this.examShellStats();
  309. // await this.checkNewVersion();
  310. if (localStorage.getItem("__swReload")) {
  311. localStorage.removeItem("__swReload");
  312. this.$Message.info({
  313. content: "正在更新版本...",
  314. });
  315. this.disableLoginBtnBecauseRefreshServiceWorker = true;
  316. await new Promise((resolve) => {
  317. setTimeout(() => {
  318. resolve();
  319. }, 2000);
  320. });
  321. location.reload(true);
  322. }
  323. // manual precache for models
  324. fetch(
  325. FACE_API_MODEL_PATH + "tiny_face_detector_model-weights_manifest.json"
  326. );
  327. fetch(FACE_API_MODEL_PATH + "face_landmark_68_model-weights_manifest.json");
  328. fetch(FACE_API_MODEL_PATH + "face_expression_model-weights_manifest.json");
  329. // alread precached
  330. // fetch("/models/tiny_face_detector_model-shard1");
  331. // fetch("/models/face_landmark_68_model-shard1");
  332. // this.checkNewVersionInterval = setInterval(() => {
  333. // if (window.__newSWAvailable) {
  334. // this.newVersionAvailable = true;
  335. // }
  336. // }, 1000);
  337. this.checkNewVersionListener = document.addEventListener(
  338. "__newSWAvailable",
  339. () => {
  340. this.newVersionAvailable = true;
  341. },
  342. { once: true }
  343. );
  344. if (this.isEPCC) {
  345. const redirectUrl = new URLSearchParams(location.search).get(
  346. "redirectUrl"
  347. );
  348. if (redirectUrl) {
  349. sessionStorage.setItem("redirectUrl", redirectUrl);
  350. }
  351. this.login();
  352. }
  353. // 上传本机加密日志
  354. this.uploadLogInterval = setInterval(() => createEncryptLog(), 5 * 1000);
  355. },
  356. async created() {
  357. createLog({
  358. currentPage: "登录页面",
  359. action: "page created",
  360. UA: navigator.userAgent,
  361. });
  362. // 测试log顺序
  363. // createLog({
  364. // currentPage: "登录页面2",
  365. // action: "page created",
  366. // UA: navigator.userAgent,
  367. // });
  368. if (navigator.connection) {
  369. console.log("UA: ", navigator.userAgent);
  370. console.log(
  371. "_trackEvent",
  372. "登录页面",
  373. `当前网速: ${Number(navigator.connection.downlink).toPrecision(
  374. 1
  375. )}Mib; 网络延迟: ${Number(navigator.connection.rtt).toPrecision(1)}ms`
  376. );
  377. window._hmt.push([
  378. "_trackEvent",
  379. "登录页面",
  380. `当前网速: ${Number(navigator.connection.downlink).toPrecision(
  381. 1
  382. )}Mib; 网络延迟: ${Number(navigator.connection.rtt).toPrecision(1)}ms`,
  383. ]);
  384. }
  385. if (this.isEPCC) {
  386. this.$Spin.show({
  387. render: () => {
  388. return <div style="font-size: 44px">正在登录...</div>;
  389. },
  390. });
  391. }
  392. this.isElectron = typeof nodeRequire != "undefined";
  393. if (
  394. navigator.userAgent.indexOf("WOW64") != -1 ||
  395. navigator.userAgent.indexOf("Win64") != -1
  396. ) {
  397. window._hmt.push(["_trackEvent", "登录页面", "64Bit OS"]);
  398. } else {
  399. window._hmt.push(["_trackEvent", "登录页面", "非64Bit OS"]);
  400. }
  401. this.$Message.config({
  402. duration: 15,
  403. size: "large",
  404. closable: true, // 没有影响到所有的组件。。。 https://github.com/iview/iview/issues/2962
  405. });
  406. window.sessionStorage.removeItem("token");
  407. window.sessionStorage.clear();
  408. window.localStorage.removeItem("key");
  409. if (localStorage.getItem("user-for-reload")) {
  410. const lsUser = JSON.parse(localStorage.getItem("user-for-reload"));
  411. this.loginForm.accountValue =
  412. process.env.NODE_ENV === "production"
  413. ? ""
  414. : lsUser.studentCode ||
  415. (lsUser.studentCodeList && lsUser.studentCodeList[0]);
  416. this.loginForm.password =
  417. process.env.NODE_ENV === "production" ? "" : "180613";
  418. }
  419. if (
  420. [
  421. "xjtu.ecs.qmth.com.cn",
  422. "ccnu.ecs.qmth.com.cn",
  423. "snnu.ecs.qmth.com.cn",
  424. "swjtu.ecs.qmth.com.cn",
  425. ].includes(this.$route.params.domain)
  426. ) {
  427. if (
  428. typeof nodeRequire == "undefined" ||
  429. !window.nodeRequire("fs").existsSync("multiCamera.exe")
  430. ) {
  431. this.disableLoginBtnBecauseAppVersionChecker = true;
  432. this.$Message.error({
  433. content: "请与学校申请最新的客户端,进行考试!",
  434. duration: 2 * 24 * 60 * 60,
  435. });
  436. }
  437. }
  438. if (["cup.ecs.qmth.com.cn"].includes(this.$route.params.domain)) {
  439. // console.log(UA.getBrowser(), chromeUA);
  440. if (
  441. UA.getBrowser().name !== "electron-exam-shell" ||
  442. (UA.getBrowser().major !== "2" && UA.getBrowser().major !== "1") ||
  443. chromeUA.major < "58"
  444. ) {
  445. this.disableLoginBtnBecauseAppVersionChecker = true;
  446. this.$Message.error({
  447. content: "请与学校申请最新的客户端,进行考试!",
  448. duration: 2 * 24 * 60 * 60,
  449. });
  450. }
  451. }
  452. // if (
  453. // ["cugr.ecs.qmth.com.cn", "sdu.ecs.qmth.com.cn"].includes(
  454. // this.$route.params.domain
  455. // )
  456. // ) {
  457. // // console.log(UA.getBrowser(), chromeUA);
  458. // if (
  459. // UA.getBrowser().name !== "electron-exam-shell" ||
  460. // UA.getBrowser().major !== "2" ||
  461. // chromeUA.major < "76"
  462. // ) {
  463. // this.disableLoginBtnBecauseAppVersionChecker = true;
  464. // this.$Message.error({
  465. // content: "请与学校申请最新的客户端,进行考试!",
  466. // duration: 2 * 24 * 60 * 60,
  467. // });
  468. // }
  469. // }
  470. await this.checkElectronConfig();
  471. await this.checkVCam();
  472. await this.checkAllowedClient();
  473. if (
  474. this.allowLoginType.length === 1 &&
  475. this.allowLoginType[0] === "IDENTITY_NUMBER"
  476. ) {
  477. this.loginType = "STUDENT_IDENTITY_NUMBER";
  478. }
  479. },
  480. beforeDestroy() {
  481. clearTimeout(this.loginTimeout);
  482. clearInterval(this.uploadLogInterval);
  483. // clearInterval(this.checkNewVersionInterval);
  484. document.removeEventListener(
  485. "__newSWAvailable",
  486. this.checkNewVersionListener
  487. );
  488. },
  489. methods: {
  490. ...mapMutations(["updateUser", "updateTimeDifference", "updateQECSConfig"]),
  491. testServiceWorker() {
  492. if ("serviceWorker" in navigator) {
  493. // Register service worker
  494. navigator.serviceWorker
  495. .register("/service-worker-test.js")
  496. .then(function (reg) {
  497. console.log("Registration OK!. Scope is " + reg.scope);
  498. })
  499. .catch(function (err) {
  500. console.error("Registration FAILED! " + err);
  501. });
  502. }
  503. },
  504. async loginForuser() {
  505. // 供user点击的 login 方法。主要是保护 login 方法,不因为user重复点击,多个请求不按预期时间进行。
  506. createLog({ currentPage: "登录页面", action: "点击登录按钮" });
  507. if (this.loginBtnLoading) {
  508. return;
  509. }
  510. this.loginBtnLoading = true;
  511. const before = Date.now();
  512. try {
  513. await this.login();
  514. } finally {
  515. const end = Date.now();
  516. this.loginTimeout = setTimeout(() => {
  517. this.loginBtnLoading = false;
  518. }, 10 * 1000 - (end - before));
  519. }
  520. },
  521. async login() {
  522. // 测试 createLog 在网络堵塞的情况下,是否会堵塞页面
  523. // alert("haha");
  524. createLog({
  525. currentPage: "登录页面--login clicked",
  526. action: "page created",
  527. UA: navigator.userAgent,
  528. });
  529. // alert("haha end");
  530. this.errorInfo = "";
  531. // epcc立即登录
  532. if (!this.isEPCC && this.disableLoginBtn) {
  533. return;
  534. }
  535. try {
  536. const hasNewVersion = await this.checkNewVersion();
  537. if (hasNewVersion) return;
  538. } catch (error) {
  539. console.log("检测新版本出错");
  540. }
  541. let loginResponse;
  542. if (!this.isEPCC) {
  543. // https://www.cnblogs.com/weiqinl/p/6708993.html
  544. const valid = await this.$refs.loginForm.validate();
  545. if (!valid) {
  546. return;
  547. }
  548. let repPara = this.loginForm;
  549. // 以下网络请求失败,直接报网络异常错误
  550. loginResponse = await this.$http.post(
  551. "/api/ecs_core/verifyCode/login",
  552. {
  553. ...repPara,
  554. accountType: this.loginType,
  555. domain: this.schoolDomain,
  556. rootOrgId: this.QECSConfig.ROOT_ORG_ID,
  557. alwaysOK: true,
  558. }
  559. );
  560. } else {
  561. loginResponse = await this.epccLogin();
  562. if (!loginResponse) {
  563. return;
  564. } else {
  565. loginResponse.data = { content: loginResponse.data, code: "200" }; // 和老接口的always ok保持一致
  566. }
  567. }
  568. let data = loginResponse.data;
  569. // if (
  570. // Math.abs() >
  571. // 5 * 60 * 1000
  572. // ) {
  573. // window._hmt.push(["_trackEvent", "登录页面", "本机时间误差过大"]);
  574. // this.$Message.error({
  575. // content: "与服务器时间差异超过5分钟,请校准本机时间之后再重试!",
  576. // duration: 30
  577. // });
  578. // throw new Error("与服务器时间差异超过5分钟,请校准本机时间之后再重试!");
  579. // }
  580. this.updateTimeDifference(
  581. moment(loginResponse.headers.date).diff(moment())
  582. );
  583. this.initVerifyCode = false;
  584. this.$nextTick(() => (this.initVerifyCode = true));
  585. if (data.code == "200") {
  586. data = data.content;
  587. this.errorInfo = "";
  588. //缓存用户信息
  589. window.sessionStorage.setItem("token", data.token);
  590. window.localStorage.setItem("key", data.key);
  591. window.localStorage.setItem("domain", this.schoolDomain);
  592. try {
  593. const student = (
  594. await this.$http.get(
  595. "/api/ecs_core/student/getStudentInfoBySession"
  596. )
  597. ).data;
  598. const specialty = (
  599. await this.$http.get(
  600. "/api/ecs_exam_work/exam_student/specialtyNameList/"
  601. )
  602. ).data;
  603. const user = {
  604. ...data,
  605. ...student,
  606. specialty: specialty.join(),
  607. schoolDomain: this.schoolDomain,
  608. };
  609. this.updateUser(user);
  610. window.localStorage.setItem("user-for-reload", JSON.stringify(user));
  611. window._hmt.push([
  612. "_trackEvent",
  613. "登录页面",
  614. "登录",
  615. this.$route.query.LogoutReason,
  616. ]);
  617. const alreadyInExam = await this.checkExamInProgress();
  618. window._hmt.push(["_trackEvent", "登录页面", "登录成功"]);
  619. createUserDetailLog({ action: "登录成功" });
  620. if (alreadyInExam) {
  621. this.logger({ action: "断点续考", detail: "登录页面" });
  622. window._hmt.push([
  623. "_trackEvent",
  624. "登录页面",
  625. "断点续考",
  626. "重新登录",
  627. ]);
  628. return;
  629. }
  630. this.$router.push("/online-exam");
  631. let userIds = JSON.parse(localStorage.getItem("userIds"));
  632. userIds = [...new Set(userIds).add(user.id)];
  633. localStorage.setItem("userIds", JSON.stringify(userIds));
  634. // 学习中心机器
  635. // (JSON.parse(localStorage.getItem("userIds")) || []).length > 5;
  636. this.$Spin.hide();
  637. } catch (error) {
  638. window._hmt.push([
  639. "_trackEvent",
  640. "登录页面",
  641. "登录失败",
  642. "getStudentInfoBySession失败",
  643. ]);
  644. this.logger({ action: "登录失败" });
  645. this.$Message.error({
  646. content: "获取学生信息失败,请重试!",
  647. duration: 15,
  648. closable: true,
  649. });
  650. if (this.isEPCC) {
  651. this.$Spin.hide();
  652. this.logout();
  653. }
  654. }
  655. } else {
  656. window._hmt.push(["_trackEvent", "登录页面", "登录失败", data.desc]);
  657. createLog({ currentPage: "登录页面", action: "登录失败" });
  658. this.errorInfo = data.desc;
  659. }
  660. },
  661. async checkNewVersion() {
  662. let myHeaders = new Headers();
  663. myHeaders.append("Content-Type", "application/javascript");
  664. myHeaders.append("Cache-Control", "no-cache");
  665. const response = await fetch(
  666. document.scripts[document.scripts.length - 1].src + "?x" + Date.now(),
  667. {
  668. method: process.env.NODE_ENV === "development" ? "GET" : "HEAD",
  669. headers: myHeaders,
  670. }
  671. );
  672. // 给后台更多时间去处理 resource/uuid.js 的请求
  673. await new Promise((resolve) => setTimeout(() => resolve(), 1500));
  674. if (!response.ok || this.newVersionAvailable) {
  675. if (
  676. response.ok &&
  677. this.newVersionAvailable &&
  678. localStorage.getItem("__swReload")
  679. ) {
  680. window._hmt.push([
  681. "_trackEvent",
  682. "登录页面",
  683. "service worker刷新失败",
  684. ]);
  685. this.$Message.destroy();
  686. this.$Message.info({
  687. content: "请重新打开程序。",
  688. duration: 2 * 24 * 60 * 60,
  689. });
  690. return true;
  691. }
  692. window._hmt.push([
  693. "_trackEvent",
  694. "登录页面",
  695. "新版本发布后,客户端自动刷新",
  696. ]);
  697. this.$Message.info({
  698. content: "正在获取新版本...",
  699. });
  700. localStorage.setItem("__swReload", "anything");
  701. this.disableLoginBtnBecauseRefreshServiceWorker = true;
  702. await new Promise((resolve) => {
  703. setTimeout(() => {
  704. resolve();
  705. }, 1000);
  706. });
  707. location.reload(true);
  708. return true;
  709. }
  710. },
  711. async checkElectronConfig() {
  712. try {
  713. const fileSever = VUE_APP_CONFIG_FILE_SEVER_URL;
  714. const res = await fetch(
  715. fileSever +
  716. "/org_properties/byOrgDomain/" +
  717. this.domainInUrl +
  718. "/studentClientConfig.json" +
  719. "?" +
  720. Date.now() +
  721. Math.random()
  722. );
  723. if (res.ok) {
  724. const json = await res.json();
  725. this.QECSConfig = json;
  726. this.updateQECSConfig(json);
  727. } else {
  728. this.$Message.error({
  729. content: "获取远程配置文件出错,请重新打开程序!",
  730. duration: 15,
  731. closable: true,
  732. });
  733. return;
  734. }
  735. } catch (e) {
  736. this.$Message.error({
  737. content: "获取远程配置文件出错,请重新打开程序!",
  738. duration: 15,
  739. closable: true,
  740. });
  741. return;
  742. }
  743. await this.checkRemoteApp();
  744. },
  745. async checkRemoteApp() {
  746. if (typeof nodeRequire == "undefined") {
  747. return;
  748. }
  749. async function checkRemoteAppTxt() {
  750. let applicationNames;
  751. try {
  752. const fs = window.nodeRequire("fs");
  753. try {
  754. applicationNames = fs.readFileSync(
  755. "remoteApplication.txt",
  756. "utf-8"
  757. );
  758. } catch (error) {
  759. console.log(error);
  760. window._hmt.push([
  761. "_trackEvent",
  762. "登录页面",
  763. "读取remoteApplication.txt出错--0",
  764. ]);
  765. await new Promise((resolve2) => setTimeout(() => resolve2(), 3000));
  766. applicationNames = fs.readFileSync(
  767. "remoteApplication.txt",
  768. "utf-8"
  769. );
  770. }
  771. } catch (error) {
  772. console.log(error);
  773. createLog({
  774. currentPage: "登录页面",
  775. errorType: "e-01",
  776. error: error.message,
  777. detail: applicationNames,
  778. });
  779. window._hmt.push([
  780. "_trackEvent",
  781. "登录页面",
  782. "读取remoteApplication.txt出错",
  783. ]);
  784. this.$Message.error({
  785. content: "系统检测出错(e-01),请退出程序后重试!",
  786. duration: 2 * 24 * 60 * 60,
  787. });
  788. return;
  789. }
  790. if (applicationNames && applicationNames.trim()) {
  791. let names = applicationNames
  792. .replace("qq", "QQ")
  793. .replace("teamviewer", "TeamViewer")
  794. .replace("lookmypc", "LookMyPC")
  795. .replace("xt", "协通")
  796. .replace("winaw32", "Symantec PCAnywhere")
  797. .replace("pcaquickconnect", "Symantec PCAnywhere")
  798. .replace("sessioncontroller", "Symantec PCAnywhere")
  799. .replace(/sunloginclient/gi, "向日葵")
  800. .replace(/sunloginremote/gi, "向日葵")
  801. .replace("wemeetapp", "腾讯会议")
  802. .replace("wechat", "微信");
  803. names = [...new Set(names.split(",").map((v) => v.trim()))].join(
  804. ","
  805. );
  806. this.disableLoginBtnBecauseRemoteApp = true;
  807. this.$Message.info({
  808. content: "在考试期间,请关掉" + names + "软件,诚信考试。",
  809. duration: 2 * 24 * 60 * 60,
  810. });
  811. } else {
  812. this.disableLoginBtnBecauseRemoteApp = false;
  813. }
  814. }
  815. //如果配置中配置了 DISABLE_REMOTE_ASSISTANCE
  816. if (
  817. this.QECSConfig.PREVENT_CHEATING_CONFIG.includes(
  818. "DISABLE_REMOTE_ASSISTANCE"
  819. )
  820. ) {
  821. let exe = "Project1.exe";
  822. if (fileExists("Project2.exe")) {
  823. const remoteAppName =
  824. "qq;teamviewer;lookmypc;xt;winaw32;pcaquickconnect;sessioncontroller;sunloginclient;sunloginremote;wemeetapp;wechat";
  825. exe = `Project2.exe "${remoteAppName}" `;
  826. }
  827. await nativeExe(exe, checkRemoteAppTxt.bind(this));
  828. } else {
  829. this.disableLoginBtnBecauseRemoteApp = false;
  830. }
  831. },
  832. async checkVCam() {
  833. if (typeof nodeRequire == "undefined") {
  834. return;
  835. }
  836. const vcamList = [
  837. "17GuaGua Cam",
  838. "91KBOX",
  839. // "ASUS Virtual Camera",
  840. "e2eSoft iVCam",
  841. "e2eSoft VCam",
  842. "FaceRig Virtual Camera",
  843. // "Lenovo Virtual Camera",
  844. "MagicCamera Capture",
  845. "MeiSe",
  846. "Virtual Cam",
  847. "YY伴侣",
  848. "WebcamMax Capture",
  849. "Wecam",
  850. "Vcam ",
  851. "softcam",
  852. "Vandate Virtual Camera",
  853. "video2webcam",
  854. "VCDCut Pro",
  855. "9158虚拟视频",
  856. "9158Capture",
  857. "Insta360 Virtual Camera",
  858. "无他直播伴侣PC客户端",
  859. "无他相机电脑版",
  860. "mobiola webcamera",
  861. "艾米秀宝(ImiShowBox)",
  862. "video2Webcam",
  863. "飞翔虚拟视频",
  864. "魔力秀",
  865. "YY开播",
  866. "无他伴侣",
  867. "视频连麦",
  868. "酷狗直播伴侣",
  869. ];
  870. async function checkVCamTxt() {
  871. let applicationNames;
  872. try {
  873. const fs = window.nodeRequire("fs");
  874. try {
  875. applicationNames = fs.readFileSync("CameraInfo.txt", "utf-8");
  876. } catch (error) {
  877. console.log(error);
  878. window._hmt.push([
  879. "_trackEvent",
  880. "登录页面",
  881. "CameraInfo.txt出错--0",
  882. ]);
  883. await new Promise((resolve2) => setTimeout(() => resolve2(), 3000));
  884. applicationNames = fs.readFileSync("CameraInfo.txt", "utf-8");
  885. }
  886. } catch (error) {
  887. console.log(error);
  888. createLog({
  889. currentPage: "登录页面",
  890. errorType: "e-02",
  891. error: error.message,
  892. detail: applicationNames,
  893. });
  894. window._hmt.push([
  895. "_trackEvent",
  896. "登录页面",
  897. "读取CameraInfo.txt出错",
  898. ]);
  899. this.$Message.error({
  900. content: "系统检测出错(e-02),请退出程序后重试!",
  901. duration: 2 * 24 * 60 * 60,
  902. });
  903. return;
  904. }
  905. this.disableLoginBtnBecauseVCam = false;
  906. if (applicationNames && applicationNames.trim()) {
  907. for (const vc of vcamList) {
  908. if (applicationNames.toUpperCase().includes(vc.toUpperCase())) {
  909. this.disableLoginBtnBecauseVCam = true;
  910. this.$Message.info({
  911. content:
  912. "在考试期间,请关掉" + "虚拟摄像头" + "软件,诚信考试。",
  913. duration: 2 * 24 * 60 * 60,
  914. });
  915. console.log(applicationNames);
  916. }
  917. }
  918. }
  919. }
  920. //如果配置中配置了 DISABLE_VIRTUAL_CAMERA
  921. if (
  922. this.QECSConfig.PREVENT_CHEATING_CONFIG.includes(
  923. "DISABLE_VIRTUAL_CAMERA"
  924. )
  925. ) {
  926. await nativeExe("multiCamera.exe", checkVCamTxt.bind(this));
  927. } else {
  928. this.disableLoginBtnBecauseVCam = false;
  929. }
  930. },
  931. async checkAllowedClient() {
  932. if (process.env.VUE_APP_SKIP_CHECK_NATIVE === "true") {
  933. console.log(
  934. "环境检查: process.env.VUE_APP_SKIP_CHECK_NATIVE === " +
  935. process.env.VUE_APP_SKIP_CHECK_NATIVE,
  936. " 允许浏览器访问"
  937. );
  938. this.disableLoginBtnBecauseNotAllowedNative = false;
  939. return;
  940. }
  941. // if (!this.FE_FEATURE_ALLOW_CHECK) {
  942. // this.disableLoginBtnBecauseNotAllowedNative = false;
  943. // return;
  944. // }
  945. /**
  946. * 本应用仅处理学生端的请求。不做跳转,不管浏览器类型。
  947. * 允许NATIVE/允许BROWSER,在以下场景中能访问:学生端/移动端浏览器
  948. */
  949. // if (
  950. // this.QECSConfig.LOGIN_SUPPORT.includes("BROWSER") &&
  951. // this.QECSConfig.LOGIN_SUPPORT.includes("NATIVE")
  952. // ) {
  953. // // FIXME: 暂时允许选择浏览器的可以在浏览器中访问
  954. // this.disableLoginBtnBecauseNotAllowedNative = false;
  955. // return;
  956. // } else
  957. if (this.QECSConfig.LOGIN_SUPPORT.includes("NATIVE")) {
  958. // 检测是否是学生端。 检测是否有node。检测是否能node调用。
  959. const hasShell = UA.getBrowser().name === "electron-exam-shell";
  960. const hasNode = typeof nodeRequire !== "undefined";
  961. let hasFs = false;
  962. let hasReadFileSync = false;
  963. if (hasShell && hasNode) {
  964. const fs = window.nodeRequire("fs");
  965. if (fs && typeof fs.readFileSync === "function") {
  966. hasFs = true;
  967. hasReadFileSync = true;
  968. this.disableLoginBtnBecauseNotAllowedNative = false;
  969. }
  970. }
  971. if (this.disableLoginBtnBecauseNotAllowedNative) {
  972. window._hmt.push([
  973. "_trackEvent",
  974. "登录页面",
  975. "不允许访问-" +
  976. JSON.stringify({
  977. hasShell,
  978. hasNode,
  979. hasFs,
  980. hasReadFileSync,
  981. }),
  982. ]);
  983. this.$Message.error({
  984. content: "请与学校申请最新的客户端,进行考试!",
  985. duration: 2 * 24 * 60 * 60,
  986. });
  987. }
  988. // } else if (this.QECSConfig.LOGIN_SUPPORT.includes("BROWSER")) {
  989. // const hasShell = UA.getBrowser().name === "electron-exam-shell";
  990. // if (hasShell) {
  991. // if (this.disableLoginBtnBecauseNotAllowedNative) {
  992. // window._hmt.push([
  993. // "_trackEvent",
  994. // "登录页面",
  995. // "不允许访问-不允许在考生端中访问只设定移动端浏览器访问的设置",
  996. // ]);
  997. // this.$Message.error({
  998. // content: "本考试不支持在考生端中进行!",
  999. // duration: 2 * 24 * 60 * 60,
  1000. // });
  1001. // }
  1002. // }
  1003. // // TODO: redirect to wap site
  1004. // // if(chromeUA.major < "66") { } // 到移动端去判断
  1005. // return;
  1006. } else {
  1007. this.$Message.error({
  1008. content: "本考试不支持在考生端中进行!",
  1009. duration: 2 * 24 * 60 * 60,
  1010. });
  1011. }
  1012. },
  1013. closeApp() {
  1014. console.log("关闭应用");
  1015. window.close();
  1016. },
  1017. examShellStats() {
  1018. const shellVersion = window.navigator.userAgent
  1019. .split(" ")
  1020. .find((v) => v.startsWith("electron-exam-shell/"));
  1021. const chromeVersion = window.navigator.userAgent
  1022. .split(" ")
  1023. .find((v) => v.startsWith("Chrome/"));
  1024. if (shellVersion) {
  1025. window._hmt.push([
  1026. "_trackEvent",
  1027. "登录页面",
  1028. "学生端版本",
  1029. shellVersion + "/" + chromeVersion,
  1030. ]);
  1031. } else {
  1032. if (chromeVersion) {
  1033. window._hmt.push([
  1034. "_trackEvent",
  1035. "登录页面",
  1036. "学生端版本/chrome",
  1037. chromeVersion,
  1038. ]);
  1039. } else {
  1040. window._hmt.push([
  1041. "_trackEvent",
  1042. "登录页面",
  1043. "学生端版本/其他浏览器",
  1044. window.navigator.userAgent,
  1045. ]);
  1046. }
  1047. }
  1048. },
  1049. async epccLogin() {
  1050. const {
  1051. accountType,
  1052. accountValue,
  1053. rootOrgId,
  1054. appId,
  1055. timestamp,
  1056. token,
  1057. redirectUrl,
  1058. } = this.$route.query;
  1059. if (
  1060. accountType &&
  1061. accountValue &&
  1062. rootOrgId &&
  1063. appId &&
  1064. timestamp &&
  1065. token &&
  1066. redirectUrl
  1067. ) {
  1068. try {
  1069. const response = await this.$http.post(
  1070. "/api/ecs_core/auth/thirdPartyStudentAccess" + location.search
  1071. );
  1072. return response;
  1073. } catch (error) {
  1074. window._hmt.push(["_trackEvent", "第三方登录页面", "接口出错", ""]);
  1075. this.$Spin.hide();
  1076. this.logout();
  1077. }
  1078. } else {
  1079. this.$Spin.hide();
  1080. this.logout();
  1081. }
  1082. },
  1083. calcVerify({ uuid, verifyCode }) {
  1084. // console.log(uuid, verifyCode);
  1085. this.loginForm.tid = uuid;
  1086. this.loginForm.verifyCode = verifyCode;
  1087. },
  1088. },
  1089. };
  1090. </script>
  1091. <style scoped>
  1092. .home {
  1093. display: flex;
  1094. flex-direction: column;
  1095. height: 100vh;
  1096. }
  1097. .school-logo {
  1098. justify-self: flex-start;
  1099. margin-left: 100px;
  1100. }
  1101. .logo-size {
  1102. height: 100px;
  1103. width: 400px;
  1104. object-fit: cover;
  1105. }
  1106. .header {
  1107. min-height: 120px;
  1108. display: grid;
  1109. align-items: center;
  1110. justify-items: center;
  1111. }
  1112. .center {
  1113. background-image: url("https://cdn.qmth.com.cn/ui/ecs-client-bg.jpg!/progressive/true");
  1114. background-position: center;
  1115. background-repeat: no-repeat;
  1116. background-size: cover;
  1117. width: 100vw;
  1118. min-height: 600px;
  1119. }
  1120. .content {
  1121. margin-top: 100px;
  1122. margin-left: 65%;
  1123. width: 300px;
  1124. border-radius: 6px;
  1125. background-color: white;
  1126. display: grid;
  1127. grid-template-areas: "";
  1128. }
  1129. .login-type {
  1130. flex: 1;
  1131. line-height: 40px;
  1132. background-color: #eeeeee;
  1133. }
  1134. .active-type {
  1135. background-color: #ffffff;
  1136. }
  1137. .single-login-type {
  1138. border-top-left-radius: 6px;
  1139. border-top-right-radius: 6px;
  1140. }
  1141. .close {
  1142. position: absolute;
  1143. top: 0;
  1144. right: 0;
  1145. background-color: #eeeeee;
  1146. color: #999999;
  1147. width: 80px;
  1148. height: 40px;
  1149. line-height: 40px;
  1150. }
  1151. .close:hover {
  1152. color: #444444;
  1153. }
  1154. .footer {
  1155. position: relative;
  1156. }
  1157. </style>
  1158. <style>
  1159. .ivu-message-notice-content-text {
  1160. font-size: 32px;
  1161. }
  1162. .ivu-message-notice-content-text i.ivu-icon {
  1163. font-size: 32px;
  1164. }
  1165. </style>