fix(auth): 支持无邮箱后缀时手动输入
Client Quality Gates / Shared client and Web (push) Has been cancelled
Client Quality Gates / macOS Desktop (push) Has been cancelled

This commit is contained in:
Cheng Zhou
2026-07-01 14:37:32 +08:00
parent 45ed1d7f36
commit 593c60c782
2 changed files with 61 additions and 11 deletions
+7 -2
View File
@@ -96,12 +96,17 @@ describe("Login protocol agreement", () => {
expect(source).toContain("max-width: calc(100vw - 40px);"); expect(source).toContain("max-width: calc(100vw - 40px);");
}); });
it("uses only server-configured email domains on web and desktop", () => { it("falls back to full email input when no server-configured domains exist", () => {
const source = readLoginView(); const source = readLoginView();
expect(source).toContain('fetchEmailDomains()'); expect(source).toContain('fetchEmailDomains()');
expect(source).toContain(':disabled="availableEmailDomains.length === 0"');
expect(source).toContain("const availableEmailDomains = computed(() => configuredEmailDomains.value)"); expect(source).toContain("const availableEmailDomains = computed(() => configuredEmailDomains.value)");
expect(source).toContain("availableEmailDomains.value.length > 0 && availableEmailDomains.value.includes(form.emailDomain)");
expect(source).toContain("const manualEmailInput = computed(() => !hasConfiguredEmailDomains.value)");
expect(source).toContain('v-if="hasConfiguredEmailDomains"');
expect(source).toContain("未检测到邮箱后缀配置,请输入完整邮箱地址。");
expect(source).toContain("form.email = local;");
expect(source).toContain("if (domains.length === 0)");
expect(source).toContain("configuredEmailDomains.value.includes(domain)"); expect(source).toContain("configuredEmailDomains.value.includes(domain)");
expect(source).not.toContain("preservedEmailDomain"); expect(source).not.toContain("preservedEmailDomain");
expect(source).not.toContain("showDomainSelect"); expect(source).not.toContain("showDomainSelect");
+54 -9
View File
@@ -145,19 +145,19 @@
<!-- 登录表单 --> <!-- 登录表单 -->
<el-form ref="formRef" :model="form" :rules="rules" @keyup.enter="onSubmit" label-position="top" class="login-form"> <el-form ref="formRef" :model="form" :rules="rules" @keyup.enter="onSubmit" label-position="top" class="login-form">
<el-form-item label="账号" prop="email"> <el-form-item label="账号" prop="email">
<div class="email-unified-box"> <div class="email-unified-box" :class="{ 'email-unified-box--manual': manualEmailInput }">
<input <input
id="email" id="email"
v-model="form.emailLocal" v-model="form.emailLocal"
type="text" :type="manualEmailInput ? 'email' : 'text'"
placeholder="请输入账号" :placeholder="emailInputPlaceholder"
name="username" name="username"
autocomplete="username" autocomplete="username"
class="email-local-field" class="email-local-field"
@paste="handleAccountPaste" @paste="handleAccountPaste"
/> />
<span class="email-at-sign">@</span> <span v-if="hasConfiguredEmailDomains" class="email-at-sign">@</span>
<div class="email-domain-wrapper"> <div v-if="hasConfiguredEmailDomains" class="email-domain-wrapper">
<select <select
v-model="form.emailDomain" v-model="form.emailDomain"
class="email-domain-field" class="email-domain-field"
@@ -169,6 +169,7 @@
<svg class="email-domain-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="m6 9 6 6 6-6"/></svg> <svg class="email-domain-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="m6 9 6 6 6-6"/></svg>
</div> </div>
</div> </div>
<p v-if="manualEmailInput" class="email-manual-hint">未检测到邮箱后缀配置请输入完整邮箱地址</p>
</el-form-item> </el-form-item>
<el-form-item label="密码" prop="password"> <el-form-item label="密码" prop="password">
@@ -294,9 +295,19 @@ const refreshDesktopServerUrl = () => {
const normalizeDomain = (value: string) => value.trim().toLowerCase().replace(/^@/, ""); const normalizeDomain = (value: string) => value.trim().toLowerCase().replace(/^@/, "");
const availableEmailDomains = computed(() => configuredEmailDomains.value); const availableEmailDomains = computed(() => configuredEmailDomains.value);
const hasConfiguredEmailDomains = computed(() =>
availableEmailDomains.value.length > 0 && availableEmailDomains.value.includes(form.emailDomain)
);
const manualEmailInput = computed(() => !hasConfiguredEmailDomains.value);
const emailInputPlaceholder = computed(() => manualEmailInput.value ? "请输入完整邮箱" : "请输入账号");
const syncEmailFromParts = () => { const syncEmailFromParts = () => {
const local = form.emailLocal.trim().toLowerCase(); const local = form.emailLocal.trim().toLowerCase();
if (manualEmailInput.value) {
form.emailDomain = "";
form.email = local;
return;
}
const domain = normalizeDomain(form.emailDomain); const domain = normalizeDomain(form.emailDomain);
form.emailDomain = domain; form.emailDomain = domain;
form.email = local && domain ? `${local}@${domain}` : ""; form.email = local && domain ? `${local}@${domain}` : "";
@@ -304,6 +315,12 @@ const syncEmailFromParts = () => {
const applyEmailValue = (email: string) => { const applyEmailValue = (email: string) => {
const normalized = email.trim().toLowerCase(); const normalized = email.trim().toLowerCase();
if (availableEmailDomains.value.length === 0) {
form.emailLocal = normalized;
form.emailDomain = "";
syncEmailFromParts();
return;
}
const separator = normalized.lastIndexOf("@"); const separator = normalized.lastIndexOf("@");
if (separator > 0) { if (separator > 0) {
form.emailLocal = normalized.slice(0, separator); form.emailLocal = normalized.slice(0, separator);
@@ -321,16 +338,32 @@ const applyEmailValue = (email: string) => {
const loadEmailDomains = async () => { const loadEmailDomains = async () => {
try { try {
const { data } = await fetchEmailDomains(); const { data } = await fetchEmailDomains();
configuredEmailDomains.value = Array.from(new Set( const domains = Array.from(new Set(
data.items.map(normalizeDomain).filter(Boolean) (Array.isArray(data.items) ? data.items : [])
.map(item => typeof item === "string" ? normalizeDomain(item) : "")
.filter(Boolean)
)); ));
if (!configuredEmailDomains.value.includes(form.emailDomain)) { configuredEmailDomains.value = domains;
form.emailDomain = configuredEmailDomains.value[0] || "";
if (domains.length === 0) {
form.emailDomain = "";
syncEmailFromParts();
return;
}
if (form.emailLocal.includes("@")) {
applyEmailValue(form.emailLocal);
return;
}
if (!domains.includes(form.emailDomain)) {
form.emailDomain = domains[0];
} }
} catch { } catch {
configuredEmailDomains.value = []; configuredEmailDomains.value = [];
form.emailDomain = ""; form.emailDomain = "";
} }
syncEmailFromParts();
}; };
const handleAccountPaste = (event: ClipboardEvent) => { const handleAccountPaste = (event: ClipboardEvent) => {
@@ -367,6 +400,7 @@ const confirmProtocol = () => { form.agreeProtocol = true; protocolDialogVisible
const onSubmit = async () => { const onSubmit = async () => {
if (!formRef.value) return; if (!formRef.value) return;
syncEmailFromParts();
const valid = await formRef.value.validate(); const valid = await formRef.value.validate();
if (!valid) return; if (!valid) return;
if (!form.agreeProtocol) { ElMessage.warning("请先阅读并同意用户协议"); return; } if (!form.agreeProtocol) { ElMessage.warning("请先阅读并同意用户协议"); return; }
@@ -948,6 +982,10 @@ const onSubmit = async () => {
border-bottom-color: #2563eb; border-bottom-color: #2563eb;
} }
.email-unified-box--manual .email-local-field {
width: 100%;
}
.email-local-field, .email-local-field,
.email-domain-field { .email-domain-field {
min-width: 0; min-width: 0;
@@ -969,6 +1007,13 @@ const onSubmit = async () => {
color: #94a3b8; color: #94a3b8;
} }
.email-manual-hint {
margin: 8px 0 0;
color: #64748b;
font-size: 12px;
line-height: 1.5;
}
.email-at-sign { .email-at-sign {
flex-shrink: 0; flex-shrink: 0;
padding: 0 4px; padding: 0 4px;