Login.vue 36 KB

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