Login.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523
  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. /**
  103. * 在任何组件需要强制退出,做以下步骤
  104. * 1. this.$Message.info()
  105. * 2. this.$router.push("/login"+domain);
  106. * 因为在/login里会删除localStorage的token,而在router.beforeEach会检查是否有token,达到退出的目的。
  107. */
  108. export default {
  109. name: "Login",
  110. data() {
  111. return {
  112. LOGIN_ID_DOMAINS: ["cugr.ecs.qmth.com.cn", "cugbr.ecs.qmth.com.cn"],
  113. domainInUrl: this.$route.params.domain,
  114. productName: "",
  115. loginType: "STUDENT_CODE",
  116. errorInfo: "",
  117. loginForm: {
  118. accountValue: "",
  119. password: ""
  120. },
  121. loginFormRule: {
  122. accountValue: [
  123. {
  124. required: true,
  125. message: "请填写登录账号",
  126. trigger: "blur"
  127. }
  128. ],
  129. password: [
  130. {
  131. required: true,
  132. message: "请填写密码",
  133. trigger: "blur"
  134. }
  135. ]
  136. },
  137. disableLoginBtn: true,
  138. VUE_APP_GIT_REPO_VERSION: process.env.VUE_APP_GIT_REPO_VERSION
  139. };
  140. },
  141. async mounted() {
  142. await this.checkNewVersion();
  143. },
  144. async created() {
  145. if (this.LOGIN_ID_DOMAINS.includes(this.schoolDomain)) {
  146. this.loginType = "STUDENT_IDENTITY_NUMBER";
  147. }
  148. this.$Message.config({
  149. duration: 15,
  150. size: "large",
  151. closable: true // 没有影响到所有的组件。。。 https://github.com/iview/iview/issues/2962
  152. });
  153. try {
  154. const res = await this.$http.get(
  155. "/api/ecs_core/org/propertyNoSession/OE_STUDENT_SYS_NAME?domainName=" +
  156. this.schoolDomain
  157. );
  158. const productName = res.data;
  159. this.productName = productName || "远程教育网络考试";
  160. } catch (e) {
  161. this.productName = "远程教育网络考试";
  162. }
  163. window.sessionStorage.removeItem("token");
  164. window.localStorage.removeItem("key");
  165. if (localStorage.getItem("user-for-reload")) {
  166. this.loginForm.accountValue = JSON.parse(
  167. localStorage.getItem("user-for-reload")
  168. ).studentCode;
  169. this.loginForm.password =
  170. process.env.NODE_ENV === "production" ? "" : "180613";
  171. }
  172. if (
  173. this.$route.path.includes("xjtu.ecs.qmth.com.cn") ||
  174. this.$route.path.includes("snnu.ecs.qmth.com.cn") ||
  175. this.$route.path.includes("cup.ecs.qmth.com.cn")
  176. ) {
  177. this.disableLoginBtn = true;
  178. if (
  179. typeof nodeRequire == "undefined" ||
  180. !window.nodeRequire("fs").existsSync("multiCamera.exe")
  181. ) {
  182. this.$Message.error({
  183. content: "请与学校申请最新的客户端,进行考试!",
  184. duration: 2 * 24 * 60 * 60
  185. });
  186. return; // 避免 disableLoginBtn 被覆盖
  187. }
  188. }
  189. if (typeof nodeRequire != "undefined") {
  190. var that = this;
  191. var fs = window.nodeRequire("fs");
  192. var config;
  193. try {
  194. config = fs.readFileSync(process.cwd() + "/" + "config.js", "utf-8");
  195. console.log("使用旧客户端");
  196. } catch (error) {
  197. console.log("尝试使用新客户端");
  198. config = fs.readFileSync("config.js", "utf-8");
  199. }
  200. var nameJson = JSON.parse(config);
  201. let electronConfig = null;
  202. try {
  203. electronConfig = (await this.$http.get(
  204. "https://ecs.qmth.com.cn:8878/electron-config/" +
  205. nameJson.name +
  206. ".js"
  207. )).data;
  208. } catch (error) {
  209. this.$Message.error("获取机构的客户端设置失败,请退出后重试!");
  210. return;
  211. }
  212. //如果配置中配置了 checkRemoteControl:true
  213. if (electronConfig.otherConfig.checkRemoteControl) {
  214. window.nodeRequire("node-cmd").get("Project1.exe", function() {
  215. var applicationNames = fs.readFileSync(
  216. "remoteApplication.txt",
  217. "utf-8"
  218. );
  219. if (applicationNames && applicationNames.trim()) {
  220. that.disableLoginBtn = true;
  221. that.$Message.info({
  222. content:
  223. "在考试期间,请关掉" +
  224. applicationNames.trim() +
  225. "软件,诚信考试。",
  226. duration: 30,
  227. closable: true
  228. });
  229. } else {
  230. that.disableLoginBtn = false;
  231. }
  232. });
  233. } else {
  234. that.disableLoginBtn = false;
  235. }
  236. } else {
  237. this.disableLoginBtn = false;
  238. }
  239. },
  240. methods: {
  241. ...mapMutations(["updateUser", "updateTimeDifference"]),
  242. async login() {
  243. if (this.disableLoginBtn) {
  244. return;
  245. }
  246. await this.checkNewVersion();
  247. this.disableLoginBtn = true;
  248. setTimeout(() => (this.disableLoginBtn = false), 5000);
  249. // https://www.cnblogs.com/weiqinl/p/6708993.html
  250. const valid = await this.$refs.loginForm.validate();
  251. if (valid) {
  252. console.log("form validated. start login...");
  253. } else {
  254. return;
  255. }
  256. let repPara = this.loginForm;
  257. // 以下网络请求失败,直接报网络异常错误
  258. const response = await this.$http.post("/api/ecs_core/auth/login", {
  259. ...repPara,
  260. accountType: this.loginType,
  261. domain: this.schoolDomain,
  262. alwaysOK: true
  263. });
  264. let data = response.data;
  265. // if (
  266. // Math.abs() >
  267. // 5 * 60 * 1000
  268. // ) {
  269. // window._hmt.push(["_trackEvent", "登录页面", "本机时间误差过大"]);
  270. // this.$Message.error({
  271. // content: "与服务器时间差异超过5分钟,请校准本机时间之后再重试!",
  272. // duration: 30
  273. // });
  274. // throw "与服务器时间差异超过5分钟,请校准本机时间之后再重试!";
  275. // }
  276. this.updateTimeDifference(moment(response.headers.date).diff(moment()));
  277. if (data.code == "200") {
  278. data = data.content;
  279. this.errorInfo = "";
  280. //缓存用户信息
  281. window.sessionStorage.setItem("token", data.token);
  282. window.localStorage.setItem("key", data.key);
  283. window.localStorage.setItem("domain", this.schoolDomain);
  284. try {
  285. const student = (await this.$http.get(
  286. "/api/ecs_core/student/getStudentInfoBySession"
  287. )).data;
  288. const specialty = (await this.$http.get(
  289. "/api/ecs_exam_work/exam_student/specialtyNameList/"
  290. )).data;
  291. const user = { ...data, ...student, specialty: specialty.join() };
  292. this.updateUser(user);
  293. window.localStorage.setItem("user-for-reload", JSON.stringify(user));
  294. window._hmt.push([
  295. "_trackEvent",
  296. "登录页面",
  297. "登录",
  298. this.$route.query.LogoutReason
  299. ]);
  300. await this.checkExamInProgress();
  301. window._hmt.push(["_trackEvent", "登录页面", "登录成功"]);
  302. } catch (error) {
  303. window._hmt.push([
  304. "_trackEvent",
  305. "登录页面",
  306. "登录失败",
  307. "getStudentInfoBySession失败"
  308. ]);
  309. this.$Message.error({
  310. content: "获取学生信息失败,请重试!",
  311. duration: 30,
  312. closable: true
  313. });
  314. }
  315. } else {
  316. window._hmt.push(["_trackEvent", "登录页面", "登录失败", data.desc]);
  317. this.errorInfo = data.desc;
  318. }
  319. },
  320. async checkExamInProgress() {
  321. try {
  322. // 断点续考
  323. const examingRes = (await this.$http.get(
  324. "/api/ecs_oe_student/examControl/checkExamInProgress"
  325. )).data;
  326. if (examingRes.isExceed) {
  327. // 超出断点续考次数的逻辑,仅此block
  328. this.$Spin.show({
  329. render: () => {
  330. return (
  331. <div style="font-size: 24px">
  332. `超出最大断点续考次数(${examingRes.maxInterruptNum}
  333. ),正在自动交卷...`
  334. </div>
  335. );
  336. }
  337. });
  338. const res = await this.$http.get(
  339. "/api/ecs_oe_student/examControl/endExam"
  340. );
  341. if (res.status === 200) {
  342. this.$router.replace({
  343. path: `/online-exam/exam/${examingRes.examId}/examRecordData/${
  344. examingRes.examRecordDataId
  345. }/end`
  346. });
  347. this.$Spin.hide();
  348. } else {
  349. this.$Message.error("交卷失败");
  350. }
  351. return;
  352. }
  353. if (examingRes) {
  354. this.$Spin.show({
  355. render: () => {
  356. return <div style="font-size: 24px">正在进入断点续考...</div>;
  357. }
  358. });
  359. window._hmt.push(["_trackEvent", "登录页面", "断点续考", "重新登录"]);
  360. this.$router.push(
  361. `/online-exam/exam/${examingRes.examId}/examRecordData/${
  362. examingRes.examRecordDataId
  363. }/order/1` +
  364. (examingRes.faceVerifyMinute
  365. ? `?faceVerifyMinute=${examingRes.faceVerifyMinute}`
  366. : "")
  367. );
  368. setTimeout(() => this.$Spin.hide(), 1000);
  369. return;
  370. }
  371. this.$router.push("/online-exam");
  372. } catch (error) {
  373. this.$Message.error("获取断点续考信息异常,退出登录");
  374. this.logout("?LogoutReason=登录页面获取断点续考信息异常");
  375. return;
  376. }
  377. },
  378. async checkNewVersion() {
  379. let myHeaders = new Headers();
  380. myHeaders.append("Content-Type", "application/javascript");
  381. myHeaders.append("Cache-Control", "no-cache");
  382. const response = await fetch(
  383. document.scripts[document.scripts.length - 1].src,
  384. {
  385. headers: myHeaders
  386. }
  387. );
  388. if (!response.ok) {
  389. window._hmt.push([
  390. "_trackEvent",
  391. "登录页面",
  392. "新版本发布后,客户端自动刷新"
  393. ]);
  394. location.reload(true);
  395. }
  396. },
  397. closeApp() {
  398. window.close();
  399. }
  400. },
  401. computed: {
  402. logoPath() {
  403. return "/api/ecs_core/org/logo?domain=" + this.domainInUrl;
  404. },
  405. schoolDomain() {
  406. const domain = this.domainInUrl;
  407. if (!domain || !domain.includes("qmth.com.cn")) {
  408. this.$Message.error("机构地址出错,请关闭程序后再登录。");
  409. }
  410. return domain;
  411. },
  412. usernameInputPlaceholder() {
  413. if (this.loginType === "STUDENT_CODE") {
  414. return "请输入学号";
  415. } else {
  416. return "请输入身份证号";
  417. }
  418. },
  419. passwordInputPlaceholder() {
  420. if (this.loginType === "STUDENT_CODE") {
  421. return "初始密码为身份证号后6位";
  422. } else {
  423. return "初始密码为身份证号后6位";
  424. }
  425. }
  426. }
  427. };
  428. </script>
  429. <style scoped>
  430. .home {
  431. display: flex;
  432. flex-direction: column;
  433. height: 100vh;
  434. }
  435. .school-logo {
  436. justify-self: flex-start;
  437. margin-left: 100px;
  438. }
  439. .logo-size {
  440. height: 100px;
  441. width: 400px;
  442. object-fit: cover;
  443. }
  444. .header {
  445. min-height: 120px;
  446. display: grid;
  447. align-items: center;
  448. justify-items: center;
  449. }
  450. .center {
  451. background-image: url("./bg.jpg");
  452. background-position: center;
  453. background-repeat: no-repeat;
  454. background-size: cover;
  455. width: 100vw;
  456. min-height: 600px;
  457. }
  458. .content {
  459. margin-top: 100px;
  460. margin-left: 65%;
  461. width: 300px;
  462. border-radius: 6px;
  463. background-color: white;
  464. display: grid;
  465. grid-template-areas: "";
  466. }
  467. .login-type {
  468. flex: 1;
  469. line-height: 40px;
  470. background-color: #eeeeee;
  471. }
  472. .active-type {
  473. background-color: #ffffff;
  474. }
  475. .close {
  476. position: absolute;
  477. top: 0;
  478. right: 0;
  479. background-color: #eeeeee;
  480. color: #999999;
  481. width: 80px;
  482. height: 40px;
  483. line-height: 40px;
  484. }
  485. .close:hover {
  486. color: #444444;
  487. }
  488. </style>
  489. <style>
  490. .ivu-message-notice-content-text {
  491. font-size: 32px;
  492. }
  493. .ivu-message-notice-content-text i.ivu-icon {
  494. font-size: 32px;
  495. }
  496. </style>