Login.vue 39 KB

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