Login.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538
  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 fetch(
  155. "/api/ecs_core/org/propertyNoSession/OE_STUDENT_SYS_NAME?domainName=" +
  156. this.schoolDomain
  157. );
  158. const productName = await res.text();
  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. content: "获取机构的客户端设置失败,请退出后重试!",
  211. duration: 15,
  212. closable: true
  213. });
  214. return;
  215. }
  216. //如果配置中配置了 checkRemoteControl:true
  217. if (electronConfig.otherConfig.checkRemoteControl) {
  218. window.nodeRequire("node-cmd").get("Project1.exe", function() {
  219. var applicationNames = fs.readFileSync(
  220. "remoteApplication.txt",
  221. "utf-8"
  222. );
  223. if (applicationNames && applicationNames.trim()) {
  224. that.disableLoginBtn = true;
  225. that.$Message.info({
  226. content:
  227. "在考试期间,请关掉" +
  228. applicationNames.trim() +
  229. "软件,诚信考试。",
  230. duration: 2 * 24 * 60 * 60
  231. });
  232. } else {
  233. that.disableLoginBtn = false;
  234. }
  235. });
  236. } else {
  237. that.disableLoginBtn = false;
  238. }
  239. } else {
  240. this.disableLoginBtn = false;
  241. }
  242. },
  243. methods: {
  244. ...mapMutations(["updateUser", "updateTimeDifference"]),
  245. async login() {
  246. if (this.disableLoginBtn) {
  247. return;
  248. }
  249. await this.checkNewVersion();
  250. this.disableLoginBtn = true;
  251. setTimeout(() => (this.disableLoginBtn = false), 5000);
  252. // https://www.cnblogs.com/weiqinl/p/6708993.html
  253. const valid = await this.$refs.loginForm.validate();
  254. if (valid) {
  255. console.log("form validated. start login...");
  256. } else {
  257. return;
  258. }
  259. let repPara = this.loginForm;
  260. // 以下网络请求失败,直接报网络异常错误
  261. const response = await this.$http.post("/api/ecs_core/auth/login", {
  262. ...repPara,
  263. accountType: this.loginType,
  264. domain: this.schoolDomain,
  265. alwaysOK: true
  266. });
  267. let data = response.data;
  268. // if (
  269. // Math.abs() >
  270. // 5 * 60 * 1000
  271. // ) {
  272. // window._hmt.push(["_trackEvent", "登录页面", "本机时间误差过大"]);
  273. // this.$Message.error({
  274. // content: "与服务器时间差异超过5分钟,请校准本机时间之后再重试!",
  275. // duration: 30
  276. // });
  277. // throw "与服务器时间差异超过5分钟,请校准本机时间之后再重试!";
  278. // }
  279. this.updateTimeDifference(moment(response.headers.date).diff(moment()));
  280. if (data.code == "200") {
  281. data = data.content;
  282. this.errorInfo = "";
  283. //缓存用户信息
  284. window.sessionStorage.setItem("token", data.token);
  285. window.localStorage.setItem("key", data.key);
  286. window.localStorage.setItem("domain", this.schoolDomain);
  287. try {
  288. const student = (await this.$http.get(
  289. "/api/ecs_core/student/getStudentInfoBySession"
  290. )).data;
  291. const specialty = (await this.$http.get(
  292. "/api/ecs_exam_work/exam_student/specialtyNameList/"
  293. )).data;
  294. const user = { ...data, ...student, specialty: specialty.join() };
  295. this.updateUser(user);
  296. window.localStorage.setItem("user-for-reload", JSON.stringify(user));
  297. window._hmt.push([
  298. "_trackEvent",
  299. "登录页面",
  300. "登录",
  301. this.$route.query.LogoutReason
  302. ]);
  303. await this.checkExamInProgress();
  304. window._hmt.push(["_trackEvent", "登录页面", "登录成功"]);
  305. } catch (error) {
  306. window._hmt.push([
  307. "_trackEvent",
  308. "登录页面",
  309. "登录失败",
  310. "getStudentInfoBySession失败"
  311. ]);
  312. this.$Message.error({
  313. content: "获取学生信息失败,请重试!",
  314. duration: 15,
  315. closable: true
  316. });
  317. }
  318. } else {
  319. window._hmt.push(["_trackEvent", "登录页面", "登录失败", data.desc]);
  320. this.errorInfo = data.desc;
  321. }
  322. },
  323. async checkExamInProgress() {
  324. try {
  325. // 断点续考
  326. const examingRes = (await this.$http.get(
  327. "/api/ecs_oe_student/examControl/checkExamInProgress"
  328. )).data;
  329. if (examingRes.isExceed) {
  330. // 超出断点续考次数的逻辑,仅此block
  331. this.$Spin.show({
  332. render: () => {
  333. return (
  334. <div style="font-size: 24px">
  335. `超出最大断点续考次数(${examingRes.maxInterruptNum}
  336. ),正在自动交卷...`
  337. </div>
  338. );
  339. }
  340. });
  341. const res = await this.$http.get(
  342. "/api/ecs_oe_student/examControl/endExam"
  343. );
  344. if (res.status === 200) {
  345. this.$router.replace({
  346. path: `/online-exam/exam/${examingRes.examId}/examRecordData/${
  347. examingRes.examRecordDataId
  348. }/end`
  349. });
  350. this.$Spin.hide();
  351. } else {
  352. this.$Message.error({
  353. content: "交卷失败",
  354. duration: 15,
  355. closable: true
  356. });
  357. }
  358. return;
  359. }
  360. if (examingRes) {
  361. this.$Spin.show({
  362. render: () => {
  363. return <div style="font-size: 24px">正在进入断点续考...</div>;
  364. }
  365. });
  366. window._hmt.push(["_trackEvent", "登录页面", "断点续考", "重新登录"]);
  367. this.$router.push(
  368. `/online-exam/exam/${examingRes.examId}/examRecordData/${
  369. examingRes.examRecordDataId
  370. }/order/1` +
  371. (examingRes.faceVerifyMinute
  372. ? `?faceVerifyMinute=${examingRes.faceVerifyMinute}`
  373. : "")
  374. );
  375. setTimeout(() => this.$Spin.hide(), 1000);
  376. return;
  377. }
  378. this.$router.push("/online-exam");
  379. } catch (error) {
  380. this.$Message.error({
  381. content: "获取断点续考信息异常,退出登录",
  382. duration: 15,
  383. closable: true
  384. });
  385. this.logout("?LogoutReason=登录页面获取断点续考信息异常");
  386. return;
  387. }
  388. },
  389. async checkNewVersion() {
  390. let myHeaders = new Headers();
  391. myHeaders.append("Content-Type", "application/javascript");
  392. myHeaders.append("Cache-Control", "no-cache");
  393. const response = await fetch(
  394. document.scripts[document.scripts.length - 1].src,
  395. {
  396. headers: myHeaders
  397. }
  398. );
  399. if (!response.ok) {
  400. window._hmt.push([
  401. "_trackEvent",
  402. "登录页面",
  403. "新版本发布后,客户端自动刷新"
  404. ]);
  405. location.reload(true);
  406. }
  407. },
  408. closeApp() {
  409. window.close();
  410. }
  411. },
  412. computed: {
  413. logoPath() {
  414. return "/api/ecs_core/org/logo?domain=" + this.domainInUrl;
  415. },
  416. schoolDomain() {
  417. const domain = this.domainInUrl;
  418. if (!domain || !domain.includes("qmth.com.cn")) {
  419. this.$Message.error({
  420. content: "机构地址出错,请关闭程序后再登录。",
  421. duration: 15,
  422. closable: true
  423. });
  424. }
  425. return domain;
  426. },
  427. usernameInputPlaceholder() {
  428. if (this.loginType === "STUDENT_CODE") {
  429. return "请输入学号";
  430. } else {
  431. return "请输入身份证号";
  432. }
  433. },
  434. passwordInputPlaceholder() {
  435. if (this.loginType === "STUDENT_CODE") {
  436. return "初始密码为身份证号后6位";
  437. } else {
  438. return "初始密码为身份证号后6位";
  439. }
  440. }
  441. }
  442. };
  443. </script>
  444. <style scoped>
  445. .home {
  446. display: flex;
  447. flex-direction: column;
  448. height: 100vh;
  449. }
  450. .school-logo {
  451. justify-self: flex-start;
  452. margin-left: 100px;
  453. }
  454. .logo-size {
  455. height: 100px;
  456. width: 400px;
  457. object-fit: cover;
  458. }
  459. .header {
  460. min-height: 120px;
  461. display: grid;
  462. align-items: center;
  463. justify-items: center;
  464. }
  465. .center {
  466. background-image: url("./bg.jpg");
  467. background-position: center;
  468. background-repeat: no-repeat;
  469. background-size: cover;
  470. width: 100vw;
  471. min-height: 600px;
  472. }
  473. .content {
  474. margin-top: 100px;
  475. margin-left: 65%;
  476. width: 300px;
  477. border-radius: 6px;
  478. background-color: white;
  479. display: grid;
  480. grid-template-areas: "";
  481. }
  482. .login-type {
  483. flex: 1;
  484. line-height: 40px;
  485. background-color: #eeeeee;
  486. }
  487. .active-type {
  488. background-color: #ffffff;
  489. }
  490. .close {
  491. position: absolute;
  492. top: 0;
  493. right: 0;
  494. background-color: #eeeeee;
  495. color: #999999;
  496. width: 80px;
  497. height: 40px;
  498. line-height: 40px;
  499. }
  500. .close:hover {
  501. color: #444444;
  502. }
  503. </style>
  504. <style>
  505. .ivu-message-notice-content-text {
  506. font-size: 32px;
  507. }
  508. .ivu-message-notice-content-text i.ivu-icon {
  509. font-size: 32px;
  510. }
  511. </style>