Login.vue 38 KB

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