创建项目

This commit is contained in:
Putoo
2026-04-21 16:54:18 +08:00
commit 6cdceccde7
59 changed files with 15320 additions and 0 deletions

View File

@@ -0,0 +1,469 @@
<template>
<div class="page-login">
<div class="login-container">
<div class="login-box">
<div class="login-header">
<h1 class="login-title">登录</h1>
<p class="login-subtitle">欢迎使用 Kx UI Framework</p>
</div>
<form class="login-form" @submit.prevent="handleLogin">
<!-- 用户名 -->
<div class="form-item">
<label class="form-label" for="username">用户名</label>
<input
id="username"
v-model="loginForm.username"
type="text"
class="form-input"
placeholder="请输入用户名"
@blur="validateField('username')"
/>
<span class="form-error" v-if="errors.username">{{ errors.username }}</span>
</div>
<!-- 密码 -->
<div class="form-item">
<label class="form-label" for="password">密码</label>
<input
id="password"
v-model="loginForm.password"
type="password"
class="form-input"
placeholder="请输入密码"
@blur="validateField('password')"
/>
<span class="form-error" v-if="errors.password">{{ errors.password }}</span>
</div>
<!-- 验证码 -->
<div class="form-item">
<label class="form-label" for="captcha">验证码</label>
<div class="captcha-wrapper">
<input
id="captcha"
v-model="loginForm.captcha"
type="text"
class="form-input captcha-input"
placeholder="请输入验证码"
@blur="validateField('captcha')"
/>
<div class="captcha-code" @click="refreshCaptcha">
{{ captchaCode }}
</div>
</div>
<span class="form-error" v-if="errors.captcha">{{ errors.captcha }}</span>
</div>
<!-- 记住密码 & 忘记密码 -->
<div class="form-options">
<label class="checkbox-label">
<input v-model="loginForm.remember" type="checkbox" />
<span>记住密码</span>
</label>
<a href="javascript:;" class="forgot-link">忘记密码</a>
</div>
<!-- 登录按钮 -->
<button type="submit" class="login-btn" :disabled="isLoading">
{{ isLoading ? '登录中...' : '登录' }}
</button>
<!-- 注册链接 -->
<div class="login-footer">
<span class="register-tip">还没有账号</span>
<a href="javascript:;" class="register-link">立即注册</a>
</div>
</form>
<!-- 提示信息 -->
<div class="login-tips" v-if="tips.show">
<p :class="['tip-text', tips.type]">{{ tips.message }}</p>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
// stores、extends、services 目录下的内容已自动导入
// types目录需要手动import type
import type { ILoginParams } from '~/types/api'
// 设置页面布局
definePageMeta({
layout: layout.empty
})
// Pinia状态仓库自动导入
const userStore = useUserStore()
const router = useRouter()
// 登录表单数据
const loginForm = reactive({
username: '',
password: '',
captcha: '',
remember: false
})
// 表单校验错误
const errors = reactive({
username: '',
password: '',
captcha: ''
})
// 加载状态
const isLoading = ref(false)
// 验证码
const captchaCode = ref('')
// 提示信息
const tips = reactive({
show: false,
message: '',
type: 'info'
})
// 生成随机验证码
const generateCaptcha = () => {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
let code = ''
for (let i = 0; i < 4; i++) {
code += chars.charAt(Math.floor(Math.random() * chars.length))
}
captchaCode.value = code
}
// 刷新验证码
const refreshCaptcha = () => {
generateCaptcha()
}
// 校验单个字段
const validateField = (field: keyof typeof errors) => {
errors[field] = ''
switch (field) {
case 'username':
if (!loginForm.username) {
errors.username = '请输入用户名'
} else if (!ValidateEXTEND.minLength(loginForm.username, 3)) {
errors.username = '用户名至少3个字符'
}
break
case 'password':
if (!loginForm.password) {
errors.password = '请输入密码'
} else if (!ValidateEXTEND.minLength(loginForm.password, 6)) {
errors.password = '密码至少6个字符'
}
break
case 'captcha':
if (!loginForm.captcha) {
errors.captcha = '请输入验证码'
} else if (loginForm.captcha.toUpperCase() !== captchaCode.value.toUpperCase()) {
errors.captcha = '验证码错误'
}
break
}
return !errors[field]
}
// 校验整个表单
const validateForm = () => {
const usernameValid = validateField('username')
const passwordValid = validateField('password')
const captchaValid = validateField('captcha')
return usernameValid && passwordValid && captchaValid
}
// 显示提示
const showTips = (message: string, type: 'success' | 'error' | 'info' = 'info') => {
tips.message = message
tips.type = type
tips.show = true
setTimeout(() => {
tips.show = false
}, 3000)
}
// 处理登录
const handleLogin = async () => {
// 校验表单
if (!validateForm()) {
return
}
// 开始加载
isLoading.value = true
userStore.setLoginLoading(true)
try {
// 模拟登录实际项目中调用API
// const userService = new UserSERVICE()
// const result = await userService.login(loginForm as ILoginParams)
// 模拟登录成功
await new Promise((resolve) => setTimeout(resolve, 1000))
// 设置用户信息
userStore.setUserInfo(
{
id: 1001,
username: loginForm.username,
nickname: loginForm.username,
role: 'admin'
},
'demo-token-' + Date.now()
)
showTips('登录成功!', 'success')
// 跳转到首页
setTimeout(() => {
router.push('/home')
}, 1000)
} catch (error: any) {
console.error('登录失败:', error)
showTips(error.message || '登录失败,请稍后重试', 'error')
// 刷新验证码
refreshCaptcha()
loginForm.captcha = ''
} finally {
isLoading.value = false
userStore.setLoginLoading(false)
}
}
// 初始化
onMounted(() => {
// 如果已登录,跳转到首页
if (userStore.isLogin) {
router.push('/home')
return
}
// 生成验证码
generateCaptcha()
})
</script>
<style scoped>
.page-login {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.login-container {
width: 100%;
max-width: 400px;
padding: 20px;
}
.login-box {
background-color: var(--bg-color);
border-radius: 12px;
padding: 40px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
}
.login-header {
text-align: center;
margin-bottom: 30px;
}
.login-title {
font-size: 28px;
font-weight: 600;
color: var(--text-color);
margin-bottom: 10px;
}
.login-subtitle {
font-size: 14px;
color: var(--text-color-secondary);
}
.login-form {
width: 100%;
}
.form-item {
margin-bottom: 20px;
}
.form-label {
display: block;
font-size: 14px;
color: var(--text-color);
margin-bottom: 8px;
}
.form-input {
width: 100%;
height: 40px;
padding: 0 12px;
font-size: 14px;
color: var(--text-color);
background-color: var(--bg-color-page);
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
transition: border-color 0.3s;
}
.form-input:focus {
border-color: var(--primary-color);
outline: none;
}
.form-input::placeholder {
color: var(--text-color-placeholder);
}
.form-error {
display: block;
font-size: 12px;
color: var(--danger-color);
margin-top: 5px;
}
.captcha-wrapper {
display: flex;
gap: 10px;
}
.captcha-input {
flex: 1;
}
.captcha-code {
width: 100px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--bg-color-page);
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
font-size: 18px;
font-weight: 600;
letter-spacing: 4px;
color: var(--primary-color);
cursor: pointer;
user-select: none;
}
.captcha-code:hover {
background-color: var(--border-color-lighter);
}
.form-options {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
color: var(--text-color-secondary);
cursor: pointer;
}
.checkbox-label input[type='checkbox'] {
width: 16px;
height: 16px;
cursor: pointer;
}
.forgot-link {
font-size: 14px;
color: var(--primary-color);
text-decoration: none;
}
.forgot-link:hover {
text-decoration: underline;
}
.login-btn {
width: 100%;
height: 44px;
font-size: 16px;
font-weight: 500;
color: #fff;
background-color: var(--primary-color);
border: none;
border-radius: var(--border-radius);
cursor: pointer;
transition: background-color 0.3s;
}
.login-btn:hover:not(:disabled) {
background-color: #66b1ff;
}
.login-btn:disabled {
background-color: var(--border-color);
cursor: not-allowed;
}
.login-footer {
text-align: center;
margin-top: 20px;
}
.register-tip {
font-size: 14px;
color: var(--text-color-secondary);
}
.register-link {
font-size: 14px;
color: var(--primary-color);
text-decoration: none;
margin-left: 5px;
}
.register-link:hover {
text-decoration: underline;
}
.login-tips {
margin-top: 20px;
text-align: center;
}
.tip-text {
font-size: 14px;
padding: 10px;
border-radius: var(--border-radius);
}
.tip-text.success {
color: var(--success-color);
background-color: rgba(103, 194, 58, 0.1);
}
.tip-text.error {
color: var(--danger-color);
background-color: rgba(245, 108, 108, 0.1);
}
.tip-text.info {
color: var(--info-color);
background-color: rgba(144, 147, 153, 0.1);
}
</style>