Login.vue 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717
  1. <template>
  2. <div class="home">
  3. <header class="header">
  4. <div class="school-logo">
  5. <img class="logo-size" :src="this.logoPath" alt="school logo" />
  6. </div>
  7. <a
  8. class="close"
  9. style="border-bottom-left-radius: 6px;"
  10. @click="closeApp"
  11. >
  12. 关闭
  13. </a>
  14. </header>
  15. <div class="center">
  16. <div class="content">
  17. <div style="display:flex;">
  18. <a
  19. v-if="allowLoginType.includes('STUDENT_CODE')"
  20. :class="[
  21. 'qm-big-text',
  22. 'login-type',
  23. loginType === 'STUDENT_CODE' && 'active-type',
  24. allowLoginType.length === 1 && 'single-login-type',
  25. ]"
  26. @click="loginType = 'STUDENT_CODE'"
  27. style="border-top-left-radius: 6px"
  28. >
  29. 学号登录
  30. </a>
  31. <a
  32. v-if="allowLoginType.includes('IDENTITY_NUMBER')"
  33. :class="[
  34. 'qm-big-text',
  35. 'login-type',
  36. loginType !== 'STUDENT_CODE' && 'active-type',
  37. allowLoginType.length === 1 && 'single-login-type',
  38. ]"
  39. @click="loginType = 'STUDENT_IDENTITY_NUMBER'"
  40. style="border-top-right-radius: 6px"
  41. >
  42. 身份证号登录
  43. </a>
  44. <a
  45. v-if="allowLoginType.length === 0"
  46. :class="['qm-big-text', 'login-type']"
  47. >loading...</a
  48. >
  49. </div>
  50. <div class="qm-title-text" style="margin: 40px 0 20px 0">
  51. {{ productName }}
  52. </div>
  53. <div style="margin: 0 40px 40px 40px">
  54. <i-form ref="loginForm" :model="loginForm" :rules="loginFormRule">
  55. <i-form-item
  56. prop="accountValue"
  57. style="margin-bottom:35px;height:42px"
  58. >
  59. <i-input
  60. type="text"
  61. size="large"
  62. v-model="loginForm.accountValue"
  63. :placeholder="usernameInputPlaceholder"
  64. >
  65. <i-icon type="ios-person" slot="prepend"></i-icon>
  66. </i-input>
  67. </i-form-item>
  68. <i-form-item prop="password" style="margin-bottom:35px;height:42px">
  69. <i-input
  70. type="password"
  71. size="large"
  72. v-model="loginForm.password"
  73. :placeholder="passwordInputPlaceholder"
  74. @on-enter="login"
  75. >
  76. <i-icon type="ios-lock" slot="prepend"></i-icon>
  77. </i-input>
  78. </i-form-item>
  79. <i-form-item style="position: relative">
  80. <div
  81. v-if="errorInfo !== ''"
  82. style="position: absolute; top: -37px; width: 100%"
  83. >
  84. <i-alert type="error" show-icon>{{ errorInfo }}</i-alert>
  85. </div>
  86. <i-button
  87. size="large"
  88. class="qm-primary-button"
  89. long
  90. :disabled="disableLoginBtn"
  91. :loading="loginBtnLoading"
  92. @click="login"
  93. >
  94. {{ newVersionAvailable ? "点击更新版本" : "登录" }}
  95. </i-button>
  96. </i-form-item>
  97. </i-form>
  98. </div>
  99. </div>
  100. </div>
  101. <footer class="footer">
  102. <div style="position: absolute; right: 20px; bottom: 20px;">
  103. 版本: {{ VUE_APP_GIT_REPO_VERSION }}
  104. </div>
  105. <DevTools />
  106. </footer>
  107. </div>
  108. </template>
  109. <script>
  110. import moment from "moment";
  111. import { mapMutations } from "vuex";
  112. import { FACE_API_MODEL_PATH } from "@/constants/constants";
  113. import DevTools from "./DevTools.vue";
  114. import nativeExe from "@/utils/nativeExe";
  115. /**
  116. * 在任何组件需要强制退出,做以下步骤
  117. * 1. this.$Message.info()
  118. * 2. this.$router.push("/login"+domain);
  119. * 因为在/login里会删除localStorage的token,而在router.beforeEach会检查是否有token,达到退出的目的。
  120. */
  121. export default {
  122. name: "Login",
  123. data() {
  124. return {
  125. // LOGIN_ID_DOMAINS: ["cugr.ecs.qmth.com.cn", "cugbr.ecs.qmth.com.cn"],
  126. domainInUrl: this.$route.params.domain,
  127. QECSConfig: {},
  128. loginType: "STUDENT_CODE",
  129. errorInfo: "",
  130. loginForm: {
  131. accountValue: "",
  132. password: "",
  133. },
  134. loginFormRule: {
  135. accountValue: [
  136. {
  137. required: true,
  138. message: "请填写登录账号",
  139. trigger: "blur",
  140. },
  141. ],
  142. password: [
  143. {
  144. required: true,
  145. message: "请填写密码",
  146. trigger: "blur",
  147. },
  148. ],
  149. },
  150. loginBtnLoading: false,
  151. isElectron: true,
  152. disableLoginBtnBecauseRemoteApp: true,
  153. disableLoginBtnBecauseVCam: true,
  154. disableLoginBtnBecauseNoRemoteAppChecker: false,
  155. newVersionAvailable: false,
  156. VUE_APP_GIT_REPO_VERSION: process.env.VUE_APP_GIT_REPO_VERSION,
  157. };
  158. },
  159. async mounted() {
  160. // await this.checkNewVersion();
  161. if (localStorage.getItem("__swReload")) {
  162. localStorage.removeItem("__swReload");
  163. this.$Message.info({
  164. content: "正在更新版本...",
  165. });
  166. await new Promise(resolve => {
  167. setTimeout(() => {
  168. resolve();
  169. }, 3000);
  170. });
  171. location.reload(true);
  172. }
  173. // manual precache for models
  174. fetch(
  175. FACE_API_MODEL_PATH + "tiny_face_detector_model-weights_manifest.json"
  176. );
  177. fetch(FACE_API_MODEL_PATH + "face_landmark_68_model-weights_manifest.json");
  178. // alread precached
  179. // fetch("/models/tiny_face_detector_model-shard1");
  180. // fetch("/models/face_landmark_68_model-shard1");
  181. this.checkNewVersionInterval = setInterval(() => {
  182. if (window.__newSWAvailable) {
  183. this.newVersionAvailable = true;
  184. }
  185. }, 1000);
  186. },
  187. async created() {
  188. this.isElectron = typeof nodeRequire != "undefined";
  189. if (
  190. navigator.userAgent.indexOf("WOW64") != -1 ||
  191. navigator.userAgent.indexOf("Win64") != -1
  192. ) {
  193. window._hmt.push(["_trackEvent", "登录页面", "64Bit OS"]);
  194. } else {
  195. window._hmt.push(["_trackEvent", "登录页面", "非64Bit OS"]);
  196. }
  197. this.$Message.config({
  198. duration: 15,
  199. size: "large",
  200. closable: true, // 没有影响到所有的组件。。。 https://github.com/iview/iview/issues/2962
  201. });
  202. window.sessionStorage.removeItem("token");
  203. window.sessionStorage.clear();
  204. window.localStorage.removeItem("key");
  205. if (localStorage.getItem("user-for-reload")) {
  206. this.loginForm.accountValue = JSON.parse(
  207. localStorage.getItem("user-for-reload")
  208. ).studentCodeList[0];
  209. this.loginForm.password =
  210. process.env.NODE_ENV === "production" ? "" : "180613";
  211. }
  212. if (
  213. [
  214. "xjtu.ecs.qmth.com.cn",
  215. "snnu.ecs.qmth.com.cn",
  216. "cup.ecs.qmth.com.cn",
  217. "swjtu.ecs.qmth.com.cn",
  218. ].includes(this.$route.params.domain)
  219. ) {
  220. if (
  221. typeof nodeRequire == "undefined" ||
  222. !window.nodeRequire("fs").existsSync("multiCamera.exe")
  223. ) {
  224. this.disableLoginBtnBecauseNoRemoteAppChecker = true;
  225. this.$Message.error({
  226. content: "请与学校申请最新的客户端,进行考试!",
  227. duration: 2 * 24 * 60 * 60,
  228. });
  229. }
  230. }
  231. await this.checkElectronConfig();
  232. await this.checkVCam();
  233. if (this.allowLoginType === "IDENTITY_NUMBER") {
  234. this.loginType = "STUDENT_IDENTITY_NUMBER";
  235. }
  236. },
  237. beforeDestroy() {
  238. clearTimeout(this.loginTimeout);
  239. clearInterval(this.checkNewVersionInterval);
  240. },
  241. methods: {
  242. ...mapMutations(["updateUser", "updateTimeDifference"]),
  243. async login() {
  244. if (this.disableLoginBtn) {
  245. return;
  246. }
  247. this.loginBtnLoading = true;
  248. await this.checkNewVersion();
  249. this.loginTimeout = setTimeout(() => {
  250. this.loginBtnLoading = false;
  251. }, 10 * 1000);
  252. // https://www.cnblogs.com/weiqinl/p/6708993.html
  253. const valid = await this.$refs.loginForm.validate();
  254. if (!valid) {
  255. return;
  256. }
  257. let repPara = this.loginForm;
  258. // 以下网络请求失败,直接报网络异常错误
  259. const response = await this.$http.post("/api/ecs_core/auth/login", {
  260. ...repPara,
  261. accountType: this.loginType,
  262. domain: this.schoolDomain,
  263. alwaysOK: true,
  264. });
  265. let data = response.data;
  266. // if (
  267. // Math.abs() >
  268. // 5 * 60 * 1000
  269. // ) {
  270. // window._hmt.push(["_trackEvent", "登录页面", "本机时间误差过大"]);
  271. // this.$Message.error({
  272. // content: "与服务器时间差异超过5分钟,请校准本机时间之后再重试!",
  273. // duration: 30
  274. // });
  275. // throw "与服务器时间差异超过5分钟,请校准本机时间之后再重试!";
  276. // }
  277. this.updateTimeDifference(moment(response.headers.date).diff(moment()));
  278. if (data.code == "200") {
  279. data = data.content;
  280. this.errorInfo = "";
  281. //缓存用户信息
  282. window.sessionStorage.setItem("token", data.token);
  283. window.localStorage.setItem("key", data.key);
  284. window.localStorage.setItem("domain", this.schoolDomain);
  285. try {
  286. const student = (await this.$http.get(
  287. "/api/ecs_core/student/getStudentInfoBySession"
  288. )).data;
  289. const specialty = (await this.$http.get(
  290. "/api/ecs_exam_work/exam_student/specialtyNameList/"
  291. )).data;
  292. const user = {
  293. ...data,
  294. ...student,
  295. specialty: specialty.join(),
  296. schoolDomain: this.schoolDomain,
  297. };
  298. this.updateUser(user);
  299. window.localStorage.setItem("user-for-reload", JSON.stringify(user));
  300. window._hmt.push([
  301. "_trackEvent",
  302. "登录页面",
  303. "登录",
  304. this.$route.query.LogoutReason,
  305. ]);
  306. await this.checkExamInProgress();
  307. window._hmt.push(["_trackEvent", "登录页面", "登录成功"]);
  308. } catch (error) {
  309. window._hmt.push([
  310. "_trackEvent",
  311. "登录页面",
  312. "登录失败",
  313. "getStudentInfoBySession失败",
  314. ]);
  315. this.$Message.error({
  316. content: "获取学生信息失败,请重试!",
  317. duration: 15,
  318. closable: true,
  319. });
  320. }
  321. } else {
  322. window._hmt.push(["_trackEvent", "登录页面", "登录失败", data.desc]);
  323. this.errorInfo = data.desc;
  324. }
  325. },
  326. async checkExamInProgress() {
  327. try {
  328. // 断点续考
  329. const examingRes = (await this.$http.get(
  330. "/api/ecs_oe_student/examControl/checkExamInProgress"
  331. )).data;
  332. if (examingRes.isExceed) {
  333. // 超出断点续考次数的逻辑,仅此block
  334. this.$Message.info({
  335. content: `超出最大断点续考次数(${
  336. examingRes.maxInterruptNum
  337. }),正在自动交卷...`,
  338. duration: 15,
  339. closable: true,
  340. });
  341. this.$Spin.show({
  342. render: () => {
  343. return (
  344. <div style="font-size: 24px">
  345. 超出最大断点续考次数({examingRes.maxInterruptNum}
  346. ),正在自动交卷...
  347. </div>
  348. );
  349. },
  350. });
  351. const res = await this.$http.get(
  352. "/api/ecs_oe_student/examControl/endExam"
  353. );
  354. if (res.status === 200) {
  355. this.$router.replace({
  356. path: `/online-exam/exam/${examingRes.examId}/examRecordData/${
  357. examingRes.examRecordDataId
  358. }/end`,
  359. });
  360. this.$Spin.hide();
  361. } else {
  362. this.$Message.error({
  363. content: "交卷失败",
  364. duration: 15,
  365. closable: true,
  366. });
  367. }
  368. return;
  369. }
  370. if (examingRes) {
  371. this.$Spin.show({
  372. render: () => {
  373. return <div style="font-size: 24px">正在进入断点续考...</div>;
  374. },
  375. });
  376. window._hmt.push(["_trackEvent", "登录页面", "断点续考", "重新登录"]);
  377. this.$router.push(
  378. `/online-exam/exam/${examingRes.examId}/examRecordData/${
  379. examingRes.examRecordDataId
  380. }/order/1` +
  381. (examingRes.faceVerifyMinute
  382. ? `?faceVerifyMinute=${examingRes.faceVerifyMinute}`
  383. : "")
  384. );
  385. setTimeout(() => this.$Spin.hide(), 1000);
  386. return;
  387. }
  388. this.$router.push("/online-exam");
  389. } catch (error) {
  390. this.$Message.error({
  391. content: "获取断点续考信息异常,退出登录",
  392. duration: 15,
  393. closable: true,
  394. });
  395. this.logout("?LogoutReason=登录页面获取断点续考信息异常");
  396. return;
  397. }
  398. },
  399. async checkNewVersion() {
  400. let myHeaders = new Headers();
  401. myHeaders.append("Content-Type", "application/javascript");
  402. myHeaders.append("Cache-Control", "no-cache");
  403. const response = await fetch(
  404. document.scripts[document.scripts.length - 1].src + "?x" + Date.now(),
  405. {
  406. method: process.env.NODE_ENV === "development" ? "GET" : "HEAD",
  407. headers: myHeaders,
  408. }
  409. );
  410. if (!response.ok) {
  411. window._hmt.push([
  412. "_trackEvent",
  413. "登录页面",
  414. "新版本发布后,客户端自动刷新",
  415. ]);
  416. this.$Message.info({
  417. content: "正在获取新版本...",
  418. });
  419. localStorage.setItem("__swReload", "anything");
  420. await new Promise(resolve => {
  421. setTimeout(() => {
  422. resolve();
  423. }, 1000);
  424. });
  425. location.reload(true);
  426. }
  427. },
  428. async checkElectronConfig() {
  429. try {
  430. const fileSever =
  431. location.hostname === "ecs.qmth.com.cn"
  432. ? "https://ecs-static.qmth.com.cn"
  433. : "https://ecs-test-static.qmth.com.cn";
  434. const res = await fetch(
  435. fileSever +
  436. "/org_properties/byOrgDomain/" +
  437. this.domainInUrl +
  438. "/studentClientConfig.json"
  439. );
  440. if (res.ok) {
  441. const json = await res.json();
  442. this.QECSConfig = json;
  443. } else {
  444. this.$Message.error({
  445. content: "获取远程配置文件出错,请重新刷新网页!",
  446. duration: 15,
  447. closable: true,
  448. });
  449. return;
  450. }
  451. } catch (e) {
  452. this.$Message.error({
  453. content: "获取远程配置文件出错,请重新刷新网页!",
  454. duration: 15,
  455. closable: true,
  456. });
  457. return;
  458. }
  459. await this.checkRemoteApp();
  460. },
  461. async checkRemoteApp() {
  462. if (typeof nodeRequire == "undefined") {
  463. return;
  464. }
  465. function checkRemoteAppTxt() {
  466. let applicationNames;
  467. try {
  468. const fs = window.nodeRequire("fs");
  469. applicationNames = fs.readFileSync("remoteApplication.txt", "utf-8");
  470. } catch (error) {
  471. this.$Message.error({
  472. content: "系统检测出错,请退出程序后重试!",
  473. duration: 2 * 24 * 60 * 60,
  474. });
  475. return;
  476. }
  477. if (applicationNames && applicationNames.trim()) {
  478. this.disableLoginBtnBecauseRemoteApp = true;
  479. this.$Message.info({
  480. content:
  481. "在考试期间,请关掉" +
  482. applicationNames.trim() +
  483. "软件,诚信考试。",
  484. duration: 2 * 24 * 60 * 60,
  485. });
  486. } else {
  487. this.disableLoginBtnBecauseRemoteApp = false;
  488. }
  489. }
  490. //如果配置中配置了 DISABLE_REMOTE_ASSISTANCE
  491. if (
  492. this.QECSConfig.PREVENT_CHEATING_CONFIG.includes[
  493. "DISABLE_REMOTE_ASSISTANCE"
  494. ]
  495. ) {
  496. await nativeExe("Project1.exe", checkRemoteAppTxt.bind(this));
  497. }
  498. },
  499. async checkVCam() {
  500. if (typeof nodeRequire == "undefined") {
  501. return;
  502. }
  503. const vcamList = [
  504. "17GuaGua Cam",
  505. "91KBOX",
  506. "ASUS Virtual Camera",
  507. "e2eSoft iVCam",
  508. "e2eSoft VCam",
  509. "FaceRig Virtual Camera",
  510. "Lenovo Virtual Camera",
  511. "MagicCamera Capture",
  512. "MeiSe",
  513. "Virtual Cam",
  514. "YY伴侣",
  515. ];
  516. function checkVCamTxt() {
  517. let applicationNames;
  518. try {
  519. const fs = window.nodeRequire("fs");
  520. applicationNames = fs.readFileSync("CameraInfo.txt", "utf-8");
  521. } catch (error) {
  522. this.$Message.error({
  523. content: "系统检测出错,请退出程序后重试!",
  524. duration: 2 * 24 * 60 * 60,
  525. });
  526. return;
  527. }
  528. if (applicationNames && applicationNames.trim()) {
  529. for (const vc of vcamList) {
  530. if (applicationNames.includes(vc)) {
  531. this.disableLoginBtnBecauseVCam = true;
  532. this.$Message.info({
  533. content:
  534. "在考试期间,请关掉" + "虚拟摄像头" + "软件,诚信考试。",
  535. duration: 2 * 24 * 60 * 60,
  536. });
  537. console.log(applicationNames);
  538. }
  539. }
  540. } else {
  541. this.disableLoginBtnBecauseVCam = false;
  542. }
  543. }
  544. //如果配置中配置了 DISABLE_VIRTUAL_CAMERA
  545. if (
  546. this.QECSConfig.PREVENT_CHEATING_CONFIG.includes[
  547. "DISABLE_VIRTUAL_CAMERA"
  548. ]
  549. ) {
  550. await nativeExe("multiCamera.exe", checkVCamTxt.bind(this));
  551. }
  552. },
  553. closeApp() {
  554. window.close();
  555. },
  556. },
  557. computed: {
  558. logoPath() {
  559. return this.QECSConfig.LOGO_FILE_URL;
  560. },
  561. productName() {
  562. return this.QECSConfig.OE_STUDENT_SYS_NAME || "远程教育网络考试";
  563. },
  564. allowLoginType() {
  565. return (
  566. (this.QECSConfig.LOGIN_TYPE && this.QECSConfig.LOGIN_TYPE.split(",")) ||
  567. []
  568. );
  569. },
  570. schoolDomain() {
  571. const domain = this.domainInUrl;
  572. if (!domain || !domain.includes("qmth.com.cn")) {
  573. this.$Message.error({
  574. content: "机构地址出错,请关闭程序后再登录。",
  575. duration: 15,
  576. closable: true,
  577. });
  578. }
  579. return domain;
  580. },
  581. usernameInputPlaceholder() {
  582. if (this.loginType === "STUDENT_CODE") {
  583. return "请输入学号";
  584. } else {
  585. return "请输入身份证号";
  586. }
  587. },
  588. passwordInputPlaceholder() {
  589. if (this.loginType === "STUDENT_CODE") {
  590. return "初始密码为身份证号后6位";
  591. } else {
  592. return "初始密码为身份证号后6位";
  593. }
  594. },
  595. disableLoginBtn() {
  596. return (
  597. this.isElectron &&
  598. (this.disableLoginBtnBecauseRemoteApp ||
  599. this.disableLoginBtnBecauseVCam ||
  600. this.disableLoginBtnBecauseNoRemoteAppChecker)
  601. );
  602. },
  603. },
  604. components: {
  605. DevTools,
  606. },
  607. };
  608. </script>
  609. <style scoped>
  610. .home {
  611. display: flex;
  612. flex-direction: column;
  613. height: 100vh;
  614. }
  615. .school-logo {
  616. justify-self: flex-start;
  617. margin-left: 100px;
  618. }
  619. .logo-size {
  620. height: 100px;
  621. width: 400px;
  622. object-fit: cover;
  623. }
  624. .header {
  625. min-height: 120px;
  626. display: grid;
  627. align-items: center;
  628. justify-items: center;
  629. }
  630. .center {
  631. background-image: url("https://cdn.qmth.com.cn/ui/ecs-client-bg.jpg!/progressive/true");
  632. background-position: center;
  633. background-repeat: no-repeat;
  634. background-size: cover;
  635. width: 100vw;
  636. min-height: 600px;
  637. }
  638. .content {
  639. margin-top: 100px;
  640. margin-left: 65%;
  641. width: 300px;
  642. border-radius: 6px;
  643. background-color: white;
  644. display: grid;
  645. grid-template-areas: "";
  646. }
  647. .login-type {
  648. flex: 1;
  649. line-height: 40px;
  650. background-color: #eeeeee;
  651. }
  652. .active-type {
  653. background-color: #ffffff;
  654. }
  655. .single-login-type {
  656. border-top-left-radius: 6px;
  657. border-top-right-radius: 6px;
  658. }
  659. .close {
  660. position: absolute;
  661. top: 0;
  662. right: 0;
  663. background-color: #eeeeee;
  664. color: #999999;
  665. width: 80px;
  666. height: 40px;
  667. line-height: 40px;
  668. }
  669. .close:hover {
  670. color: #444444;
  671. }
  672. .footer {
  673. position: relative;
  674. }
  675. </style>
  676. <style>
  677. .ivu-message-notice-content-text {
  678. font-size: 32px;
  679. }
  680. .ivu-message-notice-content-text i.ivu-icon {
  681. font-size: 32px;
  682. }
  683. </style>