Login.vue 36 KB

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