MianyVue/src/views/LoginView.vue
sladro 865091b912 refactor: 编码规范深度检查与CSS颜色变量系统优化
- 硬编码颜色修复: 替换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>
2025-09-16 11:45:43 +08:00

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>