- 硬编码颜色修复: 替换30+个硬编码rgba值为CSS变量 - 颜色变量优化: 从16个减少到10个变量 (减少37.5%) - 代码清理: 删除注释代码残留和未使用变量 - 系统优化: 建立清晰的透明度层级 (0.05/0.1/0.15/0.2/0.3/0.4) 技术改进: * 删除2个未使用的error颜色变量 * 合并2个重复的shadow变量指向已有primary变量 * 合并2个相近的白色透明度变量 * 消除硬编码,提升主题系统可维护性 影响文件: - theme.css: 重构颜色变量系统 - LoginView.vue: 更新硬编码颜色和清理注释 - DashboardView.vue: 更新硬编码颜色和清理注释 - AppHeader.vue: 更新硬编码颜色和清理注释 - CLAUDE.md: 更新项目文档记录 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
557 lines
12 KiB
Vue
557 lines
12 KiB
Vue
<template>
|
|
<div class="login-container">
|
|
<div class="login-background">
|
|
<!-- 背景装饰元素 -->
|
|
<div class="bg-decoration">
|
|
<div class="circle circle-1"></div>
|
|
<div class="circle circle-2"></div>
|
|
<div class="circle circle-3"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="login-content">
|
|
<div class="login-card">
|
|
<!-- 品牌标识 -->
|
|
<div class="brand-section">
|
|
<div class="brand-logo">
|
|
<i class="fas fa-cube"></i>
|
|
</div>
|
|
<h1 class="brand-title">工业模型软件管理统一平台</h1>
|
|
<p class="brand-subtitle">Industrial CAD Management Platform</p>
|
|
</div>
|
|
|
|
<!-- 登录表单 -->
|
|
<form @submit.prevent="handleLogin" class="login-form">
|
|
<h2 class="form-title">系统登录</h2>
|
|
|
|
<!-- 错误提示 -->
|
|
<div v-if="loginError" class="error-message">
|
|
<i class="fas fa-exclamation-triangle"></i>
|
|
<span>{{ loginError }}</span>
|
|
</div>
|
|
|
|
<!-- 用户名输入 -->
|
|
<div class="form-group">
|
|
<label for="username" class="form-label">
|
|
<i class="fas fa-user"></i>
|
|
用户名
|
|
</label>
|
|
<input
|
|
id="username"
|
|
v-model="formData.username"
|
|
type="text"
|
|
class="form-input"
|
|
:class="{ 'error': errors.username }"
|
|
placeholder="请输入用户名"
|
|
autocomplete="username"
|
|
@blur="validateField('username')"
|
|
@input="clearFieldError('username')"
|
|
/>
|
|
<div v-if="errors.username" class="field-error">
|
|
{{ errors.username }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 密码输入 -->
|
|
<div class="form-group">
|
|
<label for="password" class="form-label">
|
|
<i class="fas fa-lock"></i>
|
|
密码
|
|
</label>
|
|
<div class="password-input-wrapper">
|
|
<input
|
|
id="password"
|
|
v-model="formData.password"
|
|
:type="showPassword ? 'text' : 'password'"
|
|
class="form-input"
|
|
:class="{ 'error': errors.password }"
|
|
placeholder="请输入密码"
|
|
autocomplete="current-password"
|
|
@blur="validateField('password')"
|
|
@input="clearFieldError('password')"
|
|
/>
|
|
<button
|
|
type="button"
|
|
class="password-toggle"
|
|
@click="togglePassword"
|
|
:title="showPassword ? '隐藏密码' : '显示密码'"
|
|
>
|
|
<i :class="showPassword ? 'fas fa-eye-slash' : 'fas fa-eye'"></i>
|
|
</button>
|
|
</div>
|
|
<div v-if="errors.password" class="field-error">
|
|
{{ errors.password }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 记住我 -->
|
|
<div class="form-options">
|
|
<label class="checkbox-wrapper">
|
|
<input
|
|
v-model="formData.rememberMe"
|
|
type="checkbox"
|
|
class="checkbox-input"
|
|
/>
|
|
<span class="checkbox-custom"></span>
|
|
<span class="checkbox-label">记住我</span>
|
|
</label>
|
|
</div>
|
|
|
|
<!-- 登录按钮 -->
|
|
<button
|
|
type="submit"
|
|
class="login-button"
|
|
:disabled="isLoading || !isFormValid"
|
|
:class="{ 'loading': isLoading }"
|
|
>
|
|
<span v-if="!isLoading">
|
|
<i class="fas fa-sign-in-alt"></i>
|
|
登录
|
|
</span>
|
|
<span v-else>
|
|
<i class="fas fa-spinner fa-spin"></i>
|
|
登录中...
|
|
</span>
|
|
</button>
|
|
|
|
<!-- 帮助信息 -->
|
|
<div class="help-text">
|
|
<p>联系系统管理员获取账号信息</p>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, onMounted } from 'vue'
|
|
import { useRouter } from 'vue-router'
|
|
import { useAuthStore } from '@/stores/auth'
|
|
|
|
const router = useRouter()
|
|
const authStore = useAuthStore()
|
|
|
|
// 表单数据
|
|
const formData = ref({
|
|
username: '',
|
|
password: '',
|
|
rememberMe: false
|
|
})
|
|
|
|
// 表单验证错误
|
|
const errors = ref({
|
|
username: '',
|
|
password: ''
|
|
})
|
|
|
|
// UI状态
|
|
const showPassword = ref(false)
|
|
|
|
// 计算属性
|
|
const isLoading = computed(() => authStore.isLoading)
|
|
const loginError = computed(() => authStore.loginError)
|
|
|
|
const isFormValid = computed(() => {
|
|
return formData.value.username.trim() &&
|
|
formData.value.password.trim() &&
|
|
!errors.value.username &&
|
|
!errors.value.password
|
|
})
|
|
|
|
// 方法
|
|
const togglePassword = () => {
|
|
showPassword.value = !showPassword.value
|
|
}
|
|
|
|
const validateField = (field) => {
|
|
switch (field) {
|
|
case 'username':
|
|
if (!formData.value.username.trim()) {
|
|
errors.value.username = '请输入用户名'
|
|
} else if (formData.value.username.length < 2) {
|
|
errors.value.username = '用户名至少2个字符'
|
|
} else {
|
|
errors.value.username = ''
|
|
}
|
|
break
|
|
|
|
case 'password':
|
|
if (!formData.value.password.trim()) {
|
|
errors.value.password = '请输入密码'
|
|
} else if (formData.value.password.length < 6) {
|
|
errors.value.password = '密码至少6个字符'
|
|
} else {
|
|
errors.value.password = ''
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
const clearFieldError = (field) => {
|
|
if (errors.value[field]) {
|
|
errors.value[field] = ''
|
|
}
|
|
}
|
|
|
|
const validateForm = () => {
|
|
validateField('username')
|
|
validateField('password')
|
|
return !errors.value.username && !errors.value.password
|
|
}
|
|
|
|
const handleLogin = async () => {
|
|
// 清除之前的错误
|
|
authStore.clearError()
|
|
|
|
// 验证表单
|
|
if (!validateForm()) {
|
|
return
|
|
}
|
|
|
|
// 执行登录
|
|
const result = await authStore.login({
|
|
username: formData.value.username.trim(),
|
|
password: formData.value.password
|
|
})
|
|
|
|
if (result.success) {
|
|
// 登录成功,跳转到主页面
|
|
router.push('/')
|
|
}
|
|
}
|
|
|
|
// 组件挂载时恢复记住的用户名
|
|
onMounted(() => {
|
|
const rememberedUsername = localStorage.getItem('rememberedUsername')
|
|
if (rememberedUsername) {
|
|
formData.value.username = rememberedUsername
|
|
formData.value.rememberMe = true
|
|
}
|
|
})
|
|
|
|
</script>
|
|
|
|
<style scoped>
|
|
.login-container {
|
|
height: 100vh;
|
|
width: 100vw;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
position: relative;
|
|
background: var(--color-bg-gradient-main);
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
overflow: hidden;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
.login-background {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.bg-decoration {
|
|
position: relative;
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
.circle {
|
|
position: absolute;
|
|
border-radius: 50%;
|
|
background: var(--color-gradient-brand-bg);
|
|
animation: float 6s ease-in-out infinite;
|
|
}
|
|
|
|
.circle-1 {
|
|
width: 200px;
|
|
height: 200px;
|
|
top: 10%;
|
|
left: 10%;
|
|
animation-delay: 0s;
|
|
}
|
|
|
|
.circle-2 {
|
|
width: 150px;
|
|
height: 150px;
|
|
top: 60%;
|
|
right: 15%;
|
|
animation-delay: 2s;
|
|
}
|
|
|
|
.circle-3 {
|
|
width: 100px;
|
|
height: 100px;
|
|
bottom: 20%;
|
|
left: 20%;
|
|
animation-delay: 4s;
|
|
}
|
|
|
|
@keyframes float {
|
|
0%, 100% { transform: translateY(0px) rotate(0deg); }
|
|
50% { transform: translateY(-20px) rotate(180deg); }
|
|
}
|
|
|
|
.login-content {
|
|
position: relative;
|
|
z-index: 1;
|
|
width: 100%;
|
|
max-width: 420px;
|
|
padding: 20px;
|
|
}
|
|
|
|
.login-card {
|
|
background: var(--color-bg-dark-95);
|
|
border-radius: 16px;
|
|
padding: 40px;
|
|
box-shadow: 0 20px 40px var(--color-shadow-black-3);
|
|
backdrop-filter: blur(10px);
|
|
border: 1px solid var(--color-white-rgb-1);
|
|
}
|
|
|
|
.brand-section {
|
|
text-align: center;
|
|
margin-bottom: 40px;
|
|
}
|
|
|
|
.brand-logo {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 60px;
|
|
height: 60px;
|
|
background: var(--color-primary-gradient);
|
|
border-radius: 16px;
|
|
margin-bottom: 16px;
|
|
box-shadow: 0 4px 15px var(--color-primary-rgb-3);
|
|
}
|
|
|
|
.brand-logo i {
|
|
font-size: 24px;
|
|
color: var(--color-text-primary);
|
|
}
|
|
|
|
.brand-title {
|
|
font-size: 20px;
|
|
font-weight: 600;
|
|
color: var(--color-text-primary);
|
|
margin: 0 0 8px 0;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.brand-subtitle {
|
|
font-size: 14px;
|
|
color: var(--color-text-secondary);
|
|
margin: 0;
|
|
font-weight: 400;
|
|
}
|
|
|
|
.login-form {
|
|
width: 100%;
|
|
}
|
|
|
|
.form-title {
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
color: var(--color-text-primary);
|
|
text-align: center;
|
|
margin: 0 0 30px 0;
|
|
}
|
|
|
|
.error-message {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 12px 16px;
|
|
background: var(--color-error-rgb-1);
|
|
border: 1px solid var(--color-error-rgb-3);
|
|
border-radius: 8px;
|
|
color: var(--color-text-error);
|
|
font-size: 14px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.form-group {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.form-label {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
color: var(--color-text-secondary);
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.form-input {
|
|
width: 100%;
|
|
padding: 12px 16px;
|
|
background: var(--color-white-rgb-05);
|
|
border: 1px solid var(--color-white-rgb-1);
|
|
border-radius: 8px;
|
|
color: var(--color-text-primary);
|
|
font-size: 14px;
|
|
transition: all 0.3s ease;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.form-input:focus {
|
|
outline: none;
|
|
border-color: var(--color-border-active);
|
|
box-shadow: 0 0 0 3px var(--color-primary-rgb);
|
|
background: var(--color-white-rgb-1);
|
|
}
|
|
|
|
.form-input.error {
|
|
border-color: var(--color-border-error);
|
|
}
|
|
|
|
.form-input::placeholder {
|
|
color: var(--color-text-tertiary);
|
|
}
|
|
|
|
.password-input-wrapper {
|
|
position: relative;
|
|
}
|
|
|
|
.password-toggle {
|
|
position: absolute;
|
|
right: 12px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
background: none;
|
|
border: none;
|
|
color: var(--color-text-secondary);
|
|
cursor: pointer;
|
|
padding: 4px;
|
|
transition: color 0.3s ease;
|
|
}
|
|
|
|
.password-toggle:hover {
|
|
color: var(--color-text-primary);
|
|
}
|
|
|
|
.field-error {
|
|
font-size: 12px;
|
|
color: var(--color-text-error);
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.form-options {
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.checkbox-wrapper {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
cursor: pointer;
|
|
user-select: none;
|
|
}
|
|
|
|
.checkbox-input {
|
|
display: none;
|
|
}
|
|
|
|
.checkbox-custom {
|
|
width: 16px;
|
|
height: 16px;
|
|
border: 1px solid var(--color-white-rgb-3);
|
|
border-radius: 4px;
|
|
background: var(--color-white-rgb-05);
|
|
position: relative;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.checkbox-input:checked + .checkbox-custom {
|
|
background: var(--color-primary);
|
|
border-color: var(--color-border-active);
|
|
}
|
|
|
|
.checkbox-input:checked + .checkbox-custom::after {
|
|
content: '✓';
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
color: var(--color-text-primary);
|
|
font-size: 12px;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.checkbox-label {
|
|
font-size: 14px;
|
|
color: var(--color-text-secondary);
|
|
}
|
|
|
|
.login-button {
|
|
width: 100%;
|
|
padding: 14px;
|
|
background: var(--color-primary-gradient);
|
|
border: none;
|
|
border-radius: 8px;
|
|
color: var(--color-text-primary);
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
box-shadow: 0 4px 15px var(--color-primary-rgb-3);
|
|
}
|
|
|
|
.login-button:hover:not(:disabled) {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 6px 20px var(--color-primary-rgb-4);
|
|
}
|
|
|
|
.login-button:active:not(:disabled) {
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.login-button:disabled {
|
|
opacity: 0.7;
|
|
cursor: not-allowed;
|
|
transform: none;
|
|
}
|
|
|
|
.login-button.loading {
|
|
background: var(--color-primary-gradient-hover);
|
|
}
|
|
|
|
.help-text {
|
|
text-align: center;
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.help-text p {
|
|
font-size: 13px;
|
|
color: var(--color-text-tertiary);
|
|
margin: 0;
|
|
}
|
|
|
|
/* 响应式设计 */
|
|
@media (max-width: 480px) {
|
|
.login-content {
|
|
padding: 16px;
|
|
}
|
|
|
|
.login-card {
|
|
padding: 30px 24px;
|
|
}
|
|
|
|
.brand-title {
|
|
font-size: 18px;
|
|
}
|
|
|
|
.circle {
|
|
display: none;
|
|
}
|
|
}
|
|
</style> |