Login.vue 29 KB

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