Login.vue 40 KB

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