Pārlūkot izejas kodu

登录页-基本

Michael Wang 3 gadi atpakaļ
vecāks
revīzija
798ac64169

+ 11 - 2
src/constants/constants.ts

@@ -1,7 +1,16 @@
 export const YYYYMMDDHHmmss = "YYYY-MM-DD HH:mm:ss";
+const env = import.meta.env;
+export const VITE_SLS_STORE_NAME = env.VITE_SLS_STORE_NAME as string;
+export const VITE_CONFIG_FILE_SEVER_URL = env.VITE_CONFIG_FILE_SEVER_URL;
+export const VITE_GIT_REPO_VERSION = env.DEV
+  ? "开发中..."
+  : (env.VITE_GIT_REPO_VERSION as string);
 
-export const VITE_SLS_STORE_NAME = import.meta.env
-  .VITE_SLS_STORE_NAME as string;
+const domainCandidate =
+  window.location.hostname.split(".")[0] + ".ecs.qmth.com.cn";
+export const DOMAIN = env.DEV
+  ? (env.VITE_DEVELOPMENT_DOMAIN as string)
+  : domainCandidate;
 
 /** 限流请求的服务器 */
 export const LIMIT_SERVER = "https://tcc.qmth.com.cn";

+ 314 - 31
src/features/UserLogin/UserLogin.vue

