优化顶栏导航与登录请求体验
This commit is contained in:
@@ -18,18 +18,19 @@ describe("Login protocol agreement", () => {
|
||||
expect(protocolGuardIndex).toBeLessThan(loginCallIndex);
|
||||
});
|
||||
|
||||
it("offers remember password through browser credentials without local password storage", () => {
|
||||
it("does not access browser password credentials on the login page", () => {
|
||||
const source = readLoginView();
|
||||
|
||||
expect(source).toContain('v-model="form.rememberPassword"');
|
||||
expect(source).toContain("记住密码");
|
||||
expect(source).toContain('autocomplete="username"');
|
||||
expect(source).toContain('name="username"');
|
||||
expect(source).toContain(":name=\"form.rememberPassword ? 'password' : 'ctms-login-password'\"");
|
||||
expect(source).toContain("tryLoadBrowserCredential");
|
||||
expect(source).toContain("tryStoreBrowserCredential");
|
||||
expect(source).toContain("navigator.credentials");
|
||||
expect(source).toContain("password: true");
|
||||
expect(source).toContain('name="ctms-login-password"');
|
||||
expect(source).toContain('autocomplete="new-password"');
|
||||
expect(source).not.toContain('v-model="form.rememberPassword"');
|
||||
expect(source).not.toContain("记住密码");
|
||||
expect(source).not.toContain("tryLoadBrowserCredential");
|
||||
expect(source).not.toContain("tryStoreBrowserCredential");
|
||||
expect(source).not.toContain("navigator.credentials");
|
||||
expect(source).not.toContain("PasswordCredential");
|
||||
expect(source).not.toContain('localStorage.setItem("ctms_saved_password"');
|
||||
expect(source).not.toContain("localStorage.setItem('ctms_saved_password'");
|
||||
});
|
||||
@@ -54,11 +55,11 @@ describe("Login protocol agreement", () => {
|
||||
expect(source).toContain("confirmProtocol");
|
||||
});
|
||||
|
||||
it("avoids browser password caching when remember password is off", () => {
|
||||
it("avoids browser password caching", () => {
|
||||
const source = readLoginView();
|
||||
|
||||
expect(source).toContain(":name=\"form.rememberPassword ? 'password' : 'ctms-login-password'\"");
|
||||
expect(source).toContain(":autocomplete=\"form.rememberPassword ? 'current-password' : 'new-password'\"");
|
||||
expect(source).toContain('name="ctms-login-password"');
|
||||
expect(source).toContain('autocomplete="new-password"');
|
||||
expect(source).not.toContain('autocomplete="current-password"');
|
||||
});
|
||||
|
||||
|
||||
@@ -80,8 +80,8 @@
|
||||
placeholder="请输入密码"
|
||||
show-password
|
||||
size="large"
|
||||
:name="form.rememberPassword ? 'password' : 'ctms-login-password'"
|
||||
:autocomplete="form.rememberPassword ? 'current-password' : 'new-password'"
|
||||
name="ctms-login-password"
|
||||
autocomplete="new-password"
|
||||
class="login-input"
|
||||
>
|
||||
<template #prefix>
|
||||
@@ -94,9 +94,6 @@
|
||||
</el-form-item>
|
||||
|
||||
<div class="login-options">
|
||||
<el-checkbox v-model="form.rememberPassword" class="remember-checkbox">
|
||||
<span class="remember-text">记住密码</span>
|
||||
</el-checkbox>
|
||||
<el-checkbox v-model="form.agreeProtocol" class="protocol-checkbox">
|
||||
<span class="protocol-text">
|
||||
我已阅读并同意
|
||||
@@ -160,17 +157,12 @@ import { authProtocolSections } from "../content/authProtocol";
|
||||
|
||||
const auth = useAuthStore();
|
||||
const router = useRouter();
|
||||
const REMEMBER_PASSWORD_KEY = "ctms_remember_password";
|
||||
const AGREE_PROTOCOL_KEY = "ctms_agree_protocol";
|
||||
|
||||
type PasswordCredentialConstructor = new (data: { id: string; password: string; name?: string }) => Credential;
|
||||
type StoredPasswordCredential = Credential & { id?: string; password?: string };
|
||||
|
||||
const formRef = ref<FormInstance>();
|
||||
const form = reactive({
|
||||
email: "",
|
||||
password: "",
|
||||
rememberPassword: false,
|
||||
agreeProtocol: false,
|
||||
});
|
||||
|
||||
@@ -210,9 +202,7 @@ onMounted(async () => {
|
||||
};
|
||||
}
|
||||
form.email = localStorage.getItem("ctms_last_login_email") || "";
|
||||
form.rememberPassword = localStorage.getItem(REMEMBER_PASSWORD_KEY) === "true";
|
||||
form.agreeProtocol = localStorage.getItem(AGREE_PROTOCOL_KEY) === "true";
|
||||
await tryLoadBrowserCredential();
|
||||
});
|
||||
|
||||
watch(
|
||||
@@ -222,40 +212,6 @@ watch(
|
||||
},
|
||||
);
|
||||
|
||||
const tryLoadBrowserCredential = async () => {
|
||||
if (!form.rememberPassword || !navigator.credentials?.get) return;
|
||||
|
||||
try {
|
||||
const credential = (await navigator.credentials.get({
|
||||
password: true,
|
||||
mediation: "optional",
|
||||
} as CredentialRequestOptions)) as StoredPasswordCredential | null;
|
||||
if (!credential?.password) return;
|
||||
form.email = credential.id || form.email;
|
||||
form.password = credential.password;
|
||||
} catch {
|
||||
// 浏览器可能不支持读取密码凭据,保留用户手动输入流程。
|
||||
}
|
||||
};
|
||||
|
||||
const tryStoreBrowserCredential = async (email: string, password: string) => {
|
||||
if (!form.rememberPassword) {
|
||||
localStorage.removeItem(REMEMBER_PASSWORD_KEY);
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.setItem(REMEMBER_PASSWORD_KEY, "true");
|
||||
const PasswordCredential = (window as typeof window & { PasswordCredential?: PasswordCredentialConstructor })
|
||||
.PasswordCredential;
|
||||
if (!navigator.credentials?.store || !PasswordCredential) return;
|
||||
|
||||
try {
|
||||
await navigator.credentials.store(new PasswordCredential({ id: email, password, name: email }));
|
||||
} catch {
|
||||
// 浏览器或用户可能拒绝保存凭据,不影响登录主流程。
|
||||
}
|
||||
};
|
||||
|
||||
const openProtocolDialog = () => {
|
||||
protocolDialogVisible.value = true;
|
||||
};
|
||||
@@ -276,7 +232,6 @@ const onSubmit = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
await auth.login(form.email, form.password);
|
||||
await tryStoreBrowserCredential(form.email, form.password);
|
||||
const studyStore = useStudyStore();
|
||||
const userKey = auth.user?.email || form.email;
|
||||
await studyStore.restoreStudyForUser(userKey, { preferActive: !!auth.user?.is_admin });
|
||||
@@ -511,7 +466,6 @@ const onSubmit = async () => {
|
||||
margin: -4px 0 26px;
|
||||
}
|
||||
|
||||
.remember-checkbox,
|
||||
.protocol-checkbox {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
@@ -519,12 +473,10 @@ const onSubmit = async () => {
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
.remember-checkbox :deep(.el-checkbox__input),
|
||||
.protocol-checkbox :deep(.el-checkbox__input) {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.remember-checkbox :deep(.el-checkbox__label),
|
||||
.protocol-checkbox :deep(.el-checkbox__label) {
|
||||
display: block;
|
||||
padding-left: 10px;
|
||||
@@ -532,13 +484,6 @@ const onSubmit = async () => {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.remember-text {
|
||||
color: #52657b;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.protocol-text {
|
||||
color: #475569;
|
||||
font-size: 12px;
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const readSource = () => readFileSync(resolve(__dirname, "./SystemMonitoringPage.vue"), "utf8");
|
||||
|
||||
describe("SystemMonitoringPage", () => {
|
||||
it("uses a fixed viewport shell so monitoring tabs manage their own scrolling", () => {
|
||||
const source = readSource();
|
||||
|
||||
expect(source).toContain("display: flex");
|
||||
expect(source).toContain("height: calc(100dvh - 48px)");
|
||||
expect(source).toContain("min-height: 0");
|
||||
expect(source).toContain("overflow: hidden");
|
||||
expect(source).not.toContain("min-height: calc(100dvh - 52px)");
|
||||
});
|
||||
});
|
||||
@@ -10,9 +10,13 @@ import PermissionMonitoring from "@/components/PermissionMonitoring.vue";
|
||||
|
||||
<style scoped>
|
||||
.system-monitoring-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 48px);
|
||||
height: calc(100dvh - 48px);
|
||||
min-height: 0;
|
||||
margin: -6px -8px;
|
||||
min-height: calc(100vh - 52px);
|
||||
min-height: calc(100dvh - 52px);
|
||||
overflow: hidden;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user