Login.vue 17 KB

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