@@ -7,6 +7,12 @@ import {
 import { store } from "@/store/store";
 import { createUserDetailLog } from "@/utils/logger";
 import { useRouter } from "vue-router";
+import { getElectronConfig } from "./useElectronConfig";
+import { getGeeTestConfig } from "./useGeeTestConfig";
+import { DOMAIN, VITE_GIT_REPO_VERSION } from "@/constants/constants";
+import { onMounted, onUnmounted, watch } from "vue";
+import { Person, LockClosed, CloseCircleOutline } from "@vicons/ionicons5";
+import { FormRules, FormItemInst } from "naive-ui";
 
 logger({
   cnl: ["console", "local", "server"],
@@ -14,35 +20,124 @@ logger({
   act: "首次渲染",
 });
 
-const accountType = $ref("STUDENT_CODE");
-const accountValue = $ref("");
-const password = $ref("");
-// console.log(import.meta.env);
-const domain = import.meta.env.DEV
-  ? (import.meta.env.VITE_DEVELOPMENT_DOMAIN as string)
-  : "";
-const rootOrgId = 0;
+let newVersionAvailable = $ref(false);
+const checkNewVersionListener = () => {
+  newVersionAvailable = true;
+};
+
+onMounted(() =>
+  document.addEventListener("__newSWAvailable", checkNewVersionListener, {
+    once: true,
+  })
+);
+
+onUnmounted(() =>
+  document.removeEventListener("__newSWAvailable", checkNewVersionListener)
+);
+
+let isGeeTestEnabled = $ref(false);
+onMounted(async () => {
+  const conf = await getElectronConfig();
+  store.QECSConfig = conf;
+
+  isGeeTestEnabled = await getGeeTestConfig(store.QECSConfig.ROOT_ORG_ID);
+});
+
+const QECSConfig = store.QECSConfig;
+
+const logoPath = $computed(() => store.QECSConfig.LOGO_FILE_URL || "");
+const backgroundUrl = $computed(
+  () =>
+    `url(${
+      store.QECSConfig.STUDENT_CLIENT_BG_PICTURE_URL ||
+      "https://cdn.qmth.com.cn/ui/ecs-client-bg.jpg!/progressive/true"
+    })`
+);
+const productName = $computed(
+  () => store.QECSConfig.OE_STUDENT_SYS_NAME || "远程教育网络考试"
+);
+const allowLoginType = $computed(() => store.QECSConfig.LOGIN_TYPE ?? []);
+// const isGeeTestEnabled = $computed(() => {
+//   const thisOrg = this.GeeTestConfig[QECSConfig.ROOT_ORG_ID];
+//   const isThisOrgUndefined = thisOrg === undefined;
+//   const allOrg = this.GeeTestConfig["-1"];
+//   return isThisOrgUndefined ? allOrg : thisOrg;
+// });
+
+const domain = DOMAIN;
+
+let disableLoginBtn = $computed(
+  () => false
+  // (isElectron() &&
+  //   (this.disableLoginBtnBecauseRemoteApp ||
+  //     this.disableLoginBtnBecauseVCam)) ||
+  // this.disableLoginBtnBecauseAppVersionChecker ||
+  // this.disableLoginBtnBecauseRefreshServiceWorker ||
+  // this.disableLoginBtnBecauseNotAllowedNative
+);
+
+let loginBtnLoading = $ref(false);
+
+type FormModel = {
+  accountType: "STUDENT_CODE" | "IDENTITY_NUMBER";
+  accountValue: string;
+  password: string;
+  domain: string;
+  rootOrgId: string;
+};
+const formRef: FormItemInst = $ref();
+const formValue: FormModel = $ref({
+  accountType: "STUDENT_CODE",
+  accountValue: "",
+  password: "",
+  domain,
+  rootOrgId: "",
+});
+
+const fromRules: FormRules = {
+  accountValue: {
+    required: true,
+    trigger: "blur",
+    message: "账号必填",
+  },
+  password: {
+    required: true,
+    trigger: "blur",
+    message: "密码必填",
+  },
+};
+
+let errorInfo = $ref("");
+watch([formValue], () => (errorInfo = ""));
 
 const router = useRouter();
 
-async function handleLoginClick() {
+async function loginForuser() {
+  if (await formRef.validate().catch(() => true)) return;
+
+  logger({
+    pgn: "登录页面",
+    act: "login clicked",
+    ext: { UA: navigator.userAgent },
+  });
+  errorInfo = "";
   const res = await loginApi(
-    accountType,
-    accountValue,
-    password,
+    formValue.accountType,
+    formValue.accountValue,
+    formValue.password,
     domain,
-    rootOrgId
+    store.QECSConfig.ROOT_ORG_ID
   );
-  console.log(res);
+
   if (res.data.code === "200") {
+    errorInfo = "";
     // 准备下面的登录token
     store.user = res.data.content;
   } else {
-    // TODO: 放置在登录框
-    $message.error(res.data.desc);
+    errorInfo = res.data.desc;
     logger({
       pgu: "AUTO",
-      act: "点击登录",
+      act: "点击登录-res-error",
       stk: res.data.code + res.data.desc,
       cnl: ["console", "server"],
     });
@@ -65,23 +160,211 @@ async function handleLoginClick() {
 
   void router.push({ name: "ChangePassword" });
 }
+
+function closeApp() {
+  logger({
+    pgu: "AUTO",
+    cnl: ["local", "server"],
+    key: "退出应用",
+    act: "点击关闭按钮",
+  });
+  window.close();
+}
 </script>
 
 <template>
-  <div>login</div>
-  <div>
-    <n-form>
-      <n-form-item label="学号">
-        <n-input v-model:value="accountValue" clearable />
-      </n-form-item>
-      <n-form-item label="密码">
-        <n-input v-model:value="password" clearable />
-      </n-form-item>
-      <n-form-item>
-        <n-button @click="handleLoginClick"> 登录 </n-button>
-      </n-form-item>
-    </n-form>
+  <div class="tw-flex tw-flex-col tw-h-full">
+    <header class="header">
+      <div class="school-logo-container">
+        <img
+          v-show="logoPath"
+          class="school-logo"
+          :src="logoPath"
+          alt="school logo"
+          style="
+            background: linear-gradient(to bottom, #38f6f5 0%, #8efdf4 100%);
+          "
+        />
+        <!-- 加上它,在logo加载失败的时候有用 -->
+        <!-- @load="(e: any) => (e.target.style = '')" -->
+      </div>
+      <a class="close" @click="closeApp"> 关闭 </a>
+    </header>
+
+    <div class="center" :style="{ backgroundImage: backgroundUrl }">
+      <div class="content">
+        <div class="login-types qm-big-text tw-flex tw-overflow-clip">
+          <a
+            v-if="allowLoginType.includes('STUDENT_CODE')"
+            key="STUDENT_CODE"
+            :class="[formValue.accountType === 'STUDENT_CODE' && 'active-type']"
+            @click="formValue.accountType = 'STUDENT_CODE'"
+          >
+            {{ QECSConfig.STUDENT_CODE_LOGIN_ALIAS }}
+          </a>
+          <a
+            v-if="allowLoginType.includes('IDENTITY_NUMBER')"
+            key="IDENTITY_NUMBER"
+            :class="[formValue.accountType !== 'STUDENT_CODE' && 'active-type']"
+            @click="formValue.accountType = 'IDENTITY_NUMBER'"
+          >
+            {{ QECSConfig.IDENTITY_NUMBER_LOGIN_ALIAS }}
+          </a>
+          <a v-if="allowLoginType.length === 0" key="loading"> loading... </a>
+        </div>
+
+        <div class="qm-title-text tw-text-center tw-mt-10">
+          {{ productName }}
+        </div>
+
+        <div class="tw-mx-10">
+          <n-form ref="formRef" :model="formValue" :rules="fromRules">
+            <n-form-item class="form-item-style" path="accountValue">
+              <n-input v-model:value="formValue.accountValue">
+                <template #prefix>
+                  <n-icon :component="Person" />
+                </template>
+              </n-input>
+            </n-form-item>
+            <n-form-item
+              prop="password"
+              class="form-item-style"
+              path="password"
+            >
+              <n-input
+                v-model:value="formValue.password"
+                type="password"
+                @onEnter="loginForuser"
+              >
+                <template #prefix>
+                  <n-icon :component="LockClosed" />
+                </template>
+              </n-input>
+            </n-form-item>
+
+            <n-form-item
+              v-if="isGeeTestEnabled"
+              class="form-item-style"
+              style="height: 40px; margin-top: 0px"
+            >
+              <!-- <GeeTest :reset="resetGeeTime" @onLoad="handleGtResult" /> -->
+            </n-form-item>
+
+            <div
+              v-show="errorInfo"
+              class="tw-flex tw-items-center tw-text-red-900"
+            >
+              <n-icon :component="CloseCircleOutline" size="large" />
+              {{ errorInfo }}
+            </div>
+            <n-form-item class="tw-mb-8">
+              <n-button
+                type="success"
+                size="large"
+                style="width: 100%"
+                :disabled="disableLoginBtn"
+                :loading="loginBtnLoading"
+                @click="loginForuser"
+              >
+                {{ newVersionAvailable ? "点击更新版本" : "登录" }}
+              </n-button>
+            </n-form-item>
+          </n-form>
+        </div>
+      </div>
+    </div>
+
+    <footer class="footer">
+      <div style="position: absolute; right: 20px; bottom: 20px">
+        版本: {{ VITE_GIT_REPO_VERSION }}
+      </div>
+    </footer>
+    <!-- <GlobalNotice /> -->
   </div>
 </template>
 
-<style scoped></style>
+<style scoped>
+.header {
+  display: flex;
+  align-items: center;
+  min-height: 120px;
+}
+.school-logo-container {
+  justify-self: flex-start;
+  margin-left: 100px;
+}
+
+.school-logo {
+  height: 100px;
+  width: 400px;
+  object-fit: cover;
+}
+
+.close {
+  position: absolute;
+  top: 0;
+  right: 0;
+  background-color: #eeeeee;
+  color: #999999;
+  text-align: center;
+  width: 80px;
+  height: 40px;
+  line-height: 40px;
+  border-bottom-left-radius: 6px;
+}
+
+.close:hover {
+  color: #444444;
+  cursor: pointer;
+}
+
+.center {
+  background-position: center;
+  background-repeat: no-repeat;
+  background-size: cover;
+  width: 100vw;
+  min-height: 600px;
+}
+
+.content {
+  margin-top: 100px;
+  margin-left: 60%;
+  width: 340px;
+  border-radius: 6px;
+  overflow: hidden;
+  background-color: white;
+}
+
+/* FIXME: 样式复用候选 */
+.qm-big-text {
+  font-size: 16px;
+  font-weight: normal;
+  font-stretch: normal;
+  line-height: 22px;
+  letter-spacing: 0px;
+  color: #999999;
+}
+.qm-title-text {
+  font-size: 18px;
+  font-weight: bold;
+  font-stretch: normal;
+  line-height: 24px;
+  letter-spacing: 0px;
+  color: #444444;
+}
+
+.login-types a {
+  flex: 1;
+  line-height: 40px;
+  background-color: #eeeeee;
+  text-align: center;
+}
+.login-types a.active-type {
+  background-color: #ffffff;
+}
+
+.form-item-style {
+  margin-bottom: 30px;
+  height: 42px;
+}
+</style>

+ 41 - 0
src/features/UserLogin/useElectronConfig.ts

@@ -0,0 +1,41 @@
+import { DOMAIN, VITE_CONFIG_FILE_SEVER_URL } from "@/constants/constants";
+import { httpNoAuth } from "@/plugins/axiosNoAuth";
+import { Store } from "@/types/student-client";
+
+export async function getElectronConfig(): Promise<Store["QECSConfig"]> {
+  try {
+    const res = await httpNoAuth.get(
+      VITE_CONFIG_FILE_SEVER_URL +
+        "/org_properties/byOrgDomain/" +
+        DOMAIN +
+        "/studentClientConfig.json",
+      { noErrorMessage: true, "axios-retry": { retries: 3 } }
+    );
+    // console.log(res.data);
+    res.data.ROOT_ORG_ID = JSON.parse((res.data.ROOT_ORG_ID as string) || "-1");
+    res.data.IS_CUSTOM_MENU_LOGO = JSON.parse(
+      (res.data.IS_CUSTOM_MENU_LOGO as string) || "false"
+    );
+    res.data.LOGIN_TYPE = ((res.data.LOGIN_TYPE as string) || "[]").split(",");
+    res.data.PREVENT_CHEATING_CONFIG = (
+      (res.data.PREVENT_CHEATING_CONFIG as string) || ""
+    ).split(",");
+    // console.log(res.data);
+
+    return res.data as Store["QECSConfig"];
+  } catch (e) {
+    logger({
+      cnl: ["console", "local", "server"],
+      pgu: "AUTO",
+      ejn: JSON.stringify(e),
+      stk: e instanceof Error ? e.stack : "not Error object." + e,
+      dtl: "获取远程配置文件出错,请重新打开程序!",
+      act: "fetch studentClientConfig.json",
+    });
+    $message.error("获取远程配置文件出错,请重新打开程序!", {
+      duration: 15 * 1000,
+      closable: true,
+    });
+    throw e;
+  }
+}

+ 32 - 0
src/features/UserLogin/useGeeTestConfig.ts

@@ -0,0 +1,32 @@
+import { VITE_CONFIG_FILE_SEVER_URL } from "@/constants/constants";
+import { httpNoAuth } from "@/plugins/axiosNoAuth";
+
+export async function getGeeTestConfig(rootOrgId: number): Promise<boolean> {
+  try {
+    const res = await httpNoAuth.get(
+      VITE_CONFIG_FILE_SEVER_URL +
+        "/org_properties/geetestConfig.json" +
+        "?" +
+        Date.now() +
+        Math.random(),
+      { noErrorMessage: true, "axios-retry": { retries: 3 } }
+    );
+    // console.log(res.data);
+    const gConf: Record<string, boolean> = res.data;
+    return gConf["" + rootOrgId] ?? gConf["-1"];
+  } catch (e) {
+    logger({
+      cnl: ["console", "local", "server"],
+      pgu: "AUTO",
+      ejn: JSON.stringify(e),
+      stk: e instanceof Error ? e.stack : "not Error object." + e,
+      dtl: "获取远程配置文件出错,请重新打开程序!",
+      act: "fetch geetestConfig.json",
+    });
+    $message.error("获取远程配置文件出错,请重新打开程序!", {
+      duration: 15 * 1000,
+      closable: true,
+    });
+    throw e;
+  }
+}