feat(web): 深度定制 Element Plus 主题 — 靛蓝主色/暗色模式/侧栏折叠/面包屑/登录页重绘

- 新增 styles/theme.css:CSS 变量覆盖默认主题(主色 #4F46E5、圆角、阴影、页面背景),html.dark 暗色变量
- 新增 stores/theme.ts:暗色/亮色切换,localStorage 持久化
- main.ts 引入 element-plus 暗色 css-vars 与主题文件,挂载前初始化主题
- index.html 加 SVG favicon 与防暗色闪烁内联脚本
- AdminLayout:深色可折叠侧栏、header 面包屑 + 暗色切换按钮、页面切换过渡
- UserLayout:header 打磨 + 暗色切换按钮
- LoginView/ApplyView:统一品牌渐变背景与卡片,登录页 OTP 输入行内联发送按钮、登录按钮唯一通栏主按钮(修复按钮错位)
This commit is contained in:
2026-08-30 11:35:44 +08:00
parent b305d7b371
commit 472c66ebd8
9 changed files with 2634 additions and 116 deletions
+40 -51
View File
@@ -10,7 +10,8 @@ const router = useRouter()
const auth = useAuthStore()
const tab = ref<'admin' | 'user'>('admin')
const loading = ref(false)
const loading = ref(false) // 登录请求中
const sendingOtp = ref(false) // 发送 OTP 中
// 管理员登录
const adminForm = reactive({ username: '', password: '' })
@@ -70,6 +71,7 @@ async function onSendOtp() {
await loadCaptcha()
if (!captcha.value) return
}
sendingOtp.value = true
try {
await api.otpSend({
username: userForm.username,
@@ -84,6 +86,8 @@ async function onSendOtp() {
} catch {
// 验证码错误等已提示;重新生成一次
await loadCaptcha()
} finally {
sendingOtp.value = false
}
}
@@ -116,11 +120,14 @@ onMounted(() => {
</script>
<template>
<div class="login-page">
<el-card class="login-card" shadow="always">
<div class="brand">
<el-icon :size="30"><Monitor /></el-icon>
<div class="auth-page">
<el-card class="auth-card" shadow="never">
<div class="auth-brand">
<span class="brand-icon">
<el-icon :size="26"><Monitor /></el-icon>
</span>
<h2>服务器用户管理节点</h2>
<span class="sub">账号申请 · SSH 密钥 · 使用审计</span>
</div>
<el-tabs v-model="tab" stretch @tab-change="onTabChange">
@@ -140,7 +147,7 @@ onMounted(() => {
@keyup.enter="onAdminLogin"
/>
</el-form-item>
<el-button type="primary" class="submit" :loading="loading" @click="onAdminLogin">登录</el-button>
<el-button type="primary" class="auth-submit" :loading="loading" @click="onAdminLogin">登录</el-button>
</el-form>
</el-tab-pane>
@@ -151,85 +158,67 @@ onMounted(() => {
<el-input v-model="userForm.username" placeholder="用户名(可带或不带 ext_ 前缀)" autocomplete="username" />
</el-form-item>
<el-form-item label="图形验证码">
<div class="captcha-row">
<div class="inline-row">
<el-input v-model="userForm.captcha_code" placeholder="4 位验证码" maxlength="4" @keyup.enter="onSendOtp" />
<el-image
v-if="captcha"
:src="captcha.image"
fit="contain"
class="captcha-img"
:title="'点击刷新'"
title="点击刷新"
@click="loadCaptcha"
/>
<el-button v-else :loading="captchaLoading" @click="loadCaptcha">获取</el-button>
</div>
</el-form-item>
<el-form-item label="登录验证码(OTP)">
<el-input
v-model="userForm.code"
placeholder="6 位 OTP(邮件或 CLI 获取)"
maxlength="6"
@keyup.enter="onUserLogin"
/>
<div class="inline-row otp-row">
<el-input
v-model="userForm.code"
placeholder="6 位 OTP(邮件或 CLI 获取)"
maxlength="6"
class="otp-input"
@keyup.enter="onUserLogin"
/>
<el-button :loading="sendingOtp" :disabled="cooldown > 0" @click="onSendOtp">
{{ cooldown > 0 ? `${cooldown}s 后重发` : '发送验证码' }}
</el-button>
</div>
</el-form-item>
<el-button type="primary" class="submit" :loading="loading" @click="onSendOtp" :disabled="cooldown > 0">
{{ cooldown > 0 ? `重新发送(${cooldown}s)` : '发送验证码' }}
</el-button>
<el-button type="success" class="submit" :loading="loading" @click="onUserLogin">登录</el-button>
<el-button type="primary" class="auth-submit" :loading="loading" @click="onUserLogin">登录</el-button>
</el-form>
</el-tab-pane>
</el-tabs>
<div class="foot">
<router-link to="/apply">申请新账号</router-link>
<div class="auth-foot">
<router-link to="/apply">还没有账号?申请新账号</router-link>
</div>
</el-card>
</div>
</template>
<style scoped>
.login-page {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #1f2d3d 0%, #2c3e50 100%);
}
.login-card {
width: 400px;
border-radius: 8px;
}
.brand {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
margin-bottom: 8px;
}
.brand h2 {
margin: 0;
font-size: 18px;
}
.captcha-row {
/* 输入框 + 相邻控件同行对齐 */
.inline-row {
display: flex;
gap: 8px;
width: 100%;
}
.otp-row .otp-input {
flex: 1;
min-width: 0;
}
.captcha-img {
width: 110px;
height: 32px;
border: 1px solid var(--el-border-color);
border-radius: 4px;
border-radius: 6px;
cursor: pointer;
flex-shrink: 0;
}
.submit {
width: 100%;
/* tab 与品牌区的间距 */
:deep(.el-tabs) {
margin-top: 4px;
}
.foot {
margin-top: 16px;
text-align: center;
font-size: 13px;
}
</style>