Files
opsx-licence/frontend/src/views/Login.vue
T
Logan.Li 4bf7fb6e5c
CI / backend (push) Waiting to run
fix(login): 登录页低调化,去掉品牌与业务描述
- 删除左侧品牌区(含 opsx-licence、License 授权服务控制台、双签发/设备激活追溯/多渠道收款对账三块业务描述)
- 表单文案中性化:「登录控制台」→「登录」,去掉「请使用账号登录」与「连续登录失败会被限流…」提示
- 页面标题改为「登录」,并加 robots noindex,nofollow,noarchive
- 布局改为单卡居中(移动端无横向溢出)
- 保留「记住密码」能力
- 目的:登录页暴露在公网,避免自报服务性质、降低被针对性扫描的概率
2026-11-03 01:07:28 +08:00

338 lines
7.9 KiB
Vue

<template>
<meta name="robots" content="noindex,nofollow,noarchive" />
<div class="login-page">
<!-- 右侧登录区 -->
<main class="form-panel">
<div class="form-card">
<div class="form-head">
<h2>登录</h2>
</div>
<el-form
ref="loginFormRef"
:model="loginForm"
:rules="loginRules"
size="large"
label-position="top"
@keyup.enter="handleLogin"
>
<el-form-item label="用户名" prop="username">
<el-input
v-model="loginForm.username"
placeholder="请输入用户名"
:prefix-icon="User"
autocomplete="username"
clearable
/>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input
v-model="loginForm.password"
type="password"
placeholder="请输入密码"
show-password
:prefix-icon="Lock"
autocomplete="current-password"
/>
</el-form-item>
<div class="form-extra">
<el-checkbox v-model="rememberPassword">记住密码</el-checkbox>
<span class="remember-hint">{{ rememberHint }}</span>
</div>
<el-button
type="primary"
class="submit-btn"
:loading="loading"
@click="handleLogin"
>
{{ loading ? '登录中…' : '登 录' }}
</el-button>
</el-form>
</div>
</main>
</div>
</template>
<script setup>
import { computed, onMounted, reactive, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { User, Lock } from '@element-plus/icons-vue'
import { useUserStore } from '@/stores/user'
const router = useRouter()
const route = useRoute()
const userStore = useUserStore()
const loginFormRef = ref(null)
const loading = ref(false)
const rememberPassword = ref(false)
const loginForm = reactive({
username: '',
password: '',
})
const loginRules = {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
}
const rememberHint = computed(() =>
rememberPassword.value ? '已在本机保存,下次自动填充' : '勾选后本机自动填充账号密码'
)
// ============ 「记住密码」本地存储 ============
// 仅保存在本机浏览器;做一层 XOR + Base64 混淆,避免明文躺在 localStorage 里被随手看到。
// ⚠️ 这是混淆而非加密,公共设备请勿勾选。 // Logan.Li
const REMEMBER_KEY = 'opsx_licence_remember'
const REMEMBER_USERNAME_KEY = 'opsx_licence_last_username'
const XOR_KEY = 0x2f
function obfuscate(text) {
const xored = Array.from(text)
.map((ch) => String.fromCharCode(ch.charCodeAt(0) ^ XOR_KEY))
.join('')
return btoa(encodeURIComponent(xored))
}
function deobfuscate(token) {
const xored = decodeURIComponent(atob(token))
return Array.from(xored)
.map((ch) => String.fromCharCode(ch.charCodeAt(0) ^ XOR_KEY))
.join('')
}
// 读取上次的账号 / 密码 // Logan.Li
function restoreRemembered() {
try {
const lastUser = localStorage.getItem(REMEMBER_USERNAME_KEY)
if (lastUser) loginForm.username = lastUser
const saved = localStorage.getItem(REMEMBER_KEY)
if (!saved) return
const data = JSON.parse(deobfuscate(saved))
if (data?.u) loginForm.username = data.u
if (data?.p) {
loginForm.password = data.p
rememberPassword.value = true
}
} catch {
localStorage.removeItem(REMEMBER_KEY)
}
}
// 保存 / 清理记住的凭据 // Logan.Li
function persistRemembered() {
const username = loginForm.username.trim()
if (username) {
localStorage.setItem(REMEMBER_USERNAME_KEY, username)
}
if (rememberPassword.value && username) {
localStorage.setItem(
REMEMBER_KEY,
obfuscate(JSON.stringify({ u: username, p: loginForm.password }))
)
} else {
localStorage.removeItem(REMEMBER_KEY)
}
}
// 处理登录 // Logan.Li
async function handleLogin() {
if (!loginFormRef.value || loading.value) return
await loginFormRef.value.validate(async (valid) => {
if (!valid) return
loading.value = true
try {
await userStore.login({
username: loginForm.username.trim(),
password: loginForm.password,
})
persistRemembered()
const redirect = typeof route.query.redirect === 'string' ? route.query.redirect : '/'
router.push(redirect)
} catch {
// 拦截器已统一提示错误
} finally {
loading.value = false
}
})
}
onMounted(restoreRemembered)
</script>
<style scoped>
.login-page {
display: flex;
min-height: 100vh;
background: #f5f7fa;
}
/* ============ 左侧品牌区 ============ */
.brand-panel {
position: relative;
flex: 0 0 46%;
max-width: 620px;
overflow: hidden;
background: linear-gradient(150deg, #0b1b3a 0%, #10294f 48%, #0d3b57 100%);
color: #e8eefc;
display: flex;
align-items: center;
}
.brand-glow {
position: absolute;
width: 520px;
height: 520px;
right: -180px;
top: -160px;
border-radius: 50%;
background: radial-gradient(circle, rgba(64, 158, 255, 0.35) 0%, rgba(64, 158, 255, 0) 70%);
pointer-events: none;
}
.brand-inner {
position: relative;
padding: 56px 56px 48px;
width: 100%;
}
.brand-logo {
display: flex;
align-items: center;
gap: 10px;
font-size: 17px;
font-weight: 600;
letter-spacing: 0.02em;
color: #fff;
}
.brand-title {
margin: 34px 0 14px;
font-size: 30px;
line-height: 1.35;
font-weight: 600;
color: #fff;
}
.brand-desc {
margin: 0;
max-width: 30em;
font-size: 14px;
line-height: 1.75;
color: rgba(232, 238, 252, 0.72);
}
.brand-points {
margin: 38px 0 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: 22px;
}
.brand-points li {
padding-left: 18px;
border-left: 2px solid rgba(64, 158, 255, 0.5);
}
.brand-points b {
display: block;
font-size: 14px;
font-weight: 600;
color: #fff;
margin-bottom: 5px;
}
.brand-points span {
font-size: 13px;
line-height: 1.65;
color: rgba(232, 238, 252, 0.6);
}
.brand-foot {
margin-top: 46px;
font-size: 12px;
letter-spacing: 0.02em;
color: rgba(232, 238, 252, 0.38);
}
/* ============ 右侧表单区 ============ */
.form-panel {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 48px 24px;
}
.form-card {
width: 100%;
max-width: 380px;
}
.form-head {
margin-bottom: 26px;
}
.form-head h2 {
margin: 0 0 8px;
font-size: 22px;
font-weight: 600;
color: #1f2d3d;
}
.form-head p {
margin: 0;
font-size: 13px;
color: #8492a6;
}
.form-extra {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin: -4px 0 20px;
}
.remember-hint {
font-size: 12px;
color: #a3adbd;
}
.submit-btn {
width: 100%;
font-size: 15px;
letter-spacing: 0.08em;
}
.form-note {
margin: 18px 0 0;
font-size: 12px;
line-height: 1.6;
color: #a3adbd;
}
/* 表单项标签更紧凑,整体更接近控制台观感 */
.form-card :deep(.el-form-item) {
margin-bottom: 18px;
}
.form-card :deep(.el-form-item__label) {
padding-bottom: 6px;
font-size: 13px;
color: #5a6b83;
}
/* ============ 窄屏:收起品牌区,表单居中 ============ */
@media (max-width: 900px) {
.brand-panel {
display: none;
}
.form-panel {
padding: 32px 20px;
}
.form-head {
text-align: center;
}
}
@media (max-width: 420px) {
.form-card :deep(.el-form-item__label) {
font-size: 12px;
}
}
/* 低调化:取消左右分栏,登录卡居中 // Logan.Li */
.wrap, .login-wrap { grid-template-columns: 1fr !important; display: block !important; }
.brand-panel { display: none !important; }
.form-panel { min-height: 100vh; display: flex !important; align-items: center; justify-content: center; }
</style>