429 lines
13 KiB
Vue
429 lines
13 KiB
Vue
<script setup lang="ts">
|
||
/**
|
||
* F006 登录页
|
||
* W3.7:按 http://localhost:8770/theme-switcher.html?theme=tech-blue 设计稿 1:1 还原
|
||
* 设计稿关键数值:
|
||
* - 舞台 1324×600 圆角 16px
|
||
* - 左 65% (flex 1.857) / 右 35% (flex 1)
|
||
* - input 380×38 圆角 8px 背景 #F8FAFC 边框 #E2E8F0 padding-left 34px(给 icon)
|
||
* - 验证码 110×38 圆角 8px 背景 #DBEAFE 字 17px 600 蓝
|
||
* - 按钮 380×40 圆角 8px 字 14px 600 白 阴影 glow
|
||
* - 复选框 + 忘记密码 + divider + SSO 2 按钮
|
||
*/
|
||
import { reactive, ref } from 'vue'
|
||
import { useRouter, useRoute } from 'vue-router'
|
||
import { ElMessage } from 'element-plus'
|
||
import qs from 'qs'
|
||
import axios from 'axios'
|
||
import { useUserStore } from '@/stores/user'
|
||
import { useThemeStore } from '@/stores/theme'
|
||
import { View, Hide, Lock } from '@element-plus/icons-vue'
|
||
import { computed } from 'vue'
|
||
import logoUrl from '@/assets/logo-jubian.png'
|
||
import logoLightUrl from '@/assets/logo-jubian-light.png'
|
||
|
||
const router = useRouter()
|
||
const route = useRoute()
|
||
const userStore = useUserStore()
|
||
const themeStore = useThemeStore()
|
||
const logoSrc = computed(() => themeStore.current === 'dark' ? logoLightUrl : logoUrl)
|
||
|
||
const showPwd = ref(false)
|
||
const form = reactive({
|
||
username: 'admin',
|
||
password: 'admin',
|
||
captcha: '',
|
||
uuid: ''
|
||
})
|
||
const captchaUrl = ref('')
|
||
const loading = ref(false)
|
||
|
||
const themePalette = [
|
||
{ value: 'tech-blue', name: '科技蓝', color: '#2563EB' },
|
||
{ value: 'red', name: '中国红', color: '#DC2626' },
|
||
{ value: 'purple', name: '深空紫', color: '#5645D4' },
|
||
{ value: 'dark', name: '暗夜黑', color: '#0F172A' }
|
||
]
|
||
|
||
function refreshCaptcha() {
|
||
form.uuid = Math.random().toString(36).slice(2, 12)
|
||
captchaUrl.value = `/api/auth/captcha?uuid=${form.uuid}&_t=${Date.now()}`
|
||
}
|
||
function onCaptchaInput(e: Event) {
|
||
const t = e.target as HTMLInputElement
|
||
form.captcha = t.value
|
||
}
|
||
refreshCaptcha()
|
||
|
||
function pickTheme(value: string) {
|
||
themeStore.pick(value)
|
||
}
|
||
|
||
async function onLogin() {
|
||
if (!form.username || !form.password) {
|
||
ElMessage.warning('请输入账号和密码')
|
||
return
|
||
}
|
||
loading.value = true
|
||
try {
|
||
const res = await axios.post(
|
||
'/api/auth/oauth/token',
|
||
qs.stringify({ ...form, grant_type: 'password' }),
|
||
{
|
||
headers: {
|
||
'Content-Type': 'application/x-www-form-urlencoded',
|
||
Authorization: 'Basic cmVucmVuaW86cmVucmVuaW8='
|
||
}
|
||
}
|
||
)
|
||
if (res.data?.code === 0 && res.data?.access_token) {
|
||
userStore.setAuth({
|
||
token: res.data.access_token,
|
||
username: form.username,
|
||
permissions: res.data.permissions || ['*:*:*']
|
||
})
|
||
ElMessage.success('登录成功')
|
||
const redirect = (route.query.redirect as string) || '/'
|
||
router.push(redirect)
|
||
} else {
|
||
ElMessage.error(res.data?.msg || '登录失败')
|
||
refreshCaptcha()
|
||
}
|
||
} catch (e: any) {
|
||
ElMessage.error(e?.response?.data?.msg || e?.message || '登录失败')
|
||
refreshCaptcha()
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div class="rr-login">
|
||
<div class="rr-login-stage">
|
||
<!-- 左 65%: 品牌区 -->
|
||
<div class="rr-login-left">
|
||
<div class="rr-login-brand">
|
||
<img class="rr-login-logo" :src="logoSrc" alt="Logo" />
|
||
<div class="rr-login-product-name">产业互联网平台运营后台</div>
|
||
</div>
|
||
|
||
<div class="rr-login-display">
|
||
<h1 class="rr-login-headline">
|
||
The operating<br>system for <span class="rr-login-accent">industry.</span>
|
||
</h1>
|
||
<p class="rr-login-sub">
|
||
询价 · 订单 · 供应商 · 履约一体化协同平台,为 B2B 团队而生。
|
||
</p>
|
||
</div>
|
||
|
||
<div class="rr-login-features">
|
||
<div class="rr-login-feature">
|
||
<span class="rr-login-ck"><el-icon><Check /></el-icon></span>
|
||
ISO 27001 + SOC 2 Type II
|
||
</div>
|
||
<div class="rr-login-feature">
|
||
<span class="rr-login-ck"><el-icon><Check /></el-icon></span>
|
||
等保三级 · 99.99% SLA
|
||
</div>
|
||
<div class="rr-login-feature">
|
||
<span class="rr-login-ck"><el-icon><Check /></el-icon></span>
|
||
10 万+ 企业运营团队在用
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右 35%: 登录卡 -->
|
||
<div class="rr-login-right">
|
||
<div class="rr-login-card">
|
||
<div class="rr-login-card-title">登录工作台</div>
|
||
<div class="rr-login-card-sub">输入账号继续</div>
|
||
|
||
<form class="rr-login-fields" @submit.prevent="onLogin">
|
||
<div class="rr-field">
|
||
<label class="rr-field-label">账号</label>
|
||
<div class="rr-input-wrap">
|
||
<el-icon class="rr-input-icon"><User /></el-icon>
|
||
<input v-model="form.username" class="rr-input" placeholder="请输入账号" autocomplete="username" />
|
||
</div>
|
||
</div>
|
||
|
||
<div class="rr-field">
|
||
<label class="rr-field-label">密码</label>
|
||
<div class="rr-input-wrap">
|
||
<el-icon class="rr-input-icon"><Lock /></el-icon>
|
||
<input v-model="form.password" :type="showPwd ? 'text' : 'password'" class="rr-input has-eye" placeholder="请输入密码" autocomplete="current-password" />
|
||
<el-icon class="rr-input-eye" @click="showPwd = !showPwd" :title="showPwd ? '隐藏密码' : '查看密码'"><View v-if="showPwd" /><Hide v-else /></el-icon>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="rr-field">
|
||
<label class="rr-field-label">验证码</label>
|
||
<div class="rr-captcha-row">
|
||
<div class="rr-input-wrap">
|
||
<el-icon class="rr-input-icon"><Picture /></el-icon>
|
||
<input v-model="form.captcha" class="rr-input" placeholder="请输入验证码" maxlength="20" @input="onCaptchaInput($event)" />
|
||
</div>
|
||
<div class="rr-captcha-box" @click="refreshCaptcha" title="点击刷新">
|
||
<img v-if="captchaUrl" :src="captchaUrl" class="rr-captcha-img" alt="验证码" />
|
||
<span v-else class="rr-captcha-loading">加载中</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<button type="submit" class="rr-btn-primary" :disabled="loading">
|
||
{{ loading ? '登录中…' : '登 录' }}
|
||
</button>
|
||
</form>
|
||
|
||
<div class="rr-login-theme-section">
|
||
<div class="rr-login-theme-label">选择主题</div>
|
||
<div class="rr-login-theme-grid">
|
||
<div
|
||
v-for="t in themePalette"
|
||
:key="t.value"
|
||
class="rr-login-theme-dot"
|
||
:class="{ active: themeStore.current === t.value }"
|
||
:style="{ background: t.color }"
|
||
:title="t.name"
|
||
@click="pickTheme(t.value)"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped lang="scss">
|
||
/**
|
||
* 设计稿精确数值(从 design.html Playwright 读出来):
|
||
* 舞台 1324×600 圆角 16px / 左 65% 右 35% / input 380×38 圆角 8px / 按钮 380×40 圆角 8px
|
||
* 输入背景 #F8FAFC 边框 #E2E8F0 圆角 8px / 验证码背景 #DBEAFE / 按钮阴影 0 1px 2px rgba(37,99,235,0.15)
|
||
*/
|
||
.rr-login {
|
||
width: 100vw;
|
||
height: 100vh;
|
||
background: linear-gradient(145deg, var(--login-grad-1) 0%, var(--login-grad-2) 50%, var(--login-grad-3) 100%);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin: 0;
|
||
overflow: hidden;
|
||
padding: 24px;
|
||
}
|
||
|
||
/* 舞台(套设计稿 .login-stage) — 背景透明,logo/表单直接渲染在 .rr-login 页面渐变上 */
|
||
.rr-login-stage {
|
||
width: 1324px;
|
||
height: 600px;
|
||
max-width: 100%;
|
||
max-height: 100%;
|
||
border-radius: 16px;
|
||
overflow: hidden;
|
||
display: flex;
|
||
position: relative;
|
||
|
||
& > * { position: relative; z-index: 1; }
|
||
}
|
||
|
||
/* 左 65% 品牌区(背景透明,让 logo/品牌文字直接渲染在 .rr-login 页面渐变上) */
|
||
.rr-login-left {
|
||
flex: 1.857;
|
||
padding: 48px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: space-between;
|
||
position: relative;
|
||
background: transparent;
|
||
color: var(--ink);
|
||
overflow: hidden;
|
||
|
||
/* 之前的 ::before 蓝紫光晕 + ::after 网格装饰, 在 left 透明后会让品牌区看起来像个发光方框, 故移除 */
|
||
}
|
||
.rr-login-brand { display: flex; align-items: center; gap: 10px; }
|
||
.rr-login-logo {
|
||
width: 48px; height: 48px;
|
||
object-fit: contain;
|
||
}
|
||
.rr-login-product-name { font-size: 16px; font-weight: 600; color: var(--ink); }
|
||
.rr-login-display { max-width: 460px; }
|
||
.rr-login-headline {
|
||
font-size: 42px; font-weight: 700; line-height: 1.1;
|
||
letter-spacing: -0.03em; margin-bottom: 16px; color: var(--ink);
|
||
}
|
||
.rr-login-accent { color: var(--brand); }
|
||
.rr-login-sub { color: var(--ink-2); font-size: 15px; line-height: 1.6; }
|
||
.rr-login-features { display: flex; flex-direction: column; gap: 10px; }
|
||
.rr-login-feature {
|
||
display: flex; align-items: center; gap: 10px;
|
||
font-size: 13.5px; color: var(--ink-2);
|
||
}
|
||
.rr-login-ck {
|
||
width: 20px; height: 20px;
|
||
background: var(--brand-soft);
|
||
color: var(--brand);
|
||
border-radius: 50%;
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-size: 12px;
|
||
}
|
||
|
||
/* 右 35% 登录卡(半透明白让 stage 渐变+网格透过来) */
|
||
.rr-login-right {
|
||
flex: 1;
|
||
background: rgba(255, 255, 255, 0.85);
|
||
|
||
// W5.x.x 暗主题适配:登录卡深色半透明,让 var(--ink) 系文字清晰可见
|
||
[data-theme="dark"] & {
|
||
background: rgba(30, 41, 59, 0.85);
|
||
}
|
||
backdrop-filter: blur(4px);
|
||
-webkit-backdrop-filter: blur(4px);
|
||
padding: 48px 40px;
|
||
display: flex; align-items: center; justify-content: center;
|
||
border-left: 1px solid var(--border);
|
||
}
|
||
.rr-login-card { width: 100%; max-width: 380px; }
|
||
|
||
/* 标题(套设计稿 .login-card-title 20px 700 + .login-card-sub 13px 400 灰) */
|
||
.rr-login-card-title {
|
||
font-size: 20px; font-weight: 700; color: var(--ink);
|
||
letter-spacing: -0.01em;
|
||
margin-bottom: 4px;
|
||
}
|
||
.rr-login-card-sub {
|
||
font-size: 13px; font-weight: 400; color: var(--ink-3);
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
/* 字段(套设计稿 .field + label 12px 500 + .input-wrap) */
|
||
.rr-login-fields { display: flex; flex-direction: column; gap: 14px; }
|
||
.rr-field { display: flex; flex-direction: column; gap: 6px; }
|
||
.rr-field-label {
|
||
font-size: 12px; font-weight: 500; color: var(--ink-2);
|
||
}
|
||
.rr-input-wrap {
|
||
position: relative;
|
||
width: 100%; height: 38px;
|
||
}
|
||
.rr-input-icon {
|
||
position: absolute;
|
||
left: 12px; top: 50%;
|
||
transform: translateY(-50%);
|
||
font-size: 14px;
|
||
color: var(--ink-3);
|
||
pointer-events: none;
|
||
}
|
||
.rr-input-eye {
|
||
position: absolute;
|
||
right: 10px; top: 50%;
|
||
transform: translateY(-50%);
|
||
font-size: 16px;
|
||
color: var(--ink-3);
|
||
cursor: pointer;
|
||
z-index: 1;
|
||
|
||
&:hover { color: var(--brand); }
|
||
}
|
||
.rr-input.has-eye {
|
||
padding-right: 36px;
|
||
}
|
||
.rr-input {
|
||
width: 100%; height: 38px;
|
||
padding: 0 12px 0 34px;
|
||
background: var(--bg-soft);
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
font-size: 14px;
|
||
color: var(--ink);
|
||
outline: none;
|
||
transition: all 0.15s;
|
||
|
||
&::placeholder { color: var(--ink-3); }
|
||
&:focus {
|
||
border-color: var(--brand);
|
||
background: var(--surface);
|
||
box-shadow: 0 0 0 3px var(--brand-soft);
|
||
}
|
||
}
|
||
|
||
/* 验证码行 */
|
||
.rr-captcha-row {
|
||
display: flex; gap: 8px; width: 100%;
|
||
.rr-input-wrap { flex: 1; }
|
||
}
|
||
.rr-captcha-box {
|
||
width: 130px; height: 40px;
|
||
background: var(--brand-soft);
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
display: flex; align-items: center; justify-content: center;
|
||
cursor: pointer;
|
||
user-select: none;
|
||
overflow: hidden;
|
||
flex-shrink: 0;
|
||
}
|
||
.rr-captcha-img {
|
||
width: 100%; height: 100%;
|
||
object-fit: contain;
|
||
display: block;
|
||
}
|
||
.rr-captcha-loading {
|
||
font-size: 12px; font-weight: 600;
|
||
color: var(--brand);
|
||
}
|
||
|
||
/* 主按钮(套设计稿 .btn-primary 380×40 圆角 8px 阴影) */
|
||
.rr-btn-primary {
|
||
width: 100%; height: 40px;
|
||
background: var(--brand);
|
||
color: white;
|
||
border: 0;
|
||
border-radius: 8px;
|
||
font-size: 14px; font-weight: 600;
|
||
letter-spacing: 0.04em;
|
||
cursor: pointer;
|
||
box-shadow: 0 1px 2px rgba(37, 99, 235, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.15);
|
||
transition: all 0.15s;
|
||
|
||
&:hover { background: var(--brand-hover); }
|
||
&:active { transform: translateY(1px); }
|
||
&:disabled { opacity: 0.7; cursor: not-allowed; }
|
||
}
|
||
|
||
/* 主题色板 */
|
||
.rr-login-theme-section {
|
||
margin-top: 20px;
|
||
padding-top: 20px;
|
||
border-top: 1px solid var(--border);
|
||
}
|
||
.rr-login-theme-label {
|
||
font-size: 11px; color: var(--ink-3);
|
||
text-transform: uppercase; letter-spacing: 0.08em;
|
||
margin-bottom: 10px;
|
||
text-align: center;
|
||
}
|
||
.rr-login-theme-grid { display: flex; gap: 10px; justify-content: center; }
|
||
.rr-login-theme-dot {
|
||
width: 24px; height: 24px;
|
||
border-radius: 50%;
|
||
cursor: pointer;
|
||
border: 2px solid transparent;
|
||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
|
||
transition: all 0.15s;
|
||
|
||
&:hover { transform: scale(1.1); }
|
||
&.active {
|
||
border-color: var(--ink);
|
||
box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px currentColor;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 992px) {
|
||
.rr-login-left { display: none; }
|
||
.rr-login-stage { width: 460px; height: auto; }
|
||
.rr-login-right { width: 100%; }
|
||
}
|
||
</style>
|