Login.vue 40 KB

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